├── .gitignore ├── webpack.config.dev.cjs ├── .esdoc.json ├── babel.config.json ├── webpack.config.dist.cjs ├── index.js ├── webpack.config.default.cjs ├── LICENSE-MIT ├── src ├── alias.js ├── user.js ├── usertypes.js ├── websocket.js ├── identity.js ├── tls.js ├── bufferview.js ├── highlight.js ├── message.js ├── ignore.js ├── buffer.js ├── network.js ├── request.js └── libquassel.js ├── .eslintrc.cjs ├── package.json ├── README.md └── test ├── test.js └── manual.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .c9revisions 3 | .settings 4 | .c9 5 | /dist/ 6 | /.idea/ 7 | /doc/ 8 | -------------------------------------------------------------------------------- /webpack.config.dev.cjs: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.config.default.cjs'); 2 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 3 | 4 | module.exports = Object.assign({}, config, { 5 | mode: 'development', 6 | plugins: [ 7 | new NodePolyfillPlugin() 8 | ], 9 | }); 10 | -------------------------------------------------------------------------------- /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./doc", 4 | "plugins": [ 5 | { 6 | "name": "esdoc-standard-plugin" 7 | }, 8 | { 9 | "name": "esdoc-ecmascript-proposal-plugin", 10 | "option": { 11 | "classProperties": true, 12 | "objectRestSpread": true, 13 | "decorators": true 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "include": [ 7 | "@babel/plugin-transform-class-properties", 8 | "@babel/plugin-proposal-class-properties" 9 | ] 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | [ 15 | "@babel/plugin-proposal-decorators", 16 | { 17 | "version": "legacy" 18 | } 19 | ], 20 | "@babel/plugin-transform-runtime", 21 | "@babel/plugin-syntax-import-assertions" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.dist.cjs: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.config.default.cjs'); 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 5 | 6 | module.exports = Object.assign({}, config, { 7 | mode: 'production', 8 | plugins: [ 9 | new CleanWebpackPlugin(), 10 | new NodePolyfillPlugin() 11 | ], 12 | optimization: { 13 | minimize: true, 14 | minimizer: [ new TerserPlugin() ], 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Features, Client } = require('./src/libquassel'); 2 | 3 | module.exports = { 4 | alias: require('./src/alias'), 5 | buffer: require('./src/buffer'), 6 | bufferview: require('./src/bufferview'), 7 | identity: require('./src/identity'), 8 | ignore: require('./src/ignore'), 9 | highlight: require('./src/highlight'), 10 | message: require('./src/message'), 11 | network: require('./src/network'), 12 | request: require('./src/request'), 13 | user: require('./src/user'), 14 | WebSocketStream: require('./src/websocket').default, 15 | debug: require('debug'), 16 | Features, 17 | Client 18 | }; 19 | -------------------------------------------------------------------------------- /webpack.config.default.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, 'index.js'), 5 | output: { 6 | path: path.resolve(__dirname, 'dist'), 7 | filename: 'libquassel.js', 8 | library: 'libquassel', 9 | libraryTarget: 'var' 10 | }, 11 | resolve: { 12 | alias: { 13 | tls: path.resolve(__dirname, 'src/tls'), 14 | }, 15 | symlinks: false 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(m?|c?)js$/, 21 | exclude: /(node_modules|bower_components)/, 22 | use: { 23 | loader: 'babel-loader', 24 | options: { 25 | presets: [ 26 | [ 27 | '@babel/preset-env', 28 | { 29 | targets: 'defaults', 30 | include: [ 31 | '@babel/plugin-transform-class-properties' 32 | ] 33 | } 34 | ] 35 | ] 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Joël Charles 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/alias.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | /** 10 | * Converts core object to an Array of {@link AliasItem} 11 | * @param {Object} data 12 | * @returns {AliasItem[]} 13 | */ 14 | export function toArray(data) { 15 | let i = 0, ret = Array(data.Aliases.names.length); 16 | for (; i 5 | * @property {INT} this 6 | */ 7 | qtypes.QUserType.register('NetworkId', qtypes.Types.INT); 8 | 9 | /** 10 | * @typedef {QUserType} UserType 11 | * @property {INT} this 12 | */ 13 | qtypes.QUserType.register('IdentityId', qtypes.Types.INT); 14 | 15 | /** 16 | * @typedef {QUserType} UserType 17 | * @property {INT} this 18 | */ 19 | qtypes.QUserType.register('BufferId', qtypes.Types.INT); 20 | 21 | /** 22 | * @typedef {QUserType} UserType 23 | * @property {INT} this 24 | */ 25 | qtypes.QUserType.register('MsgId', qtypes.Types.INT); 26 | 27 | /** 28 | * @typedef {QUserType} UserType 29 | * @property {MAP} this 30 | */ 31 | qtypes.QUserType.register('Identity', qtypes.Types.MAP); 32 | 33 | /** 34 | * @typedef {QUserType} UserType 35 | * @property {MAP} this 36 | */ 37 | qtypes.QUserType.register('NetworkInfo', qtypes.Types.MAP); 38 | 39 | /** 40 | * @typedef {QUserType} UserType 41 | * @property {MAP} this 42 | */ 43 | qtypes.QUserType.register('Network::Server', qtypes.Types.MAP); 44 | 45 | /** 46 | * @typedef {QUserType} UserType 47 | * @property {INT} this 48 | */ 49 | qtypes.QUserType.register('NetworkId', qtypes.Types.INT); 50 | 51 | /** 52 | * @typedef {QUserType} UserType 53 | * @property {INT} id 54 | * @property {INT} network 55 | * @property {SHORT} type 56 | * @property {UINT} group 57 | * @property {BYTEARRAY} name 58 | */ 59 | qtypes.QUserType.register('BufferInfo', [ 60 | { id: qtypes.Types.INT }, 61 | { network: qtypes.Types.INT }, 62 | { type: qtypes.Types.SHORT }, 63 | { group: qtypes.Types.UINT }, 64 | { name: qtypes.Types.BYTEARRAY } 65 | ]); 66 | 67 | /** 68 | * @typedef {QUserType} UserType 69 | * @property {INT} id 70 | * @property {UINT} timestamp 71 | * @property {UINT} type 72 | * @property {BOOL} flags 73 | * @property {UserType} bufferInfo 74 | * @property {BYTEARRAY} sender 75 | * @property {BYTEARRAY} content 76 | */ 77 | qtypes.QUserType.register('Message', [ 78 | { id: qtypes.Types.INT }, 79 | { timestamp: qtypes.Types.UINT }, 80 | { type: qtypes.Types.UINT }, 81 | { flags: qtypes.Types.BOOL }, 82 | { bufferInfo: 'BufferInfo' }, 83 | { sender: qtypes.Types.BYTEARRAY }, 84 | { content: qtypes.Types.BYTEARRAY } 85 | ]); 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libquassel", 3 | "description": "Javascript lib to connect and interact with Quassel IRC core", 4 | "version": "4.0.0", 5 | "homepage": "https://github.com/magne4000/node-libquassel", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Joël Charles", 9 | "email": "joel.charles91@gmail.com" 10 | }, 11 | "files": [ 12 | "LICENSE-MIT", 13 | "README.md", 14 | "src", 15 | "dist", 16 | "index.js" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/magne4000/node-libquassel.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/magne4000/node-libquassel/issues" 24 | }, 25 | "licenses": [ 26 | { 27 | "type": "MIT", 28 | "url": "https://github.com/magne4000/node-libquassel/blob/master/LICENSE-MIT" 29 | } 30 | ], 31 | "scripts": { 32 | "test": "tape -r @babel/register test/test.js", 33 | "test-manual": "babel-node test/manual.js", 34 | "build": "webpack --config webpack.config.dist.cjs", 35 | "build-dev": "webpack --config webpack.config.dev.cjs", 36 | "watch": "webpack --config webpack.config.dev.cjs --watch --progress", 37 | "doc": "esdoc", 38 | "eslint": "eslint src", 39 | "prepublishOnly": "npm run eslint && npm run build" 40 | }, 41 | "main": "index.js", 42 | "engines": { 43 | "node": ">= 16" 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "^7.23.0", 47 | "@babel/core": "^7.23.2", 48 | "@babel/eslint-parser": "^7.22.15", 49 | "@babel/node": "^7.22.19", 50 | "@babel/plugin-proposal-class-properties": "^7.18.6", 51 | "@babel/plugin-proposal-decorators": "^7.23.2", 52 | "@babel/plugin-proposal-import-attributes-to-assertions": "^7.22.5", 53 | "@babel/plugin-syntax-import-assertions": "^7.22.5", 54 | "@babel/plugin-transform-runtime": "^7.23.2", 55 | "@babel/preset-env": "^7.23.2", 56 | "@babel/register": "^7.22.15", 57 | "ansi-styles": "^6.2.1", 58 | "babel-loader": "^9.1.3", 59 | "babel-preset-env": "^1.7.0", 60 | "blob-to-buffer": "^1.2.9", 61 | "clean-webpack-plugin": "^4.0.0", 62 | "esdoc": "^1.1.0", 63 | "esdoc-ecmascript-proposal-plugin": "^1.0.0", 64 | "esdoc-standard-plugin": "^1.0.0", 65 | "eslint": "^8.52.0", 66 | "eslint-plugin-import": "^2.29.0", 67 | "inquirer": "^9.2.11", 68 | "node-polyfill-webpack-plugin": "^2.0.1", 69 | "node-stdlib-browser": "^1.2.0", 70 | "tape": "^5.7.2", 71 | "terser-webpack-plugin": "^5.3.9", 72 | "uglify-js": "^3.17.4", 73 | "webpack": "^5.89.0", 74 | "webpack-cli": "^5.1.4" 75 | }, 76 | "keywords": [ 77 | "quassel", 78 | "libquassel" 79 | ], 80 | "dependencies": { 81 | "debug": "^4.3.4", 82 | "node-forge": "^1.3.1", 83 | "qtdatastream": "^1.1.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/websocket.js: -------------------------------------------------------------------------------- 1 | import { Duplex } from 'stream'; 2 | import toBuffer from 'blob-to-buffer'; 3 | 4 | const ErrorCodes = { 5 | 1001: 'The endpoint is going away, either because of a server failure or because the browser is navigating away from the page that opened the connection.', 6 | 1002: 'The endpoint is terminating the connection due to a protocol error.', 7 | 1003: 'The connection is being terminated because the endpoint received data of a type it cannot accept (for example, a text-only endpoint received binary data).', 8 | 1005: 'No status code was provided even though one was expected.', 9 | 1006: 'The connection was closed abnormally (that is, with no close frame being sent) when a status code was expected.', 10 | 1007: 'The endpoint is terminating the connection because a message was received that contained inconsistent data (e.g., non-UTF-8 data within a text message).', 11 | 1008: 'The endpoint is terminating the connection because it received a message that violates its policy. This is a generic status code, used when codes 1003 and 1009 are not suitable.', 12 | 1009: 'The endpoint is terminating the connection because a data frame was received that is too large.', 13 | 1010: 'The client is terminating the connection because it expected the server to negotiate one or more extension, but the server didn\'t.', 14 | 1011: 'The server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.', 15 | 1012: 'The server is terminating the connection because it is restarting.', 16 | 1013: 'The server is terminating the connection due to a temporary condition, e.g. it is overloaded and is casting off some of its clients', 17 | 1015: 'The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can\'t be verified).' 18 | }; 19 | 20 | export default class WebSocketStream extends Duplex { 21 | constructor(target, protocols) { 22 | super(); 23 | if (typeof target === 'object') { 24 | this.socket = target; 25 | } else { 26 | this.socket = new WebSocket(target, protocols); 27 | } 28 | this.socket.onopen = () => this.emit('connected'); 29 | this.socket.onclose = (event) => { 30 | if (event.code > 1000) { 31 | console.error(event); 32 | if (event.code in ErrorCodes) { 33 | this.emit('error', new Error(ErrorCodes[event.code])); 34 | } else { 35 | this.emit('error', new Error('Unknown WebSocket error')); 36 | } 37 | } 38 | this.emit('end'); 39 | }; 40 | this.socket.onerror = _err => {}; // Handled by onclose 41 | this.socket.onmessage = (event) => { 42 | toBuffer(event.data, (err, buffer) => { 43 | if (err) throw err; 44 | this.push(buffer); 45 | }); 46 | }; 47 | } 48 | 49 | _read(_size) {} 50 | 51 | _write(chunk, encoding, callback) { 52 | if (this.socket.readyState === 1) { // open 53 | this.socket.send(chunk); 54 | callback(); 55 | } else if (this.socket.readyState === 0) { // connecting 56 | this.once('connected', () => this.write(chunk, encoding, () => {})); 57 | callback(); 58 | } else { // closing or close 59 | callback('Attempt to write on a closed websocket'); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/identity.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | import { serialization, types as qtypes } from 'qtdatastream'; 10 | 11 | const { Serializable, serialize } = serialization; 12 | 13 | /** 14 | * Quassel Identity 15 | * @implements {Serializable} 16 | */ 17 | @Serializable() 18 | export default class Identity { 19 | @serialize(qtypes.QBool) 20 | autoAwayEnabled = false; 21 | 22 | @serialize(qtypes.QString) 23 | autoAwayReason = 'Not here. No, really. not here!'; 24 | 25 | @serialize(qtypes.QBool) 26 | autoAwayReasonEnabled = false; 27 | 28 | @serialize(qtypes.QUInt) 29 | autoAwayTime = 10; 30 | 31 | @serialize(qtypes.QString) 32 | awayNick = ''; 33 | 34 | @serialize(qtypes.QBool) 35 | awayNickEnabled = false; 36 | 37 | @serialize(qtypes.QString) 38 | awayReason = 'Gone fishing.'; 39 | 40 | @serialize(qtypes.QBool) 41 | awayReasonEnabled = true; 42 | 43 | @serialize(qtypes.QBool) 44 | detachAwayEnabled = false; 45 | 46 | @serialize(qtypes.QString) 47 | detachAwayReason = 'All Quassel clients vanished from the face of the earth...'; 48 | 49 | @serialize(qtypes.QBool) 50 | detachAwayReasonEnabled = false; 51 | 52 | @serialize(qtypes.QString) 53 | ident = 'quassel'; 54 | 55 | @serialize(qtypes.QUserType.get('IdentityId')) 56 | identityId = -1; 57 | 58 | /** @type {string} */ 59 | @serialize(qtypes.QString) 60 | identityName; 61 | 62 | /** @type {string} */ 63 | @serialize(qtypes.QString) 64 | realName; 65 | 66 | /** @type {string[]} */ 67 | @serialize(qtypes.QList) 68 | get nicks() { 69 | return this._nicks; 70 | } 71 | 72 | /** @type {string[]} */ 73 | set nicks(value) { 74 | this._nicks = value; 75 | this.nickRegexes = value.map(nick => nick.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')); 76 | } 77 | 78 | /** @type {number} */ 79 | set id(value) { 80 | this.identityId = value; 81 | } 82 | 83 | /** @type {number} */ 84 | get id() { 85 | return this.identityId; 86 | } 87 | 88 | @serialize(qtypes.QString) 89 | kickReason = 'Kindergarten is elsewhere!'; 90 | 91 | @serialize(qtypes.QString) 92 | partReason = 'http://quassel-irc.org - Chat comfortably. Anywhere.'; 93 | 94 | @serialize(qtypes.QString) 95 | quitReason = 'http://quassel-irc.org - Chat comfortably. Anywhere.'; 96 | 97 | constructor(data) { 98 | this._nicks = []; 99 | this.nickRegexes = []; 100 | if (data) { 101 | this.update(data); 102 | } 103 | // TODO see if identityId is always a number, otherwise parseInt 104 | } 105 | 106 | update(data) { 107 | Object.assign(this, data); 108 | } 109 | 110 | /** 111 | * Create an {@link module:identity} object with default values 112 | * @param {String} name 113 | * @param {?String} nick 114 | * @param {?String} realname 115 | */ 116 | static create(name, nick, realname) { 117 | const options = { 118 | identityName: name, 119 | realName: realname || name, 120 | nicks: [ nick || name ] 121 | }; 122 | return new this(options); 123 | } 124 | 125 | toString() { 126 | return ``; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libquassel 2 | Javascript library to connect and interact with Quassel IRC server. 3 | 4 | ## Install 5 | ```sh 6 | npm install --production libquassel 7 | ``` 8 | 9 | ## Use in browser 10 | You just need to import `dist/libquassel.js` in your HTML page. 11 | 12 | ## Development 13 | ```sh 14 | npm install libquassel 15 | ``` 16 | 17 | In order to create a browser compatible file, run the following commands 18 | ```sh 19 | # use browserify to build on change 20 | npm run watch 21 | # before commit, make the dev version + minified version + the doc 22 | npm run build 23 | ``` 24 | 25 | ### 3.0 breaking changes 26 | Version `3.0` introduces the following breaking changes: 27 | 28 | - `message.Type` has been superseded by `message.Types`, and all its constants are now UPPERCASE 29 | - `channel.active` has been superseded by `channel.isActive` 30 | - `channel.isChannel()` has been superseded by `channel.isChannel` 31 | - `channel.isHighlighted()` has been superseded by `channel.isHighlighted` 32 | - `message.isHighlighted()` has been superseded by `message.isHighlighted` 33 | - `message.isSelf()` has been superseded by `message.isSelf` 34 | - `network.getBufferCollection()` and `network.getBufferMap()` have been merged into `networks.buffers` 35 | - `networkCollection.findBuffer(...)` and `networkCollection.get(...)` have been merged into `network.getBuffer(...)` 36 | - The majority of setter methods has been replaced by direct affectation to the target property 37 | - e.g. `network.setName(name)` as been superseded by `network.name = name` 38 | - The majority of getter methods has been replaced by direct access to the target property 39 | - e.g. `network.getStatusBuffer()` as been superseded by `network.statusBuffer` 40 | 41 | #### node specific 42 | - `Client(...).connect` method expects a `Socket` or any other `Duplex` as parameter. 43 | 44 | #### browser specific 45 | - `libquassel` is available as a global object. 46 | - `Client(...).connect` method expects a `libquassel.WebSocketStream` or any other `Duplex` as parameter. 47 | 48 | ### Getting Started 49 | #### node 50 | ```javascript 51 | const { Client } = require('libquassel'); 52 | const net = require('net'); 53 | 54 | const socket = net.createConnection({ 55 | host: "localhost", 56 | port: 4242 57 | }); 58 | 59 | const quassel = new Client((next) => next("user", "password")); 60 | 61 | quassel.on('network.init', (networkId) => { 62 | network = quassel.networks.get(networkId); 63 | // ... 64 | }); 65 | 66 | // ... 67 | 68 | quassel.connect(socket); 69 | ``` 70 | 71 | #### browser 72 | ```html 73 | 74 | 75 | ``` 76 | ```javascript 77 | // libquassel in available as a global in browser 78 | const socket = new libquassel.WebSocketStream('wss://domain.tld:12345', ['binary', 'base64']); 79 | const quassel = new libquassel.Client((next) => next("user", "password")); 80 | 81 | quassel.on('network.init', (networkId) => { 82 | network = quassel.networks.get(networkId); 83 | // ... 84 | }); 85 | 86 | // ... 87 | 88 | quassel.connect(socket); 89 | ``` 90 | 91 | ### Documentation 92 | [3.1.0](https://magne4000.github.com/libquassel/3.1.0 "libquassel 3.1.0 documentation") 93 | 94 | ### Examples 95 | See [test](test) folder for examples. 96 | 97 | ### Changelog 98 | 99 | #### 3.1.0 100 | - Add support for core highlight rules 101 | 102 | #### 3.1.1 103 | - Update dependencies 104 | 105 | ## License 106 | Copyright (c) 2019 Joël Charles 107 | Licensed under the MIT license. 108 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // First, run a local quasselcore server with the following command: 2 | // quasselcore -L Debug -p 4243 -c /tmp; rm /tmp/quassel* 3 | 4 | import { Client } from '../src/libquassel.js'; 5 | import { test } from 'tape'; 6 | import net from 'net'; 7 | 8 | function loginOk(next) { 9 | next("unittest", "unittest"); 10 | } 11 | 12 | function loginError(next) { 13 | next("wrong", "wrong"); 14 | } 15 | 16 | const timeoutObj = {timeout: 3000}; 17 | 18 | const quassel = new Client(loginError); 19 | 20 | const socket = net.createConnection({ 21 | host: "localhost", 22 | port: 4243 23 | }); 24 | 25 | socket.on('error', err => { 26 | console.error(err); 27 | process.exit(1); 28 | }); 29 | 30 | test('setup', timeoutObj, (t) => { 31 | t.plan(3); 32 | 33 | quassel.once('setup', (data) => { 34 | t.pass("setup event received"); 35 | quassel.setupCore('wrongbackend', 'unittest', 'unittest'); 36 | }); 37 | 38 | quassel.once('setupfailed', (data) => { 39 | t.pass("setupfailed event received"); 40 | quassel.setupCore('SQLite', 'unittest', 'unittest'); 41 | }); 42 | 43 | quassel.once('setupok', (data) => { 44 | t.pass("setupok event received"); 45 | }); 46 | }); 47 | 48 | test('login', timeoutObj, (t) => { 49 | t.plan(5); 50 | 51 | quassel.once('loginfailed', () => { 52 | t.pass("loginfailed event received"); 53 | quassel.loginCallback = loginOk; 54 | quassel.login(); 55 | }); 56 | 57 | quassel.once('login', () => { 58 | t.pass("login event received"); 59 | }); 60 | 61 | quassel.once('ignorelist', () => { 62 | t.pass("ignorelist event received"); 63 | }); 64 | 65 | quassel.once('bufferview.ids', () => { 66 | t.pass("bufferview.ids event received"); 67 | }); 68 | 69 | quassel.once('aliases', () => { 70 | t.pass("aliases event received"); 71 | }); 72 | 73 | quassel.login(); 74 | }); 75 | 76 | test('identity', timeoutObj, (t) => { 77 | const Identity = require('../src/identity').default; 78 | t.plan(6); 79 | 80 | quassel.once('identity.new', (identityId) => { 81 | t.pass("identity.new event received"); 82 | const identity = quassel.identities.get(identityId); 83 | t.equals(identity.identityName, 'unittestidentity'); 84 | t.equals(identity.realName, 'libquassel unittest build'); 85 | t.deepEqual(identity._nicks, [ 'unittestlibquassel' ]); 86 | t.equals(identity.ident, 'quassel'); 87 | t.equals(identity.identityId, 1); 88 | }); 89 | 90 | quassel.core.createIdentity(Identity.create('unittestidentity', 'unittestlibquassel', 'libquassel unittest build')); 91 | }); 92 | 93 | test('network', timeoutObj, (t) => { 94 | t.plan(5); 95 | 96 | quassel.once('network.new', (networkId) => { 97 | t.pass("network.new event received"); 98 | const network = quassel.networks.get(networkId); 99 | t.equals(network.networkId, 1); 100 | }); 101 | 102 | quassel.once('network.init', (networkId) => { 103 | t.pass("network.init event received"); 104 | const network = quassel.networks.get(networkId); 105 | t.deepEqual(network.ServerList, [{ 106 | Host: 'chat.freenode.net', 107 | Password: '', 108 | Port: 6665, 109 | ProxyHost: 'localhost', 110 | ProxyPass: '', 111 | ProxyPort: 8080, 112 | ProxyType: 0, 113 | ProxyUser: '', 114 | UseProxy: false, 115 | UseSSL: true, 116 | sslVerify: false, 117 | sslVersion: 0 118 | }]); 119 | t.equals(network.networkName, 'UTF-8 Network ♥♦♣∞'); 120 | }); 121 | 122 | quassel.core.createNetwork('UTF-8 Network ♥♦♣∞', 1, { 123 | Host: 'chat.freenode.net', 124 | Port: 6665, 125 | UseSSL: true, 126 | ProxyHost: 'localhost', 127 | }); 128 | }); 129 | 130 | test.onFinish(() => { 131 | quassel.disconnect(); 132 | }); 133 | 134 | quassel.connect(socket); 135 | -------------------------------------------------------------------------------- /src/tls.js: -------------------------------------------------------------------------------- 1 | import forge from 'node-forge'; 2 | import { Duplex } from 'stream'; 3 | import debug from 'debug'; 4 | const logger = debug('libquassel:network'); 5 | 6 | export class TLSSocket extends Duplex { 7 | constructor(duplex, options) { 8 | super(options); 9 | this._tlsOptions = options; 10 | this._secureEstablished = false; 11 | this._duplex = duplex; 12 | this._duplex.push = (data) => this._ssl.process(data.toString('binary')); 13 | this._ssl = null; 14 | this._before_secure_chunks = []; 15 | this._init(); 16 | this._start(); 17 | } 18 | 19 | _init() { 20 | const self = this; 21 | this._ssl = forge.tls.createConnection({ 22 | server: false, 23 | verify: function(connection, verified, depth, certs) { 24 | if (!self._tlsOptions.rejectUnauthorized || !self._tlsOptions.servername) { 25 | logger('server certificate verification skipped'); 26 | return true; 27 | } 28 | 29 | if (depth === 0) { 30 | const cn = certs[0].subject.getField('CN').value; 31 | if (cn !== self._tlsOptions.servername) { 32 | verified = { 33 | alert: forge.tls.Alert.Description.bad_certificate, 34 | message: 'Certificate Common Name does not match hostname.' 35 | }; 36 | logger('%s !== %s', cn, self._tlsOptions.servername); 37 | } 38 | logger('server certificate verified'); 39 | } else { 40 | logger('skipping certificate trust verification'); 41 | verified = true; 42 | } 43 | 44 | return verified; 45 | }, 46 | connected: function(_connection) { 47 | logger('connected'); 48 | self._secureEstablished = true; 49 | self.emit('secure'); 50 | }, 51 | tlsDataReady: function(connection) { 52 | // encrypted data is ready to be sent to the server 53 | const data = connection.tlsData.getBytes(); 54 | self._duplex.write(data, 'binary'); 55 | }, 56 | dataReady: function(connection) { 57 | // clear data from the server is ready 58 | const data = connection.data.getBytes(); 59 | self.push(Buffer.from(data, 'binary')); 60 | }, 61 | closed: function() { 62 | logger('disconnected'); 63 | self.end(); 64 | }, 65 | error: function(connection, error) { 66 | logger('error', error); 67 | error.toString = function () { 68 | return 'TLS error: ' + error.message; 69 | }; 70 | self.emit('error', error); 71 | } 72 | }); 73 | } 74 | 75 | _start() { 76 | logger('handshake'); 77 | this._ssl.handshake(); 78 | } 79 | 80 | _write(chunk, encoding, callback) { 81 | if (!this._secureEstablished) { 82 | this._before_secure_chunks.push([ chunk, encoding, callback ]); 83 | } else { 84 | if (this._before_secure_chunks.length > 0) { 85 | for (let [ chunk, encoding, callback ] of this._before_secure_chunks) { 86 | this._writenow(chunk, encoding, callback); 87 | } 88 | this._before_secure_chunks = []; 89 | } 90 | this._writenow(chunk, encoding, callback); 91 | } 92 | } 93 | 94 | _writenow(chunk, encoding, callback=()=>{}) { 95 | const result = this._ssl.prepare(chunk); 96 | process.nextTick(() => { 97 | callback(result ? 'Error while packaging data into a TLS record' : null); 98 | }); 99 | } 100 | 101 | _read(_size) {} 102 | } 103 | 104 | export function connect(options, callback=()=>{}) { 105 | const defaults = { 106 | rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED 107 | }; 108 | options = Object.assign({}, defaults, options); 109 | 110 | const socket = new TLSSocket(options.socket, options); 111 | socket.once('secure', callback); 112 | 113 | return socket; 114 | } 115 | 116 | export function createSecureContext(options) { 117 | return forge.tls.createSecureContext(options); 118 | } 119 | -------------------------------------------------------------------------------- /src/bufferview.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | /** 10 | * Quassel BufferView 11 | */ 12 | export default class BufferView { 13 | /** @type {number} */ 14 | id; 15 | /** @type {boolean} */ 16 | sortAlphabetically; 17 | /** @type {number} */ 18 | showSearch; 19 | /** @type {number} */ 20 | networkId; 21 | /** @type {number} */ 22 | minimumActivity; 23 | /** @type {boolean} */ 24 | hideInactiveNetworks; 25 | /** @type {boolean} */ 26 | hideInactiveBuffers; 27 | /** @type {boolean} */ 28 | disableDecoration; 29 | /** @type {String} */ 30 | bufferViewName; 31 | /** @type {number} */ 32 | allowedBufferTypes; 33 | /** @type {boolean} */ 34 | addNewBuffersAutomatically; 35 | /** @type {number[]} */ 36 | TemporarilyRemovedBuffers; 37 | /** @type {number[]} */ 38 | RemovedBuffers; 39 | /** @type {number[]} */ 40 | BufferList; 41 | 42 | constructor (id, data) { 43 | this.id = id; 44 | 45 | if (data) { 46 | this.update(data); 47 | } 48 | } 49 | 50 | /** 51 | * Returns `true` if given `bufferId` is temporarily hidden 52 | * @param {number} bufferId 53 | * @returns {boolean} 54 | */ 55 | isTemporarilyRemoved(bufferId) { 56 | return this.TemporarilyRemovedBuffers.indexOf(bufferId) !== -1; 57 | } 58 | 59 | /** 60 | * Returns `true` if given `bufferId` is permanently hidden 61 | * @param {number} bufferId 62 | * @returns {boolean} 63 | */ 64 | isPermanentlyRemoved(bufferId) { 65 | return this.RemovedBuffers.indexOf(bufferId) !== -1; 66 | } 67 | 68 | /** 69 | * Returns `true` if given `bufferId` is hidden 70 | * @param {number} bufferId 71 | * @returns {boolean} 72 | */ 73 | isHidden(bufferId) { 74 | return this.isTemporarilyRemoved(bufferId) || this.isPermanentlyRemoved(bufferId); 75 | } 76 | 77 | /** 78 | * Remove hidden status for given `bufferId` 79 | * @param {number} bufferId 80 | */ 81 | unhide(bufferId) { 82 | if (typeof bufferId !== 'number') return; 83 | let index = this.TemporarilyRemovedBuffers.indexOf(bufferId); 84 | if (index !== -1) { 85 | this.TemporarilyRemovedBuffers.splice(index, 1); 86 | } else { 87 | index = this.RemovedBuffers.indexOf(bufferId); 88 | if (index !== -1) { 89 | this.RemovedBuffers.splice(index, 1); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Temporarily hide given `bufferId` 96 | * @param {number} bufferId 97 | */ 98 | setTemporarilyRemoved(bufferId) { 99 | if (typeof bufferId !== 'number') return; 100 | this.unhide(bufferId); 101 | this.TemporarilyRemovedBuffers.push(bufferId); 102 | } 103 | 104 | /** 105 | * Permanently hide given `bufferId` 106 | * @param {number} bufferId 107 | */ 108 | setPermanentlyRemoved(bufferId) { 109 | if (typeof bufferId !== 'number') return; 110 | this.unhide(bufferId); 111 | this.RemovedBuffers.push(bufferId); 112 | } 113 | 114 | /** 115 | * Add (or move) a buffer to a specified position 116 | * @param {number} bufferId 117 | * @param {number} position 118 | */ 119 | addBuffer(bufferId, position) { 120 | const index = this.BufferList.indexOf(bufferId); 121 | if (index !== -1) { 122 | this.moveBuffer(bufferId, position); 123 | } else { 124 | this.BufferList.splice(position, 0, bufferId); 125 | } 126 | } 127 | 128 | /** 129 | * Move a buffer to another position 130 | * @param {number} bufferId 131 | * @param {number} position 132 | */ 133 | moveBuffer(bufferId, position) { 134 | const index = this.BufferList.indexOf(bufferId); 135 | if (index !== -1) { 136 | this.BufferList.splice(index, 1); 137 | this.BufferList.splice(position, 0, bufferId); 138 | } 139 | } 140 | 141 | /** 142 | * Used by sort methods 143 | * @param {number} id1 144 | * @param {number} id2 145 | * @returns {number} 146 | * @example 147 | * const bufferView = new BufferView(1, {...}); 148 | * anArrayOfBufferIds.sort(bufferView.comparator); 149 | */ 150 | comparator(id1, id2) { 151 | if (!this.BufferList) return 0; 152 | const iid1 = this.BufferList.indexOf(id1); 153 | const iid2 = this.BufferList.indexOf(id2); 154 | if (iid1 === iid2) { // -1 === -1 155 | return 0; 156 | } 157 | return iid1 < iid2 ? -1 : 1; 158 | } 159 | 160 | update(data) { 161 | Object.assign(this, data); 162 | } 163 | 164 | toString() { 165 | return ``; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/highlight.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2018 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | import debug from 'debug'; 10 | const logger = debug('libquassel:highlight'); 11 | import { serialization } from 'qtdatastream'; 12 | const { Serializable } = serialization; 13 | 14 | /** 15 | * @type {Object} 16 | * @property {number} IgnoreTypes.NONICK 17 | * @property {number} IgnoreTypes.CURRENTNICK 18 | * @property {number} IgnoreTypes.ALLNICKS 19 | */ 20 | export const HighlightNickType = { 21 | NONICK: 0, 22 | CURRENTNICK: 1, 23 | ALLNICKS: 2 24 | }; 25 | 26 | /** 27 | * Represent a unique highlight rule defined by the user 28 | */ 29 | export class HighlightRule { 30 | /** @type {string} */ 31 | name; 32 | /** @type {boolean} */ 33 | isRegEx; 34 | /** @type {boolean} */ 35 | isCaseSensitive; 36 | /** @type {boolean} */ 37 | isEnabled; 38 | /** @type {boolean} */ 39 | isInverse; 40 | /** @type {string} */ 41 | sender; 42 | /** @type {string} */ 43 | channel; 44 | 45 | /** 46 | * @param {string} name 47 | * @param {boolean} isRegEx 48 | * @param {boolean} isCaseSensitive 49 | * @param {boolean} isEnabled 50 | * @param {boolean} isInverse 51 | * @param {string} sender 52 | * @param {string} channel 53 | */ 54 | constructor(name, isRegEx, isCaseSensitive, isEnabled, isInverse, sender, channel){ 55 | this.name = name; 56 | this.isRegEx = isRegEx; 57 | this.isCaseSensitive = isCaseSensitive; 58 | this.isEnabled = isEnabled; 59 | this.isInverse = isInverse; 60 | this.sender = sender; 61 | this.channel = channel; 62 | } 63 | 64 | 65 | toString() { 66 | const ret = [ ''); 71 | return ret.join(' '); 72 | } 73 | } 74 | 75 | /** 76 | * Handles list of {@link IgnoreItem} 77 | * @implements {Serializable} 78 | */ 79 | @Serializable() 80 | export class HighlightRuleManager { 81 | constructor() { 82 | this.list = []; 83 | this.highlightNick = HighlightNickType.NONICK; 84 | this.nicksCaseSensitive = false; 85 | } 86 | 87 | /** 88 | * Import object as a list of {@link HighlightRule} 89 | * @param {Object} data 90 | */ 91 | import(data) { 92 | logger('import', data); 93 | this.list = new Array(data.HighlightRuleList.name.length); 94 | for (let i=0; i '\n\t' + x), 148 | '>' 149 | ].join(' '); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/message.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | import { util } from 'qtdatastream'; 10 | 11 | /** 12 | * @type {Object} 13 | * @property {number} Types.PLAIN 14 | * @property {number} Types.NOTICE 15 | * @property {number} Types.ACTION 16 | * @property {number} Types.NICK 17 | * @property {number} Types.MODE 18 | * @property {number} Types.JOIN 19 | * @property {number} Types.PART 20 | * @property {number} Types.QUIT 21 | * @property {number} Types.KICK 22 | * @property {number} Types.KILL 23 | * @property {number} Types.SERVER 24 | * @property {number} Types.INFO 25 | * @property {number} Types.ERROR 26 | * @property {number} Types.DAYCHANGE 27 | * @property {number} Types.TOPIC 28 | * @property {number} Types.NETSPLITJOIN 29 | * @property {number} Types.NETSPLITQUIT 30 | * @property {number} Types.INVITE 31 | */ 32 | export const Types = { 33 | PLAIN: 0x00001, 34 | NOTICE: 0x00002, 35 | ACTION: 0x00004, 36 | NICK: 0x00008, 37 | MODE: 0x00010, 38 | JOIN: 0x00020, 39 | PART: 0x00040, 40 | QUIT: 0x00080, 41 | KICK: 0x00100, 42 | KILL: 0x00200, 43 | SERVER: 0x00400, 44 | INFO: 0x00800, 45 | ERROR: 0x01000, 46 | DAYCHANGE: 0x02000, 47 | TOPIC: 0x04000, 48 | NETSPLITJOIN: 0x08000, 49 | NETSPLITQUIT: 0x10000, 50 | INVITE: 0x20000 51 | }; 52 | 53 | 54 | /** 55 | * @type {Object} 56 | * @property {number} Flags.NONE 57 | * @property {number} Flags.SELF 58 | * @property {number} Flags.HIGHLIGHT 59 | * @property {number} Flags.REDIRECTED 60 | * @property {number} Flags.SERVERMSG 61 | * @property {number} Flags.BACKLOG 62 | */ 63 | export const Flags = { 64 | NONE: 0x00, 65 | SELF: 0x01, 66 | HIGHLIGHT: 0x02, 67 | REDIRECTED: 0x04, 68 | SERVERMSG: 0x08, 69 | BACKLOG: 0x80 70 | }; 71 | 72 | /** 73 | * @type {Object} 74 | * @property {number} HighlightModes.NONE 75 | * @property {number} HighlightModes.CURRENTNICK 76 | * @property {number} HighlightModes.ALLIDENTITYNICKS 77 | */ 78 | export const HighlightModes = { 79 | NONE: 0x01, 80 | CURRENTNICK: 0x02, 81 | ALLIDENTITYNICKS: 0x03 82 | }; 83 | 84 | /** 85 | * IRC Message 86 | */ 87 | export class IRCMessage { 88 | /** @type {number} */ 89 | id; 90 | /** @type {Date} */ 91 | datetime; 92 | /** @type {number} */ 93 | type; 94 | /** @type {?string} */ 95 | content; 96 | /** @type {BufferInfo} */ 97 | bufferInfo; 98 | /** @type {boolean} */ 99 | isHighlighted; 100 | /** @type {?string} */ 101 | nick; 102 | /** @type {?string} */ 103 | hostmask; 104 | 105 | constructor(message) { 106 | this.nick = null; 107 | this.hostmask = null; 108 | this._flags = null; 109 | this.isSelf = false; 110 | this.isHighlighted = false; 111 | this._sender = null; 112 | this.id = message.id; 113 | this.datetime = new Date(message.timestamp * 1000); 114 | this.type = message.type; 115 | this.flags = message.flags; 116 | this.sender = message.sender ? util.str(message.sender) : null; 117 | this.content = message.content ? util.str(message.content) : null; 118 | this.bufferInfo = message.bufferInfo; 119 | } 120 | 121 | /** 122 | * Update internal highlight flags 123 | * @param {Network} network 124 | * @param {Identity} identity 125 | * @param {number} mode 126 | * @protected 127 | */ 128 | _updateFlags(network, identity, mode) { 129 | let nickRegex = null, nicks = []; 130 | switch (mode) { 131 | case HighlightModes.NONE: 132 | // None, do nothing 133 | return; 134 | case HighlightModes.CURRENTNICK: 135 | if (this.type !== Types.PLAIN && this.type !== Types.ACTION) return; 136 | if (!network.nick) return; 137 | ({ nickRegex } = network); 138 | break; 139 | case HighlightModes.ALLIDENTITYNICKS: 140 | if (this.type !== Types.PLAIN && this.type !== Types.ACTION) return; 141 | if (identity.nicks.length === 0) return; 142 | for (let identityNickRegex of identity.nickRegexes) { 143 | nicks.push(identityNickRegex); 144 | } 145 | if (network.nick && identity.nicks.indexOf(network.nick) === -1) { 146 | nicks.push(network.nickRegex); 147 | } 148 | nickRegex = `(${nicks.join('|')})`; 149 | break; 150 | default: 151 | // Invalid, do nothing 152 | return; 153 | } 154 | let regex = new RegExp(`([\\W]|^)${nickRegex}([\\W]|$)`, 'i'); 155 | if (regex.test(this.content)) { 156 | this.flags = this.flags | Flags.HIGHLIGHT; 157 | } 158 | } 159 | 160 | /** @type {number} */ 161 | set flags(value) { 162 | this._flags = value; 163 | this.isSelf = (value & Flags.SELF) !== 0; 164 | this.isHighlighted = ((value & Flags.HIGHLIGHT) !== 0) && !this.isSelf; 165 | } 166 | 167 | /** @type {number} */ 168 | get flags() { 169 | return this._flags; 170 | } 171 | 172 | /** @type {?string} */ 173 | set sender(value) { 174 | this._sender = value; 175 | if (value) { 176 | [ this.nick, this.hostmask ] = value.split('!'); 177 | } else { 178 | this.nick = this.hostmask = null; 179 | } 180 | } 181 | 182 | /** @type {?string} */ 183 | get sender() { 184 | return this._sender; 185 | } 186 | 187 | toString() { 188 | return ``; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/ignore.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | import debug from 'debug'; 10 | const logger = debug('libquassel:ignore'); 11 | import { serialization } from 'qtdatastream'; 12 | const { Serializable } = serialization; 13 | 14 | import { Types } from './message.js'; 15 | 16 | /** 17 | * @type {Object} 18 | * @property {number} IgnoreTypes.SENDER 19 | * @property {number} IgnoreTypes.MESSAGE 20 | * @property {number} IgnoreTypes.CTCP 21 | */ 22 | export const IgnoreTypes = { 23 | SENDER: 0, 24 | MESSAGE: 1, 25 | CTCP: 2 26 | }; 27 | 28 | /** 29 | * @type {Object} 30 | * @property {number} StrictnessTypes.UNMATCHED 31 | * @property {number} StrictnessTypes.SOFT 32 | * @property {number} StrictnessTypes.HARD 33 | */ 34 | export const StrictnessTypes = { 35 | UNMATCHED: 0, 36 | SOFT: 1, 37 | HARD: 2 38 | }; 39 | 40 | /** 41 | * @type {Object} 42 | * @property {number} ScopeTypes.GLOBAL 43 | * @property {number} ScopeTypes.NETWORK 44 | * @property {number} ScopeTypes.CHANNEL 45 | */ 46 | export const ScopeTypes = { 47 | GLOBAL: 0, 48 | NETWORK: 1, 49 | CHANNEL: 2 50 | }; 51 | 52 | /** 53 | * Ignore item as represented in the configuration 54 | */ 55 | export class IgnoreItem { 56 | /** @type {number} */ 57 | strictness; 58 | /** @type {string} */ 59 | scopeRule; 60 | /** @type {number} */ 61 | scope; 62 | /** @type {boolean} */ 63 | isRegEx; 64 | /** @type {boolean} */ 65 | isActive; 66 | /** @type {number} */ 67 | ignoreType; 68 | /** @type {string} */ 69 | ignoreRule; 70 | /** @type {RegExp[]} */ 71 | regexScope; 72 | /** @type {RegExp} */ 73 | regexIgnore; 74 | 75 | /** 76 | * @param {number} strictness 77 | * @param {string} scopeRule 78 | * @param {number} scope 79 | * @param {boolean} isRegEx 80 | * @param {boolean} isActive 81 | * @param {number} ignoreType 82 | * @param {string} ignoreRule 83 | */ 84 | constructor(strictness, scopeRule, scope, isRegEx, isActive, ignoreType, ignoreRule){ 85 | this.strictness = strictness; 86 | this.scopeRule = scopeRule; 87 | this.scope = scope; 88 | this.isRegEx = isRegEx; 89 | this.isActive = isActive; 90 | this.ignoreType = ignoreType; 91 | this.ignoreRule = ignoreRule; 92 | this.regexScope = []; 93 | this.compile(); 94 | } 95 | 96 | /** 97 | * Returns `true` if subject match the scope rules, `false` otherwhise 98 | * @param {string} subject 99 | * @returns {boolean} 100 | */ 101 | matchScope(subject) { 102 | if (typeof subject !== 'string') return false; 103 | let ret = false; 104 | for (let regexScope of this.regexScope) { 105 | ret = subject.match(regexScope) !== null; 106 | if (ret) break; 107 | } 108 | return ret; 109 | } 110 | 111 | /** 112 | * Returns `true` if subject match ignore rule, `false` otherwhise 113 | * @param {String} subject 114 | * @returns {boolean} 115 | */ 116 | matchIgnore(subject) { 117 | if (typeof subject !== 'string') return false; 118 | return subject.match(this.regexIgnore) !== null; 119 | } 120 | 121 | /** 122 | * Compile internal regexes from `scopeRules` and `ignoreRule` attributes 123 | */ 124 | compile() { 125 | const scopeRules = this.scopeRule.split(';'); 126 | this.regexScope = []; 127 | for (let scopeRule of scopeRules) { 128 | this.regexScope.push(wildcardToRegex(scopeRule)); 129 | } 130 | try { 131 | this.regexIgnore = this.isRegEx ? new RegExp(this.ignoreRule, 'i') : wildcardToRegex(this.ignoreRule); 132 | } catch (e) { 133 | logger('Invalid RexExp', e); 134 | this.isActive = false; 135 | } 136 | } 137 | 138 | toString() { 139 | const ret = [ ''); 144 | return ret.join(' '); 145 | } 146 | } 147 | 148 | function wildcardToRegex(subject) { 149 | const input = subject.trim().replace(/([.+^$\\(){}|-])/g, '\\$1').replace(/\*/g, '.*').replace(/\?/g, '.'); 150 | return new RegExp(`^${input}$`, 'i'); 151 | } 152 | 153 | /** 154 | * Handles list of {@link IgnoreItem} 155 | * @implements {Serializable} 156 | */ 157 | @Serializable() 158 | export class IgnoreList { 159 | constructor() { 160 | this.list = []; 161 | } 162 | 163 | /** 164 | * Import object as a list of {@link IgnoreItem} 165 | * @param {Object} data 166 | */ 167 | import(data) { 168 | let item; 169 | this.list = new Array(data.IgnoreList.ignoreRule.length); 170 | for (let i=0; i '\n\t' + x), '>' ].join(' '); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/buffer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | /** @module buffer */ 10 | 11 | import debug from 'debug'; 12 | const logger = debug('libquassel:buffer'); 13 | import { util } from 'qtdatastream'; 14 | 15 | import { IRCMessage } from './message.js'; 16 | 17 | /** 18 | * BufferInfo object representation 19 | * @typedef {Object} BufferInfo 20 | * @property {number} id 21 | * @property {number} network 22 | * @property {number} type 23 | * @property {number} group 24 | * @property {String} name 25 | */ 26 | 27 | /** 28 | * @type {Object} 29 | * @property {number} Types.INVALID 30 | * @property {number} Types.STATUS 31 | * @property {number} Types.CHANNEL 32 | * @property {number} Types.QUERY 33 | * @property {number} Types.GROUP 34 | */ 35 | export const Types = { 36 | INVALID: 0x00, 37 | STATUS: 0x01, 38 | CHANNEL: 0x02, 39 | QUERY: 0x04, 40 | GROUP: 0x08 41 | }; 42 | 43 | /** 44 | * User attached to a buffer, with its modes 45 | */ 46 | export class IRCBufferUser { 47 | /** @type {IRCUser} */ 48 | user; 49 | /** @type {boolean} */ 50 | isOp; 51 | /** @type {boolean} */ 52 | isHalfOp; /** @type {boolean} */ 53 | isOwner; 54 | /** @type {boolean} */ 55 | isAdmin; 56 | /** @type {boolean} */ 57 | isVoiced; 58 | 59 | /** 60 | * @param {IRCUser} user 61 | * @param {string} modes 62 | */ 63 | constructor(user, modes) { 64 | this.user = user; 65 | this.isOp = false; 66 | this.isHalfOp = false; 67 | this.isOwner = false; 68 | this.isAdmin = false; 69 | this.isVoiced = false; 70 | this._modes = ''; 71 | this.modes = modes; 72 | } 73 | 74 | /** @type {string} */ 75 | get modes() { 76 | return this._modes; 77 | } 78 | 79 | /** @type {string} */ 80 | set modes(value) { 81 | this._modes = value; 82 | this.isOp = this.hasMode('o'); 83 | this.isHalfOp = this.hasMode('h'); 84 | this.isOwner = this.hasMode('q'); 85 | this.isAdmin = this.hasMode('a'); 86 | this.isVoiced = this.hasMode('v'); 87 | } 88 | 89 | /** 90 | * Returns `true` if user has specified mode 91 | * @param {string} mode 92 | * @returns {boolean} 93 | */ 94 | hasMode(mode) { 95 | return this._modes.indexOf(mode) !== -1; 96 | } 97 | } 98 | 99 | /** 100 | * Quassel respresentation of a buffer 101 | */ 102 | export class IRCBuffer { 103 | /** @type {?number} */ 104 | id; 105 | /** @type {boolean} */ 106 | isChannel; 107 | /** @type {boolean} */ 108 | isActive; 109 | /** @type {boolean} */ 110 | isStatusBuffer; 111 | /** @type {?number} */ 112 | lastMessageId; 113 | /** @type {?number} */ 114 | firstMessageId; 115 | /** 116 | * Map of users of this channel. 117 | * @type {Map} 118 | */ 119 | users; 120 | /** 121 | * Map of messages in this buffer. 122 | * @type {Map} 123 | */ 124 | messages; 125 | /** @type {?Types} */ 126 | type; 127 | /** @type {number} */ 128 | network; 129 | /** @type {number} */ 130 | group; 131 | 132 | 133 | /** 134 | * @param {object} data 135 | */ 136 | constructor(data) { 137 | this._name = null; 138 | this.isChannel = false; 139 | this.isActive = false; 140 | this.id = null; 141 | this.users = new Map(); 142 | this.messages = new Map(); 143 | this.lastMessageId = null; 144 | this.firstMessageId = null; 145 | 146 | this.update(data); 147 | this.isStatusBuffer = (this.type === Types.STATUS); 148 | } 149 | 150 | /** 151 | * Add {@link IRCUser} to the buffer 152 | * @param {IRCUser} user 153 | * @param {string} modes 154 | */ 155 | addUser(user, modes) { 156 | if (user && typeof user.nick === 'string') { 157 | this.users.set(user.nick, new IRCBufferUser(user, modes)); 158 | } 159 | } 160 | 161 | /** 162 | * Add a mode to an {@link IRCUser} 163 | * @param {IRCUser} user 164 | * @param {string} mode 165 | */ 166 | addUserMode(user, mode) { 167 | if (user && typeof user.nick === 'string') { 168 | const userAndModes = this.users.get(user.nick); 169 | if (userAndModes) userAndModes.modes += mode; 170 | } 171 | } 172 | 173 | /** 174 | * remove mode from an {@link IRCUser} 175 | * @param {IRCUser} user 176 | * @param {string} mode 177 | */ 178 | removeUserMode(user, mode) { 179 | if (user && typeof user.nick === 'string') { 180 | let userAndModes = this.users.get(user.nick); 181 | if (userAndModes) userAndModes.modes = userAndModes.modes.replace(mode, ''); 182 | } 183 | } 184 | 185 | /** 186 | * Check if current buffer contains specified user 187 | * @param {string|IRCUser} nick 188 | * @returns {boolean} 189 | */ 190 | hasUser(nick) { 191 | if (nick === undefined || nick === null) { 192 | logger('User should not be null or undefined'); 193 | return null; 194 | } 195 | return this.users.has(typeof nick.nick === 'string' ? nick.nick : nick); 196 | } 197 | 198 | /** 199 | * Remove user from buffer 200 | * @param {string|IRCUser} nick 201 | */ 202 | removeUser(nick) { 203 | this.users.delete(typeof nick.nick === 'string' ? nick.nick : nick); 204 | } 205 | 206 | /** 207 | * Update user maps hashes with current .nick 208 | * @param {string} oldnick 209 | */ 210 | updateUserMaps(oldnick) { 211 | const userAndModes = this.users.get(oldnick); 212 | if (oldnick !== userAndModes.user.nick) { 213 | this.users.set(userAndModes.user.nick, userAndModes); 214 | this.users.delete(oldnick); 215 | } 216 | } 217 | 218 | /** 219 | * Add an {@link IRCMessage} to the buffer 220 | * @param {Object} message 221 | * @returns {?IRCMessage} the message, if successfully added, `undefined` otherwise 222 | */ 223 | addMessage(message) { 224 | if (this.messages.has(message.id)) return undefined; 225 | if (this.lastMessageId === null || this.lastMessageId < message.id) { 226 | this.lastMessageId = message.id; 227 | } 228 | if (this.firstMessageId === null || this.firstMessageId > message.id) { 229 | this.firstMessageId = message.id; 230 | } 231 | const ircmsg = new IRCMessage(message); 232 | this.messages.set(message.id, ircmsg); 233 | return ircmsg; 234 | } 235 | 236 | /** 237 | * Update internal lastMessageId and firstMessageId 238 | * @protected 239 | */ 240 | _updateFirstAndLast() { 241 | this.lastMessageId = null; 242 | this.firstMessageId = null; 243 | for (let key of this.messages.keys()) { 244 | if (this.lastMessageId === null || this.lastMessageId < key) this.lastMessageId = key; 245 | if (this.firstMessageId === null || this.firstMessageId > key) this.firstMessageId = key; 246 | } 247 | } 248 | 249 | /** 250 | * Clear buffer messages 251 | */ 252 | clearMessages() { 253 | this.lastMessageId = null; 254 | this.firstMessageId = null; 255 | this.messages.clear(); 256 | } 257 | 258 | /** 259 | * Delete a message from the buffer 260 | * @param {number} messageId 261 | */ 262 | deleteMessage(messageId) { 263 | if (this.messages.size <= 1) { 264 | this.clearMessages(); 265 | } else { 266 | this.messages.delete(messageId); 267 | this._updateFirstAndLast(); 268 | } 269 | } 270 | 271 | /** 272 | * Trim messages and leave only `n` messages 273 | * @param {number} n 274 | */ 275 | trimMessages(n) { 276 | if (n <= 0) { 277 | this.clearMessages(); 278 | } else if (n < this.messages.size) { 279 | let idsToKeep = [], newMap = new Map; 280 | this.messages.forEach((val, key) => { 281 | idsToKeep.push(key); 282 | }); 283 | idsToKeep.sort(); 284 | idsToKeep.splice(0, idsToKeep.length - n); 285 | idsToKeep.forEach(val => { 286 | newMap.set(val, this.messages.get(val)); 287 | }); 288 | this.messages = newMap; 289 | this._updateFirstAndLast(); 290 | } 291 | } 292 | 293 | /** 294 | * Check if specified `messageId` is the last one of this buffer 295 | * @param {number} messageId 296 | * @returns {boolean} 297 | */ 298 | isLast(messageId) { 299 | return this.lastMessageId === messageId; 300 | } 301 | 302 | /** 303 | * get {@link BufferInfo} corresponding to the current buffer 304 | * @returns {BufferInfo} 305 | */ 306 | getBufferInfo() { 307 | return { 308 | id: this.id, 309 | network: this.network, 310 | type: this.type, 311 | group: this.group || 0, 312 | name: this.name 313 | }; 314 | } 315 | 316 | get firstMessage() { 317 | return this.messages.get(this.firstMessageId); 318 | } 319 | 320 | get lastMessage() { 321 | return this.messages.get(this.lastMessageId); 322 | } 323 | 324 | set name(value) { 325 | this._name = value ? value.toString() : null; 326 | this.isChannel = (this._name && '#&+!'.indexOf(this._name[0]) !== -1); 327 | } 328 | 329 | get name() { 330 | return this._name; 331 | } 332 | 333 | update(data) { 334 | Object.assign(this, data); 335 | } 336 | 337 | toString() { 338 | return ``; 339 | } 340 | } 341 | 342 | /** 343 | * A collection of buffers 344 | */ 345 | export class IRCBufferCollection extends Map { 346 | constructor(...args) { 347 | if (args.length > 0) throw new Error(`IRCBufferCollection doesn't support initializing with values.`); 348 | super(); 349 | // This map references buffers by their IDs for quick lookup 350 | this._map_buffer_ids = new Map(); 351 | } 352 | 353 | /** 354 | * Add a buffer to this collection 355 | * @param {IRCBuffer} buffer 356 | */ 357 | add(buffer) { 358 | if (this.has(buffer.name)) { 359 | logger('Buffer already added (%s)', buffer.name); 360 | return; 361 | } 362 | this.set(buffer.name, buffer); 363 | } 364 | 365 | /** 366 | * @override 367 | */ 368 | set(key, value) { 369 | if (key !== null && typeof key !== 'string') throw new Error(`Key must be a string or null`); 370 | key = key === null ? null : key.toLowerCase(); 371 | if (value.id !== -1) { 372 | this._map_buffer_ids.set(value.id, key); 373 | } 374 | super.set(key, value); 375 | } 376 | 377 | /** 378 | * Get the buffer by name if `key` is a `String` or a `Buffer`, by id otherwise 379 | * @param {number|string|Buffer} key 380 | * @override 381 | * @returns {?Buffer} 382 | */ 383 | get(key) { 384 | if (typeof key === 'number') { 385 | return this.get(this._map_buffer_ids.get(key)); 386 | } 387 | if (key === undefined) return void 0; 388 | if (key instanceof Buffer) { 389 | key = util.str(key); 390 | } 391 | return super.get(key === null ? null : key.toLowerCase()); 392 | } 393 | 394 | /** 395 | * Does the buffer exists in this collection 396 | * @param {number|string|Buffer} key 397 | * @override 398 | * @returns {boolean} 399 | */ 400 | has(key) { 401 | if (key === undefined) return false; 402 | if (key instanceof Buffer) { 403 | key = util.str(key); 404 | } 405 | if (typeof key === 'number') { 406 | return this._map_buffer_ids.has(key); 407 | } 408 | return super.has(key === null ? null : key.toLowerCase()); 409 | } 410 | 411 | /** 412 | * Delete the buffer from the collection 413 | * @param {number|string|Buffer} key 414 | * @override 415 | * @returns {boolean} 416 | */ 417 | delete(key) { 418 | if (key === undefined) return false; 419 | if (key instanceof Buffer) { 420 | key = util.str(key); 421 | } 422 | if (typeof key === 'number') { 423 | const actualKey = this._map_buffer_ids.get(key); 424 | this._map_buffer_ids.delete(key); 425 | return super.delete(actualKey); 426 | } 427 | key = key === null ? null : key.toLowerCase(); 428 | if (super.has(key)) { 429 | const elementToDelete = super.get(key); 430 | super.delete(key); 431 | this._map_buffer_ids.delete(elementToDelete.id); 432 | return true; 433 | } 434 | } 435 | 436 | /** 437 | * Clear the buffer 438 | * @override 439 | */ 440 | clear() { 441 | super.clear(); 442 | this._map_buffer_ids.clear(); 443 | } 444 | 445 | /** 446 | * Change buffer id 447 | * @param {IRCBuffer} buffer 448 | * @param {number} bufferIdTo 449 | */ 450 | move(buffer, bufferIdTo) { 451 | this.delete(buffer.name); 452 | buffer.id = bufferIdTo; 453 | this.set(buffer.name, buffer); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/network.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | import EventEmitter from 'events'; 10 | import debug from 'debug'; 11 | const logger = debug('libquassel:network'); 12 | import { util, types as qtypes, serialization } from 'qtdatastream'; 13 | const { Serializable, serialize } = serialization; 14 | 15 | import IRCUser from './user.js'; 16 | import { IRCBufferCollection } from './buffer.js'; 17 | 18 | /** 19 | * @type {Object} 20 | * @property {number} ConnectionStates.DISCONNECTED 21 | * @property {number} ConnectionStates.CONNECTING 22 | * @property {number} ConnectionStates.INITIALIZING 23 | * @property {number} ConnectionStates.INITIALIZED 24 | * @property {number} ConnectionStates.RECONNECTING 25 | * @property {number} ConnectionStates.DISCONNECTING 26 | */ 27 | export const ConnectionStates = { 28 | DISCONNECTED: 0x00, 29 | CONNECTING: 0x01, 30 | INITIALIZING: 0x02, 31 | INITIALIZED: 0x03, 32 | RECONNECTING: 0x04, 33 | DISCONNECTING: 0x05 34 | }; 35 | 36 | function setter(fn) { 37 | return function (aclass, key, descriptor) { 38 | if (!Object.prototype.hasOwnProperty.call(aclass, '__values')) { 39 | Object.defineProperty(aclass, '__values', { 40 | enumerable: false, 41 | writable: false, 42 | configurable: false, 43 | value: {} 44 | }); 45 | } 46 | Object.assign(descriptor, { 47 | enumerable: true, 48 | get: function () { 49 | return this.__values[key]; 50 | }, 51 | set: function (value) { 52 | this.__values[key] = fn(value); 53 | } 54 | }); 55 | return descriptor; 56 | }; 57 | } 58 | 59 | /** 60 | * A server as used in {@link Network} 61 | * @implements {Serializable} 62 | */ 63 | @Serializable('Network::Server') 64 | export class Server { 65 | /** @type {string} */ 66 | @serialize(qtypes.QString, 'Host') 67 | host; 68 | 69 | @serialize(qtypes.QUInt, 'Port') 70 | port = 6667; 71 | 72 | @serialize(qtypes.QString, 'Password') 73 | password = ''; 74 | 75 | @serialize(qtypes.QBool, 'UseSSL') 76 | useSSL = false; 77 | 78 | @serialize(qtypes.QBool, 'UseProxy') 79 | useProxy = false; 80 | 81 | @serialize(qtypes.QInt, 'ProxyType') 82 | proxyType = 0; 83 | 84 | @serialize(qtypes.QString, 'ProxyHost') 85 | proxyHost = ''; 86 | 87 | @serialize(qtypes.QUInt, 'ProxyPort') 88 | proxyPort = 8080; 89 | 90 | @serialize(qtypes.QString, 'ProxyUser') 91 | proxyUser = ''; 92 | 93 | @serialize(qtypes.QString, 'ProxyPass') 94 | proxyPass = ''; 95 | 96 | @serialize(qtypes.QBool) 97 | sslVerify = false; 98 | 99 | @serialize(qtypes.QInt) 100 | sslVersion = 0; 101 | 102 | constructor(args) { 103 | Object.assign(this, args); 104 | } 105 | } 106 | 107 | function toStr(s) { 108 | return Buffer.isBuffer(s) ? util.str(s) : s; 109 | } 110 | 111 | /** 112 | * Quassel Network 113 | * @implements {Serializable} 114 | */ 115 | @Serializable('NetworkInfo') 116 | export class Network extends EventEmitter { 117 | /** @type {number} */ 118 | @serialize(qtypes.QUserType.get('NetworkId'), 'NetworkId') 119 | get networkId() { 120 | return this.id; 121 | } 122 | 123 | /** @type {number} */ 124 | set networkId(value) { 125 | this.id = value; 126 | } 127 | 128 | /** @type {string} */ 129 | @serialize(qtypes.QString, 'NetworkName') 130 | get networkName() { 131 | return this.name; 132 | } 133 | 134 | /** @type {string} */ 135 | set networkName(value) { 136 | this.name = value; 137 | } 138 | 139 | /** @type {number} */ 140 | @serialize(qtypes.QUserType.get('IdentityId'), 'Identity') 141 | identityId; 142 | 143 | /** @type {string} */ 144 | @setter(toStr) 145 | @serialize(qtypes.QByteArray, 'CodecForServer') 146 | codecForServer = null; 147 | 148 | /** @type {string} */ 149 | @setter(toStr) 150 | @serialize(qtypes.QByteArray, 'CodecForEncoding') 151 | codecForEncoding = null; 152 | 153 | /** @type {string} */ 154 | @setter(toStr) 155 | @serialize(qtypes.QByteArray, 'CodecForDecoding') 156 | codecForDecoding = null; 157 | 158 | @serialize(qtypes.QList.of(Server), 'ServerList') 159 | ServerList = []; 160 | 161 | @serialize(qtypes.QBool, 'UseRandomServer') 162 | useRandomServer = false; 163 | 164 | @serialize(qtypes.QStringList, 'Perform') 165 | perform = []; 166 | 167 | @serialize(qtypes.QBool, 'UseAutoIdentify') 168 | useAutoIdentify = false; 169 | 170 | @serialize(qtypes.QString, 'AutoIdentifyService') 171 | autoIdentifyService = 'NickServ'; 172 | 173 | @serialize(qtypes.QString, 'AutoIdentifyPassword') 174 | autoIdentifyPassword = ''; 175 | 176 | @serialize(qtypes.QBool, 'UseSasl') 177 | useSasl = false; 178 | 179 | @serialize(qtypes.QString, 'SaslAccount') 180 | saslAccount = ''; 181 | 182 | @serialize(qtypes.QString, 'SaslPassword') 183 | saslPassword = ''; 184 | 185 | @serialize(qtypes.QBool, 'UseAutoReconnect') 186 | useAutoReconnect = true; 187 | 188 | @serialize(qtypes.QUInt, 'AutoReconnectInterval') 189 | autoReconnectInterval = 60; 190 | 191 | @serialize(qtypes.QUInt, 'AutoReconnectRetries') 192 | autoReconnectRetries = 20; 193 | 194 | @serialize(qtypes.QBool, 'UnlimitedReconnectRetries') 195 | unlimitedReconnectRetries = false; 196 | 197 | @serialize(qtypes.QBool, 'RejoinChannels') 198 | rejoinChannels = true; 199 | 200 | @serialize(qtypes.QBool, 'UseCustomMessageRate') 201 | useCustomMessageRate = false; 202 | 203 | @serialize(qtypes.QBool, 'UnlimitedMessageRate') 204 | unlimitedMessageRate = false; 205 | 206 | @serialize(qtypes.QUInt, 'MessageRateDelay') 207 | msgRateMessageDelay = 2200; 208 | 209 | @serialize(qtypes.QUInt, 'MessageRateBurstSize') 210 | msgRateBurstSize = 5; 211 | 212 | /** @type {string} */ 213 | set myNick(value) { 214 | this.nick = value; 215 | } 216 | 217 | /** @type {string} */ 218 | get myNick() { 219 | return this._nick; 220 | } 221 | 222 | /** @type {string} */ 223 | set nick(value) { 224 | this._nick = value; 225 | this.nickRegex = value.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); 226 | } 227 | 228 | /** @type {string} */ 229 | get nick() { 230 | return this._nick; 231 | } 232 | 233 | /** @type {boolean} */ 234 | set isConnected(connected) { 235 | connected = Boolean(connected); 236 | if (this.statusBuffer) { 237 | this.statusBuffer.isActive = connected; 238 | } 239 | this._isConnected = connected; 240 | } 241 | 242 | /** @type {boolean} */ 243 | get isConnected() { 244 | return this._isConnected; 245 | } 246 | 247 | constructor(id, name = null) { 248 | super(); 249 | this._isConnected = false; 250 | this._nick = null; 251 | this.id = typeof id === 'number' ? id : -1; 252 | /** @type {IRCBufferCollection} */ 253 | this.buffers = new IRCBufferCollection(); 254 | /** @type {Map} */ 255 | this.users = new Map; 256 | /** @type {boolean} */ 257 | this.open = false; 258 | /** @type {number} */ 259 | this.connectionState = ConnectionStates.DISCONNECTED; 260 | /** @type {number} */ 261 | this.latency = 0; 262 | /** @type {?IRCBuffer} */ 263 | this.statusBuffer = null; 264 | this.nickRegex = null; 265 | this.name = name; 266 | } 267 | 268 | getUser(nick) { 269 | return this.users.get(nick); 270 | } 271 | 272 | /** 273 | * Add given user to the network 274 | * @param {IRCUser} user 275 | */ 276 | addUser(user) { 277 | this.users.set(user.nick, user); 278 | } 279 | 280 | /** 281 | * Returns `true` if the specified nick/{@link IRCUser} exists in the network, `false` otherwise 282 | * @param {string|IRCUser} nick 283 | * @returns {boolean} 284 | */ 285 | hasUser(nick) { 286 | return this.users.has(typeof nick.nick === 'string' ? nick.nick : nick); 287 | } 288 | 289 | /** 290 | * Replace `oldNick` by `newNick` in current network and buffers 291 | * @param {string} oldNick 292 | * @param {string} newNick 293 | */ 294 | renameUser(oldNick, newNick) { 295 | const user = this.users.get(oldNick); 296 | if (user) { 297 | user.nick = newNick; 298 | this.users.set(newNick, user); 299 | this.users.delete(oldNick); 300 | for (let buffer of this.buffers.values()) { 301 | if (buffer.isChannel && buffer.hasUser(oldNick)) { 302 | buffer.updateUserMaps(oldNick); 303 | } 304 | } 305 | } 306 | } 307 | 308 | /** 309 | * Delete the user identified by `nick` from the network and buffers 310 | * @param {string} nick 311 | * @returns {number[]} list of buffer ids that has been deactivated 312 | */ 313 | deleteUser(nick) { 314 | const ids = []; 315 | for (let buffer of this.buffers.values()) { 316 | if (buffer.isChannel) { 317 | if (buffer.hasUser(nick)) { 318 | buffer.removeUser(nick); 319 | if (this.nick && this.nick.toLowerCase() === nick.toLowerCase()) { 320 | buffer.isActive = false; 321 | ids.push(buffer.id); 322 | } 323 | } 324 | } else if (buffer.name === nick) { 325 | buffer.isActive = false; 326 | ids.push(buffer.id); 327 | } 328 | } 329 | this.users.delete(nick); 330 | return ids; 331 | } 332 | 333 | /** 334 | * @param {IRCUser[]} userlist 335 | */ 336 | updateUsers(userlist) { 337 | this.users.clear(); 338 | if (Array.isArray(userlist) && userlist.length > 0) { 339 | for (let user of userlist) { 340 | this.users.set(user.nick, user); 341 | } 342 | } 343 | } 344 | 345 | /** 346 | * Get the {IRCBuffer} corresponding to specified id or name 347 | * @param {number|string} bufferId 348 | */ 349 | getBuffer(bufferId) { 350 | return this.buffers.get(bufferId); 351 | } 352 | 353 | /** 354 | * Returns `true` if a buffer exists with corresponding id or name 355 | * @param {number|string} bufferId 356 | */ 357 | hasBuffer(bufferId) { 358 | return this.buffers.has(bufferId); 359 | } 360 | 361 | /** 362 | * This method is used internally by update method 363 | * @protected 364 | * @param {Object} uac 365 | */ 366 | @serialize(qtypes.QMap, 'IrcUsersAndChannels') 367 | set ircUsersAndChannels(uac) { 368 | // Create IRCUsers and attach them to network 369 | for (let user of Object.values(uac.users)) { 370 | this.addUser(new IRCUser(user)); 371 | } 372 | // If there is a buffer corresponding to a nick, activate the buffer 373 | for (let buffer of this.buffers.values()) { 374 | if (!buffer.isChannel && !buffer.isStatusBuffer && this.hasUser(buffer.name)) { 375 | buffer.isActive = true; 376 | } 377 | } 378 | // Attach channels to network 379 | let channel, nick, user; 380 | for (let key of Object.keys(uac.channels)) { 381 | channel = this.getBuffer(key); 382 | // Then attach users to channels 383 | for (nick in uac.channels[key].UserModes) { 384 | user = this.getUser(nick); 385 | if (user) { 386 | channel.addUser(user, uac.channels[key].UserModes[nick]); 387 | } else { 388 | logger('User %s have not been found on server', nick); 389 | } 390 | } 391 | } 392 | } 393 | 394 | update(data) { 395 | Object.assign(this, data); 396 | } 397 | 398 | toString() { 399 | return ``; 400 | } 401 | } 402 | 403 | /** 404 | * Map of {@link Network}, with helpers 405 | */ 406 | export class NetworkCollection extends Map { 407 | /** 408 | * Add an empty {@linkNetwork} identified by `networkId` to the collection 409 | * @param {number} networkId 410 | * @returns {module:network.Network} 411 | */ 412 | add(networkId) { 413 | networkId = parseInt(networkId, 10); 414 | const network = new Network(networkId); 415 | this.set(networkId, network); 416 | return network; 417 | } 418 | 419 | /** 420 | * Returns {@link IRCBuffer} corresponding to given `bufferId`, or `undefined` otherwise 421 | * @param {number} bufferId 422 | * @returns {?IRCBuffer} 423 | */ 424 | getBuffer(bufferId) { 425 | if (typeof bufferId !== 'number') return undefined; 426 | let buffer; 427 | for (let network of this.values()) { 428 | buffer = network.buffers.get(bufferId); 429 | if (buffer) return buffer; 430 | } 431 | return undefined; 432 | } 433 | 434 | /** 435 | * Delete the {@link IRCBuffer} object identified by `bufferId` from the networks 436 | * @param {number} bufferId 437 | */ 438 | deleteBuffer(bufferId) { 439 | if (typeof bufferId !== 'number') { 440 | logger('deleteBuffer:%O is not a number', bufferId); 441 | return; 442 | } 443 | const buffer = this.getBuffer(bufferId); 444 | if (buffer) { 445 | this.get(buffer.network).buffers.delete(bufferId); 446 | } 447 | } 448 | 449 | /** 450 | * Yields all buffers of all networks 451 | */ 452 | * buffers() { 453 | for (let network of this.values()) { 454 | for (let buffer of network.buffers.values()) { 455 | yield buffer; 456 | } 457 | } 458 | } 459 | 460 | /** 461 | * Returns `true` if buffer identified by `bufferId` exists 462 | * @param {number} bufferId 463 | */ 464 | hasBuffer(bufferId) { 465 | if (typeof bufferId !== 'number') { 466 | logger('hasBuffer:%O is not a number', bufferId); 467 | return false; 468 | } 469 | for (let network of this.values()) { 470 | if (network.hasBuffer(bufferId)) return true; 471 | } 472 | return false; 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | import EventEmitter from 'events'; 10 | import { types as qtypes, socket } from 'qtdatastream'; 11 | import debug from 'debug'; 12 | const logger = debug('libquassel:request'); 13 | import pkg from '../package.json' assert { type: 'json' }; 14 | 15 | import * as tls from 'tls'; 16 | import { Network, Server } from './network.js'; 17 | 18 | /** 19 | * @type {Object} 20 | * @property {number} Types.INVALID 21 | * @property {number} Types.SYNC 22 | * @property {number} Types.RPCCALL 23 | * @property {number} Types.INITREQUEST 24 | * @property {number} Types.INITDATA 25 | * @property {number} Types.HEARTBEAT 26 | * @property {number} Types.HEARTBEATREPLY 27 | */ 28 | export const Types = { 29 | INVALID: 0x00, 30 | SYNC: 0x01, 31 | RPCCALL: 0x02, 32 | INITREQUEST: 0x03, 33 | INITDATA: 0x04, 34 | HEARTBEAT: 0x05, 35 | HEARTBEATREPLY: 0x06 36 | }; 37 | 38 | /** 39 | * Decorator for SYNC methods 40 | * @protected 41 | */ 42 | function sync(className, functionName, ...datatypes) { 43 | const qsync = qtypes.QInt.from(Types.SYNC); 44 | const qclassName = qtypes.QByteArray.from(className); 45 | const qfunctionName = qtypes.QByteArray.from(functionName); 46 | return function decorator(target, _key, descriptor) { 47 | return { 48 | enumerable: false, 49 | configurable: false, 50 | writable: false, 51 | value: function(...args) { 52 | const [ id, ...data ] = descriptor.value.apply(this, args); 53 | this.qtsocket.write([ 54 | qsync, 55 | qclassName, 56 | qtypes.QByteArray.from(id), 57 | qfunctionName, 58 | ...data.map((value, index) => datatypes[index].from(value)) 59 | ]); 60 | } 61 | }; 62 | }; 63 | } 64 | 65 | /** 66 | * Decorator for RPC methods 67 | * @protected 68 | */ 69 | function rpc(functionName, ...datatypes) { 70 | const qrpc = qtypes.QInt.from(Types.RPCCALL); 71 | const qfunctionName = qtypes.QByteArray.from(`2${functionName}`); 72 | return function(target, _key, descriptor) { 73 | return { 74 | enumerable: false, 75 | configurable: false, 76 | writable: false, 77 | value: function(...args) { 78 | const data = descriptor.value.apply(this, args); 79 | this.qtsocket.write([ 80 | qrpc, 81 | qfunctionName, 82 | ...data.map((value, index) => datatypes[index].from(value)) 83 | ]); 84 | } 85 | }; 86 | }; 87 | } 88 | 89 | /** 90 | * Send commands to the core 91 | */ 92 | export class Core extends EventEmitter { 93 | constructor(options) { 94 | super(); 95 | this.options = options; 96 | this.useSSL = false; 97 | this.useCompression = false; 98 | this.qtsocket = null; 99 | this.duplex = null; 100 | } 101 | 102 | /** 103 | * Handle magic number response 104 | * @param {net.Duplex} duplex 105 | */ 106 | init(duplex) { 107 | this.duplex = duplex; 108 | this.duplex.once('data', data => { 109 | const ret = data.readUInt32BE(0); 110 | if (((ret >> 24) & 0x01) > 0) { 111 | this.useSSL = true; 112 | logger('Core supports SSL'); 113 | } 114 | 115 | if (((ret >> 24) & 0x02) > 0) { 116 | this.useCompression = true; 117 | logger('Core supports compression'); 118 | } 119 | 120 | // if (self.useCompression) { 121 | // const zlib = require('zlib'); 122 | // // Not working, don't know why yet 123 | // self.qtsocket = new qtdatastream.socket.Socket(self.client, function(buffer, next) { 124 | // zlib.inflate(buffer, next); 125 | // }, function(buffer, next) { 126 | // var deflate = zlib.createDeflate({flush: zlib.Z_SYNC_FLUSH}), buffers = []; 127 | // deflate.on('data', function(chunk) { 128 | // buffers.push(chunk); 129 | // }); 130 | 131 | // deflate.on('end', function() { 132 | // logger(buffers); 133 | // next(null, Buffer.concat(buffers)); 134 | // }); 135 | 136 | // deflate.end(buffer); 137 | // }); 138 | // } else { 139 | this.qtsocket = new socket.Socket(duplex); 140 | // } 141 | 142 | this.qtsocket 143 | .on('data', data => this.emit('data', data)) 144 | .on('close', () => this.emit('close')) 145 | .on('end', () => this.emit('end')) 146 | .on('error', e => this.emit('error', e)); 147 | 148 | this.sendClientInfo(this.useSSL, this.useCompression); 149 | }); 150 | } 151 | 152 | /** 153 | * Begins SSL handshake if necessary. 154 | * This method returns a Promise. 155 | * @param {function} callback 156 | */ 157 | finishClientInit(callback) { 158 | if (this.useSSL) { 159 | logger('SECURE'); 160 | const secureContext = tls.createSecureContext({ 161 | minVersion: 'TLSv1.2' 162 | }); 163 | const secureStream = tls.connect({ 164 | socket: this.duplex, 165 | rejectUnauthorized: false, 166 | secureContext: secureContext 167 | }); 168 | secureStream.once('secure', callback); 169 | this.qtsocket.setSocket(secureStream); 170 | } else { 171 | logger('PLAIN'); 172 | callback(); 173 | } 174 | } 175 | 176 | /** 177 | * Send heartbeat response 178 | * @param {boolean} reply 179 | */ 180 | heartBeat(reply) { 181 | const d = new Date(); 182 | const ms = d.getTime() - d.setHours(0, 0, 0, 0); 183 | const slist = [ 184 | reply ? Types.HEARTBEAT : Types.HEARTBEATREPLY, 185 | qtypes.QTime.from(ms) 186 | ]; 187 | logger('Sending heartbeat'); 188 | this.qtsocket.write(slist); 189 | } 190 | 191 | /** 192 | * Core Sync request - Backlogs 193 | * @param {number} bufferId 194 | * @param {number} [firstMsgId=-1] 195 | * @param {number} [lastMsgId=-1] 196 | * @param {?number} [maxAmount=backloglimit] 197 | */ 198 | @sync( 199 | 'BacklogManager', 200 | 'requestBacklog', 201 | qtypes.QUserType.get('BufferId'), 202 | qtypes.QUserType.get('MsgId'), 203 | qtypes.QUserType.get('MsgId'), 204 | qtypes.QInt, 205 | qtypes.QInt 206 | ) 207 | backlog(bufferId, firstMsgId = -1, lastMsgId = -1, maxAmount = undefined) { 208 | maxAmount = maxAmount === undefined ? this.options.backloglimit : maxAmount; 209 | logger('Sending backlog request'); 210 | return [ '', bufferId, firstMsgId, lastMsgId, maxAmount, 0 ]; 211 | } 212 | 213 | /** 214 | * Core Sync request - Connect the specified network 215 | * @param {number} networkId 216 | */ 217 | @sync('Network', 'requestConnect') 218 | connectNetwork(networkId) { 219 | logger('Sending connection request'); 220 | return [ String(networkId) ]; 221 | } 222 | 223 | /** 224 | * Core Sync request - Disconnect the specified network 225 | * @param {number} networkId 226 | */ 227 | @sync('Network', 'requestDisconnect') 228 | disconnectNetwork(networkId) { 229 | logger('Sending disconnection request'); 230 | return [ String(networkId) ]; 231 | } 232 | 233 | /** 234 | * Core Sync request - Update network information 235 | * @param {Number} networkId 236 | * @param {Network} network 237 | */ 238 | @sync('Network', 'requestSetNetworkInfo', qtypes.QClass) 239 | setNetworkInfo(networkId, network) { 240 | logger('Sending update request (Network)'); 241 | return [ String(networkId), network ]; 242 | } 243 | 244 | /** 245 | * Core Sync request - Mark buffer as read 246 | * @param {number} bufferId 247 | */ 248 | @sync('BufferSyncer', 'requestMarkBufferAsRead', qtypes.QUserType.get('BufferId')) 249 | markBufferAsRead(bufferId) { 250 | logger('Sending mark buffer as read request'); 251 | return [ '', bufferId ]; 252 | } 253 | 254 | /** 255 | * Core Sync request - Set all messages before messageId as read for specified buffer 256 | * @param {number} bufferId 257 | * @param {number} messageId 258 | */ 259 | @sync('BufferSyncer', 'requestSetLastSeenMsg', qtypes.QUserType.get('BufferId'), qtypes.QUserType.get('MsgId')) 260 | setLastMsgRead(bufferId, messageId) { 261 | logger('Sending last message read request'); 262 | return [ '', bufferId, messageId ]; 263 | } 264 | 265 | /** 266 | * Core Sync request - Mark a specified buffer line 267 | * @param {number} bufferId 268 | * @param {number} messageId 269 | */ 270 | @sync('BufferSyncer', 'requestSetMarkerLine', qtypes.QUserType.get('BufferId'), qtypes.QUserType.get('MsgId')) 271 | setMarkerLine(bufferId, messageId) { 272 | logger('Sending mark line request'); 273 | return [ '', bufferId, messageId ]; 274 | } 275 | 276 | /** 277 | * Core Sync request - Remove a buffer 278 | * @param {number} bufferId 279 | */ 280 | @sync('BufferSyncer', 'requestRemoveBuffer', qtypes.QUserType.get('BufferId')) 281 | removeBuffer(bufferId) { 282 | logger('Sending perm hide request'); 283 | return [ '', bufferId ]; 284 | } 285 | 286 | /** 287 | * Core Sync request - Merge bufferId2 into bufferId1 288 | * @param {number} bufferId1 289 | * @param {number} bufferId2 290 | */ 291 | @sync('BufferSyncer', 'requestMergeBuffersPermanently', qtypes.QUserType.get('BufferId'), qtypes.QUserType.get('BufferId')) 292 | mergeBuffersPermanently( bufferId1, bufferId2) { 293 | logger('Sending merge request'); 294 | return [ '', bufferId1, bufferId2 ]; 295 | } 296 | 297 | /** 298 | * Core Sync request - Rename a buffer 299 | * @param {number} bufferId 300 | * @param {string} newName 301 | */ 302 | @sync('BufferSyncer', 'requestMergeBuffersPermanently', qtypes.QUserType.get('BufferId'), qtypes.QString) 303 | renameBuffer(bufferId, newName) { 304 | logger('Sending rename buffer request'); 305 | return [ '', bufferId, newName ]; 306 | } 307 | 308 | /** 309 | * Core Sync request - Hide a buffer temporarily 310 | * @param {number} bufferViewId 311 | * @param {number} bufferId 312 | */ 313 | @sync('BufferViewConfig', 'requestRemoveBuffer', qtypes.QUserType.get('BufferId')) 314 | hideBufferTemporarily(bufferViewId, bufferId) { 315 | logger('Sending temp hide request'); 316 | return [ String(bufferViewId), bufferId ]; 317 | } 318 | 319 | /** 320 | * Core Sync request - Hide a buffer permanently 321 | * @param {number} bufferViewId 322 | * @param {number} bufferId 323 | */ 324 | @sync('BufferViewConfig', 'requestRemoveBufferPermanently', qtypes.QUserType.get('BufferId')) 325 | hideBufferPermanently(bufferViewId, bufferId) { 326 | logger('Sending perm hide request'); 327 | return [ String(bufferViewId), bufferId ]; 328 | } 329 | 330 | /** 331 | * Core Sync request - Unhide a buffer 332 | * @param {number} bufferViewId 333 | * @param {number} bufferId 334 | * @param {number} pos 335 | */ 336 | @sync('BufferViewConfig', 'requestAddBuffer', qtypes.QUserType.get('BufferId'), qtypes.QInt) 337 | unhideBuffer(bufferViewId, bufferId, pos) { 338 | logger('Sending unhide request'); 339 | return [ String(bufferViewId), bufferId, pos ]; 340 | } 341 | 342 | /** 343 | * Core Sync request - Create a new chat list 344 | * @experimental 345 | * @param {Object} data 346 | * @example 347 | * FIXME 348 | * createBufferView({ 349 | * sortAlphabetically: 1, 350 | * showSearch: 0, 351 | * networkId: 0, 352 | * minimumActivity: 0, 353 | * hideInactiveNetworks: 0, 354 | * hideInactiveBuffers: 0, 355 | * disableDecoration: 0, 356 | * bufferViewName: 'All Chats', 357 | * allowedBufferTypes: 15, 358 | * addNewBuffersAutomatically: 1, 359 | * TemporarilyRemovedBuffers: [], 360 | * RemovedBuffers: [], 361 | * BufferList: [] 362 | * }); 363 | */ 364 | @sync('BufferViewManager', 'requestCreateBufferView', qtypes.QMap) 365 | createBufferView(data) { 366 | logger('Sending create buffer view request'); 367 | return [ '', data ]; 368 | } 369 | 370 | /** 371 | * Core Sync request - Update ignoreList 372 | * @param {object} ignoreList 373 | */ 374 | @sync('IgnoreListManager', 'requestUpdate', qtypes.QMap) 375 | updateIgnoreListManager(ignoreList) { 376 | logger('Sending update request (IgnoreListManager)'); 377 | return [ '', ignoreList ]; 378 | } 379 | 380 | /** 381 | * Core Sync request - Update highlightRuleManager 382 | * @param {object} highlightRuleManager 383 | */ 384 | @sync('HighlightRuleManager', 'requestUpdate', qtypes.QMap) 385 | updateHighlightRuleManager(highlightRuleManager) { 386 | logger('Sending update request (HighlightRuleManager)'); 387 | return [ '', highlightRuleManager ]; 388 | } 389 | 390 | /** 391 | * Core Sync request - Update identity 392 | * @param {Number} identityId 393 | * @param {Identity} identity 394 | */ 395 | @sync('Identity', 'requestUpdate', qtypes.QClass) 396 | updateIdentity(identityId, identity) { 397 | logger('Sending update request (Identity)'); 398 | return [ String(identityId), identity ]; 399 | } 400 | 401 | /** 402 | * Core Sync request - Update aliases 403 | * @param {object} data see {@link toCoreObject} 404 | */ 405 | @sync('AliasManager', 'requestUpdate', qtypes.QMap) 406 | updateAliasManager(data) { 407 | logger('Sending update request (AliasManager)'); 408 | return [ '', data ]; 409 | } 410 | 411 | @sync('BufferSyncer', 'requestPurgeBufferIds') 412 | purgeBufferIds() { 413 | logger('Sending purge buffer ids request'); 414 | return []; 415 | } 416 | 417 | /** 418 | * Core RPC request - Remove an {@link Identity} 419 | * @param {number} identityId 420 | */ 421 | @rpc('removeIdentity(IdentityId)', qtypes.QUserType.get('IdentityId')) 422 | removeIdentity(identityId) { 423 | logger('Deleting identity'); 424 | return [ identityId ]; 425 | } 426 | 427 | /** 428 | * Core RPC request - Remove a {@link Network} 429 | * @param {number} networkId 430 | */ 431 | @rpc('removeNetwork(NetworkId)', qtypes.QUserType.get('NetworkId')) 432 | removeNetwork (networkId) { 433 | logger('Deleting network'); 434 | return [ networkId ]; 435 | } 436 | 437 | /** 438 | * Core RPC request - Send a user input to a specified buffer 439 | * @param {bufferInfo} bufferInfo 440 | * @param {String} message 441 | */ 442 | @rpc('sendInput(BufferInfo,QString)', qtypes.QUserType.get('BufferInfo'), qtypes.QString) 443 | sendMessage(bufferInfo, message) { 444 | logger('Sending message'); 445 | return [ bufferInfo, message ]; 446 | } 447 | 448 | /** 449 | * Core RPC request - Create a new {@link module:identity} 450 | * @param {module:identity} identity 451 | */ 452 | @rpc('createIdentity(Identity,QVariantMap)', qtypes.QUserType.get('Identity'), qtypes.QMap) 453 | createIdentity(identity) { 454 | logger('Creating identity'); 455 | return [ identity, {}]; 456 | } 457 | 458 | /** 459 | * Core RPC request - Create a new {@link module:network.Network} 460 | * @param {String} networkName 461 | * @param {number} identityId 462 | * @param {String|Object} initialServer - Server hostname or IP, or full Network::Server Object. Can also be undefined if options.ServerList is defined. 463 | * @param {String} [initialServer.Host=initialServer] 464 | * @param {String} [initialServer.Port="6667"] 465 | * @param {String} [initialServer.Password=""] 466 | * @param {boolean} [initialServer.UseSSL=true] 467 | * @param {number} [initialServer.sslVersion=0] 468 | * @param {boolean} [initialServer.UseProxy=false] 469 | * @param {number} [initialServer.ProxyType=0] 470 | * @param {String} [initialServer.ProxyHost=""] 471 | * @param {String} [initialServer.ProxyPort=""] 472 | * @param {String} [initialServer.ProxyUser=""] 473 | * @param {String} [initialServer.ProxyPass=""] 474 | * @param {Object} [options] 475 | * @param {String} [options.codecForServer=""] 476 | * @param {String} [options.codecForEncoding=""] 477 | * @param {String} [options.codecForDecoding=""] 478 | * @param {boolean} [options.useRandomServer=false] 479 | * @param {String[]} [options.perform=[]] 480 | * @param {Object[]} [options.ServerList=[]] 481 | * @param {boolean} [options.useAutoIdentify=false] 482 | * @param {String} [options.autoIdentifyService="NickServ"] 483 | * @param {String} [options.autoIdentifyPassword=""] 484 | * @param {boolean} [options.useSasl=false] 485 | * @param {String} [options.saslAccount=""] 486 | * @param {String} [options.saslPassword=""] 487 | * @param {boolean} [options.useAutoReconnect=true] 488 | * @param {number} [options.autoReconnectInterval=60] 489 | * @param {number} [options.autoReconnectRetries=20] 490 | * @param {boolean} [options.unlimitedReconnectRetries=false] 491 | * @param {boolean} [options.rejoinChannels=true] 492 | * @param {boolean} [options.useCustomMessageRate=false] 493 | * @param {boolean} [options.unlimitedMessageRate=false] 494 | * @param {number} [options.msgRateMessageDelay=2200] 495 | * @param {number} [options.msgRateBurstSize=5] 496 | */ 497 | @rpc('createNetwork(NetworkInfo,QStringList)', qtypes.QClass, qtypes.QStringList) 498 | createNetwork(networkName, identityId, initialServer, options = {}) { 499 | const network = new Network(-1, networkName); 500 | network.identityId = identityId; 501 | network.update(options); 502 | if (typeof initialServer === 'string') { 503 | initialServer = { 504 | host: initialServer 505 | }; 506 | } 507 | network.ServerList.push(new Server(initialServer)); 508 | logger('Creating network'); 509 | return [ network, []]; 510 | } 511 | 512 | /** 513 | * Sends an initialization request to quasselcore for specified `classname` and `objectname` 514 | * @param {String} classname 515 | * @param {String} objectname 516 | * @example 517 | * quassel.core.sendInitRequest("IrcUser", "1/randomuser"); 518 | */ 519 | sendInitRequest(classname, objectname = '') { 520 | const initRequest = [ 521 | qtypes.QUInt.from(Types.INITREQUEST), 522 | qtypes.QString.from(classname), 523 | qtypes.QString.from(objectname) 524 | ]; 525 | this.qtsocket.write(initRequest); 526 | } 527 | 528 | /** 529 | * Sends client information to the core 530 | * @param {boolean} useSSL 531 | * @param {boolean} useCompression - Not supported 532 | * @protected 533 | */ 534 | sendClientInfo(useSSL, useCompression){ 535 | const smap = { 536 | 'ClientDate': '', 537 | 'UseSsl': useSSL, 538 | 'ClientVersion': `${pkg.name} ${pkg.version}`, 539 | 'UseCompression': useCompression, 540 | 'MsgType': 'ClientInit', 541 | 'ProtocolVersion': 10 542 | }; 543 | logger('Sending client informations'); 544 | this.qtsocket.write(smap); 545 | } 546 | 547 | /** 548 | * Setup core 549 | * @param {String} backend 550 | * @param {String} adminuser 551 | * @param {String} adminpassword 552 | * @param {Object} [properties={}] 553 | */ 554 | setupCore(backend, adminuser, adminpassword, properties = {}) { 555 | const obj = { 556 | SetupData: { 557 | ConnectionProperties: properties, 558 | Backend: backend, 559 | AdminUser: adminuser, 560 | AdminPasswd: adminpassword 561 | }, 562 | MsgType: 'CoreSetupData' 563 | }; 564 | 565 | this.qtsocket.write(obj); 566 | } 567 | 568 | /** 569 | * Send login request to the core 570 | * @param {String} user 571 | * @param {String} password 572 | */ 573 | login(user, password) { 574 | const obj = { 575 | 'MsgType': 'ClientLogin', 576 | 'User': user, 577 | 'Password': password 578 | }; 579 | this.qtsocket.write(obj); 580 | } 581 | 582 | /** 583 | * Send magic number to the core 584 | */ 585 | connect() { 586 | let magic = 0x42b33f00; 587 | // magic | 0x01 Encryption 588 | // magic | 0x02 Compression 589 | if (this.options.securecore) { 590 | magic |= 0x01; 591 | } 592 | 593 | // At this point `duplex` must already be connected 594 | const bufs = [ 595 | qtypes.QUInt.from(magic).toBuffer(), 596 | qtypes.QUInt.from(0x01).toBuffer(), 597 | qtypes.QUInt.from(2147483648).toBuffer() 598 | ]; 599 | this.duplex.write(Buffer.concat(bufs)); 600 | } 601 | 602 | /** 603 | * Disconnect the client from the core 604 | */ 605 | disconnect() { 606 | this.duplex.end(); 607 | this.duplex.destroy(); 608 | } 609 | } 610 | -------------------------------------------------------------------------------- /test/manual.js: -------------------------------------------------------------------------------- 1 | import { Client } from '../src/libquassel.js'; 2 | import inquirer from 'inquirer'; 3 | import net from 'net'; 4 | import ansiStyles from 'ansi-styles'; 5 | 6 | 7 | const ACTIONS = [ 8 | { name: "Disconnect Network", value: "network-disconnect" }, 9 | { name: "Connect Network", value: "network-connect" }, 10 | { name: "Create network", value: "network-create" }, 11 | { name: "Remove network", value: "network-remove" }, 12 | { name: "Update network", value: "network-update" }, 13 | { name: "Mark buffer as read", value: "buffer-mark-read" }, 14 | { name: "Mark last line of a buffer", value: "buffer-mark-last-line" }, 15 | { name: "Hide buffer permanently", value: "buffer-hide-perm" }, 16 | { name: "Hide buffer temporarily", value: "buffer-hide-temp" }, 17 | { name: "Unhide buffer", value: "buffer-unhide" }, 18 | { name: "Remove buffer", value: "buffer-remove" }, 19 | { name: "Merge buffers request", value: "buffer-merge" }, 20 | { name: "Rename buffer", value: "buffer-rename" }, 21 | { name: "Request 20 more backlogs for a buffer", value: "backlogs" }, 22 | { name: "Send a message", value: "send-message" }, 23 | { name: "Update ignoreList", value: "ignorelist" }, 24 | { name: "Create identity", value: "identity-create" }, 25 | { name: "Remove identity", value: "identity-remove" }, 26 | { name: "Update identity", value: "identity-update" }, 27 | { name: "Setup core", value: "setup" } 28 | ]; 29 | 30 | function ask() { 31 | return inquirer.prompt([{ 32 | type: 'list', 33 | name: 'action', 34 | message: 'Choose your action', 35 | choices: ACTIONS 36 | }]); 37 | } 38 | 39 | function ask_creds() { 40 | return inquirer.prompt([{ 41 | type: 'input', 42 | name: 'username', 43 | message: 'username' 44 | },{ 45 | type: 'password', 46 | name: 'password', 47 | message: 'password' 48 | }]); 49 | } 50 | 51 | function red_if_undefined(s) { 52 | if (s === undefined) { 53 | return `${ansiStyles.red.open}${s}${ansiStyles.red.close}`; 54 | } 55 | return s; 56 | } 57 | 58 | function log(key, ...args) { 59 | const colors = [ 'grey', 'green', 'blue', 'magenta', 'cyan' ]; 60 | const indice = [...key].map(x => x.charCodeAt(0)).reduce((x, y) => x + y) % 5; 61 | const stylekey = ansiStyles[colors[indice]]; 62 | console.log(`${stylekey.open}${key}${stylekey.close} `); 63 | console.log(...args.map(x => red_if_undefined(x))); 64 | } 65 | 66 | const socket = net.createConnection({ 67 | host: "localhost", 68 | port: 4242 69 | }); 70 | 71 | const quassel = new Client((next) => { 72 | ask_creds().then((creds) => { 73 | next(creds.username, creds.password); 74 | }); 75 | }); 76 | 77 | quassel.on('buffer.backlog', function(bufferId, messageIds) { 78 | const buffer = quassel.networks.getBuffer(bufferId); 79 | log('buffer.backlog', '%s: %d messages', buffer, buffer.messages.size); 80 | }); 81 | 82 | quassel.on('network.init', function(networkId) { 83 | const network = quassel.networks.get(networkId); 84 | log('network.init', '%s', network); 85 | }); 86 | 87 | quassel.on('coreinfoinit', function(coreinfo) { 88 | log('coreinfoinit', coreinfo); 89 | }); 90 | 91 | quassel.on('coreinfo', function(coreinfo) { 92 | log('coreinfo', coreinfo); 93 | }); 94 | 95 | quassel.on('network.addbuffer', function(networkId, bufferId) { 96 | const network = quassel.networks.get(networkId); 97 | const buffer = network.buffers.get(bufferId); 98 | log('network.addbuffer', '%s %s', network, buffer); 99 | }); 100 | 101 | quassel.on('network.latency', function(networkId, latency) { 102 | const network = quassel.networks.get(networkId); 103 | log('network.latency', '%s: %d', network, latency); 104 | }); 105 | 106 | quassel.on('network.adduser', function(networkId, nick) { 107 | const network = quassel.networks.get(networkId); 108 | log('network.adduser', '%s %s', network, nick); 109 | }); 110 | 111 | quassel.on('network.connectionstate', function(networkId, state) { 112 | const network = quassel.networks.get(networkId); 113 | log('network.connectionstate', '%s %s', network, state); 114 | }); 115 | 116 | quassel.on('network.connected', function(networkId) { 117 | const network = quassel.networks.get(networkId); 118 | log('network.connected', '%s', network); 119 | }); 120 | 121 | quassel.on('network.disconnected', function(networkId) { 122 | const network = quassel.networks.get(networkId); 123 | log('network.disconnected', '%s', network); 124 | }); 125 | 126 | quassel.on('network.mynick', function(networkId, nick) { 127 | const network = quassel.networks.get(networkId); 128 | log('network.mynick', '%s: %s', network, nick); 129 | }); 130 | 131 | quassel.on('network.networkname', function(networkId, name) { 132 | const network = quassel.networks.get(networkId); 133 | log('network.networkname', '%s: %s', network, name); 134 | }); 135 | 136 | quassel.on('network.server', function(networkId, server) { 137 | const network = quassel.networks.get(networkId); 138 | log('network.server', '%s: %s', network, server); 139 | }); 140 | 141 | quassel.on('buffer.message', function(bufferId, messageId) { 142 | const buffer = quassel.networks.getBuffer(bufferId); 143 | const message = buffer.messages.get(messageId); 144 | log('buffer.message', '[%s] %s %s', quassel.ignoreList.matches(message, quassel.networks) ? 'h' : 'v', buffer, message); 145 | }); 146 | 147 | quassel.on('buffer.read', function(bufferId) { 148 | const buffer = quassel.networks.getBuffer(bufferId); 149 | log('buffer.read', '%s', buffer); 150 | }); 151 | 152 | quassel.on('buffer.remove', function(bufferId) { 153 | const buffer = quassel.networks.getBuffer(bufferId); 154 | log('buffer.remove', '%s', buffer); 155 | }); 156 | 157 | quassel.on('buffer.rename', function(bufferId, newName) { 158 | const buffer = quassel.networks.getBuffer(bufferId); 159 | log('buffer.remove', '%s: %s', buffer, newName); 160 | }); 161 | 162 | quassel.on('buffer.merge', function(bufferId1, bufferId2) { 163 | const buffer = quassel.networks.getBuffer(bufferId1); 164 | log('buffer.merge', '%s -> %s', bufferId2, buffer); 165 | }); 166 | 167 | quassel.on('buffer.lastseen', function(bufferId, messageId) { 168 | const buffer = quassel.networks.getBuffer(bufferId); 169 | log('buffer.lastseen', '%s: #%d', buffer, messageId); 170 | }); 171 | 172 | quassel.on('buffer.markerline', function(bufferId, messageId) { 173 | const buffer = quassel.networks.getBuffer(bufferId); 174 | log('buffer.markerline', '%s above %d', buffer, messageId); 175 | }); 176 | 177 | quassel.on('buffer.activate', function(bufferId) { 178 | const buffer = quassel.networks.getBuffer(bufferId); 179 | log('buffer.activate', '%s', buffer); 180 | }); 181 | 182 | quassel.on('buffer.deactivate', function(bufferId) { 183 | const buffer = quassel.networks.getBuffer(bufferId); 184 | log('buffer.deactivate', '%s', buffer); 185 | }); 186 | 187 | quassel.on('bufferview.bufferunhide', function(bufferViewId, bufferId) { 188 | const bufferView = quassel.bufferViews.get(bufferViewId); 189 | log('bufferview.bufferunhide', '%s #%d', bufferView, bufferId); 190 | }); 191 | 192 | quassel.on('bufferview.bufferhidden', function(bufferViewId, bufferId, type) { 193 | const bufferView = quassel.bufferViews.get(bufferViewId); 194 | // type can be either "temp" or "perm" 195 | log('bufferview.bufferhidden', '(%s) %s #%d', type, bufferView, bufferId); 196 | }); 197 | 198 | quassel.on('bufferview.orderchanged', function(bufferViewId) { 199 | const bufferView = quassel.bufferViews.get(bufferViewId); 200 | log('bufferview.orderchanged', '%s', bufferView); 201 | }); 202 | 203 | quassel.on('user.quit', function(networkId, username) { 204 | const network = quassel.networks.get(networkId); 205 | log('user.quit', '%s: %s', network, username); 206 | }); 207 | 208 | quassel.on('user.part', function(networkId, username, bufferId) { 209 | const network = quassel.networks.get(networkId); 210 | const buffer = quassel.networks.getBuffer(bufferId); 211 | log('user.quit', '%s %s: %s', network, buffer, username); 212 | }); 213 | 214 | quassel.on('user.away', function(networkId, username, isAway) { 215 | const network = quassel.networks.get(networkId); 216 | log('user.away', '(%s) %s: %s', isAway, network, username); 217 | }); 218 | 219 | quassel.on('user.realname', function(networkId, username, realname) { 220 | const network = quassel.networks.get(networkId); 221 | log('user.realname', '%s: %s is %s', network, username, realname); 222 | }); 223 | 224 | quassel.on('channel.join', function(bufferId, nick) { 225 | const buffer = quassel.networks.getBuffer(bufferId); 226 | log('channel.join', '%s: %s', buffer, nick); 227 | }); 228 | 229 | quassel.on('channel.addusermode', function(bufferId, nick, mode) { 230 | const buffer = quassel.networks.getBuffer(bufferId); 231 | log('channel.addusermode', '%s: %s modes +%s', buffer, nick, mode); 232 | }); 233 | 234 | quassel.on('channel.removeusermode', function(bufferId, nick, mode) { 235 | const buffer = quassel.networks.getBuffer(bufferId); 236 | log('channel.removeusermode', '%s: %s modes -%s', buffer, nick, mode); 237 | }); 238 | 239 | quassel.on('channel.topic', function(bufferId, topic) { 240 | const buffer = quassel.networks.getBuffer(bufferId); 241 | log('channel.topic', '%s: %s', buffer, topic); 242 | }); 243 | 244 | quassel.on('ignorelist', function(ignorelist) { 245 | log('ignorelist', '%s', ignorelist); 246 | }); 247 | 248 | quassel.on('network.new', function(networkId) { 249 | const network = quassel.networks.get(networkId); 250 | log('network.new', '%s', network); 251 | }); 252 | 253 | quassel.on('network.remove', function(networkId) { 254 | const network = quassel.networks.get(networkId); 255 | log('network.remove', '%s', network); 256 | }); 257 | 258 | quassel.on('identity.new', function(identityId) { 259 | const identity = quassel.identities.get(identityId); 260 | log('identity.new', '%s', identity); 261 | }); 262 | 263 | quassel.on('identity.remove', function(identityId) { 264 | log('identity.remove', identityId); 265 | }); 266 | 267 | quassel.on('identities.init', function(identities) { 268 | const ids = []; 269 | for (let identity of identities.values()) { 270 | ids.push(identity.toString()); 271 | } 272 | log('identities.init', ids); 273 | }); 274 | 275 | quassel.on('setup', function(data) { 276 | log('setup', data); 277 | }); 278 | 279 | quassel.on('init', function(data) { 280 | log('init'); 281 | }); 282 | 283 | 284 | // function echoBackends() { 285 | // for (let storageBackend of quassel.coreInfo.StorageBackends) { 286 | // console.log(`${storageBackend.DisplayName}`); 287 | // } 288 | // } 289 | 290 | // function echoIdentities() { 291 | // quassel.identities.forEach(function(value, key) { 292 | // console.log(value.identityName + ": " + key); 293 | // }); 294 | // } 295 | 296 | // function echoBufferList() { 297 | // quassel.getNetworksMap().forEach(function(val, key){ 298 | // console.log(val.networkName + " :"); 299 | // var buffs = []; 300 | // val.getBufferMap().forEach(function(val2, key2){ 301 | // buffs.push(val2.name + ": " + val2.id); 302 | // }); 303 | // console.log(buffs.join(", ")); 304 | // }); 305 | // } 306 | 307 | // function echoNetworkList() { 308 | // quassel.getNetworksMap().forEach(function(val, key){ 309 | // console.log(val.networkName + " : " + val.networkId); 310 | // }); 311 | // } 312 | 313 | 314 | // } else { 315 | 316 | // var schemaActionChoices = [{ 317 | // name: 'id', 318 | // description: 'Choose action', 319 | // enum: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20'], 320 | // required: true 321 | // }], schemaBuffer = [{ 322 | // name: 'id', 323 | // type: 'string', 324 | // description: 'Buffer ID', 325 | // required: true 326 | // }], schemaMessage = [{ 327 | // name: 'id', 328 | // type: 'string', 329 | // description: 'Buffer ID', 330 | // required: true 331 | // }, { 332 | // name: 'message', 333 | // description: 'Message', 334 | // type: 'string', 335 | // required: true 336 | // }], schemaConnect = [{ 337 | // name: 'id', 338 | // type: 'string', 339 | // description: 'Choose a networkId', 340 | // required: true 341 | // }], schemaMerge = [{ 342 | // name: 'id1', 343 | // type: 'string', 344 | // description: 'Buffer ID 1', 345 | // required: true 346 | // },{ 347 | // name: 'id2', 348 | // type: 'string', 349 | // description: 'Buffer ID 2', 350 | // required: true 351 | // }], schemaName = [{ 352 | // name: 'id', 353 | // type: 'string', 354 | // description: 'Name', 355 | // required: true 356 | // }], schemaId = [{ 357 | // name: 'id', 358 | // type: 'number', 359 | // description: 'ID', 360 | // required: true 361 | // }], schemaCoreSetup = [{ 362 | // name: 'backend', 363 | // type: 'number', 364 | // description: 'Storage backend', 365 | // required: true 366 | // }, { 367 | // name: 'adminuser', 368 | // type: 'string', 369 | // description: 'Admin user', 370 | // required: true 371 | // }, { 372 | // name: 'adminpassword', 373 | // type: 'string', 374 | // hidden: true, 375 | // description: 'Admin user', 376 | // required: true 377 | // }], schemaNewName = [{ 378 | // name: 'id', 379 | // type: 'string', 380 | // description: 'Buffer ID', 381 | // required: true 382 | // }, { 383 | // name: 'name', 384 | // description: 'New name', 385 | // type: 'string', 386 | // required: true 387 | // }]; 388 | 389 | // var p = function() { 390 | // echoActionChoices(); 391 | 392 | // pprompt.get(schemaActionChoices, function (err, result) { 393 | // if (err) console.log(err); 394 | // else { 395 | // switch(result.id) { 396 | // case '1': 397 | // // Disconnect Network 398 | // echoNetworkList(); 399 | // pprompt.get(schemaConnect, function (err2, result2) { 400 | // if (err2) console.log(err2); 401 | // else { 402 | // quassel.requestDisconnectNetwork(result2.id); 403 | // setTimeout(p, 1); 404 | // } 405 | // }); 406 | // break; 407 | // case '2': 408 | // // Connect Network 409 | // echoNetworkList(); 410 | // pprompt.get(schemaConnect, function (err2, result2) { 411 | // if (err2) console.log(err2); 412 | // else { 413 | // quassel.requestConnectNetwork(result2.id); 414 | // setTimeout(p, 1); 415 | // } 416 | // }); 417 | // break; 418 | // case '3': 419 | // // Mark buffer as read 420 | // echoBufferList(); 421 | // pprompt.get(schemaBuffer, function (err2, result2) { 422 | // if (err2) console.log(err2); 423 | // else { 424 | // var ids = quassel.getNetworks().findBuffer(parseInt(result2.id, 10)).messages.keys(); 425 | // var max = Math.max.apply(null, ids); 426 | // quassel.requestSetLastMsgRead(result2.id, max); 427 | // quassel.requestMarkBufferAsRead(result2.id); 428 | // setTimeout(p, 1); 429 | // } 430 | // }); 431 | // break; 432 | // case '4': 433 | // // Mark last line of a buffer 434 | // echoBufferList(); 435 | // pprompt.get(schemaBuffer, function (err2, result2) { 436 | // if (err2) console.log(err2); 437 | // else { 438 | // var ids = quassel.getNetworks().findBuffer(parseInt(result2.id, 10)).messages.keys(); 439 | // var max = Math.max.apply(null, ids); 440 | // quassel.requestSetMarkerLine(result2.id, max); 441 | // setTimeout(p, 1); 442 | // } 443 | // }); 444 | // break; 445 | // case '5': 446 | // // Hide buffer permanently 447 | // echoBufferList(); 448 | // pprompt.get(schemaBuffer, function (err2, result2) { 449 | // if (err2) console.log(err2); 450 | // else { 451 | // quassel.requestHideBufferPermanently(result2.id); 452 | // setTimeout(p, 1); 453 | // } 454 | // }); 455 | // break; 456 | // case '6': 457 | // // Hide buffer temporarily 458 | // echoBufferList(); 459 | // pprompt.get(schemaBuffer, function (err2, result2) { 460 | // if (err2) console.log(err2); 461 | // else { 462 | // quassel.requestHideBufferTemporarily(result2.id); 463 | // setTimeout(p, 1); 464 | // } 465 | // }); 466 | // break; 467 | // case '7': 468 | // // Unhide buffer 469 | // echoBufferList(); 470 | // pprompt.get(schemaBuffer, function (err2, result2) { 471 | // if (err2) console.log(err2); 472 | // else { 473 | // quassel.requestUnhideBuffer(result2.id); 474 | // setTimeout(p, 1); 475 | // } 476 | // }); 477 | // break; 478 | // case '8': 479 | // //Remove buffer 480 | // echoBufferList(); 481 | // pprompt.get(schemaBuffer, function (err2, result2) { 482 | // if (err2) console.log(err2); 483 | // else { 484 | // quassel.requestRemoveBuffer(result2.id); 485 | // setTimeout(p, 1); 486 | // } 487 | // }); 488 | // break; 489 | // case '9': 490 | // // Request 20 more backlogs for a buffer 491 | // echoBufferList(); 492 | // pprompt.get(schemaBuffer, function (err2, result2) { 493 | // if (err2) console.log(err2); 494 | // else { 495 | // var ids = quassel.getNetworks().findBuffer(parseInt(result2.id, 10)).messages.keys(); 496 | // var min = Math.min.apply(null, ids); 497 | // quassel.once('buffer.backlog', function(bufferId, messageIds) { 498 | // var buf = quassel.getNetworks().findBuffer(bufferId); 499 | // console.log(buf.name + " : " + buf.messages.count() + " total messages fetched"); 500 | // }); 501 | // quassel.requestBacklog(result2.id, -1, min, 20); 502 | // setTimeout(p, 1); 503 | // } 504 | // }); 505 | // break; 506 | // case '10': 507 | // // Send a message 508 | // echoBufferList(); 509 | // pprompt.get(schemaMessage, function (err2, result2) { 510 | // if (err2) console.log(err2); 511 | // else { 512 | // quassel.sendMessage(result2.id, result2.message); 513 | // setTimeout(p, 1); 514 | // } 515 | // }); 516 | // break; 517 | // case '11': 518 | // // Send merge buffers requests 519 | // echoBufferList(); 520 | // pprompt.get(schemaMerge, function (err2, result2) { 521 | // if (err2) console.log(err2); 522 | // else { 523 | // quassel.requestMergeBuffersPermanently(result2.id1, result2.id2); 524 | // setTimeout(p, 1); 525 | // } 526 | // }); 527 | // break; 528 | // case '12': 529 | // // Send update (ignoreList) request 530 | // quassel.requestUpdate(quassel.ignoreList.export()); 531 | // setTimeout(p, 1); 532 | // break; 533 | // case '13': 534 | // // Send create identity request 535 | // pprompt.get(schemaName, function (err2, result2) { 536 | // if (err2) console.log(err2); 537 | // else { 538 | // quassel.createIdentity(result2.id); 539 | // setTimeout(p, 1); 540 | // } 541 | // }); 542 | // break; 543 | // case '14': 544 | // // Send remove identity request 545 | // echoIdentities(); 546 | // pprompt.get(schemaId, function (err2, result2) { 547 | // if (err2) console.log(err2); 548 | // else { 549 | // if (result2.id > 1 && quassel.identities.has(result2.id)) { 550 | // quassel.removeIdentity(result2.id); 551 | // setTimeout(p, 1); 552 | // } 553 | // } 554 | // }); 555 | // break; 556 | // case '15': 557 | // // Send create network request 558 | // pprompt.get(schemaName, function (err2, result2) { 559 | // if (err2) console.log(err2); 560 | // else { 561 | // quassel.createNetwork(result2.id, 1, "test.test.test"); 562 | // setTimeout(p, 1); 563 | // } 564 | // }); 565 | // break; 566 | // case '16': 567 | // // Send remove network request 568 | // echoNetworkList(); 569 | // pprompt.get(schemaId, function (err2, result2) { 570 | // if (err2) console.log(err2); 571 | // else { 572 | // quassel.removeNetwork(result2.id); 573 | // setTimeout(p, 1); 574 | // } 575 | // }); 576 | // break; 577 | // case '17': 578 | // // Send update identity request 579 | // echoIdentities(); 580 | // pprompt.get(schemaId, function (err2, result2) { 581 | // if (err2) console.log(err2); 582 | // else { 583 | // if (result2.id > 1 && quassel.identities.has(result2.id)) { 584 | // var identity = quassel.identities.get(result2.id); 585 | // identity.identityName += "_bis"; 586 | // quassel.requestUpdateIdentity(result2.id, identity); 587 | // setTimeout(p, 1); 588 | // } 589 | // } 590 | // }); 591 | // break; 592 | // case '18': 593 | // // Send update network request 594 | // echoNetworkList(); 595 | // pprompt.get(schemaId, function (err2, result2) { 596 | // if (err2) console.log(err2); 597 | // else { 598 | // var network = quassel.networks.get(result2.id); 599 | // network.networkName += "_bis"; 600 | // quassel.requestSetNetworkInfo(result2.id, network); 601 | // setTimeout(p, 1); 602 | // } 603 | // }); 604 | // break; 605 | // case '19': 606 | // // Setup core 607 | // echoBackends(); 608 | // pprompt.get(schemaCoreSetup, function (err2, result2) { 609 | // if (err2) console.log(err2); 610 | // else { 611 | // quassel.setupCore(quassel.coreInfo.StorageBackends[result2.backend].DisplayName, result2.adminuser, result2.adminpassword); 612 | // setTimeout(p, 1); 613 | // } 614 | // }); 615 | // break; 616 | // case '20': 617 | // // Rename buffer 618 | // echoBufferList(); 619 | // pprompt.get(schemaNewName, function (err2, result2) { 620 | // if (err2) console.log(err2); 621 | // else { 622 | // quassel.requestRenameBuffer(result2.id, result2.name); 623 | // setTimeout(p, 1); 624 | // } 625 | // }); 626 | // break; 627 | // default: 628 | // console.log('Wrong choice'); 629 | // setTimeout(p, 1); 630 | // } 631 | // } 632 | // }); 633 | // }; 634 | 635 | // var bufTimeout; 636 | 637 | // quassel.once('network.addbuffer', function(network, bufferId) { 638 | // clearTimeout(bufTimeout); 639 | // bufTimeout = setTimeout(function(){ 640 | // p(); 641 | // }, 1000); 642 | // }); 643 | 644 | // quassel.once('setup', function(network, bufferId) { 645 | // p(); 646 | // }); 647 | // } 648 | quassel.connect(socket); 649 | -------------------------------------------------------------------------------- /src/libquassel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * libquassel 3 | * https://github.com/magne4000/node-libquassel 4 | * 5 | * Copyright (c) 2017 Joël Charles 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | import './usertypes.js'; // register usertypes first 10 | import EventEmitter from 'events'; 11 | import debug from 'debug'; 12 | const logger = debug('libquassel:main'); 13 | 14 | import { Types as RequestTypes, Core } from './request.js'; 15 | import { NetworkCollection } from './network.js'; 16 | import { IRCBuffer } from './buffer.js'; 17 | import IRCUser from './user.js'; 18 | import Identity from './identity.js'; 19 | import BufferView from './bufferview.js'; 20 | import { Types as MessageTypes, HighlightModes } from './message.js'; 21 | import * as alias from './alias.js'; 22 | import * as ignore from './ignore.js'; 23 | import * as highlight from './highlight.js'; 24 | 25 | /** 26 | * @type {Object} 27 | * @property {number} Features.SYNCHRONIZEDMARKERLINE 28 | * @property {number} Features.SASLAUTHENTICATION 29 | * @property {number} Features.SASLEXTERNAL 30 | * @property {number} Features.HIDEINACTIVENETWORKS 31 | * @property {number} Features.PASSWORDCHANGE 32 | * @property {number} Features.CAPNEGOTIATION 33 | * @property {number} Features.VERIFYSERVERSSL 34 | * @property {number} Features.CUSTOMRATELIMITS 35 | * @property {number} Features.NUMFEATURES 36 | */ 37 | export const Features = { 38 | SYNCHRONIZEDMARKERLINE: 0x0001, 39 | SASLAUTHENTICATION: 0x0002, 40 | SASLEXTERNAL: 0x0004, 41 | HIDEINACTIVENETWORKS: 0x0008, 42 | PASSWORDCHANGE: 0x0010, 43 | CAPNEGOTIATION: 0x0020, // IRCv3 capability negotiation, account tracking 44 | VERIFYSERVERSSL: 0x0040, // IRC server SSL validation 45 | CUSTOMRATELIMITS: 0x0080, // IRC server custom message rate limits 46 | // DCCFILETRANSFER: 0x0100, // DCC file transfer support (forcefully disabled for now) 47 | // AWAYFORMATTIMESTAMP: 0x0200, // Timestamp formatting in away (e.g. %%hh:mm%%) 48 | // AUTHENTICATORS: 0x0400, // Whether or not the core supports auth backends. 49 | BUFFERACTIVITYSYNC: 0x0800, // Sync buffer activity status 50 | CORESIDEHIGHLIGHTS: 0x1000, // Core-Side highlight configuration and matching 51 | // SENDERPREFIXES: 0x2000, // Show prefixes for senders in backlog 52 | // REMOTEDISCONNECT: 0x4000, // Allow this peer to be remotely disconnected 53 | NUMFEATURES: 0x0800 54 | }; 55 | 56 | /** 57 | * @typedef {function(user: string, password: string)} loginCallback 58 | */ 59 | 60 | /** 61 | * Main class to interact with Quassel instance 62 | * @example 63 | * import { Client } from 'libquassel'; 64 | * const net = require('net'); 65 | * 66 | * const quassel = new Client(function(next) { 67 | * next("user", "password"); 68 | * }); 69 | * const socket = net.createConnection({ 70 | * host: "localhost", 71 | * port: 4242 72 | * }); 73 | * 74 | * quassel.connect(socket); 75 | */ 76 | export class Client extends EventEmitter { 77 | /** 78 | * @param {function(next: loginCallback)} loginCallback 79 | * @param {Object} [options] Allows optionnal parameters 80 | * @param {number} [options.initialbackloglimit=options.backloglimit] number of backlogs to request per buffer at connection 81 | * @param {number} [options.backloglimit=100] number of backlogs to request per buffer after connection 82 | * @param {boolean} [options.securecore=true] Use SSL to connect to the core (if the core allows it) 83 | * @param {number} [options.highlightmode=0x02] Choose how highlights on nicks works. Defaults to only highlight a message if current nick is present. 84 | */ 85 | constructor(loginCallback, options = {}) { 86 | super(); 87 | /** @type {Object} options */ 88 | this.options = {}; 89 | this.options.backloglimit = parseInt(options.backloglimit || 100, 10); 90 | this.options.initialbackloglimit = parseInt(options.initialbackloglimit || this.options.backloglimit, 10); 91 | this.options.highlightmode = (typeof options.highlightmode === 'number') ? options.highlightmode : HighlightModes.CURRENTNICK; 92 | this.options.securecore = options.securecore !== false; 93 | /** @type {Core} */ 94 | this.core = new Core(this.options); 95 | /** @type {NetworkCollection} */ 96 | this.networks = new NetworkCollection(); 97 | /** @type {Map} */ 98 | this.identities = new Map(); 99 | /** @type {IgnoreList} */ 100 | this.ignoreList = new ignore.IgnoreList(); 101 | /** @type {HighlightRuleManager} */ 102 | this.highlightRuleManager = new highlight.HighlightRuleManager(); 103 | /** @type {AliasItem[]} */ 104 | this.aliases = []; 105 | /** @type {Map} */ 106 | this.bufferViews = new Map(); 107 | /** @type {?number} */ 108 | this.heartbeatInterval = null; 109 | /** @type {boolean} */ 110 | this.useCompression = false; 111 | /** @type {?boolean} */ 112 | this.connected = null; 113 | /** @type {?Object} */ 114 | this.coreInfo = null; 115 | /** @type {?Object} */ 116 | this.coreData = null; 117 | /** @type {function(next: loginCallback)} */ 118 | this.loginCallback = loginCallback; 119 | 120 | if (typeof loginCallback !== 'function') { 121 | throw new Error('loginCallback parameter is mandatory and must be a function'); 122 | } 123 | 124 | this.core.on('data', data => this.dispatch(data)); 125 | this.core.on('error', err => this.emit('error', err)); 126 | } 127 | 128 | /** 129 | * Handles quasselcore messages that possesses a `MsgType` attribute 130 | * @param {Object} obj 131 | * @emits {Event:coreinfoinit} 132 | * @emits {Event:login} 133 | * @emits {Event:loginfailed} 134 | * @emits {Event:network.addbuffer} 135 | * @emits {Event:init} 136 | * @emits {Event:setup} 137 | * @emits {Event:setupok} 138 | * @emits {Event:setupfailed} 139 | * @emits {Event:identities.init} 140 | * @emits {Event:unhandled} 141 | * @protected 142 | */ 143 | handleMsgType(obj) { 144 | switch (obj.MsgType) { 145 | case 'ClientInitAck': 146 | this.handleClientInitAck(obj); 147 | break; 148 | case 'ClientLoginAck': 149 | logger('Logged in'); 150 | this.emit('login'); 151 | break; 152 | case 'ClientLoginReject': 153 | logger('ClientLoginReject: %O', obj); 154 | this.emit('loginfailed'); 155 | break; 156 | case 'CoreSetupAck': 157 | logger('Core setup successful'); 158 | this.emit('setupok'); 159 | break; 160 | case 'CoreSetupReject': 161 | logger('Core setup failed: %O', obj.Error); 162 | this.emit('setupfailed', obj.Error); 163 | break; 164 | case 'SessionInit': 165 | this.handleSessionInit(obj); 166 | break; 167 | default: 168 | logger('Unhandled MsgType %s', obj.MsgType); 169 | this.emit('unhandled', obj); 170 | } 171 | } 172 | 173 | /** 174 | * @protected 175 | */ 176 | handleClientInitAck(obj) { 177 | this.coreInfo = obj; 178 | this.emit('coreinfoinit', obj); 179 | if (!obj.Configured) { 180 | this.core.finishClientInit(() => this.emit('setup', obj.StorageBackends)); 181 | } else if (obj.LoginEnabled) { 182 | this.core.finishClientInit(() => this.login()); 183 | } else { 184 | this.emit('error', new Error('Your core is not supported')); 185 | } 186 | } 187 | 188 | /** 189 | * @protected 190 | */ 191 | handleSessionInit(obj) { 192 | this.emit('init', obj); 193 | // Init networks 194 | for (let networkId of obj.SessionState.NetworkIds) { 195 | // Save network list 196 | this.networks.add(networkId); 197 | this.core.sendInitRequest('Network', String(networkId)); 198 | } 199 | // Attach buffers to network 200 | for (let bufferInfo of obj.SessionState.BufferInfos) { 201 | const ircbuffer = new IRCBuffer(bufferInfo); 202 | this.networks.get(ircbuffer.network).buffers.add(ircbuffer); 203 | if (ircbuffer.isChannel) { 204 | this.core.sendInitRequest('IrcChannel', `${ircbuffer.network}/${ircbuffer.name}`); 205 | } 206 | this.emit('network.addbuffer', ircbuffer.network, bufferInfo.id); 207 | // Init backlogs for this buffer 208 | if (this.options.initialbackloglimit > 0) { 209 | this.core.backlog(bufferInfo.id, -1, -1, this.options.initialbackloglimit); 210 | } 211 | } 212 | // Init Identities 213 | for (let identity of obj.SessionState.Identities) { 214 | this.identities.set(identity.identityId, new Identity(identity)); 215 | } 216 | this.emit('identities.init', this.identities); 217 | this.core.sendInitRequest('BufferSyncer'); 218 | this.core.sendInitRequest('BufferViewManager'); 219 | this.core.sendInitRequest('IgnoreListManager'); 220 | if (this.supports(Features.CORESIDEHIGHLIGHTS)) { 221 | this.core.sendInitRequest('HighlightRuleManager'); 222 | } 223 | this.core.sendInitRequest('AliasManager'); 224 | this.heartbeatInterval = setInterval(() => this.core.heartBeat(), 30000); 225 | } 226 | 227 | /** 228 | * Returns `true` if the core supports the given feature 229 | * @example 230 | * quassel.supports(Features.PASSWORDCHANGE); 231 | * @param {number} feature 232 | * @returns {boolean} 233 | */ 234 | supports(feature) { 235 | return (this.coreInfo.CoreFeatures & feature) > 0; 236 | } 237 | 238 | /** 239 | * Dispatch quasselcore messages 240 | * @param {Object} obj 241 | * @protected 242 | */ 243 | dispatch(obj) { 244 | if (obj === null) { 245 | logger('Received null object ... ?'); 246 | } else if (obj.MsgType !== undefined) { 247 | this.handleMsgType(obj); 248 | } else if (Array.isArray(obj)) { 249 | this.handleStruct(obj); 250 | } else { 251 | logger('Unknown message: %O', obj); 252 | } 253 | } 254 | 255 | /** 256 | * @protected 257 | */ 258 | handleInitDataNetwork(id, data) { 259 | id = parseInt(id, 10); 260 | const network = this.networks.get(id); 261 | network.update(data); 262 | return network; 263 | } 264 | 265 | /** 266 | * Handles most of the quasselcore messages 267 | * @param {*} obj - quasselcore message decoded by qtdatasteam 268 | * @emits {Event:coreinfo} 269 | * @emits {Event:network.init} 270 | * @emits {Event:network.latency} 271 | * @emits {Event:network.connectionstate} 272 | * @emits {Event:network.addbuffer} 273 | * @emits {Event:network.connected} 274 | * @emits {Event:network.disconnected} 275 | * @emits {Event:network.userrenamed} 276 | * @emits {Event:network.mynick} 277 | * @emits {Event:network.networkname} 278 | * @emits {Event:network.server} 279 | * @emits {Event:network.serverlist} 280 | * @emits {Event:network.adduser} 281 | * @emits {Event:network.new} 282 | * @emits {Event:network.remove} 283 | * @emits {Event:network.codec.decoding} 284 | * @emits {Event:network.codec.encoding} 285 | * @emits {Event:network.codec.server} 286 | * @emits {Event:network.perform} 287 | * @emits {Event:network.identity} 288 | * @emits {Event:network.autoreconnect.interval} 289 | * @emits {Event:network.autoreconnect.retries} 290 | * @emits {Event:network.autoidentify.service} 291 | * @emits {Event:network.autoidentify.password} 292 | * @emits {Event:network.unlimitedreconnectretries} 293 | * @emits {Event:network.usesasl} 294 | * @emits {Event:network.sasl.account} 295 | * @emits {Event:network.sasl.password} 296 | * @emits {Event:network.rejoinchannels} 297 | * @emits {Event:network.usecustommessagerate} 298 | * @emits {Event:network.messagerate.unlimited} 299 | * @emits {Event:network.messagerate.delay} 300 | * @emits {Event:network.messagerate.burstsize} 301 | * @emits {Event:buffer.read} 302 | * @emits {Event:buffer.lastseen} 303 | * @emits {Event:buffer.markerline} 304 | * @emits {Event:buffer.remove} 305 | * @emits {Event:buffer.rename} 306 | * @emits {Event:buffer.merge} 307 | * @emits {Event:buffer.deactivate} 308 | * @emits {Event:buffer.activate} 309 | * @emits {Event:buffer.backlog} 310 | * @emits {Event:buffer.message} 311 | * @emits {Event:bufferview.ids} 312 | * @emits {Event:bufferview.bufferunhide} 313 | * @emits {Event:bufferview.bufferhidden} 314 | * @emits {Event:bufferview.orderchanged} 315 | * @emits {Event:bufferview.init} 316 | * @emits {Event:bufferview.networkid} 317 | * @emits {Event:bufferview.search} 318 | * @emits {Event:bufferview.hideinactivenetworks} 319 | * @emits {Event:bufferview.hideinactivebuffers} 320 | * @emits {Event:bufferview.allowedbuffertypes} 321 | * @emits {Event:bufferview.addnewbuffersautomatically} 322 | * @emits {Event:bufferview.minimumactivity} 323 | * @emits {Event:bufferview.bufferviewname} 324 | * @emits {Event:bufferview.disabledecoration} 325 | * @emits {Event:bufferview.update} 326 | * @emits {Event:user.part} 327 | * @emits {Event:user.quit} 328 | * @emits {Event:user.away} 329 | * @emits {Event:user.realname} 330 | * @emits {Event:channel.join} 331 | * @emits {Event:channel.addusermode} 332 | * @emits {Event:channel.removeusermode} 333 | * @emits {Event:channel.topic} 334 | * @emits {Event:ignorelist} 335 | * @emits {Event:identity} 336 | * @emits {Event:identity.new} 337 | * @emits {Event:identity.remove} 338 | * @emits {Event:aliases} 339 | * @protected 340 | */ 341 | handleStruct(obj) { 342 | const [ requesttype ] = obj; 343 | let className, id, functionName, data; 344 | switch (requesttype) { 345 | case RequestTypes.SYNC: 346 | [ , className, id, functionName, ...data ] = obj; 347 | this.handleStructSync(className.toString(), id, functionName.toString(), data); 348 | break; 349 | case RequestTypes.RPCCALL: 350 | [ , functionName, ...data ] = obj; 351 | this.handleStructRpcCall(functionName.toString(), data); 352 | break; 353 | case RequestTypes.INITDATA: 354 | [ , className, id, ...data ] = obj; 355 | this.handleStructInitData(className.toString(), id, data); 356 | break; 357 | case RequestTypes.HEARTBEAT: 358 | logger('HeartBeat'); 359 | this.core.heartBeat(true); 360 | break; 361 | case RequestTypes.HEARTBEATREPLY: 362 | logger('HeartBeatReply'); 363 | break; 364 | default: 365 | logger('Unhandled RequestType #%s', requesttype); 366 | } 367 | } 368 | 369 | /** 370 | * @protected 371 | */ 372 | handleStructSync(className, id, functionName, data) { 373 | let networkId, username, buffername; 374 | switch (className) { 375 | case 'Network': 376 | return this.handleStructSyncNetwork(parseInt(id.toString(), 10), functionName, data); 377 | case 'BufferSyncer': 378 | return this.handleStructSyncBufferSyncer(functionName, data); 379 | case 'BufferViewManager': 380 | return this.handleStructSyncBufferViewManager(functionName, data); 381 | case 'BufferViewConfig': 382 | return this.handleStructSyncBufferViewConfig(parseInt(id.toString(), 10), functionName, data); 383 | case 'IrcUser': 384 | [ networkId, username ] = splitOnce(id, '/'); 385 | return this.handleStructSyncIrcUser(parseInt(networkId, 10), username, functionName, data); 386 | case 'IrcChannel': 387 | [ networkId, buffername ] = splitOnce(id, '/'); 388 | return this.handleStructSyncIrcChannel(parseInt(networkId, 10), buffername, functionName, data); 389 | case 'BacklogManager': 390 | return this.handleStructSyncBacklogManager(functionName, data); 391 | case 'IgnoreListManager': 392 | return this.handleStructSyncIgnoreListManager(functionName, data); 393 | case 'HighlightRuleManager': 394 | return this.handleStructSyncHighlightRuleManager(functionName, data); 395 | case 'Identity': 396 | return this.handleStructSyncIdentity(parseInt(id, 10), functionName, data); 397 | case 'AliasManager': 398 | return this.handleStructSyncAliasManager(functionName, data); 399 | default: 400 | logger('Unhandled Sync %s', className); 401 | } 402 | } 403 | 404 | /** 405 | * @protected 406 | */ 407 | handleStructRpcCall(functionName, data) { 408 | switch (functionName) { 409 | case '2displayStatusMsg(QString,QString)': 410 | // Even official client doesn't use this ... 411 | break; 412 | case '2displayMsg(Message)': 413 | this.handleStructRpcCallDisplayMsg(data); 414 | break; 415 | case '__objectRenamed__': 416 | this.handleStructRpcCall__objectRenamed__(data); 417 | break; 418 | case '2networkCreated(NetworkId)': 419 | // data[0] is networkId 420 | this.networks.add(data[0]); 421 | this.core.sendInitRequest('Network', String(data[0])); 422 | this.emit('network.new', data[0]); 423 | break; 424 | case '2networkRemoved(NetworkId)': 425 | // data[0] is networkId 426 | this.networks.add(data[0]); 427 | this.networks.delete(data[0]); 428 | this.emit('network.remove', data[0]); 429 | break; 430 | case '2identityCreated(Identity)': 431 | // data[0] is identity 432 | this.identities.set(data[0].identityId, new Identity(data[0])); 433 | this.emit('identity.new', data[0].identityId); 434 | break; 435 | case '2identityRemoved(IdentityId)': 436 | // data[0] is identityId 437 | this.identities.delete(data[0]); 438 | this.emit('identity.remove', data[0]); 439 | break; 440 | default: 441 | logger('Unhandled RpcCall %s', functionName); 442 | } 443 | } 444 | 445 | /** 446 | * @protected 447 | */ 448 | handleStructRpcCallDisplayMsg([ message ]) { 449 | const network = this.networks.get(message.bufferInfo.network); 450 | if (network) { 451 | const identity = this.identities.get(network.identityId); 452 | let buffer = network.buffers.get(message.bufferInfo.id); 453 | if (!buffer) { 454 | buffer = network.buffers.get(message.bufferInfo.name); 455 | if (buffer) { 456 | buffer.update(message.bufferInfo); 457 | network.buffers.move(buffer, message.bufferInfo.id); 458 | } else { 459 | buffer = new IRCBuffer(message.bufferInfo); 460 | this.networks.get(message.bufferInfo.network).buffers.add(buffer); 461 | this.emit('network.addbuffer', message.bufferInfo.network, message.bufferInfo.id); 462 | } 463 | } 464 | if (message.type === MessageTypes.NETSPLITJOIN) { 465 | // TODO 466 | } else if (message.type === MessageTypes.NETSPLITQUIT) { 467 | // TODO 468 | } 469 | 470 | const simpleMessage = buffer.addMessage(message); 471 | if (simpleMessage) { 472 | simpleMessage._updateFlags(network, identity, this.options.highlightmode); 473 | this.emit('buffer.message', message.bufferInfo.id, simpleMessage.id); 474 | } 475 | } else { 476 | logger('Network %d does not exists', message.bufferInfo.network); 477 | } 478 | } 479 | 480 | /** 481 | * @protected 482 | */ 483 | handleStructRpcCall__objectRenamed__([ renamedSubject, oldSubject, newSubject ]) { 484 | renamedSubject = renamedSubject.toString(); 485 | let networkId, newNick, oldNick; 486 | switch (renamedSubject) { 487 | case 'IrcUser': 488 | [ networkId, newNick ] = splitOnce(oldSubject, '/'); // 1/Nick 489 | networkId = parseInt(networkId, 10); 490 | [ , oldNick ] = splitOnce(newSubject, '/'); // 1/Nick_ 491 | this.networks.get(networkId).renameUser(oldNick, newNick); 492 | this.emit('network.userrenamed', networkId, oldNick, newNick); 493 | break; 494 | default: 495 | logger('Unhandled RpcCall.__objectRenamed__ %s', renamedSubject); 496 | } 497 | } 498 | 499 | /** 500 | * @protected 501 | */ 502 | handleStructInitData(className, id, [ data ]) { 503 | let network, bufferViewIds; 504 | switch (className) { 505 | case 'Network': 506 | network = this.handleInitDataNetwork(id, data); 507 | this.emit('network.init', network.networkId); 508 | break; 509 | case 'BufferSyncer': 510 | this.handleStructInitDataBufferSyncer(data); 511 | break; 512 | case 'IrcUser': 513 | this.handleStructInitDataIrcUser(id, data); 514 | break; 515 | case 'IrcChannel': 516 | this.handleStructInitDataIrcChannel(id, data); 517 | break; 518 | case 'BufferViewManager': 519 | ({ BufferViewIds: bufferViewIds } = data); 520 | for (let bufferViewId of bufferViewIds) { 521 | this.core.sendInitRequest('BufferViewConfig', bufferViewId); 522 | } 523 | this.emit('bufferview.ids', bufferViewIds); 524 | break; 525 | case 'BufferViewConfig': 526 | this.handleStructInitDataBufferViewConfig(id, data); 527 | break; 528 | case 'IgnoreListManager': 529 | this.ignoreList.import(data); 530 | this.emit('ignorelist', this.ignoreList); 531 | break; 532 | case 'HighlightRuleManager': 533 | this.highlightRuleManager.import(data); 534 | this.emit('highlightrules', this.highlightRuleManager); 535 | break; 536 | case 'AliasManager': 537 | this.aliases = alias.toArray(data); 538 | this.emit('aliases', this.aliases); 539 | break; 540 | case 'CoreInfo': 541 | this.coreData = data; 542 | this.emit('coreinfo', data); 543 | break; 544 | default: 545 | logger('Unhandled InitData %s', className); 546 | } 547 | } 548 | 549 | handleStructInitDataBufferSyncerProperty(data, eventName) { 550 | let bufferId, value, i; 551 | for (i=0; i this.core.login(user, password)); 1100 | } 1101 | 1102 | /** 1103 | * Connect to the core 1104 | * @param {net.Duplex} duplex 1105 | */ 1106 | connect(duplex) { 1107 | this.core.init(duplex); 1108 | this.core.connect(); 1109 | } 1110 | 1111 | /** 1112 | * Disconnect the client from the core 1113 | */ 1114 | disconnect() { 1115 | clearInterval(this.heartbeatInterval); 1116 | this.core.disconnect(); 1117 | } 1118 | } 1119 | 1120 | /** 1121 | * This event is fired when quasselcore information are received 1122 | * @typedef {Event} Event:coreinfoinit 1123 | * @property {Object} data 1124 | * @property {boolean} data.Configured - Is the core configured 1125 | * @property {number} data.CoreFeatures 1126 | * @property {String} data.CoreInfo 1127 | * @property {boolean} data.LoginEnabled 1128 | * @property {boolean} data.MsgType - Is always "ClientInitAck" 1129 | * @property {number} data.ProtocolVersion 1130 | * @property {Array} [data.StorageBackends] 1131 | * @property {boolean} data.SupportSsl 1132 | * @property {boolean} data.SupportsCompression 1133 | */ 1134 | /** 1135 | * This event is fired upon successful login 1136 | * @typedef {Event} Event:login 1137 | */ 1138 | /** 1139 | * This event is fired upon unsuccessful login 1140 | * @typedef {Event} Event:loginfailed 1141 | */ 1142 | /** 1143 | * This event is fired upon successful session initialization 1144 | * @typedef {Event} Event:init 1145 | * @property {Object} obj 1146 | */ 1147 | /** 1148 | * This event is fired when {@link Identity} objects are first initialized 1149 | * @typedef {Event} Event:identities.init 1150 | * @property {Map} identities 1151 | */ 1152 | /** 1153 | * This event is fired when a buffer is added to a network 1154 | * @typedef {Event} Event:network.addbuffer 1155 | * @property {number} networkId 1156 | * @property {number|String} bufferId 1157 | */ 1158 | /** 1159 | * Network latency value 1160 | * @typedef {Event} Event:network.latency 1161 | * @property {number} networkId 1162 | * @property {number} value 1163 | */ 1164 | /** 1165 | * Network connection state 1166 | * @typedef {Event} Event:network.connectionstate 1167 | * @property {number} networkId 1168 | * @property {number} connectionState 1169 | */ 1170 | /** 1171 | * This event is fired when a network state is switched to connected 1172 | * @typedef {Event} Event:network.connected 1173 | * @property {number} networkId 1174 | */ 1175 | /** 1176 | * This event is fired when a network state is switched to disconnected 1177 | * @typedef {Event} Event:network.disconnected 1178 | * @property {number} networkId 1179 | */ 1180 | /** 1181 | * This event is fired when a user is renamed on a network 1182 | * @typedef {Event} Event:network.userrenamed 1183 | * @property {number} networkId 1184 | * @property {String} oldNick 1185 | * @property {String} nick 1186 | */ 1187 | /** 1188 | * This event is fired when current connected user is renamed on a network 1189 | * @typedef {Event} Event:network.mynick 1190 | * @property {number} networkId 1191 | * @property {String} nick 1192 | */ 1193 | /** 1194 | * This event is fired when the name of a network changes 1195 | * @typedef {Event} Event:network.networkname 1196 | * @property {number} networkId 1197 | * @property {String} networkName 1198 | */ 1199 | /** 1200 | * This event is fired when the server on which a network is connected changes 1201 | * @typedef {Event} Event:network.server 1202 | * @property {number} networkId 1203 | * @property {String} server 1204 | */ 1205 | /** 1206 | * This event is fired when a network server list is updated 1207 | * @typedef {Event} Event:network.serverlist 1208 | * @property {number} networkId 1209 | * @property {Object[]} serverlist 1210 | */ 1211 | /** 1212 | * Fired when encoding for sent messages has changed 1213 | * @typedef {Event} Event:network.codec.decoding 1214 | * @property {number} networkId 1215 | * @property {String} codec 1216 | */ 1217 | /** 1218 | * Fired when encoding for received messages has changed 1219 | * @typedef {Event} Event:network.codec.encoding 1220 | * @property {number} networkId 1221 | * @property {String} codec 1222 | */ 1223 | /** 1224 | * Fired when server encoding has changed 1225 | * @typedef {Event} Event:network.codec.server 1226 | * @property {number} networkId 1227 | * @property {String} codec 1228 | */ 1229 | /** 1230 | * Fired when the list of commands to perform on connection to a server has changed 1231 | * @typedef {Event} Event:network.perform 1232 | * @property {number} networkId 1233 | * @property {String[]} commands 1234 | */ 1235 | /** 1236 | * Fired when the network identity changed 1237 | * @typedef {Event} Event:network.identity 1238 | * @property {number} networkId 1239 | * @property {number} identityId 1240 | */ 1241 | /** 1242 | * Fired when interval value for reconnecting to the network changed 1243 | * @typedef {Event} Event:network.autoreconnect.interval 1244 | * @property {number} networkId 1245 | * @property {number} interval 1246 | */ 1247 | /** 1248 | * Fired when retries value for reconnecting to the network changed 1249 | * @typedef {Event} Event:network.autoreconnect.retries 1250 | * @property {number} networkId 1251 | * @property {number} retries 1252 | */ 1253 | /** 1254 | * Fired when auto identify service changed 1255 | * @typedef {Event} Event:network.autoidentify.service 1256 | * @property {number} networkId 1257 | * @property {String} service 1258 | */ 1259 | /** 1260 | * Fired when auto identify service password changed 1261 | * @typedef {Event} Event:network.autoidentify.password 1262 | * @property {number} networkId 1263 | * @property {String} password 1264 | */ 1265 | /** 1266 | * Fired when Unlimited reconnect retries value has changed 1267 | * @typedef {Event} Event:network.unlimitedreconnectretries 1268 | * @property {number} networkId 1269 | * @property {boolean} unlimitedreconnectretries 1270 | */ 1271 | /** 1272 | * Fired when Use Sasl value has changed 1273 | * @typedef {Event} Event:network.usesasl 1274 | * @property {number} networkId 1275 | * @property {boolean} usesasl 1276 | */ 1277 | /** 1278 | * Fired when Sasl account has changed 1279 | * @typedef {Event} Event:network.sasl.account 1280 | * @property {number} networkId 1281 | * @property {String} account 1282 | */ 1283 | /** 1284 | * Fired when Sasl account password has changed 1285 | * @typedef {Event} Event:network.sasl.password 1286 | * @property {number} networkId 1287 | * @property {String} password 1288 | */ 1289 | /** 1290 | * Fired when Rejoin Channels value has changed 1291 | * @typedef {Event} Event:network.rejoinchannels 1292 | * @property {number} networkId 1293 | * @property {boolean} rejoinchannels 1294 | */ 1295 | /** 1296 | * Fired when Use Custom Message Rate value has changed 1297 | * @typedef {Event} Event:network.usecustommessagerate 1298 | * @property {number} networkId 1299 | * @property {boolean} usecustommessagerate 1300 | */ 1301 | /** 1302 | * Fired when Unlimited Message Rate Burst Size value has changed 1303 | * @typedef {Event} Event:network.messagerate.unlimited 1304 | * @property {number} networkId 1305 | * @property {boolean} unlimited 1306 | */ 1307 | /** 1308 | * Fired when Message Rate Burst Size value has changed 1309 | * @typedef {Event} Event:network.messagerate.burstsize 1310 | * @property {number} networkId 1311 | * @property {number} burstsize 1312 | */ 1313 | /** 1314 | * Fired when Message Rate Delay value has changed 1315 | * @typedef {Event} Event:network.messagerate.delay 1316 | * @property {number} networkId 1317 | * @property {number} delay 1318 | */ 1319 | /** 1320 | * Buffer has been marked as read 1321 | * @typedef {Event} Event:buffer.read 1322 | * @property {number} bufferId 1323 | */ 1324 | /** 1325 | * Buffer's last seen message updated 1326 | * @typedef {Event} Event:buffer.lastseen 1327 | * @property {number} bufferId 1328 | * @property {number} messageId 1329 | */ 1330 | /** 1331 | * Buffer's markeline attached to a message 1332 | * @typedef {Event} Event:buffer.markerline 1333 | * @property {number} bufferId 1334 | * @property {number} messageId 1335 | */ 1336 | /** 1337 | * Buffer's activity which represents all unread message types for this buffer 1338 | * @typedef {Event} Event:buffer.activity 1339 | * @property {number} bufferId 1340 | * @property {number} unreadTypes 1341 | */ 1342 | /** 1343 | * Buffer has been removed 1344 | * @typedef {Event} Event:buffer.remove 1345 | * @property {number} bufferId 1346 | */ 1347 | /** 1348 | * Buffer has been renamed 1349 | * @typedef {Event} Event:buffer.rename 1350 | * @property {number} bufferId 1351 | */ 1352 | /** 1353 | * bufferId2 has been merged into bufferId1 1354 | * @typedef {Event} Event:buffer.merge 1355 | * @property {number} bufferId1 1356 | * @property {number} bufferId2 1357 | */ 1358 | /** 1359 | * Buffer's hidden state removed 1360 | * @typedef {Event} Event:bufferview.bufferunhide 1361 | * @property {number} bufferViewId 1362 | * @property {number} bufferId 1363 | */ 1364 | /** 1365 | * Buffer's hidden state set 1366 | * @typedef {Event} Event:bufferview.bufferhidden 1367 | * @property {number} bufferViewId 1368 | * @property {number} bufferId 1369 | * @property {String} type Either "temp" or "perm" 1370 | */ 1371 | /** 1372 | * Buffer set as inactive 1373 | * @typedef {Event} Event:buffer.deactivate 1374 | * @property {number} bufferId 1375 | */ 1376 | /** 1377 | * User has left a channel 1378 | * @typedef {Event} Event:user.part 1379 | * @property {number} networkId 1380 | * @property {String} nick 1381 | * @property {number} bufferId 1382 | */ 1383 | /** 1384 | * User has left a network 1385 | * @typedef {Event} Event:user.quit 1386 | * @property {number} networkId 1387 | * @property {String} nick 1388 | */ 1389 | /** 1390 | * User away state changed 1391 | * @typedef {Event} Event:user.away 1392 | * @property {number} networkId 1393 | * @property {String} nick 1394 | * @property {boolean} isAway 1395 | */ 1396 | /** 1397 | * User realname changed 1398 | * @typedef {Event} Event:user.realname 1399 | * @property {number} networkId 1400 | * @property {String} nick 1401 | * @property {String} realname 1402 | */ 1403 | /** 1404 | * User joined a channel 1405 | * @typedef {Event} Event:channel.join 1406 | * @property {number} bufferId 1407 | * @property {String} nick 1408 | */ 1409 | /** 1410 | * User mode has been added 1411 | * @typedef {Event} Event:channel.addusermode 1412 | * @property {number} bufferId 1413 | * @property {String} nick 1414 | * @property {String} mode 1415 | */ 1416 | /** 1417 | * User mode has been removed 1418 | * @typedef {Event} Event:channel.removeusermode 1419 | * @property {number} bufferId 1420 | * @property {String} nick 1421 | * @property {String} mode 1422 | */ 1423 | /** 1424 | * Channel topic changed 1425 | * @typedef {Event} Event:channel.topic 1426 | * @property {number} bufferId 1427 | * @property {String} topic 1428 | */ 1429 | /** 1430 | * Core information 1431 | * @typedef {Event} Event:coreinfo 1432 | * @property {Object} data 1433 | */ 1434 | /** 1435 | * {@link IRCBuffer} activated 1436 | * @typedef {Event} Event:buffer.activate 1437 | * @property {number} bufferId 1438 | */ 1439 | /** 1440 | * Backlogs received 1441 | * @typedef {Event} Event:buffer.backlog 1442 | * @property {number} bufferId 1443 | * @property {number[]} messageIds 1444 | */ 1445 | /** 1446 | * {@link IRCMessage} received on a buffer 1447 | * @typedef {Event} Event:buffer.message 1448 | * @property {number} bufferId 1449 | * @property {number} messageId 1450 | */ 1451 | /** 1452 | * Buffers order changed 1453 | * @typedef {Event} Event:bufferview.orderchanged 1454 | * @property {number} bufferViewId 1455 | */ 1456 | /** 1457 | * {@link BufferView} manager init request received 1458 | * @typedef {Event} Event:bufferview.ids 1459 | * @property {number[]} ids 1460 | */ 1461 | /** 1462 | * {@link BufferView} initialized 1463 | * @typedef {Event} Event:bufferview.init 1464 | * @property {number} bufferViewId 1465 | */ 1466 | /** 1467 | * {@link BufferView} networkId updated 1468 | * @typedef {Event} Event:bufferview.networkid 1469 | * @property {number} bufferViewId 1470 | * @property {number} networkId 1471 | */ 1472 | /** 1473 | * {@link BufferView} search updated 1474 | * @typedef {Event} Event:bufferview.search 1475 | * @property {number} bufferViewId 1476 | * @property {boolean} search 1477 | */ 1478 | /** 1479 | * {@link BufferView} hideInactiveNetworks updated 1480 | * @typedef {Event} Event:bufferview.hideinactivenetworks 1481 | * @property {number} bufferViewId 1482 | * @property {boolean} hideinactivenetworks 1483 | */ 1484 | /** 1485 | * {@link BufferView} hideInactiveBuffers updated 1486 | * @typedef {Event} Event:bufferview.hideinactivebuffers 1487 | * @property {number} bufferViewId 1488 | * @property {boolean} hideinactivebuffers 1489 | */ 1490 | /** 1491 | * {@link BufferView} allowedBufferTypes updated 1492 | * @typedef {Event} Event:bufferview.allowedbuffertypes 1493 | * @property {number} bufferViewId 1494 | * @property {number} allowedbuffertypes 1495 | */ 1496 | /** 1497 | * {@link BufferView} addNewBuffersAutomatically updated 1498 | * @typedef {Event} Event:bufferview.addnewbuffersautomatically 1499 | * @property {number} bufferViewId 1500 | * @property {boolean} addnewbuffersautomatically 1501 | */ 1502 | /** 1503 | * {@link BufferView} minimumActivity updated 1504 | * @typedef {Event} Event:bufferview.minimumactivity 1505 | * @property {number} bufferViewId 1506 | * @property {boolean} minimumactivity 1507 | */ 1508 | /** 1509 | * {@link BufferView} bufferViewName updated 1510 | * @typedef {Event} Event:bufferview.bufferviewname 1511 | * @property {number} bufferViewId 1512 | * @property {String} bufferviewname 1513 | */ 1514 | /** 1515 | * {@link BufferView} disableDecoration updated 1516 | * @typedef {Event} Event:bufferview.disabledecoration 1517 | * @property {number} bufferViewId 1518 | * @property {boolean} disabledecoration 1519 | */ 1520 | /** 1521 | * {@link BufferView} object updated 1522 | * @typedef {Event} Event:bufferview.update 1523 | * @property {number} bufferViewId 1524 | * @property {object} data 1525 | */ 1526 | /** 1527 | * {@link IgnoreList} updated 1528 | * @typedef {Event} Event:ignorelist 1529 | */ 1530 | /** 1531 | * {@link HighLightRuleManager} updated 1532 | * @typedef {Event} Event:highlightrules 1533 | */ 1534 | /** 1535 | * {@link Identity} updated 1536 | * @typedef {Event} Event:identity 1537 | */ 1538 | /** 1539 | * New {@link Identity} created 1540 | * @typedef {Event} Event:identity.new 1541 | * @property {number} identityId 1542 | */ 1543 | /** 1544 | * {@link Identity} removed 1545 | * @typedef {Event} Event:identity.remove 1546 | * @property {number} identityId 1547 | */ 1548 | /** 1549 | * User connected to the {@link Network} 1550 | * @typedef {Event} Event:network.adduser 1551 | * @property {number} networkId 1552 | * @property {String} nick 1553 | */ 1554 | /** 1555 | * New {@link Network} created 1556 | * @typedef {Event} Event:network.new 1557 | * @property {number} networkId 1558 | */ 1559 | /** 1560 | * {@link Network} removed 1561 | * @typedef {Event} Event:network.remove 1562 | * @property {number} networkId 1563 | */ 1564 | /** 1565 | * {@link Network} is ready 1566 | * @typedef {Event} Event:network.init 1567 | * @property {number} networkId 1568 | */ 1569 | /** 1570 | * Aliases updated 1571 | * @typedef {Event} Event:aliases 1572 | */ 1573 | /** 1574 | * This event is fired when the core needs to be setup 1575 | * @typedef {Event} Event:setup 1576 | * @property {Object[]} backends - List of available storage backends 1577 | * @property {String} backends[].DisplayName - Storage backends name 1578 | * @property {String} backends[].Description - Storage backends description 1579 | * @property {String[]} backends[].SetupKeys - Keys that will need a corresponding value to configure chosen storage backend 1580 | * @property {Object} backends[].SetupDefaults - Defaults values for corresponding SetupKeys 1581 | */ 1582 | /** 1583 | * This event is fired if the setup of the core was successful 1584 | * @typedef {Event} Event:setupok 1585 | */ 1586 | /** 1587 | * This event is fired if the setup of the core has failed 1588 | * @typedef {Event} Event:setupfailed 1589 | * @property {Object} error - The reason of the failure 1590 | */ 1591 | /** 1592 | * This event is fired if an unhandled message is received 1593 | * @typedef {Event} Event:unhandled 1594 | * @property {Object} obj 1595 | */ 1596 | /** 1597 | * An error occured 1598 | * @typedef {Event} Event:error 1599 | * @property {Object} error 1600 | */ 1601 | 1602 | function splitOnce(str, character) { 1603 | const i = str.indexOf(character); 1604 | return [ str.slice(0, i), str.slice(i+1) ]; 1605 | } 1606 | --------------------------------------------------------------------------------