@sockethub/platform-xmpp 5.0.0-alpha.3

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 ADDED
@@ -0,0 +1,485 @@
1
+ /**
2
+ * This is a platform for Sockethub implementing XMPP 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
+ 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');
24
+
25
+
26
+ /**
27
+ * Handles all actions related to communication via. the XMPP protocol.
28
+ *
29
+ * Uses `xmpp.js` as a base tool for interacting with XMPP.
30
+ *
31
+ * {@link https://github.com/xmppjs/xmpp.js}
32
+ */
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();
129
+ }
130
+ this.debug('connect called for ' + job.actor.id);
131
+ this.__client = client(utils.buildXmppCredentials(credentials));
132
+ this.__client.on("offline", (a) => {
133
+ this.debug('offline');
134
+ // console.log("offline", a);
135
+ });
136
+
137
+ this.__client.start().then(() => {
138
+ // connected
139
+ this.debug('connection successful');
140
+ this.initialized = true;
141
+ this.__registerHandlers();
142
+ return done();
143
+ }).catch((err) => {
144
+ this.debug(`connect error: ${err}`);
145
+ delete this.__client;
146
+ return done(err);
147
+ });
148
+ };
149
+
150
+ /**
151
+ * Join a room, optionally defining a display name for that room.
152
+ *
153
+ * @param {object} job activity streams object // TODO LINK
154
+ * @param {object} done callback when job is done // TODO LINK
155
+ *
156
+ * @example
157
+ *
158
+ * {
159
+ * context: 'xmpp',
160
+ * type: 'join',
161
+ * actor: {
162
+ * type: 'person',
163
+ * id: 'slvrbckt@jabber.net/Home',
164
+ * name: 'Mr. Pimp'
165
+ * },
166
+ * target: {
167
+ * type: 'room',
168
+ * id: 'PartyChatRoom@muc.jabber.net'
169
+ * }
170
+ * }
171
+ */
172
+ join(job, done) {
173
+ this.debug(`sending join from ${job.actor.id} to ` +
174
+ `${job.target.id}/${job.actor.name}`);
175
+ // TODO optional passwords not handled for now
176
+ // TODO investigate implementation reserved nickname discovery
177
+ let id = job.target.id.split('/')[0];
178
+
179
+ this.__client.send(xml("presence", {
180
+ from: job.actor.id,
181
+ to: `${job.target.id}/${job.actor.name || id}`
182
+ })).then(done);
183
+ };
184
+
185
+ /**
186
+ * Leave a room
187
+ *
188
+ * @param {object} job activity streams object // TODO LINK
189
+ * @param {object} done callback when job is done // TODO LINK
190
+ *
191
+ * @example
192
+ *
193
+ * {
194
+ * context: 'xmpp',
195
+ * type: 'leave',
196
+ * actor: {
197
+ * type: 'person',
198
+ * id: 'slvrbckt@jabber.net/Home',
199
+ * name: 'slvrbckt'
200
+ * },
201
+ * target: {
202
+ * type: 'room'
203
+ * id: 'PartyChatRoom@muc.jabber.net',
204
+ * }
205
+ * }
206
+ */
207
+ leave(job, done) {
208
+ this.debug(`sending leave from ${job.actor.id} to ` +
209
+ `${job.target.id}/${job.actor.name}`);
210
+
211
+ let id = job.target.id.split('/')[0];
212
+
213
+ this.__client.send(xml("presence", {
214
+ from: job.actor.id,
215
+ to: `${job.target.id}/${job.actor.name}` || id,
216
+ type: 'unavailable'
217
+ })).then(done);
218
+ };
219
+
220
+ /**
221
+ * Send a message to a room or private conversation.
222
+ *
223
+ * @param {object} job activity streams object // TODO LINK
224
+ * @param {object} done callback when job is done // TODO LINK
225
+ *
226
+ * @example
227
+ *
228
+ * {
229
+ * context: 'xmpp',
230
+ * type: 'send',
231
+ * actor: {
232
+ * id: 'slvrbckt@jabber.net/Home',
233
+ * type: 'person',
234
+ * name: 'Nick Jennings',
235
+ * userName: 'slvrbckt'
236
+ * },
237
+ * target: {
238
+ * id: 'homer@jabber.net/Home',
239
+ * type: 'user',
240
+ * name: 'Homer'
241
+ * },
242
+ * object: {
243
+ * type: 'message',
244
+ * content: 'Hello from Sockethub!'
245
+ * }
246
+ * }
247
+ *
248
+ * {
249
+ * context: 'xmpp',
250
+ * type: 'send',
251
+ * actor: {
252
+ * id: 'slvrbckt@jabber.net/Home',
253
+ * type: 'person',
254
+ * name: 'Nick Jennings',
255
+ * userName: 'slvrbckt'
256
+ * },
257
+ * target: {
258
+ * id: 'party-room@jabber.net',
259
+ * type: 'room'
260
+ * },
261
+ * object: {
262
+ * type: 'message',
263
+ * content: 'Hello from Sockethub!'
264
+ * }
265
+ * }
266
+ *
267
+ */
268
+ send(job, done) {
269
+ this.debug('send() called for ' + job.actor.id);
270
+ // send message
271
+ const message = xml(
272
+ "message", {
273
+ type: job.target.type === 'room' ? 'groupchat' : 'chat',
274
+ to: job.target.id,
275
+ id: job.object.id
276
+ },
277
+ xml("body", {}, job.object.content),
278
+ job.object['xmpp:replace'] ? xml("replace", {
279
+ id: job.object['xmpp:replace'].id,
280
+ xmlns: 'urn:xmpp:message-correct:0'
281
+ }) : undefined
282
+ );
283
+ this.__client.send(message).then(done);
284
+ };
285
+
286
+ /**
287
+ * @description
288
+ * Indicate presence and status message.
289
+ * Valid presence values are "away", "chat", "dnd", "xa", "offline", "online".
290
+ *
291
+ * @param {object} job activity streams object // TODO LINK
292
+ * @param {object} done callback when job is done // TODO LINK
293
+ *
294
+ * @example
295
+ *
296
+ * {
297
+ * context: 'xmpp',
298
+ * type: 'update',
299
+ * actor: {
300
+ * id: 'user@host.org/Home'
301
+ * },
302
+ * object: {
303
+ * type: 'presence'
304
+ * presence: 'away',
305
+ * content: '...clever saying goes here...'
306
+ * }
307
+ * }
308
+ */
309
+ update(job, done) {
310
+ this.debug(`update() called for ${job.actor.id}`);
311
+ const props = {};
312
+ const show = {};
313
+ const status = {};
314
+ if (job.object.type === 'presence') {
315
+ if (job.object.presence === "offline") {
316
+ props.type = 'unavailable';
317
+ } else if (job.object.presence !== "online") {
318
+ show.show = job.object.presence;
319
+ }
320
+ if (job.object.content) {
321
+ status.status = job.object.content;
322
+ }
323
+ // setting presence
324
+ this.debug(`setting presence: ${job.object.presence}`);
325
+ this.__client.send(xml("presence", props, show, status)).then(done);
326
+ } else {
327
+ done(`unknown update object type: ${job.object.type}`);
328
+ }
329
+ };
330
+
331
+ /**
332
+ * @description
333
+ * Send friend request
334
+ *
335
+ * @param {object} job activity streams object // TODO LINK
336
+ * @param {object} done callback when job is done // TODO LINK
337
+ *
338
+ * @example
339
+ *
340
+ * {
341
+ * context: 'xmpp',
342
+ * type: 'request-friend',
343
+ * actor: {
344
+ * id: 'user@host.org/Home'
345
+ * },
346
+ * target: {
347
+ * id: 'homer@jabber.net/Home',
348
+ * }
349
+ * }
350
+ */
351
+ 'request-friend'(job, done) {
352
+ this.debug('request-friend() called for ' + job.actor.id);
353
+ this.__client.send(xml("presence", { type: "subscribe", to:job.target.id })).then(done);
354
+ };
355
+
356
+ /**
357
+ * @description
358
+ * Send a remove friend request
359
+ *
360
+ * @param {object} job activity streams object // TODO LINK
361
+ * @param {object} done callback when job is done // TODO LINK
362
+ *
363
+ * @example
364
+ *
365
+ * {
366
+ * context: 'xmpp',
367
+ * type: 'remove-friend',
368
+ * actor: {
369
+ * id: 'user@host.org/Home'
370
+ * },
371
+ * target: {
372
+ * id: 'homer@jabber.net/Home',
373
+ * }
374
+ * }
375
+ */
376
+ 'remove-friend'(job, done) {
377
+ this.debug('remove-friend() called for ' + job.actor.id);
378
+ this.__client.send(xml("presence", { type: "unsubscribe", to:job.target.id })).then(done);
379
+ };
380
+
381
+ /**
382
+ * @description
383
+ * Confirm a friend request
384
+ *
385
+ * @param {object} job activity streams object // TODO LINK
386
+ * @param {object} done callback when job is done // TODO LINK
387
+ *
388
+ * @example
389
+ *
390
+ * {
391
+ * context: 'xmpp',
392
+ * type: 'make-friend',
393
+ * actor: {
394
+ * id: 'user@host.org/Home'
395
+ * },
396
+ * target: {
397
+ * id: 'homer@jabber.net/Home',
398
+ * }
399
+ * }
400
+ */
401
+ 'make-friend'(job, done) {
402
+ this.debug('make-friend() called for ' + job.actor.id);
403
+ this.__client.send(xml("presence", { type: "subscribe", to:job.target.id })).then(done);
404
+ };
405
+
406
+ /**
407
+ * Indicate an intent to query something (ie. get a list of users in a room).
408
+ *
409
+ * @param {object} job activity streams object // TODO LINK
410
+ * @param {object} done callback when job is done // TODO LINK
411
+ *
412
+ * @example
413
+ *
414
+ * {
415
+ * context: 'xmpp',
416
+ * type: 'query',
417
+ * actor: {
418
+ * id: 'slvrbckt@jabber.net/Home',
419
+ * type: 'person'
420
+ * },
421
+ * target: {
422
+ * id: 'PartyChatRoom@muc.jabber.net',
423
+ * type: 'room'
424
+ * },
425
+ * object: {
426
+ * type: 'attendance'
427
+ * }
428
+ * }
429
+ *
430
+ * // The above object might return:
431
+ * {
432
+ * context: 'xmpp',
433
+ * type: 'query',
434
+ * actor: {
435
+ * id: 'PartyChatRoom@muc.jabber.net',
436
+ * type: 'room'
437
+ * },
438
+ * target: {
439
+ * id: 'slvrbckt@jabber.net/Home',
440
+ * type: 'person'
441
+ * },
442
+ * object: {
443
+ * type: 'attendance'
444
+ * members: [
445
+ * 'RyanGosling',
446
+ * 'PeeWeeHerman',
447
+ * 'Commando',
448
+ * 'Smoochie',
449
+ * 'neo'
450
+ * ]
451
+ * }
452
+ * }
453
+ */
454
+ query(job, done) {
455
+ this.debug('sending query from ' + job.actor.id + ' for ' + job.target.id);
456
+ this.__client.send(xml("iq", {
457
+ id: 'muc_id',
458
+ type: 'get',
459
+ from: job.actor.id,
460
+ to: job.target.id
461
+ }, xml("query", {xmlns: 'http://jabber.org/protocol/disco#items'}))).then(done);
462
+ };
463
+
464
+ /**
465
+ * Called when it's time to close any connections or clean data before being wiped
466
+ * forcefully.
467
+ * @param {function} done - callback when complete
468
+ */
469
+ cleanup(done) {
470
+ this.debug('attempting to close connection now');
471
+ this.__forceDisconnect = true;
472
+ this.__client.stop();
473
+ done();
474
+ };
475
+
476
+ __registerHandlers() {
477
+ const ih = new IncomingHandlers(this);
478
+ this.__client.on('close', ih.close.bind(ih));
479
+ this.__client.on('error', ih.error.bind(ih));
480
+ this.__client.on('online', ih.online.bind(ih));
481
+ this.__client.on('stanza', ih.stanza.bind(ih));
482
+ };
483
+ }
484
+
485
+ module.exports = XMPP;