@sockethub/platform-irc 4.0.0-alpha.9 → 4.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,30 +8,47 @@ This platform provides IRC client functionality, allowing web applications to co
8
8
  IRC servers, join channels, send messages, and handle IRC events through ActivityStreams
9
9
  messages.
10
10
 
11
- ## Implemented Verbs (`@type`)
11
+ ## Implemented Verbs (`type`)
12
12
 
13
+ * **connect** - Open the IRC connection after credentials are stored
13
14
  * **send** - Send messages to channels or users
14
15
  * **join** - Join IRC channels
15
16
  * **leave** - Leave IRC channels
16
- * **observe** - Monitor channel activity and user events
17
- * **update** - Update user information (nick, etc.)
17
+ * **update** - Change a nick (`address`) or a channel topic (`topic`)
18
+ * **query** - List who is in a channel, or list channels on the server
19
+ * **disconnect** - Close the IRC connection
20
+
21
+ Incoming chat, joins, presence, and nick changes arrive as messages. There is
22
+ no separate observe command. `/me` is a `send` whose `object.content` starts
23
+ with `/me`.
18
24
 
19
25
  ## Usage
20
26
 
27
+ IRC room targets use `#channel@server` in `target.id` (for example
28
+ `#general@irc.libera.chat`). When you include `target.name`, use the channel
29
+ name as it appears on IRC (for example `#general`).
30
+
21
31
  ### Send Message Example
22
32
 
23
33
  ```json
24
34
  {
25
- "@type": "send",
26
- "context": "irc",
35
+ "type": "send",
36
+ "@context": [
37
+ "https://www.w3.org/ns/activitystreams",
38
+ "https://sockethub.org/ns/context/v1.jsonld",
39
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
40
+ ],
27
41
  "actor": {
28
- "@id": "mynick@irc.libera.chat"
42
+ "id": "mynick@irc.libera.chat",
43
+ "type": "person"
29
44
  },
30
45
  "target": {
31
- "@id": "#general@irc.libera.chat"
46
+ "id": "#general@irc.libera.chat",
47
+ "type": "room",
48
+ "name": "#general"
32
49
  },
33
50
  "object": {
34
- "@type": "Note",
51
+ "type": "message",
35
52
  "content": "Hello IRC channel!"
36
53
  }
37
54
  }
@@ -41,13 +58,20 @@ messages.
41
58
 
42
59
  ```json
43
60
  {
44
- "@type": "join",
45
- "context": "irc",
61
+ "type": "join",
62
+ "@context": [
63
+ "https://www.w3.org/ns/activitystreams",
64
+ "https://sockethub.org/ns/context/v1.jsonld",
65
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
66
+ ],
46
67
  "actor": {
47
- "@id": "mynick@irc.libera.chat"
68
+ "id": "mynick@irc.libera.chat",
69
+ "type": "person"
48
70
  },
49
71
  "target": {
50
- "@id": "#general@irc.libera.chat"
72
+ "id": "#general@irc.libera.chat",
73
+ "type": "room",
74
+ "name": "#general"
51
75
  }
52
76
  }
53
77
  ```
@@ -56,21 +80,370 @@ messages.
56
80
 
57
81
  ```json
58
82
  {
59
- "@type": "send",
60
- "context": "irc",
83
+ "type": "send",
84
+ "@context": [
85
+ "https://www.w3.org/ns/activitystreams",
86
+ "https://sockethub.org/ns/context/v1.jsonld",
87
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
88
+ ],
61
89
  "actor": {
62
- "@id": "mynick@irc.libera.chat"
90
+ "id": "mynick@irc.libera.chat",
91
+ "type": "person"
63
92
  },
64
93
  "target": {
65
- "@id": "friend@irc.libera.chat"
94
+ "id": "friend@irc.libera.chat",
95
+ "type": "person"
66
96
  },
67
97
  "object": {
68
- "@type": "Note",
98
+ "type": "message",
69
99
  "content": "Hello friend!"
70
100
  }
71
101
  }
72
102
  ```
73
103
 
104
+ ### Update Topic Example
105
+
106
+ ```json
107
+ {
108
+ "type": "update",
109
+ "@context": [
110
+ "https://www.w3.org/ns/activitystreams",
111
+ "https://sockethub.org/ns/context/v1.jsonld",
112
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
113
+ ],
114
+ "actor": {
115
+ "id": "mynick@irc.libera.chat",
116
+ "type": "person"
117
+ },
118
+ "target": {
119
+ "id": "#general@irc.libera.chat",
120
+ "type": "room",
121
+ "name": "#general"
122
+ },
123
+ "object": {
124
+ "type": "topic",
125
+ "content": "New channel topic"
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Update Nickname Example
131
+
132
+ ```json
133
+ {
134
+ "type": "update",
135
+ "@context": [
136
+ "https://www.w3.org/ns/activitystreams",
137
+ "https://sockethub.org/ns/context/v1.jsonld",
138
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
139
+ ],
140
+ "actor": {
141
+ "id": "mynick@irc.libera.chat",
142
+ "type": "person"
143
+ },
144
+ "target": {
145
+ "id": "newnick@irc.libera.chat",
146
+ "type": "person",
147
+ "name": "newnick"
148
+ },
149
+ "object": {
150
+ "type": "address"
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### Nick changes
156
+
157
+ `target.name` is the nick sent in `NICK`. `target.id` is that nick as a person
158
+ id, `newnick@server`. On success the connection is re-keyed to that actor.
159
+ Later commands must use the new `actor.id`.
160
+
161
+ The client library moves stored credentials, connect, and join entries when
162
+ the update is acknowledged without an error, and when the server reports a
163
+ rename of an actor this session holds. The credential object stays as
164
+ submitted (`nick`, `password`, `token`, and the rest). The live connection is
165
+ selected by a fingerprint of that original object, so a reconnect that also
166
+ rewrites `nick` misses it and opens another session. See the
167
+ [client guide](../../docs/client-guide.md#irc-nick-changes).
168
+
169
+ A callback or message that contains `error` means the nick did not change.
170
+ Keep the previous actor. That includes:
171
+
172
+ * Another account is already stored for the target actor, in this session or
173
+ in another session on the same connection. `NICK` is not sent. The error is
174
+ `cannot rename <old-actor> to <new-actor>: credentials already stored for <new-actor>`.
175
+ The same credential object already stored at the new actor is not a
176
+ collision.
177
+ * The IRC server rejects the nick. The callback `error` is the server's
178
+ reason text. These numeric replies fail the command instead of leaving it
179
+ waiting: 431 (no nickname), 432 (erroneous nickname), 433 (nickname in
180
+ use), 436 (collision), 437 (temporarily unavailable), and 438 (nick change
181
+ too fast; sent by solanum, ircu, and Unreal).
182
+
183
+ A server-forced rename of this connection arrives as an `update` with
184
+ `object.type` `address`, `actor` the previous identity and `target` the new
185
+ one. If Sockethub cannot re-key the worker, that message includes `error`
186
+ and the session stays on the previous actor. Other users' nick changes use
187
+ the same shape and are delivered as ordinary messages.
188
+
189
+ ### Query Attendance Example
190
+
191
+ ```json
192
+ {
193
+ "type": "query",
194
+ "@context": [
195
+ "https://www.w3.org/ns/activitystreams",
196
+ "https://sockethub.org/ns/context/v1.jsonld",
197
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
198
+ ],
199
+ "actor": {
200
+ "id": "mynick@irc.libera.chat",
201
+ "type": "person"
202
+ },
203
+ "target": {
204
+ "id": "#general@irc.libera.chat",
205
+ "type": "room",
206
+ "name": "#general"
207
+ },
208
+ "object": {
209
+ "type": "attendance"
210
+ }
211
+ }
212
+ ```
213
+
214
+ ### Query Attendance Response Example
215
+
216
+ ```json
217
+ {
218
+ "type": "query",
219
+ "@context": [
220
+ "https://www.w3.org/ns/activitystreams",
221
+ "https://sockethub.org/ns/context/v1.jsonld",
222
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
223
+ ],
224
+ "actor": {
225
+ "id": "#general@irc.libera.chat",
226
+ "type": "room",
227
+ "name": "#general"
228
+ },
229
+ "target": {
230
+ "id": "mynick@irc.libera.chat",
231
+ "type": "person",
232
+ "name": "mynick"
233
+ },
234
+ "object": {
235
+ "type": "attendance",
236
+ "members": ["alice", "bob", "carol"],
237
+ "roles": { "alice": "owner", "bob": "participant" }
238
+ }
239
+ }
240
+ ```
241
+
242
+ `roles` maps a nick to `owner` (channel op, admin or founder), `admin` (half-op)
243
+ or `participant` (voiced). Nicks absent from it are plain members.
244
+
245
+ ### Query Channels Example
246
+
247
+ Lists channels on the server. `object.name` is an optional `LIST` mask (one
248
+ parameter, no spaces). Rows use the server's `CHANTYPES` (`#` and `&` by
249
+ default). The server still sends every row; the response is capped at 5000
250
+ items and includes `"truncated": true` when rows were dropped.
251
+
252
+ ```json
253
+ {
254
+ "type": "query",
255
+ "@context": [
256
+ "https://www.w3.org/ns/activitystreams",
257
+ "https://sockethub.org/ns/context/v1.jsonld",
258
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
259
+ ],
260
+ "actor": {
261
+ "id": "mynick@irc.libera.chat",
262
+ "type": "person"
263
+ },
264
+ "target": {
265
+ "id": "irc.libera.chat",
266
+ "type": "service"
267
+ },
268
+ "object": {
269
+ "type": "channels",
270
+ "name": "#sockethub*"
271
+ }
272
+ }
273
+ ```
274
+
275
+ ### Query Channels Response Example
276
+
277
+ One message is sent once the server finishes the list.
278
+
279
+ ```json
280
+ {
281
+ "type": "query",
282
+ "@context": [
283
+ "https://www.w3.org/ns/activitystreams",
284
+ "https://sockethub.org/ns/context/v1.jsonld",
285
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
286
+ ],
287
+ "actor": {
288
+ "id": "irc.libera.chat",
289
+ "type": "service",
290
+ "name": "irc.libera.chat"
291
+ },
292
+ "object": {
293
+ "type": "channels",
294
+ "items": [
295
+ {
296
+ "id": "#sockethub@irc.libera.chat",
297
+ "type": "room",
298
+ "name": "#sockethub",
299
+ "members": 12,
300
+ "summary": "Sockethub development"
301
+ }
302
+ ]
303
+ }
304
+ }
305
+ ```
306
+
307
+ ### Disconnect Example
308
+
309
+ ```json
310
+ {
311
+ "type": "disconnect",
312
+ "@context": [
313
+ "https://www.w3.org/ns/activitystreams",
314
+ "https://sockethub.org/ns/context/v1.jsonld",
315
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
316
+ ],
317
+ "actor": {
318
+ "id": "mynick@irc.libera.chat",
319
+ "type": "person"
320
+ }
321
+ }
322
+ ```
323
+
324
+ ## Authentication
325
+
326
+ Authenticated connections use SASL. Two mechanisms are supported:
327
+
328
+ * **`PLAIN`** — sends a username and secret via SASL PLAIN. This is the
329
+ default mechanism, and is supported by most IRC networks (Libera.Chat,
330
+ OFTC, etc.). Both `password` and `token` use PLAIN by default. Use
331
+ `token` for personal access tokens (e.g. Libera.Chat NickServ tokens)
332
+ to avoid storing primary account passwords.
333
+ * **`OAUTHBEARER`** — sends an OAuth 2.0 access token instead of a password
334
+ ([RFC 7628](https://datatracker.ietf.org/doc/html/rfc7628)). Requires
335
+ `saslMechanism: "OAUTHBEARER"` to be set explicitly. Adoption on
336
+ public IRC networks is still limited; the main deployment is SourceHut's
337
+ `chat.sr.ht` (via the [soju](https://soju.im/) bouncer). Major networks
338
+ such as Libera.Chat, OFTC, and Hackint do not currently advertise
339
+ OAUTHBEARER. See emersion's [IRC × OAuth 2.0](https://emersion.fr/blog/2022/irc-and-oauth2/)
340
+ writeup for background. How to obtain the token is provider-specific and
341
+ outside the scope of this module.
342
+
343
+ `password` and `token` are mutually exclusive. Both default to SASL PLAIN;
344
+ set `saslMechanism: "OAUTHBEARER"` explicitly for OAuth 2.0 bearer tokens.
345
+ Secure connections validate the IRC server's TLS certificate by default. The
346
+ optional `allowInvalidCert` field defaults to `false`; when set to `true`, it
347
+ disables TLS certificate validation for secure connections. Use this
348
+ security-relevant opt-out only for trusted self-signed IRC networks.
349
+
350
+ ### Credentials with password (SASL PLAIN)
351
+
352
+ ```json
353
+ {
354
+ "type": "credentials",
355
+ "@context": [
356
+ "https://www.w3.org/ns/activitystreams",
357
+ "https://sockethub.org/ns/context/v1.jsonld",
358
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
359
+ ],
360
+ "actor": {
361
+ "id": "mynick@irc.libera.chat",
362
+ "type": "person"
363
+ },
364
+ "object": {
365
+ "type": "credentials",
366
+ "nick": "mynick",
367
+ "server": "irc.libera.chat",
368
+ "password": "secret",
369
+ "port": 6697,
370
+ "secure": true
371
+ }
372
+ }
373
+ ```
374
+
375
+ ### Credentials with personal access token (SASL PLAIN)
376
+
377
+ Networks like Libera.Chat accept NickServ personal access tokens via
378
+ SASL PLAIN. Use the `token` field instead of `password` to make the
379
+ distinction clear:
380
+
381
+ ```json
382
+ {
383
+ "type": "credentials",
384
+ "@context": [
385
+ "https://www.w3.org/ns/activitystreams",
386
+ "https://sockethub.org/ns/context/v1.jsonld",
387
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
388
+ ],
389
+ "actor": {
390
+ "id": "mynick@irc.libera.chat",
391
+ "type": "person"
392
+ },
393
+ "object": {
394
+ "type": "credentials",
395
+ "nick": "mynick",
396
+ "server": "irc.libera.chat",
397
+ "token": "my-personal-access-token",
398
+ "port": 6697,
399
+ "secure": true
400
+ }
401
+ }
402
+ ```
403
+
404
+ ### Credentials with OAuth token (SASL OAUTHBEARER)
405
+
406
+ ```json
407
+ {
408
+ "type": "credentials",
409
+ "@context": [
410
+ "https://www.w3.org/ns/activitystreams",
411
+ "https://sockethub.org/ns/context/v1.jsonld",
412
+ "https://sockethub.org/ns/context/platform/irc/v1.jsonld"
413
+ ],
414
+ "actor": {
415
+ "id": "mynick@chat.sr.ht",
416
+ "type": "person"
417
+ },
418
+ "object": {
419
+ "type": "credentials",
420
+ "nick": "mynick",
421
+ "server": "chat.sr.ht",
422
+ "token": "oauth-access-token",
423
+ "saslMechanism": "OAUTHBEARER",
424
+ "port": 6697,
425
+ "secure": true
426
+ }
427
+ }
428
+ ```
429
+
430
+ ## Messages and CTCP
431
+
432
+ A normal `send` is a channel or private `PRIVMSG`. The request schema only
433
+ accepts `object.type` `message`; the content decides the IRC command. Content
434
+ starting with `/me` is sent as a CTCP `ACTION` and content starting with
435
+ `/notice` is sent as a `NOTICE`, with the prefix stripped. An incoming
436
+ `ACTION` is delivered with `object.type` `me`. An incoming `NOTICE` is
437
+ delivered as a plain `message`.
438
+
439
+ Other incoming CTCP is not delivered to the application. The platform answers
440
+ `VERSION` with `Sockethub <platform version>` and `PING` by echoing the
441
+ argument. Other requests, and every CTCP reply, are ignored. Sending CTCP
442
+ other than `me` is not supported.
443
+
444
+ Channel and private `send` values, nicks, and topics must not contain CR or
445
+ LF. A value that does is rejected before anything is written to the socket.
446
+
74
447
  ## Features
75
448
 
76
449
  * **Channel communication**: Join and participate in IRC channels
@@ -88,4 +461,5 @@ messages.
88
461
 
89
462
  ## API
90
463
 
91
- Detailed API documentation can be found [here](API.md)
464
+ API reference docs are generated from the source JSDoc and are not committed to this repository.
465
+ Run `bun run doc` in this package to generate them locally.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * This is a platform for Sockethub implementing IRC functionality.
3
+ *
4
+ * Developed by Nick Jennings (https://github.com/silverbucket)
5
+ *
6
+ * Sockethub is licensed under the LGPLv3.
7
+ * See the LICENSE file for details.
8
+ *
9
+ * The latest version of this module can be found here:
10
+ * git://github.com/sockethub/sockethub.git
11
+ *
12
+ * For more information about Sockethub visit http://sockethub.org/.
13
+ *
14
+ * This program is distributed in the hope that it will be useful,
15
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17
+ */
18
+ import type { ActivityStream, PersistentPlatformConfig, PersistentPlatformInterface, PlatformCallback, PlatformSchemaStruct, PlatformSession } from "@sockethub/schemas";
19
+ import { type IrcSocketInstance } from "irc-socket-sasl";
20
+ import type { PlatformIrcCredentialsObject } from "./types.js";
21
+ export type { IrcSocketInstance } from "irc-socket-sasl";
22
+ export type { PlatformIrcCredentialsObject } from "./types.js";
23
+ export declare function flattenConnectOptions(options: Record<string, unknown>): Record<string, unknown>;
24
+ export type GetClientCallback = (err: string | null, client?: IrcSocketInstance) => void;
25
+ /**
26
+ * Handles all actions related to communication via the IRC protocol.
27
+ */
28
+ export declare class IRC implements PersistentPlatformInterface {
29
+ private readonly log;
30
+ credentialsHash: string | undefined;
31
+ config: PersistentPlatformConfig;
32
+ private readonly updateActor;
33
+ private readonly prepareActorUpdate?;
34
+ private readonly sendToClient;
35
+ private irc2as;
36
+ private forceDisconnect;
37
+ private clientConnecting;
38
+ private initialized;
39
+ private client?;
40
+ private jobQueue;
41
+ private pongAcksToSkip;
42
+ private channels;
43
+ private handledActors;
44
+ private credentials?;
45
+ constructor(session: PlatformSession);
46
+ /**
47
+ * JSON schema defining the types this platform accepts.
48
+ *
49
+ * `password` and `token` are mutually exclusive. Both default to SASL
50
+ * PLAIN; set `saslMechanism: 'OAUTHBEARER'` explicitly for OAuth 2.0
51
+ * bearer tokens (RFC 7628). See the package README for canonical
52
+ * credentials payload examples.
53
+ */
54
+ get schema(): PlatformSchemaStruct;
55
+ /**
56
+ * Returns whether the platform is ready to handle jobs.
57
+ * For IRC, this means we have successfully connected to the server.
58
+ */
59
+ isInitialized(): boolean;
60
+ /**
61
+ * Function: connect
62
+ *
63
+ * Connect to an IRC server.
64
+ *
65
+ * @param {object} job activity streams object
66
+ * @param {object} credentials credentials object
67
+ * @param {object} done callback when job is done
68
+ */
69
+ connect(job: ActivityStream, credentials: PlatformIrcCredentialsObject, done: PlatformCallback): void;
70
+ /**
71
+ * Function: join
72
+ *
73
+ * Join a room or private conversation.
74
+ *
75
+ * @param {object} job activity streams object
76
+ * @param {object} done callback when job is done
77
+ */
78
+ join(job: ActivityStream, done: PlatformCallback): any;
79
+ /**
80
+ * Function leave
81
+ *
82
+ * Leave a room or private conversation.
83
+ *
84
+ * @param {object} job activity streams object
85
+ * @param {object} done callback when job is done
86
+ */
87
+ leave(job: ActivityStream, done: PlatformCallback): any;
88
+ /**
89
+ * Function: send
90
+ *
91
+ * Send a message to a room or private conversation.
92
+ *
93
+ * @param {object} job activity streams object
94
+ * @param {object} done callback when job is done
95
+ */
96
+ send(job: ActivityStream, done: PlatformCallback): any;
97
+ /**
98
+ * Function: update
99
+ *
100
+ * Indicate a change (i.e. room topic update, or nickname change).
101
+ *
102
+ * @param {object} job activity streams object
103
+ * @param {object} credentials credentials to verify this user is the right one
104
+ * @param {object} done callback when job is done
105
+ */
106
+ update(job: ActivityStream, credentials: PlatformIrcCredentialsObject, done: PlatformCallback): any;
107
+ /**
108
+ * Function: query
109
+ *
110
+ * Indicate an intent to query something (e.g. get a list of users in a room).
111
+ *
112
+ * @param {object} job activity streams object
113
+ * @param {object} done callback when job is done
114
+ */
115
+ query(job: ActivityStream, done: PlatformCallback): any;
116
+ /**
117
+ * Disconnect IRC client
118
+ * @param {object} job activity streams object
119
+ * @param done
120
+ */
121
+ disconnect(job: ActivityStream, done: PlatformCallback): void;
122
+ cleanup(done: PlatformCallback): any;
123
+ /**
124
+ * Resolve an activity target to its IRC recipient (channel or nick).
125
+ *
126
+ * Targets are server-qualified for consistency with what the platform emits
127
+ * (see irc2as): rooms are addressed as `#channel@server` and users
128
+ * (private messages) as
129
+ * `nick@server` — the same `@server` suffix for both. Returns `null` for a
130
+ * `room` target that isn't server-qualified (a bare channel), which callers
131
+ * reject — bare channel names are no longer accepted.
132
+ *
133
+ * @param target activity stream target (`job.target`)
134
+ * @returns the IRC channel (`#channel`) or nick, or `null` for a bare room
135
+ */
136
+ private resolveIrcTarget;
137
+ private isJoined;
138
+ private hasJoined;
139
+ private hasLeft;
140
+ /**
141
+ * A socket holds exactly one nick. Replace whatever handledActors held
142
+ * rather than deleting the actor a job was addressed as: after a rename
143
+ * the store rejected, the client still addresses jobs as the old actor,
144
+ * so deleting by job actor leaves the nick the socket released behind
145
+ * and whoever takes it next is treated as this connection.
146
+ */
147
+ private setSocketNick;
148
+ private getClient;
149
+ private ircConnect;
150
+ private completeJob;
151
+ private isNickAck;
152
+ private adoptForcedNickChange;
153
+ private registerListeners;
154
+ }
155
+ export default IRC;