@sockethub/platform-irc 4.0.0-alpha.9 → 4.0.0-beta.0
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 +332 -16
- package/dist/index.d.ts +155 -0
- package/dist/index.js +18333 -1727
- package/dist/index.js.map +159 -17
- package/dist/schema.d.ts +421 -0
- package/{src/types.ts → dist/types.d.ts} +4 -2
- package/package.json +10 -9
- package/src/index.test.data.ts +0 -60
- package/src/index.test.ts +0 -277
- package/src/index.ts +0 -764
- package/src/octal-hack.js +0 -4
- package/src/schema.ts +0 -66
package/src/index.ts
DELETED
|
@@ -1,764 +0,0 @@
|
|
|
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
|
-
|
|
19
|
-
import net from "node:net";
|
|
20
|
-
import tls from "node:tls";
|
|
21
|
-
import IrcSocket from "irc-socket-sasl";
|
|
22
|
-
|
|
23
|
-
import { IrcToActivityStreams } from "@sockethub/irc2as";
|
|
24
|
-
import type {
|
|
25
|
-
ActivityStream,
|
|
26
|
-
Logger,
|
|
27
|
-
PersistentPlatformConfig,
|
|
28
|
-
PersistentPlatformInterface,
|
|
29
|
-
PlatformCallback,
|
|
30
|
-
PlatformSchemaStruct,
|
|
31
|
-
PlatformSendToClient,
|
|
32
|
-
PlatformSession,
|
|
33
|
-
PlatformUpdateActor,
|
|
34
|
-
} from "@sockethub/schemas";
|
|
35
|
-
|
|
36
|
-
import { PlatformIrcSchema } from "./schema.js";
|
|
37
|
-
import type { PlatformIrcCredentialsObject } from "./types.js";
|
|
38
|
-
|
|
39
|
-
export type GetClientCallback = (
|
|
40
|
-
err: string | null,
|
|
41
|
-
client?: typeof IrcSocket,
|
|
42
|
-
) => void;
|
|
43
|
-
|
|
44
|
-
interface IrcSocketOptionsCapabilities {
|
|
45
|
-
requires: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface IrcSocketConnectResponse {
|
|
49
|
-
isFail(): boolean;
|
|
50
|
-
fail(): string;
|
|
51
|
-
ok(): boolean;
|
|
52
|
-
end(): void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface IrcSocketOptionsConnect {
|
|
56
|
-
rejectUnauthorized: boolean;
|
|
57
|
-
}
|
|
58
|
-
interface IrcSocketOptions {
|
|
59
|
-
username: string;
|
|
60
|
-
nicknames: string[];
|
|
61
|
-
server: string;
|
|
62
|
-
realname: string;
|
|
63
|
-
port: number;
|
|
64
|
-
debug: typeof console.log;
|
|
65
|
-
saslPassword?: string;
|
|
66
|
-
capabilities?: IrcSocketOptionsCapabilities;
|
|
67
|
-
connectOptions?: IrcSocketOptionsConnect;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @class IRC
|
|
72
|
-
* @constructor
|
|
73
|
-
*
|
|
74
|
-
* @description
|
|
75
|
-
* Handles all actions related to communication via. the IRC protocol.
|
|
76
|
-
*
|
|
77
|
-
* @param {object} cfg a unique config object for this instance
|
|
78
|
-
*/
|
|
79
|
-
export default class IRC implements PersistentPlatformInterface {
|
|
80
|
-
debug: Logger;
|
|
81
|
-
credentialsHash: string;
|
|
82
|
-
config: PersistentPlatformConfig = {
|
|
83
|
-
persist: true,
|
|
84
|
-
requireCredentials: ["connect", "update"],
|
|
85
|
-
initialized: false,
|
|
86
|
-
connectTimeoutMs: 30000,
|
|
87
|
-
};
|
|
88
|
-
private readonly updateActor: PlatformUpdateActor;
|
|
89
|
-
private readonly sendToClient: PlatformSendToClient;
|
|
90
|
-
private irc2as: typeof IrcToActivityStreams;
|
|
91
|
-
private forceDisconnect = false;
|
|
92
|
-
private clientConnecting = false;
|
|
93
|
-
private client: typeof IrcSocket;
|
|
94
|
-
private jobQueue = []; // list of handlers to confirm when message delivery confirmed
|
|
95
|
-
private channels = new Set();
|
|
96
|
-
private handledActors = new Set();
|
|
97
|
-
|
|
98
|
-
constructor(session: PlatformSession) {
|
|
99
|
-
this.debug = session.debug;
|
|
100
|
-
this.sendToClient = session.sendToClient;
|
|
101
|
-
this.updateActor = session.updateActor;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Property: schema
|
|
106
|
-
*
|
|
107
|
-
* @description
|
|
108
|
-
* JSON schema defining the types this platform accepts.
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* In the below example, Sockethub will validate the incoming credentials object
|
|
112
|
-
* against whatever is defined in the `credentials` portion of the schema
|
|
113
|
-
* object.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* It will also check if the incoming AS object uses a type which exists in the
|
|
117
|
-
* `types` portion of the schema object (should be an array of type names).
|
|
118
|
-
*
|
|
119
|
-
* * **NOTE**: For more information on using the credentials object from a client,
|
|
120
|
-
* see [Sockethub Client](https://github.com/sockethub/sockethub/wiki/Sockethub-Client)
|
|
121
|
-
*
|
|
122
|
-
* Valid AS object for setting IRC credentials:
|
|
123
|
-
* @example
|
|
124
|
-
*
|
|
125
|
-
* {
|
|
126
|
-
* type: 'credentials',
|
|
127
|
-
* context: 'irc',
|
|
128
|
-
* actor: {
|
|
129
|
-
* id: 'testuser@irc.host.net',
|
|
130
|
-
* type: 'person',
|
|
131
|
-
* name: 'Mr. Test User',
|
|
132
|
-
* userName: 'testuser'
|
|
133
|
-
* },
|
|
134
|
-
* object: {
|
|
135
|
-
* type: 'credentials',
|
|
136
|
-
* server: 'irc.host.net',
|
|
137
|
-
* nick: 'testuser',
|
|
138
|
-
* password: 'secret',
|
|
139
|
-
* port: 6697,
|
|
140
|
-
* secure: true,
|
|
141
|
-
* sasl: true
|
|
142
|
-
* }
|
|
143
|
-
* }
|
|
144
|
-
*/
|
|
145
|
-
get schema(): PlatformSchemaStruct {
|
|
146
|
-
return PlatformIrcSchema;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Function: connect
|
|
151
|
-
*
|
|
152
|
-
* Connect to an IRC server.
|
|
153
|
-
*
|
|
154
|
-
* @param {object} job activity streams object
|
|
155
|
-
* @param {object} credentials credentials object
|
|
156
|
-
* @param {object} done callback when job is done
|
|
157
|
-
*/
|
|
158
|
-
connect(
|
|
159
|
-
job: ActivityStream,
|
|
160
|
-
credentials: PlatformIrcCredentialsObject,
|
|
161
|
-
done: PlatformCallback,
|
|
162
|
-
) {
|
|
163
|
-
this.getClient(job.actor.id, credentials, (err) => {
|
|
164
|
-
if (err) {
|
|
165
|
-
return done(err);
|
|
166
|
-
}
|
|
167
|
-
return done();
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Function: join
|
|
173
|
-
*
|
|
174
|
-
* Join a room or private conversation.
|
|
175
|
-
*
|
|
176
|
-
* @param {object} job activity streams object
|
|
177
|
-
* @param {object} done callback when job is done
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
*
|
|
181
|
-
* {
|
|
182
|
-
* context: 'irc',
|
|
183
|
-
* type: 'join',
|
|
184
|
-
* actor: {
|
|
185
|
-
* id: 'slvrbckt@irc.freenode.net',
|
|
186
|
-
* type: 'person',
|
|
187
|
-
* name: 'slvrbckt'
|
|
188
|
-
* },
|
|
189
|
-
* target: {
|
|
190
|
-
* id: 'irc.freenode.net/a-room',
|
|
191
|
-
* type: 'room',
|
|
192
|
-
* name: '#a-room'
|
|
193
|
-
* },
|
|
194
|
-
* object: {}
|
|
195
|
-
* }
|
|
196
|
-
*
|
|
197
|
-
*/
|
|
198
|
-
join(job: ActivityStream, done: PlatformCallback) {
|
|
199
|
-
this.debug(`join() called for ${job.actor.id}`);
|
|
200
|
-
this.getClient(job.actor.id, false, (err, client) => {
|
|
201
|
-
if (err) {
|
|
202
|
-
return done(err);
|
|
203
|
-
}
|
|
204
|
-
if (this.channels.has(job.target.name)) {
|
|
205
|
-
this.debug(`channel ${job.target.name} already joined`);
|
|
206
|
-
return done();
|
|
207
|
-
}
|
|
208
|
-
// join channel
|
|
209
|
-
this.jobQueue.push(() => {
|
|
210
|
-
this.hasJoined(job.target.name);
|
|
211
|
-
done();
|
|
212
|
-
});
|
|
213
|
-
this.debug(`sending join ${job.target.name}`);
|
|
214
|
-
client.raw(["JOIN", job.target.name]);
|
|
215
|
-
client.raw(`PING ${job.actor.name}`);
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Function leave
|
|
221
|
-
*
|
|
222
|
-
* Leave a room or private conversation.
|
|
223
|
-
*
|
|
224
|
-
* @param {object} job activity streams object
|
|
225
|
-
* @param {object} done callback when job is done
|
|
226
|
-
*
|
|
227
|
-
* @example
|
|
228
|
-
* {
|
|
229
|
-
* context: 'irc',
|
|
230
|
-
* type: 'leave',
|
|
231
|
-
* actor: {
|
|
232
|
-
* id: 'slvrbckt@irc.freenode.net',
|
|
233
|
-
* type: 'person',
|
|
234
|
-
* name: 'slvrbckt'
|
|
235
|
-
* },
|
|
236
|
-
* target: {
|
|
237
|
-
* id: 'irc.freenode.net/remotestorage',
|
|
238
|
-
* type: 'room',
|
|
239
|
-
* name: '#remotestorage'
|
|
240
|
-
* },
|
|
241
|
-
* object: {}
|
|
242
|
-
* }
|
|
243
|
-
*
|
|
244
|
-
*/
|
|
245
|
-
leave(job: ActivityStream, done: PlatformCallback) {
|
|
246
|
-
this.debug(`leave() called for ${job.actor.name}`);
|
|
247
|
-
this.getClient(job.actor.id, false, (err, client) => {
|
|
248
|
-
if (err) {
|
|
249
|
-
return done(err);
|
|
250
|
-
}
|
|
251
|
-
// leave channel
|
|
252
|
-
this.hasLeft(job.target.name);
|
|
253
|
-
client.raw(["PART", job.target.name]);
|
|
254
|
-
done();
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Function: send
|
|
260
|
-
*
|
|
261
|
-
* Send a message to a room or private conversation.
|
|
262
|
-
*
|
|
263
|
-
* @param {object} job activity streams object
|
|
264
|
-
* @param {object} done callback when job is done
|
|
265
|
-
*
|
|
266
|
-
* @example
|
|
267
|
-
*
|
|
268
|
-
* {
|
|
269
|
-
* context: 'irc',
|
|
270
|
-
* type: 'send',
|
|
271
|
-
* actor: {
|
|
272
|
-
* id: 'slvrbckt@irc.freenode.net',
|
|
273
|
-
* type: 'person',
|
|
274
|
-
* name: 'Nick Jennings',
|
|
275
|
-
* userName: 'slvrbckt'
|
|
276
|
-
* },
|
|
277
|
-
* target: {
|
|
278
|
-
* id: 'irc.freenode.net/remotestorage',
|
|
279
|
-
* type: 'room',
|
|
280
|
-
* name: '#remotestorage'
|
|
281
|
-
* },
|
|
282
|
-
* object: {
|
|
283
|
-
* type: 'message',
|
|
284
|
-
* content: 'Hello from Sockethub!'
|
|
285
|
-
* }
|
|
286
|
-
* }
|
|
287
|
-
*
|
|
288
|
-
*/
|
|
289
|
-
send(job: ActivityStream, done: PlatformCallback) {
|
|
290
|
-
this.debug(
|
|
291
|
-
`send() called for ${job.actor.id} target: ${job.target.id}`,
|
|
292
|
-
);
|
|
293
|
-
this.getClient(job.actor.id, false, async (err, client) => {
|
|
294
|
-
if (err) {
|
|
295
|
-
return done(err);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (typeof job.object.content !== "string") {
|
|
299
|
-
return done("cannot send message with no object.content");
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const match = /(\/\w+)\s*([\s\S]*)/.exec(job.object.content);
|
|
303
|
-
if (match) {
|
|
304
|
-
const cmd = match[1].substring(1).toUpperCase(); // get command
|
|
305
|
-
const msg = match[2].trim(); // remove leading/trailing whitespace from remaining text
|
|
306
|
-
if (cmd === "ME") {
|
|
307
|
-
// handle /me messages uniquely
|
|
308
|
-
job.object.type = "me";
|
|
309
|
-
job.object.content = msg;
|
|
310
|
-
} else if (cmd === "NOTICE") {
|
|
311
|
-
// attempt to send as raw command
|
|
312
|
-
job.object.type = "notice";
|
|
313
|
-
job.object.content = msg;
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
job.object.content = job.object.content.trim();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (job.object.type === "me") {
|
|
320
|
-
// message intended as command
|
|
321
|
-
// jsdoc does not like this octal escape sequence, but it's needed for proper behavior in IRC
|
|
322
|
-
// so the following line needs to be commented out when the API doc is built.
|
|
323
|
-
// investigate:
|
|
324
|
-
// https://github.com/jsdoc2md/jsdoc-to-markdown/issues/197#issuecomment-976851915
|
|
325
|
-
const buildCommand = await import("./octal-hack.js");
|
|
326
|
-
const message = buildCommand(job.object.content);
|
|
327
|
-
// biome-ignore lint/style/useTemplate: <explanation>
|
|
328
|
-
client.raw("PRIVMSG " + job.target.name + " :" + message);
|
|
329
|
-
return done("IRC commands temporarily disabled");
|
|
330
|
-
}
|
|
331
|
-
if (job.object.type === "notice") {
|
|
332
|
-
// attempt to send as raw command
|
|
333
|
-
client.raw(`NOTICE ${job.target.name} :${job.object.content}`);
|
|
334
|
-
} else if (this.isJoined(job.target.name)) {
|
|
335
|
-
client.raw(`PRIVMSG ${job.target.name} :${job.object.content}`);
|
|
336
|
-
} else {
|
|
337
|
-
return done(
|
|
338
|
-
"cannot send message to a channel of which you've not first joined.",
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
this.jobQueue.push(done);
|
|
342
|
-
client.raw(`PING ${job.actor.name}`);
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Function: update
|
|
348
|
-
*
|
|
349
|
-
* Indicate a change (i.e. room topic update, or nickname change).
|
|
350
|
-
*
|
|
351
|
-
* @param {object} job activity streams object
|
|
352
|
-
* @param {object} credentials credentials to verify this user is the right one
|
|
353
|
-
* @param {object} done callback when job is done
|
|
354
|
-
*
|
|
355
|
-
* @example change topic
|
|
356
|
-
*
|
|
357
|
-
* {
|
|
358
|
-
* context: 'irc',
|
|
359
|
-
* type: 'update',
|
|
360
|
-
* actor: {
|
|
361
|
-
* id: 'slvrbckt@irc.freenode.net',
|
|
362
|
-
* type: 'person',
|
|
363
|
-
* name: 'Nick Jennings',
|
|
364
|
-
* userName: 'slvrbckt'
|
|
365
|
-
* },
|
|
366
|
-
* target: {
|
|
367
|
-
* id: 'irc.freenode.net/a-room',
|
|
368
|
-
* type: 'room',
|
|
369
|
-
* name: '#a-room'
|
|
370
|
-
* },
|
|
371
|
-
* object: {
|
|
372
|
-
* type: 'topic',
|
|
373
|
-
* content: 'New version of Sockethub released!'
|
|
374
|
-
* }
|
|
375
|
-
* }
|
|
376
|
-
*
|
|
377
|
-
* @example change nickname
|
|
378
|
-
* {
|
|
379
|
-
* context: 'irc'
|
|
380
|
-
* type: 'update',
|
|
381
|
-
* actor: {
|
|
382
|
-
* id: 'slvrbckt@irc.freenode.net',
|
|
383
|
-
* type: 'person',
|
|
384
|
-
* name: 'slvrbckt'
|
|
385
|
-
* },
|
|
386
|
-
* object: {
|
|
387
|
-
* type: "address",
|
|
388
|
-
* },
|
|
389
|
-
* target: {
|
|
390
|
-
* id: 'cooldude@irc.freenode.net',
|
|
391
|
-
* type: 'person',
|
|
392
|
-
* name: 'cooldude'
|
|
393
|
-
* }
|
|
394
|
-
* }
|
|
395
|
-
*/
|
|
396
|
-
update(
|
|
397
|
-
job: ActivityStream,
|
|
398
|
-
credentials: PlatformIrcCredentialsObject,
|
|
399
|
-
done: PlatformCallback,
|
|
400
|
-
) {
|
|
401
|
-
this.debug(`update() called for ${job.actor.id}`);
|
|
402
|
-
this.getClient(job.actor.id, false, (err, client) => {
|
|
403
|
-
if (err) {
|
|
404
|
-
return done(err);
|
|
405
|
-
}
|
|
406
|
-
if (job.object.type === "address") {
|
|
407
|
-
this.debug(
|
|
408
|
-
`changing nick from ${job.actor.name} to ${job.target.name}`,
|
|
409
|
-
);
|
|
410
|
-
this.handledActors.add(job.target.id);
|
|
411
|
-
this.jobQueue.push(async (err: Error) => {
|
|
412
|
-
if (err) {
|
|
413
|
-
this.handledActors.delete(job.target.id);
|
|
414
|
-
return done(err);
|
|
415
|
-
}
|
|
416
|
-
credentials.object.nick = job.target.name;
|
|
417
|
-
credentials.actor.id = `${job.target.name}@${credentials.object.server}`;
|
|
418
|
-
credentials.actor.name = job.target.name;
|
|
419
|
-
await this.updateActor(credentials);
|
|
420
|
-
done();
|
|
421
|
-
});
|
|
422
|
-
// send nick change command
|
|
423
|
-
client.raw(["NICK", job.target.name]);
|
|
424
|
-
} else if (job.object.type === "topic") {
|
|
425
|
-
// update topic
|
|
426
|
-
this.debug(`changing topic in channel ${job.target.name}`);
|
|
427
|
-
this.jobQueue.push(done);
|
|
428
|
-
client.raw(["topic", job.target.name, job.object.content]);
|
|
429
|
-
} else {
|
|
430
|
-
return done(`unknown update action: ${job.object.type}`);
|
|
431
|
-
}
|
|
432
|
-
client.raw(`PING ${job.actor.name}`);
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Function: query
|
|
438
|
-
*
|
|
439
|
-
* Indicate an intent to query something (e.g. get a list of users in a room).
|
|
440
|
-
*
|
|
441
|
-
* @param {object} job activity streams object
|
|
442
|
-
* @param {object} done callback when job is done
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
*
|
|
446
|
-
* {
|
|
447
|
-
* context: 'irc',
|
|
448
|
-
* type: 'query',
|
|
449
|
-
* actor: {
|
|
450
|
-
* id: 'slvrbckt@irc.freenode.net',
|
|
451
|
-
* type: 'person',
|
|
452
|
-
* name: 'Nick Jennings',
|
|
453
|
-
* userName: 'slvrbckt'
|
|
454
|
-
* },
|
|
455
|
-
* target: {
|
|
456
|
-
* id: 'irc.freenode.net/a-room',
|
|
457
|
-
* type: 'room',
|
|
458
|
-
* name: '#a-room'
|
|
459
|
-
* },
|
|
460
|
-
* object: {
|
|
461
|
-
* type: 'attendance'
|
|
462
|
-
* }
|
|
463
|
-
* }
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
* // The above object might return:
|
|
467
|
-
* {
|
|
468
|
-
* context: 'irc',
|
|
469
|
-
* type: 'query',
|
|
470
|
-
* actor: {
|
|
471
|
-
* id: 'irc.freenode.net/a-room',
|
|
472
|
-
* type: 'room',
|
|
473
|
-
* name: '#a-room'
|
|
474
|
-
* },
|
|
475
|
-
* target: {},
|
|
476
|
-
* object: {
|
|
477
|
-
* type: 'attendance'
|
|
478
|
-
* members: [
|
|
479
|
-
* 'RyanGosling',
|
|
480
|
-
* 'PeeWeeHerman',
|
|
481
|
-
* 'Commando',
|
|
482
|
-
* 'Smoochie',
|
|
483
|
-
* 'neo'
|
|
484
|
-
* ]
|
|
485
|
-
* }
|
|
486
|
-
* }
|
|
487
|
-
*
|
|
488
|
-
*/
|
|
489
|
-
query(job: ActivityStream, done: PlatformCallback) {
|
|
490
|
-
this.debug(`query() called for ${job.actor.id}`);
|
|
491
|
-
this.getClient(job.actor.id, false, (err, client) => {
|
|
492
|
-
if (err) {
|
|
493
|
-
return done(err);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (job.object.type === "attendance") {
|
|
497
|
-
this.debug(`query() - sending NAMES for ${job.target.name}`);
|
|
498
|
-
client.raw(["NAMES", job.target.name]);
|
|
499
|
-
done();
|
|
500
|
-
} else {
|
|
501
|
-
done(`unknown 'type' '${job.object.type}'`);
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Disconnect IRC client
|
|
508
|
-
* @param {object} job activity streams object
|
|
509
|
-
* @param done
|
|
510
|
-
*
|
|
511
|
-
* @example
|
|
512
|
-
*
|
|
513
|
-
* {
|
|
514
|
-
* context: 'irc',
|
|
515
|
-
* type: 'disconnect',
|
|
516
|
-
* actor: {
|
|
517
|
-
* id: 'slvrbckt@irc.freenode.net',
|
|
518
|
-
* type: 'person'
|
|
519
|
-
* }
|
|
520
|
-
* }
|
|
521
|
-
*/
|
|
522
|
-
disconnect(job: ActivityStream, done: PlatformCallback) {
|
|
523
|
-
this.debug(`disconnect called for ${job.actor.id}`);
|
|
524
|
-
this.cleanup(done);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
cleanup(done: PlatformCallback) {
|
|
528
|
-
this.debug("cleanup() called");
|
|
529
|
-
this.config.initialized = false;
|
|
530
|
-
this.forceDisconnect = true;
|
|
531
|
-
if (typeof this.client === "object") {
|
|
532
|
-
if (typeof this.client.end === "function") {
|
|
533
|
-
this.client.end();
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
this.client = undefined;
|
|
537
|
-
return done();
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
// Private methods
|
|
542
|
-
//
|
|
543
|
-
private isJoined(channel: string) {
|
|
544
|
-
if (channel.indexOf("#") === 0) {
|
|
545
|
-
// valid channel name
|
|
546
|
-
return this.channels.has(channel);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// usernames are always OK to send to
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
private hasJoined(channel: string) {
|
|
554
|
-
this.debug(`joined ${channel}`);
|
|
555
|
-
// keep track of channels joined
|
|
556
|
-
if (!this.channels.has(channel)) {
|
|
557
|
-
this.channels.add(channel);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
private hasLeft(channel: string) {
|
|
562
|
-
this.debug(`left ${channel}`);
|
|
563
|
-
// keep track of channels left
|
|
564
|
-
if (this.channels.has(channel)) {
|
|
565
|
-
this.channels.delete(channel);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
private getClient(
|
|
570
|
-
key: string,
|
|
571
|
-
credentials: PlatformIrcCredentialsObject | false,
|
|
572
|
-
cb: GetClientCallback,
|
|
573
|
-
) {
|
|
574
|
-
this.debug(`getClient called, connecting: ${this.clientConnecting}`);
|
|
575
|
-
if (this.client) {
|
|
576
|
-
this.handledActors.add(key);
|
|
577
|
-
return cb(null, this.client);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (this.clientConnecting) {
|
|
581
|
-
// client is in the process of connecting, wait
|
|
582
|
-
setTimeout(() => {
|
|
583
|
-
if (this.client) {
|
|
584
|
-
this.debug(`resolving delayed getClient call for ${key}`);
|
|
585
|
-
this.handledActors.add(key);
|
|
586
|
-
return cb(null, this.client);
|
|
587
|
-
}
|
|
588
|
-
return cb("failed to get irc client, please try again.");
|
|
589
|
-
}, this.config.connectTimeoutMs);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
if (!credentials) {
|
|
594
|
-
return cb(
|
|
595
|
-
"no client found, and no credentials specified. you must connect first",
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
this.ircConnect(credentials, (err, client) => {
|
|
600
|
-
if (err) {
|
|
601
|
-
this.config.initialized = false;
|
|
602
|
-
return cb(err);
|
|
603
|
-
}
|
|
604
|
-
this.handledActors.add(key);
|
|
605
|
-
this.client = client;
|
|
606
|
-
this.registerListeners(credentials.object.server);
|
|
607
|
-
this.config.initialized = true;
|
|
608
|
-
return cb(null, client);
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
private ircConnect(
|
|
613
|
-
credentials: PlatformIrcCredentialsObject,
|
|
614
|
-
cb: GetClientCallback,
|
|
615
|
-
) {
|
|
616
|
-
this.clientConnecting = true;
|
|
617
|
-
const is_secure =
|
|
618
|
-
typeof credentials.object.secure === "boolean"
|
|
619
|
-
? credentials.object.secure
|
|
620
|
-
: true;
|
|
621
|
-
const is_sasl =
|
|
622
|
-
typeof credentials.object.sasl === "boolean"
|
|
623
|
-
? credentials.object.sasl
|
|
624
|
-
: !!credentials.object.password;
|
|
625
|
-
|
|
626
|
-
const module_options: IrcSocketOptions = {
|
|
627
|
-
username: credentials.object.username || credentials.object.nick,
|
|
628
|
-
nicknames: [credentials.object.nick],
|
|
629
|
-
server: credentials.object.server || "irc.libera.chat",
|
|
630
|
-
realname: credentials.actor.name || credentials.object.nick,
|
|
631
|
-
port: credentials.object.port
|
|
632
|
-
? typeof credentials.object.port === "string"
|
|
633
|
-
? Number.parseInt(credentials.object.port, 10)
|
|
634
|
-
: credentials.object.port
|
|
635
|
-
: is_secure
|
|
636
|
-
? 6697
|
|
637
|
-
: 6667,
|
|
638
|
-
debug: console.log,
|
|
639
|
-
};
|
|
640
|
-
if (is_secure) {
|
|
641
|
-
module_options.connectOptions = { rejectUnauthorized: false };
|
|
642
|
-
}
|
|
643
|
-
if (is_sasl) {
|
|
644
|
-
module_options.saslPassword = credentials.object.password;
|
|
645
|
-
module_options.capabilities = { requires: ["sasl"] };
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
this.debug(
|
|
649
|
-
`attempting to connect to ${module_options.server}:${module_options.port} transport: ${
|
|
650
|
-
is_secure ? "secure" : "clear"
|
|
651
|
-
} sasl: ${is_sasl}`,
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
const client: IrcSocket = new IrcSocket(
|
|
655
|
-
module_options,
|
|
656
|
-
is_secure ? tls : net,
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
const forceDisconnect = (err: string) => {
|
|
660
|
-
this.forceDisconnect = true;
|
|
661
|
-
this.clientConnecting = false;
|
|
662
|
-
if (client && typeof client.end === "function") {
|
|
663
|
-
client.end();
|
|
664
|
-
}
|
|
665
|
-
if (this.client && typeof this.client.end === "function") {
|
|
666
|
-
this.client.end();
|
|
667
|
-
}
|
|
668
|
-
cb(err);
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
client.once("error", (err: string) => {
|
|
672
|
-
this.debug(`irc client 'error' occurred. `, err);
|
|
673
|
-
forceDisconnect("error connecting to server.");
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
client.once("close", () => {
|
|
677
|
-
this.debug(`irc client 'close' event fired.`);
|
|
678
|
-
forceDisconnect("connection to server closed.");
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
client.once("timeout", () => {
|
|
682
|
-
this.debug("timeout occurred, force-disconnect");
|
|
683
|
-
forceDisconnect("connection timeout to server.");
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
client.connect().then((res: IrcSocketConnectResponse) => {
|
|
687
|
-
if (res.isFail()) {
|
|
688
|
-
return cb(`unable to connect to server: ${res.fail()}`);
|
|
689
|
-
}
|
|
690
|
-
const capabilities = res.ok();
|
|
691
|
-
this.clientConnecting = false;
|
|
692
|
-
if (this.forceDisconnect) {
|
|
693
|
-
client.end();
|
|
694
|
-
return cb("force disconnect active, aborting connect.");
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
this.debug(
|
|
698
|
-
`connected to ${module_options.server} capabilities: `,
|
|
699
|
-
capabilities,
|
|
700
|
-
);
|
|
701
|
-
return cb(null, client);
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
private completeJob(err?: string) {
|
|
706
|
-
this.debug(`completing job, queue count: ${this.jobQueue.length}`);
|
|
707
|
-
const done = this.jobQueue.shift();
|
|
708
|
-
if (typeof done === "function") {
|
|
709
|
-
done(err);
|
|
710
|
-
} else if (this.jobQueue.length === 0) {
|
|
711
|
-
this.debug(
|
|
712
|
-
"WARNING: job completion event received with an empty job queue.",
|
|
713
|
-
);
|
|
714
|
-
} else {
|
|
715
|
-
this.debug(
|
|
716
|
-
`WARNING: job completion found non-function in queue (${typeof done}), ${this.jobQueue.length} items remain.`,
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
private registerListeners(server: string) {
|
|
722
|
-
this.irc2as = new IrcToActivityStreams({ server: server });
|
|
723
|
-
this.client.on("data", (data: never) => {
|
|
724
|
-
this.irc2as.input(data);
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
this.irc2as.events.on("incoming", (asObject: ActivityStream) => {
|
|
728
|
-
if (
|
|
729
|
-
typeof asObject.actor === "object" &&
|
|
730
|
-
typeof asObject.actor.name === "string" &&
|
|
731
|
-
this.handledActors.has(asObject.actor.id)
|
|
732
|
-
) {
|
|
733
|
-
this.completeJob();
|
|
734
|
-
} else {
|
|
735
|
-
this.debug(`calling sendToClient for ${asObject.actor.id}`, [
|
|
736
|
-
...this.handledActors.keys(),
|
|
737
|
-
]);
|
|
738
|
-
this.sendToClient(asObject);
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
this.irc2as.events.on("unprocessed", (s: string) => {
|
|
743
|
-
this.debug(`unprocessed irc message:> ${s}`);
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
// The generated eslint error expects that the `error` event is propagating an Error object
|
|
747
|
-
// however for irc2as this event delivers an AS object of type `error`.
|
|
748
|
-
|
|
749
|
-
this.irc2as.events.on("error", (asObject: ActivityStream) => {
|
|
750
|
-
this.debug(`message error response ${asObject.object.content}`);
|
|
751
|
-
this.completeJob(asObject.object.content);
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
this.irc2as.events.on("pong", (timestamp: string) => {
|
|
755
|
-
this.debug(`received PONG at ${timestamp}`);
|
|
756
|
-
this.completeJob();
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
this.irc2as.events.on("ping", (timestamp: string) => {
|
|
760
|
-
this.debug(`received PING at ${timestamp}`);
|
|
761
|
-
this.client.raw("PONG");
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
}
|