├── .eslintrc ├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── changelog.yml │ ├── sign-off.yml │ └── tests.yml ├── .gitignore ├── .node-version ├── .npmignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── COPYING ├── README.md ├── changelog.d ├── .gitkeep ├── 114.feature ├── 115.bugfix ├── 116.bugfix ├── 117.bugfix └── 118.bugfix ├── docs ├── API.rst ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── example ├── bot.js ├── externalSasl.js └── secure.js ├── jest.config.js ├── package.json ├── pyproject.toml ├── scripts ├── changelog-check.sh └── changelog-release.sh ├── spec ├── client.spec.ts └── external-connection.spec.ts ├── src ├── capabilities.ts ├── codes.ts ├── colors.ts ├── events.ts ├── index.ts ├── irc.ts ├── parse_message.ts ├── splitLines.ts ├── state.ts └── testing │ └── index.ts ├── test ├── data │ ├── fixtures.json │ ├── ircd.key │ └── ircd.pem ├── helpers.ts ├── test-433-before-001.spec.ts ├── test-auditorium.spec.ts ├── test-convert-encoding.spec.ts ├── test-double-crlf.spec.ts ├── test-irc.spec.ts ├── test-parse-line.spec.ts ├── test-split-messages.spec.ts └── tsconfig.json ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/matrix-org/matrix-appservice-irc/blob/d69e4d1c40f9856812c948e4dc17e7747273fd67/.eslintrc 2 | { 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "parserOptions": { 8 | "ecmaVersion": 9, 9 | "ecmaFeatures": { 10 | "jsx": false 11 | } 12 | }, 13 | "env": { 14 | "node": true, 15 | "es6": true, 16 | "jasmine": true 17 | }, 18 | "extends": [ 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/recommended" 21 | ], 22 | "rules": { 23 | "camelcase": 0, 24 | "consistent-return": "off", 25 | "curly": 1, 26 | "default-case": 2, 27 | "guard-for-in": 2, 28 | "no-alert": 2, 29 | "no-caller": 2, 30 | "no-cond-assign": 2, 31 | "no-constant-condition": 0, 32 | "no-debugger": 2, 33 | "no-dupe-args": 2, 34 | "no-dupe-keys": 2, 35 | "no-duplicate-case": 2, 36 | "no-else-return": 2, 37 | "no-empty": 2, 38 | "no-empty-character-class": 2, 39 | "no-eq-null": 2, 40 | "no-ex-assign": 2, 41 | "no-extend-native": 2, 42 | "no-extra-boolean-cast": 2, 43 | "no-extra-semi": 1, 44 | "no-fallthrough": 2, 45 | "no-func-assign": 2, 46 | "no-invalid-regexp": 2, 47 | "no-invalid-this": 2, 48 | "no-irregular-whitespace": 1, 49 | "no-lone-blocks": 2, 50 | "no-loop-func": 2, 51 | "no-multi-spaces": 1, 52 | "no-new-wrappers": 2, 53 | "no-new": 2, 54 | "no-octal": 2, 55 | "no-negated-in-lhs": 2, 56 | "no-obj-calls": 2, 57 | "no-redeclare": 2, 58 | "no-regex-spaces": 2, 59 | "no-return-assign": 2, 60 | "no-self-compare": 2, 61 | "no-shadow": "off", 62 | "no-unreachable": 2, 63 | "no-unexpected-multiline": 2, 64 | "no-unused-expressions": 2, 65 | "no-use-before-define": [1, "nofunc"], 66 | "no-useless-escape": 0, 67 | "no-var": 2, 68 | "use-isnan": 2, 69 | "valid-typeof": 2, 70 | "array-bracket-spacing": [1, "never"], 71 | "max-len": [1, 120], 72 | "brace-style": [1, "stroustrup", { "allowSingleLine": true }], 73 | "comma-spacing": [1, {"before": false, "after": true}], 74 | "comma-style": [1, "last"], 75 | "computed-property-spacing": [1, "never"], 76 | "consistent-this": [1, "self"], 77 | "eol-last": 1, 78 | "new-cap": 0, 79 | "new-parens": 1, 80 | "no-mixed-spaces-and-tabs": 1, 81 | "no-nested-ternary": 1, 82 | "no-spaced-func": 1, 83 | "no-trailing-spaces": 1, 84 | "keyword-spacing": [1, {"before": true, "after": true}], 85 | "space-before-blocks": [1, "always"], 86 | "@typescript-eslint/ban-ts-ignore": 0, 87 | "@typescript-eslint/explicit-function-return-type": 0, 88 | "@typescript-eslint/explicit-module-boundary-types": 0, 89 | "@typescript-eslint/no-shadow": ["error"], 90 | "no-unused-vars": 0, // covered by @typescript-eslint/no-unused-vars 91 | "strict": ["error", "never" ], 92 | "eqeqeq": 2, 93 | "prefer-const": 2, 94 | "indent": ["error", 4, { 95 | "FunctionDeclaration": {"parameters": "first"}, 96 | "FunctionExpression": {"parameters": "first"}, 97 | "SwitchCase": 1} 98 | ], 99 | "no-control-regex": "off", 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @matrix-org/bridges 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | changelog: 8 | # TODO: Change to 'main' 9 | if: ${{ github.base_ref == 'master' || contains(github.base_ref, 'release-') }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | ref: ${{ github.event.pull_request.head.sha }} 15 | fetch-depth: 0 16 | - uses: actions/setup-python@v3 17 | - run: pip install towncrier>=22 18 | - run: scripts/changelog-check.sh 19 | env: 20 | PULL_REQUEST_NUMBER: ${{ github.event.number }} 21 | -------------------------------------------------------------------------------- /.github/workflows/sign-off.yml: -------------------------------------------------------------------------------- 1 | name: Contribution requirements 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize] 6 | 7 | jobs: 8 | signoff: 9 | uses: matrix-org/backend-meta/.github/workflows/sign-off.yml@v1.4.1 -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version-file: .node-version 22 | - run: yarn 23 | - run: yarn lint 24 | test: 25 | runs-on: ubuntu-latest 26 | timeout-minutes: 10 27 | strategy: 28 | matrix: 29 | node_version: [20] 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: Use Node.js ${{ matrix.node_version }} 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: ${{ matrix.node_version }} 36 | - run: yarn 37 | - run: yarn test 38 | integration: 39 | runs-on: ubuntu-latest 40 | timeout-minutes: 10 41 | services: 42 | ircd: 43 | image: ghcr.io/ergochat/ergo:stable 44 | ports: 45 | - 6667:6667 46 | strategy: 47 | matrix: 48 | node_version: [20] 49 | steps: 50 | - uses: actions/checkout@v3 51 | - name: Use Node.js ${{ matrix.node_version }} 52 | uses: actions/setup-node@v3 53 | with: 54 | node-version: ${{ matrix.node_version }} 55 | - run: yarn 56 | - run: yarn test:integration 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | _build/ 4 | .idea/ 5 | lib/ -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Because yarn is [hot garbage](https://github.com/yarnpkg/yarn/issues/5235#issuecomment-571206092), we need an empty .npmignore file -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 3.0.0 (2024-03-27) 2 | ================== 3 | 4 | The new mimimum node version is now Node 20. 5 | 6 | Internal Changes 7 | ---------------- 8 | 9 | - Update dependencies. ([\#111](https://github.com/matrix-org/node-irc/issues/111)) 10 | 11 | 12 | 2.2.0 (2023-08-11) 13 | ================== 14 | 15 | Internal Changes 16 | ---------------- 17 | 18 | - Include any certfp lines in a whois response. ([\#110](https://github.com/matrix-org/node-irc/issues/110)) 19 | 20 | 21 | 2.1.0 (2023-07-27) 22 | ================== 23 | 24 | Bugfixes 25 | -------- 26 | 27 | - Fix values in isupport duplicating if multiple version responses are returned. ([\#108](https://github.com/matrix-org/node-irc/issues/108)) 28 | 29 | 30 | Internal Changes 31 | ---------------- 32 | 33 | - Migrate to using Jest for all tests. ([\#106](https://github.com/matrix-org/node-irc/issues/106)) 34 | 35 | 36 | 2.0.1 (2023-05-18) 37 | ================== 38 | 39 | Bugfixes 40 | -------- 41 | 42 | - Users that quit IRC network now leave Matrix channel properly. 43 | Users that are killed from IRC network now leave Matrix channel properly. 44 | It also fixes nick changes: old nick leaves Matrix room and new nick joins Matrix room. ([\#103](https://github.com/matrix-org/node-irc/issues/103)) 45 | 46 | 47 | Internal Changes 48 | ---------------- 49 | 50 | - Increase the integration test timeout to 15s. ([\#104](https://github.com/matrix-org/node-irc/issues/104)) 51 | 52 | 53 | 2.0.0 (2023-04-26) 54 | ================== 55 | 56 | NOTE: This release removes support for Node 16. Please update to Node 18 or greater. 57 | 58 | Features 59 | -------- 60 | 61 | - Add support for splitting out the IRC connection state, and connecting via an existing socket. ([\#99](https://github.com/matrix-org/node-irc/issues/99)) 62 | - Export utilities for testing against ircds. ([\#102](https://github.com/matrix-org/node-irc/issues/102)) 63 | 64 | Deprecations and Removals 65 | ------------------------- 66 | 67 | - Use `yarn` instead of `npm`, to be in-line with other matrix.org projects. ([\#95](https://github.com/matrix-org/node-irc/issues/95)) 68 | - Add support for Node 20, and drop support for Node 16. ([\#100](https://github.com/matrix-org/node-irc/issues/100)) 69 | 70 | 71 | Internal Changes 72 | ---------------- 73 | 74 | - Add support for testing against an actual IRCD. ([\#94](https://github.com/matrix-org/node-irc/issues/94)) 75 | - Use ergo as our ircd of choice for automated testing. ([\#101](https://github.com/matrix-org/node-irc/issues/101)) 76 | 77 | 78 | 1.5.0 (2022-10-03) 79 | ================== 80 | 81 | Internal Changes 82 | ---------------- 83 | 84 | - Add support for testing against an actual IRCD. ([\#94](https://github.com/matrix-org/node-irc/issues/94)) 85 | 86 | 87 | 1.4.0 (2022-09-22) 88 | ================== 89 | 90 | **Please note:** Minimum Node.JS version is now 16 91 | 92 | Features 93 | -------- 94 | 95 | - The `Client` class now uses strong typing for it's emitter. ([\#91](https://github.com/matrix-org/node-irc/issues/91)) 96 | 97 | 98 | Bugfixes 99 | -------- 100 | 101 | - Prevent connection immediately terminating on expired certificate when allowed by config. Contributed by @f0x52. ([\#90](https://github.com/matrix-org/node-irc/issues/90)) 102 | 103 | 104 | Deprecations and Removals 105 | ------------------------- 106 | 107 | - Drop support for Node 12,14 and support Node 16+. ([\#92](https://github.com/matrix-org/node-irc/issues/92)) 108 | 109 | 110 | 1.2.1 (2022-05-04) 111 | =================== 112 | 113 | Bugfixes 114 | -------- 115 | 116 | - Split lines on CR as well as CR/CRLF. 117 | 118 | 1.2.0 (2021-08-18) 119 | =================== 120 | 121 | Bugfixes 122 | -------- 123 | 124 | - Fix an issue where setting `opts.encodingFallback` would cause the process to crash. ([\#84](https://github.com/matrix-org/node-irc/issues/84)) 125 | 126 | 127 | 1.1.1 (2021-07-27) 128 | =================== 129 | 130 | Bugfixes 131 | -------- 132 | 133 | - Fix when 'registered' is emitted ([\#75](https://github.com/matrix-org/node-irc/issues/75)) 134 | 135 | 136 | Internal Changes 137 | ---------------- 138 | 139 | - Add towncrier-based changelog setup ([\#76](https://github.com/matrix-org/node-irc/issues/76)) 140 | 141 | 1.1.0 (2021-07-26) 142 | =================== 143 | 144 | Features 145 | -------- 146 | 147 | - Add a getter for maxLineLength 148 | 149 | Bugfixes 150 | -------- 151 | 152 | - Fix multiline CAP responses not being processed correctly 153 | 154 | Pre 1.1.0 155 | ========== 156 | 157 | # 0.3.8 to 0.3.9 (2015-01-16) 158 | ## Added 159 | * Included notes in the README about icu / iconv 160 | * First draft of contributor doc! 161 | * Log network connection errors 162 | * This changelog 163 | 164 | ## Changed 165 | * Factored out parseMessage for better decoupling 166 | * Turn off autorejoin on kicks 167 | * Factored out test data to fixtures 168 | * Moved to irc-colors for stripping colors 169 | 170 | ## Fixed 171 | * Fixed line split delimiter regex to be more correct and robust 172 | * Fixed issue where self.hostMask may not be set when the client's nick is in use 173 | * Fixed hostmask length calculation--n.b., some ircds don't give the full hostmask 174 | * Style cleanups 175 | * Fixed SSL 176 | 177 | # 0.3.7 to 0.3.8 (2015-01-09) 178 | ## Added 179 | * Added support for binding to a specific local address 180 | * WEBIRC support 181 | 182 | ## Changed 183 | * Various small changes and fixes 184 | 185 | ## Fixed 186 | * Proper line wrapping 187 | * Fixed bold and underline codes 188 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hi there! Please read the [CONTRIBUTING.md](https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/CONTRIBUTING.md) guide for all matrix.org bridge 2 | projects. 3 | 4 | This project was forked from [node-irc](https://github.com/martynsmith/node-irc), and is used to power our [IRC bridge](https://github.com/matrix-org/matrix-appservice-irc). 5 | 6 | ## node-irc Guidelines 7 | 8 | - Unless your bug is very specific to the IRC client implementation, you should submit bugs to [matrix-appservice-irc](https://github.com/matrix-org/matrix-appservice-irc). 9 | If you are uncertain, do not file them in this repository. 10 | - The official IRC bridge support/development room is [#irc:matrix.org](https://matrix.to/#/#irc:matrix.org) 11 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](http://opensource.org/licenses/GPL-3.0) 2 | 3 | This is a fork of [node-irc](http://node-irc.readthedocs.org/), which is an IRC client library written in TypeScript for [Node](http://nodejs.org/). This fork is used by the [Matrix-IRC application service](http://github.com/matrix-org/matrix-appservice-irc). 4 | 5 | To use this package: 6 | ``` 7 | yarn add matrix-org-irc 8 | ``` 9 | 10 | # Differences from `node-irc` 11 | The `node-irc` library isn't well maintained and there are a number of issues which are impacting development of the [Matrix-IRC application service](http://github.com/matrix-org/matrix-appservice-irc). We made the decision to fork the project in order to improve reliability of the application service. A summary of modifications from `node-irc@0.3.12` are below: 12 | - TypeScript support 13 | - https://github.com/matrix-org/node-irc/pull/1 - Manifested as [BOTS-80](https://matrix.org/jira/browse/BOTS-80) 14 | - https://github.com/matrix-org/node-irc/pull/4 - Manifested as [BOTS-73] (https://matrix.org/jira/browse/BOTS-73) 15 | - Handle +R - https://github.com/matrix-org/node-irc/commit/7c16b994b12145b6da8961790bcfa808fb7fcba9 16 | - Handle more error codes (430,435,438) 17 | - Fix bug which would fail to connect conflicting nicks which `== NICKLEN`. 18 | - Fix `err_unavailresource` on connection with reserved nicks. 19 | - Workaround for the Scunthorpe problem: https://github.com/matrix-org/matrix-appservice-irc/issues/103 20 | - Add methods for working out if a given text will be split and into how many lines. 21 | - Add `names` support (incl. multi-prefix). 22 | - Add functions to determine if a user prefix is more powerful than another (e.g. `@ > &`) 23 | - Case-map all incoming channels correctly (e.g on PRIVMSG and NOTICE) 24 | - Allow IP family to be chosen to allow IPv6 connections. 25 | - Add function for getting channel modes. 26 | - Workaround terrible RFC3484 rules which means that IPv6 DNS rotations would not be honoured. 27 | - Add `setUserMode` to set a user's mode. 28 | - Addition of `encodingFallback` option which allows setting encoding to use for non-UTF-8 encoded messages. 29 | - Addition of `onNickConflict()` option which is called on `err_nicknameinuse`. This function should return the next nick to try. The function defaults to suffixing monotonically increasing integers. Usage: 30 | ```javascript 31 | new Client("server.com", "MyNick", { 32 | onNickConflict: function() { 33 | return "_MyNick_"; 34 | } 35 | }); 36 | ``` 37 | -------------------------------------------------------------------------------- /changelog.d/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/node-irc/34ab4f3dc863512b4322113841da0e55b75a90f0/changelog.d/.gitkeep -------------------------------------------------------------------------------- /changelog.d/114.feature: -------------------------------------------------------------------------------- 1 | Handle error 415 (+R/+M). 2 | -------------------------------------------------------------------------------- /changelog.d/115.bugfix: -------------------------------------------------------------------------------- 1 | Fix reconnect loop due to the close listener being called on socket destruction. 2 | -------------------------------------------------------------------------------- /changelog.d/116.bugfix: -------------------------------------------------------------------------------- 1 | Fix auto reconnect due to event listeners not being called. 2 | -------------------------------------------------------------------------------- /changelog.d/117.bugfix: -------------------------------------------------------------------------------- 1 | Fix nick changes not being bridged to matrix. 2 | -------------------------------------------------------------------------------- /changelog.d/118.bugfix: -------------------------------------------------------------------------------- 1 | Ensure channel.users is an accurate representation of the NAMES response after updates, removing stale users. -------------------------------------------------------------------------------- /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 everything, it represents a single nick connected to 12 | a single IRC server. 13 | 14 | The first two arguments are the server to connect to, and the nickname to 15 | attempt to use. The third optional argument is an options object with default 16 | values:: 17 | 18 | { 19 | userName: 'nodebot', 20 | realName: 'nodeJS IRC client', 21 | port: 6667, 22 | localAddress: null, 23 | localPort: null, 24 | debug: false, 25 | showErrors: false, 26 | autoRejoin: false, 27 | autoConnect: true, 28 | channels: [], 29 | secure: false, 30 | selfSigned: false, 31 | certExpired: false, 32 | floodProtection: false, 33 | floodProtectionDelay: 1000, 34 | sasl: false, 35 | stripColors: false, 36 | channelPrefixes: "&#", 37 | messageSplit: 512, 38 | encoding: '', 39 | encodingFallback: null, 40 | } 41 | 42 | `secure` (SSL connection) can be a true value or an object (the kind of object 43 | returned from `crypto.createCredentials()`) specifying cert etc for validation. 44 | If you set `selfSigned` to true SSL accepts certificates from a non trusted CA. 45 | If you set `certExpired` to true, the bot connects even if the ssl cert has expired. 46 | 47 | `localAddress` is the local address to bind to when connecting, such as `127.0.0.1` 48 | 49 | `localPort` is the local port to bind when connecting, such as `50555` 50 | 51 | `floodProtection` queues all your messages and slowly unpacks it to make sure 52 | that we won't get kicked out because for Excess Flood. You can also use 53 | `Client.activateFloodProtection()` to activate flood protection after 54 | instantiating the client. 55 | 56 | `floodProtectionDelay` sets the amount of time that the client will wait 57 | between sending subsequent messages when `floodProtection` is enabled. 58 | 59 | Set `sasl` to true to enable SASL support. You'll also want to set `nick`, 60 | `userName`, and `password` for authentication. 61 | 62 | `stripColors` removes mirc colors (0x03 followed by one or two ascii 63 | numbers for foreground,background) and ircII "effect" codes (0x02 64 | bold, 0x1f underline, 0x16 reverse, 0x0f reset) from the entire 65 | message before parsing it and passing it along. 66 | 67 | `messageSplit` will split up large messages sent with the `say` method 68 | into multiple messages of length fewer than `messageSplit` characters. 69 | 70 | With `encoding` you can set IRC bot to convert all messages to specified character set. If you don't want to use 71 | this just leave value blank or false. Example values are UTF-8, ISO-8859-15, etc. 72 | 73 | With `encodingFallback` you can choose what encoding to use for non-UTF-8 messages. 74 | Leave null for no fallback. Do not set `encoding` when using this as it assumes incoming 75 | messages are UTF-8 OR fallback encoding. 76 | 77 | Setting `debug` to true will emit timestamped messages to the console 78 | using `util.log` when certain events are fired. 79 | 80 | `autoRejoin` has the client rejoin channels after being kicked. 81 | 82 | Setting `autoConnect` to false prevents the Client from connecting on 83 | instantiation. You will need to call `connect()` on the client instance:: 84 | 85 | var client = new irc.Client({ autoConnect: false, ... }); 86 | client.connect(); 87 | 88 | 89 | .. js:function:: Client.send(command, arg1, arg2, ...) 90 | 91 | Sends a raw message to the server; generally speaking, it's best not to use 92 | this method unless you know what you're doing. Instead, use one of the 93 | methods below. 94 | 95 | .. js:function:: Client.join(channel, callback) 96 | 97 | Joins the specified channel. 98 | 99 | :param string channel: Channel to join 100 | :param function callback: Callback to automatically subscribed to the 101 | `join#channel` event, but removed after the first invocation. `channel` 102 | supports multiple JOIN arguments as a space separated string (similar to 103 | the IRC protocol). 104 | 105 | .. js:function:: Client.part(channel, [message], callback) 106 | 107 | Parts the specified channel. 108 | 109 | :param string channel: Channel to part 110 | :param string message: Optional message to send upon leaving the channel 111 | :param function callback: Callback to automatically subscribed to the 112 | `part#channel` event, but removed after the first invocation. 113 | 114 | .. js:function:: Client.say(target, message) 115 | 116 | Sends a message to the specified target. 117 | 118 | :param string target: is either a nickname, or a channel. 119 | :param string message: the message to send to the target. 120 | 121 | .. js:function:: Client.ctcp(target, type, text) 122 | 123 | Sends a CTCP message to the specified target. 124 | 125 | :param string target: is either a nickname, or a channel. 126 | :param string type: the type of the CTCP message. Specify "privmsg" for a 127 | PRIVMSG, and anything else for a NOTICE. 128 | :param string text: the CTCP message to send. 129 | 130 | .. js:function:: Client.action(target, message) 131 | 132 | Sends an action to the specified target. 133 | 134 | .. js:function:: Client.notice(target, message) 135 | 136 | Sends a notice to the specified target. 137 | 138 | :param string target: is either a nickname, or a channel. 139 | :param string message: the message to send as a notice to the target. 140 | 141 | .. js:function:: Client.whois(nick, callback) 142 | 143 | Request a whois for the specified `nick`. 144 | 145 | :param string nick: is a nickname 146 | :param function callback: Callback to fire when the server has finished 147 | generating the whois information and is passed exactly the same 148 | information as a `whois` event described above. 149 | 150 | .. js:function:: Client.list([arg1, arg2, ...]) 151 | 152 | Request a channel listing from the server. The arguments for this method are 153 | fairly server specific, this method just passes them through exactly as 154 | specified. 155 | 156 | Responses from the server are available via the `channellist_start`, 157 | `channellist_item`, and `channellist` events. 158 | 159 | .. js:function:: Client.connect([retryCount [, callback]]) 160 | 161 | Connects to the server. Used when `autoConnect` in the options is set to 162 | false. If `retryCount` is a function it will be treated as the `callback` 163 | (i.e. both arguments to this function are optional). 164 | 165 | :param integer retryCount: Optional number of times to attempt reconnection 166 | :param function callback: Optional callback 167 | 168 | .. js:function:: Client.disconnect([message [, callback]]) 169 | 170 | Disconnects from the IRC server. If `message` is a function it will be 171 | treated as the `callback` (i.e. both arguments to this function are 172 | optional). 173 | 174 | :param string message: Optional message to send when disconnecting. 175 | :param function callback: Optional callback 176 | 177 | .. js:function:: Client.activateFloodProtection([interval]) 178 | 179 | Activates flood protection "after the fact". You can also use 180 | `floodProtection` while instantiating the Client to enable flood 181 | protection, and `floodProtectionDelay` to set the default message 182 | interval. 183 | 184 | :param integer interval: Optional configuration for amount of time 185 | to wait between messages. Takes value from client configuration 186 | if unspecified. 187 | 188 | Events 189 | ------ 190 | 191 | `irc.Client` instances are EventEmitters with the following events: 192 | 193 | 194 | .. js:data:: 'registered' 195 | 196 | `function (message) { }` 197 | 198 | Emitted when the server sends the initial 001 line, indicating you've connected 199 | to the server. See the `raw` event for details on the `message` object. 200 | 201 | .. js:data:: 'motd' 202 | 203 | `function (motd) { }` 204 | 205 | Emitted when the server sends the message of the day to clients. 206 | 207 | .. js:data:: 'names' 208 | 209 | `function (channel, nicks) { }` 210 | 211 | Emitted when the server sends a list of nicks for a channel (which happens 212 | immediately after joining and on request. The nicks object passed to the 213 | callback is keyed by nick names, and has values '', '+', or '@' depending on the 214 | level of that nick in the channel. 215 | 216 | .. js:data:: 'names#channel' 217 | 218 | `function (nicks) { }` 219 | 220 | As per 'names' event but only emits for the subscribed channel. 221 | 222 | .. js:data:: 'topic' 223 | 224 | `function (channel, topic, nick, message) { }` 225 | 226 | Emitted when the server sends the channel topic on joining a channel, or when a 227 | user changes the topic on a channel. See the `raw` event for details on the 228 | `message` object. 229 | 230 | .. js:data:: 'join' 231 | 232 | `function (channel, nick, message) { }` 233 | 234 | Emitted when a user joins a channel (including when the client itself joins a 235 | channel). See the `raw` event for details on the `message` object. 236 | 237 | .. js:data:: 'join#channel' 238 | 239 | `function (nick, message) { }` 240 | 241 | As per 'join' event but only emits for the subscribed channel. 242 | See the `raw` event for details on the `message` object. 243 | 244 | .. js:data:: 'part' 245 | 246 | `function (channel, nick, reason, message) { }` 247 | 248 | Emitted when a user parts a channel (including when the client itself parts a 249 | channel). See the `raw` event for details on the `message` object. 250 | 251 | .. js:data:: 'part#channel' 252 | 253 | `function (nick, reason, message) { }` 254 | 255 | As per 'part' event but only emits for the subscribed channel. 256 | See the `raw` event for details on the `message` object. 257 | 258 | .. js:data:: 'quit' 259 | 260 | `function (nick, reason, channels, message) { }` 261 | 262 | Emitted when a user disconnects from the IRC, leaving the specified array of 263 | channels. See the `raw` event for details on the `message` object. 264 | 265 | .. js:data:: 'kick' 266 | 267 | `function (channel, nick, by, reason, message) { }` 268 | 269 | Emitted when a user is kicked from a channel. See the `raw` event for details 270 | on the `message` object. 271 | 272 | .. js:data:: 'kick#channel' 273 | 274 | `function (nick, by, reason, message) { }` 275 | 276 | As per 'kick' event but only emits for the subscribed channel. 277 | See the `raw` event for details on the `message` object. 278 | 279 | .. js:data:: 'kill' 280 | 281 | `function (nick, reason, channels, message) { }` 282 | 283 | Emitted when a user is killed from the IRC server. 284 | `channels` is an array of channels the killed user was in which 285 | are known to the client. 286 | See the `raw` event for details on the `message` object. 287 | 288 | .. js:data:: 'message' 289 | 290 | `function (nick, to, text, message) { }` 291 | 292 | Emitted when a message is sent. `to` can be either a nick (which is most likely 293 | this clients nick and means a private message), or a channel (which means a 294 | message to that channel). See the `raw` event for details on the `message` object. 295 | 296 | .. js:data:: 'message#' 297 | 298 | `function (nick, to, text, message) { }` 299 | 300 | Emitted when a message is sent to any channel (i.e. exactly the same as the 301 | `message` event but excluding private messages. 302 | See the `raw` event for details on the `message` object. 303 | 304 | .. js:data:: 'message#channel' 305 | 306 | `function (nick, text, message) { }` 307 | 308 | As per 'message' event but only emits for the subscribed channel. 309 | See the `raw` event for details on the `message` object. 310 | 311 | .. js:data:: 'selfMessage' 312 | 313 | `function (to, text) { }` 314 | 315 | Emitted when a message is sent from the client. `to` is who the message was 316 | sent to. It can be either a nick (which most likely means a private message), 317 | or a channel (which means a message to that channel). 318 | 319 | .. js:data:: 'notice' 320 | 321 | `function (nick, to, text, message) { }` 322 | 323 | Emitted when a notice is sent. `to` can be either a nick (which is most likely 324 | this clients nick and means a private message), or a channel (which means a 325 | message to that channel). `nick` is either the senders nick or `null` which 326 | means that the notice comes from the server. See the `raw` event for details 327 | on the `message` object. 328 | 329 | .. js:data:: 'ping' 330 | 331 | `function (server) { }` 332 | 333 | Emitted when a server PINGs the client. The client will automatically send a 334 | PONG request just before this is emitted. 335 | 336 | .. js:data:: 'pm' 337 | 338 | `function (nick, text, message) { }` 339 | 340 | As per 'message' event but only emits when the message is direct to the client. 341 | See the `raw` event for details on the `message` object. 342 | 343 | .. js:data:: 'ctcp' 344 | 345 | `function (from, to, text, type, message) { }` 346 | 347 | Emitted when a CTCP notice or privmsg was received (`type` is either `'notice'` 348 | or `'privmsg'`). See the `raw` event for details on the `message` object. 349 | 350 | .. js:data:: 'ctcp-notice' 351 | 352 | `function (from, to, text, message) { }` 353 | 354 | Emitted when a CTCP notice was received. 355 | See the `raw` event for details on the `message` object. 356 | 357 | .. js:data:: 'ctcp-privmsg' 358 | 359 | `function (from, to, text, message) { }` 360 | 361 | Emitted when a CTCP privmsg was received. 362 | See the `raw` event for details on the `message` object. 363 | 364 | .. js:data:: 'ctcp-version' 365 | 366 | `function (from, to, message) { }` 367 | 368 | Emitted when a CTCP VERSION request was received. 369 | See the `raw` event for details on the `message` object. 370 | 371 | .. js:data:: 'nick' 372 | 373 | `function (oldnick, newnick, channels, message) { }` 374 | 375 | Emitted when a user changes nick along with the channels the user is in. 376 | See the `raw` event for details on the `message` object. 377 | 378 | .. js:data:: 'invite' 379 | 380 | `function (channel, from, message) { }` 381 | 382 | Emitted when the client receives an `/invite`. See the `raw` event for details 383 | on the `message` object. 384 | 385 | .. js:data:: '+mode' 386 | 387 | `function (channel, by, mode, argument, message) { }` 388 | 389 | Emitted when a mode is added to a user or channel. `channel` is the channel 390 | which the mode is being set on/in. `by` is the user setting the mode. `mode` 391 | is the single character mode identifier. If the mode is being set on a user, 392 | `argument` is the nick of the user. If the mode is being set on a channel, 393 | `argument` is the argument to the mode. If a channel mode doesn't have any 394 | arguments, `argument` will be 'undefined'. See the `raw` event for details 395 | on the `message` object. 396 | 397 | .. js:data:: '-mode' 398 | 399 | `function (channel, by, mode, argument, message) { }` 400 | 401 | Emitted when a mode is removed from a user or channel. `channel` is the channel 402 | which the mode is being set on/in. `by` is the user setting the mode. `mode` 403 | is the single character mode identifier. If the mode is being set on a user, 404 | `argument` is the nick of the user. If the mode is being set on a channel, 405 | `argument` is the argument to the mode. If a channel mode doesn't have any 406 | arguments, `argument` will be 'undefined'. See the `raw` event for details 407 | on the `message` object. 408 | 409 | .. js:data:: 'whois' 410 | 411 | `function (info) { }` 412 | 413 | Emitted whenever the server finishes outputting a WHOIS response. The 414 | information should look something like:: 415 | 416 | { 417 | nick: "Ned", 418 | user: "martyn", 419 | host: "10.0.0.18", 420 | realname: "Unknown", 421 | channels: ["@#purpledishwashers", "#blah", "#mmmmbacon"], 422 | server: "*.dollyfish.net.nz", 423 | serverinfo: "The Dollyfish Underworld", 424 | operator: "is an IRC Operator" 425 | } 426 | 427 | .. js:data:: 'channellist_start' 428 | 429 | `function () {}` 430 | 431 | Emitted whenever the server starts a new channel listing 432 | 433 | .. js:data:: 'channellist_item' 434 | 435 | `function (channel_info) {}` 436 | 437 | Emitted for each channel the server returns. The channel_info object 438 | contains keys 'name', 'users' (number of users on the channel), and 'topic'. 439 | 440 | .. js:data:: 'channellist' 441 | 442 | `function (channel_list) {}` 443 | 444 | Emitted when the server has finished returning a channel list. The 445 | channel_list array is simply a list of the objects that were returned in the 446 | intervening `channellist_item` events. 447 | 448 | This data is also available via the Client.channellist property after this 449 | event has fired. 450 | 451 | .. js:data:: 'raw' 452 | 453 | `function (message) { }` 454 | 455 | Emitted when ever the client receives a "message" from the server. A message is 456 | basically a single line of data from the server, but the parameter to the 457 | callback has already been parsed and contains:: 458 | 459 | message = { 460 | prefix: "The prefix for the message (optional)", 461 | nick: "The nickname portion of the prefix (optional)", 462 | user: "The username portion of the prefix (optional)", 463 | host: "The hostname portion of the prefix (optional)", 464 | server: "The servername (if the prefix was a servername)", 465 | rawCommand: "The command exactly as sent from the server", 466 | command: "Human readable version of the command", 467 | commandType: "normal, error, or reply", 468 | args: ['arguments', 'to', 'the', 'command'], 469 | } 470 | 471 | You can read more about the IRC protocol by reading `RFC 1459 472 | `_ 473 | 474 | .. js:data:: 'error' 475 | 476 | `function (message) { }` 477 | 478 | Emitted when ever the server responds with an error-type message. The message 479 | parameter is exactly as in the 'raw' event. 480 | 481 | .. js:data:: 'action' 482 | 483 | `function (from, to, text, message) { }` 484 | 485 | Emitted whenever a user performs an action (e.g. `/me waves`). 486 | The message parameter is exactly as in the 'raw' event. 487 | 488 | Colors 489 | ------ 490 | 491 | .. js:function:: irc.colors.wrap(color, text [, reset_color]) 492 | 493 | Takes a color by name, text, and optionally what color to return. 494 | 495 | :param string color: the name of the color as a string 496 | :param string text: the text you want colorized 497 | :param string reset_color: the name of the color you want set after the text (defaults to 'reset') 498 | 499 | .. js:data:: irc.colors.codes 500 | 501 | This contains the set of colors available and a function to wrap text in a 502 | color. 503 | 504 | The following color choices are available: 505 | 506 | { 507 | white: '\u000300', 508 | black: '\u000301', 509 | dark_blue: '\u000302', 510 | dark_green: '\u000303', 511 | light_red: '\u000304', 512 | dark_red: '\u000305', 513 | magenta: '\u000306', 514 | orange: '\u000307', 515 | yellow: '\u000308', 516 | light_green: '\u000309', 517 | cyan: '\u000310', 518 | light_cyan: '\u000311', 519 | light_blue: '\u000312', 520 | light_magenta: '\u000313', 521 | gray: '\u000314', 522 | light_gray: '\u000315', 523 | reset: '\u000f', 524 | } 525 | 526 | Internal 527 | ------ 528 | 529 | .. js:data:: Client.conn 530 | 531 | Socket to the server. Rarely, if ever needed. Use `Client.send` instead. 532 | 533 | .. js:data:: Client.chans 534 | 535 | Channels joined. Includes channel modes, user list, and topic information. Only updated *after* the server recognizes the join. 536 | 537 | .. js:data:: Client.nick 538 | 539 | The current nick of the client. Updated if the nick changes (e.g. nick collision when connecting to a server). 540 | 541 | .. js:function:: client._whoisData 542 | 543 | Buffer of whois data as whois is sent over multiple lines. 544 | 545 | .. js:function:: client._addWhoisData 546 | 547 | Self-explanatory. 548 | 549 | .. js:function:: client._clearWhoisData 550 | 551 | Self-explanatory. 552 | -------------------------------------------------------------------------------- /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'2011, Martyn Smith' 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 = '2.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '2.1' 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'Martyn Smith', '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'Martyn Smith'], 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: ['#blah', '#test'] 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/externalSasl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var irc = require('../'); 4 | var fs = require('fs'); 5 | 6 | // example of authenticating through external sasl on freenode 7 | // set up your cert as instructed here https://freenode.net/kb/answer/certfp 8 | // change these to your cert 9 | var options = { 10 | key: fs.readFileSync('/path/to/.weechat/certs/freenode.pem'), 11 | cert: fs.readFileSync('/path/to/.weechat/certs/freenode.pem') 12 | }; 13 | 14 | var bot = new irc.Client('chat.freenode.net', 'nodebot', { 15 | port: 6697, 16 | sasl: true, 17 | saslType: 'EXTERNAL', 18 | debug: true, 19 | secure: options, 20 | channels: ['#botwar'] 21 | }); 22 | 23 | bot.addListener('error', function(message) { 24 | console.error('ERROR: %s: %s', message.command, message.args.join(' ')); 25 | }); 26 | 27 | bot.addListener('message#blah', function(from, message) { 28 | console.log('<%s> %s', from, message); 29 | }); 30 | 31 | bot.addListener('message', function(from, to, message) { 32 | console.log('%s => %s: %s', from, to, message); 33 | 34 | if (to.match(/^[#&]/)) { 35 | // channel message 36 | if (message.match(/hello/i)) { 37 | bot.say(to, 'Hello there ' + from); 38 | } 39 | if (message.match(/dance/)) { 40 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D\\-<\u0001'); }, 1000); 41 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D|-<\u0001'); }, 2000); 42 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D/-<\u0001'); }, 3000); 43 | setTimeout(function() { bot.say(to, '\u0001ACTION dances: :D|-<\u0001'); }, 4000); 44 | } 45 | } 46 | else { 47 | // private message 48 | console.log('private message'); 49 | } 50 | }); 51 | bot.addListener('pm', function(nick, message) { 52 | console.log('Got private message from %s: %s', nick, message); 53 | }); 54 | bot.addListener('join', function(channel, who) { 55 | console.log('%s has joined %s', who, channel); 56 | }); 57 | bot.addListener('part', function(channel, who, reason) { 58 | console.log('%s has left %s: %s', who, channel, reason); 59 | }); 60 | bot.addListener('kick', function(channel, who, by, reason) { 61 | console.log('%s was kicked from %s by %s: %s', who, channel, by, reason); 62 | }); 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testTimeout: 15000, // seconds 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matrix-org-irc", 3 | "description": "An IRC client library for node, written in Typescript.", 4 | "version": "3.0.0", 5 | "author": "Matrix.org (original fork from Martyn Smith )", 6 | "scripts": { 7 | "prepare": "yarn run build", 8 | "build": "tsc --project tsconfig.json", 9 | "lint": "eslint -c .eslintrc --max-warnings 0 'src/**/*.ts'", 10 | "test": "yarn test:unit # alias for the moment", 11 | "test:unit": "jest test/*", 12 | "test:integration": "jest spec/*" 13 | }, 14 | "contributors": [ 15 | "node-irc contributors" 16 | ], 17 | "types": "lib/index.d.ts", 18 | "repository": { 19 | "type": "git", 20 | "url": "http://github.com/matrix-org/node-irc" 21 | }, 22 | "bugs": { 23 | "url": "http://github.com/matrix-org/node-irc/issues" 24 | }, 25 | "main": "lib/index", 26 | "engines": { 27 | "node": ">=20.0.0" 28 | }, 29 | "license": "GPL-3.0", 30 | "dependencies": { 31 | "chardet": "^2.0.0", 32 | "iconv-lite": "^0.6.3", 33 | "typed-emitter": "^2.1.0", 34 | "utf-8-validate": "^6.0.3" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "^20.11.28", 38 | "@types/utf-8-validate": "^5.0.0", 39 | "@typescript-eslint/eslint-plugin": "^7.2.0", 40 | "@typescript-eslint/parser": "^7.2.0", 41 | "eslint": "^8.39.0", 42 | "jest": "^29.5.0", 43 | "ts-jest": "^29.1.0", 44 | "typescript": "^5.0.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | # The name of your Python package 3 | filename = "CHANGELOG.md" 4 | directory = "changelog.d" 5 | issue_format = "[\\#{issue}](https://github.com/matrix-org/node-irc/issues/{issue})" 6 | ignore = ['.gitkeep'] 7 | 8 | [[tool.towncrier.type]] 9 | directory = "feature" 10 | name = "Features" 11 | showcontent = true 12 | 13 | [[tool.towncrier.type]] 14 | directory = "bugfix" 15 | name = "Bugfixes" 16 | showcontent = true 17 | 18 | [[tool.towncrier.type]] 19 | directory = "doc" 20 | name = "Improved Documentation" 21 | showcontent = true 22 | 23 | [[tool.towncrier.type]] 24 | directory = "removal" 25 | name = "Deprecations and Removals" 26 | showcontent = true 27 | 28 | [[tool.towncrier.type]] 29 | directory = "misc" 30 | name = "Internal Changes" 31 | showcontent = true 32 | -------------------------------------------------------------------------------- /scripts/changelog-check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # A script which checks that an appropriate news file has been added on this 4 | # branch. 5 | 6 | 7 | echo -e "+++ \033[32mChecking newsfragment\033[m" 8 | 9 | set -e 10 | 11 | # make sure that origin/master is up to date 12 | git remote set-branches --add origin master 13 | git fetch -q origin master 14 | 15 | pr="$PULL_REQUEST_NUMBER" 16 | 17 | # Print a link to the contributing guide if the user makes a mistake 18 | CONTRIBUTING_GUIDE_TEXT="!! Please see the contributing guide for help writing your changelog entry: 19 | https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/CONTRIBUTING.md#%EF%B8%8F-pull-requests" 20 | 21 | # If check-newsfragment returns a non-zero exit code, print the contributing guide and exit 22 | python3 -m towncrier.check --compare-with=origin/master || (echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2 && exit 1) 23 | 24 | echo 25 | echo "--------------------------" 26 | echo 27 | 28 | matched=0 29 | for f in $(git diff --diff-filter=d --name-only FETCH_HEAD... -- changelog.d); do 30 | # check that any added newsfiles on this branch end with a full stop. 31 | lastchar=$(tr -d '\n' < "$f" | tail -c 1) 32 | if [ "$lastchar" != '.' ] && [ "$lastchar" != '!' ]; then 33 | echo -e "\e[31mERROR: newsfragment $f does not end with a '.' or '!'\e[39m" >&2 34 | echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2 35 | exit 1 36 | fi 37 | 38 | # see if this newsfile corresponds to the right PR 39 | [[ -n "$pr" && "$f" == changelog.d/"$pr".* ]] && matched=1 40 | done 41 | 42 | if [[ -n "$pr" && "$matched" -eq 0 ]]; then 43 | echo -e "\e[31mERROR: Did not find a news fragment with the right number: expected changelog.d/$pr.*.\e[39m" >&2 44 | echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2 45 | exit 1 46 | fi 47 | -------------------------------------------------------------------------------- /scripts/changelog-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=`python3 -c "import json; f = open('./package.json', 'r'); v = json.loads(f.read())['version']; f.close(); print(v)"` 3 | towncrier build --version $VERSION $1 4 | -------------------------------------------------------------------------------- /spec/client.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, afterEach, expect, test } from '@jest/globals'; 2 | import { TestIrcServer } from '../src/testing'; 3 | import { IrcSupported } from '../src'; 4 | 5 | describe('Client', () => { 6 | let server: TestIrcServer; 7 | beforeEach(() => { 8 | server = new TestIrcServer(); 9 | return server.setUp(); 10 | }); 11 | afterEach(() => { 12 | return server.tearDown(); 13 | }) 14 | describe('joining channels', () => { 15 | test('will get a join event from a newly joined user', async () => { 16 | const { speaker, listener } = server.clients; 17 | const expectedChannel = TestIrcServer.generateUniqueChannel('foobar'); 18 | 19 | // Join the room and listen 20 | const listenerJoinPromise = listener.waitForEvent('join'); 21 | await listener.join(expectedChannel); 22 | const [lChannel, lNick] = await listenerJoinPromise; 23 | expect(lNick).toBe(listener.nick); 24 | expect(lChannel).toBe(expectedChannel); 25 | 26 | const speakerJoinPromise = listener.waitForEvent('join'); 27 | await speaker.join(expectedChannel); 28 | const [channel, nick] = await speakerJoinPromise; 29 | expect(nick).toBe(speaker.nick); 30 | expect(channel).toBe(expectedChannel); 31 | }); 32 | test('can join a channel and send a message', async () => { 33 | const { speaker, listener } = server.clients; 34 | const expectedChannel = TestIrcServer.generateUniqueChannel('foobar'); 35 | await listener.join(expectedChannel); 36 | const messagePromise = listener.waitForEvent('message'); 37 | await speaker.join(expectedChannel); 38 | await speaker.say(expectedChannel, 'Hello world!'); 39 | 40 | const [nick, channel, text] = await messagePromise; 41 | expect(nick).toBe(speaker.nick); 42 | expect(channel).toBe(expectedChannel); 43 | expect(text).toBe('Hello world!'); 44 | }); 45 | test('will store channel information', async () => { 46 | const { speaker } = server.clients; 47 | const expectedChannel = TestIrcServer.generateUniqueChannel('foobar'); 48 | expect(speaker.chanData(expectedChannel)).toBeUndefined(); 49 | speaker.join(expectedChannel); 50 | await speaker.waitForEvent('join'); 51 | 52 | const channel = speaker.chanData(expectedChannel); 53 | expect(channel).toBeDefined(); 54 | expect(channel?.key).toEqual(expectedChannel); 55 | expect(channel?.serverName).toEqual(expectedChannel); 56 | expect(channel?.users.get(speaker.nick)).toBeDefined(); 57 | }); 58 | }); 59 | describe('mode changes', () => { 60 | test('will handle adding a parameter-less mode', async () => { 61 | const { speaker } = server.clients; 62 | const expectedChannel = TestIrcServer.generateUniqueChannel('foobar'); 63 | await speaker.join(expectedChannel); 64 | await speaker.waitForEvent('join'); 65 | const modeEvent = speaker.waitForEvent('+mode'); 66 | await speaker.send('MODE', expectedChannel, '+m'); 67 | 68 | const [channel, nick, mode, user] = await modeEvent; 69 | expect(nick).toBe(speaker.nick); 70 | expect(channel).toBe(expectedChannel); 71 | expect(mode).toBe('m'); 72 | expect(user).toBeUndefined(); 73 | }); 74 | test('will handle removing a parameter-less mode', async () => { 75 | const { speaker } = server.clients; 76 | const expectedChannel = TestIrcServer.generateUniqueChannel('foobar'); 77 | await speaker.join(expectedChannel); 78 | await speaker.waitForEvent('join'); 79 | const modeEvent = speaker.waitForEvent('-mode'); 80 | await speaker.send('MODE', expectedChannel, '+m'); 81 | await speaker.send('MODE', expectedChannel, '-m'); 82 | 83 | const [channel, nick, mode, user] = await modeEvent; 84 | expect(nick).toBe(speaker.nick); 85 | expect(channel).toBe(expectedChannel); 86 | expect(mode).toBe('m'); 87 | expect(user).toBeUndefined(); 88 | }); 89 | test('will handle adding a parameter mode', async () => { 90 | const { speaker, listener } = server.clients; 91 | const expectedChannel = TestIrcServer.generateUniqueChannel('foobar'); 92 | await speaker.join(expectedChannel); 93 | await listener.join(expectedChannel); 94 | await speaker.waitForEvent('join'); 95 | const modeEvent = speaker.waitForEvent('+mode'); 96 | await speaker.send('MODE', expectedChannel, '+o', listener.nick); 97 | 98 | const [channel, nick, mode, user] = await modeEvent; 99 | expect(nick).toBe(speaker.nick); 100 | expect(channel).toBe(expectedChannel); 101 | expect(mode).toBe('o'); 102 | expect(user).toBe(listener.nick); 103 | }); 104 | test('will handle removing a parameter mode', async () => { 105 | const { speaker, listener } = server.clients; 106 | const expectedChannel = TestIrcServer.generateUniqueChannel('foobar'); 107 | await speaker.join(expectedChannel); 108 | await listener.join(expectedChannel); 109 | await speaker.waitForEvent('join'); 110 | await speaker.send('MODE', expectedChannel, '+o', listener.nick); 111 | const modeEvent = speaker.waitForEvent('-mode'); 112 | await speaker.send('MODE', expectedChannel, '-o', listener.nick); 113 | 114 | const [channel, nick, mode, user] = await modeEvent; 115 | expect(nick).toBe(speaker.nick); 116 | expect(channel).toBe(expectedChannel); 117 | expect(mode).toBe('o'); 118 | expect(user).toBe(listener.nick); 119 | }); 120 | }); 121 | describe('isupport', () => { 122 | test('will not duplicate isupport values', async () => { 123 | const { speaker } = server.clients; 124 | // We assume the original isupport has arrived by this point. 125 | const isupportEventPromise = speaker.waitForEvent('isupport'); 126 | await speaker.send('VERSION'); 127 | await isupportEventPromise; 128 | 129 | expect(speaker.supported.channel.modes.a).toHaveLength(new Set(speaker.supported.channel.modes.a).size) 130 | expect(speaker.supported.channel.modes.b).toHaveLength(new Set(speaker.supported.channel.modes.b).size) 131 | expect(speaker.supported.channel.modes.c).toHaveLength(new Set(speaker.supported.channel.modes.c).size) 132 | expect(speaker.supported.channel.modes.d).toHaveLength(new Set(speaker.supported.channel.modes.d).size) 133 | expect(speaker.supported.extra).toHaveLength(new Set(speaker.supported.extra).size); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /spec/external-connection.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe, beforeEach, afterEach } from '@jest/globals'; 2 | import { TestIrcServer, TestClient } from '../src/testing'; 3 | import { DefaultIrcSupported, IrcConnection, IrcInMemoryState } from '../src'; 4 | import { createConnection } from 'net'; 5 | 6 | class TestIrcInMemoryState extends IrcInMemoryState { 7 | public flushCount = 0; 8 | public flush() { 9 | this.flushCount++; 10 | } 11 | } 12 | 13 | describe('Client with external connection', () => { 14 | let server: TestIrcServer; 15 | beforeEach(() => { 16 | server = new TestIrcServer(); 17 | return server.setUp([]); 18 | }); 19 | afterEach(() => { 20 | return server.tearDown(); 21 | }) 22 | let client: TestClient; 23 | test('can connect with a fresh session', async () => { 24 | const inMemoryState = new TestIrcInMemoryState(DefaultIrcSupported); 25 | client = new TestClient(server.address, TestIrcServer.generateUniqueNick("mynick"), { 26 | port: server.port, 27 | autoConnect: false, 28 | connectionTimeout: 4000, 29 | }, inMemoryState, createConnection({ 30 | port: server.port, 31 | host: server.address, 32 | }) as IrcConnection); 33 | client.connect(); 34 | await client.waitForEvent('registered'); 35 | expect(inMemoryState.registered).toBe(true); 36 | expect(inMemoryState.flushCount).toBeGreaterThan(0); 37 | client.disconnect(); 38 | }); 39 | test('can connect with a reused session', async () => { 40 | const inMemoryState = new TestIrcInMemoryState(DefaultIrcSupported); 41 | const persistentConnection = createConnection({ 42 | port: server.port, 43 | host: server.address, 44 | }) as IrcConnection; 45 | client = new TestClient(server.address, TestIrcServer.generateUniqueNick("mynick"), { 46 | port: server.port, 47 | autoConnect: false, 48 | connectionTimeout: 4000, 49 | }, inMemoryState, persistentConnection); 50 | client.connect(); 51 | await client.waitForEvent('registered'); 52 | client.destroy(); 53 | 54 | // Somehow we need to clear away the client. 55 | const reusedClient = new TestClient(server.address, TestIrcServer.generateUniqueNick("mynick"), { 56 | port: server.port, 57 | autoConnect: false, 58 | connectionTimeout: 4000, 59 | }, inMemoryState, persistentConnection); 60 | const promise = reusedClient.waitForEvent('registered'); 61 | reusedClient.connect(); 62 | await promise; 63 | client.disconnect(); 64 | }, 15000); 65 | }); 66 | -------------------------------------------------------------------------------- /src/capabilities.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | import { Message } from "./parse_message"; 3 | import TypedEmitter from "typed-emitter"; 4 | 5 | class Capabilities { 6 | constructor( 7 | public readonly caps = new Set(), 8 | public readonly saslTypes = new Set(), 9 | public ready = false, 10 | ) {} 11 | 12 | extend(messageArgs: string[]) { 13 | let capabilityString = messageArgs[0]; 14 | // https://ircv3.net/specs/extensions/capability-negotiation.html#multiline-replies-to-cap-ls-and-cap-list 15 | if (capabilityString === '*') { 16 | capabilityString = messageArgs[1]; 17 | } 18 | else { 19 | this.ready = true; 20 | } 21 | const allCaps = capabilityString.trim().split(' '); 22 | // Not all servers respond with the type of sasl supported. 23 | const saslTypes = allCaps.find((s) => s.startsWith('sasl='))?.split('=')[1] 24 | .split(',') 25 | .map((s) => s.toUpperCase()) || []; 26 | if (saslTypes) { 27 | allCaps.push('sasl'); 28 | } 29 | 30 | allCaps.forEach(c => this.caps.add(c)); 31 | saslTypes.forEach(t => this.saslTypes.add(t)); 32 | } 33 | } 34 | 35 | type IrcCapabilitiesEventEmitter = TypedEmitter<{ 36 | serverCapabilitesReady: () => void, 37 | userCapabilitesReady: () => void, 38 | }>; 39 | 40 | 41 | /** 42 | * A helper class to handle capabilities sent by the IRCd. 43 | */ 44 | export class IrcCapabilities extends (EventEmitter as new () => IrcCapabilitiesEventEmitter) { 45 | private serverCapabilites = new Capabilities(); 46 | private userCapabilites = new Capabilities(); 47 | 48 | constructor(data?: ReturnType) { 49 | super(); 50 | data?.serverCapabilites.forEach(v => this.serverCapabilites.caps.add(v)); 51 | data?.serverCapabilitesSasl.forEach(v => this.serverCapabilites.saslTypes.add(v)); 52 | data?.userCapabilites.forEach(v => this.serverCapabilites.caps.add(v)); 53 | data?.userCapabilitesSasl.forEach(v => this.userCapabilites.saslTypes.add(v)); 54 | } 55 | 56 | 57 | public serialise() { 58 | return { 59 | serverCapabilites: [...this.serverCapabilites.caps.values()], 60 | serverCapabilitesSasl: [...this.serverCapabilites.saslTypes.values()], 61 | userCapabilites: [...this.userCapabilites.caps.values()], 62 | userCapabilitesSasl: [...this.userCapabilites.saslTypes.values()], 63 | } 64 | } 65 | 66 | public get capsReady() { 67 | return this.userCapabilites.ready; 68 | } 69 | 70 | public get supportsSasl() { 71 | if (!this.serverCapabilites.ready) { 72 | throw Error('Server response has not arrived yet'); 73 | } 74 | return this.serverCapabilites.caps.has('sasl'); 75 | } 76 | 77 | /** 78 | * Check if the IRCD supports a given Sasl method. 79 | * @param method The method of SASL (e.g. 'PLAIN', 'EXTERNAL') to check support for. Case insensitive. 80 | * @param allowNoMethods Not all implementations support explicitly mentioning SASL methods, 81 | * so optionally we can return true here. 82 | * @returns True if supported, false otherwise. 83 | * @throws If the capabilites have not returned yet. 84 | */ 85 | public supportsSaslMethod(method: string, allowNoMethods=false) { 86 | if (!this.serverCapabilites.ready) { 87 | throw Error('Server caps response has not arrived yet'); 88 | } 89 | if (!this.serverCapabilites.caps.has('sasl')) { 90 | return false; 91 | } 92 | if (this.serverCapabilites.saslTypes.size === 0) { 93 | return allowNoMethods; 94 | } 95 | return this.serverCapabilites.saslTypes.has(method.toUpperCase()); 96 | } 97 | 98 | /** 99 | * Handle an incoming `CAP` message. 100 | */ 101 | public onCap(message: Message) { 102 | // E.g. CAP * LS :account-notify away-notify chghost extended-join multi-prefix 103 | // sasl=PLAIN,ECDSA-NIST256P-CHALLENGE,EXTERNAL tls account-tag cap-notify echo-message 104 | // solanum.chat/identify-msg solanum.chat/realhost 105 | const [, subCmd, ...parts] = message.args; 106 | if (subCmd === 'LS') { 107 | this.serverCapabilites.extend(parts); 108 | 109 | if (this.serverCapabilites.ready) { 110 | // We now need to request user caps 111 | this.emit('serverCapabilitesReady'); 112 | } 113 | } 114 | // The target might be * or the nickname, for now just accept either. 115 | if (subCmd === 'ACK') { 116 | this.userCapabilites.extend(parts); 117 | 118 | if (this.userCapabilites.ready) { 119 | this.emit('userCapabilitesReady'); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/codes.ts: -------------------------------------------------------------------------------- 1 | export type CommandType = 'reply'|'error'|'normal'; 2 | 3 | export const replyCodes = { 4 | '001': { 5 | name: 'rpl_welcome', 6 | type: 'reply' 7 | }, 8 | '002': { 9 | name: 'rpl_yourhost', 10 | type: 'reply' 11 | }, 12 | '003': { 13 | name: 'rpl_created', 14 | type: 'reply' 15 | }, 16 | '004': { 17 | name: 'rpl_myinfo', 18 | type: 'reply' 19 | }, 20 | '005': { 21 | name: 'rpl_isupport', 22 | type: 'reply' 23 | }, 24 | 200: { 25 | name: 'rpl_tracelink', 26 | type: 'reply' 27 | }, 28 | 201: { 29 | name: 'rpl_traceconnecting', 30 | type: 'reply' 31 | }, 32 | 202: { 33 | name: 'rpl_tracehandshake', 34 | type: 'reply' 35 | }, 36 | 203: { 37 | name: 'rpl_traceunknown', 38 | type: 'reply' 39 | }, 40 | 204: { 41 | name: 'rpl_traceoperator', 42 | type: 'reply' 43 | }, 44 | 205: { 45 | name: 'rpl_traceuser', 46 | type: 'reply' 47 | }, 48 | 206: { 49 | name: 'rpl_traceserver', 50 | type: 'reply' 51 | }, 52 | 208: { 53 | name: 'rpl_tracenewtype', 54 | type: 'reply' 55 | }, 56 | 211: { 57 | name: 'rpl_statslinkinfo', 58 | type: 'reply' 59 | }, 60 | 212: { 61 | name: 'rpl_statscommands', 62 | type: 'reply' 63 | }, 64 | 213: { 65 | name: 'rpl_statscline', 66 | type: 'reply' 67 | }, 68 | 214: { 69 | name: 'rpl_statsnline', 70 | type: 'reply' 71 | }, 72 | 215: { 73 | name: 'rpl_statsiline', 74 | type: 'reply' 75 | }, 76 | 216: { 77 | name: 'rpl_statskline', 78 | type: 'reply' 79 | }, 80 | 218: { 81 | name: 'rpl_statsyline', 82 | type: 'reply' 83 | }, 84 | 219: { 85 | name: 'rpl_endofstats', 86 | type: 'reply' 87 | }, 88 | 221: { 89 | name: 'rpl_umodeis', 90 | type: 'reply' 91 | }, 92 | 241: { 93 | name: 'rpl_statslline', 94 | type: 'reply' 95 | }, 96 | 242: { 97 | name: 'rpl_statsuptime', 98 | type: 'reply' 99 | }, 100 | 243: { 101 | name: 'rpl_statsoline', 102 | type: 'reply' 103 | }, 104 | 244: { 105 | name: 'rpl_statshline', 106 | type: 'reply' 107 | }, 108 | 250: { 109 | name: 'rpl_statsconn', 110 | type: 'reply' 111 | }, 112 | 251: { 113 | name: 'rpl_luserclient', 114 | type: 'reply' 115 | }, 116 | 252: { 117 | name: 'rpl_luserop', 118 | type: 'reply' 119 | }, 120 | 253: { 121 | name: 'rpl_luserunknown', 122 | type: 'reply' 123 | }, 124 | 254: { 125 | name: 'rpl_luserchannels', 126 | type: 'reply' 127 | }, 128 | 255: { 129 | name: 'rpl_luserme', 130 | type: 'reply' 131 | }, 132 | 256: { 133 | name: 'rpl_adminme', 134 | type: 'reply' 135 | }, 136 | 257: { 137 | name: 'rpl_adminloc1', 138 | type: 'reply' 139 | }, 140 | 258: { 141 | name: 'rpl_adminloc2', 142 | type: 'reply' 143 | }, 144 | 259: { 145 | name: 'rpl_adminemail', 146 | type: 'reply' 147 | }, 148 | 261: { 149 | name: 'rpl_tracelog', 150 | type: 'reply' 151 | }, 152 | 265: { 153 | name: 'rpl_localusers', 154 | type: 'reply' 155 | }, 156 | 266: { 157 | name: 'rpl_globalusers', 158 | type: 'reply' 159 | }, 160 | 276: { 161 | name: 'rpl_whoiscertfp', 162 | type: 'reply', 163 | }, 164 | 300: { 165 | name: 'rpl_none', 166 | type: 'reply' 167 | }, 168 | 301: { 169 | name: 'rpl_away', 170 | type: 'reply' 171 | }, 172 | 302: { 173 | name: 'rpl_userhost', 174 | type: 'reply' 175 | }, 176 | 303: { 177 | name: 'rpl_ison', 178 | type: 'reply' 179 | }, 180 | 305: { 181 | name: 'rpl_unaway', 182 | type: 'reply' 183 | }, 184 | 306: { 185 | name: 'rpl_nowaway', 186 | type: 'reply' 187 | }, 188 | 311: { 189 | name: 'rpl_whoisuser', 190 | type: 'reply' 191 | }, 192 | 312: { 193 | name: 'rpl_whoisserver', 194 | type: 'reply' 195 | }, 196 | 313: { 197 | name: 'rpl_whoisoperator', 198 | type: 'reply' 199 | }, 200 | 314: { 201 | name: 'rpl_whowasuser', 202 | type: 'reply' 203 | }, 204 | 315: { 205 | name: 'rpl_endofwho', 206 | type: 'reply' 207 | }, 208 | 317: { 209 | name: 'rpl_whoisidle', 210 | type: 'reply' 211 | }, 212 | 318: { 213 | name: 'rpl_endofwhois', 214 | type: 'reply' 215 | }, 216 | 319: { 217 | name: 'rpl_whoischannels', 218 | type: 'reply' 219 | }, 220 | 321: { 221 | name: 'rpl_liststart', 222 | type: 'reply' 223 | }, 224 | 322: { 225 | name: 'rpl_list', 226 | type: 'reply' 227 | }, 228 | 323: { 229 | name: 'rpl_listend', 230 | type: 'reply' 231 | }, 232 | 324: { 233 | name: 'rpl_channelmodeis', 234 | type: 'reply' 235 | }, 236 | 329: { 237 | name: 'rpl_creationtime', 238 | type: 'reply' 239 | }, 240 | 330: { 241 | name: 'rpl_whoisaccount', 242 | type: 'reply' 243 | }, 244 | 331: { 245 | name: 'rpl_notopic', 246 | type: 'reply' 247 | }, 248 | 332: { 249 | name: 'rpl_topic', 250 | type: 'reply' 251 | }, 252 | 333: { 253 | name: 'rpl_topicwhotime', 254 | type: 'reply' 255 | }, 256 | 338: { 257 | name: 'rpl_whoisactually', 258 | type: 'reply' 259 | }, 260 | 341: { 261 | name: 'rpl_inviting', 262 | type: 'reply' 263 | }, 264 | 342: { 265 | name: 'rpl_summoning', 266 | type: 'reply' 267 | }, 268 | 351: { 269 | name: 'rpl_version', 270 | type: 'reply' 271 | }, 272 | 352: { 273 | name: 'rpl_whoreply', 274 | type: 'reply' 275 | }, 276 | 353: { 277 | name: 'rpl_namreply', 278 | type: 'reply' 279 | }, 280 | 364: { 281 | name: 'rpl_links', 282 | type: 'reply' 283 | }, 284 | 365: { 285 | name: 'rpl_endoflinks', 286 | type: 'reply' 287 | }, 288 | 366: { 289 | name: 'rpl_endofnames', 290 | type: 'reply' 291 | }, 292 | 367: { 293 | name: 'rpl_banlist', 294 | type: 'reply' 295 | }, 296 | 368: { 297 | name: 'rpl_endofbanlist', 298 | type: 'reply' 299 | }, 300 | 369: { 301 | name: 'rpl_endofwhowas', 302 | type: 'reply' 303 | }, 304 | 371: { 305 | name: 'rpl_info', 306 | type: 'reply' 307 | }, 308 | 372: { 309 | name: 'rpl_motd', 310 | type: 'reply' 311 | }, 312 | 374: { 313 | name: 'rpl_endofinfo', 314 | type: 'reply' 315 | }, 316 | 375: { 317 | name: 'rpl_motdstart', 318 | type: 'reply' 319 | }, 320 | 376: { 321 | name: 'rpl_endofmotd', 322 | type: 'reply' 323 | }, 324 | 381: { 325 | name: 'rpl_youreoper', 326 | type: 'reply' 327 | }, 328 | 382: { 329 | name: 'rpl_rehashing', 330 | type: 'reply' 331 | }, 332 | 391: { 333 | name: 'rpl_time', 334 | type: 'reply' 335 | }, 336 | 392: { 337 | name: 'rpl_usersstart', 338 | type: 'reply' 339 | }, 340 | 393: { 341 | name: 'rpl_users', 342 | type: 'reply' 343 | }, 344 | 394: { 345 | name: 'rpl_endofusers', 346 | type: 'reply' 347 | }, 348 | 395: { 349 | name: 'rpl_nousers', 350 | type: 'reply' 351 | }, 352 | 401: { 353 | name: 'err_nosuchnick', 354 | type: 'error' 355 | }, 356 | 402: { 357 | name: 'err_nosuchserver', 358 | type: 'error' 359 | }, 360 | 403: { 361 | name: 'err_nosuchchannel', 362 | type: 'error' 363 | }, 364 | 404: { 365 | name: 'err_cannotsendtochan', 366 | type: 'error' 367 | }, 368 | 405: { 369 | name: 'err_toomanychannels', 370 | type: 'error' 371 | }, 372 | 406: { 373 | name: 'err_wasnosuchnick', 374 | type: 'error' 375 | }, 376 | 407: { 377 | name: 'err_toomanytargets', 378 | type: 'error' 379 | }, 380 | 409: { 381 | name: 'err_noorigin', 382 | type: 'error' 383 | }, 384 | 411: { 385 | name: 'err_norecipient', 386 | type: 'error' 387 | }, 388 | 412: { 389 | name: 'err_notexttosend', 390 | type: 'error' 391 | }, 392 | 413: { 393 | name: 'err_notoplevel', 394 | type: 'error' 395 | }, 396 | 414: { 397 | name: 'err_wildtoplevel', 398 | type: 'error' 399 | }, 400 | // Used in oftc-hybrid-1.7.3 and Solanum. 401 | // Returned when a channel requires its users to have a NickServ account to 402 | // talk. 403 | 415: { 404 | name: 'err_needreggednick', 405 | type: 'error' 406 | }, 407 | 421: { 408 | name: 'err_unknowncommand', 409 | type: 'error' 410 | }, 411 | 422: { 412 | name: 'err_nomotd', 413 | type: 'error' 414 | }, 415 | 423: { 416 | name: 'err_noadmininfo', 417 | type: 'error' 418 | }, 419 | 424: { 420 | name: 'err_fileerror', 421 | type: 'error' 422 | }, 423 | // Non-standard by AustHex: 424 | // Returned by NICK when the user is not allowed to change their nickname due to 425 | // a channel event (channel mode +E) 426 | 430: { 427 | name: 'err_eventnickchange', 428 | type: 'error' 429 | }, 430 | 431: { 431 | name: 'err_nonicknamegiven', 432 | type: 'error' 433 | }, 434 | 432: { 435 | name: 'err_erroneusnickname', 436 | type: 'error' 437 | }, 438 | 433: { 439 | name: 'err_nicknameinuse', 440 | type: 'error' 441 | }, 442 | // 435 is also ERR_SERVICECONFUSED on Unreal, but looking at Unreal's source 443 | // it doesn't appear to be used. We DO see ERR_BANONCHAN (Bahamut) in the wild 444 | // though (on Freenode), so assume it is that. 445 | 435: { 446 | name: 'err_banonchan', 447 | type: 'error' 448 | }, 449 | 436: { 450 | name: 'err_nickcollision', 451 | type: 'error' 452 | }, 453 | // Seen in the wild on Freenode when trying to *connect* with the nickname "boot". 454 | // Also seen when trying to change nicks (cooldown period?) 455 | 437: { 456 | name: 'err_unavailresource', 457 | type: 'error' 458 | }, 459 | 438: { 460 | name: 'err_nicktoofast', 461 | type: 'error' 462 | }, 463 | 441: { 464 | name: 'err_usernotinchannel', 465 | type: 'error' 466 | }, 467 | 442: { 468 | name: 'err_notonchannel', 469 | type: 'error' 470 | }, 471 | 443: { 472 | name: 'err_useronchannel', 473 | type: 'error' 474 | }, 475 | 444: { 476 | name: 'err_nologin', 477 | type: 'error' 478 | }, 479 | 445: { 480 | name: 'err_summondisabled', 481 | type: 'error' 482 | }, 483 | 446: { 484 | name: 'err_usersdisabled', 485 | type: 'error' 486 | }, 487 | 451: { 488 | name: 'err_notregistered', 489 | type: 'error' 490 | }, 491 | 461: { 492 | name: 'err_needmoreparams', 493 | type: 'error' 494 | }, 495 | 462: { 496 | name: 'err_alreadyregistred', 497 | type: 'error' 498 | }, 499 | 463: { 500 | name: 'err_nopermforhost', 501 | type: 'error' 502 | }, 503 | 464: { 504 | name: 'err_passwdmismatch', 505 | type: 'error' 506 | }, 507 | 465: { 508 | name: 'err_yourebannedcreep', 509 | type: 'error' 510 | }, 511 | 467: { 512 | name: 'err_keyset', 513 | type: 'error' 514 | }, 515 | 471: { 516 | name: 'err_channelisfull', 517 | type: 'error' 518 | }, 519 | 472: { 520 | name: 'err_unknownmode', 521 | type: 'error' 522 | }, 523 | 473: { 524 | name: 'err_inviteonlychan', 525 | type: 'error' 526 | }, 527 | 474: { 528 | name: 'err_bannedfromchan', 529 | type: 'error' 530 | }, 531 | 475: { 532 | name: 'err_badchannelkey', 533 | type: 'error' 534 | }, 535 | 477: { 536 | name: 'err_needreggednick', 537 | type: 'error' 538 | }, 539 | 481: { 540 | name: 'err_noprivileges', 541 | type: 'error' 542 | }, 543 | 482: { 544 | name: 'err_chanoprivsneeded', 545 | type: 'error' 546 | }, 547 | 483: { 548 | name: 'err_cantkillserver', 549 | type: 'error' 550 | }, 551 | 486: { 552 | name: 'err_nononreg', // Sent when trying to PM a user (+R) while we are not. 553 | type: 'error' 554 | }, 555 | 491: { 556 | name: 'err_nooperhost', 557 | type: 'error' 558 | }, 559 | 501: { 560 | name: 'err_umodeunknownflag', 561 | type: 'error' 562 | }, 563 | 502: { 564 | name: 'err_usersdontmatch', 565 | type: 'error' 566 | }, 567 | // https://ircv3.net/registry 568 | 900: { 569 | name: 'rpl_loggedin', 570 | type: 'reply', 571 | }, 572 | 901: { 573 | name: 'rpl_loggedout', 574 | type: 'reply', 575 | }, 576 | 903: { 577 | name: 'rpl_saslsuccess', 578 | type: 'normal', 579 | }, 580 | 904: { 581 | name: 'err_saslfail', 582 | type: 'error', 583 | }, 584 | 905: { 585 | name: 'err_sasltoolong', 586 | type: 'error', 587 | }, 588 | 906: { 589 | name: 'err_saslaborted', 590 | type: 'error', 591 | }, 592 | 907: { 593 | name: 'err_saslalready', 594 | type: 'error', 595 | }, 596 | } as {[id: string]: {name: string, type: CommandType}}; 597 | -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | export const 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 | 25 | // https://modern.ircdocs.horse/formatting.html 26 | const styles = Object.freeze({ 27 | normal : '\x0F', 28 | underline : '\x1F', 29 | bold : '\x02', 30 | italic : '\x1D', 31 | inverse : '\x16', 32 | strikethrough : '\x1E', 33 | monospace : '\x11', 34 | }); 35 | 36 | const styleChars: Readonly = Object.freeze([...Object.values(styles)]); 37 | const coloringCharacter = '\x03'; 38 | 39 | export function wrap(color: keyof(typeof codes), text: string, resetColor: keyof(typeof codes)) { 40 | if (codes[color]) { 41 | text = codes[color] + text; 42 | text += (codes[resetColor]) ? codes[resetColor] : codes.reset; 43 | } 44 | return text; 45 | } 46 | 47 | export function stripColorsAndStyle(str: string): string { 48 | // We borrowed the logic from https://github.com/fent/irc-colors.js 49 | // when writing this function, so the license is given below. 50 | /** 51 | * MIT License 52 | 53 | Copyright (C) 2011 by fent 54 | 55 | Permission is hereby granted, free of charge, to any person obtaining a copy 56 | of this software and associated documentation files (the "Software"), to deal 57 | in the Software without restriction, including without limitation the rights 58 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 59 | copies of the Software, and to permit persons to whom the Software is 60 | furnished to do so, subject to the following conditions: 61 | 62 | The above copyright notice and this permission notice shall be included in 63 | all copies or substantial portions of the Software. 64 | 65 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 66 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 67 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 68 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 69 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 70 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 71 | THE SOFTWARE. 72 | */ 73 | 74 | // Strip style 75 | const path: [string, number][] = []; 76 | for (let i = 0, len = str.length; i < len; i++) { 77 | const char = str[i]; 78 | if (styleChars.includes(char) || char === coloringCharacter) { 79 | const lastChar = path[path.length - 1]; 80 | if (lastChar && lastChar[0] === char) { 81 | const p0 = lastChar[1]; 82 | // Don't strip out styles with no characters inbetween. 83 | // And don't strip out color codes. 84 | if (i - p0 > 1 && char !== coloringCharacter) { 85 | str = str.slice(0, p0) + str.slice(p0 + 1, i) + str.slice(i + 1); 86 | i -= 2; 87 | } 88 | path.pop(); 89 | } 90 | else { 91 | path.push([str[i], i]); 92 | } 93 | } 94 | } 95 | 96 | // Remove any unmatching style characters. 97 | // Traverse list backwards to make removing less complicated. 98 | for (const char of path.reverse()) { 99 | if (char[0] !== coloringCharacter) { 100 | const pos = char[1]; 101 | str = str.slice(0, pos) + str.slice(pos + 1); 102 | } 103 | } 104 | 105 | // Strip colors 106 | str = str.replace(/\x03\d{0,2}(,\d{0,2}|\x02\x02)?/g, ''); 107 | return str; 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import { WhoisResponse } from "./state"; 2 | import { ChanListItem, SaslErrors } from './irc'; 3 | import { Message } from './parse_message'; 4 | 5 | type IrcChannelName = string; 6 | type CtcpType = string; 7 | type MessageNick = string; 8 | 9 | export type CtcpEventIndex = `ctcp-${CtcpType}`; 10 | 11 | // Due to a horrible design decision of the past, there is no seperator between the type 12 | // and channel in this event type. We also can't mandate that Channel has a non-zero length. 13 | // The combination of which means that the signatures below would clash with the channel-less 14 | // siguatures, so we've introduced a fake `#` to make the types work. This is a known bug, and 15 | // until TypeScript let's us specify non-zero string, a bug it will remain. 16 | 17 | export type PartEventIndex = `part#${IrcChannelName}`; 18 | export type JoinEventIndex = `join#${IrcChannelName}`; 19 | export type MessageEventIndex = `message#${IrcChannelName}`; 20 | 21 | export type ClientEvents = { 22 | registered: () => void, 23 | notice: (from: MessageNick, to: string, noticeText: string, message: Message) => void, 24 | mode_is: (channel: IrcChannelName, mode: string) => void, 25 | '+mode': ( 26 | channel: IrcChannelName, nick: MessageNick, mode: string, user: string|undefined, message: Message) => void, 27 | '-mode': ( 28 | channel: IrcChannelName, nick: MessageNick, mode: string, user: string|undefined, message: Message) => void, 29 | nick: (oldNick: MessageNick, newNick: string, channels: string[], message: Message) => void, 30 | motd: (modt: string) => void, 31 | action: (from: string, to: string, action: string, message: Message) => void, 32 | ctcp: (from: string, to: string, text: string, CtcpType: string, message: Message) => void, 33 | [key: CtcpEventIndex]: (from: string, to: string, text: string, message: Message) => void, 34 | raw: (message: Message) => void, 35 | kick: (channel: IrcChannelName, who: string, by: MessageNick, reason: string, message: Message) => void, 36 | names: (channel: IrcChannelName, users: Map) => void 37 | topic: (channel: IrcChannelName, topic: string, by: MessageNick, message: Message) => void, 38 | channellist: (items: ChanListItem[]) => void, 39 | channellist_start: () => void, 40 | channellist_item: (item: ChanListItem) => void, 41 | whois: (whois: WhoisResponse) => void, 42 | selfMessage: (target: string, messageLine: string) => void, 43 | kill: (nick: string, reason: string, channels: string[], message: Message) => void, 44 | message: (from: MessageNick, to: string, text: string, message: Message) => void, 45 | pm: (from: MessageNick, text: string, message: Message) => void, 46 | invite: (channel: IrcChannelName, from: MessageNick, message: Message) => void, 47 | quit: (nick: MessageNick, reason: string, channels: string[], message: Message) => void, 48 | join: (channel: IrcChannelName, nick: MessageNick, message: Message) => void, 49 | abort: (retryCount: number) => void, 50 | connect: () => void, 51 | error: (message: Message) => void, 52 | sasl_error: (error: SaslErrors, ...args: string[]) => void, 53 | sasl_loggedin: (nick: string, ident: string, account: string, message: string) => void, 54 | sasl_loggedout: (nick: string, ident: string, message: string) => void, 55 | ping: (pingData: string) => void, 56 | pong: (pingData: string) => void, 57 | netError: (error: Error) => void, 58 | part: (channel: IrcChannelName, nick: MessageNick, reason: string, message: Message) => void, 59 | isupport: () => void, 60 | [key: JoinEventIndex]: (who: MessageNick, message: Message) => void, 61 | [key: PartEventIndex]: (nick: MessageNick, who: string, message: Message) => void, 62 | [key: MessageEventIndex]: (nick: MessageNick, who: string, message: Message) => void, 63 | } 64 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './colors'; 2 | export * from './irc'; 3 | export * from './events'; 4 | export * from './parse_message'; 5 | export * from './state'; 6 | export * from './capabilities'; 7 | export * from './testing/index'; 8 | -------------------------------------------------------------------------------- /src/parse_message.ts: -------------------------------------------------------------------------------- 1 | import { CommandType, replyCodes } from './codes'; 2 | import { stripColorsAndStyle } from './colors'; 3 | 4 | export interface Message { 5 | prefix?: string; 6 | server?: string; 7 | nick?: string; 8 | user?: string; 9 | host?: string; 10 | args: string[]; 11 | command?: string; 12 | rawCommand?: string; 13 | commandType: CommandType; 14 | } 15 | 16 | /** 17 | * parseMessage(line, stripColors) 18 | * 19 | * takes a raw "line" from the IRC server and turns it into an object with 20 | * useful keys 21 | * @param line Raw message from IRC server. 22 | * @param stripColors If true, strip IRC colors. 23 | * @return A parsed message object. 24 | */ 25 | export function parseMessage(line: string, stripColors: boolean): Message { 26 | const message: Message = { 27 | args: [], 28 | commandType: 'normal', 29 | }; 30 | if (stripColors) { 31 | line = stripColorsAndStyle(line); 32 | } 33 | 34 | // Parse prefix 35 | let match = line.match(/^:([^ ]+) +/); 36 | if (match) { 37 | message.prefix = match[1]; 38 | line = line.replace(/^:[^ ]+ +/, ''); 39 | match = message.prefix.match(/^([_a-zA-Z0-9\[\]\\`^{}|-]*)(!([^@]+)@(.*))?$/); 40 | if (match) { 41 | message.nick = match[1]; 42 | message.user = match[3]; 43 | message.host = match[4]; 44 | } 45 | else { 46 | message.server = message.prefix; 47 | } 48 | } 49 | 50 | // Parse command 51 | match = line.match(/^([^ ]+) */); 52 | message.command = match?.[1]; 53 | message.rawCommand = match?.[1]; 54 | line = line.replace(/^[^ ]+ +/, ''); 55 | if (message.rawCommand && replyCodes[message.rawCommand]) { 56 | message.command = replyCodes[message.rawCommand].name; 57 | message.commandType = replyCodes[message.rawCommand].type; 58 | } 59 | 60 | let middle, trailing; 61 | 62 | // Parse parameters 63 | if (line.search(/^:| +:/) !== -1) { 64 | match = line.match(/(.*?)(?:^:| +:)(.*)/); 65 | if (!match) { 66 | throw Error('Invalid format, could not parse parameters'); 67 | } 68 | middle = match[1].trimEnd(); 69 | trailing = match[2]; 70 | } 71 | else { 72 | middle = line; 73 | } 74 | 75 | if (middle.length) {message.args = middle.split(/ +/);} 76 | 77 | if (typeof (trailing) !== 'undefined' && trailing.length) {message.args.push(trailing);} 78 | 79 | return message; 80 | } 81 | -------------------------------------------------------------------------------- /src/splitLines.ts: -------------------------------------------------------------------------------- 1 | function getUtf8LenAt(text: string, cutPosCurr: number) { 2 | const char = text.charCodeAt(cutPosCurr); 3 | if (char <= 0x7f) { // Single width 4 | return 1; 5 | } 6 | if (char <= 0x7ff) { // Double width 7 | return 2; 8 | } 9 | return 3; 10 | } 11 | 12 | 13 | export default function splitLongLines(text: string, maxLengthInBytes = 450): string[] { 14 | // If maxLength hasn't been initialized yet, prefer an arbitrarily low 15 | // line length over crashing. 16 | if (maxLengthInBytes < 4) { 17 | // That tight restriction makes no sense in IRC. 18 | return [text]; 19 | } 20 | 21 | const result = []; 22 | 23 | let cutPosStart = 0, bytesStart = 0; 24 | let cutPosWord = 0, bytesWord = 0; 25 | let cutPosCurr = 0, bytesCurr = 0; 26 | let wasSpace = true; 27 | 28 | while (cutPosCurr < text.length) { 29 | let utf8len, utf16len, isWhitespace; 30 | if ((text.charCodeAt(cutPosCurr) & 0xF800) === 0xD800) { 31 | // Surrogate pair. 32 | utf8len = 4; 33 | utf16len = 2; 34 | isWhitespace = false; 35 | } 36 | else { 37 | utf8len = getUtf8LenAt(text, cutPosCurr); 38 | utf16len = 1; 39 | isWhitespace = /\s/.test(text[cutPosCurr]); 40 | } 41 | 42 | if (!wasSpace && isWhitespace) { 43 | cutPosWord = cutPosCurr; 44 | bytesWord = bytesCurr; 45 | } 46 | 47 | if (bytesCurr + utf8len - bytesStart > maxLengthInBytes) { 48 | if (cutPosWord !== cutPosStart) { 49 | cutPosCurr = cutPosWord; 50 | bytesCurr = bytesWord; 51 | } 52 | 53 | result.push(text.substring(cutPosStart, cutPosCurr)); 54 | 55 | // Skip leading spaces. 56 | while (cutPosCurr < text.length && text[cutPosCurr].match(/\s/)) { 57 | // According to [1], `/\s/` does not match code points beyond 58 | // 0xffff. Consequently, we can assume that any whitespace 59 | // character consists of single UTF-16 code unit, not a 60 | // surrogate pair. 61 | // [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp 62 | cutPosCurr += 1; 63 | bytesCurr += getUtf8LenAt(text, cutPosCurr); 64 | } 65 | 66 | cutPosWord = cutPosStart = cutPosCurr; 67 | bytesWord = bytesStart = bytesCurr; 68 | wasSpace = true; 69 | } 70 | else { 71 | cutPosCurr += utf16len; 72 | bytesCurr += utf8len; 73 | wasSpace = isWhitespace; 74 | } 75 | } 76 | 77 | 78 | if (cutPosStart !== cutPosCurr) { 79 | let startPos = cutPosStart; 80 | if (result.length !== 0) { 81 | while (startPos < text.length && /\s/.test(text[startPos])) { 82 | startPos++; 83 | } 84 | } 85 | if (startPos !== text.length || result.length === 0) { 86 | result.push(text.substring(startPos)); 87 | } 88 | } 89 | return result; 90 | } 91 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | import { IrcCapabilities } from "./capabilities"; 2 | 3 | export interface WhoisResponse { 4 | nick: string; 5 | user?: string; 6 | channels?: string[]; 7 | host?: string; 8 | realname?: string; 9 | away?: string; 10 | idle?: string; 11 | server?: string; 12 | serverinfo?: string; 13 | operator?: string; 14 | account?: string; 15 | accountinfo?: string; 16 | realHost?: string; 17 | certfp?: string; 18 | } 19 | 20 | export interface IrcSupported { 21 | channel: { 22 | idlength: {[key: string]: string}; 23 | length: number; 24 | limit: {[key: string]: number}; 25 | // https://www.irc.info/articles/rpl_isupport 26 | modes: { 27 | /** 28 | * Always take a parameter when specified by the server. 29 | * May have a parameter when specificed by the client. 30 | */ 31 | a: string; 32 | /** 33 | * Alwyas take a parameter. 34 | */ 35 | b: string; 36 | /** 37 | * Take a parameter when set, absent when removed. 38 | */ 39 | c: string; 40 | /** 41 | * Never take a parameter. 42 | */ 43 | d: string; 44 | }, 45 | types: string; 46 | }; 47 | maxlist: {[key: string]: number}; 48 | maxtargets:{[key: string]: number}; 49 | modes: number; 50 | nicklength: number; 51 | topiclength: number; 52 | kicklength: number; 53 | usermodes: string; 54 | usermodepriority: string; // E.g "ov" 55 | // http://www.irc.org/tech_docs/005.html 56 | casemapping: 'ascii'|'rfc1459'|'strict-rfc1459'; 57 | extra: string[]; 58 | } 59 | 60 | /** 61 | * Features supported by the server 62 | * (initial values are RFC 1459 defaults. Zeros signify 63 | * no default or unlimited value) 64 | */ 65 | export const DefaultIrcSupported = { 66 | channel: { 67 | idlength: {}, 68 | length: 200, 69 | limit: {}, 70 | modes: { a: '', b: '', c: '', d: ''}, 71 | types: '', 72 | }, 73 | kicklength: 0, 74 | maxlist: {}, 75 | maxtargets: {}, 76 | modes: 3, 77 | nicklength: 9, 78 | topiclength: 0, 79 | usermodes: '', 80 | usermodepriority: '', // E.g "ov" 81 | casemapping: 'ascii', 82 | extra: [], 83 | } as IrcSupported; 84 | 85 | export interface ChanData { 86 | created?: string; 87 | key: string; 88 | serverName: string; 89 | /** 90 | * nick => mode 91 | */ 92 | users: Map, 93 | tmpUsers: Map, // used while processing NAMES replies 94 | mode: string; 95 | modeParams: Map, 96 | topic?: string; 97 | topicBy?: string; 98 | } 99 | 100 | export interface IrcClientState { 101 | loggedIn: boolean; 102 | registered: boolean; 103 | /** 104 | * This will either be the requested nick or the actual nickname. 105 | */ 106 | currentNick: string; 107 | whoisData: Map; 108 | nickMod: number; 109 | modeForPrefix: {[prefix: string]: string}; // @ => o 110 | capabilities: IrcCapabilities; 111 | supportedState: IrcSupported; 112 | hostMask: string; 113 | chans: Map; 114 | prefixForMode: {[mode: string]: string}; // o => @ 115 | lastSendTime: number; 116 | flush?: () => void; 117 | } 118 | 119 | export class IrcInMemoryState implements IrcClientState { 120 | public loggedIn = false; 121 | public registered = false; 122 | public currentNick = ''; 123 | public whoisData = new Map(); 124 | public nickMod = 0; 125 | public modeForPrefix: {[prefix: string]: string} = {}; 126 | public prefixForMode: {[mode: string]: string} = {}; // o => @ 127 | public hostMask = ''; 128 | public chans = new Map(); 129 | public lastSendTime = 0; 130 | public capabilities: IrcCapabilities = new IrcCapabilities(); 131 | constructor (public supportedState: IrcSupported) { 132 | 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/testing/index.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | import { Client, ClientEvents, IrcClientOpts, Message } from '..'; 3 | 4 | const DEFAULT_PORT = parseInt(process.env.IRC_TEST_PORT ?? '6667', 10); 5 | const DEFAULT_ADDRESS = process.env.IRC_TEST_ADDRESS ?? "127.0.0.1"; 6 | 7 | /** 8 | * Exposes a client instance with helper methods to listen 9 | * for events. 10 | */ 11 | export class TestClient extends Client { 12 | public readonly errors: Message[] = []; 13 | 14 | public connect(fn?: () => void) { 15 | // These can be IRC errors which aren't fatal to tests. 16 | this.on('error', msg => this.errors.push(msg)); 17 | super.connect(fn); 18 | } 19 | 20 | public waitForEvent( 21 | eventName: T, timeoutMs = 5000 22 | ): Promise> { 23 | return new Promise>( 24 | (resolve, reject) => { 25 | const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${eventName}`)), timeoutMs); 26 | this.once(eventName, (...m: unknown[]) => { 27 | clearTimeout(timeout); 28 | resolve(m as Parameters); 29 | }); 30 | }, 31 | ); 32 | } 33 | } 34 | 35 | /** 36 | * A jest-compatible test rig that can be used to run tests against an IRC server. 37 | * 38 | * @example 39 | * ```ts 40 | let server: TestIrcServer; 41 | beforeEach(() => { 42 | server = new TestIrcServer(); 43 | return server.setUp(); 44 | }); 45 | afterEach(() => { 46 | return server.tearDown(); 47 | }) 48 | describe('joining channels', () => { 49 | test('will get a join event from a newly joined user', async () => { 50 | const { speaker, listener } = server.clients; 51 | 52 | // Join the room and listen 53 | const listenerJoinPromise = listener.waitForEvent('join'); 54 | await listener.join('#foobar'); 55 | const [lChannel, lNick] = await listenerJoinPromise; 56 | expect(lNick).toBe(listener.nick); 57 | expect(lChannel).toBe('#foobar'); 58 | 59 | const speakerJoinPromise = listener.waitForEvent('join'); 60 | await speaker.join('#foobar'); 61 | const [channel, nick] = await speakerJoinPromise; 62 | expect(nick).toBe(speaker.nick); 63 | expect(channel).toBe('#foobar'); 64 | }); 65 | }); 66 | * ``` 67 | */ 68 | export class TestIrcServer { 69 | 70 | static generateUniqueNick(name = 'default') { 71 | return `${name}-${randomUUID().replace('-', '').substring(0, 8)}`; 72 | } 73 | static generateUniqueChannel(name = 'default') { 74 | return `#${this.generateUniqueNick(name)}`; 75 | } 76 | 77 | public readonly clients: Record = {}; 78 | constructor( 79 | public readonly address = DEFAULT_ADDRESS, public readonly port = DEFAULT_PORT, 80 | public readonly customConfig: Partial = {} 81 | ) { } 82 | 83 | async setUp(clients = ['speaker', 'listener']) { 84 | const connections: Promise[] = []; 85 | for (const clientName of clients) { 86 | const client = 87 | new TestClient(this.address, TestIrcServer.generateUniqueNick(clientName), { 88 | port: this.port, 89 | autoConnect: false, 90 | connectionTimeout: 4000, 91 | debug: true, 92 | ...this.customConfig, 93 | }); 94 | this.clients[clientName] = client; 95 | // Make sure we load isupport before reporting readyness. 96 | const isupportEvent = client.waitForEvent('isupport').then(() => { /* not interested in the value */ }); 97 | const connectionPromise = new Promise((resolve, reject) => { 98 | client.once('error', e => reject(e)); 99 | client.connect(resolve) 100 | }).then(() => isupportEvent); 101 | connections.push(connectionPromise); 102 | } 103 | await Promise.all(connections); 104 | } 105 | 106 | async tearDown() { 107 | const connections: Promise[] = []; 108 | for (const client of Object.values(this.clients)) { 109 | connections.push(new Promise((resolve, reject) => { 110 | client.once('error', e => reject(e)); 111 | client.disconnect(resolve) 112 | })); 113 | } 114 | await Promise.all(connections); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/data/fixtures.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic": { 3 | "sent": [ 4 | ["CAP LS 302", "Client sent CAP request"], 5 | ["NICK testbot", "Client sent NICK message"], 6 | ["USER nodebot 8 * :nodeJS IRC client", "Client sent USER message"], 7 | ["QUIT :node-irc says goodbye", "Client sent QUIT message"] 8 | ], 9 | 10 | "received": [ 11 | [":localhost 001 testbot :Welcome to the Internet Relay Chat Network testbot\r\n", "Received welcome message"] 12 | ] 13 | }, 14 | "doubleCRLF": { 15 | "sent": [ 16 | ["CAP LS 302", "Client sent CAP request"], 17 | ["NICK testbot", "Client sent NICK message"], 18 | ["USER nodebot 8 * :nodeJS IRC client", "Client sent USER message"], 19 | ["QUIT :node-irc says goodbye", "Client sent QUIT message"] 20 | ], 21 | 22 | "received": [ 23 | [":localhost 001 testbot :Welcome to the Internet Relay Chat Network testbot\r\n\r\n", "Received welcome message"] 24 | ] 25 | }, 26 | "parseline": { 27 | ":irc.dollyfish.net.nz 372 nodebot :The message of the day was last changed: 2012-6-16 23:57": { 28 | "prefix": "irc.dollyfish.net.nz", 29 | "server": "irc.dollyfish.net.nz", 30 | "command": "rpl_motd", 31 | "rawCommand": "372", 32 | "commandType": "reply", 33 | "args": ["nodebot", "The message of the day was last changed: 2012-6-16 23:57"] 34 | }, 35 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test :Hello nodebot!": { 36 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 37 | "nick": "Ned", 38 | "user": "~martyn", 39 | "host": "irc.dollyfish.net.nz", 40 | "command": "PRIVMSG", 41 | "rawCommand": "PRIVMSG", 42 | "commandType": "normal", 43 | "args": ["#test", "Hello nodebot!"] 44 | }, 45 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test ::-)": { 46 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 47 | "nick": "Ned", 48 | "user": "~martyn", 49 | "host": "irc.dollyfish.net.nz", 50 | "command": "PRIVMSG", 51 | "rawCommand": "PRIVMSG", 52 | "commandType": "normal", 53 | "args": ["#test", ":-)"] 54 | }, 55 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test ::": { 56 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 57 | "nick": "Ned", 58 | "user": "~martyn", 59 | "host": "irc.dollyfish.net.nz", 60 | "command": "PRIVMSG", 61 | "rawCommand": "PRIVMSG", 62 | "commandType": "normal", 63 | "args": ["#test", ":"] 64 | }, 65 | ":Ned!~martyn@irc.dollyfish.net.nz PRIVMSG #test ::^:^:": { 66 | "prefix": "Ned!~martyn@irc.dollyfish.net.nz", 67 | "nick": "Ned", 68 | "user": "~martyn", 69 | "host": "irc.dollyfish.net.nz", 70 | "command": "PRIVMSG", 71 | "rawCommand": "PRIVMSG", 72 | "commandType": "normal", 73 | "args": ["#test", ":^:^:"] 74 | }, 75 | ":some.irc.net 324 webuser #channel +Cnj 5:10": { 76 | "prefix": "some.irc.net", 77 | "server": "some.irc.net", 78 | "command": "rpl_channelmodeis", 79 | "rawCommand": "324", 80 | "commandType": "reply", 81 | "args": ["webuser", "#channel", "+Cnj", "5:10"] 82 | }, 83 | ":nick!user@host QUIT :Ping timeout: 252 seconds": { 84 | "prefix": "nick!user@host", 85 | "nick": "nick", 86 | "user": "user", 87 | "host": "host", 88 | "command": "QUIT", 89 | "rawCommand": "QUIT", 90 | "commandType": "normal", 91 | "args": ["Ping timeout: 252 seconds"] 92 | }, 93 | ":nick!user@host PRIVMSG #channel :so : colons: :are :: not a problem ::::": { 94 | "prefix": "nick!user@host", 95 | "nick": "nick", 96 | "user": "user", 97 | "host": "host", 98 | "command": "PRIVMSG", 99 | "rawCommand": "PRIVMSG", 100 | "commandType": "normal", 101 | "args": ["#channel", "so : colons: :are :: not a problem ::::"] 102 | }, 103 | ":nick!user@host PRIVMSG #channel :\u000314,01\u001fneither are colors or styles\u001f\u0003": { 104 | "prefix": "nick!user@host", 105 | "nick": "nick", 106 | "user": "user", 107 | "host": "host", 108 | "command": "PRIVMSG", 109 | "rawCommand": "PRIVMSG", 110 | "commandType": "normal", 111 | "args": ["#channel", "neither are colors or styles"], 112 | "stripColors": true 113 | }, 114 | ":nick!user@host PRIVMSG #channel :\u000314,01\u001fwe can leave styles and colors alone if desired\u001f\u0003": { 115 | "prefix": "nick!user@host", 116 | "nick": "nick", 117 | "user": "user", 118 | "host": "host", 119 | "command": "PRIVMSG", 120 | "rawCommand": "PRIVMSG", 121 | "commandType": "normal", 122 | "args": ["#channel", "\u000314,01\u001fwe can leave styles and colors alone if desired\u001f\u0003"], 123 | "stripColors": false 124 | }, 125 | ":pratchett.freenode.net 324 nodebot #ubuntu +CLcntjf 5:10 #ubuntu-unregged": { 126 | "prefix": "pratchett.freenode.net", 127 | "server": "pratchett.freenode.net", 128 | "command": "rpl_channelmodeis", 129 | "rawCommand": "324", 130 | "commandType": "reply", 131 | "args": ["nodebot", "#ubuntu", "+CLcntjf", "5:10", "#ubuntu-unregged"] 132 | } 133 | 134 | }, 135 | "_433before001": { 136 | "sent": [ 137 | ["CAP LS 302", "Client sent CAP request"], 138 | ["NICK testbot", "Client sent NICK message"], 139 | ["USER nodebot 8 * :nodeJS IRC client", "Client sent USER message"], 140 | ["NICK testbot1", "Client sent proper response to 433 nickname in use message"], 141 | ["QUIT :node-irc says goodbye", "Client sent QUIT message"] 142 | ], 143 | 144 | "received": [ 145 | [":localhost 433 * testbot :Nickname is already in use.\r\n", "Received nick in use error"], 146 | [":localhost 001 testbot1 :Welcome to the Internet Relay Chat Network testbot\r\n", "Received welcome message"] 147 | ], 148 | "clientInfo": [ 149 | "hostmask is as expected after 433", 150 | "nick is as expected after 433", 151 | "maxLineLength is as expected after 433" 152 | ] 153 | }, 154 | "convertEncoding": { 155 | "causesException": [ 156 | ":ubottu!ubottu@ubuntu/bot/ubottu MODE #ubuntu -bo *!~Brian@* ubottu\r\n", 157 | "Elizabeth", 158 | ":sblack1!~sblack1@unaffiliated/sblack1 NICK :sblack\r\n", 159 | ":TijG!~TijG@null.1ago.be PRIVMSG #ubuntu :ThinkPad\r\n" 160 | ] 161 | }, 162 | "_splitLongLines": [ 163 | { 164 | "input": "abcde ", 165 | "maxLength": 5, 166 | "result": ["abcde"] 167 | }, 168 | { 169 | "input": "abcde", 170 | "maxLength": 5, 171 | "result": ["abcde"] 172 | }, 173 | { 174 | "input": "abcdefghijklmnopqrstuvwxyz", 175 | "maxLength": 5, 176 | "result": ["abcde", "fghij", "klmno", "pqrst", "uvwxy", "z"] 177 | }, 178 | { 179 | "input": "abc abcdef abc abcd abc", 180 | "maxLength": 5, 181 | "result": ["abc", "abcde", "f abc", "abcd", "abc"] 182 | }, 183 | { 184 | "input": "æøå", 185 | "maxLength": 4, 186 | "result": ["æø", "å"] 187 | }, 188 | { 189 | "input": "æøå", 190 | "maxLength": 5, 191 | "result": ["æø", "å"] 192 | }, 193 | { 194 | "input": "\u000308\u0002\u0002\u0002bold and yellow\u0002\u0003", 195 | "maxLength": 12, 196 | "result": ["\u000308\u0002\u0002\u0002bold", "and yellow\u0002\u0003"] 197 | }, 198 | { 199 | "input": "abc ", 200 | "maxLength": 5, 201 | "result": ["abc "] 202 | }, 203 | { 204 | "input": " abc", 205 | "maxLength": 5, 206 | "result": [" abc"] 207 | }, 208 | { 209 | "input": " abc ", 210 | "maxLength": 5, 211 | "result": [" abc "] 212 | }, 213 | { 214 | "input": " abc abc abc abc ", 215 | "maxLength": 5, 216 | "result": [" abc", "abc", "abc", "abc "] 217 | }, 218 | { 219 | "input": "αβγδε αβγδaε αβγδεa", 220 | "maxLength": 6, 221 | "result": ["αβγ", "δε", "αβγ", "δaε", "αβγ", "δεa"] 222 | }, 223 | { 224 | "input": "αβγδε", 225 | "maxLength": 5, 226 | "result": ["αβ", "γδ", "ε"] 227 | }, 228 | { 229 | "input": "abcdefg 😸😹😺😻 😸😹a😺😻", 230 | "maxLength": 9, 231 | "result": ["abcdefg", "😸😹", "😺😻", "😸😹a", "😺😻"] 232 | } 233 | ] 234 | } 235 | -------------------------------------------------------------------------------- /test/data/ircd.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKgIBAAKCAgEAtLd9Ya8qzsdvNZVgzC4JZjs+hm6thnGFFXOifp0t3Ue6cZLD 3 | SuT01FLA36INoZhtxXFsv+d+8sS5bZuV0D+q2aJBJiFgvC4I+r4mN7BwiMjDwV8R 4 | VG6qdufG6n4jipdKx5GjL9Xr4Duelb1ya+mjeS5v7eUdwNs2L6VyWbOs4jin9/Cn 5 | VMsz3ArvNvcuuolMtRWKniX0j2LyHHhjQLp/OubUYXVDe7oDJAFk10DMbjzpUPtj 6 | O7gpu7L31M9hF5nf0lfTV38vrZ/gLk/ZDu3FfnJ1aqGzeTzWQbFnEYqUhQdEhfhr 7 | aHjnmaBjA3+8ugXiDCrkX4/ZZBil7BMzcq0ovB31AThGDM7mFFXL4zSFkCD4mviS 8 | 2zrf0ZjjPaOLCIyVMKIixHmtAIs+UPgqwQ8GOjaJcGNljuc5m5xJW2BHPrDd7tQ0 9 | J0Lu0sn2fias10V+5v79++i2FxekLiM4jKPzUauAoLPGZELrpKCyizMJOIlv174S 10 | 9FlVhUO1ccgWDQ7ux1l0UH8JzwOvFAc57dVD527ELoLmupTqqJtbX1Fdc1N4tFoD 11 | pKdr/exohYYB/Fu6jjF4JW/YHbl6tYCgqjngODn8k45MlWe94lGDNuVMk0/42Nk7 12 | ksSBILmADeRR0bDFB51vP87IEeuFr/tzVntteaYKc7Dn95coyUfKExMl0JcCAwEA 13 | AQKCAgAGsfWXNOIlHwZjudEIP3xhqTg7ysXrATGpBcuzXSdh11J0+rb5g1n+s8Ip 14 | httybS9D7VvWEEGHxPoJsYXvXSx7O6OmQf5PenUitQC9d2/z4Vw/QcJmmmL+XL/l 15 | 2B6A9/HxStf84bQHbq4FZitjDBjeWHYVHjPn/TcYtMxzvlBdYTP335aTcaPONyl9 16 | o9K7XnLVEqM8ELPqzAOkQmGK+F3WVM7xfWKupsmO/+44e1IXk3Ihae7XO49wQMUl 17 | wTkborvEEzTlPPULPa0UiijEgNKcSKlI9gysJTDa5jOnVrcB5q8HN5jjGfeanXKN 18 | oqHfUnB5eu1TDQVEzBT5lgyF5xxnK8BrMuPC2Gnd8ZKDNWUvX3xUVkJJS19SkWvn 19 | oRHT0hH3QhrXkOCajLPfke4FIlFjjqi06WxL9ZwQdeF0mOpKruTAjNFsIfzrtHQv 20 | QyVBOLshlxUAQvwIe0Aj4497maKib38kGsIgzSMzbyl3QNu4OP0hHwPNgdPK/uDy 21 | o5Wzm0mn+8dxu8j+IrDYBASu4aHgW2qTak5G1Xi5EOKWE9SC3/DQ5flWqfzwGq+6 22 | MpnBS/nRwzmjkQrEPbkeA61RIniwND87X0ULMJsMUfyPPHcWW5SbRjLFTVpMJte9 23 | oBuWIP3qqfYWxbfDnmQ+SbX787C3SWuROhbw3HIURWnGOS5YgQKCAQEA7cqGGK5f 24 | AofkTXhOEYkq1CPh2mJY8V6xxbHqP5/ZrkuGmOsaMtgYaYrpqRKLA4LaDArUf3Nj 25 | MxNBnwMwzfbpY5p4NkiIiezI8WKPJox6acWULE3pxbbqeL9KXdaSOKVzwgLh8Ntb 26 | IUFVhqj/Ls6Lhj3Ng7GT9Z2ejYEFOgdLpIpz6kmXMycjEEBSSd+TX3lbWox/kR4A 27 | JAPcfSg2D3ri+Pk8GyNxOV690zgr3Bhi0lZ7ORK/sN7yf9Bspjf+jnqNQnZMK5C5 28 | 9kH+CQEtfoIhs5X0zfMcXWU3aZmNHgFfoT3FDyw1GSHgRthBD34o3Dd0jbIMkzDF 29 | Y+HW+QOjoiXtUQKCAQEAwo4frWiun5Zz/igPc+tx4b0fwEkEbo2SdiXT65FB25Gk 30 | i7zCxH6CWmGOV1yh+fSgsCkKKdQUunk3n3HEQkyUcFP4WWO3qvqwtm2VqTExoXxY 31 | 6nRAh8NXtt47hpvYj8Ku35/y25DUEaOAMmdXeZU8wNa4ZOEcUQZCXujFfLrpzIaQ 32 | b6Yi0HK010iDpu08sZlqM/YDn118DJKaLq9DkpZdRkll0PxkAHCP2Rnni/k3Fljs 33 | 5yrm8H5OLbaK+8wd/C4jEOOGYkSqWMllOn9gmE/W+G+p+Ba/xgtN/T/dJzAoMa9c 34 | t4N93d6RJwL55FZtIQnYKi3y9g3DxHg3Gxs+oQTFZwKCAQEAwr99N7WHpqD4/+Gp 35 | vn7ijr+cd6jYQ0ZUvh7KRLV8KF0+rPrPiBinVbkpSQkgxQ1j2zz7cC5mbiw1MDAC 36 | xoyT9LlL/tlEygEdSWR47Q9cKkhg5DAjZ4Q5YA76rwPO2YnX1mtZ9FMSvZeungzG 37 | geUzLAxtxo+nKB+g/S9Pwoi7ENU7vgPrSz+gXezv+ASdxDG1+eDbkVRKtTRcXjyS 38 | mfcA8PvemDNcxamsOdLlSOrH9JBTdxi92fOeE8P1V+TAHJyOGIKeO4faZa8CiQln 39 | 4xZc16HWzt1uu6brzRavFoX1di8KtzRzgFPYRO1Ty4Z9nG3mjS3nUp087GLIF0U9 40 | vMznIQKCAQEApEf0Ua4iPdmCSmszWTPHbtEOvYQqfNuIf8FDaBe435nksqYKZHda 41 | xMy5r+UlVPYOtZGB5n4Rnr/6iuU6zqzxbsRI4dpE3dhfXTu9cyd5/B0Oy7KsRrdZ 42 | Gq4e33Q7cnD2zxe1r1dk6xv/hRAkGiM9MKxe+bfn/Dbn1lKBZ+hAwZYi4lQL863Y 43 | LC0sFckfRewAdK3YsznyJH+qN5+A1IepbU9O7SAhpQlnPfAUx+oBbRpbuHtOlGZi 44 | x1DrnODntOiUbY9iCxpmKSCuHK4wN4y7Pf60LCuxdZ5YFW9W499TIVktVjxvDOkB 45 | 8koeDoQ4E/zHDh7MmJ5Y306PYZEo2jg4IwKCAQEAi6VCAmNLDawjTVTZM1ZYYFUg 46 | mBPeUc2hOIxdoqSDbm0eVBQg0n6gOMxJYy/O99J/n2rffa+PGysGVHi3Innp3SBe 47 | fsnloHA1T0ldP8Pah3dQqH/28Qacs+i3jYU+kZ+j0JKQ73EwWENvbt7l2+Wm4wPm 48 | L08axZwv1/bbnbw2NDk4bxtFjNNW8FUW4hlBsDFhe3KwJrt9AfQnrdDp6j1O6+gr 49 | rqcclE2JVh+JdYmReXhzdyxzJfKU9snoFWn4c7gWZkvFvcO3PDEY1ifveuyJ8xfR 50 | CNSuBB7KwnKLL4PHMM3IKboeybuhW8t9X2znykBNaip0eKXQxJ8t+GQCf3Epjw== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /test/data/ircd.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFbTCCA1WgAwIBAgIUOYynUbNzP3Hoix87g3aUWfZdMxwwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0xOTEwMjgxMzIyMTdaGA8zMDE5 5 | MDIyODEzMjIxN1owRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx 6 | ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN 7 | AQEBBQADggIPADCCAgoCggIBALS3fWGvKs7HbzWVYMwuCWY7PoZurYZxhRVzon6d 8 | Ld1HunGSw0rk9NRSwN+iDaGYbcVxbL/nfvLEuW2bldA/qtmiQSYhYLwuCPq+Jjew 9 | cIjIw8FfEVRuqnbnxup+I4qXSseRoy/V6+A7npW9cmvpo3kub+3lHcDbNi+lclmz 10 | rOI4p/fwp1TLM9wK7zb3LrqJTLUVip4l9I9i8hx4Y0C6fzrm1GF1Q3u6AyQBZNdA 11 | zG486VD7Yzu4Kbuy99TPYReZ39JX01d/L62f4C5P2Q7txX5ydWqhs3k81kGxZxGK 12 | lIUHRIX4a2h455mgYwN/vLoF4gwq5F+P2WQYpewTM3KtKLwd9QE4RgzO5hRVy+M0 13 | hZAg+Jr4kts639GY4z2jiwiMlTCiIsR5rQCLPlD4KsEPBjo2iXBjZY7nOZucSVtg 14 | Rz6w3e7UNCdC7tLJ9n4mrNdFfub+/fvothcXpC4jOIyj81GrgKCzxmRC66Sgsosz 15 | CTiJb9e+EvRZVYVDtXHIFg0O7sdZdFB/Cc8DrxQHOe3VQ+duxC6C5rqU6qibW19R 16 | XXNTeLRaA6Sna/3saIWGAfxbuo4xeCVv2B25erWAoKo54Dg5/JOOTJVnveJRgzbl 17 | TJNP+NjZO5LEgSC5gA3kUdGwxQedbz/OyBHrha/7c1Z7bXmmCnOw5/eXKMlHyhMT 18 | JdCXAgMBAAGjUzBRMB0GA1UdDgQWBBQ1VDcq9WZZZnvnCuOwkYTKRywVwDAfBgNV 19 | HSMEGDAWgBQ1VDcq9WZZZnvnCuOwkYTKRywVwDAPBgNVHRMBAf8EBTADAQH/MA0G 20 | CSqGSIb3DQEBCwUAA4ICAQAuJG2opo+1rLeQw1hbUH3zCn9DFkhWhK2eL+WMBPAV 21 | S0EjhrH9A9un0keY9fLYIyUK6btaqc29tcAHzh3y6Gd9lqZvXlR8uCJNyCxNQ4KZ 22 | 6op1seEJJ/JymQh83eICxgZ6k3LQhtq6rMMCJQKDWVHUdXAOALnQsF+xDwvMsZZR 23 | nfJQ0b+VmZDvqrdsjjTl9ticnT4FLrl+QENBnTWK+uIfKtSD39uNmfzxyfIR0yHo 24 | RBDre2TZMa4TODnjB7XTNQdxPKODEvh98E9tP+tMgxz9aiw6WlWmXh4WxLsrdQJQ 25 | vVs/iKu6U/roGZfJbGzL9PpZcJYwtfQTFianQPIepshZKrVGKnscCPPZ4iZU/Qpf 26 | 2jWHizLvzQm1IsQePNdZqt61IOx0hQ9HXtB9NAHW2ljscnTV3vtBL42UTjFrYeXN 27 | exa5ZrerDKqEXK0wyZInoIgJKG2eqH4oC70+14xk2hv6f23N8zaObXsskhDeoZtd 28 | v/GGiE54mfpIIYcRuOHWCQZMZyTWZIgSVDE/h5D4PMjIRZt66mfNZjPNMkTA30rm 29 | qKI7kosDLe6BAV8u4ShMDExPMrmc5Lk+z7EkW5PFj7Edc62vacfO33p41S1IsSYQ 30 | r0CDDCKkF2PeIr9xyZ5l9mK1RspRVduBIaDVWi1zJ3YVgZdHlbqYmWUveGKGeCbN 31 | PA== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import * as path from "node:path"; 3 | import * as fs from "node:fs"; 4 | import * as net from "node:net"; 5 | import * as tls from "node:tls"; 6 | import EventEmitter from "node:events"; 7 | import { AddressInfo } from "node:net"; 8 | 9 | export class MockIrcd extends EventEmitter { 10 | incoming: string[] = []; 11 | outgoing: unknown[] = []; 12 | server: net.Server; 13 | 14 | constructor(public readonly encoding: BufferEncoding = "utf-8", isSecure = false) { 15 | super(); 16 | let connectionFn; 17 | let options = {}; 18 | 19 | if (isSecure) { 20 | connectionFn = (opts: any, f: (c: net.Socket) => void) => tls.createServer(opts, f); 21 | options = { 22 | key: fs.readFileSync(path.resolve(__dirname, 'data/ircd.key')), 23 | cert: fs.readFileSync(path.resolve(__dirname, 'data/ircd.pem')) 24 | }; 25 | } 26 | else { 27 | connectionFn = (opts: any, f: (c: net.Socket) => void) => net.createServer(opts, f); 28 | } 29 | 30 | this.server = connectionFn(options, (c) => { 31 | c.on('data', (data) => { 32 | const msg = data.toString(encoding).split('\r\n').filter(function(m) { return m; }); 33 | this.incoming = this.incoming.concat(msg); 34 | }); 35 | 36 | this.on('send', (data) => { 37 | this.outgoing.push(data); 38 | c.write(data); 39 | }); 40 | 41 | c.on('end', () => { 42 | this.emit('end'); 43 | }); 44 | }); 45 | } 46 | 47 | async listen() { 48 | return new Promise((resolve) => { 49 | this.server.listen(0, () => { 50 | resolve((this.server.address() as AddressInfo).port) 51 | }); 52 | }); 53 | } 54 | 55 | send(data: string) { 56 | this.emit('send', data); 57 | } 58 | 59 | close() { 60 | this.server.close(); 61 | } 62 | 63 | getIncomingMsgs() { 64 | return this.incoming; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/test-433-before-001.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockIrcd } from './helpers'; 2 | import { Client } from "../src"; 3 | import { test, expect } from "@jest/globals"; 4 | import { _433before001 as fixtures } from "./data/fixtures.json"; 5 | 6 | test('connect and sets hostmask when nick in use', async () => { 7 | 8 | const mock = new MockIrcd(); 9 | const client = new Client('localhost', 'testbot', {debug: true, port: await mock.listen()}); 10 | 11 | mock.server.on('connection', function() { 12 | mock.send(':localhost 433 * testbot :Nickname is already in use.\r\n') 13 | mock.send(':localhost 001 testbot1 :Welcome to the Internet Relay Chat Network testbot\r\n'); 14 | }); 15 | 16 | client.on('registered', function() { 17 | expect(mock.outgoing[0]).toEqual(fixtures.received[0][0]); 18 | expect(mock.outgoing[1]).toEqual(fixtures.received[1][0]); 19 | client.disconnect(function() { 20 | expect(client.hostMask).toEqual('testbot'); 21 | expect(client.nick).toEqual('testbot1'); 22 | expect(client.maxLineLength).toEqual(482); 23 | }); 24 | }); 25 | 26 | mock.on('end', function() { 27 | mock.close(); 28 | const msgs = mock.getIncomingMsgs(); 29 | 30 | for (let i = 0; i < msgs.length; i++) { 31 | expect(msgs[i]).toEqual(fixtures.sent[i][0]); 32 | } 33 | 34 | expect.assertions(fixtures.sent.length + fixtures.received.length + fixtures.clientInfo.length); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/test-auditorium.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockIrcd } from './helpers'; 2 | import { Client } from "../src"; 3 | import { test, expect} from "@jest/globals"; 4 | 5 | test('user gets opped in auditorium', async () => { 6 | const mock = new MockIrcd(); 7 | const client = new Client('localhost', 'testbot', {debug: true, port: await mock.listen()}); 8 | 9 | 10 | mock.server.on('connection', function() { 11 | // Initiate connection 12 | mock.send(':localhost 001 testbot :Welcome to the Internet Relay Chat Network testbot\r\n'); 13 | 14 | // Set prefix modes 15 | mock.send(':localhost 005 testbot PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server\r\n'); 16 | 17 | // Force join into auditorium 18 | mock.send(':testbot JOIN #auditorium\r\n'); 19 | 20 | // +o the invisible user 21 | mock.send(':ChanServ MODE #auditorium +o user\r\n'); 22 | }); 23 | 24 | mock.on('end', function() { 25 | mock.close(); 26 | }); 27 | 28 | await new Promise((resolve) => { 29 | client.on('+mode', (channel: string, by: string, mode: string, argument?: string) => { 30 | expect(channel).toEqual('#auditorium'); 31 | expect(argument).toEqual('user'); 32 | resolve(); 33 | }); 34 | }); 35 | 36 | client.disconnect(); 37 | }); 38 | -------------------------------------------------------------------------------- /test/test-convert-encoding.spec.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "../src"; 2 | import { describe, test, expect } from "@jest/globals"; 3 | import { convertEncoding as checks } from "./data/fixtures.json"; 4 | 5 | const bindTo = { opt: { encoding: 'utf-8' } }; 6 | 7 | describe('Client.convertEncoding', () => { 8 | test.each(checks.causesException)("new implementation should not throw with string '%s'", (line) => { 9 | const client = new Client('localhost', 'test', { autoConnect: false }); 10 | const convertEncoding = client.convertEncoding.bind(bindTo); 11 | expect(() => convertEncoding(Buffer.from(line))).not.toThrow(); 12 | }); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /test/test-double-crlf.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { MockIrcd } from './helpers'; 3 | import { Client } from "../src"; 4 | import { test, expect } from "@jest/globals"; 5 | import { doubleCRLF as doubleCRLFFixture } from "./data/fixtures.json"; 6 | 7 | test('sent messages ending with double CRLF', async () => { 8 | const mock = new MockIrcd(); 9 | const port = await mock.listen(); 10 | const client = new Client('localhost', 'testbot', { debug: true, port }); 11 | 12 | const expected = doubleCRLFFixture; 13 | 14 | mock.server.on('connection', function() { 15 | mock.send(expected.received[0][0]); 16 | }); 17 | 18 | client.on('registered', function() { 19 | expect(mock.outgoing[0]).toEqual(expected.received[0][0]); 20 | client.disconnect(); 21 | }); 22 | 23 | mock.on('end', function() { 24 | mock.close(); 25 | const msgs = mock.getIncomingMsgs(); 26 | 27 | for (let i = 0; i < msgs.length; i++) { 28 | expect(msgs[i]).toEqual(expected.sent[i][0]); 29 | } 30 | 31 | expect.assertions(expected.sent.length + expected.received.length); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/test-irc.spec.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "../src"; 2 | import { MockIrcd } from "./helpers"; 3 | import { basic as expected } from "./data/fixtures.json"; 4 | import { describe, test, expect } from "@jest/globals"; 5 | 6 | const greeting = ':localhost 001 testbot :Welcome to the Internet Relay Chat Network testbot\r\n'; 7 | 8 | 9 | describe('IRC client basics', () => { 10 | test.each([ 11 | ['connect, register and quit', false, false], 12 | ['connect, register and quit, securely', true, false], 13 | ['connect, register and quit, securely, with secure object', true, true], 14 | ])('%s', async (_name, isSecure, useSecureObject) => { 15 | const mock = new MockIrcd("utf-8", isSecure); 16 | const port = await mock.listen(); 17 | let client: Client; 18 | if (isSecure) { 19 | client = new Client( useSecureObject ? 'notlocalhost' : 'localhost', 'testbot', { 20 | secure: useSecureObject ? { 21 | host: 'localhost', 22 | port: port, 23 | rejectUnauthorized: false 24 | } : true, 25 | port, 26 | selfSigned: true, 27 | retryCount: 0, 28 | debug: true 29 | }); 30 | } 31 | else { 32 | client = new Client('localhost', 'testbot', { 33 | secure: isSecure, 34 | selfSigned: true, 35 | port: port, 36 | retryCount: 0, 37 | debug: true 38 | }); 39 | } 40 | 41 | mock.server.on(isSecure ? 'secureConnection' : 'connection', function() { 42 | mock.send(greeting); 43 | }); 44 | 45 | client.on('registered', function() { 46 | expect(mock.outgoing[0]).toEqual(expected.received[0][0]); 47 | client.disconnect(); 48 | }); 49 | 50 | mock.on('end', function() { 51 | mock.close(); 52 | const msgs = mock.getIncomingMsgs(); 53 | 54 | for (let i = 0; i < msgs.length; i++) { 55 | expect(msgs[i]).toEqual(expected.sent[i][0]); 56 | } 57 | 58 | expect.assertions(expected.sent.length + expected.received.length); 59 | }); 60 | }); 61 | 62 | test('part reasons', async () => { 63 | const mock = new MockIrcd(); 64 | const client = new Client('localhost', 'testbot', {debug: true, port: await mock.listen()}); 65 | 66 | mock.server.on('connection', function() { 67 | mock.send(greeting); 68 | }); 69 | 70 | client.on('registered', async () => { 71 | await client.part('#testchannel', 'bye'); 72 | client.disconnect(); 73 | }); 74 | 75 | await new Promise((resolve, reject) => { 76 | mock.on('end', function() { 77 | mock.close(); 78 | const msgs = mock.getIncomingMsgs(); 79 | 80 | try { 81 | expect(msgs.filter(msg => msg.startsWith('PART') && msg.endsWith('bye'))).toHaveLength(1); 82 | } catch (err) { 83 | reject(err); 84 | } 85 | resolve(); 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/test-parse-line.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseMessage } from "../src"; 2 | import { parseline } from "./data/fixtures.json"; 3 | import { describe, test, expect } from "@jest/globals"; 4 | 5 | describe('parseMessage', () => { 6 | test.each( 7 | Object.entries(parseline) 8 | )("can parse '%s'", (line, resultWithOpts) => { 9 | const result: typeof resultWithOpts&{stripColors?: boolean} = resultWithOpts; 10 | let stripColors = false; 11 | if ('stripColors' in result) { 12 | stripColors = result.stripColors ?? false; 13 | delete result.stripColors; 14 | } 15 | 16 | expect( 17 | parseMessage(line, stripColors) 18 | ).toEqual( 19 | result 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/test-split-messages.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from '@jest/globals'; 2 | import { Client } from '../src'; 3 | 4 | describe('Client', () => { 5 | test('can split messages', () => { 6 | const client = new Client('localhost', 'test', { autoConnect: false }); 7 | expect(client.getSplitMessages('#chan', 'foo\nbar\nbaz')).toEqual(['foo', 'bar', 'baz']); 8 | expect(client.getSplitMessages('#chan', 'foo\r\nbar\r\nbaz')).toEqual(['foo', 'bar', 'baz']); 9 | expect(client.getSplitMessages('#chan', 'foo\rbar\rbaz')).toEqual(['foo', 'bar', 'baz']); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": [], 4 | "include": [ 5 | "**/*.spec.ts" 6 | ] 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "ES2019", 5 | "module": "commonjs", 6 | "allowJs": true, 7 | "checkJs": false, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "outDir": "./lib", 11 | "composite": false, 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "strictNullChecks": true, 15 | "baseUrl": "./src", 16 | "paths": { 17 | "*": ["./typings/*"] 18 | }, 19 | "resolveJsonModule": true 20 | }, 21 | "include": [ 22 | "src/**/*" 23 | ], 24 | "exclude": [ 25 | "test/**/*", 26 | "example/**/*", 27 | "app.js" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------