@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/.eslintignore +2 -0
- package/API.md +492 -0
- package/LICENSE +165 -0
- package/README.md +32 -0
- package/coverage/tmp/coverage-93018-1649152156910-0.json +1 -0
- package/package.json +57 -0
- package/src/incoming-handlers.data.js +194 -0
- package/src/incoming-handlers.js +309 -0
- package/src/incoming-handlers.test.js +35 -0
- package/src/index.js +485 -0
- package/src/index.test.js +367 -0
- package/src/schema.js +49 -0
- package/src/utils.js +18 -0
- package/src/utils.test.js +38 -0
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;
|