@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/src/index.js CHANGED
@@ -16,12 +16,11 @@
16
16
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17
17
  */
18
18
 
19
- const { client, xml } = require("@xmpp/client");
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
- * Constructor called from the Sockethub `Platform` instance, passing in a
36
- * session object.
37
- * @param {object} session - {@link Sockethub.Platform.PlatformSession#object}
38
- */
39
- constructor(session) {
40
- session = (typeof session === 'object') ? session : {};
41
- this.id = session.id; // actor
42
- this.initialized = false;
43
- this.debug = session.debug;
44
- this.sendToClient = session.sendToClient;
45
- this.__forceDisconnect = false;
46
- this.__channels = [];
47
- }
48
-
49
- /**
50
- * @description
51
- * JSON schema defining the types this platform accepts.
52
- *
53
- * Actual handling of incoming 'set' commands are handled by dispatcher,
54
- * but the dispatcher uses this defined schema to validate credentials
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
- this.debug('connect called for ' + job.actor.id);
131
- this.__client = client(utils.buildXmppCredentials(credentials));
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
- module.exports = XMPP;
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
+ }