@sockethub/platform-xmpp 5.0.0-alpha.4 → 5.0.0-alpha.6
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/API.md +146 -97
- package/README.md +75 -18
- package/package.json +15 -20
- package/src/incoming-handlers.js +298 -263
- package/src/incoming-handlers.test.data.js +260 -0
- package/src/incoming-handlers.test.js +87 -29
- package/src/index.js +696 -454
- package/src/index.test.js +477 -319
- package/src/schema.js +60 -48
- package/src/utils.js +21 -18
- package/src/utils.test.js +53 -33
- package/.eslintignore +0 -2
- package/coverage/tmp/coverage-39286-1663949499396-0.json +0 -1
- package/src/incoming-handlers.data.js +0 -198
package/src/index.js
CHANGED
|
@@ -16,12 +16,11 @@
|
|
|
16
16
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const IncomingHandlers = require('./incoming-handlers');
|
|
22
|
-
const PlatformSchema = require('./schema.js');
|
|
23
|
-
const utils = require('./utils.js');
|
|
19
|
+
import { client, xml } from "@xmpp/client";
|
|
24
20
|
|
|
21
|
+
import { IncomingHandlers } from "./incoming-handlers.js";
|
|
22
|
+
import { PlatformSchema } from "./schema.js";
|
|
23
|
+
import { utils } from "./utils.js";
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Handles all actions related to communication via. the XMPP protocol.
|
|
@@ -30,455 +29,698 @@ const utils = require('./utils.js');
|
|
|
30
29
|
*
|
|
31
30
|
* {@link https://github.com/xmppjs/xmpp.js}
|
|
32
31
|
*/
|
|
33
|
-
class XMPP {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* received, so that when a @context type is called, it can fetch the
|
|
56
|
-
* credentials (`session.getConfig()`), knowing they will have already been
|
|
57
|
-
* validated against this schema.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* In the below example, Sockethub will validate the incoming credentials object
|
|
61
|
-
* against whatever is defined in the `credentials` portion of the schema
|
|
62
|
-
* object.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* It will also check if the incoming AS object uses a type which exists in the
|
|
66
|
-
* `types` portion of the schema object (should be an array of type names).
|
|
67
|
-
*
|
|
68
|
-
* **NOTE**: For more information on using the credentials object from a client,
|
|
69
|
-
* see [Sockethub Client](https://github.com/sockethub/sockethub/wiki/Sockethub-Client)
|
|
70
|
-
*
|
|
71
|
-
* Valid AS object for setting XMPP credentials:
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
*
|
|
75
|
-
* {
|
|
76
|
-
* type: 'credentials',
|
|
77
|
-
* context: 'xmpp',
|
|
78
|
-
* actor: {
|
|
79
|
-
* id: 'testuser@jabber.net',
|
|
80
|
-
* type: 'person',
|
|
81
|
-
* name: 'Mr. Test User'
|
|
82
|
-
* },
|
|
83
|
-
* object: {
|
|
84
|
-
* type: 'credentials',
|
|
85
|
-
* userAddress: 'testuser@jabber.net',
|
|
86
|
-
* password: 'asdasdasdasd',
|
|
87
|
-
* resource: 'phone'
|
|
88
|
-
* }
|
|
89
|
-
* }
|
|
90
|
-
**/
|
|
91
|
-
get schema() {
|
|
92
|
-
return PlatformSchema;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
get config() {
|
|
96
|
-
return {
|
|
97
|
-
persist: true,
|
|
98
|
-
requireCredentials: [ 'connect' ],
|
|
99
|
-
initialized: false
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Connect to the XMPP server.
|
|
105
|
-
*
|
|
106
|
-
* @param {object} job activity streams object // TODO LINK
|
|
107
|
-
* @param {object} credentials credentials object // TODO LINK
|
|
108
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
109
|
-
*
|
|
110
|
-
* @example
|
|
111
|
-
*
|
|
112
|
-
* {
|
|
113
|
-
* context: 'xmpp',
|
|
114
|
-
* type: 'connect',
|
|
115
|
-
* actor: {
|
|
116
|
-
* id: 'slvrbckt@jabber.net/Home',
|
|
117
|
-
* type: 'person',
|
|
118
|
-
* name: 'Nick Jennings',
|
|
119
|
-
* userName: 'slvrbckt'
|
|
120
|
-
* }
|
|
121
|
-
* }
|
|
122
|
-
*/
|
|
123
|
-
connect(job, credentials, done) {
|
|
124
|
-
if (this.__client) {
|
|
125
|
-
// TODO verify client is actually connected
|
|
126
|
-
this.debug('client connection already exists for ' + job.actor.id);
|
|
127
|
-
this.initialized = true;
|
|
128
|
-
return done();
|
|
32
|
+
export default class XMPP {
|
|
33
|
+
/**
|
|
34
|
+
* Constructor called from the Sockethub `Platform` instance, passing in a
|
|
35
|
+
* session object.
|
|
36
|
+
* @param {object} session - {@link Sockethub.Platform.PlatformSession#object}
|
|
37
|
+
*/
|
|
38
|
+
constructor(session) {
|
|
39
|
+
this.id = session.id; // actor
|
|
40
|
+
this.config = {
|
|
41
|
+
connectTimeoutMs: 10000,
|
|
42
|
+
persist: true,
|
|
43
|
+
initialized: false,
|
|
44
|
+
requireCredentials: ["connect"],
|
|
45
|
+
};
|
|
46
|
+
this.debug = session.debug;
|
|
47
|
+
this.sendToClient = session.sendToClient;
|
|
48
|
+
this.createClient();
|
|
49
|
+
this.createXml();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
createClient() {
|
|
53
|
+
this.__clientConstructor = client;
|
|
129
54
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.__client.on("offline", () => {
|
|
133
|
-
this.debug('offline');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
this.__client.start().then(() => {
|
|
137
|
-
// connected
|
|
138
|
-
this.debug('connection successful');
|
|
139
|
-
this.initialized = true;
|
|
140
|
-
this.__registerHandlers();
|
|
141
|
-
return done();
|
|
142
|
-
}).catch((err) => {
|
|
143
|
-
this.debug(`connect error: ${err}`);
|
|
144
|
-
delete this.__client;
|
|
145
|
-
return done(err);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Join a room, optionally defining a display name for that room.
|
|
151
|
-
*
|
|
152
|
-
* @param {object} job activity streams object // TODO LINK
|
|
153
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
154
|
-
*
|
|
155
|
-
* @example
|
|
156
|
-
*
|
|
157
|
-
* {
|
|
158
|
-
* context: 'xmpp',
|
|
159
|
-
* type: 'join',
|
|
160
|
-
* actor: {
|
|
161
|
-
* type: 'person',
|
|
162
|
-
* id: 'slvrbckt@jabber.net/Home',
|
|
163
|
-
* name: 'Mr. Pimp'
|
|
164
|
-
* },
|
|
165
|
-
* target: {
|
|
166
|
-
* type: 'room',
|
|
167
|
-
* id: 'PartyChatRoom@muc.jabber.net'
|
|
168
|
-
* }
|
|
169
|
-
* }
|
|
170
|
-
*/
|
|
171
|
-
join(job, done) {
|
|
172
|
-
this.debug(`sending join from ${job.actor.id} to ` +
|
|
173
|
-
`${job.target.id}/${job.actor.name}`);
|
|
174
|
-
// TODO optional passwords not handled for now
|
|
175
|
-
// TODO investigate implementation reserved nickname discovery
|
|
176
|
-
let id = job.target.id.split('/')[0];
|
|
177
|
-
|
|
178
|
-
this.__client.send(xml("presence", {
|
|
179
|
-
from: job.actor.id,
|
|
180
|
-
to: `${job.target.id}/${job.actor.name || id}`
|
|
181
|
-
})).then(done);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Leave a room
|
|
186
|
-
*
|
|
187
|
-
* @param {object} job activity streams object // TODO LINK
|
|
188
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
*
|
|
192
|
-
* {
|
|
193
|
-
* context: 'xmpp',
|
|
194
|
-
* type: 'leave',
|
|
195
|
-
* actor: {
|
|
196
|
-
* type: 'person',
|
|
197
|
-
* id: 'slvrbckt@jabber.net/Home',
|
|
198
|
-
* name: 'slvrbckt'
|
|
199
|
-
* },
|
|
200
|
-
* target: {
|
|
201
|
-
* type: 'room'
|
|
202
|
-
* id: 'PartyChatRoom@muc.jabber.net',
|
|
203
|
-
* }
|
|
204
|
-
* }
|
|
205
|
-
*/
|
|
206
|
-
leave(job, done) {
|
|
207
|
-
this.debug(`sending leave from ${job.actor.id} to ` +
|
|
208
|
-
`${job.target.id}/${job.actor.name}`);
|
|
209
|
-
|
|
210
|
-
let id = job.target.id.split('/')[0];
|
|
211
|
-
|
|
212
|
-
this.__client.send(xml("presence", {
|
|
213
|
-
from: job.actor.id,
|
|
214
|
-
to: `${job.target.id}/${job.actor.name}` || id,
|
|
215
|
-
type: 'unavailable'
|
|
216
|
-
})).then(done);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Send a message to a room or private conversation.
|
|
221
|
-
*
|
|
222
|
-
* @param {object} job activity streams object // TODO LINK
|
|
223
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
224
|
-
*
|
|
225
|
-
* @example
|
|
226
|
-
*
|
|
227
|
-
* {
|
|
228
|
-
* context: 'xmpp',
|
|
229
|
-
* type: 'send',
|
|
230
|
-
* actor: {
|
|
231
|
-
* id: 'slvrbckt@jabber.net/Home',
|
|
232
|
-
* type: 'person',
|
|
233
|
-
* name: 'Nick Jennings',
|
|
234
|
-
* userName: 'slvrbckt'
|
|
235
|
-
* },
|
|
236
|
-
* target: {
|
|
237
|
-
* id: 'homer@jabber.net/Home',
|
|
238
|
-
* type: 'user',
|
|
239
|
-
* name: 'Homer'
|
|
240
|
-
* },
|
|
241
|
-
* object: {
|
|
242
|
-
* type: 'message',
|
|
243
|
-
* content: 'Hello from Sockethub!'
|
|
244
|
-
* }
|
|
245
|
-
* }
|
|
246
|
-
*
|
|
247
|
-
* {
|
|
248
|
-
* context: 'xmpp',
|
|
249
|
-
* type: 'send',
|
|
250
|
-
* actor: {
|
|
251
|
-
* id: 'slvrbckt@jabber.net/Home',
|
|
252
|
-
* type: 'person',
|
|
253
|
-
* name: 'Nick Jennings',
|
|
254
|
-
* userName: 'slvrbckt'
|
|
255
|
-
* },
|
|
256
|
-
* target: {
|
|
257
|
-
* id: 'party-room@jabber.net',
|
|
258
|
-
* type: 'room'
|
|
259
|
-
* },
|
|
260
|
-
* object: {
|
|
261
|
-
* type: 'message',
|
|
262
|
-
* content: 'Hello from Sockethub!'
|
|
263
|
-
* }
|
|
264
|
-
* }
|
|
265
|
-
*
|
|
266
|
-
*/
|
|
267
|
-
send(job, done) {
|
|
268
|
-
this.debug('send() called for ' + job.actor.id);
|
|
269
|
-
// send message
|
|
270
|
-
const message = xml(
|
|
271
|
-
"message", {
|
|
272
|
-
type: job.target.type === 'room' ? 'groupchat' : 'chat',
|
|
273
|
-
to: job.target.id,
|
|
274
|
-
id: job.object.id
|
|
275
|
-
},
|
|
276
|
-
xml("body", {}, job.object.content),
|
|
277
|
-
job.object['xmpp:replace'] ? xml("replace", {
|
|
278
|
-
id: job.object['xmpp:replace'].id,
|
|
279
|
-
xmlns: 'urn:xmpp:message-correct:0'
|
|
280
|
-
}) : undefined
|
|
281
|
-
);
|
|
282
|
-
this.__client.send(message).then(done);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* @description
|
|
287
|
-
* Indicate presence and status message.
|
|
288
|
-
* Valid presence values are "away", "chat", "dnd", "xa", "offline", "online".
|
|
289
|
-
*
|
|
290
|
-
* @param {object} job activity streams object // TODO LINK
|
|
291
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
292
|
-
*
|
|
293
|
-
* @example
|
|
294
|
-
*
|
|
295
|
-
* {
|
|
296
|
-
* context: 'xmpp',
|
|
297
|
-
* type: 'update',
|
|
298
|
-
* actor: {
|
|
299
|
-
* id: 'user@host.org/Home'
|
|
300
|
-
* },
|
|
301
|
-
* object: {
|
|
302
|
-
* type: 'presence'
|
|
303
|
-
* presence: 'away',
|
|
304
|
-
* content: '...clever saying goes here...'
|
|
305
|
-
* }
|
|
306
|
-
* }
|
|
307
|
-
*/
|
|
308
|
-
update(job, done) {
|
|
309
|
-
this.debug(`update() called for ${job.actor.id}`);
|
|
310
|
-
const props = {};
|
|
311
|
-
const show = {};
|
|
312
|
-
const status = {};
|
|
313
|
-
if (job.object.type === 'presence') {
|
|
314
|
-
if (job.object.presence === "offline") {
|
|
315
|
-
props.type = 'unavailable';
|
|
316
|
-
} else if (job.object.presence !== "online") {
|
|
317
|
-
show.show = job.object.presence;
|
|
318
|
-
}
|
|
319
|
-
if (job.object.content) {
|
|
320
|
-
status.status = job.object.content;
|
|
321
|
-
}
|
|
322
|
-
// setting presence
|
|
323
|
-
this.debug(`setting presence: ${job.object.presence}`);
|
|
324
|
-
this.__client.send(xml("presence", props, show, status)).then(done);
|
|
325
|
-
} else {
|
|
326
|
-
done(`unknown update object type: ${job.object.type}`);
|
|
55
|
+
createXml() {
|
|
56
|
+
this.__xml = xml;
|
|
327
57
|
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* @description
|
|
332
|
-
* Send friend request
|
|
333
|
-
*
|
|
334
|
-
* @param {object} job activity streams object // TODO LINK
|
|
335
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
*
|
|
339
|
-
* {
|
|
340
|
-
* context: 'xmpp',
|
|
341
|
-
* type: 'request-friend',
|
|
342
|
-
* actor: {
|
|
343
|
-
* id: 'user@host.org/Home'
|
|
344
|
-
* },
|
|
345
|
-
* target: {
|
|
346
|
-
* id: 'homer@jabber.net/Home',
|
|
347
|
-
* }
|
|
348
|
-
* }
|
|
349
|
-
*/
|
|
350
|
-
'request-friend'(job, done) {
|
|
351
|
-
this.debug('request-friend() called for ' + job.actor.id);
|
|
352
|
-
this.__client.send(xml("presence", { type: "subscribe", to:job.target.id })).then(done);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* @description
|
|
357
|
-
* Send a remove friend request
|
|
358
|
-
*
|
|
359
|
-
* @param {object} job activity streams object // TODO LINK
|
|
360
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
361
|
-
*
|
|
362
|
-
* @example
|
|
363
|
-
*
|
|
364
|
-
* {
|
|
365
|
-
* context: 'xmpp',
|
|
366
|
-
* type: 'remove-friend',
|
|
367
|
-
* actor: {
|
|
368
|
-
* id: 'user@host.org/Home'
|
|
369
|
-
* },
|
|
370
|
-
* target: {
|
|
371
|
-
* id: 'homer@jabber.net/Home',
|
|
372
|
-
* }
|
|
373
|
-
* }
|
|
374
|
-
*/
|
|
375
|
-
'remove-friend'(job, done) {
|
|
376
|
-
this.debug('remove-friend() called for ' + job.actor.id);
|
|
377
|
-
this.__client.send(xml("presence", { type: "unsubscribe", to:job.target.id })).then(done);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* @description
|
|
382
|
-
* Confirm a friend request
|
|
383
|
-
*
|
|
384
|
-
* @param {object} job activity streams object // TODO LINK
|
|
385
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
386
|
-
*
|
|
387
|
-
* @example
|
|
388
|
-
*
|
|
389
|
-
* {
|
|
390
|
-
* context: 'xmpp',
|
|
391
|
-
* type: 'make-friend',
|
|
392
|
-
* actor: {
|
|
393
|
-
* id: 'user@host.org/Home'
|
|
394
|
-
* },
|
|
395
|
-
* target: {
|
|
396
|
-
* id: 'homer@jabber.net/Home',
|
|
397
|
-
* }
|
|
398
|
-
* }
|
|
399
|
-
*/
|
|
400
|
-
'make-friend'(job, done) {
|
|
401
|
-
this.debug('make-friend() called for ' + job.actor.id);
|
|
402
|
-
this.__client.send(xml("presence", { type: "subscribe", to:job.target.id })).then(done);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Indicate an intent to query something (ie. get a list of users in a room).
|
|
407
|
-
*
|
|
408
|
-
* @param {object} job activity streams object // TODO LINK
|
|
409
|
-
* @param {object} done callback when job is done // TODO LINK
|
|
410
|
-
*
|
|
411
|
-
* @example
|
|
412
|
-
*
|
|
413
|
-
* {
|
|
414
|
-
* context: 'xmpp',
|
|
415
|
-
* type: 'query',
|
|
416
|
-
* actor: {
|
|
417
|
-
* id: 'slvrbckt@jabber.net/Home',
|
|
418
|
-
* type: 'person'
|
|
419
|
-
* },
|
|
420
|
-
* target: {
|
|
421
|
-
* id: 'PartyChatRoom@muc.jabber.net',
|
|
422
|
-
* type: 'room'
|
|
423
|
-
* },
|
|
424
|
-
* object: {
|
|
425
|
-
* type: 'attendance'
|
|
426
|
-
* }
|
|
427
|
-
* }
|
|
428
|
-
*
|
|
429
|
-
* // The above object might return:
|
|
430
|
-
* {
|
|
431
|
-
* context: 'xmpp',
|
|
432
|
-
* type: 'query',
|
|
433
|
-
* actor: {
|
|
434
|
-
* id: 'PartyChatRoom@muc.jabber.net',
|
|
435
|
-
* type: 'room'
|
|
436
|
-
* },
|
|
437
|
-
* target: {
|
|
438
|
-
* id: 'slvrbckt@jabber.net/Home',
|
|
439
|
-
* type: 'person'
|
|
440
|
-
* },
|
|
441
|
-
* object: {
|
|
442
|
-
* type: 'attendance'
|
|
443
|
-
* members: [
|
|
444
|
-
* 'RyanGosling',
|
|
445
|
-
* 'PeeWeeHerman',
|
|
446
|
-
* 'Commando',
|
|
447
|
-
* 'Smoochie',
|
|
448
|
-
* 'neo'
|
|
449
|
-
* ]
|
|
450
|
-
* }
|
|
451
|
-
* }
|
|
452
|
-
*/
|
|
453
|
-
query(job, done) {
|
|
454
|
-
this.debug('sending query from ' + job.actor.id + ' for ' + job.target.id);
|
|
455
|
-
this.__client.send(xml("iq", {
|
|
456
|
-
id: 'muc_id',
|
|
457
|
-
type: 'get',
|
|
458
|
-
from: job.actor.id,
|
|
459
|
-
to: job.target.id
|
|
460
|
-
}, xml("query", {xmlns: 'http://jabber.org/protocol/disco#items'}))).then(done);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Called when it's time to close any connections or clean data before being wiped
|
|
465
|
-
* forcefully.
|
|
466
|
-
* @param {function} done - callback when complete
|
|
467
|
-
*/
|
|
468
|
-
cleanup(done) {
|
|
469
|
-
this.debug('attempting to close connection now');
|
|
470
|
-
this.__forceDisconnect = true;
|
|
471
|
-
this.__client.stop();
|
|
472
|
-
done();
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
__registerHandlers() {
|
|
476
|
-
const ih = new IncomingHandlers(this);
|
|
477
|
-
this.__client.on('close', ih.close.bind(ih));
|
|
478
|
-
this.__client.on('error', ih.error.bind(ih));
|
|
479
|
-
this.__client.on('online', ih.online.bind(ih));
|
|
480
|
-
this.__client.on('stanza', ih.stanza.bind(ih));
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
58
|
|
|
484
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Mark the platform as disconnected and uninitialized
|
|
61
|
+
* @param {boolean} stopReconnection - If true, stop automatic reconnection
|
|
62
|
+
*/
|
|
63
|
+
__markDisconnected(stopReconnection = false) {
|
|
64
|
+
this.debug(`marking client as disconnected for ${this.id}`);
|
|
65
|
+
|
|
66
|
+
if (stopReconnection && this.__client) {
|
|
67
|
+
this.debug(`stopping automatic reconnection for ${this.id}`);
|
|
68
|
+
this.__client.stop();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.__client = undefined;
|
|
72
|
+
this.config.initialized = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Classify error to determine if reconnection should be attempted
|
|
77
|
+
* @param {Error} err - The error from XMPP client
|
|
78
|
+
* @returns {string} 'RECOVERABLE' or 'NON_RECOVERABLE'
|
|
79
|
+
*/
|
|
80
|
+
__classifyError(err) {
|
|
81
|
+
const errorString = err.toString();
|
|
82
|
+
const condition = err.condition;
|
|
83
|
+
|
|
84
|
+
// ONLY these errors are safe to reconnect on
|
|
85
|
+
const recoverableErrors = [
|
|
86
|
+
"ECONNRESET", // Network connection reset
|
|
87
|
+
"ECONNREFUSED", // Connection refused (server down)
|
|
88
|
+
"ETIMEDOUT", // Network timeout
|
|
89
|
+
"ENOTFOUND", // DNS resolution failed
|
|
90
|
+
"EHOSTUNREACH", // Host unreachable
|
|
91
|
+
"ENETUNREACH", // Network unreachable
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// Check if this is explicitly a recoverable network error
|
|
95
|
+
if (
|
|
96
|
+
recoverableErrors.some((pattern) => errorString.includes(pattern))
|
|
97
|
+
) {
|
|
98
|
+
return "RECOVERABLE";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Also check for specific network-level error codes
|
|
102
|
+
if (err.code && recoverableErrors.includes(err.code)) {
|
|
103
|
+
return "RECOVERABLE";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// DEFAULT: Everything else is non-recoverable
|
|
107
|
+
// This includes:
|
|
108
|
+
// - StreamError: conflict
|
|
109
|
+
// - SASLError: not-authorized
|
|
110
|
+
// - StreamError: policy-violation
|
|
111
|
+
// - Any unknown XMPP protocol errors
|
|
112
|
+
// - Any authentication failures
|
|
113
|
+
// - Any server policy violations
|
|
114
|
+
// - Any new error types we haven't seen before
|
|
115
|
+
return "NON_RECOVERABLE";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if the XMPP client is properly connected and can send messages
|
|
120
|
+
* @returns {boolean} true if client is connected and operational
|
|
121
|
+
*/
|
|
122
|
+
__isClientConnected() {
|
|
123
|
+
if (!this.__client) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if the client has a socket and it's writable
|
|
128
|
+
try {
|
|
129
|
+
return (
|
|
130
|
+
this.__client.socket &&
|
|
131
|
+
this.__client.socket.writable !== false &&
|
|
132
|
+
this.__client.status === "online"
|
|
133
|
+
);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this.debug("Error checking client connection status:", err);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @description
|
|
142
|
+
* JSON schema defining the types this platform accepts.
|
|
143
|
+
*
|
|
144
|
+
* Actual handling of incoming 'set' commands are handled by dispatcher,
|
|
145
|
+
* but the dispatcher uses this defined schema to validate credentials
|
|
146
|
+
* received, so that when a @context type is called, it can fetch the
|
|
147
|
+
* credentials (`session.getConfig()`), knowing they will have already been
|
|
148
|
+
* validated against this schema.
|
|
149
|
+
*
|
|
150
|
+
*
|
|
151
|
+
* In the below example, Sockethub will validate the incoming credentials object
|
|
152
|
+
* against whatever is defined in the `credentials` portion of the schema
|
|
153
|
+
* object.
|
|
154
|
+
*
|
|
155
|
+
*
|
|
156
|
+
* It will also check if the incoming AS object uses a type which exists in the
|
|
157
|
+
* `types` portion of the schema object (should be an array of type names).
|
|
158
|
+
*
|
|
159
|
+
* **NOTE**: For more information on using the credentials object from a client,
|
|
160
|
+
* see [Sockethub Client](https://github.com/sockethub/sockethub/wiki/Sockethub-Client)
|
|
161
|
+
*
|
|
162
|
+
* Valid AS object for setting XMPP credentials:
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
*
|
|
166
|
+
* {
|
|
167
|
+
* type: 'credentials',
|
|
168
|
+
* context: 'xmpp',
|
|
169
|
+
* actor: {
|
|
170
|
+
* id: 'testuser@jabber.net',
|
|
171
|
+
* type: 'person',
|
|
172
|
+
* name: 'Mr. Test User'
|
|
173
|
+
* },
|
|
174
|
+
* object: {
|
|
175
|
+
* type: 'credentials',
|
|
176
|
+
* userAddress: 'testuser@jabber.net',
|
|
177
|
+
* password: 'asdasdasdasd',
|
|
178
|
+
* resource: 'phone'
|
|
179
|
+
* }
|
|
180
|
+
* }
|
|
181
|
+
**/
|
|
182
|
+
get schema() {
|
|
183
|
+
return PlatformSchema;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Connect to the XMPP server.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} job activity streams object
|
|
190
|
+
* @param {object} credentials credentials object
|
|
191
|
+
* @param {object} done callback when job is done
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
*
|
|
195
|
+
* {
|
|
196
|
+
* context: 'xmpp',
|
|
197
|
+
* type: 'connect',
|
|
198
|
+
* actor: {
|
|
199
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
200
|
+
* type: 'person',
|
|
201
|
+
* name: 'Nick Jennings',
|
|
202
|
+
* userName: 'slvrbckt'
|
|
203
|
+
* }
|
|
204
|
+
* }
|
|
205
|
+
*/
|
|
206
|
+
connect(job, credentials, done) {
|
|
207
|
+
if (this.__isClientConnected()) {
|
|
208
|
+
this.debug(`client connection already exists for ${job.actor.id}`);
|
|
209
|
+
this.config.initialized = true;
|
|
210
|
+
return done();
|
|
211
|
+
}
|
|
212
|
+
this.debug(`connect() called for ${job.actor.id}`);
|
|
213
|
+
|
|
214
|
+
// Log credential processing
|
|
215
|
+
const xmppCreds = utils.buildXmppCredentials(credentials);
|
|
216
|
+
this.debug(
|
|
217
|
+
`building XMPP credentials for ${job.actor.id}:`,
|
|
218
|
+
JSON.stringify({
|
|
219
|
+
service: xmppCreds.service,
|
|
220
|
+
username: xmppCreds.username,
|
|
221
|
+
resource: xmppCreds.resource,
|
|
222
|
+
timeout: this.config.connectTimeoutMs,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Log before client creation
|
|
227
|
+
this.debug(`creating XMPP client for ${job.actor.id}`);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
this.__client = this.__clientConstructor({
|
|
231
|
+
...xmppCreds,
|
|
232
|
+
...{ timeout: this.config.connectTimeoutMs, tls: false },
|
|
233
|
+
});
|
|
234
|
+
this.debug(`XMPP client created successfully for ${job.actor.id}`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
this.debug(`XMPP client creation failed for ${job.actor.id}:`, err);
|
|
237
|
+
return done(`client creation failed: ${err.message}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.__client.on("offline", () => {
|
|
241
|
+
this.debug(`offline event received for ${job.actor.id}`);
|
|
242
|
+
this.__markDisconnected();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.__client.on("error", (err) => {
|
|
246
|
+
this.debug(
|
|
247
|
+
`network error event for ${job.actor.id}:${err.toString()}`,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const errorType = this.__classifyError(err);
|
|
251
|
+
|
|
252
|
+
const as = {
|
|
253
|
+
context: "xmpp",
|
|
254
|
+
type: "connect",
|
|
255
|
+
actor: { id: job.actor.id },
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (errorType === "RECOVERABLE") {
|
|
259
|
+
// Clean up state but allow reconnection
|
|
260
|
+
this.__markDisconnected(false);
|
|
261
|
+
|
|
262
|
+
as.error = `Connection lost: ${err.toString()}. Attempting automatic reconnection...`;
|
|
263
|
+
as.object = {
|
|
264
|
+
type: "connect",
|
|
265
|
+
status: "reconnecting",
|
|
266
|
+
condition: err.condition || "network",
|
|
267
|
+
};
|
|
268
|
+
} else {
|
|
269
|
+
// Clean up state and stop reconnection
|
|
270
|
+
this.__markDisconnected(true);
|
|
271
|
+
|
|
272
|
+
as.error = `Connection failed: ${err.toString()}. Manual reconnection required.`;
|
|
273
|
+
as.object = {
|
|
274
|
+
type: "connect",
|
|
275
|
+
status: "failed",
|
|
276
|
+
condition: err.condition || "protocol",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
this.sendToClient(as);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
this.__client.on("online", () => {
|
|
283
|
+
this.debug(`online event received for ${job.actor.id}`);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
this.debug(`starting XMPP client connection for ${job.actor.id}`);
|
|
287
|
+
const startTime = Date.now();
|
|
288
|
+
|
|
289
|
+
this.__client
|
|
290
|
+
.start()
|
|
291
|
+
.then(() => {
|
|
292
|
+
// connected
|
|
293
|
+
const duration = Date.now() - startTime;
|
|
294
|
+
this.debug(
|
|
295
|
+
`connection successful for ${job.actor.id} after ${duration}ms`,
|
|
296
|
+
);
|
|
297
|
+
this.config.initialized = true;
|
|
298
|
+
this.__registerHandlers();
|
|
299
|
+
return done();
|
|
300
|
+
})
|
|
301
|
+
.catch((err) => {
|
|
302
|
+
const duration = Date.now() - startTime;
|
|
303
|
+
this.debug(
|
|
304
|
+
`connection failed for ${job.actor.id} after ${duration}ms:`,
|
|
305
|
+
{
|
|
306
|
+
error: err,
|
|
307
|
+
message: err?.message,
|
|
308
|
+
code: err?.code,
|
|
309
|
+
stack: err?.stack,
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
this.__client = undefined;
|
|
313
|
+
return done(
|
|
314
|
+
`connection failed: ${err?.message || err}. (service: ${xmppCreds.service})`,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Join a room, optionally defining a display name for that room.
|
|
321
|
+
*
|
|
322
|
+
* @param {object} job activity streams object
|
|
323
|
+
* @param {object} done callback when job is done
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
*
|
|
327
|
+
* {
|
|
328
|
+
* context: 'xmpp',
|
|
329
|
+
* type: 'join',
|
|
330
|
+
* actor: {
|
|
331
|
+
* type: 'person',
|
|
332
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
333
|
+
* name: 'Mr. Pimp'
|
|
334
|
+
* },
|
|
335
|
+
* target: {
|
|
336
|
+
* type: 'room',
|
|
337
|
+
* id: 'PartyChatRoom@muc.jabber.net'
|
|
338
|
+
* }
|
|
339
|
+
* }
|
|
340
|
+
*/
|
|
341
|
+
async join(job, done) {
|
|
342
|
+
this.debug(
|
|
343
|
+
`sending join from ${job.actor.id} to ` +
|
|
344
|
+
`${job.target.id}/${job.actor.name}`,
|
|
345
|
+
);
|
|
346
|
+
// TODO optional passwords not handled for now
|
|
347
|
+
// TODO investigate implementation reserved nickname discovery
|
|
348
|
+
const id = job.target.id.split("/")[0];
|
|
349
|
+
|
|
350
|
+
const presence = this.__xml(
|
|
351
|
+
"presence",
|
|
352
|
+
{
|
|
353
|
+
from: job.actor.id,
|
|
354
|
+
to: `${job.target.id}/${job.actor.name || id}`,
|
|
355
|
+
},
|
|
356
|
+
this.__xml("x", { xmlns: "http://jabber.org/protocol/muc" }),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return this.__client.send(presence).then(done).catch(done);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Leave a room
|
|
364
|
+
*
|
|
365
|
+
* @param {object} job activity streams object
|
|
366
|
+
* @param {object} done callback when job is done
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
*
|
|
370
|
+
* {
|
|
371
|
+
* context: 'xmpp',
|
|
372
|
+
* type: 'leave',
|
|
373
|
+
* actor: {
|
|
374
|
+
* type: 'person',
|
|
375
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
376
|
+
* name: 'slvrbckt'
|
|
377
|
+
* },
|
|
378
|
+
* target: {
|
|
379
|
+
* type: 'room'
|
|
380
|
+
* id: 'PartyChatRoom@muc.jabber.net',
|
|
381
|
+
* }
|
|
382
|
+
* }
|
|
383
|
+
*/
|
|
384
|
+
leave(job, done) {
|
|
385
|
+
this.debug(
|
|
386
|
+
`sending leave from ${job.actor.id} to ` +
|
|
387
|
+
`${job.target.id}/${job.actor.name}`,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const id = job.target.id.split("/")[0];
|
|
391
|
+
|
|
392
|
+
this.__client
|
|
393
|
+
.send(
|
|
394
|
+
this.__xml("presence", {
|
|
395
|
+
from: job.actor.id,
|
|
396
|
+
to:
|
|
397
|
+
job.target?.id && job.actor?.name
|
|
398
|
+
? `${job.target.id}/${job.actor.name}`
|
|
399
|
+
: id,
|
|
400
|
+
type: "unavailable",
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
403
|
+
.then(done);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Send a message to a room or private conversation.
|
|
408
|
+
*
|
|
409
|
+
* @param {object} job activity streams object
|
|
410
|
+
* @param {object} done callback when job is done
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
*
|
|
414
|
+
* {
|
|
415
|
+
* context: 'xmpp',
|
|
416
|
+
* type: 'send',
|
|
417
|
+
* actor: {
|
|
418
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
419
|
+
* type: 'person',
|
|
420
|
+
* name: 'Nick Jennings',
|
|
421
|
+
* userName: 'slvrbckt'
|
|
422
|
+
* },
|
|
423
|
+
* target: {
|
|
424
|
+
* id: 'homer@jabber.net/Home',
|
|
425
|
+
* type: 'user',
|
|
426
|
+
* name: 'Homer'
|
|
427
|
+
* },
|
|
428
|
+
* object: {
|
|
429
|
+
* type: 'message',
|
|
430
|
+
* content: 'Hello from Sockethub!'
|
|
431
|
+
* }
|
|
432
|
+
* }
|
|
433
|
+
*
|
|
434
|
+
* {
|
|
435
|
+
* context: 'xmpp',
|
|
436
|
+
* type: 'send',
|
|
437
|
+
* actor: {
|
|
438
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
439
|
+
* type: 'person',
|
|
440
|
+
* name: 'Nick Jennings',
|
|
441
|
+
* userName: 'slvrbckt'
|
|
442
|
+
* },
|
|
443
|
+
* target: {
|
|
444
|
+
* id: 'party-room@jabber.net',
|
|
445
|
+
* type: 'room'
|
|
446
|
+
* },
|
|
447
|
+
* object: {
|
|
448
|
+
* type: 'message',
|
|
449
|
+
* content: 'Hello from Sockethub!'
|
|
450
|
+
* }
|
|
451
|
+
* }
|
|
452
|
+
*
|
|
453
|
+
*/
|
|
454
|
+
send(job, done) {
|
|
455
|
+
this.debug(`send() called for ${job.actor.id}`);
|
|
456
|
+
// send message
|
|
457
|
+
const message = this.__xml(
|
|
458
|
+
"message",
|
|
459
|
+
{
|
|
460
|
+
type: job.target.type === "room" ? "groupchat" : "chat",
|
|
461
|
+
to: job.target.id,
|
|
462
|
+
id: job.object.id,
|
|
463
|
+
},
|
|
464
|
+
this.__xml("body", {}, job.object.content),
|
|
465
|
+
job.object["xmpp:replace"]
|
|
466
|
+
? this.__xml("replace", {
|
|
467
|
+
id: job.object["xmpp:replace"].id,
|
|
468
|
+
xmlns: "urn:xmpp:message-correct:0",
|
|
469
|
+
})
|
|
470
|
+
: undefined,
|
|
471
|
+
);
|
|
472
|
+
this.__client.send(message).then(done);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* @description
|
|
477
|
+
* Indicate presence and status message.
|
|
478
|
+
* Valid presence values are "away", "chat", "dnd", "xa", "offline", "online".
|
|
479
|
+
*
|
|
480
|
+
* @param {object} job activity streams object
|
|
481
|
+
* @param {object} done callback when job is done
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
*
|
|
485
|
+
* {
|
|
486
|
+
* context: 'xmpp',
|
|
487
|
+
* type: 'update',
|
|
488
|
+
* actor: {
|
|
489
|
+
* id: 'user@host.org/Home'
|
|
490
|
+
* },
|
|
491
|
+
* object: {
|
|
492
|
+
* type: 'presence'
|
|
493
|
+
* presence: 'away',
|
|
494
|
+
* content: '...clever saying goes here...'
|
|
495
|
+
* }
|
|
496
|
+
* }
|
|
497
|
+
*/
|
|
498
|
+
update(job, done) {
|
|
499
|
+
this.debug(`update() called for ${job.actor.id}`);
|
|
500
|
+
const props = {};
|
|
501
|
+
const show = {};
|
|
502
|
+
const status = {};
|
|
503
|
+
if (job.object.type === "presence") {
|
|
504
|
+
if (job.object.presence === "offline") {
|
|
505
|
+
props.type = "unavailable";
|
|
506
|
+
} else if (job.object.presence !== "online") {
|
|
507
|
+
show.show = job.object.presence;
|
|
508
|
+
}
|
|
509
|
+
if (job.object.content) {
|
|
510
|
+
status.status = job.object.content;
|
|
511
|
+
}
|
|
512
|
+
// setting presence
|
|
513
|
+
this.debug(`setting presence: ${job.object.presence}`);
|
|
514
|
+
this.__client
|
|
515
|
+
.send(this.__xml("presence", props, show, status))
|
|
516
|
+
.then(done);
|
|
517
|
+
} else {
|
|
518
|
+
done(`unknown update object type: ${job.object.type}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @description
|
|
524
|
+
* Send friend request
|
|
525
|
+
*
|
|
526
|
+
* @param {object} job activity streams object
|
|
527
|
+
* @param {object} done callback when job is done
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
*
|
|
531
|
+
* {
|
|
532
|
+
* context: 'xmpp',
|
|
533
|
+
* type: 'request-friend',
|
|
534
|
+
* actor: {
|
|
535
|
+
* id: 'user@host.org/Home'
|
|
536
|
+
* },
|
|
537
|
+
* target: {
|
|
538
|
+
* id: 'homer@jabber.net/Home',
|
|
539
|
+
* }
|
|
540
|
+
* }
|
|
541
|
+
*/
|
|
542
|
+
"request-friend"(job, done) {
|
|
543
|
+
this.debug(`request-friend() called for ${job.actor.id}`);
|
|
544
|
+
this.__client
|
|
545
|
+
.send(
|
|
546
|
+
this.__xml("presence", {
|
|
547
|
+
type: "subscribe",
|
|
548
|
+
to: job.target.id,
|
|
549
|
+
}),
|
|
550
|
+
)
|
|
551
|
+
.then(done);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* @description
|
|
556
|
+
* Send a remove friend request
|
|
557
|
+
*
|
|
558
|
+
* @param {object} job activity streams object
|
|
559
|
+
* @param {object} done callback when job is done
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
*
|
|
563
|
+
* {
|
|
564
|
+
* context: 'xmpp',
|
|
565
|
+
* type: 'remove-friend',
|
|
566
|
+
* actor: {
|
|
567
|
+
* id: 'user@host.org/Home'
|
|
568
|
+
* },
|
|
569
|
+
* target: {
|
|
570
|
+
* id: 'homer@jabber.net/Home',
|
|
571
|
+
* }
|
|
572
|
+
* }
|
|
573
|
+
*/
|
|
574
|
+
"remove-friend"(job, done) {
|
|
575
|
+
this.debug(`remove-friend() called for ${job.actor.id}`);
|
|
576
|
+
this.__client
|
|
577
|
+
.send(
|
|
578
|
+
this.__xml("presence", {
|
|
579
|
+
type: "unsubscribe",
|
|
580
|
+
to: job.target.id,
|
|
581
|
+
}),
|
|
582
|
+
)
|
|
583
|
+
.then(done);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* @description
|
|
588
|
+
* Confirm a friend request
|
|
589
|
+
*
|
|
590
|
+
* @param {object} job activity streams object
|
|
591
|
+
* @param {object} done callback when job is done
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
*
|
|
595
|
+
* {
|
|
596
|
+
* context: 'xmpp',
|
|
597
|
+
* type: 'make-friend',
|
|
598
|
+
* actor: {
|
|
599
|
+
* id: 'user@host.org/Home'
|
|
600
|
+
* },
|
|
601
|
+
* target: {
|
|
602
|
+
* id: 'homer@jabber.net/Home',
|
|
603
|
+
* }
|
|
604
|
+
* }
|
|
605
|
+
*/
|
|
606
|
+
"make-friend"(job, done) {
|
|
607
|
+
this.debug(`make-friend() called for ${job.actor.id}`);
|
|
608
|
+
this.__client
|
|
609
|
+
.send(
|
|
610
|
+
this.__xml("presence", {
|
|
611
|
+
type: "subscribe",
|
|
612
|
+
to: job.target.id,
|
|
613
|
+
}),
|
|
614
|
+
)
|
|
615
|
+
.then(done);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Indicate an intent to query something (i.e. get a list of users in a room).
|
|
620
|
+
*
|
|
621
|
+
* @param {object} job activity streams object
|
|
622
|
+
* @param {object} done callback when job is done
|
|
623
|
+
*
|
|
624
|
+
* @example
|
|
625
|
+
*
|
|
626
|
+
* {
|
|
627
|
+
* context: 'xmpp',
|
|
628
|
+
* type: 'query',
|
|
629
|
+
* actor: {
|
|
630
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
631
|
+
* type: 'person'
|
|
632
|
+
* },
|
|
633
|
+
* target: {
|
|
634
|
+
* id: 'PartyChatRoom@muc.jabber.net',
|
|
635
|
+
* type: 'room'
|
|
636
|
+
* },
|
|
637
|
+
* object: {
|
|
638
|
+
* type: 'attendance'
|
|
639
|
+
* }
|
|
640
|
+
* }
|
|
641
|
+
*
|
|
642
|
+
* // The above object might return:
|
|
643
|
+
* {
|
|
644
|
+
* context: 'xmpp',
|
|
645
|
+
* type: 'query',
|
|
646
|
+
* actor: {
|
|
647
|
+
* id: 'PartyChatRoom@muc.jabber.net',
|
|
648
|
+
* type: 'room'
|
|
649
|
+
* },
|
|
650
|
+
* target: {
|
|
651
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
652
|
+
* type: 'person'
|
|
653
|
+
* },
|
|
654
|
+
* object: {
|
|
655
|
+
* type: 'attendance'
|
|
656
|
+
* members: [
|
|
657
|
+
* 'RyanGosling',
|
|
658
|
+
* 'PeeWeeHerman',
|
|
659
|
+
* 'Commando',
|
|
660
|
+
* 'Smoochie',
|
|
661
|
+
* 'neo'
|
|
662
|
+
* ]
|
|
663
|
+
* }
|
|
664
|
+
* }
|
|
665
|
+
*/
|
|
666
|
+
query(job, done) {
|
|
667
|
+
this.debug(`sending query from ${job.actor.id} for ${job.target.id}`);
|
|
668
|
+
this.__client
|
|
669
|
+
.send(
|
|
670
|
+
this.__xml(
|
|
671
|
+
"iq",
|
|
672
|
+
{
|
|
673
|
+
id: "muc_id",
|
|
674
|
+
type: "get",
|
|
675
|
+
from: job.actor.id,
|
|
676
|
+
to: job.target.id,
|
|
677
|
+
},
|
|
678
|
+
this.__xml("query", {
|
|
679
|
+
xmlns: "http://jabber.org/protocol/disco#items",
|
|
680
|
+
}),
|
|
681
|
+
),
|
|
682
|
+
)
|
|
683
|
+
.then(done);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Disconnect XMPP client
|
|
688
|
+
* @param {object} job activity streams object
|
|
689
|
+
* @param done
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
*
|
|
693
|
+
* {
|
|
694
|
+
* context: 'xmpp',
|
|
695
|
+
* type: 'disconnect',
|
|
696
|
+
* actor: {
|
|
697
|
+
* id: 'slvrbckt@jabber.net/Home',
|
|
698
|
+
* type: 'person'
|
|
699
|
+
* }
|
|
700
|
+
* }
|
|
701
|
+
*/
|
|
702
|
+
disconnect(job, done) {
|
|
703
|
+
this.debug("disconnecting");
|
|
704
|
+
this.cleanup(done);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Called when it's time to close any connections or clean data before being wiped
|
|
709
|
+
* forcefully.
|
|
710
|
+
* @param {function} done - callback when complete
|
|
711
|
+
*/
|
|
712
|
+
cleanup(done) {
|
|
713
|
+
this.debug("cleanup");
|
|
714
|
+
this.config.initialized = false;
|
|
715
|
+
this.__client.stop();
|
|
716
|
+
done();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
__registerHandlers() {
|
|
720
|
+
const ih = new IncomingHandlers(this);
|
|
721
|
+
this.__client.on("close", ih.close.bind(ih));
|
|
722
|
+
this.__client.on("error", ih.error.bind(ih));
|
|
723
|
+
this.__client.on("online", ih.online.bind(ih));
|
|
724
|
+
this.__client.on("stanza", ih.stanza.bind(ih));
|
|
725
|
+
}
|
|
726
|
+
}
|