├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── COPYING ├── README.md ├── docs ├── API.rst ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── example ├── bot.js └── secure.js ├── lib ├── codes.js ├── colors.js ├── cycling_ping_timer.js ├── irc.js └── parse_message.js ├── package-lock.json ├── package.json ├── test.js └── test ├── data ├── fixtures.json ├── ircd.key └── ircd.pem ├── helpers.js ├── test-api.js ├── test-autorenick.js ├── test-cmdqueue.js ├── test-colors.js ├── test-convert-encoding.js ├── test-cyclingpingtimer.js ├── test-irc.js ├── test-mode.js ├── test-output.js ├── test-parse-line.js ├── test-quit.js ├── test-reconnect.js └── test-user-events.js /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: "eslint:recommended" 2 | env: 3 | node: true 4 | mocha: true 5 | rules: 6 | camelcase: 7 | - 2 8 | - properties: "never" 9 | consistent-this: 10 | - 2 11 | - "self" 12 | no-unused-vars: 13 | - 2 14 | - argsIgnorePattern: "^_" 15 | no-console: 0 16 | block-scoped-var: 2 17 | dot-notation: 2 18 | eqeqeq: 2 19 | no-else-return: 1 20 | no-useless-call: 2 21 | no-useless-return: 1 22 | no-shadow: 1 23 | eol-last: 1 24 | no-lonely-if: 1 25 | quote-props: 26 | - 2 27 | - "as-needed" 28 | semi: 2 29 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | test: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x, 16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | - run: npm run save-coverage 31 | - name: Report to Coveralls (parallel) 32 | uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | flag-name: run-${{ matrix.node-version }} 36 | parallel: true 37 | 38 | finish: 39 | 40 | needs: test 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - name: Report to Coveralls 45 | uses: coverallsapp/github-action@master 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | parallel-finished: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules/ 3 | _build/ 4 | .idea/ 5 | 6 | # Coverage directory used by tools like istanbul 7 | coverage 8 | .nyc_output 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to node-irc 2 | 3 | First, and most importantly, thank you for contributing to node-irc! Your efforts make the library 4 | as awesome as it is, and we really couldn't do it without you. Through your pull requests, issues, 5 | and discussion, together we are building the best IRC library for node. So, once again, *thank you*! 6 | 7 | What follows is a set of guidelines for contributing to node-irc. We ask that you follow them 8 | because they make our lives as maintainers easier, which means that your pull requests and issues 9 | have a higher chance of being addressed quickly. Please help us help you! 10 | 11 | This guide is roughly based on the [Atom contributor's guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), so thanks to the Atom team for 12 | providing such a solid framework on which to hang our ideas. 13 | 14 | # Submitting Issues 15 | * Include the version of node-irc, or the SHA1 from git if you're using git. 16 | * Include the behavior you expect, other clients / bots where you've seen that behavior, the 17 | observed behavior, and details on how the two differ if it's not obvious. 18 | * Enable debug mode for node-irc and include its output in your report. 19 | * In the case of a crash, include the full stack trace and any error output. 20 | * Perform a cursory search to see if similar issues or pull requests have already been filed, and 21 | comment on them to move the discussion forward. 22 | * Most importantly, provide a minimal test case that demonstrates your bug. This is the best way 23 | to help us quickly isolate and fix your bug. 24 | * Consider [joining us on Gitter](https://gitter.im/node-irc) for realtime discussion. 25 | Not only is it a friendly gesture, it also helps us isolate your issue far more quickly than the back-and-forth of issues on github allows. 26 | 27 | # Pull requests 28 | * Add yourself to the contributors section of package.json to claim credit for your work! 29 | * Do your work on a branch from master and file your pull request from that branch to master. 30 | * Make sure your code passes all tests (`npm test`). 31 | * If possible, write *new* tests for the functionality you add. Once we have sane testing in place, 32 | this will become a hard requirement, so it's best to get used to it now! 33 | * If you change any user-facing element of the library (e.g. the API), document your changes. 34 | * If you make *breaking* changes, say so clearly in your pull request, so we know to schedule the 35 | merge when we plan to break the API for other changes. 36 | * End files with a newline. 37 | 38 | # Commit messages 39 | * Use the present tense ("Add feature" not "Added feature"). 40 | * Use the imperative mood ("Change message handling..." not "Changes message handling..."). 41 | * Limit the first line to 72 characters or less. 42 | * Reference issues and pull requests liberally. 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis](https://travis-ci.org/Throne3d/node-irc.svg?branch=master)](https://travis-ci.org/Throne3d/node-irc) 2 | [![npm](https://badge.fury.io/js/irc-upd.svg)](https://www.npmjs.com/package/irc-upd) 3 | 4 | [![Dependency Status](https://david-dm.org/Throne3d/node-irc.svg)](https://david-dm.org/Throne3d/node-irc) 5 | [![devDependency Status](https://david-dm.org/Throne3d/node-irc/dev-status.svg)](https://david-dm.org/Throne3d/node-irc?type=dev) 6 | [![Coverage Status](https://coveralls.io/repos/github/Throne3d/node-irc/badge.svg?branch=master)](https://coveralls.io/github/Throne3d/node-irc?branch=master) 7 | [![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](http://opensource.org/licenses/GPL-3.0) 8 | [![Join the chat at https://gitter.im/node-irc](https://badges.gitter.im/node-irc.svg)](https://gitter.im/node-irc) 9 | 10 | 11 | [node-irc](https://node-irc-upd.readthedocs.io/) is an IRC client library written in [JavaScript](http://en.wikipedia.org/wiki/JavaScript) for [Node](http://nodejs.org/). 12 | This project is a fork of [another GitHub repository](https://github.com/martynsmith/node-irc). 13 | 14 | You can access more detailed documentation for this module at [Read the Docs](https://node-irc-upd.readthedocs.io/en/latest/) 15 | 16 | 17 | ## Installation 18 | 19 | The easiest way to get it is via [npm](http://github.com/isaacs/npm): 20 | 21 | ``` 22 | npm install irc-upd 23 | ``` 24 | 25 | If you want to run the latest version (i.e. later than the version available via [npm](http://github.com/isaacs/npm)) you can clone this repo, then use [npm](http://github.com/isaacs/npm) to link-install it: 26 | 27 | ``` 28 | npm link /path/to/your/clone 29 | ``` 30 | 31 | Of course, you can just clone this, and manually point at the library itself, but we really recommend using [npm](http://github.com/isaacs/npm)! 32 | 33 | ### Character set detection 34 | 35 | Character set detection was introduced to node-irc in version 0.3.8. 36 | Since then, it's had a revamp, and now uses [chardet](https://github.com/runk/node-chardet) and [iconv-lite](https://github.com/ashtuchkin/iconv-lite). 37 | It should no longer be necessary to install local libraries to get the encoding conversion to function. 38 | However, if the libraries fail to install for whatever reason, node-irc will still install (assuming nothing else failed) and you'll be able to use it, just not the character set features. 39 | 40 | If you install the library without the relevant dependencies at first, and later wish to enable to character set conversion functionality, simply re-run `npm install`. 41 | 42 | ## Basic Usage 43 | 44 | This library provides basic IRC client functionality. 45 | In the simplest case you can connect to an IRC server like so: 46 | 47 | ```js 48 | var irc = require('irc-upd'); 49 | var client = new irc.Client('irc.yourserver.com', 'myNick', { 50 | channels: ['#channel'], 51 | }); 52 | ``` 53 | 54 | Of course it's not much use once it's connected if that's all you have! 55 | 56 | The client emits a large number of events that correlate to things you'd normally see in your favorite IRC client. 57 | Most likely the first one you'll want to use is: 58 | 59 | ```js 60 | client.addListener('message', function (from, to, message) { 61 | console.log(from + ' => ' + to + ': ' + message); 62 | }); 63 | ``` 64 | 65 | or if you're only interested in messages to the bot itself: 66 | 67 | ```js 68 | client.addListener('pm', function (from, message) { 69 | console.log(from + ' => ME: ' + message); 70 | }); 71 | ``` 72 | 73 | or to a particular channel: 74 | 75 | ```js 76 | client.addListener('message#yourchannel', function (from, message) { 77 | console.log(from + ' => #yourchannel: ' + message); 78 | }); 79 | ``` 80 | 81 | At the moment there are functions for joining: 82 | 83 | ```js 84 | client.join('#yourchannel yourpass'); 85 | ``` 86 | 87 | parting: 88 | 89 | ```js 90 | client.part('#yourchannel'); 91 | ``` 92 | 93 | talking: 94 | 95 | ```js 96 | client.say('#yourchannel', "I'm a bot!"); 97 | client.say('nonbeliever', "SRSLY, I AM!"); 98 | ``` 99 | 100 | and many others. Check out the API documentation for a complete reference. 101 | 102 | For any commands that there aren't methods for you can use the send() method which sends raw messages to the server: 103 | 104 | ```js 105 | client.send('MODE', '#yourchannel', '+o', 'yournick'); 106 | ``` 107 | 108 | ## Help! - it keeps crashing! 109 | 110 | When the client receives errors from the IRC network, it emits an "error" event. 111 | As stated in the [Node JS EventEmitter documentation](http://nodejs.org/api/events.html#events_class_events_eventemitter) if you don't bind something to this error, it will cause a fatal stack trace. 112 | 113 | The upshot of this is basically that if you bind an error handler to your client, errors will be sent there instead of crashing your program: 114 | 115 | ```js 116 | client.addListener('error', function(message) { 117 | console.log('error: ', message); 118 | }); 119 | ``` 120 | 121 | 122 | ## Further Support 123 | 124 | Further documentation (including a complete API reference) is available in reStructuredText format in the docs/ folder of this project, or online at [Read the Docs](https://node-irc-upd.readthedocs.io/en/latest/). 125 | 126 | If you find any issues with the documentation (or the module) please send a pull request or file an issue and we'll do our best to accommodate. 127 | 128 | You can visit us on our Gitter (found [here](https://gitter.im/node-irc)) to discuss issues you're having with the library, pull requests, or anything else related to node-irc. 129 | 130 | If you want to, you can connect to Gitter with your standard IRC client – instructions [here](https://irc.gitter.im/). 131 | -------------------------------------------------------------------------------- /docs/API.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | This library provides IRC client functionality 5 | 6 | Client 7 | ---------- 8 | 9 | .. js:function:: irc.Client(server, nick [, options]) 10 | 11 | This object is the base of the library. 12 | It represents a single nick connected to a single IRC server. 13 | 14 | The first argument is the server address to connect to, and the second is the nickname to attempt to use. 15 | The third optional argument is an options object with default 16 | values:: 17 | 18 | { 19 | userName: 'nodebot', 20 | realName: 'nodeJS IRC client', 21 | password: null, 22 | port: 6667, 23 | localAddress: null, 24 | debug: false, 25 | showErrors: false, 26 | channels: [], 27 | autoConnect: true, 28 | autoRejoin: false, 29 | autoRenick: false, 30 | renickCount: null, 31 | renickDelay: 60000, 32 | retryCount: null, 33 | retryDelay: 2000, 34 | secure: false, 35 | selfSigned: false, 36 | certExpired: false, 37 | floodProtection: false, 38 | floodProtectionDelay: 1000, 39 | sasl: false, 40 | webirc: { 41 | pass: '', 42 | ip: '', 43 | host: '' 44 | }, 45 | stripColors: false, 46 | channelPrefixes: "&#", 47 | messageSplit: 512, 48 | encoding: null, 49 | millisecondsOfSilenceBeforePingSent: 15 * 1000, 50 | millisecondsBeforePingTimeout: 8 * 1000, 51 | enableStrictParse: false 52 | } 53 | 54 | 55 | ``localAddress`` is the address to bind to when connecting. 56 | 57 | ``debug`` will output timestamped messages to the console using ``util.log`` when certain events are fired. If this is true, it will override ``showErrors``. 58 | 59 | ``showErrors`` will output timestamped errors to the console using ``util.log``, such as when certain IRC responses are encountered or an attempt to find the message charset fails. 60 | If ``debug`` is true, it will override this. 61 | 62 | ``channels`` is an array of channels to join on connect. 63 | 64 | ``autoConnect`` has the client connect when instantiated. 65 | If disabled, you will need to call ``connect()`` on the client instance:: 66 | 67 | var client = new irc.Client({ autoConnect: false, ... }); 68 | client.connect(); 69 | 70 | ``autoRejoin`` has the client rejoin channels after being kicked. 71 | 72 | ``autoRenick`` has the client attempt to renick to its configured nickname if it can't originally join with it (due to nickname clash). 73 | Only takes effect when the client receives a ``err_nicknameinuse`` message – if disabled after this point, will not cancel the effect. 74 | See ``cancelAutoRenick`` to cancel a current renick attempt. 75 | See ``renickCount`` and ``renickDelay`` to configure. 76 | 77 | ``renickCount`` is the number of times the client will try to automatically renick (reset each time it connects). 78 | It defaults to ``null`` (meaning infinite retry). 79 | 80 | ``renickDelay`` is the number of milliseconds to wait before retrying to automatically renick. 81 | It defaults to 60,000ms (60 seconds). 82 | 83 | ``retryCount`` is the number of times the client will try to automatically reconnect when disconnected from the server. 84 | It defaults to ``null`` (meaning infinite retry). 85 | 86 | ``retryDelay`` is the number of milliseconds to wait before retrying to automatically reconnect when disconnected from the server. 87 | It defaults to 2000ms (2 seconds). 88 | 89 | ``secure`` (SSL connection) can be a true value or an object (the kind returned by ``crypto.createCredentials()``) specifying the certificate and other details for validation. 90 | If you set ``selfSigned`` to true, it will accept certificates from a non-trusted CA. 91 | If you set ``certExpired`` to true, the bot will accept expired certificates. 92 | 93 | ``floodProtection`` queues all your messages and slowly transmits them to prevent being kicked for flooding. 94 | Alternatively, use ``Client.activateFloodProtection()`` to activate flood protection after instantiating the client. 95 | 96 | ``floodProtectionDelay`` sets the amount of time that the client will wait between sending subsequent messages when ``floodProtection`` is enabled. 97 | 98 | ``sasl`` enables SASL support. 99 | You'll also want to set ``nick``, ``userName``, and ``password`` for authentication. 100 | 101 | ``webirc`` is an object that contains WEBIRC credentials (if applicable). 102 | 103 | ``stripColors`` removes mIRC colors (0x03 followed by one or two ASCII numbers for the foreground and background color), as well as ircII "effect" codes (``0x02`` bold, ``0x1f`` underline, ``0x16`` reverse, ``0x0f`` reset) from the message before parsing it. 104 | 105 | ``messageSplit`` will split up large messages sent with the ``say`` method into multiple messages of lengths shorter than ``messageSplit`` bytes, attempting to split at whitespace where possible. 106 | 107 | ``encoding`` specifies the encoding for the bot to convert messages to. 108 | To disable this, leave the value blank or false. 109 | Example values are ``UTF-8`` and ``ISO-8859-15``. 110 | 111 | ``millisecondsOfSilenceBeforePingSent`` controls the amount of time the ping timer will wait before sending a ping request. 112 | 113 | ``millisecondsBeforePingTimeout`` controls the amount of time the ping timer will wait after sending a ping request before the bot receives a ``pingTimeout`` event. 114 | 115 | ``enableStrictParse`` will make the client try to conform more strictly to `the RFC 2812 standard `_ for parsing nicknames, preventing eg CJK characters from appearing in them. 116 | 117 | .. js:function:: Client.connect([retryCount [, callback]]) 118 | 119 | Connects to the server. 120 | Used when ``autoConnect`` in the options is set to false, or after a disconnect. 121 | Outputs an error to console if there is already an active connection. 122 | If ``retryCount`` is a function, it will be treated as a ``callback`` (i.e. both arguments to this function are optional). 123 | 124 | :param integer retryCount: an optional number of times to attempt reconnection 125 | :param function callback: an optional callback to fire upon connection 126 | 127 | .. js:function:: Client.disconnect([message [, callback]]) 128 | 129 | Disconnects from the IRC server. 130 | If ``message`` is a function it will be treated as a ``callback`` (i.e. both arguments to this function are optional). 131 | Outputs an error to console if it is already disconnected or disconnecting. 132 | 133 | :param string message: an optional message to send when disconnecting 134 | :param function callback: an optional callback 135 | 136 | .. js:function:: Client.send(command, arg1, arg2, ...) 137 | 138 | Sends a raw message to the server. 139 | Generally speaking, it's best to use other, more specific methods with priority, unless you know what you're doing. 140 | 141 | .. js:function:: Client.join(channelList, [callback]) 142 | 143 | Joins the specified channel. 144 | 145 | :param string channelList: the channel(s) to join 146 | :param function callback: an optional callback to automatically attach to ``join#channelname`` for each channel 147 | 148 | ``channelList`` supports multiple channels in a comma-separated string (`as in the IRC protocol `_). 149 | The callback is called for each channel, but does not include the ``channel`` parameter (see the ``join#channel`` event). 150 | 151 | To optionally send a list of keys (channel passwords) associated with each channel, add a space after the list of channels and append the list of channels. 152 | For example: ``#foo,&bar fubar,foobar`` will join the channel ``#foo`` with the key ``fubar`` and the channel ``&bar`` with the key ``foobar``. 153 | 154 | Passing ``'0'`` to the ``channelList`` parameter will send ``JOIN 0`` to the server. 155 | As in the IRC spec, this will cause the client to part from all current channels. 156 | In such a case, the callback will not be called; you should instead bind to the ``part`` event to keep track of the progress made. 157 | 158 | .. js:function:: Client.part(channel, [message], [callback]) 159 | 160 | Parts the specified channel. 161 | 162 | :param string channelList: the channel(s) to part 163 | :param string message: an optional message to send upon leaving the channel 164 | :param function callback: a optional callback to automatically attach to ``part#channelname`` for each channel 165 | 166 | As with ``Client.join``, the ``channelList`` parameter supports multiple channels in a comma-separated string, for each of which the callback will be called. 167 | 168 | .. js:function:: Client.say(target, message) 169 | 170 | Sends a message to the specified target. 171 | 172 | :param string target: a nickname or a channel to send the message to 173 | :param string message: the message to send 174 | 175 | .. js:function:: Client.action(target, message) 176 | 177 | Sends an action to the specified target. 178 | Often transmitted with ``/me`` in IRC clients. 179 | 180 | :param string target: a nickname or a channel to send the action message to 181 | :param string text: the text of the action to send 182 | 183 | .. js:function:: Client.notice(target, message) 184 | 185 | Sends a notice to the specified target. 186 | 187 | :param string target: a nickname or a channel to send the notice to 188 | :param string message: the message to send to the target 189 | 190 | .. js:function:: Client.whois(nick, callback) 191 | 192 | Request a whois for the specified ``nick``. 193 | 194 | :param string nick: a nickname to request a whois of 195 | :param function callback: a callback to fire when the server sends the response; is passed the same information as in the ``whois`` event above 196 | 197 | .. js:function:: Client.ctcp(target, type, text) 198 | 199 | Sends a CTCP message to the specified target. 200 | 201 | :param string target: a nickname or a channel to send the CTCP message to 202 | :param string type: the type of the CTCP message; that is, "privmsg" for a ``PRIVMSG``, and anything else for a ``NOTICE`` 203 | :param string text: the CTCP message to send 204 | 205 | .. js:function:: Client.list([arg1, arg2, ...]) 206 | 207 | Request a channel listing from the server. 208 | The arguments for this method are fairly server specific, so this method passes them through exactly as specified. 209 | 210 | Responses from the server are available through the ``channellist_start``, ``channellist_item``, and ``channellist`` events. 211 | 212 | .. js:function:: Client.activateFloodProtection([interval]) 213 | 214 | Activates flood protection manually after instantiation of the client. 215 | You can also use the ``floodProtection`` option while instantiating the client to enable flood protection then; see also ``floodProtectionDelay`` to set the message interval. 216 | 217 | This should only be called once per Client instance, not on every connection, and cannot currently be deactivated. 218 | 219 | :param integer interval: an optional configuration for amount of time to wait between messages, defaults to client configuration value 220 | 221 | .. js:function:: Client.cancelAutoRenick() 222 | 223 | Cancels the current auto-renick event; see the ``autoRenick`` config option for more details. 224 | Returns the interval object, if it existed. 225 | 226 | .. js:function:: Client.canConvertEncoding() 227 | 228 | Calls the exported function ``irc.canConvertEncoding()``. 229 | 230 | Events 231 | ------ 232 | 233 | ``irc.Client`` instances are ``EventEmitters`` with the following events: 234 | 235 | 236 | .. js:data:: 'registered' 237 | 238 | ``function (message) { }`` 239 | 240 | Emitted when the server sends the initial 001 line, indicating you've connected to the server. 241 | See the ``raw`` event for details on the ``message`` object. 242 | 243 | .. js:data:: 'motd' 244 | 245 | ``function (motd) { }`` 246 | 247 | Emitted when the server sends the message of the day to clients. 248 | 249 | .. js:data:: 'message' 250 | 251 | ``function (nick, to, text, message) { }`` 252 | 253 | Emitted when a message is sent. 254 | The ``to`` parameter can be either a nick (which is most likely this client's nick and represents a private message), or a channel (which represents a message to that channel). 255 | See the ``raw`` event for details on the ``message`` object. 256 | 257 | .. js:data:: 'message#' 258 | 259 | ``function (nick, to, text, message) { }`` 260 | 261 | Emitted when a message is sent to any channel (i.e. exactly the same as the ``message`` event but excluding private messages). 262 | See the ``raw`` event for details on the ``message`` object. 263 | 264 | .. js:data:: 'message#channel' 265 | 266 | ``function (nick, text, message) { }`` 267 | 268 | Same as the 'message' event, but only emitted for the specified channel. 269 | See the ``raw`` event for details on the ``message`` object. 270 | 271 | .. js:data:: 'selfMessage' 272 | 273 | ``function (to, text) { }`` 274 | 275 | Emitted when a message is sent from the client. 276 | The ``to`` parameter is the target of the message, which can be either a nick (in a private message) or a channel (as in a message to that channel) 277 | 278 | .. js:data:: 'notice' 279 | 280 | ``function (nick, to, text, message) { }`` 281 | 282 | Emitted when a notice is sent. 283 | The ``to`` parameter can be either a nick (most likely this client's nick and so represents a private message), or a channel (which represents a message to that channel). 284 | The ``nick`` parameter is either the sender's nick or ``null``, representing that the notice comes from the server. 285 | See the ``raw`` event for details on the ``message`` object. 286 | 287 | .. js:data:: 'action' 288 | 289 | ``function (from, to, text, message) { }`` 290 | 291 | Emitted whenever a user performs an action (e.g. ``/me waves``). 292 | See the ``raw`` event for details on the ``message`` object. 293 | 294 | .. js:data:: 'pm' 295 | 296 | ``function (nick, text, message) { }`` 297 | 298 | Same as the 'message' event, but only emitted when the message is directed to the client. 299 | See the ``raw`` event for details on the ``message`` object. 300 | 301 | .. js:data:: 'invite' 302 | 303 | ``function (channel, from, message) { }`` 304 | 305 | Emitted when the client receives an ``/invite``. 306 | See the ``raw`` event for details on the ``message`` object. 307 | 308 | .. js:data:: 'names' 309 | 310 | ``function (channel, nicks) { }`` 311 | 312 | Emitted when the server sends a list of nicks for a channel (which happens immediately after joining or on request). 313 | The nicks object passed to the callback is keyed by nickname, and has values '', '+', or '@' depending on the level of that nick in the channel. 314 | 315 | .. js:data:: 'names#channel' 316 | 317 | ``function (nicks) { }`` 318 | 319 | Same as the 'names' event, but only emitted for the specified channel. 320 | 321 | .. js:data:: 'topic' 322 | 323 | ``function (channel, topic, nick, message) { }`` 324 | 325 | Emitted when the server sends the channel topic after joining a channel, or when a user changes the topic on a channel. 326 | See the ``raw`` event for details on the ``message`` object. 327 | 328 | .. js:data:: 'join' 329 | 330 | ``function (channel, nick, message) { }`` 331 | 332 | Emitted when a user joins a channel (including when the client itself joins a channel). 333 | See the ``raw`` event for details on the ``message`` object. 334 | 335 | .. js:data:: 'join#channel' 336 | 337 | ``function (nick, message) { }`` 338 | 339 | Same as the 'join' event, but only emitted for the specified channel. 340 | See the ``raw`` event for details on the ``message`` object. 341 | 342 | .. js:data:: 'part' 343 | 344 | ``function (channel, nick, reason, message) { }`` 345 | 346 | Emitted when a user parts a channel (including when the client itself parts a channel). 347 | See the ``raw`` event for details on the ``message`` object. 348 | 349 | .. js:data:: 'part#channel' 350 | 351 | ``function (nick, reason, message) { }`` 352 | 353 | Same as the 'part' event, but only emitted for the specified channel. 354 | See the ``raw`` event for details on the ``message`` object. 355 | 356 | .. js:data:: 'quit' 357 | 358 | ``function (nick, reason, channels, message) { }`` 359 | 360 | Emitted when a user disconnects from the IRC server, leaving the specified array of channels. 361 | Channels are emitted case-lowered. 362 | 363 | See the ``raw`` event for details on the ``message`` object. 364 | 365 | .. js:data:: 'kick' 366 | 367 | ``function (channel, nick, by, reason, message) { }`` 368 | 369 | Emitted when a user is kicked from a channel. 370 | See the ``raw`` event for details on the ``message`` object. 371 | 372 | .. js:data:: 'kick#channel' 373 | 374 | ``function (nick, by, reason, message) { }`` 375 | 376 | Same as the 'kick' event, but only emitted for the specified channel. 377 | See the ``raw`` event for details on the ``message`` object. 378 | 379 | .. js:data:: 'kill' 380 | 381 | ``function (nick, reason, channels, message) { }`` 382 | 383 | Emitted when a user is killed from the IRC server. 384 | The ``channels`` parameter is an array of channels the killed user was in, those known to the client (that is, the ones the bot was present in). 385 | Channels are emitted case-lowered. 386 | 387 | See the ``raw`` event for details on the ``message`` object. 388 | 389 | .. js:data:: 'nick' 390 | 391 | ``function (oldnick, newnick, channels, message) { }`` 392 | 393 | Emitted when a user changes nick, with the channels the user is known to be in. 394 | Channels are emitted case-lowered. 395 | 396 | See the ``raw`` event for details on the ``message`` object. 397 | 398 | .. js:data:: '+mode' 399 | 400 | ``function (channel, by, mode, argument, message) { }`` 401 | 402 | Emitted when a mode is added to a user or channel. 403 | The ``channel`` parameter is the channel which the mode is being set on/in. 404 | The ``by`` parameter is the user setting the mode. 405 | The ``mode`` parameter is the single character mode identifier. 406 | If the mode is being set on a user, ``argument`` is the nick of the user. If the mode is being set on a channel, ``argument`` is the argument to the mode. 407 | If a channel mode doesn't have any arguments, ``argument`` will be 'undefined'. 408 | See the ``raw`` event for details on the ``message`` object. 409 | 410 | .. js:data:: '-mode' 411 | 412 | ``function (channel, by, mode, argument, message) { }`` 413 | 414 | Emitted when a mode is removed from a user or channel. 415 | The other arguments are as in the ``+mode`` event. 416 | 417 | .. js:data:: 'whois' 418 | 419 | ``function (info) { }`` 420 | 421 | Emitted when the server finishes outputting a WHOIS response. 422 | The information should look something like:: 423 | 424 | { 425 | nick: "Throne", 426 | user: "throne3d", 427 | host: "10.0.0.1", 428 | realname: "Unknown", 429 | channels: ["@#throne3d", "#blah", "#channel"], 430 | server: "irc.example.com", 431 | serverinfo: "Example IRC server", 432 | operator: "is an IRC Operator" 433 | } 434 | 435 | .. js:data:: 'ping' 436 | 437 | ``function (server) { }`` 438 | 439 | Emitted when a server PINGs the client. 440 | The client will automatically send a PONG request just before this is emitted. 441 | 442 | .. js:data:: 'ctcp' 443 | 444 | ``function (from, to, text, type, message) { }`` 445 | 446 | Emitted when a CTCP notice or privmsg was received (``type`` is either ``notice`` or ``privmsg``). 447 | See the ``raw`` event for details on the ``message`` object. 448 | 449 | .. js:data:: 'ctcp-notice' 450 | 451 | ``function (from, to, text, message) { }`` 452 | 453 | Emitted when a CTCP notice is received. 454 | See the ``raw`` event for details on the ``message`` object. 455 | 456 | .. js:data:: 'ctcp-privmsg' 457 | 458 | ``function (from, to, text, message) { }`` 459 | 460 | Emitted when a CTCP privmsg was received. 461 | See the ``raw`` event for details on the ``message`` object. 462 | 463 | .. js:data:: 'ctcp-version' 464 | 465 | ``function (from, to, message) { }`` 466 | 467 | Emitted when a CTCP VERSION request is received. 468 | See the ``raw`` event for details on the ``message`` object. 469 | 470 | .. js:data:: 'channellist_start' 471 | 472 | ``function () {}`` 473 | 474 | Emitted when the server starts a new channel listing. 475 | 476 | .. js:data:: 'channellist_item' 477 | 478 | ``function (channel_info) {}`` 479 | 480 | Emitted for each channel the server returns in a channel listing. 481 | The ``channel_info`` object contains keys 'name', 'users' (number of users in the channel), and 'topic'. 482 | 483 | .. js:data:: 'channellist' 484 | 485 | ``function (channel_list) {}`` 486 | 487 | Emitted when the server has finished returning a channel list. 488 | The ``channel_list`` array is simply a list of the objects that were returned in the intervening ``channellist_item`` events. 489 | 490 | This data is also available through the ``Client.channellist`` property after this event has fired. 491 | 492 | .. js:data:: 'raw' 493 | 494 | ``function (message) { }`` 495 | 496 | Emitted when the client receives a "message" from the server. 497 | A message is a single line of data from the server. 498 | The ``message`` parameter to the callback is the processed version of this message, and contains something of the form: 499 | 500 | .. code-block:: js 501 | 502 | message = { 503 | prefix: "user!~realname@example.host", // the prefix for the message (optional, user prefix here) 504 | prefix: "irc.example.com", // the prefix for the message (optional, server prefix here) 505 | nick: "user", // the nickname portion of the prefix (if the prefix is a user prefix) 506 | user: "~realname", // the username portion of the prefix (if the prefix is a user prefix) 507 | host: "example.host", // the hostname portion of the prefix (if the prefix is a user prefix) 508 | server: "irc.example.com", // the server address (if the prefix was a server prefix) 509 | rawCommand: "PRIVMSG", // the command exactly as sent from the server 510 | command: "PRIVMSG", // human-readable version of the command (if it was previously, say, numeric) 511 | commandType: "normal", // normal, error, or reply 512 | args: ['#test', 'test message'] // arguments to the command 513 | } 514 | 515 | You can read more about the IRC protocol in `RFC 1459 516 | `_ and `RFC 2812 `_. 517 | 518 | .. js:data:: 'error' 519 | 520 | ``function (message) { }`` 521 | 522 | Emitted whenever the server responds with an error-type message. 523 | See the ``raw`` event for details on the ``message`` object. 524 | Unhandled messages, although they are shown as errors in the log, are not emitted using this event: see ``unhandled``. 525 | 526 | .. js:data:: 'netError' 527 | 528 | ``function (exception) { }`` 529 | 530 | Emitted when the socket connection to the server emits an error event. 531 | See `net.Socket's error event `_ for more information. 532 | 533 | .. js:data:: 'unhandled' 534 | 535 | ``function (message) { }`` 536 | 537 | Emitted whenever the server responds with a message the bot doesn't recognize and doesn't handle. 538 | See the ``raw`` event for details on the ``message`` object. 539 | 540 | This must not be relied on to emit particular event codes, as the codes the bot does and does not handle can change between minor versions. 541 | It should instead be used as a handler to do something when the bot does not recognize a message, such as warning a user. 542 | 543 | Colors 544 | ------ 545 | 546 | .. js:function:: irc.colors.wrap(color, text [, reset_color]) 547 | 548 | Takes a color by name, text, and optionally what color to return to after the text. 549 | 550 | :param string color: the name of the desired color, as a string 551 | :param string text: the text you want colorized 552 | :param string reset_color: the name of the color you want set after the text (defaults to 'reset') 553 | 554 | .. js:data:: irc.colors.codes 555 | 556 | Lists the colors available and the relevant mIRC color codes. 557 | 558 | .. code-block:: js 559 | 560 | { 561 | white: '\u000300', 562 | black: '\u000301', 563 | dark_blue: '\u000302', 564 | dark_green: '\u000303', 565 | light_red: '\u000304', 566 | dark_red: '\u000305', 567 | magenta: '\u000306', 568 | orange: '\u000307', 569 | yellow: '\u000308', 570 | light_green: '\u000309', 571 | cyan: '\u000310', 572 | light_cyan: '\u000311', 573 | light_blue: '\u000312', 574 | light_magenta: '\u000313', 575 | gray: '\u000314', 576 | light_gray: '\u000315', 577 | reset: '\u000f', 578 | } 579 | 580 | Encoding 581 | ------ 582 | 583 | .. js:function:: irc.canConvertEncoding() 584 | 585 | Tests if the library can convert messages with different encodings, using the ``chardet`` and ``iconv-lite`` libraries. 586 | Allows you to more easily (programmatically) detect if the ``encoding`` option will result in any effect, instead of setting it and otherwise resulting in errors. 587 | (See also ``Client.canConvertEncoding``, an alias for this function.) 588 | 589 | Internal 590 | ------ 591 | 592 | .. js:data:: Client.conn 593 | 594 | Socket to the server. 595 | Rarely, if ever, needed; use ``Client.send`` instead. 596 | 597 | .. js:data:: Client.chans 598 | 599 | The list of channels joined. 600 | Includes channel modes, user lists, and topic information. 601 | It is only updated *after* the server recognizes the join. 602 | 603 | .. js:data:: Client.nick 604 | 605 | The current nick of the client. 606 | Updated if the nick changes (e.g. upon nick collision when connecting to a server). 607 | 608 | .. js:data:: Client._whoisData 609 | 610 | A buffer of whois data, as whois responses are sent over multiple messages. 611 | 612 | .. js:function:: Client._addWhoisData(nick, key, value, onlyIfExists) 613 | 614 | Adds the relevant whois data (key-value pair), for the specified nick, optionally only if the value exists (is truthy). 615 | 616 | .. js:function:: Client._clearWhoisData(nick) 617 | 618 | Clears whois data for the specified nick. 619 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/node-irc.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/node-irc.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/node-irc" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/node-irc" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # node-irc documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Oct 1 00:02:31 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'node-irc' 44 | copyright = u'2020, Edward Jones' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.11' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.11.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'node-ircdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'node-irc.tex', u'node-irc Documentation', 182 | u'Edward Jones', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'node-irc', u'node-irc Documentation', 215 | [u'Edward Jones'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to node-irc's documentation! 2 | ==================================== 3 | 4 | .. include:: ../README.rst 5 | 6 | More detailed docs: 7 | ------------------- 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | API 13 | 14 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\node-irc.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\node-irc.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /example/bot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var irc = require('../'); 4 | 5 | var bot = new irc.Client('irc.dollyfish.net.nz', 'nodebot', { 6 | debug: true, 7 | channels: ['#test', '#othertest'] 8 | }); 9 | 10 | bot.addListener('error', function(message) { 11 | console.error('ERROR: %s: %s', message.command, message.args.join(' ')); 12 | }); 13 | 14 | bot.addListener('message#blah', function(from, message) { 15 | console.log('<%s> %s', from, message); 16 | }); 17 | 18 | bot.addListener('message', function(from, to, message) { 19 | console.log('%s => %s: %s', from, to, message); 20 | 21 | if (to.match(/^[#&]/)) { 22 | // channel message 23 | if (message.match(/hello/i)) { 24 | bot.say(to, 'Hello there ' + from); 25 | } 26 | if (message.match(/dance/)) { 27 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D\\-<\u0001'); }, 1000); 28 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D|-<\u0001'); }, 2000); 29 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D/-<\u0001'); }, 3000); 30 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D|-<\u0001'); }, 4000); 31 | } 32 | } 33 | else { 34 | // private message 35 | console.log('private message'); 36 | } 37 | }); 38 | bot.addListener('pm', function(nick, message) { 39 | console.log('Got private message from %s: %s', nick, message); 40 | }); 41 | bot.addListener('join', function(channel, who) { 42 | console.log('%s has joined %s', who, channel); 43 | }); 44 | bot.addListener('part', function(channel, who, reason) { 45 | console.log('%s has left %s: %s', who, channel, reason); 46 | }); 47 | bot.addListener('kick', function(channel, who, by, reason) { 48 | console.log('%s was kicked from %s by %s: %s', who, channel, by, reason); 49 | }); 50 | -------------------------------------------------------------------------------- /example/secure.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var irc = require('../'); 4 | /* 5 | * To set the key/cert explicitly, you could do the following 6 | var fs = require('fs'); 7 | 8 | var options = { 9 | key: fs.readFileSync('privkey.pem'), 10 | cert: fs.readFileSync('certificate.crt') 11 | }; 12 | */ 13 | 14 | // Or to just use defaults 15 | var options = true; 16 | 17 | var bot = new irc.Client('chat.us.freenode.net', 'nodebot', { 18 | port: 6697, 19 | debug: true, 20 | secure: options, 21 | channels: ['#botwar'] 22 | }); 23 | 24 | bot.addListener('error', function(message) { 25 | console.error('ERROR: %s: %s', message.command, message.args.join(' ')); 26 | }); 27 | 28 | bot.addListener('message#blah', function(from, message) { 29 | console.log('<%s> %s', from, message); 30 | }); 31 | 32 | bot.addListener('message', function(from, to, message) { 33 | console.log('%s => %s: %s', from, to, message); 34 | 35 | if (to.match(/^[#&]/)) { 36 | // channel message 37 | if (message.match(/hello/i)) { 38 | bot.say(to, 'Hello there ' + from); 39 | } 40 | if (message.match(/dance/)) { 41 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D\\-<\u0001'); }, 1000); 42 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D|-<\u0001'); }, 2000); 43 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D/-<\u0001'); }, 3000); 44 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D|-<\u0001'); }, 4000); 45 | } 46 | } 47 | else { 48 | // private message 49 | console.log('private message'); 50 | } 51 | }); 52 | bot.addListener('pm', function(nick, message) { 53 | console.log('Got private message from %s: %s', nick, message); 54 | }); 55 | bot.addListener('join', function(channel, who) { 56 | console.log('%s has joined %s', who, channel); 57 | }); 58 | bot.addListener('part', function(channel, who, reason) { 59 | console.log('%s has left %s: %s', who, channel, reason); 60 | }); 61 | bot.addListener('kick', function(channel, who, by, reason) { 62 | console.log('%s was kicked from %s by %s: %s', who, channel, by, reason); 63 | }); 64 | -------------------------------------------------------------------------------- /lib/codes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '001': { 3 | name: 'rpl_welcome', 4 | type: 'reply' 5 | }, 6 | '002': { 7 | name: 'rpl_yourhost', 8 | type: 'reply' 9 | }, 10 | '003': { 11 | name: 'rpl_created', 12 | type: 'reply' 13 | }, 14 | '004': { 15 | name: 'rpl_myinfo', 16 | type: 'reply' 17 | }, 18 | '005': { 19 | name: 'rpl_isupport', 20 | type: 'reply' 21 | }, 22 | 200: { 23 | name: 'rpl_tracelink', 24 | type: 'reply' 25 | }, 26 | 201: { 27 | name: 'rpl_traceconnecting', 28 | type: 'reply' 29 | }, 30 | 202: { 31 | name: 'rpl_tracehandshake', 32 | type: 'reply' 33 | }, 34 | 203: { 35 | name: 'rpl_traceunknown', 36 | type: 'reply' 37 | }, 38 | 204: { 39 | name: 'rpl_traceoperator', 40 | type: 'reply' 41 | }, 42 | 205: { 43 | name: 'rpl_traceuser', 44 | type: 'reply' 45 | }, 46 | 206: { 47 | name: 'rpl_traceserver', 48 | type: 'reply' 49 | }, 50 | 208: { 51 | name: 'rpl_tracenewtype', 52 | type: 'reply' 53 | }, 54 | 211: { 55 | name: 'rpl_statslinkinfo', 56 | type: 'reply' 57 | }, 58 | 212: { 59 | name: 'rpl_statscommands', 60 | type: 'reply' 61 | }, 62 | 213: { 63 | name: 'rpl_statscline', 64 | type: 'reply' 65 | }, 66 | 214: { 67 | name: 'rpl_statsnline', 68 | type: 'reply' 69 | }, 70 | 215: { 71 | name: 'rpl_statsiline', 72 | type: 'reply' 73 | }, 74 | 216: { 75 | name: 'rpl_statskline', 76 | type: 'reply' 77 | }, 78 | 218: { 79 | name: 'rpl_statsyline', 80 | type: 'reply' 81 | }, 82 | 219: { 83 | name: 'rpl_endofstats', 84 | type: 'reply' 85 | }, 86 | 221: { 87 | name: 'rpl_umodeis', 88 | type: 'reply' 89 | }, 90 | 241: { 91 | name: 'rpl_statslline', 92 | type: 'reply' 93 | }, 94 | 242: { 95 | name: 'rpl_statsuptime', 96 | type: 'reply' 97 | }, 98 | 243: { 99 | name: 'rpl_statsoline', 100 | type: 'reply' 101 | }, 102 | 244: { 103 | name: 'rpl_statshline', 104 | type: 'reply' 105 | }, 106 | 250: { 107 | name: 'rpl_statsconn', 108 | type: 'reply' 109 | }, 110 | 251: { 111 | name: 'rpl_luserclient', 112 | type: 'reply' 113 | }, 114 | 252: { 115 | name: 'rpl_luserop', 116 | type: 'reply' 117 | }, 118 | 253: { 119 | name: 'rpl_luserunknown', 120 | type: 'reply' 121 | }, 122 | 254: { 123 | name: 'rpl_luserchannels', 124 | type: 'reply' 125 | }, 126 | 255: { 127 | name: 'rpl_luserme', 128 | type: 'reply' 129 | }, 130 | 256: { 131 | name: 'rpl_adminme', 132 | type: 'reply' 133 | }, 134 | 257: { 135 | name: 'rpl_adminloc1', 136 | type: 'reply' 137 | }, 138 | 258: { 139 | name: 'rpl_adminloc2', 140 | type: 'reply' 141 | }, 142 | 259: { 143 | name: 'rpl_adminemail', 144 | type: 'reply' 145 | }, 146 | 261: { 147 | name: 'rpl_tracelog', 148 | type: 'reply' 149 | }, 150 | 265: { 151 | name: 'rpl_localusers', 152 | type: 'reply' 153 | }, 154 | 266: { 155 | name: 'rpl_globalusers', 156 | type: 'reply' 157 | }, 158 | 300: { 159 | name: 'rpl_none', 160 | type: 'reply' 161 | }, 162 | 301: { 163 | name: 'rpl_away', 164 | type: 'reply' 165 | }, 166 | 302: { 167 | name: 'rpl_userhost', 168 | type: 'reply' 169 | }, 170 | 303: { 171 | name: 'rpl_ison', 172 | type: 'reply' 173 | }, 174 | 305: { 175 | name: 'rpl_unaway', 176 | type: 'reply' 177 | }, 178 | 306: { 179 | name: 'rpl_nowaway', 180 | type: 'reply' 181 | }, 182 | 307: { 183 | type: 'reply' 184 | }, 185 | 311: { 186 | name: 'rpl_whoisuser', 187 | type: 'reply' 188 | }, 189 | 312: { 190 | name: 'rpl_whoisserver', 191 | type: 'reply' 192 | }, 193 | 313: { 194 | name: 'rpl_whoisoperator', 195 | type: 'reply' 196 | }, 197 | 314: { 198 | name: 'rpl_whowasuser', 199 | type: 'reply' 200 | }, 201 | 315: { 202 | name: 'rpl_endofwho', 203 | type: 'reply' 204 | }, 205 | 317: { 206 | name: 'rpl_whoisidle', 207 | type: 'reply' 208 | }, 209 | 318: { 210 | name: 'rpl_endofwhois', 211 | type: 'reply' 212 | }, 213 | 319: { 214 | name: 'rpl_whoischannels', 215 | type: 'reply' 216 | }, 217 | 321: { 218 | name: 'rpl_liststart', 219 | type: 'reply' 220 | }, 221 | 322: { 222 | name: 'rpl_list', 223 | type: 'reply' 224 | }, 225 | 323: { 226 | name: 'rpl_listend', 227 | type: 'reply' 228 | }, 229 | 324: { 230 | name: 'rpl_channelmodeis', 231 | type: 'reply' 232 | }, 233 | 329: { 234 | name: 'rpl_creationtime', 235 | type: 'reply' 236 | }, 237 | 331: { 238 | name: 'rpl_notopic', 239 | type: 'reply' 240 | }, 241 | 332: { 242 | name: 'rpl_topic', 243 | type: 'reply' 244 | }, 245 | 333: { 246 | name: 'rpl_topicwhotime', 247 | type: 'reply' 248 | }, 249 | 335: { 250 | name: 'rpl_whoisbot', 251 | type: 'reply' 252 | }, 253 | 341: { 254 | name: 'rpl_inviting', 255 | type: 'reply' 256 | }, 257 | 342: { 258 | name: 'rpl_summoning', 259 | type: 'reply' 260 | }, 261 | 351: { 262 | name: 'rpl_version', 263 | type: 'reply' 264 | }, 265 | 352: { 266 | name: 'rpl_whoreply', 267 | type: 'reply' 268 | }, 269 | 353: { 270 | name: 'rpl_namreply', 271 | type: 'reply' 272 | }, 273 | 364: { 274 | name: 'rpl_links', 275 | type: 'reply' 276 | }, 277 | 365: { 278 | name: 'rpl_endoflinks', 279 | type: 'reply' 280 | }, 281 | 366: { 282 | name: 'rpl_endofnames', 283 | type: 'reply' 284 | }, 285 | 367: { 286 | name: 'rpl_banlist', 287 | type: 'reply' 288 | }, 289 | 368: { 290 | name: 'rpl_endofbanlist', 291 | type: 'reply' 292 | }, 293 | 369: { 294 | name: 'rpl_endofwhowas', 295 | type: 'reply' 296 | }, 297 | 371: { 298 | name: 'rpl_info', 299 | type: 'reply' 300 | }, 301 | 372: { 302 | name: 'rpl_motd', 303 | type: 'reply' 304 | }, 305 | 374: { 306 | name: 'rpl_endofinfo', 307 | type: 'reply' 308 | }, 309 | 375: { 310 | name: 'rpl_motdstart', 311 | type: 'reply' 312 | }, 313 | 376: { 314 | name: 'rpl_endofmotd', 315 | type: 'reply' 316 | }, 317 | 378: { 318 | name: 'rpl_whoishost', 319 | type: 'reply' 320 | }, 321 | 379: { 322 | name: 'rpl_whoismodes', 323 | type: 'reply' 324 | }, 325 | 381: { 326 | name: 'rpl_youreoper', 327 | type: 'reply' 328 | }, 329 | 382: { 330 | name: 'rpl_rehashing', 331 | type: 'reply' 332 | }, 333 | 391: { 334 | name: 'rpl_time', 335 | type: 'reply' 336 | }, 337 | 392: { 338 | name: 'rpl_usersstart', 339 | type: 'reply' 340 | }, 341 | 393: { 342 | name: 'rpl_users', 343 | type: 'reply' 344 | }, 345 | 394: { 346 | name: 'rpl_endofusers', 347 | type: 'reply' 348 | }, 349 | 395: { 350 | name: 'rpl_nousers', 351 | type: 'reply' 352 | }, 353 | 401: { 354 | name: 'err_nosuchnick', 355 | type: 'error' 356 | }, 357 | 402: { 358 | name: 'err_nosuchserver', 359 | type: 'error' 360 | }, 361 | 403: { 362 | name: 'err_nosuchchannel', 363 | type: 'error' 364 | }, 365 | 404: { 366 | name: 'err_cannotsendtochan', 367 | type: 'error' 368 | }, 369 | 405: { 370 | name: 'err_toomanychannels', 371 | type: 'error' 372 | }, 373 | 406: { 374 | name: 'err_wasnosuchnick', 375 | type: 'error' 376 | }, 377 | 407: { 378 | name: 'err_toomanytargets', 379 | type: 'error' 380 | }, 381 | 409: { 382 | name: 'err_noorigin', 383 | type: 'error' 384 | }, 385 | 411: { 386 | name: 'err_norecipient', 387 | type: 'error' 388 | }, 389 | 412: { 390 | name: 'err_notexttosend', 391 | type: 'error' 392 | }, 393 | 413: { 394 | name: 'err_notoplevel', 395 | type: 'error' 396 | }, 397 | 414: { 398 | name: 'err_wildtoplevel', 399 | type: 'error' 400 | }, 401 | 421: { 402 | name: 'err_unknowncommand', 403 | type: 'error' 404 | }, 405 | 422: { 406 | name: 'err_nomotd', 407 | type: 'error' 408 | }, 409 | 423: { 410 | name: 'err_noadmininfo', 411 | type: 'error' 412 | }, 413 | 424: { 414 | name: 'err_fileerror', 415 | type: 'error' 416 | }, 417 | 431: { 418 | name: 'err_nonicknamegiven', 419 | type: 'error' 420 | }, 421 | 432: { 422 | name: 'err_erroneusnickname', 423 | type: 'error' 424 | }, 425 | 433: { 426 | name: 'err_nicknameinuse', 427 | type: 'error' 428 | }, 429 | 436: { 430 | name: 'err_nickcollision', 431 | type: 'error' 432 | }, 433 | 441: { 434 | name: 'err_usernotinchannel', 435 | type: 'error' 436 | }, 437 | 442: { 438 | name: 'err_notonchannel', 439 | type: 'error' 440 | }, 441 | 443: { 442 | name: 'err_useronchannel', 443 | type: 'error' 444 | }, 445 | 444: { 446 | name: 'err_nologin', 447 | type: 'error' 448 | }, 449 | 445: { 450 | name: 'err_summondisabled', 451 | type: 'error' 452 | }, 453 | 446: { 454 | name: 'err_usersdisabled', 455 | type: 'error' 456 | }, 457 | 451: { 458 | name: 'err_notregistered', 459 | type: 'error' 460 | }, 461 | 461: { 462 | name: 'err_needmoreparams', 463 | type: 'error' 464 | }, 465 | 462: { 466 | name: 'err_alreadyregistred', 467 | type: 'error' 468 | }, 469 | 463: { 470 | name: 'err_nopermforhost', 471 | type: 'error' 472 | }, 473 | 464: { 474 | name: 'err_passwdmismatch', 475 | type: 'error' 476 | }, 477 | 465: { 478 | name: 'err_yourebannedcreep', 479 | type: 'error' 480 | }, 481 | 467: { 482 | name: 'err_keyset', 483 | type: 'error' 484 | }, 485 | 471: { 486 | name: 'err_channelisfull', 487 | type: 'error' 488 | }, 489 | 472: { 490 | name: 'err_unknownmode', 491 | type: 'error' 492 | }, 493 | 473: { 494 | name: 'err_inviteonlychan', 495 | type: 'error' 496 | }, 497 | 474: { 498 | name: 'err_bannedfromchan', 499 | type: 'error' 500 | }, 501 | 475: { 502 | name: 'err_badchannelkey', 503 | type: 'error' 504 | }, 505 | 477: { 506 | type: 'error' 507 | }, 508 | 481: { 509 | name: 'err_noprivileges', 510 | type: 'error' 511 | }, 512 | 482: { 513 | name: 'err_chanoprivsneeded', 514 | type: 'error' 515 | }, 516 | 483: { 517 | name: 'err_cantkillserver', 518 | type: 'error' 519 | }, 520 | 491: { 521 | name: 'err_nooperhost', 522 | type: 'error' 523 | }, 524 | 501: { 525 | name: 'err_umodeunknownflag', 526 | type: 'error' 527 | }, 528 | 502: { 529 | name: 'err_usersdontmatch', 530 | type: 'error' 531 | }, 532 | 671: { 533 | name: 'rpl_whoissecure', 534 | type: 'reply' 535 | }, 536 | 900: { 537 | name: 'rpl_loggedin', 538 | type: 'reply' 539 | }, 540 | 901: { 541 | name: 'rpl_loggedout', 542 | type: 'reply' 543 | }, 544 | 902: { 545 | name: 'err_nicklocked', 546 | type: 'error' 547 | }, 548 | 903: { 549 | name: 'rpl_saslsuccess', 550 | type: 'reply' 551 | }, 552 | 904: { 553 | name: 'err_saslfail', 554 | type: 'error' 555 | }, 556 | 905: { 557 | name: 'err_sasltoolong', 558 | type: 'error' 559 | }, 560 | 906: { 561 | name: 'err_saslaborted', 562 | type: 'error' 563 | }, 564 | 907: { 565 | name: 'err_saslalready', 566 | type: 'error' 567 | }, 568 | 908: { 569 | name: 'rpl_saslmechs', 570 | type: 'reply' 571 | } 572 | }; 573 | -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | var codes = { 2 | white: '\u000300', 3 | black: '\u000301', 4 | dark_blue: '\u000302', 5 | dark_green: '\u000303', 6 | light_red: '\u000304', 7 | dark_red: '\u000305', 8 | magenta: '\u000306', 9 | orange: '\u000307', 10 | yellow: '\u000308', 11 | light_green: '\u000309', 12 | cyan: '\u000310', 13 | light_cyan: '\u000311', 14 | light_blue: '\u000312', 15 | light_magenta: '\u000313', 16 | gray: '\u000314', 17 | light_gray: '\u000315', 18 | 19 | bold: '\u0002', 20 | underline: '\u001f', 21 | 22 | reset: '\u000f' 23 | }; 24 | exports.codes = codes; 25 | 26 | function wrap(color, text, resetColor) { 27 | if (codes[color]) { 28 | text = codes[color] + text; 29 | text += (codes[resetColor]) ? codes[resetColor] : codes.reset; 30 | } 31 | return text; 32 | } 33 | exports.wrap = wrap; 34 | -------------------------------------------------------------------------------- /lib/cycling_ping_timer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var EventEmitter = require('events'); 5 | 6 | /** 7 | * This class encapsulates the ping timeout functionality. 8 | * When enough silence (lack of server-sent activity) passes, an object of this type will emit a 'wantPing' event, indicating you should send a PING message to the server in order to get some signs of life from it. 9 | * If enough time passes after that (i.e. server does not respond to PING), then an object of this type will emit a 'pingTimeout' event. 10 | * 11 | * To start the gears turning, call start() on an instance of this class to put it in the 'started' state. 12 | * 13 | * When server-side activity occurs, call notifyOfActivity() on the object. 14 | * 15 | * When a pingTimeout occurs, the object will go into the 'stopped' state. 16 | */ 17 | var ctr = 0; 18 | 19 | function CyclingPingTimer(client) { 20 | var self = this; 21 | self.timerNumber = ctr++; 22 | self.started = false; 23 | 24 | // Only one of these two should be non-null at any given time. 25 | self.loopingTimeout = null; 26 | self.pingWaitTimeout = null; 27 | 28 | // conditionally log debug messages 29 | function debug(msg) { 30 | client.out.debug('CyclingPingTimer ' + self.timerNumber + ':', msg); 31 | } 32 | 33 | // set up EventEmitter functionality 34 | EventEmitter.call(self); 35 | 36 | self.on('wantPing', function() { 37 | debug('server silent for too long, let\'s send a PING'); 38 | self.pingWaitTimeout = setTimeout(function() { 39 | self.stop(); 40 | debug('ping timeout!'); 41 | self.emit('pingTimeout'); 42 | }, client.opt.millisecondsBeforePingTimeout); 43 | }); 44 | 45 | self.notifyOfActivity = function() { 46 | if (self.started) { 47 | _stop(); 48 | _start(); 49 | } 50 | }; 51 | 52 | self.stop = function() { 53 | if (!self.started) { 54 | return; 55 | } 56 | debug('ping timer stopped'); 57 | _stop(); 58 | }; 59 | function _stop() { 60 | self.started = false; 61 | 62 | clearTimeout(self.loopingTimeout); 63 | clearTimeout(self.pingWaitTimeout); 64 | 65 | self.loopingTimeout = null; 66 | self.pingWaitTimeout = null; 67 | } 68 | 69 | self.start = function() { 70 | if (self.started) { 71 | debug('can\'t start, not stopped!'); 72 | return; 73 | } 74 | debug('ping timer started'); 75 | _start(); 76 | }; 77 | function _start() { 78 | self.started = true; 79 | 80 | self.loopingTimeout = setTimeout(function() { 81 | self.loopingTimeout = null; 82 | self.emit('wantPing'); 83 | }, client.opt.millisecondsOfSilenceBeforePingSent); 84 | } 85 | } 86 | 87 | util.inherits(CyclingPingTimer, EventEmitter); 88 | 89 | module.exports = CyclingPingTimer; 90 | -------------------------------------------------------------------------------- /lib/parse_message.js: -------------------------------------------------------------------------------- 1 | var ircColors = require('irc-colors'); 2 | var replyFor = require('./codes'); 3 | 4 | /** 5 | * parseMessage(line, stripColors) 6 | * 7 | * takes a raw "line" from the IRC server and turns it into an object with 8 | * useful keys 9 | * @param {String} line Raw message from IRC server. 10 | * @param {Boolean} stripColors If true, strip IRC colors. 11 | * @param {Boolean} enableStrictParse If true, will try to conform to RFC2812 strictly for parsing usernames (and disallow eg CJK characters). 12 | * @return {Object} A parsed message object. 13 | */ 14 | module.exports = function parseMessage(line, stripColors, enableStrictParse) { 15 | var message = {}; 16 | var match; 17 | 18 | if (stripColors) { 19 | line = ircColors.stripColorsAndStyle(line); 20 | } 21 | 22 | // Parse prefix 23 | match = line.match(/^:([^ ]+) +/); 24 | if (match) { 25 | message.prefix = match[1]; 26 | line = line.replace(/^:[^ ]+ +/, ''); 27 | if (enableStrictParse) { 28 | match = message.prefix.match(/^([_a-zA-Z0-9~[\]\\`^{}|-]*)(!([^@]+)@(.*))?$/); 29 | } else { 30 | match = message.prefix.match(/^([\u1100-\u11FF\u3040-\u309fF\u30A0-\u30FF\u3130-\u318F\u31F0-\u31FF\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF_a-zA-Z0-9~[\]\\/?`^{}|-]*)(!([^@]+)@(.*))?$/); 31 | } 32 | if (match) { 33 | message.nick = match[1]; 34 | message.user = match[3]; 35 | message.host = match[4]; 36 | } 37 | else { 38 | message.server = message.prefix; 39 | } 40 | } 41 | 42 | // Parse command 43 | match = line.match(/^([^ ]+) */); 44 | message.command = match[1]; 45 | message.rawCommand = match[1]; 46 | message.commandType = 'normal'; 47 | line = line.replace(/^[^ ]+ +/, ''); 48 | 49 | var codeData = replyFor[message.rawCommand]; 50 | if (codeData) { 51 | if ('name' in codeData) message.command = codeData.name; 52 | message.commandType = codeData.type; 53 | } 54 | 55 | message.args = []; 56 | var middle, trailing; 57 | 58 | // Parse parameters 59 | if (line.search(/^:|\s+:/) !== -1) { 60 | match = line.match(/(.*?)(?:^:|\s+:)(.*)/); 61 | middle = match[1].trimRight(); 62 | trailing = match[2]; 63 | } 64 | else { 65 | middle = line; 66 | } 67 | 68 | if (middle.length) 69 | message.args = middle.split(/ +/); 70 | 71 | if (typeof trailing !== 'undefined' && trailing.length) 72 | message.args.push(trailing); 73 | 74 | return message; 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "irc-upd", 3 | "description": "An IRC client library for node", 4 | "version": "0.11.0", 5 | "author": "Edward Jones ", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "posttest": "nyc report && npm run lint", 9 | "save-coverage": "nyc report --reporter=lcov", 10 | "test": "nyc --reporter=none mocha test/test-*.js" 11 | }, 12 | "contributors": [ 13 | "Fionn Kelleher ", 14 | "xAndy ", 15 | "Mischa Spiegelmock ", 16 | "Justin Gallardo ", 17 | "Chris Nehren ", 18 | "Henri Niemeläinen ", 19 | "Alex Miles ", 20 | "Simmo Saan ", 21 | "Martyn Smith " 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "http://github.com/Throne3d/node-irc" 26 | }, 27 | "bugs": { 28 | "url": "http://github.com/Throne3d/node-irc/issues" 29 | }, 30 | "main": "lib/irc", 31 | "engines": { 32 | "node": ">=0.10.0" 33 | }, 34 | "license": "GPL-3.0", 35 | "dependencies": { 36 | "irc-colors": "^1.5.0" 37 | }, 38 | "optionalDependencies": { 39 | "chardet": "^1.2.1", 40 | "iconv-lite": "^0.6.2" 41 | }, 42 | "devDependencies": { 43 | "ansi-color": "0.2.1", 44 | "chai": "^4.3.4", 45 | "coveralls": "^3.1.0", 46 | "eslint": "^7.25.0", 47 | "mocha": "^8.3.2", 48 | "nyc": "^15.1.0", 49 | "proxyquire": "^2.1.3", 50 | "sinon": "^10.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var irc = require('./lib/irc.js'); 4 | var util = require('util'); 5 | var color = require('ansi-color').set; 6 | 7 | var ircServer = process.argv[2] || process.env.IRCSERVER || 'localhost'; 8 | 9 | console.log('Starting IRC connection to', ircServer, 'with REPL'); 10 | 11 | var c = new irc.Client( 12 | ircServer, 13 | 'nodebot', 14 | { 15 | channels: ['#test'], 16 | debug: true 17 | } 18 | ); 19 | 20 | var messages = []; 21 | c.addListener('raw', function(message) { messages.push(message); }); 22 | c.addListener('unhandled', function(message) { 23 | console.log(color('unhandled: ', 'red'), message); 24 | }); 25 | c.addListener('error', function(message) { console.log(color('error: ', 'red'), message); }); 26 | 27 | function printRecent(number) { 28 | number = number || 1; 29 | var toPrint = messages.slice(messages.length-number, messages.length); 30 | toPrint.forEach(function(message) { 31 | console.log(message); 32 | }); 33 | } 34 | 35 | var repl = require('repl').start('> '); 36 | repl.context.repl = repl; 37 | repl.context.util = util; 38 | repl.context.irc = irc; 39 | repl.context.c = c; 40 | repl.context.printRecent = printRecent; 41 | repl.context.recent = printRecent; 42 | 43 | repl.addListener('exit', function() { 44 | console.log("\nClosing session"); 45 | c.disconnect('Closing session'); 46 | }); 47 | -------------------------------------------------------------------------------- /test/data/fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic": { 3 | "sent": [ 4 | "NICK testbot", 5 | "USER nodebot 8 * :nodeJS IRC client", 6 | "QUIT :node-irc says goodbye" 7 | ], 8 | "received": [ 9 | ":localhost 001 testbot :Welcome to the Internet Relay Chat Network testbot\r\n" 10 | ] 11 | }, 12 | "double-CRLF": { 13 | "sent": [ 14 | ["NICK testbot", "Client sent NICK message"], 15 | ["USER nodebot 8 * :nodeJS IRC client", "Client sent USER message"], 16 | ["QUIT :node-irc says goodbye", "Client sent QUIT message"] 17 | ], 18 | "received": [ 19 | [":localhost 001 testbot :Welcome to the Internet Relay Chat Network testbot\r\n\r\n", "Received welcome message"] 20 | ] 21 | }, 22 | "parse-line": { 23 | ":irc.dollyfish.net.nz 372 nodebot :The message of the day was last changed: 2012-6-16 23:57": { 24 | "prefix": "irc.dollyfish.net.nz", 25 | "server": "irc.dollyfish.net.nz", 26 | "command": "rpl_motd", 27 | "rawCommand": "372", 28 | "commandType": "reply", 29 | "args": ["nodebot", "The message of the day was last changed: 2012-6-16 23:57"] 30 | }, 31 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test :Hello nodebot!": { 32 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 33 | "nick": "Ned", 34 | "user": "~martyn", 35 | "host": "irc.dollyfish.net.nz", 36 | "command": "PRIVMSG", 37 | "rawCommand": "PRIVMSG", 38 | "commandType": "normal", 39 | "args": ["#test", "Hello nodebot!"] 40 | }, 41 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test ::-)": { 42 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 43 | "nick": "Ned", 44 | "user": "~martyn", 45 | "host": "irc.dollyfish.net.nz", 46 | "command": "PRIVMSG", 47 | "rawCommand": "PRIVMSG", 48 | "commandType": "normal", 49 | "args": ["#test", ":-)"] 50 | }, 51 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test ::": { 52 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 53 | "nick": "Ned", 54 | "user": "~martyn", 55 | "host": "irc.dollyfish.net.nz", 56 | "command": "PRIVMSG", 57 | "rawCommand": "PRIVMSG", 58 | "commandType": "normal", 59 | "args": ["#test", ":"] 60 | }, 61 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test ::^:^:": { 62 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 63 | "nick": "Ned", 64 | "user": "~martyn", 65 | "host": "irc.dollyfish.net.nz", 66 | "command": "PRIVMSG", 67 | "rawCommand": "PRIVMSG", 68 | "commandType": "normal", 69 | "args": ["#test", ":^:^:"] 70 | }, 71 | ":some.irc.net 324 webuser #channel +Cnj 5:10": { 72 | "prefix": "some.irc.net", 73 | "server": "some.irc.net", 74 | "command": "rpl_channelmodeis", 75 | "rawCommand": "324", 76 | "commandType": "reply", 77 | "args": ["webuser", "#channel", "+Cnj", "5:10"] 78 | }, 79 | ":nick!user@host QUIT :Ping timeout: 252 seconds": { 80 | "prefix": "nick!user@host", 81 | "nick": "nick", 82 | "user": "user", 83 | "host": "host", 84 | "command": "QUIT", 85 | "rawCommand": "QUIT", 86 | "commandType": "normal", 87 | "args": ["Ping timeout: 252 seconds"] 88 | }, 89 | ":nick!user@host PRIVMSG #channel :so : colons: :are :: not a problem ::::": { 90 | "prefix": "nick!user@host", 91 | "nick": "nick", 92 | "user": "user", 93 | "host": "host", 94 | "command": "PRIVMSG", 95 | "rawCommand": "PRIVMSG", 96 | "commandType": "normal", 97 | "args": ["#channel", "so : colons: :are :: not a problem ::::"] 98 | }, 99 | ":nick!user@host PRIVMSG #channel :\u000314,01\u001fneither are colors or styles\u001f\u0003": { 100 | "prefix": "nick!user@host", 101 | "nick": "nick", 102 | "user": "user", 103 | "host": "host", 104 | "command": "PRIVMSG", 105 | "rawCommand": "PRIVMSG", 106 | "commandType": "normal", 107 | "args": ["#channel", "neither are colors or styles"], 108 | "stripColors": true 109 | }, 110 | ":nick!user@host PRIVMSG #channel :\u000314,01\u001fwe can leave styles and colors alone if desired\u001f\u0003": { 111 | "prefix": "nick!user@host", 112 | "nick": "nick", 113 | "user": "user", 114 | "host": "host", 115 | "command": "PRIVMSG", 116 | "rawCommand": "PRIVMSG", 117 | "commandType": "normal", 118 | "args": ["#channel", "\u000314,01\u001fwe can leave styles and colors alone if desired\u001f\u0003"], 119 | "stripColors": false 120 | }, 121 | ":pratchett.freenode.net 324 nodebot #ubuntu +CLcntjf 5:10 #ubuntu-unregged": { 122 | "prefix": "pratchett.freenode.net", 123 | "server": "pratchett.freenode.net", 124 | "command": "rpl_channelmodeis", 125 | "rawCommand": "324", 126 | "commandType": "reply", 127 | "args": ["nodebot", "#ubuntu", "+CLcntjf", "5:10", "#ubuntu-unregged"] 128 | }, 129 | ":127.0.0.1 477 nodebot #channel :Cannot join channel (+r) - you need to be identified with services": { 130 | "prefix": "127.0.0.1", 131 | "server": "127.0.0.1", 132 | "command": "477", 133 | "rawCommand": "477", 134 | "commandType": "error", 135 | "args": ["nodebot", "#channel", "Cannot join channel (+r) - you need to be identified with services"] 136 | } 137 | }, 138 | "parse-line-nonstrict": { 139 | ":견본!~examplename@example.host PRIVMSG #channel :test message": { 140 | "prefix": "견본!~examplename@example.host", 141 | "nick": "견본", 142 | "user": "~examplename", 143 | "host": "example.host", 144 | "command": "PRIVMSG", 145 | "rawCommand": "PRIVMSG", 146 | "commandType": "normal", 147 | "args": ["#channel", "test message"] 148 | }, 149 | ":x/y!~examplename@example.host PRIVMSG #channel :test message": { 150 | "prefix": "x/y!~examplename@example.host", 151 | "nick": "x/y", 152 | "user": "~examplename", 153 | "host": "example.host", 154 | "command": "PRIVMSG", 155 | "rawCommand": "PRIVMSG", 156 | "commandType": "normal", 157 | "args": ["#channel", "test message"] 158 | }, 159 | ":?nick!~examplename@example.host PRIVMSG #channel :test message": { 160 | "prefix": "?nick!~examplename@example.host", 161 | "nick": "?nick", 162 | "user": "~examplename", 163 | "host": "example.host", 164 | "command": "PRIVMSG", 165 | "rawCommand": "PRIVMSG", 166 | "commandType": "normal", 167 | "args": ["#channel", "test message"] 168 | } 169 | }, 170 | "parse-line-strict": { 171 | ":x/y!~examplename@example.host PRIVMSG #channel :test message": { 172 | "prefix": "x/y!~examplename@example.host", 173 | "server": "x/y!~examplename@example.host", 174 | "command": "PRIVMSG", 175 | "rawCommand": "PRIVMSG", 176 | "commandType": "normal", 177 | "args": ["#channel", "test message"] 178 | }, 179 | ":?nick!~examplename@example.host PRIVMSG #channel :test message": { 180 | "prefix": "?nick!~examplename@example.host", 181 | "server": "?nick!~examplename@example.host", 182 | "command": "PRIVMSG", 183 | "rawCommand": "PRIVMSG", 184 | "commandType": "normal", 185 | "args": ["#channel", "test message"] 186 | } 187 | }, 188 | "parse-line-noprefix": { 189 | "477 nodebot #channel :Cannot join channel (+r) - you need to be identified with services": { 190 | "command": "477", 191 | "rawCommand": "477", 192 | "commandType": "error", 193 | "args": ["nodebot", "#channel", "Cannot join channel (+r) - you need to be identified with services"] 194 | } 195 | }, 196 | "convert-encoding": { 197 | "causesException": [ 198 | ":ubottu!ubottu@ubuntu/bot/ubottu MODE #ubuntu -bo *!~Brian@* ubottu\r\n", 199 | "Elizabeth", 200 | ":sblack1!~sblack1@unaffiliated/sblack1 NICK :sblack\r\n", 201 | ":TijG!~TijG@null.1ago.be PRIVMSG #ubuntu :ThinkPad\r\n" 202 | ], 203 | "sampleData": { 204 | "iso-8859-1 to utf-8": [ 205 | [ 206 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 101, 117, 32, 110, 227, 111, 32, 103, 111, 115, 116, 111, 32, 100, 101, 32, 100, 105, 102, 101, 114, 101, 110, 99, 105, 97, 231, 227, 111, 13, 10], 207 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 101, 117, 32, 110, 195, 163, 111, 32, 103, 111, 115, 116, 111, 32, 100, 101, 32, 100, 105, 102, 101, 114, 101, 110, 99, 105, 97, 195, 167, 195, 163, 111, 13, 10] 208 | ], 209 | [ 210 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 105, 110, 102, 111, 114, 109, 97, 231, 245, 101, 115, 13, 10], 211 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 105, 110, 102, 111, 114, 109, 97, 195, 167, 195, 181, 101, 115, 13, 10] 212 | ], 213 | [ 214 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 231, 97, 32, 109, 101, 32, 102, 97, 105, 116, 32, 114, 105, 114, 101, 13, 10], 215 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 195, 167, 97, 32, 109, 101, 32, 102, 97, 105, 116, 32, 114, 105, 114, 101, 13, 10] 216 | ], 217 | [ 218 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 110, 105, 105, 110, 32, 107, 97, 105, 32, 115, 105, 116, 228, 32, 118, 111, 105, 115, 13, 10], 219 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 110, 105, 105, 110, 32, 107, 97, 105, 32, 115, 105, 116, 195, 164, 32, 118, 111, 105, 115, 13, 10] 220 | ] 221 | ], 222 | "utf-8 to utf-8": [ 223 | [ 224 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 107, 121, 108, 108, 195, 164, 13, 10], 225 | [58, 84, 104, 114, 111, 110, 101, 51, 100, 33, 126, 84, 104, 114, 111, 110, 101, 51, 100, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 32, 80, 82, 73, 86, 77, 83, 71, 32, 35, 116, 101, 115, 116, 98, 101, 100, 32, 58, 107, 121, 108, 108, 195, 164, 13, 10] 226 | ] 227 | ] 228 | } 229 | }, 230 | "_splitLongLines": [ 231 | { 232 | "input": "abcde ", 233 | "maxLength": 5, 234 | "result": ["abcde"] 235 | }, 236 | { 237 | "input": "abcde", 238 | "maxLength": 5, 239 | "result": ["abcde"] 240 | }, 241 | { 242 | "input": "abcdefghijklmnopqrstuvwxyz", 243 | "maxLength": 5, 244 | "result": ["abcde", "fghij", "klmno", "pqrst", "uvwxy", "z"] 245 | }, 246 | { 247 | "input": "abc abcdef abc abcd abc", 248 | "maxLength": 5, 249 | "result": ["abc", "abcde", "f abc", "abcd", "abc"] 250 | } 251 | ], 252 | "_splitLongLines_no_max": [ 253 | { 254 | "input": "abcdefghijklmnopqrstuvwxyz", 255 | "result": ["abcdefghijklmnopqrstuvwxyz"] 256 | } 257 | ], 258 | "_splitLongLines_bytes": [ 259 | { 260 | "input": " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQR", 261 | "result": ["", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH", "IJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQR"] 262 | }, 263 | { 264 | "input": " ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ", 265 | "result": ["", "ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペ", "ホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジス", "ズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲン", "ヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒ", "ビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケ", "ゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリル", "レロヮワヰヱヲンヴヵヶヷヸヹヺ・ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ"] 266 | } 267 | ], 268 | "mode": { 269 | "expected": [ 270 | {"key": "#channel","serverName": "#channel","users": {},"modeParams": {"n": []},"mode": "n"}, 271 | {"key": "#channel","serverName": "#channel","users": {},"modeParams": {"n": [],"t": []},"mode": "nt"}, 272 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"modeParams": {"n": [],"t": []},"mode": "+nt"}, 273 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntb","modeParams": {"b": ["*!*@AN.IP.1"],"n": [],"t": []}}, 274 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntb","modeParams": {"b": ["*!*@AN.IP.1","*!*@AN.IP.2"],"n": [],"t": []}}, 275 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntb","modeParams": {"b": ["*!*@AN.IP.1","*!*@AN.IP.2","*!*@AN.IP.3"],"n": [],"t": []}}, 276 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntb","modeParams": {"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 277 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbf","modeParams": {"f": ["[10j]:15"],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 278 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbf","modeParams": {"f": ["[8j]:15"],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 279 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntb","modeParams": {"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 280 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbj","modeParams": {"j": ["3:5"],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 281 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbj","modeParams": {"j": ["2:5"],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 282 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntb","modeParams": {"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 283 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbp","modeParams": {"p": [],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 284 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbps","modeParams": {"s": [],"p": [],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 285 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbpsK","modeParams": {"K": [],"s": [],"p": [],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 286 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbsK","modeParams": {"K": [],"s": [],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 287 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbK","modeParams": {"K": [],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}}, 288 | {"key": "#channel","serverName": "#channel","users": {"testbot": "@"},"mode": "+ntbKF","modeParams": {"F": [],"K": [],"b": ["*!*@AN.IP.1","*!*@AN.IP.3"],"n": [],"t": []}} 289 | ], 290 | "serverMessages": [ 291 | ":localhost 005 testbot MODES=12 CHANTYPES=# PREFIX=(ohv)@%+ CHANMODES=beIqa,kfL,lj,psmntirRcOAQKVCuzNSMTGHFEB\r\n", 292 | ":testbot MODE testbot :+ix\r\n", 293 | ":testbot JOIN :#channel\r\n", 294 | ":localhost MODE #channel +nt\r\n", 295 | ":localhost 353 testbot = #channel :@testbot\r\n", 296 | ":localhost 366 testbot #channel :End of /NAMES list.\r\n", 297 | ":localhost 324 testbot #channel +nt\r\n", 298 | ":localhost MODE #channel -b *!*@AN.IP.1\r\n", 299 | ":localhost MODE #channel +b *!*@AN.IP.1\r\n", 300 | ":localhost MODE #channel +bb *!*@AN.IP.2 *!*@AN.IP.3\r\n", 301 | ":localhost MODE #channel -b *!*@AN.IP.2\r\n", 302 | ":localhost MODE #channel +f [10j]:15\r\n", 303 | ":localhost MODE #channel +f [8j]:15\r\n", 304 | ":localhost MODE #channel -f+j [10j]:15 3:5\r\n", 305 | ":localhost MODE #channel +j 2:5\r\n", 306 | ":localhost MODE #channel -j\r\n", 307 | ":localhost MODE #channel +ps\r\n", 308 | ":localhost MODE #channel +K-p-s+F\r\n" 309 | ] 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /test/data/ircd.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQDH5pYbcECKUrbRbUXKUu7lMCgb9UkPi4+Ur9f0LYdspHZJlv0S 3 | yBn4RpJOl8EsMhWI+houY3mBlcCL/DwiGfMDk5TSomyrI6eONFworokTJpG2h0f0 4 | cWnGdDW1zu8Z1odo047NWzwwv2mU03fkZmzfCclAzjKkDMMqP34mPl5TnwIDAQAB 5 | AoGAJslK3tAM9cnOxxvYqsUkrTuGzMXvAyElHshvsmUTHbVbbjPprrc8sruer7kq 6 | NhURsJ42bkHG1ankzkSGtmcqi3LdBBhVLm5gyog2JxQlTxvUVOPvyrOsQkl3uDwL 7 | aZqGTESHlLx7jhOKgiImqo0uGxNy46tzsHbpFGAeqTYcYKECQQD6faxqytMpMc/h 8 | zcrWsRhe7Omj5D6VdrbkGkM8razn4Oyr42p8Xylcde2MlnTiTAL5ElxlLd4PYsLD 9 | hKme/M5tAkEAzEwT1GU7CYjPdHHfsHUbDIHBh0BOJje2TXhDOa5tiZbOZevIk6TZ 10 | V6p/9zjLe5RAc/dpzHv1C+vQOkhgvoNyuwJARwjGkU5NTXxTwGwUnoeAKsMyioia 11 | etY8jTkpYha6VtOBKkmGlBiEaTUEFX9BTD9UBIABdavpMiHGq51+YJi+jQJAGYic 12 | pdwtH8jwnM4qtgQ86DhDduMLoW0vJMmWJVxuplap30Uz4XgmDfXqXnzDueNSluvi 13 | VkNb4iyL7uzi4ozNRwJALT0vP65RQ2d7OUEwB4XZFExKYzHADiFtw0NZtcWRW6y3 14 | rN0uXMxEZ6vRQurVjO9GhB76fAo/UooX0MVF0ShFNQ== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/data/ircd.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICojCCAgugAwIBAgIJAMid3M25tUeUMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV 3 | BAYTAlpaMREwDwYDVQQIDAhJbnRlcm5ldDEPMA0GA1UEBwwGZ2l0aHViMREwDwYD 4 | VQQKDAhub2RlLWlyYzEQMA4GA1UECwwHdGVzdGluZzESMBAGA1UEAwwJbG9jYWxo 5 | b3N0MB4XDTE1MDExMjIzNDg0MloXDTI1MDEwOTIzNDg0MlowajELMAkGA1UEBhMC 6 | WloxETAPBgNVBAgMCEludGVybmV0MQ8wDQYDVQQHDAZnaXRodWIxETAPBgNVBAoM 7 | CG5vZGUtaXJjMRAwDgYDVQQLDAd0ZXN0aW5nMRIwEAYDVQQDDAlsb2NhbGhvc3Qw 8 | gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMfmlhtwQIpSttFtRcpS7uUwKBv1 9 | SQ+Lj5Sv1/Qth2ykdkmW/RLIGfhGkk6XwSwyFYj6Gi5jeYGVwIv8PCIZ8wOTlNKi 10 | bKsjp440XCiuiRMmkbaHR/RxacZ0NbXO7xnWh2jTjs1bPDC/aZTTd+RmbN8JyUDO 11 | MqQMwyo/fiY+XlOfAgMBAAGjUDBOMB0GA1UdDgQWBBTUaumzrTJrl1goRRzOGgEO 12 | VNKFmjAfBgNVHSMEGDAWgBTUaumzrTJrl1goRRzOGgEOVNKFmjAMBgNVHRMEBTAD 13 | AQH/MA0GCSqGSIb3DQEBBQUAA4GBAGKppBE9mjk2zJPSxPcHl3RSpnPs5ZkuBLnK 14 | rxZ2bR9VJhoQEwtiZRxkSXSdooj3eJgzMobYMEhSvFibUeBuIppB7oacys2Bd+O1 15 | xzILcbgEPqsk5JFbYT9KD8r+sZy5Wa1A39eNkmdD/oWt9Mb1PLrDfM/melvZ9/vW 16 | oMSmMipK 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /* Mock irc server */ 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var net = require('net'); 6 | var tls = require('tls'); 7 | var util = require('util'); 8 | var EventEmitter = require('events'); 9 | var sinon = require('sinon'); 10 | var proxyquire = require('proxyquire'); 11 | var stubbedUtil = {log: util.log}; 12 | var irc = proxyquire('../lib/irc', {util: stubbedUtil}); 13 | 14 | module.exports.ircWithStubbedOutput = function(server, clientNick, opt) { 15 | stubbedUtil.log = sinon.stub(); 16 | return new irc.Client(server, clientNick, opt); 17 | }; 18 | 19 | var MockIrcd = function(port, encoding, isSecure, quiet) { 20 | var self = this; 21 | var connectionClass; 22 | var options = {}; 23 | 24 | if (isSecure) { 25 | connectionClass = tls; 26 | options = { 27 | key: fs.readFileSync(path.resolve(__dirname, 'data/ircd.key')), 28 | cert: fs.readFileSync(path.resolve(__dirname, 'data/ircd.pem')) 29 | }; 30 | } else { 31 | connectionClass = net; 32 | } 33 | 34 | this.port = port || (isSecure ? 6697 : 6667); 35 | this.encoding = encoding || 'utf-8'; 36 | this.incoming = []; 37 | this.outgoing = []; 38 | if (!quiet) console.log('Mock server initializing.'); 39 | 40 | this.server = connectionClass.createServer(options, function(c) { 41 | var active = true; 42 | c.on('data', function(data) { 43 | var msg = data.toString(self.encoding).split('\r\n').filter(function(m) { return m; }); 44 | self.incoming = self.incoming.concat(msg); 45 | msg.forEach(function(line) { self.emit('line', line); }); 46 | }); 47 | 48 | self.on('send', function(data) { 49 | if (!active || c.destroyed) return; 50 | self.outgoing.push(data); 51 | c.write(data); 52 | }); 53 | 54 | c.on('end', function() { 55 | active = false; 56 | self.emit('end'); 57 | }); 58 | }); 59 | 60 | this.server.listen(this.port); 61 | 62 | this.server.on('close', function(){ 63 | if (!quiet) console.log('Mock server closed.'); 64 | }); 65 | }; 66 | util.inherits(MockIrcd, EventEmitter); 67 | 68 | MockIrcd.prototype.send = function(data) { 69 | if (data.slice(-2) !== '\r\n') { 70 | console.log("WARNING: mock.send called without trailing \\r\\n"); 71 | console.log("data:", data); 72 | } 73 | this.emit('send', data); 74 | }; 75 | 76 | MockIrcd.prototype.close = function() { 77 | this.server.close.apply(this.server, arguments); 78 | }; 79 | 80 | MockIrcd.prototype.getIncomingMsgs = function() { 81 | return this.incoming; 82 | }; 83 | 84 | MockIrcd.prototype.greet = function(username) { 85 | username = username || 'testbot'; 86 | this.send(':localhost 001 ' + username + ' :Welcome to the Internet Relay Chat Network testbot\r\n'); 87 | }; 88 | 89 | var fixtures = require('./data/fixtures'); 90 | module.exports.getFixtures = function(testSuite) { 91 | return fixtures[testSuite]; 92 | }; 93 | 94 | module.exports.MockIrcd = function(port, encoding, isSecure, quiet) { 95 | return new MockIrcd(port, encoding, isSecure, quiet); 96 | }; 97 | 98 | var pingTimerStubs = { 99 | notifyOfActivity: sinon.stub(), 100 | stop: sinon.stub(), 101 | start: sinon.stub() 102 | }; 103 | 104 | var MockPingTimer = function() { 105 | for (var key in pingTimerStubs) { 106 | pingTimerStubs[key].reset(); 107 | } 108 | Object.assign(this, pingTimerStubs); 109 | }; 110 | util.inherits(MockPingTimer, EventEmitter); 111 | 112 | module.exports.MockPingTimer = function() { 113 | return new MockPingTimer(); 114 | }; 115 | 116 | var ircWithPingStub = proxyquire('../lib/irc', {util: stubbedUtil, './cycling_ping_timer.js': MockPingTimer}); 117 | 118 | function setupMocks(config, callback) { 119 | // both args optional 120 | // config.client gets merged into irc.Client config, except client.server and client.nick (used in irc.Client params) 121 | // config.server is used for MockIrcd (port, encoding, isSecure, disableOutput) 122 | // config.meta: 123 | // - autoGreet (default true): automatically greets client on connection 124 | // - callbackEarly (default false): calls callback when initialization finished instead ofon client registered event 125 | // - disableOutput (default true): stubs client's util.log to reduce output clutter 126 | // - withoutServer (default false): skips server, makes client not autoConnect by default and enables callbackEarly 127 | // - stubTimer (default false): stubs the cyclingPingTimer 128 | 129 | if (typeof callback === 'undefined' && typeof config === 'function') { callback = config; config = undefined; } 130 | config = config || {}; 131 | config.meta = config.meta || {}; 132 | config.client = config.client || {}; 133 | config.server = config.server || {}; 134 | 135 | var defaultMeta = {autoGreet: true, callbackEarly: false, disableOutput: true, withoutServer: false, stubTimer: false}; 136 | var defaultClient = {debug: true}; 137 | var defaultServer = {}; 138 | 139 | var metaConfig = Object.assign(defaultMeta, config.meta); 140 | if (metaConfig.withoutServer) defaultClient.autoConnect = false; 141 | 142 | var clientConfig = Object.assign(defaultClient, config.client); 143 | var serverConfig = Object.assign(defaultServer, config.server); 144 | 145 | if (clientConfig.autoConnect === false) metaConfig.callbackEarly = true; 146 | 147 | var quiet = metaConfig.disableOutput; 148 | var stubTimer = metaConfig.stubTimer; 149 | 150 | var mockObj = {}; 151 | 152 | if (!metaConfig.withoutServer) { 153 | var lineSpy = sinon.spy(); 154 | var mock = module.exports.MockIrcd(serverConfig.port, serverConfig.encoding, serverConfig.isSecure, quiet); 155 | mock.on('line', lineSpy); 156 | mock.server.on('connection', function() { 157 | if (metaConfig.autoGreet) mock.greet(); 158 | }); 159 | mockObj.mock = mock; 160 | mockObj.lineSpy = lineSpy; 161 | } 162 | 163 | var clientServer = 'localhost'; 164 | if (clientConfig.server) { 165 | clientServer = clientConfig.server; 166 | delete clientConfig.server; 167 | } 168 | var clientNick = 'testbot'; 169 | if (clientConfig.nick) { 170 | clientNick = clientConfig.nick; 171 | delete clientConfig.nick; 172 | } 173 | 174 | if (quiet) { 175 | stubbedUtil.log = sinon.stub(); 176 | } else { 177 | stubbedUtil.log = util.log.bind(util); 178 | } 179 | var lib = irc; 180 | if (stubTimer) { 181 | lib = ircWithPingStub; 182 | mockObj.pingTimerStubs = pingTimerStubs; 183 | } 184 | var client = new lib.Client(clientServer, clientNick, clientConfig); 185 | mockObj.client = client; 186 | 187 | var unhandledSpy = sinon.spy(); 188 | client.on('unhandled', unhandledSpy); 189 | mockObj.unhandledSpy = unhandledSpy; 190 | 191 | client.once('registered', function() { 192 | if (!metaConfig.callbackEarly) callback(mockObj); 193 | }); 194 | if (metaConfig.callbackEarly) callback(mockObj); 195 | } 196 | module.exports.setupMocks = setupMocks; 197 | 198 | function teardownMocks(mockObj, callback) { 199 | teardownClient(); 200 | function teardownClient() { 201 | if (mockObj.client) { 202 | if (mockObj.client.floodProtectionEnabled) mockObj.client.deactivateFloodProtection(); 203 | if (mockObj.client.conn && !mockObj.client.conn.requestedDisconnect) { 204 | mockObj.client.disconnect(teardownMock); 205 | return; 206 | } 207 | mockObj.client.disconnect(); 208 | } 209 | teardownMock(); 210 | } 211 | function teardownMock() { 212 | if (mockObj.mock) { 213 | mockObj.mock.close(function() { callback(); }); 214 | } else { 215 | callback(); 216 | } 217 | } 218 | } 219 | module.exports.teardownMocks = teardownMocks; 220 | 221 | module.exports.hookMockSetup = function hookMockSetup(beforeEach, afterEach, config) { 222 | config = config || {}; 223 | beforeEach(function(done) { 224 | var self = this; 225 | setupMocks(config, function(mocks) { 226 | for (var key in mocks) { 227 | self[key] = mocks[key]; 228 | } 229 | done(); 230 | }); 231 | }); 232 | 233 | afterEach(function(done) { 234 | teardownMocks({client: this.client, mock: this.mock}, done); 235 | }); 236 | }; 237 | 238 | function joinChannels(local, localChannels, remoteChannels, done) { 239 | var i = 0; 240 | local.client.on('join', function() { 241 | i++; 242 | if (i === localChannels.length) { 243 | setTimeout(function() { 244 | if (local.debugSpy) local.debugSpy.resetHistory(); 245 | if (local.sendSpy) local.sendSpy.resetHistory(); 246 | done(); 247 | }, 10); 248 | } 249 | }); 250 | localChannels.forEach(function(chan) { 251 | local.client.join(chan); 252 | }); 253 | remoteChannels.forEach(function(remoteChan) { 254 | local.mock.send(':testbot!~testbot@EXAMPLE.HOST JOIN :' + remoteChan + '\r\n'); 255 | }); 256 | } 257 | module.exports.joinChannels = joinChannels; 258 | 259 | module.exports.joinChannelsBefore = function (beforeEach, localChannels, remoteChannels) { 260 | beforeEach(function(done) { 261 | joinChannels(this, localChannels, remoteChannels, done); 262 | }); 263 | }; 264 | 265 | module.exports.emitNames = function (local, channelName, names) { 266 | local.mock.send(':localhost 353 testbot = ' + channelName + ' :' + names + '\r\n'); 267 | local.mock.send(':localhost 366 testbot ' + channelName + ' :End of /NAMES list.\r\n'); 268 | }; 269 | -------------------------------------------------------------------------------- /test/test-api.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var joinChannelsBefore = testHelpers.joinChannelsBefore; 3 | var chai = require('chai'); 4 | var expect = chai.expect; 5 | var sinon = require('sinon'); 6 | 7 | describe('Client', function() { 8 | describe('raw handler', function() { 9 | testHelpers.hookMockSetup(beforeEach, afterEach); 10 | it('throws if an error occurs and no error handler is bound', function() { 11 | var self = this; 12 | function wrap() { 13 | self.client.conn.emit('data', ':127.0.0.1 PING :1\r\n'); 14 | } 15 | self.client.on('raw', function() { 16 | throw new Error('test error'); 17 | }); 18 | expect(wrap).to.throw(Error, 'test error'); 19 | }); 20 | 21 | it('passes error to error handler if bound', function() { 22 | var self = this; 23 | var errorSpy = sinon.spy(); 24 | var error = new Error('test error'); 25 | function wrap() { 26 | self.client.conn.emit('data', ':127.0.0.1 PING :1\r\n'); 27 | } 28 | self.client.on('raw', function() { 29 | throw error; 30 | }); 31 | self.client.on('error', errorSpy); 32 | expect(wrap).not.to.throw(); 33 | expect(errorSpy.args).to.deep.equal([[error]]); 34 | }); 35 | 36 | it('does not emit error if disconnect requested', function() { 37 | var self = this; 38 | var errorSpy = sinon.spy(); 39 | var error = new Error('test error'); 40 | function wrap() { 41 | self.client.conn.emit('data', ':127.0.0.1 PING :1\r\n'); 42 | } 43 | self.client.on('raw', function() { 44 | self.client.disconnect(); 45 | throw error; 46 | }); 47 | self.client.on('error', errorSpy); 48 | expect(wrap).not.to.throw(); 49 | expect(errorSpy.callCount).to.equal(0); 50 | }); 51 | }); 52 | 53 | describe('#connect', function() { 54 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: {autoConnect: false}}); 55 | 56 | it('establishes socket without arguments', function(done) { 57 | var self = this; 58 | self.client.connect(); 59 | self.client.on('registered', function() { 60 | done(); 61 | }); 62 | }); 63 | 64 | it('accepts callback in first position', function(done) { 65 | var self = this; 66 | self.client.connect(function() { done(); }); 67 | }); 68 | 69 | it('accepts callback in second position', function(done) { 70 | var self = this; 71 | self.client.connect(0, function() { done(); }); 72 | }); 73 | 74 | // see test-reconnect.js for testing of retryCount parameter 75 | }); 76 | 77 | describe('#send', function() { 78 | describe('with standard client and server', function() { 79 | testHelpers.hookMockSetup(beforeEach, afterEach); 80 | 81 | beforeEach(function() { 82 | this.debugSpy = sinon.spy(); 83 | this.connSpy = sinon.spy(); 84 | this.client.out.debug = this.debugSpy; 85 | this.client.conn.write = this.connSpy; 86 | }); 87 | 88 | it('works with simple data', function() { 89 | this.client.send('JOIN', '#channel'); 90 | expect(this.debugSpy.args).to.deep.equal([ 91 | ['SEND:', 'JOIN #channel'] 92 | ]); 93 | expect(this.connSpy.args).to.deep.equal([ 94 | ['JOIN #channel\r\n'] 95 | ]); 96 | }); 97 | 98 | it('works with multiple arguments', function() { 99 | this.client.send('TEST', 'example', 'data'); 100 | expect(this.debugSpy.args).to.deep.equal([ 101 | ['SEND:', 'TEST example data'] 102 | ]); 103 | expect(this.connSpy.args).to.deep.equal([ 104 | ['TEST example data\r\n'] 105 | ]); 106 | }); 107 | 108 | it('works with multi-word last parameter', function() { 109 | this.client.send('TEST', 'example data'); 110 | expect(this.debugSpy.args).to.deep.equal([ 111 | ['SEND:', 'TEST :example data'] 112 | ]); 113 | expect(this.connSpy.args).to.deep.equal([ 114 | ['TEST :example data\r\n'] 115 | ]); 116 | }); 117 | 118 | it('works with comma-separated channel example', function() { 119 | this.client.send('PART', '#channel1,#channel2,#channel3', 'test message'); 120 | expect(this.debugSpy.args).to.deep.equal([ 121 | ['SEND:', 'PART #channel1,#channel2,#channel3 :test message'] 122 | ]); 123 | expect(this.connSpy.args).to.deep.equal([ 124 | ['PART #channel1,#channel2,#channel3 :test message\r\n'] 125 | ]); 126 | }); 127 | 128 | it('does not throw when disconnecting', function() { 129 | var self = this; 130 | function wrap() { 131 | self.client.disconnect(); 132 | self.client.send('TEST', 'example data'); 133 | } 134 | expect(wrap).not.to.throw(); 135 | expect(self.debugSpy.args).to.deep.include([ 136 | '(Disconnected) SEND:', 'TEST :example data' 137 | ]); 138 | expect(self.connSpy.args).to.deep.equal([ 139 | ['QUIT :node-irc says goodbye\r\n'] 140 | ]); 141 | }); 142 | }); 143 | 144 | describe('with client but no server', function() { 145 | testHelpers.hookMockSetup(beforeEach, afterEach, {meta: {withoutServer: true}}); 146 | 147 | beforeEach(function() { 148 | this.debugSpy = sinon.spy(); 149 | this.client.out.debug = this.debugSpy; 150 | }); 151 | 152 | it('does not throw when disconnected', function() { 153 | var self = this; 154 | function wrap() { 155 | self.client.send('TEST', 'example data'); 156 | } 157 | expect(wrap).not.to.throw(); 158 | expect(self.debugSpy.args).to.deep.equal([ 159 | ['(Disconnected) SEND:', 'TEST :example data'] 160 | ]); 161 | }); 162 | }); 163 | }); 164 | 165 | describe('#join', function() { 166 | testHelpers.hookMockSetup(beforeEach, afterEach); 167 | 168 | function downcaseChannels(chans) { 169 | return chans.map(function(x) { return x.toLowerCase(); }); 170 | } 171 | 172 | beforeEach(function(done) { 173 | var self = this; 174 | setTimeout(function() { 175 | self.debugSpy = sinon.spy(); 176 | self.sendSpy = sinon.spy(); 177 | self.client.out.debug = self.debugSpy; 178 | self.client.send = self.sendSpy; 179 | done(); 180 | }, 10); 181 | }); 182 | 183 | function sharedExamplesFor(channels, remoteChannels) { 184 | it('sends correct command and does not throw with no callback', function() { 185 | var self = this; 186 | function wrap() { 187 | self.client.join(channels.join(',')); 188 | } 189 | expect(wrap).not.to.throw(); 190 | expect(this.sendSpy.args).to.deep.equal([ 191 | ['JOIN', channels.join(',')] 192 | ]); 193 | }); 194 | 195 | it('adds to opt.channels on successful join', function(done) { 196 | var self = this; 197 | var i = 0; 198 | self.client.on('join', function() { 199 | i++; 200 | if (i === channels.length) setTimeout(check, 10); 201 | }); 202 | expect(self.client.opt.channels).to.be.empty; 203 | self.client.join(channels.join(',')); 204 | remoteChannels.forEach(function(remoteChan) { 205 | self.mock.send(':testbot!~testbot@EXAMPLE.HOST JOIN :' + remoteChan + '\r\n'); 206 | }); 207 | 208 | function check() { 209 | expect(downcaseChannels(self.client.opt.channels)).to.deep.equal(downcaseChannels(channels)); 210 | done(); 211 | } 212 | }); 213 | 214 | it('does not add duplicates to opt.channels', function(done) { 215 | var self = this; 216 | var i = 0; 217 | self.client.on('join', function() { 218 | i++; 219 | if (i === channels.length) setTimeout(check, 10); 220 | }); 221 | self.client.opt.channels = channels; 222 | self.client.join(channels.join(',')); 223 | remoteChannels.forEach(function(remoteChan) { 224 | self.mock.send(':testbot!~testbot@EXAMPLE.HOST JOIN :' + remoteChan + '\r\n'); 225 | }); 226 | 227 | function check() { 228 | expect(downcaseChannels(self.client.opt.channels)).to.deep.equal(downcaseChannels(channels)); 229 | done(); 230 | } 231 | }); 232 | 233 | it('calls given callback', function(done) { 234 | var self = this; 235 | expect(self.client.opt.channels).to.be.empty; 236 | self.client.join(channels.join(','), check); 237 | remoteChannels.forEach(function(remoteChan) { 238 | self.mock.send(':testbot!~testbot@EXAMPLE.HOST JOIN :' + remoteChan + '\r\n'); 239 | }); 240 | 241 | var i = 0; 242 | function check(nick, message) { 243 | expect(nick).to.equal('testbot'); 244 | expect(message).to.deep.equal({ 245 | prefix: 'testbot!~testbot@EXAMPLE.HOST', 246 | nick: 'testbot', 247 | user: '~testbot', 248 | host: 'EXAMPLE.HOST', 249 | commandType: 'normal', 250 | command: 'JOIN', 251 | rawCommand: 'JOIN', 252 | args: [remoteChannels[i]] 253 | }); 254 | i++; 255 | if (i === channels.length) done(); 256 | } 257 | }); 258 | 259 | it('sends right command with key', function() { 260 | var self = this; 261 | function wrap() { 262 | self.client.join(channels.join(',') + ' ' + channels.map(function() { return 'key'; }).join(',')); 263 | } 264 | expect(wrap).not.to.throw(); 265 | expect(this.sendSpy.args).to.deep.equal([ 266 | ['JOIN', channels.join(','), channels.map(function() { return 'key'; }).join(',')] 267 | ]); 268 | }); 269 | 270 | it('works with key', function(done) { 271 | var self = this; 272 | self.client.opt.channels = [channels[0]]; 273 | self.client.join(channels.join(',') + ' ' + channels.map(function() { return 'key'; }).join(','), check); 274 | remoteChannels.forEach(function(remoteChan) { 275 | self.mock.send(':testbot!~testbot@EXAMPLE.HOST JOIN :' + remoteChan + '\r\n'); 276 | }); 277 | 278 | var i = 0; 279 | function check(nick, message) { 280 | expect(nick).to.equal('testbot'); 281 | expect(message).to.deep.equal({ 282 | prefix: 'testbot!~testbot@EXAMPLE.HOST', 283 | nick: 'testbot', 284 | user: '~testbot', 285 | host: 'EXAMPLE.HOST', 286 | commandType: 'normal', 287 | command: 'JOIN', 288 | rawCommand: 'JOIN', 289 | args: [remoteChannels[i]] 290 | }); 291 | i++; 292 | if (i === channels.length) end(); 293 | } 294 | 295 | function end() { 296 | var expected = downcaseChannels(channels); 297 | expected = expected.map(function(x, index) { 298 | if (index === 0) return x; 299 | return x + ' key'; 300 | }); 301 | expect(downcaseChannels(self.client.opt.channels)).to.deep.equal(expected); 302 | done(); 303 | } 304 | }); 305 | } 306 | 307 | context('with same lowercase local and remote channel', function() { 308 | sharedExamplesFor(['#channel'], ['#channel']); 309 | }); 310 | 311 | context('with same mixed-case local and remote channel', function() { 312 | sharedExamplesFor(['#Channel'], ['#Channel']); 313 | }); 314 | 315 | context('with mixed-case local channel differing from lowercase remote channel', function() { 316 | sharedExamplesFor(['#Channel'], ['#channel']); 317 | }); 318 | 319 | context('with lowercase local channel differing from mixed-case remote channel', function() { 320 | sharedExamplesFor(['#channel'], ['#Channel']); 321 | }); 322 | 323 | context('with multiple channels', function() { 324 | var localChannels = ['#channel', '#channel2', '#Test', '#Test2']; 325 | var remoteChannels = ['#channel', '#Channel2', '#test', '#Test2']; 326 | 327 | sharedExamplesFor(localChannels, remoteChannels); 328 | }); 329 | 330 | context('with zero parameter', function() { 331 | var localChannels = ['#channel', '#channel2', '#Test', '#Test2']; 332 | var remoteChannels = ['#channel', '#Channel2', '#test', '#Test2']; 333 | 334 | joinChannelsBefore(beforeEach, localChannels, remoteChannels); 335 | 336 | it('sends correct command and does not throw without callback', function() { 337 | var self = this; 338 | function wrap() { 339 | self.client.join('0'); 340 | } 341 | expect(wrap).not.to.throw(); 342 | expect(this.sendSpy.args).to.deep.equal([ 343 | ['JOIN', '0'] 344 | ]); 345 | }); 346 | 347 | it('removes all channels from opt.channels and does not call callback', function() { 348 | var self = this; 349 | var localPartSpy = sinon.spy(); 350 | var callbackSpy = sinon.spy(); 351 | self.client.on('part', localPartSpy); 352 | self.client.join('0', callbackSpy); 353 | self.mock.on('line', function(line) { 354 | if (line !== 'JOIN 0') return; 355 | remoteChannels.forEach(function(remoteChan) { 356 | self.mock.send(':testbot!~testbot@EXAMPLE.HOST PART ' + remoteChan + ' :Left all channels\r\n'); 357 | }); 358 | }); 359 | var i = 0; 360 | self.client.on('part', function() { 361 | i++; 362 | if (i === localChannels.length) setTimeout(teardown, 10); 363 | }); 364 | 365 | function teardown() { 366 | expect(self.client.opt.channels).to.be.empty; 367 | expect(callbackSpy.callCount).to.equal(0); 368 | var standardMsg = { 369 | prefix: 'testbot!~testbot@EXAMPLE.HOST', 370 | nick: 'testbot', 371 | user: '~testbot', 372 | host: 'EXAMPLE.HOST', 373 | command: 'PART', 374 | rawCommand: 'PART', 375 | commandType: 'normal' 376 | }; 377 | var expected = remoteChannels.map(function(remoteChan) { 378 | var message = Object.assign({args: [remoteChan, 'Left all channels']}, standardMsg); 379 | return [remoteChan, 'testbot', 'Left all channels', message]; 380 | }); 381 | expect(localPartSpy.args).to.deep.equal(expected); 382 | } 383 | }); 384 | }); 385 | }); 386 | 387 | describe('#part', function() { 388 | testHelpers.hookMockSetup(beforeEach, afterEach); 389 | 390 | function downcaseChannels(chans) { 391 | return chans.map(function(x) { return x.toLowerCase(); }); 392 | } 393 | 394 | beforeEach(function(done) { 395 | var self = this; 396 | setTimeout(function() { 397 | self.debugSpy = sinon.spy(); 398 | self.sendSpy = sinon.spy(); 399 | self.client.out.debug = self.debugSpy; 400 | self.client.send = self.sendSpy; 401 | done(); 402 | }, 10); 403 | }); 404 | 405 | function sharedExamplesFor(channels, remoteChannels) { 406 | joinChannelsBefore(beforeEach, channels, remoteChannels); 407 | 408 | it('works with no message or callback', function() { 409 | var self = this; 410 | function wrap() { 411 | self.client.part(channels.join(',')); 412 | } 413 | expect(wrap).not.to.throw(); 414 | expect(this.sendSpy.args).to.deep.equal([ 415 | ['PART', channels.join(',')] 416 | ]); 417 | }); 418 | 419 | it('sends with message and no callback', function() { 420 | var self = this; 421 | function wrap() { 422 | self.client.part(channels.join(','), 'test message'); 423 | } 424 | expect(wrap).not.to.throw(); 425 | expect(this.sendSpy.args).to.deep.equal([ 426 | ['PART', channels.join(','), 'test message'] 427 | ]); 428 | }); 429 | 430 | it('sends with callback and no message', function() { 431 | var self = this; 432 | function wrap() { 433 | self.client.part(channels.join(','), function() {}); 434 | } 435 | expect(wrap).not.to.throw(); 436 | expect(this.sendSpy.args).to.deep.equal([ 437 | ['PART', channels.join(',')] 438 | ]); 439 | }); 440 | 441 | it('sends with message and callback', function() { 442 | var self = this; 443 | function wrap() { 444 | self.client.part(channels.join(','), 'test message', function() {}); 445 | } 446 | expect(wrap).not.to.throw(); 447 | expect(this.sendSpy.args).to.deep.equal([ 448 | ['PART', channels.join(','), 'test message'] 449 | ]); 450 | }); 451 | 452 | it('removes from opt.channels', function(done) { 453 | var self = this; 454 | var i = 0; 455 | self.client.on('part', function() { 456 | i++; 457 | if (i === channels.length) setTimeout(check, 10); 458 | }); 459 | expect(downcaseChannels(self.client.opt.channels)).to.deep.equal(downcaseChannels(remoteChannels)); 460 | self.client.part(channels.join(','), 'test message'); 461 | remoteChannels.forEach(function(remoteChan) { 462 | self.mock.send(':testbot!~testbot@EXAMPLE.HOST PART ' + remoteChan + ' :test message\r\n'); 463 | }); 464 | 465 | function check() { 466 | expect(self.client.opt.channels).to.empty; 467 | done(); 468 | } 469 | }); 470 | 471 | it('calls given callback', function(done) { 472 | var self = this; 473 | expect(downcaseChannels(self.client.opt.channels)).to.deep.equal(downcaseChannels(remoteChannels)); 474 | self.client.part(channels.join(','), check); 475 | remoteChannels.forEach(function(remoteChan) { 476 | self.mock.send(':testbot!~testbot@EXAMPLE.HOST PART ' + remoteChan + ' :test message\r\n'); 477 | }); 478 | 479 | var i = 0; 480 | function check(nick, reason, message) { 481 | expect(nick).to.equal('testbot'); 482 | expect(reason).to.equal('test message'); 483 | expect(message).to.deep.equal({ 484 | prefix: 'testbot!~testbot@EXAMPLE.HOST', 485 | nick: 'testbot', 486 | user: '~testbot', 487 | host: 'EXAMPLE.HOST', 488 | commandType: 'normal', 489 | command: 'PART', 490 | rawCommand: 'PART', 491 | args: [remoteChannels[i], 'test message'] 492 | }); 493 | i++; 494 | if (i === channels.length) done(); 495 | } 496 | }); 497 | } 498 | 499 | context('with same lowercase local and remote channel', function() { 500 | sharedExamplesFor(['#channel'], ['#channel']); 501 | }); 502 | 503 | context('with same mixed-case local and remote channel', function() { 504 | sharedExamplesFor(['#Channel'], ['#Channel']); 505 | }); 506 | 507 | context('with mixed-case local channel differing from lowercase remote channel', function() { 508 | sharedExamplesFor(['#Channel'], ['#channel']); 509 | }); 510 | 511 | context('with lowercase local channel differing from mixed-case remote channel', function() { 512 | sharedExamplesFor(['#channel'], ['#Channel']); 513 | }); 514 | 515 | context('with multiple channels', function() { 516 | var localChannels = ['#channel', '#channel2', '#Test', '#Test2']; 517 | var remoteChannels = ['#channel', '#Channel2', '#test', '#Test2']; 518 | 519 | sharedExamplesFor(localChannels, remoteChannels); 520 | }); 521 | }); 522 | 523 | describe('#list', function() { 524 | testHelpers.hookMockSetup(beforeEach, afterEach); 525 | 526 | beforeEach(function(done) { 527 | var self = this; 528 | setTimeout(function() { 529 | self.sendSpy = sinon.spy(); 530 | self.client.send = self.sendSpy; 531 | done(); 532 | }, 10); 533 | }); 534 | 535 | it('sends given arguments directly', function() { 536 | this.client.list('arg1', 'arg2'); 537 | expect(this.sendSpy.args).to.deep.equal([ 538 | ['LIST', 'arg1', 'arg2'] 539 | ]); 540 | }); 541 | }); 542 | 543 | describe('#whois', function() { 544 | testHelpers.hookMockSetup(beforeEach, afterEach); 545 | 546 | // skip client whois handling 547 | beforeEach(function(done) { 548 | setTimeout(done, 10); 549 | }); 550 | 551 | context('with stubbed send', function() { 552 | beforeEach(function() { 553 | this.sendSpy = sinon.spy(); 554 | this.client.send = this.sendSpy; 555 | }); 556 | 557 | it('sends whois with given nick and no callback', function() { 558 | this.client.whois('testbot2'); 559 | expect(this.sendSpy.args).to.deep.equal([ 560 | ['WHOIS', 'testbot2'] 561 | ]); 562 | }); 563 | 564 | it('sends whois with given nick and callback', function() { 565 | this.client.whois('testbot3', function() {}); 566 | expect(this.sendSpy.args).to.deep.equal([ 567 | ['WHOIS', 'testbot3'] 568 | ]); 569 | }); 570 | }); 571 | 572 | function respondWhois(local, targetUsername) { 573 | // first arg is nick of target 574 | local.mock.send(':127.0.0.1 311 testbot ' + targetUsername + ' ~testbot2 EXAMPLE.HOST * :testbot2\r\n'); // whoisuser (user, host, ?, realname) 575 | local.mock.send(':127.0.0.1 319 testbot ' + targetUsername + ' :#channel @#channel2\r\n'); // rpl_whoischannels (channels) 576 | local.mock.send(':127.0.0.1 312 testbot ' + targetUsername + ' 127.0.0.1 :Test server\r\n'); // whoisserver (server, serverinfo) 577 | local.mock.send(':127.0.0.1 301 testbot ' + targetUsername + ' :I\'m busy\r\n'); // away (away) 578 | local.mock.send(':127.0.0.1 317 testbot ' + targetUsername + ' 100 1000000000 :seconds idle, signon time\r\n'); // whoisidle (idle) 579 | local.mock.send(':127.0.0.1 318 testbot ' + targetUsername + ' :End of /WHOIS list.\r\n'); 580 | } 581 | 582 | function whoisResponse(local, localNick, remoteNick, done) { 583 | local.mock.on('line', function(line) { 584 | if (line !== 'WHOIS ' + localNick) return; 585 | respondWhois(local, remoteNick); 586 | }); 587 | 588 | local.client.whois(localNick, function(data) { 589 | expect(data).to.deep.equal({ 590 | user: '~testbot2', 591 | host: 'EXAMPLE.HOST', 592 | realname: 'testbot2', 593 | server: '127.0.0.1', 594 | serverinfo: 'Test server', 595 | idle: '100', 596 | nick: remoteNick, 597 | channels: ['#channel', '@#channel2'], 598 | away: 'I\'m busy' 599 | }); 600 | done(); 601 | }); 602 | } 603 | 604 | it('calls callback on whois response with lowercase nick', function(done) { 605 | whoisResponse(this, 'testbot4', 'testbot4', done); 606 | }); 607 | 608 | it('calls callback on whois response with mixed-case nick', function(done) { 609 | whoisResponse(this, 'Testbot4', 'Testbot4', done); 610 | }); 611 | 612 | it('calls callback on whois response with differing mixed-case server nick', function(done) { 613 | whoisResponse(this, 'testbot4', 'Testbot4', done); 614 | }); 615 | 616 | it('calls callback on whois response with differing lowercase server nick', function(done) { 617 | whoisResponse(this, 'Testbot4', 'testbot4', done); 618 | }); 619 | }); 620 | 621 | // for notice, say, as they work the same 622 | function sharedExamplesForSpeaks(commandName, expectedCommand, isAction) { 623 | // see also test-irc.js #_speak 624 | 625 | function wrapMessage(message) { 626 | if (!isAction) return message; 627 | return '\u0001ACTION ' + message + '\u0001'; 628 | } 629 | 630 | beforeEach(function() { 631 | this.sendStub = sinon.stub(this.client, 'send'); 632 | }); 633 | 634 | it('skips if no text given', function() { 635 | this.client[commandName]('#channel'); 636 | expect(this.sendStub.callCount).to.equal(0); 637 | }); 638 | 639 | it('works with basic text', function() { 640 | this.client[commandName]('#channel', 'test message'); 641 | expect(this.sendStub.args).to.deep.equal([ 642 | [expectedCommand, '#channel', wrapMessage('test message')] 643 | ]); 644 | }); 645 | 646 | it('works with basic text and user', function() { 647 | this.client[commandName]('testbot2', 'test message'); 648 | expect(this.sendStub.args).to.deep.equal([ 649 | [expectedCommand, 'testbot2', wrapMessage('test message')] 650 | ]); 651 | }); 652 | 653 | it('splits on manual linebreak and skips empty lines', function() { 654 | this.client[commandName]('#channel', 'test\r\nmessage\r\n\r\nanother'); 655 | expect(this.sendStub.args).to.deep.equal([ 656 | [expectedCommand, '#channel', wrapMessage('test')], 657 | [expectedCommand, '#channel', wrapMessage('message')], 658 | [expectedCommand, '#channel', wrapMessage('another')] 659 | ]); 660 | }); 661 | 662 | it('splits on long lines as normal for _speak', function() { 663 | var splitStub = sinon.stub(); 664 | splitStub.callsFake(function(str) { 665 | return str.split(' '); 666 | }); 667 | this.client._splitLongLines = splitStub; 668 | this.client[commandName]('#channel', 'test message lines'); 669 | expect(this.sendStub.args).to.deep.equal([ 670 | [expectedCommand, '#channel', wrapMessage('test')], 671 | [expectedCommand, '#channel', wrapMessage('message')], 672 | [expectedCommand, '#channel', wrapMessage('lines')] 673 | ]); 674 | }); 675 | } 676 | 677 | describe('#notice', function() { 678 | testHelpers.hookMockSetup(beforeEach, afterEach); 679 | sharedExamplesForSpeaks('notice', 'NOTICE'); 680 | 681 | it('does not emit selfMessage on send', function() { 682 | var selfMsgSpy = sinon.spy(); 683 | this.client.on('selfMessage', selfMsgSpy); 684 | this.client.notice('target', 'test message'); 685 | expect(selfMsgSpy.callCount).to.equal(0); 686 | }); 687 | }); 688 | 689 | describe('#say', function() { 690 | testHelpers.hookMockSetup(beforeEach, afterEach); 691 | sharedExamplesForSpeaks('say', 'PRIVMSG'); 692 | 693 | it('emits selfMessage on send', function() { 694 | var selfMsgSpy = sinon.spy(); 695 | this.client.on('selfMessage', selfMsgSpy); 696 | this.client.say('target', 'test message'); 697 | expect(selfMsgSpy.args).to.deep.equal([ 698 | ['target', 'test message'] 699 | ]); 700 | }); 701 | }); 702 | 703 | describe('#action', function() { 704 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: {messageSplit: 19}, meta: {withoutServer: true}}); 705 | sharedExamplesForSpeaks('action', 'PRIVMSG', true); 706 | 707 | it('calculates maxLength correctly', function() { 708 | var client = this.client; 709 | var tests = [ 710 | {maxLineLength: 39, expected: 10}, 711 | {maxLineLength: 16, expected: 1} 712 | ]; 713 | tests.forEach(function(item) { 714 | var splitStub = sinon.stub().callsFake(function(words) { return [words]; }); 715 | client._splitLongLines = splitStub; 716 | client.maxLineLength = item.maxLineLength; 717 | client.action('target', 'test message'); // sample data 718 | expect(splitStub.args).to.deep.equal([ 719 | ['test message', item.expected, []] 720 | ]); 721 | }); 722 | }); 723 | 724 | it('emits selfMessage on action send', function() { 725 | var selfMsgSpy = sinon.spy(); 726 | this.client.on('selfMessage', selfMsgSpy); 727 | this.client.action('target', 'test message'); 728 | expect(selfMsgSpy.args).to.deep.equal([ 729 | ['target', '\u0001ACTION test message\u0001'] 730 | ]); 731 | }); 732 | }); 733 | 734 | describe('#ctcp', function() { 735 | testHelpers.hookMockSetup(beforeEach, afterEach, {meta: {withoutServer: true}}); 736 | 737 | beforeEach(function() { 738 | this.saySpy = sinon.spy(); 739 | this.noticeSpy = sinon.spy(); 740 | this.client.say = this.saySpy; 741 | this.client.notice = this.noticeSpy; 742 | }); 743 | 744 | it('passes through properly', function() { 745 | this.client.ctcp('user', null, 'ACTION message'); 746 | expect(this.noticeSpy.args).to.deep.equal([ 747 | ['user', '\u0001ACTION message\u0001'] 748 | ]); 749 | expect(this.saySpy.callCount).to.equal(0); 750 | }); 751 | 752 | it('uses privmsg instead if told to', function() { 753 | this.client.ctcp('user', 'privmsg', 'ACTION message'); 754 | expect(this.noticeSpy.callCount).to.equal(0); 755 | expect(this.saySpy.args).to.deep.equal([ 756 | ['user', '\u0001ACTION message\u0001'] 757 | ]); 758 | }); 759 | }); 760 | }); 761 | -------------------------------------------------------------------------------- /test/test-autorenick.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | var sinon = require('sinon'); 5 | 6 | describe('Client', function() { 7 | describe('autoRenick', function() { 8 | function defaultRenickTest(local, onRegister) { 9 | var rebuked = false; 10 | var mock = local.mock; 11 | var client = local.client; 12 | mock.on('line', function(line) { 13 | var args = line.split(' '); 14 | if (args[0] !== 'NICK') return; 15 | if (args[1] === 'testbot') { 16 | if (!rebuked) { 17 | rebuked = true; 18 | mock.send(':localhost 433 * testbot :Nickname is already in use.\r\n'); 19 | } else { 20 | mock.send(':testbot2!~testbot@mockhost.com NICK :testbot\r\n'); 21 | } 22 | } else if (args[1] === 'testbot1') { 23 | mock.send(':localhost 433 * testbot1 :Nickname is already in use.\r\n'); 24 | } else if (args[1] === 'testbot2') { 25 | mock.greet('testbot2'); 26 | } 27 | }); 28 | client.on('registered', function() { 29 | if (onRegister) onRegister(); 30 | }); 31 | } 32 | 33 | context('with config disabled', function() { 34 | var clientConfig = {autoRenick: false, renickDelay: 300}; 35 | 36 | context('when it gets the desired nickname', function() { 37 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig}); 38 | 39 | beforeEach(function() { 40 | this.nickSpy = this.lineSpy.withArgs(sinon.match(/^NICK/i)); 41 | }); 42 | 43 | it('settles on first nickname', function() { 44 | expect(this.client.nick).to.equal('testbot'); 45 | expect(this.client.hostMask).to.equal('testbot'); 46 | expect(this.client.maxLineLength).to.equal(483); 47 | }); 48 | 49 | it('does not plan to renick', function() { 50 | expect(this.client.conn.renickInterval).to.be.undefined; 51 | }); 52 | 53 | it('does not send renick', function(done) { 54 | var nickSpy = this.nickSpy; 55 | setTimeout(function() { 56 | expect(nickSpy.calledOnce).to.be.true; 57 | expect(nickSpy.calledWithExactly('NICK testbot')).to.be.true; 58 | done(); 59 | }, 400); 60 | }); 61 | }); 62 | 63 | context('when it does not get the desired nickname', function() { 64 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig, meta: {callbackEarly: true, autoGreet: false}}); 65 | 66 | beforeEach(function() { 67 | this.nickSpy = this.lineSpy.withArgs(sinon.match(/^NICK/i)); 68 | }); 69 | 70 | it('attains suitable fallback', function(done) { 71 | var client = this.client; 72 | var nickSpy = this.nickSpy; 73 | defaultRenickTest(this, function onRegistered() { 74 | expect(nickSpy.args).to.deep.equal([ 75 | ['NICK testbot'], 76 | ['NICK testbot1'], 77 | ['NICK testbot2'] 78 | ]); 79 | expect(client.nick).to.eq('testbot2'); 80 | expect(client.hostMask).to.equal('testbot'); 81 | expect(client.maxLineLength).to.equal(482); 82 | done(); 83 | }); 84 | }); 85 | 86 | it('does not plan to renick', function(done) { 87 | var client = this.client; 88 | defaultRenickTest(this, function onRegistered() { 89 | expect(client.conn.renickInterval).to.be.undefined; 90 | done(); 91 | }); 92 | }); 93 | 94 | it('does not send renick', function(done) { 95 | var nickSpy = this.nickSpy; 96 | setTimeout(function() { 97 | expect(nickSpy.calledOnce).to.be.true; 98 | expect(nickSpy.calledWithExactly('NICK testbot')).to.be.true; 99 | done(); 100 | }, 500); 101 | }); 102 | }); 103 | }); 104 | 105 | context('with config enabled', function() { 106 | context('when it gets the desired nickname', function() { 107 | var clientConfig = {autoRenick: true, renickDelay: 300}; 108 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig}); 109 | 110 | beforeEach(function() { 111 | this.nickSpy = this.lineSpy.withArgs(sinon.match(/^NICK/i)); 112 | }); 113 | 114 | it('settles on first nickname', function() { 115 | expect(this.client.nick).to.equal('testbot'); 116 | expect(this.client.hostMask).to.equal('testbot'); 117 | expect(this.client.maxLineLength).to.equal(483); 118 | }); 119 | 120 | it('does not plan to renick', function() { 121 | expect(this.client.conn.renickInterval).to.be.undefined; 122 | }); 123 | 124 | it('does not send renick', function(done) { 125 | var nickSpy = this.nickSpy; 126 | setTimeout(function() { 127 | expect(nickSpy.calledOnce).to.be.true; 128 | expect(nickSpy.calledWithExactly('NICK testbot')).to.be.true; 129 | done(); 130 | }, 400); 131 | }); 132 | }); 133 | 134 | context('when it does not get the desired nickname', function() { 135 | context('longer tests', function() { 136 | var clientConfig = {autoRenick: true, renickDelay: 300}; 137 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig, meta: {callbackEarly: true, autoGreet: false}}); 138 | 139 | beforeEach(function() { 140 | this.nickSpy = this.lineSpy.withArgs(sinon.match(/^NICK/i)); 141 | }); 142 | 143 | it('attains suitable fallback', function(done) { 144 | var client = this.client; 145 | var nickSpy = this.nickSpy; 146 | defaultRenickTest(this, function onRegistered() { 147 | expect(nickSpy.args).to.deep.equal([ 148 | ['NICK testbot'], 149 | ['NICK testbot1'], 150 | ['NICK testbot2'] 151 | ]); 152 | expect(client.nick).to.eq('testbot2'); 153 | expect(client.hostMask).to.equal('testbot'); 154 | expect(client.maxLineLength).to.equal(482); 155 | done(); 156 | }); 157 | }); 158 | 159 | it('plans to renick', function(done) { 160 | var client = this.client; 161 | defaultRenickTest(this, function onRegistered() { 162 | expect(client.conn.renickInterval).to.be.ok; 163 | done(); 164 | }); 165 | }); 166 | 167 | it('does not renick early', function(done) { 168 | var nickSpy = this.nickSpy; 169 | defaultRenickTest(this, function onRegistered() { 170 | setTimeout(function() { 171 | expect(nickSpy.args).to.deep.equal([ 172 | ['NICK testbot'], 173 | ['NICK testbot1'], 174 | ['NICK testbot2'] 175 | ]); 176 | done(); 177 | }, 250); 178 | }); 179 | }); 180 | 181 | it('renicks', function(done) { 182 | var nickSpy = this.nickSpy; 183 | defaultRenickTest(this, function onRegistered() { 184 | setTimeout(function() { 185 | expect(nickSpy.args).to.deep.equal([ 186 | ['NICK testbot'], 187 | ['NICK testbot1'], 188 | ['NICK testbot2'], 189 | ['NICK testbot'] 190 | ]); 191 | done(); 192 | }, 350); 193 | }); 194 | }); 195 | 196 | it('stops renicking when it gets its nickname', function(done) { 197 | var client = this.client; 198 | var nickSpy = this.nickSpy; 199 | defaultRenickTest(this, function onRegistered() { 200 | client.on('nick', function(oldNick, newNick) { 201 | if (oldNick !== 'testbot2' || newNick !== 'testbot') return; 202 | expect(client.nick).to.eq('testbot'); 203 | expect(client.hostMask).to.equal('testbot'); 204 | expect(client.maxLineLength).to.equal(483); 205 | expect(client.conn.renickInterval).to.be.null; 206 | expect(nickSpy.args).to.deep.equal([ 207 | ['NICK testbot'], 208 | ['NICK testbot1'], 209 | ['NICK testbot2'], 210 | ['NICK testbot'] 211 | ]); 212 | setTimeout(function() { 213 | expect(nickSpy.args).to.deep.equal([ 214 | ['NICK testbot'], 215 | ['NICK testbot1'], 216 | ['NICK testbot2'], 217 | ['NICK testbot'] 218 | ]); 219 | done(); 220 | }, 350); 221 | }); 222 | }); 223 | }); 224 | }); 225 | 226 | context('shorter tests', function() { 227 | function basicRenickTest(local, expected, onFinish, repeatRebuke) { 228 | var rebuked = false; 229 | var greeted = false; 230 | var mock = local.mock; 231 | var nickSpy = local.nickSpy; 232 | mock.on('line', function(line) { 233 | var args = line.split(' '); 234 | if (args[0] !== 'NICK') return; 235 | if (args[1] === 'testbot') { 236 | if (repeatRebuke || !rebuked) { 237 | rebuked = true; 238 | mock.send(':localhost 433 * testbot :Nickname is already in use.\r\n'); 239 | } 240 | } else if (!greeted) { 241 | mock.greet(args[1]); 242 | } 243 | if (expected.length === nickSpy.args.length) { 244 | expect(nickSpy.args).to.deep.equal(expected); 245 | setTimeout(function() { 246 | expect(nickSpy.args).to.deep.equal(expected); 247 | onFinish(); 248 | }, 200); 249 | } 250 | }); 251 | } 252 | 253 | var metaConfig = {callbackEarly: true, autoGreet: false}; 254 | var clientConfig = {autoRenick: true, renickDelay: 50, renickCount: 3}; 255 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig, meta: metaConfig}); 256 | 257 | beforeEach(function() { 258 | this.nickSpy = this.lineSpy.withArgs(sinon.match(/^NICK/i)); 259 | }); 260 | 261 | it('only renicks given amount without further response', function(done) { 262 | basicRenickTest( 263 | this, 264 | [ 265 | ['NICK testbot'], 266 | ['NICK testbot1'], 267 | ['NICK testbot'], 268 | ['NICK testbot'], 269 | ['NICK testbot'] 270 | ], 271 | done 272 | ); 273 | }); 274 | 275 | it('only renicks given amount with further response', function(done) { 276 | basicRenickTest( 277 | this, 278 | [ 279 | ['NICK testbot'], 280 | ['NICK testbot1'], 281 | ['NICK testbot'], 282 | ['NICK testbot'], 283 | ['NICK testbot'] 284 | ], 285 | done, 286 | true 287 | ); 288 | }); 289 | 290 | it('does not renick if nickinuse handler calls cancelAutoRenick', function(done) { 291 | var client = this.client; 292 | client.on('raw', function handler(message) { 293 | if (message.command !== 'err_nicknameinuse') return; 294 | client.removeListener('raw', handler); 295 | setTimeout(function() { 296 | client.cancelAutoRenick(); 297 | expect(client.conn.renickInterval).to.be.null; 298 | }, 75); 299 | }); 300 | 301 | basicRenickTest( 302 | this, 303 | [ 304 | ['NICK testbot'], 305 | ['NICK testbot1'], 306 | ['NICK testbot'] 307 | ], 308 | function() { 309 | expect(client.conn.renickInterval).to.be.null; 310 | done(); 311 | } 312 | ); 313 | }); 314 | 315 | it('does not renick if it renicks in the meantime', function(done) { 316 | var client = this.client; 317 | var mock = this.mock; 318 | client.on('raw', function handler(message) { 319 | if (message.command !== 'err_nicknameinuse') return; 320 | client.removeListener('raw', handler); 321 | expect(client.conn.renickInterval).to.be.ok; 322 | mock.send(':testbot1!~testbot@mockhost.com NICK :testbot2\r\n'); 323 | }); 324 | 325 | basicRenickTest( 326 | this, 327 | [ 328 | ['NICK testbot'], 329 | ['NICK testbot1'] 330 | ], 331 | function() { 332 | expect(client.conn.renickInterval).to.be.null; 333 | done(); 334 | } 335 | ); 336 | }); 337 | 338 | it('does not renick if it has its config nick changed to the temporary one', function(done) { 339 | var client = this.client; 340 | client.on('raw', function handler(message) { 341 | if (message.command !== 'err_nicknameinuse') return; 342 | client.removeListener('raw', handler); 343 | expect(client.conn.renickInterval).to.be.ok; 344 | client.opt.nick = 'testbot1'; 345 | }); 346 | 347 | basicRenickTest( 348 | this, 349 | [ 350 | ['NICK testbot'], 351 | ['NICK testbot1'] 352 | ], 353 | function() { 354 | expect(client.nick).to.eq('testbot1'); 355 | expect(client.opt.nick).to.eq('testbot1'); 356 | expect(client.conn.renickInterval).to.be.null; 357 | done(); 358 | } 359 | ); 360 | }); 361 | }); 362 | }); 363 | }); 364 | }); 365 | }); 366 | -------------------------------------------------------------------------------- /test/test-cmdqueue.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | var sinon = require('sinon'); 5 | 6 | describe('Client', function() { 7 | describe('command queue', function() { 8 | context('with config disabled', function() { 9 | testHelpers.hookMockSetup(beforeEach, afterEach); 10 | 11 | beforeEach(function(done) { 12 | var self = this; 13 | setTimeout(function() { 14 | self.connSpy = sinon.spy(); 15 | self.client.conn.write = self.connSpy; 16 | done(); 17 | }, 10); 18 | }); 19 | 20 | it('is not enabled', function() { 21 | this.client.send('PING', 'test 1'); 22 | this.client.send('PING', 'test 2'); 23 | expect(this.connSpy.args).to.deep.equal([ 24 | ['PING :test 1\r\n'], 25 | ['PING :test 2\r\n'] 26 | ]); 27 | }); 28 | }); 29 | 30 | function sharedExamplesForCmdQueue() { 31 | beforeEach(function() { 32 | this.connSpy = sinon.spy(); 33 | this.client.conn.write = this.connSpy; 34 | }); 35 | 36 | it('is enabled', function(done) { 37 | var self = this; 38 | self.client.send('PING', 'test 1'); 39 | self.client.send('PING', 'test 2'); 40 | expect(self.connSpy.callCount).to.equal(0); 41 | setTimeout(first, 10); 42 | setTimeout(second, 30); 43 | 44 | function first() { 45 | expect(self.connSpy.args).to.deep.equal([ 46 | ['PING :test 1\r\n'] 47 | ]); 48 | } 49 | function second() { 50 | expect(self.connSpy.args).to.deep.equal([ 51 | ['PING :test 1\r\n'], 52 | ['PING :test 2\r\n'] 53 | ]); 54 | done(); 55 | } 56 | }); 57 | 58 | it('provides internal _sendImmediate to send immediately', function() { 59 | var self = this; 60 | function wrap() { 61 | self.client._sendImmediate('PING', 'test 1'); 62 | self.client._sendImmediate('PING', 'test 2'); 63 | } 64 | expect(wrap).not.to.throw(); 65 | expect(this.connSpy.args).to.deep.equal([ 66 | ['PING :test 1\r\n'], 67 | ['PING :test 2\r\n'] 68 | ]); 69 | }); 70 | 71 | it('disconnects immediately and clears queue', function(done) { 72 | var self = this; 73 | self.client.send('PING', 'test 1'); 74 | self.client.send('PING', 'test 2'); 75 | expect(self.connSpy.callCount).to.equal(0); 76 | setTimeout(first, 10); 77 | setTimeout(second, 50); 78 | 79 | function first() { 80 | expect(self.connSpy.args).to.deep.equal([ 81 | ['PING :test 1\r\n'] 82 | ]); 83 | self.client.disconnect(); 84 | expect(self.connSpy.args).to.deep.equal([ 85 | ['PING :test 1\r\n'], 86 | ['QUIT :node-irc says goodbye\r\n'] 87 | ]); 88 | } 89 | function second() { 90 | expect(self.connSpy.args).to.deep.equal([ 91 | ['PING :test 1\r\n'], 92 | ['QUIT :node-irc says goodbye\r\n'] 93 | ]); 94 | done(); 95 | } 96 | }); 97 | 98 | it('can be disabled', function() { 99 | expect(this.client.floodProtectionInterval).to.be.ok; 100 | expect(this.client.floodProtectionEnabled).to.be.true; 101 | expect(this.client.cmdQueue).to.be.empty; 102 | expect(this.connSpy.args).to.be.empty; 103 | 104 | this.client.deactivateFloodProtection(); 105 | 106 | // it clears the relevant things: 107 | expect(this.client.floodProtectionInterval).to.be.null; 108 | expect(this.client.floodProtectionEnabled).to.be.false; 109 | expect(this.client.cmdQueue).to.be.empty; 110 | expect(this.client._origSend).to.be.null; 111 | expect(this.client._sendImmediate).to.be.null; 112 | expect(this.client._clearCmdQueue).to.be.null; 113 | expect(this.client.dequeue).to.be.null; 114 | expect(this.connSpy.args).to.be.empty; 115 | 116 | // and it now sends immediately: 117 | this.client.send('PING', 'test1'); 118 | this.client.send('PING', 'test2'); 119 | expect(this.connSpy.args).to.deep.equal([ 120 | ['PING test1\r\n'], 121 | ['PING test2\r\n'] 122 | ]); 123 | }); 124 | 125 | it('sends queue immediately in order when disabled', function(done) { 126 | var self = this; 127 | self.client.send('PING', 'test1'); 128 | self.client.send('PING', 'test2'); 129 | self.client.send('PING', 'test3'); 130 | expect(self.connSpy.args).to.be.empty; 131 | 132 | self.client.deactivateFloodProtection(); 133 | // it sends in order immediately: 134 | expect(self.client.cmdQueue).to.be.empty; 135 | expect(self.connSpy.callCount).to.equal(3); 136 | expect(self.connSpy.args).to.deep.equal([ 137 | ['PING test1\r\n'], 138 | ['PING test2\r\n'], 139 | ['PING test3\r\n'] 140 | ]); 141 | 142 | // and it now sends immediately when called more: 143 | self.client.send('PING', 'test4'); 144 | self.client.send('PING', 'test5'); 145 | expect(self.client.cmdQueue).to.be.empty; 146 | expect(self.connSpy.args).to.deep.equal([ 147 | ['PING test1\r\n'], 148 | ['PING test2\r\n'], 149 | ['PING test3\r\n'], 150 | ['PING test4\r\n'], 151 | ['PING test5\r\n'] 152 | ]); 153 | 154 | // and it doesn't send duplicates: 155 | setTimeout(end, 50); 156 | 157 | function end() { 158 | expect(self.connSpy.callCount).to.equal(5); 159 | done(); 160 | } 161 | }); 162 | 163 | it('does not do anything when deactivated twice', function(done) { 164 | var self = this; 165 | self.client.send('PING', 'test1'); 166 | self.client.send('PING', 'test2'); 167 | expect(self.connSpy.args).to.be.empty; 168 | 169 | self.client.deactivateFloodProtection(); 170 | expect(self.client.cmdQueue).to.be.empty; 171 | expect(self.connSpy.callCount).to.equal(2); 172 | 173 | // and it now sends immediately when called more: 174 | self.client.send('PING', 'test3'); 175 | self.client.send('PING', 'test4'); 176 | expect(self.client.cmdQueue).to.be.empty; 177 | expect(self.connSpy.args).to.deep.equal([ 178 | ['PING test1\r\n'], 179 | ['PING test2\r\n'], 180 | ['PING test3\r\n'], 181 | ['PING test4\r\n'] 182 | ]); 183 | 184 | // and doesn't fail when re-disabled: 185 | self.client.deactivateFloodProtection(); 186 | expect(self.client.cmdQueue).to.be.empty; 187 | expect(self.connSpy.args).to.deep.equal([ 188 | ['PING test1\r\n'], 189 | ['PING test2\r\n'], 190 | ['PING test3\r\n'], 191 | ['PING test4\r\n'] 192 | ]); 193 | 194 | // and it doesn't send duplicates: 195 | setTimeout(end, 50); 196 | 197 | function end() { 198 | expect(self.connSpy.callCount).to.equal(4); 199 | done(); 200 | } 201 | }); 202 | } 203 | 204 | context('with config enabled', function() { 205 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: {floodProtection: true, floodProtectionDelay: 10}}); 206 | 207 | beforeEach(function(done) { 208 | setTimeout(done, 50); 209 | }); 210 | 211 | sharedExamplesForCmdQueue(); 212 | }); 213 | 214 | context('with method called', function() { 215 | testHelpers.hookMockSetup(beforeEach, afterEach); 216 | 217 | beforeEach(function(done) { 218 | this.client.activateFloodProtection(10); 219 | setTimeout(done, 10); 220 | }); 221 | 222 | sharedExamplesForCmdQueue(); 223 | }); 224 | 225 | context('with method called to enable, disable and re-enable', function() { 226 | testHelpers.hookMockSetup(beforeEach, afterEach); 227 | 228 | beforeEach(function(done) { 229 | this.client.activateFloodProtection(10); 230 | this.client.deactivateFloodProtection(); 231 | this.client.activateFloodProtection(10); 232 | setTimeout(done, 10); 233 | }); 234 | 235 | sharedExamplesForCmdQueue(); 236 | }); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /test/test-colors.js: -------------------------------------------------------------------------------- 1 | var colors = require('../lib/colors.js'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | 5 | describe('Colors', function() { 6 | describe('.wrap', function() { 7 | it('does nothing if invalid color given', function() { 8 | expect(colors.wrap('unknown', 'test')).to.equal('test'); 9 | }); 10 | 11 | it('wraps in color without resetColor given', function() { 12 | expect(colors.wrap('white', 'test')).to.equal('\u000300test\u000f'); 13 | }); 14 | 15 | it('wraps in color with resetColor given', function() { 16 | expect(colors.wrap('white', 'test', 'black')).to.equal('\u000300test\u000301'); 17 | }); 18 | 19 | it('wraps in color even with invalid resetColor given', function() { 20 | expect(colors.wrap('white', 'test', 'invalid')).to.equal('\u000300test\u000f'); 21 | }); 22 | }); 23 | 24 | describe('.codes', function() { 25 | specify('should be exported', function() { 26 | expect(colors.codes).to.be.an('object'); 27 | expect(colors.codes.reset).to.eq('\u000f'); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/test-convert-encoding.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var checks = testHelpers.getFixtures('convert-encoding'); 3 | var proxyquire = require('proxyquire'); 4 | var chai = require('chai'); 5 | var expect = chai.expect; 6 | 7 | describe('Client', function() { 8 | function ircStubbedWith(chardet, converter) { 9 | return proxyquire('../lib/irc', { chardet: chardet, 'iconv-lite': converter }); 10 | } 11 | 12 | describe('convertEncoding', function() { 13 | function sharedExamplesFor(encoding) { 14 | var clientConfig = {}; 15 | if (encoding) clientConfig.encoding = encoding; 16 | 17 | context('with stubbed libraries', function() { 18 | it('works with valid data'); 19 | it('does not throw with sample data'); 20 | it('does not throw with invalid data'); 21 | }); 22 | 23 | context('without optional libraries', function() { 24 | it('does not throw with sample data', function() { 25 | var ircWithoutCharset = ircStubbedWith(null, null); 26 | var client = new ircWithoutCharset.Client('localhost', 'nick', Object.assign({autoConnect: false}, clientConfig)); 27 | expect(client.canConvertEncoding()).not.to.be.true; 28 | checks.causesException.forEach(function(line) { 29 | var wrap = function() { 30 | client.convertEncoding(line); 31 | }; 32 | expect(wrap).not.to.throw(); 33 | }); 34 | }); 35 | }); 36 | 37 | context('with proper charset detector and converter', function() { 38 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig}); 39 | beforeEach(function() { 40 | if (!this.client.canConvertEncoding()) this.skip(); 41 | }); 42 | 43 | it('does not throw with sample data', function() { 44 | var client = this.client; 45 | checks.causesException.forEach(function(line) { 46 | var wrap = function() { 47 | client.convertEncoding(Buffer.from(line)); 48 | }; 49 | expect(wrap).not.to.throw(); 50 | }); 51 | }); 52 | 53 | it('does not throw with invalid data'); 54 | }); 55 | } 56 | 57 | context('without encoding config', function() { 58 | sharedExamplesFor(); 59 | }); 60 | 61 | context('with utf-8 encoding config', function() { 62 | var encoding = 'utf-8'; 63 | sharedExamplesFor(encoding); 64 | 65 | context("with valid data", function() { 66 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: {encoding: encoding}}); 67 | 68 | var data = checks.sampleData; 69 | Object.keys(data).forEach(function(conversion) { 70 | var tests = data[conversion]; 71 | it('works ' + conversion, function() { 72 | var client = this.client; 73 | tests.forEach(function(pair) { 74 | var original = Buffer.from(pair[0]); 75 | var expected = Buffer.from(pair[1]); 76 | var actual = client.convertEncoding(original); 77 | expect(actual).to.deep.equal(expected); 78 | }); 79 | }); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('canConvertEncoding', function() { 86 | var latinData = [0x73, 0x63, 0x68, 0xf6, 0x6e]; 87 | var utfData = [0x73, 0x63, 0x68, 0xc3, 0xb6, 0x6e]; 88 | var mockCharsetDetector = {detect: function(str) { 89 | expect(Array.from(str)).to.deep.equal(latinData); 90 | return 'ISO-8859-1'; 91 | }}; 92 | var mockConv = { 93 | decode: function(str, charset) { 94 | expect(Array.from(str)).to.deep.equal(latinData); 95 | expect(charset).to.equal('ISO-8859-1'); 96 | return 'stubbed data'; 97 | }, 98 | encode: function(decoded, encoding) { 99 | expect(decoded).to.equal('stubbed data'); 100 | expect(encoding).to.equal('utf-8'); 101 | return utfData; 102 | } 103 | }; 104 | 105 | it('is false when the charset detector doesn\'t load', function() { 106 | var ircWithoutCharsetDetector = ircStubbedWith(null, mockConv); 107 | var client = new ircWithoutCharsetDetector.Client('localhost', 'nick', {autoConnect: false}); 108 | expect(ircWithoutCharsetDetector.canConvertEncoding()).to.be.false; 109 | expect(client.canConvertEncoding()).to.be.false; 110 | }); 111 | 112 | it('is false when the encoding converter doesn\'t load', function() { 113 | var ircWithoutIconv = ircStubbedWith(mockCharsetDetector, null); 114 | var client = new ircWithoutIconv.Client('localhost', 'nick', {autoConnect: false}); 115 | expect(ircWithoutIconv.canConvertEncoding()).to.be.false; 116 | expect(client.canConvertEncoding()).to.be.false; 117 | }); 118 | 119 | it('is true when convertEncoding works with test data', function() { 120 | var ircWithRequires = ircStubbedWith(mockCharsetDetector, mockConv); 121 | var client = new ircWithRequires.Client('localhost', 'nick', {autoConnect: false}); 122 | expect(ircWithRequires.canConvertEncoding()).to.be.true; 123 | expect(client.canConvertEncoding()).to.be.true; 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/test-cyclingpingtimer.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var CyclingPingTimer = require('../lib/cycling_ping_timer.js'); 3 | var chai = require('chai'); 4 | var expect = chai.expect; 5 | var sinon = require('sinon'); 6 | 7 | describe('CyclingPingTimer', function() { 8 | beforeEach(function() { 9 | this.debugSpy = sinon.spy(); 10 | this.wantPingSpy = sinon.spy(); 11 | this.pingTimeoutSpy = sinon.spy(); 12 | this.clientStub = { 13 | opt: {millisecondsBeforePingTimeout: 500, millisecondsOfSilenceBeforePingSent: 200}, 14 | out: {debug: this.debugSpy} 15 | }; 16 | this.cyclingPingTimer = new CyclingPingTimer(this.clientStub); 17 | this.cyclingPingTimer.on('wantPing', this.wantPingSpy); 18 | this.cyclingPingTimer.on('pingTimeout', this.pingTimeoutSpy); 19 | }); 20 | 21 | afterEach(function() { 22 | this.cyclingPingTimer.stop(); 23 | }); 24 | 25 | it('is stopped by default', function() { 26 | expect(this.cyclingPingTimer.started).to.be.false; 27 | }); 28 | 29 | it('starts', function() { 30 | this.cyclingPingTimer.start(); 31 | expect(this.debugSpy.args).to.deep.equal([ 32 | ['CyclingPingTimer ' + this.cyclingPingTimer.timerNumber + ':', 'ping timer started'] 33 | ]); 34 | expect(this.cyclingPingTimer.started).to.be.true; 35 | expect(this.cyclingPingTimer.loopingTimeout).to.be.ok; 36 | }); 37 | 38 | it('cannot start when started', function() { 39 | this.cyclingPingTimer.start(); 40 | var loopingTimeout = this.cyclingPingTimer.loopingTimeout; 41 | this.cyclingPingTimer.start(); 42 | expect(this.debugSpy.args).to.deep.equal([ 43 | ['CyclingPingTimer ' + this.cyclingPingTimer.timerNumber + ':', 'ping timer started'], 44 | ['CyclingPingTimer ' + this.cyclingPingTimer.timerNumber + ':', 'can\'t start, not stopped!'] 45 | ]); 46 | expect(this.cyclingPingTimer.loopingTimeout).to.equal(loopingTimeout); 47 | expect(this.cyclingPingTimer.started).to.be.true; 48 | }); 49 | 50 | it('cannot stop when stopped', function() { 51 | this.cyclingPingTimer.stop(); 52 | expect(this.debugSpy.args).to.deep.equal([]); 53 | expect(this.cyclingPingTimer.started).to.be.false; 54 | }); 55 | 56 | it('stops', function() { 57 | this.cyclingPingTimer.start(); 58 | this.cyclingPingTimer.stop(); 59 | expect(this.debugSpy.args).to.deep.equal([ 60 | ['CyclingPingTimer ' + this.cyclingPingTimer.timerNumber + ':', 'ping timer started'], 61 | ['CyclingPingTimer ' + this.cyclingPingTimer.timerNumber + ':', 'ping timer stopped'] 62 | ]); 63 | expect(this.cyclingPingTimer.started).to.be.false; 64 | expect(this.cyclingPingTimer.loopingTimeout).to.be.null; 65 | }); 66 | 67 | it('does nothing if notified of activity when stopped', function() { 68 | var self = this; 69 | function wrap() { 70 | self.cyclingPingTimer.notifyOfActivity(); 71 | } 72 | expect(wrap).not.to.throw(); 73 | expect(self.debugSpy.args).to.be.empty; 74 | expect(self.cyclingPingTimer.started).to.be.false; 75 | expect(self.cyclingPingTimer.loopingTimeout).to.be.null; 76 | }); 77 | 78 | it('does not want ping early', function(done) { 79 | var cyclingPingTimer = this.cyclingPingTimer; 80 | cyclingPingTimer.start(); 81 | var wantPingSpy = this.wantPingSpy; 82 | setTimeout(function() { 83 | expect(wantPingSpy.called).to.be.false; 84 | done(); 85 | }, 150); 86 | }); 87 | 88 | it('wants ping after configured time', function(done) { 89 | var cyclingPingTimer = this.cyclingPingTimer; 90 | var debugSpy = this.debugSpy; 91 | var wantPingSpy = this.wantPingSpy; 92 | cyclingPingTimer.start(); 93 | setTimeout(function() { 94 | expect(wantPingSpy.calledOnce).to.be.true; 95 | expect(debugSpy.args).to.deep.equal([ 96 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer started'], 97 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'server silent for too long, let\'s send a PING'] 98 | ]); 99 | expect(cyclingPingTimer.pingWaitTimeout).to.be.ok; 100 | done(); 101 | }, 250); 102 | }); 103 | 104 | it('does not want ping if notified of activity', function(done) { 105 | var cyclingPingTimer = this.cyclingPingTimer; 106 | var debugSpy = this.debugSpy; 107 | cyclingPingTimer.start(); 108 | var wantPingSpy = this.wantPingSpy; 109 | setTimeout(function() { 110 | cyclingPingTimer.notifyOfActivity(); 111 | }, 150); 112 | setTimeout(function() { 113 | expect(wantPingSpy.called).to.be.false; 114 | expect(debugSpy.args).to.deep.equal([ 115 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer started'] 116 | ]); 117 | expect(cyclingPingTimer.loopingTimeout).to.be.ok; 118 | expect(cyclingPingTimer.pingWaitTimeout).to.be.null; 119 | expect(cyclingPingTimer.started).to.be.true; 120 | done(); 121 | }, 250); 122 | }); 123 | 124 | it('does not want ping if stopped', function(done) { 125 | var cyclingPingTimer = this.cyclingPingTimer; 126 | var debugSpy = this.debugSpy; 127 | cyclingPingTimer.start(); 128 | var wantPingSpy = this.wantPingSpy; 129 | setTimeout(function() { 130 | cyclingPingTimer.stop(); 131 | expect(cyclingPingTimer.loopingTimeout).to.be.null; 132 | expect(cyclingPingTimer.started).to.be.false; 133 | }, 150); 134 | setTimeout(function() { 135 | expect(wantPingSpy.called).to.be.false; 136 | expect(debugSpy.args).to.deep.equal([ 137 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer started'], 138 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer stopped'] 139 | ]); 140 | expect(cyclingPingTimer.loopingTimeout).to.be.null; 141 | expect(cyclingPingTimer.pingWaitTimeout).to.be.null; 142 | expect(cyclingPingTimer.started).to.be.false; 143 | done(); 144 | }, 250); 145 | }); 146 | 147 | it('does not time out early', function(done) { 148 | var cyclingPingTimer = this.cyclingPingTimer; 149 | var pingTimeoutSpy = this.pingTimeoutSpy; 150 | cyclingPingTimer.start(); 151 | cyclingPingTimer.on('wantPing', function() { 152 | setTimeout(function() { 153 | expect(pingTimeoutSpy.called).to.be.false; 154 | done(); 155 | }, 450); 156 | }); 157 | }); 158 | 159 | it('times out after configured time', function(done) { 160 | var cyclingPingTimer = this.cyclingPingTimer; 161 | var debugSpy = this.debugSpy; 162 | var pingTimeoutSpy = this.pingTimeoutSpy; 163 | cyclingPingTimer.start(); 164 | cyclingPingTimer.on('wantPing', function() { 165 | setTimeout(function() { 166 | expect(pingTimeoutSpy.calledOnce).to.be.true; 167 | expect(debugSpy.args).to.deep.equal([ 168 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer started'], 169 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'server silent for too long, let\'s send a PING'], 170 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer stopped'], 171 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timeout!'] 172 | ]); 173 | done(); 174 | }, 550); 175 | }); 176 | }); 177 | 178 | it('does not time out if notified of activity', function(done) { 179 | var cyclingPingTimer = this.cyclingPingTimer; 180 | var debugSpy = this.debugSpy; 181 | cyclingPingTimer.start(); 182 | var pingTimeoutSpy = this.pingTimeoutSpy; 183 | cyclingPingTimer.on('wantPing', function() { 184 | setTimeout(function() { 185 | cyclingPingTimer.notifyOfActivity(); 186 | }, 450); 187 | setTimeout(function() { 188 | expect(pingTimeoutSpy.called).to.be.false; 189 | expect(debugSpy.args).to.deep.equal([ 190 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer started'], 191 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'server silent for too long, let\'s send a PING'] 192 | ]); 193 | expect(cyclingPingTimer.loopingTimeout).to.be.ok; 194 | expect(cyclingPingTimer.pingWaitTimeout).to.be.null; 195 | expect(cyclingPingTimer.started).to.be.true; 196 | done(); 197 | }, 550); 198 | }); 199 | }); 200 | 201 | it('does not time out if stopped', function(done) { 202 | var cyclingPingTimer = this.cyclingPingTimer; 203 | var debugSpy = this.debugSpy; 204 | cyclingPingTimer.start(); 205 | var pingTimeoutSpy = this.pingTimeoutSpy; 206 | cyclingPingTimer.on('wantPing', function() { 207 | setTimeout(function() { 208 | cyclingPingTimer.stop(); 209 | expect(cyclingPingTimer.loopingTimeout).to.be.null; 210 | expect(cyclingPingTimer.started).to.be.false; 211 | }, 450); 212 | setTimeout(function() { 213 | expect(pingTimeoutSpy.called).to.be.false; 214 | expect(debugSpy.args).to.deep.equal([ 215 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer started'], 216 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'server silent for too long, let\'s send a PING'], 217 | ['CyclingPingTimer ' + cyclingPingTimer.timerNumber + ':', 'ping timer stopped'] 218 | ]); 219 | expect(cyclingPingTimer.loopingTimeout).to.be.null; 220 | expect(cyclingPingTimer.pingWaitTimeout).to.be.null; 221 | expect(cyclingPingTimer.started).to.be.false; 222 | done(); 223 | }, 550); 224 | }); 225 | }); 226 | }); 227 | 228 | describe('Client', function() { 229 | describe('ping timer', function() { 230 | context('with stubs', function() { 231 | testHelpers.hookMockSetup(beforeEach, afterEach, {meta: {stubTimer: true}}); 232 | beforeEach(function() { 233 | this.pingSpy = this.lineSpy.withArgs(sinon.match(/^PING/i)); 234 | }); 235 | 236 | it('sends ping when timer emits wantPing', function(done) { 237 | var self = this; 238 | self.mock.on('line', function(line) { 239 | if (line === 'PING 1') teardown(); 240 | }); 241 | 242 | self.client.conn.cyclingPingTimer.emit('wantPing'); 243 | 244 | function teardown() { 245 | expect(self.pingSpy.callCount).to.equal(1); 246 | done(); 247 | } 248 | }); 249 | 250 | it('notifies timer of activity when PONG received', function(done) { 251 | var self = this; 252 | expect(self.pingTimerStubs.notifyOfActivity.callCount).to.equal(1); 253 | expect(self.pingTimerStubs.start.callCount).to.equal(1); 254 | expect(self.pingTimerStubs.stop.callCount).to.equal(0); 255 | 256 | self.client.on('raw', function(message) { 257 | if (message.command !== 'PONG') return; 258 | expect(message.args).to.deep.equal(['1']); 259 | teardown(); 260 | }); 261 | 262 | self.mock.send(':localhost PONG 1\r\n'); 263 | 264 | function teardown() { 265 | expect(self.pingTimerStubs.notifyOfActivity.callCount).to.equal(2); 266 | expect(self.pingTimerStubs.start.callCount).to.equal(1); 267 | expect(self.pingTimerStubs.stop.callCount).to.equal(0); 268 | expect(self.pingSpy.callCount).to.equal(0); 269 | done(); 270 | } 271 | }); 272 | 273 | it('disconnects when timer emits pingTimeout', function(done) { 274 | var self = this; 275 | 276 | self.client.conn.once('close', function() { 277 | teardown(); 278 | }); 279 | 280 | expect(self.pingTimerStubs.notifyOfActivity.callCount).to.equal(1); 281 | expect(self.pingTimerStubs.start.callCount).to.equal(1); 282 | expect(self.pingTimerStubs.stop.callCount).to.equal(0); 283 | expect(self.pingSpy.callCount).to.equal(0); 284 | 285 | self.client.conn.cyclingPingTimer.emit('pingTimeout'); 286 | 287 | function teardown() { 288 | expect(self.pingTimerStubs.notifyOfActivity.callCount).to.equal(1); 289 | expect(self.pingTimerStubs.start.callCount).to.equal(1); 290 | expect(self.pingTimerStubs.stop.callCount).to.equal(2); // one for end(), one for on(close) 291 | expect(self.pingSpy.callCount).to.equal(0); 292 | // client attempts reconnect: 293 | expect(self.client.conn).to.be.null; 294 | expect(self.client.retryTimeout).to.be.ok; 295 | done(); 296 | } 297 | }); 298 | 299 | context('with foreign timer', function() { 300 | beforeEach(function(done) { 301 | this.foreignConn = this.client.conn; 302 | this.client.conn = null; 303 | this.client.connect(function() { 304 | done(); 305 | }); 306 | }); 307 | 308 | afterEach(function() { 309 | this.foreignConn.destroy(); 310 | }); 311 | 312 | it('ignores wantPing', function(done) { 313 | var self = this, conn = this.foreignConn; 314 | conn.cyclingPingTimer.emit('wantPing'); 315 | setTimeout(function() { 316 | expect(self.pingSpy.callCount).to.equal(0); 317 | done(); 318 | }, 50); 319 | }); 320 | 321 | it('ignores pingTimeout', function(done) { 322 | var self = this, conn = this.foreignConn; 323 | conn.cyclingPingTimer.emit('pingTimeout'); 324 | setTimeout(function() { 325 | expect(self.client.conn).to.be.ok; 326 | done(); 327 | }, 50); 328 | }); 329 | }); 330 | }); 331 | 332 | context('with server', function() { 333 | var clientConfig = { 334 | millisecondsOfSilenceBeforePingSent: 200, 335 | millisecondsBeforePingTimeout: 500 336 | }; 337 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig}); 338 | 339 | beforeEach(function() { 340 | this.pingSpy = this.lineSpy.withArgs(sinon.match(/^PING/i)); 341 | this.cyclingPingTimer = this.client.conn.cyclingPingTimer; 342 | }); 343 | 344 | it('does not send ping early', function(done) { 345 | var pingSpy = this.pingSpy; 346 | setTimeout(function() { 347 | expect(pingSpy.called).to.be.false; 348 | done(); 349 | }, 150); 350 | }); 351 | 352 | it('sends ping after time with no server activity', function(done) { 353 | var pingSpy = this.pingSpy; 354 | setTimeout(function() { 355 | expect(pingSpy.calledOnce).to.be.true; 356 | done(); 357 | }, 250); 358 | }); 359 | 360 | it('does not time out early', function(done) { 361 | var cyclingPingTimer = this.cyclingPingTimer; 362 | var conn = this.client.conn; 363 | cyclingPingTimer.on('wantPing', function() { 364 | setTimeout(function() { 365 | expect(cyclingPingTimer.started).to.be.true; 366 | expect(conn.destroyed).to.be.false; 367 | done(); 368 | }, 450); 369 | }); 370 | }); 371 | 372 | it('times out after period with no server activity', function(done) { 373 | var cyclingPingTimer = this.cyclingPingTimer; 374 | var conn = this.client.conn; 375 | this.cyclingPingTimer.on('wantPing', function() { 376 | setTimeout(function() { 377 | expect(cyclingPingTimer.started).to.be.false; 378 | expect(conn.destroyed).to.be.true; 379 | done(); 380 | }, 550); 381 | }); 382 | }); 383 | 384 | it('does not time out with activity', function(done) { 385 | var mock = this.mock; 386 | var pingSpy = this.pingSpy; 387 | var clientPingSpy = sinon.spy(); 388 | this.client.on('ping', clientPingSpy); 389 | var activityGiven = 0; 390 | var activityGiver; 391 | var giveActivity = function() { 392 | if (activityGiven >= 5) { 393 | clearInterval(activityGiver); 394 | expect(pingSpy.called).to.be.false; 395 | expect(clientPingSpy.called).to.be.true; 396 | done(); 397 | return; 398 | } 399 | activityGiven += 1; 400 | mock.send(':localhost PING ' + activityGiven.toString() + '\r\n'); 401 | }; 402 | 403 | activityGiver = setInterval(giveActivity, 100); 404 | }); 405 | }); 406 | }); 407 | }); 408 | -------------------------------------------------------------------------------- /test/test-mode.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | 5 | describe('Client events', function() { 6 | describe('+mode', function() { 7 | testHelpers.hookMockSetup(beforeEach, afterEach); 8 | 9 | it('should trigger +mode when joining as operator', function(done) { 10 | // Set prefix modes 11 | this.mock.send(':localhost 005 testbot PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server\r\n'); 12 | 13 | // Force join into auditorium 14 | this.mock.send(':testbot JOIN #auditorium\r\n'); 15 | 16 | // +o the invisible user 17 | this.mock.send(':ChanServ MODE #auditorium +o user\r\n'); 18 | 19 | this.client.on('+mode', function(channel, by, mode, argument) { 20 | if (channel === '#auditorium' && argument === 'user') { 21 | done(); 22 | } 23 | }); 24 | }); 25 | }); 26 | 27 | describe('modes', function() { 28 | testHelpers.hookMockSetup(beforeEach, afterEach); 29 | it('should have accurate state at each step as per fixture', function(done) { 30 | var mock = this.mock; 31 | var client = this.client; 32 | 33 | var count = 0; 34 | function checkModes() { 35 | expect(client.chans['#channel']).to.deep.equal(expected[count++]); 36 | if (count === expected.length) teardown(); 37 | } 38 | client.on('+mode', checkModes); 39 | client.on('-mode', checkModes); 40 | 41 | var fixture = testHelpers.getFixtures('mode'); 42 | var expected = fixture.expected; 43 | var serverMessages = fixture.serverMessages; 44 | 45 | serverMessages.forEach(function(message) { 46 | mock.send(message); 47 | }); 48 | 49 | function teardown() { 50 | expect(count).to.equal(expected.length); 51 | done(); 52 | } 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/test-output.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var expect = chai.expect; 3 | var sinon = require('sinon'); 4 | var proxyquire = require('proxyquire'); 5 | 6 | describe('Client', function() { 7 | describe('out', function() { 8 | before(function() { 9 | var logStub = sinon.spy(); 10 | var utilStub = {log: logStub}; 11 | var irc = proxyquire('../lib/irc', {util: utilStub}); 12 | this.logStub = logStub; 13 | this.ircWithStub = irc; 14 | 15 | this.makeClient = function(config) { 16 | this.client = new this.ircWithStub.Client('localhost', 'nick', Object.assign({autoConnect: false}, config)); 17 | return this.client; 18 | }; 19 | }); 20 | 21 | beforeEach(function() { 22 | this.logStub.resetHistory(); 23 | }); 24 | 25 | describe('debug', function() { 26 | it('does not output with debug config disabled', function() { 27 | this.makeClient({debug: false}); 28 | this.client.out.debug('Test message'); 29 | expect(this.logStub.args).to.deep.equal([]); 30 | }); 31 | 32 | it('outputs with debug config enabled', function() { 33 | this.makeClient({debug: true}); 34 | this.client.out.debug('Test message'); 35 | expect(this.logStub.args).to.deep.equal([['Test message']]); 36 | this.client.out.debug('New message'); 37 | expect(this.logStub.args).to.deep.equal([['Test message'], ['New message']]); 38 | }); 39 | }); 40 | 41 | describe('error', function() { 42 | before(function(){ 43 | this.testErrorOutput = function() { 44 | this.client.out.error('Test message'); 45 | expect(this.logStub.args).to.deep.equal([ 46 | ['\u001b[01;31mERROR:', 'Test message', '\u001b[0m'] 47 | ]); 48 | this.client.out.error('New message'); 49 | expect(this.logStub.args).to.deep.equal([ 50 | ['\u001b[01;31mERROR:', 'Test message', '\u001b[0m'], 51 | ['\u001b[01;31mERROR:', 'New message', '\u001b[0m'] 52 | ]); 53 | }; 54 | }); 55 | 56 | it('does not output with both configs disabled', function() { 57 | this.makeClient({debug: false, showErrors: false}); 58 | this.client.out.error('Test message'); 59 | expect(this.logStub.args).to.deep.equal([]); 60 | }); 61 | 62 | it('outputs if debug config enabled', function() { 63 | this.makeClient({debug: true, showErrors: false}); 64 | this.testErrorOutput(); 65 | }); 66 | 67 | it('outputs if errors config enabled', function() { 68 | this.makeClient({debug: false, showErrors: true}); 69 | this.testErrorOutput(); 70 | }); 71 | 72 | it('outputs if both configs enabled', function() { 73 | this.makeClient({debug: true, showErrors: true}); 74 | this.testErrorOutput(); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/test-parse-line.js: -------------------------------------------------------------------------------- 1 | var parseMessage = require('../lib/parse_message'); 2 | var testHelpers = require('./helpers'); 3 | var chai = require('chai'); 4 | var expect = chai.expect; 5 | 6 | describe('parseMessage', function() { 7 | function sharedExamples(type) { 8 | it('parses fixtures correctly', function() { 9 | var checks = testHelpers.getFixtures('parse-line'); 10 | Object.keys(checks).forEach(function(line) { 11 | var stripColors = false; 12 | var expected = Object.assign({}, checks[line]); 13 | if (Object.prototype.hasOwnProperty.call(expected, 'stripColors')) { 14 | stripColors = expected.stripColors; 15 | delete expected.stripColors; 16 | } 17 | expect(JSON.stringify(parseMessage(line, stripColors, type === 'strict'))).to.equal(JSON.stringify(expected)); 18 | }); 19 | }); 20 | } 21 | 22 | context('in strict mode', function() { 23 | sharedExamples('strict'); 24 | 25 | it('parses nonstandard fixtures according to spec', function() { 26 | var checks = testHelpers.getFixtures('parse-line-strict'); 27 | 28 | Object.keys(checks).forEach(function(line) { 29 | expect(JSON.stringify(parseMessage(line, false, true))).to.equal(JSON.stringify(checks[line])); 30 | }); 31 | }); 32 | }); 33 | 34 | context('in non-strict mode', function() { 35 | sharedExamples('non-strict'); 36 | 37 | it('parses Unicode fixtures correctly', function() { 38 | var checks = testHelpers.getFixtures('parse-line-nonstrict'); 39 | 40 | Object.keys(checks).forEach(function(line) { 41 | expect(JSON.stringify(parseMessage(line, false, false))).to.equal(JSON.stringify(checks[line])); 42 | }); 43 | }); 44 | 45 | it('does not crash with no prefix', function() { 46 | var checks = testHelpers.getFixtures('parse-line-noprefix'); 47 | 48 | Object.keys(checks).forEach(function(line) { 49 | expect(JSON.stringify(parseMessage(line, false, false))).to.equal(JSON.stringify(checks[line])); 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/test-quit.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | var sinon = require('sinon'); 5 | 6 | describe('Client', function() { 7 | describe('disconnect', function() { 8 | it('sends correct message to server', function(done) { 9 | function teardown(mocks) { 10 | var quitSpy = mocks.lineSpy.withArgs(sinon.match(/^QUIT/i)); 11 | expect(quitSpy.args).to.deep.equal([ 12 | ['QUIT :quitting as a test'] 13 | ]); 14 | mocks.mock.close(function(){ done(); }); 15 | } 16 | function body(mocks) { 17 | mocks.mock.on('end', function(){ teardown(mocks); }); 18 | mocks.client.disconnect('quitting as a test', function() { }); 19 | } 20 | testHelpers.setupMocks(null, body); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/test-reconnect.js: -------------------------------------------------------------------------------- 1 | var testHelpers = require('./helpers'); 2 | var chai = require('chai'); 3 | var expect = chai.expect; 4 | var sinon = require('sinon'); 5 | 6 | describe('Client', function() { 7 | describe('reconnection', function() { 8 | context('on connection interruption', function() { 9 | var clientConfig = {retryDelay: 50, autoConnect: false, millisecondsOfSilenceBeforePingSent: 100, millisecondsBeforePingTimeout: 200}; 10 | var metaConfig = {callbackEarly: true, autoGreet: false}; 11 | 12 | function sharedExample(callback) { 13 | context('with reconnecting client', function() { 14 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: clientConfig, meta: metaConfig}); 15 | 16 | it('reconnects with exactly one connection at a time', function(done) { 17 | var mock = this.mock; 18 | var client = this.client; 19 | var registeredSpy = sinon.spy(); 20 | client.on('registered', registeredSpy); 21 | var debugStub = sinon.stub(client.out, 'debug'); 22 | var abortSpy = sinon.spy(); 23 | client.on('abort', abortSpy); 24 | 25 | var conns = []; 26 | mock.server.on('connection', function(c) { 27 | conns.push(c); 28 | mock.greet(); 29 | }); 30 | 31 | client.once('registered', function() { 32 | setTimeout(function() { 33 | callback(client, conns); 34 | }, 100); 35 | setTimeout(teardown, 400); 36 | }); 37 | 38 | client.connect(); 39 | 40 | function teardown() { 41 | expect(registeredSpy.calledTwice).to.be.true; 42 | expect(conns.length).to.equal(2); 43 | expect(conns[0].destroyed).to.be.true; 44 | expect(debugStub.args).to.deep.include(['Waiting 50ms before retrying']); 45 | expect(abortSpy.callCount).to.equal(0); 46 | done(); 47 | } 48 | }); 49 | }); 50 | 51 | context('with opt.retryCount', function() { 52 | testHelpers.hookMockSetup(beforeEach, afterEach, {client: Object.assign({retryCount: 1}, clientConfig), meta: metaConfig}); 53 | 54 | it('retries when disconnected', function(done) { 55 | var self = this, mock = self.mock, client = self.client; 56 | var registerSpy = sinon.spy(); 57 | client.on('registered', registerSpy); 58 | var debugStub = sinon.stub(client.out, 'debug'); 59 | var abortSpy = sinon.spy(); 60 | client.on('abort', abortSpy); 61 | 62 | var conns = []; 63 | mock.server.on('connection', function(c) { 64 | conns.push(c); 65 | mock.greet(); 66 | }); 67 | 68 | client.once('registered', function() { 69 | setTimeout(function() { 70 | callback(client, conns); 71 | }, 100); 72 | setTimeout(teardown, 400); 73 | }); 74 | 75 | client.connect(); 76 | 77 | function teardown() { 78 | expect(registerSpy.callCount).to.equal(2); 79 | expect(conns.length).to.equal(2); 80 | expect(conns[0].destroyed).to.be.true; 81 | expect(debugStub.args).to.deep.include(['Waiting 50ms before retrying']); 82 | expect(abortSpy.callCount).to.equal(0); 83 | done(); 84 | } 85 | }); 86 | 87 | it('retries only once', function(done) { 88 | var self = this, mock = self.mock, client = self.client; 89 | var registerSpy = sinon.spy(); 90 | client.on('registered', registerSpy); 91 | var debugStub = sinon.stub(client.out, 'debug'); 92 | var abortSpy = sinon.spy(); 93 | client.on('abort', abortSpy); 94 | 95 | var conns = []; 96 | mock.server.on('connection', function(c) { 97 | conns.push(c); 98 | mock.greet(); 99 | }); 100 | 101 | client.once('registered', function() { 102 | setTimeout(function() { 103 | callback(client, conns); 104 | }, 100); 105 | setTimeout(function() { 106 | callback(client, conns); 107 | }, 200); 108 | setTimeout(teardown, 400); 109 | }); 110 | 111 | client.connect(); 112 | 113 | function teardown() { 114 | expect(registerSpy.callCount).to.equal(2); 115 | expect(conns.length).to.equal(2); 116 | expect(conns[0].destroyed).to.be.true; 117 | expect(debugStub.args).to.deep.include(['Maximum retry count (1) reached. Aborting']); 118 | expect(abortSpy.args).to.deep.equal([ 119 | [1] 120 | ]); 121 | done(); 122 | } 123 | }); 124 | 125 | it('obeys first parameter to Client.connect', function(done) { 126 | // doesn't reconnect 127 | var self = this, mock = self.mock, client = self.client; 128 | var registerSpy = sinon.spy(); 129 | client.on('registered', registerSpy); 130 | var debugStub = sinon.stub(client.out, 'debug'); 131 | var abortSpy = sinon.spy(); 132 | client.on('abort', abortSpy); 133 | 134 | var conns = []; 135 | mock.server.on('connection', function(c) { 136 | conns.push(c); 137 | mock.greet(); 138 | }); 139 | 140 | client.once('registered', function() { 141 | setTimeout(function() { 142 | callback(client, conns); 143 | }, 100); 144 | setTimeout(teardown, 400); 145 | }); 146 | 147 | client.connect(1); 148 | 149 | function teardown() { 150 | expect(registerSpy.callCount).to.equal(1); 151 | expect(conns.length).to.equal(1); 152 | expect(conns[0].destroyed).to.be.true; 153 | expect(debugStub.args).to.deep.include(['Maximum retry count (1) reached. Aborting']); 154 | expect(abortSpy.args).to.deep.equal([ 155 | [1] 156 | ]); 157 | done(); 158 | } 159 | }); 160 | }); 161 | } 162 | 163 | context('when ended', function() { 164 | // when the client ends the connection (potentially unexpectedly) 165 | sharedExample(function(client, _conns) { 166 | client.end(); 167 | }); 168 | }); 169 | 170 | context('when connection breaks', function() { 171 | // when connection breaks from server end, like connection error 172 | sharedExample(function(_client, conns) { 173 | conns[conns.length-1].destroy(); 174 | }); 175 | }); 176 | }); 177 | }); 178 | }); 179 | --------------------------------------------------------------------------------