├── .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 | [](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 |
--------------------------------------------------------------------------------