@webex/internal-plugin-conversation 2.59.2 → 2.59.3-next.1
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/.eslintrc.js +6 -6
- package/README.md +47 -47
- package/babel.config.js +3 -3
- package/dist/activities.js +4 -4
- package/dist/activities.js.map +1 -1
- package/dist/activity-thread-ordering.js +34 -34
- package/dist/activity-thread-ordering.js.map +1 -1
- package/dist/config.js +12 -12
- package/dist/config.js.map +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/conversation.js +474 -474
- package/dist/conversation.js.map +1 -1
- package/dist/convo-error.js +4 -4
- package/dist/convo-error.js.map +1 -1
- package/dist/decryption-transforms.js +155 -155
- package/dist/decryption-transforms.js.map +1 -1
- package/dist/encryption-transforms.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/share-activity.js +57 -57
- package/dist/share-activity.js.map +1 -1
- package/dist/to-array.js +7 -7
- package/dist/to-array.js.map +1 -1
- package/jest.config.js +3 -3
- package/package.json +21 -20
- package/process +1 -1
- package/src/activities.js +157 -157
- package/src/activity-thread-ordering.js +283 -283
- package/src/activity-threading.md +282 -282
- package/src/config.js +37 -37
- package/src/constants.js +3 -3
- package/src/conversation.js +2535 -2535
- package/src/convo-error.js +15 -15
- package/src/decryption-transforms.js +541 -541
- package/src/encryption-transforms.js +345 -345
- package/src/index.js +327 -327
- package/src/share-activity.js +436 -436
- package/src/to-array.js +29 -29
- package/test/integration/spec/create.js +290 -290
- package/test/integration/spec/encryption.js +333 -333
- package/test/integration/spec/get.js +1255 -1255
- package/test/integration/spec/mercury.js +94 -94
- package/test/integration/spec/share.js +537 -537
- package/test/integration/spec/verbs.js +1041 -1041
- package/test/unit/spec/conversation.js +823 -823
- package/test/unit/spec/decrypt-transforms.js +460 -460
- package/test/unit/spec/encryption-transforms.js +93 -93
- package/test/unit/spec/share-activity.js +178 -178
package/src/share-activity.js
CHANGED
|
@@ -1,436 +1,436 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {EventEmitter} from 'events';
|
|
6
|
-
|
|
7
|
-
import SCR from 'node-scr';
|
|
8
|
-
import {proxyEvents, transferEvents} from '@webex/common';
|
|
9
|
-
import {WebexPlugin} from '@webex/webex-core';
|
|
10
|
-
import {filter, map, pick, some} from 'lodash';
|
|
11
|
-
import {detectFileType, processImage} from '@webex/helper-image';
|
|
12
|
-
import sha256 from 'crypto-js/sha256';
|
|
13
|
-
|
|
14
|
-
export const EMITTER_SYMBOL = Symbol('EMITTER_SYMBOL');
|
|
15
|
-
export const FILE_SYMBOL = Symbol('FILE_SYMBOL');
|
|
16
|
-
const PROMISE_SYMBOL = Symbol('PROMISE_SYMBOL');
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @class
|
|
20
|
-
*/
|
|
21
|
-
const ShareActivity = WebexPlugin.extend({
|
|
22
|
-
getSymbols() {
|
|
23
|
-
return {
|
|
24
|
-
file: FILE_SYMBOL,
|
|
25
|
-
emitter: EMITTER_SYMBOL,
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
namespace: 'Conversation',
|
|
30
|
-
|
|
31
|
-
derived: {
|
|
32
|
-
target: {
|
|
33
|
-
deps: ['conversation'],
|
|
34
|
-
fn() {
|
|
35
|
-
return this.conversation;
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
session: {
|
|
41
|
-
claimedFileType: 'string',
|
|
42
|
-
conversation: {
|
|
43
|
-
required: true,
|
|
44
|
-
type: 'object',
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
content: 'string',
|
|
48
|
-
|
|
49
|
-
clientTempId: 'string',
|
|
50
|
-
|
|
51
|
-
displayName: 'string',
|
|
52
|
-
|
|
53
|
-
enableThumbnails: {
|
|
54
|
-
default: true,
|
|
55
|
-
type: 'boolean',
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
hiddenSpaceUrl: 'object',
|
|
59
|
-
|
|
60
|
-
mentions: 'object',
|
|
61
|
-
|
|
62
|
-
spaceUrl: 'object',
|
|
63
|
-
|
|
64
|
-
uploads: {
|
|
65
|
-
type: 'object',
|
|
66
|
-
default() {
|
|
67
|
-
return new Map();
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
initialize(attrs, options) {
|
|
73
|
-
Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
|
|
74
|
-
|
|
75
|
-
if (attrs && attrs.conversation) {
|
|
76
|
-
this.spaceUrl = Promise.resolve(
|
|
77
|
-
attrs.conversation._spaceUrl ||
|
|
78
|
-
this._retrieveSpaceUrl(`${attrs.conversation.url}/space`).then((url) => {
|
|
79
|
-
attrs.conversation._spaceUrl = url;
|
|
80
|
-
|
|
81
|
-
return url;
|
|
82
|
-
})
|
|
83
|
-
);
|
|
84
|
-
this.hiddenSpaceUrl = Promise.resolve(
|
|
85
|
-
attrs.conversation._hiddenSpaceUrl ||
|
|
86
|
-
this._retrieveSpaceUrl(`${attrs.conversation.url}/space/hidden`).then((url) => {
|
|
87
|
-
attrs.conversation._hiddenSpaceUrl = url;
|
|
88
|
-
|
|
89
|
-
return url;
|
|
90
|
-
})
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Adds an additional GIF to the share activity
|
|
97
|
-
* Different from regular add to skip uploading to webex files service
|
|
98
|
-
* @param {File} gif
|
|
99
|
-
* @param {File} gif.image // thumbnail
|
|
100
|
-
* @param {Object} options
|
|
101
|
-
* @param {Object} options.actions
|
|
102
|
-
* @returns {Promise}
|
|
103
|
-
*/
|
|
104
|
-
addGif(gif, options) {
|
|
105
|
-
let gifToAdd = this.uploads.get(gif);
|
|
106
|
-
|
|
107
|
-
// If the gif already exists, then don't do anything
|
|
108
|
-
if (gifToAdd) {
|
|
109
|
-
return Promise.resolve();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
gifToAdd = {
|
|
113
|
-
displayName: gif.name,
|
|
114
|
-
fileSize: gif.size || gif.byteLength || gif.length,
|
|
115
|
-
mimeType: gif.type,
|
|
116
|
-
url: 'https://giphy.com',
|
|
117
|
-
objectType: 'file',
|
|
118
|
-
height: gif.height,
|
|
119
|
-
width: gif.width,
|
|
120
|
-
image: {
|
|
121
|
-
height: gif.image.height,
|
|
122
|
-
width: gif.image.width,
|
|
123
|
-
url: 'https://giphy.com',
|
|
124
|
-
},
|
|
125
|
-
[FILE_SYMBOL]: gif,
|
|
126
|
-
...pick(options, 'actions'),
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
this.uploads.set(gif, gifToAdd);
|
|
130
|
-
|
|
131
|
-
/* Instead of encryptBinary, which produces a encrypted version of
|
|
132
|
-
* the file for upload and a SCR (contains info needed to encrypt the
|
|
133
|
-
* SCR itself and the displayName), we directly create an SCR.
|
|
134
|
-
* Because we are skipping uploading, the encrypted file is not needed.
|
|
135
|
-
*/
|
|
136
|
-
return SCR.create()
|
|
137
|
-
.then((scr) => {
|
|
138
|
-
scr.loc = gif.url;
|
|
139
|
-
gifToAdd.scr = scr;
|
|
140
|
-
|
|
141
|
-
return SCR.create();
|
|
142
|
-
})
|
|
143
|
-
.then((thumbnailScr) => {
|
|
144
|
-
thumbnailScr.loc = gif.image.url;
|
|
145
|
-
gifToAdd.image.scr = thumbnailScr;
|
|
146
|
-
});
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Adds an additional file to the share and begins submitting it to webex
|
|
151
|
-
* files
|
|
152
|
-
* @param {File} file
|
|
153
|
-
* @param {Object} options
|
|
154
|
-
* @param {Object} options.actions
|
|
155
|
-
* @returns {EventEmittingPromise}
|
|
156
|
-
*/
|
|
157
|
-
add(file, options) {
|
|
158
|
-
options = options || {};
|
|
159
|
-
options.claimedFileType = file.name.substring(file.name.lastIndexOf('.'));
|
|
160
|
-
let upload = this.uploads.get(file);
|
|
161
|
-
|
|
162
|
-
if (upload) {
|
|
163
|
-
return upload[PROMISE_SYMBOL];
|
|
164
|
-
}
|
|
165
|
-
const emitter = new EventEmitter();
|
|
166
|
-
|
|
167
|
-
upload = {
|
|
168
|
-
displayName: file.name,
|
|
169
|
-
fileSize: file.size || file.byteLength || file.length,
|
|
170
|
-
mimeType: file.type,
|
|
171
|
-
objectType: 'file',
|
|
172
|
-
[EMITTER_SYMBOL]: emitter,
|
|
173
|
-
[FILE_SYMBOL]: file,
|
|
174
|
-
...pick(options, 'actions'),
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
this.uploads.set(file, upload);
|
|
178
|
-
const promise = detectFileType(file, this.logger)
|
|
179
|
-
.then((type) => {
|
|
180
|
-
upload.mimeType = type;
|
|
181
|
-
|
|
182
|
-
return processImage({
|
|
183
|
-
file,
|
|
184
|
-
type,
|
|
185
|
-
thumbnailMaxWidth: this.config.thumbnailMaxWidth,
|
|
186
|
-
thumbnailMaxHeight: this.config.thumbnailMaxHeight,
|
|
187
|
-
enableThumbnails: this.enableThumbnails,
|
|
188
|
-
logger: this.logger,
|
|
189
|
-
});
|
|
190
|
-
})
|
|
191
|
-
.then((imageData) => {
|
|
192
|
-
const main = this.webex.internal.encryption
|
|
193
|
-
.encryptBinary(file)
|
|
194
|
-
.then(({scr, cdata}) => {
|
|
195
|
-
upload.scr = scr;
|
|
196
|
-
|
|
197
|
-
return Promise.all([cdata, this.spaceUrl]);
|
|
198
|
-
})
|
|
199
|
-
.then(([cdata, spaceUrl]) => {
|
|
200
|
-
const uploadPromise = this._upload(cdata, `${spaceUrl}/upload_sessions`, options);
|
|
201
|
-
|
|
202
|
-
transferEvents('progress', uploadPromise, emitter);
|
|
203
|
-
|
|
204
|
-
return uploadPromise;
|
|
205
|
-
})
|
|
206
|
-
.then((metadata) => {
|
|
207
|
-
upload.url = upload.scr.loc = metadata.downloadUrl;
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
let thumb;
|
|
211
|
-
|
|
212
|
-
if (imageData) {
|
|
213
|
-
const [thumbnail, fileDimensions, thumbnailDimensions] = imageData;
|
|
214
|
-
|
|
215
|
-
Object.assign(upload, fileDimensions);
|
|
216
|
-
|
|
217
|
-
if (thumbnail && thumbnailDimensions) {
|
|
218
|
-
upload.image = thumbnailDimensions;
|
|
219
|
-
thumb = this.webex.internal.encryption
|
|
220
|
-
.encryptBinary(thumbnail)
|
|
221
|
-
.then(({scr, cdata}) => {
|
|
222
|
-
upload.image.scr = scr;
|
|
223
|
-
|
|
224
|
-
return Promise.all([cdata, this.hiddenSpaceUrl]);
|
|
225
|
-
})
|
|
226
|
-
.then(([cdata, spaceUrl]) => this._upload(cdata, `${spaceUrl}/upload_sessions`))
|
|
227
|
-
.then((metadata) => {
|
|
228
|
-
upload.image.url = upload.image.scr.loc = metadata.downloadUrl;
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return Promise.all([main, thumb]);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
upload[PROMISE_SYMBOL] = promise;
|
|
237
|
-
|
|
238
|
-
proxyEvents(emitter, promise);
|
|
239
|
-
|
|
240
|
-
return promise;
|
|
241
|
-
},
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Fetches the files from the share
|
|
245
|
-
* @returns {Array}
|
|
246
|
-
*/
|
|
247
|
-
getFiles() {
|
|
248
|
-
const files = [];
|
|
249
|
-
|
|
250
|
-
for (const [key] of this.uploads) {
|
|
251
|
-
files.push(this.uploads.get(key)[FILE_SYMBOL]);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return files;
|
|
255
|
-
},
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* @param {File} file
|
|
259
|
-
* @param {string} uri
|
|
260
|
-
* @param {Object} uploadOptions - Optional object adding additional params to request body
|
|
261
|
-
* @private
|
|
262
|
-
* @returns {Promise}
|
|
263
|
-
*/
|
|
264
|
-
_upload(file, uri, uploadOptions) {
|
|
265
|
-
const fileSize = file.length || file.size || file.byteLength;
|
|
266
|
-
const fileHash = sha256(file).toString();
|
|
267
|
-
const {role, claimedFileType} = uploadOptions ?? {};
|
|
268
|
-
const initializeBody = {fileSize, claimedFileType, ...(role && {role})};
|
|
269
|
-
|
|
270
|
-
return this.webex.upload({
|
|
271
|
-
uri,
|
|
272
|
-
file,
|
|
273
|
-
qs: {
|
|
274
|
-
transcode: true,
|
|
275
|
-
},
|
|
276
|
-
phases: {
|
|
277
|
-
initialize: {
|
|
278
|
-
body: initializeBody,
|
|
279
|
-
},
|
|
280
|
-
upload: {
|
|
281
|
-
$url(session) {
|
|
282
|
-
return session.uploadUrl;
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
finalize: {
|
|
286
|
-
$uri(session) {
|
|
287
|
-
return session.finishUploadUrl;
|
|
288
|
-
},
|
|
289
|
-
body: {fileSize, fileHash},
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
},
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Removes the specified file from the share (Does not currently delete the
|
|
297
|
-
* uploaded file)
|
|
298
|
-
* @param {File} file
|
|
299
|
-
* @returns {Promise}
|
|
300
|
-
*/
|
|
301
|
-
remove(file) {
|
|
302
|
-
this.uploads.delete(file);
|
|
303
|
-
|
|
304
|
-
// Returns a promise for future-proofiness.
|
|
305
|
-
return Promise.resolve();
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* @private
|
|
310
|
-
* @returns {Promise<Object>}
|
|
311
|
-
*/
|
|
312
|
-
prepare() {
|
|
313
|
-
if (!this.uploads.size) {
|
|
314
|
-
throw new Error('Cannot submit a share activity without at least one file');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const activity = {
|
|
318
|
-
verb: 'share',
|
|
319
|
-
object: {
|
|
320
|
-
objectType: 'content',
|
|
321
|
-
displayName: this.object && this.object.displayName ? this.object.displayName : undefined,
|
|
322
|
-
content: this.object && this.object.content ? this.object.content : undefined,
|
|
323
|
-
mentions: this.object && this.object.mentions ? this.object.mentions : undefined,
|
|
324
|
-
files: {
|
|
325
|
-
items: [],
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
clientTempId: this.clientTempId,
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const promises = [];
|
|
332
|
-
|
|
333
|
-
this.uploads.forEach((item) => {
|
|
334
|
-
activity.object.files.items.push(item);
|
|
335
|
-
promises.push(item[PROMISE_SYMBOL]);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
activity.object.contentCategory = this._determineContentCategory(activity.object.files.items);
|
|
339
|
-
|
|
340
|
-
return Promise.all(promises).then(() => activity);
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* @param {Array} items
|
|
345
|
-
* @param {string} mimeType
|
|
346
|
-
* @private
|
|
347
|
-
* @returns {boolean}
|
|
348
|
-
*/
|
|
349
|
-
_itemContainsActionWithMimeType(items, mimeType) {
|
|
350
|
-
return some(items.map((item) => some(item.actions, {mimeType})));
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* @param {Array} items
|
|
355
|
-
* @private
|
|
356
|
-
* @returns {string}
|
|
357
|
-
*/
|
|
358
|
-
_determineContentCategory(items) {
|
|
359
|
-
// determine if the items contain an image
|
|
360
|
-
if (this._itemContainsActionWithMimeType(items, 'application/x-cisco-webex-whiteboard')) {
|
|
361
|
-
return 'documents';
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const mimeTypes = filter(map(items, 'mimeType'));
|
|
365
|
-
|
|
366
|
-
if (mimeTypes.length !== items.length) {
|
|
367
|
-
return 'documents';
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const contentCategory = mimeTypes[0].split('/').shift();
|
|
371
|
-
|
|
372
|
-
if (contentCategory !== 'video' && contentCategory !== 'image') {
|
|
373
|
-
return 'documents';
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
for (const mimeType of mimeTypes) {
|
|
377
|
-
if (mimeType.split('/').shift() !== contentCategory) {
|
|
378
|
-
return 'documents';
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return `${contentCategory}s`;
|
|
383
|
-
},
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* @param {string} uri
|
|
387
|
-
* @returns {Promise}
|
|
388
|
-
*/
|
|
389
|
-
_retrieveSpaceUrl(uri) {
|
|
390
|
-
return this.webex
|
|
391
|
-
.request({
|
|
392
|
-
method: 'PUT',
|
|
393
|
-
uri,
|
|
394
|
-
})
|
|
395
|
-
.then((res) => res.body.spaceUrl);
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Instantiates a ShareActivity
|
|
401
|
-
* @param {Object} conversation
|
|
402
|
-
* @param {ShareActivity|Object|array} object
|
|
403
|
-
* @param {ProxyWebex} webex
|
|
404
|
-
* @returns {ShareActivity}
|
|
405
|
-
*/
|
|
406
|
-
ShareActivity.create = function create(conversation, object, webex) {
|
|
407
|
-
if (object instanceof ShareActivity) {
|
|
408
|
-
return object;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
let files;
|
|
412
|
-
|
|
413
|
-
if (object?.object?.files) {
|
|
414
|
-
files = object.object.files;
|
|
415
|
-
Reflect.deleteProperty(object.object, 'files');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const share = new ShareActivity(
|
|
419
|
-
{
|
|
420
|
-
conversation,
|
|
421
|
-
...object,
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
parent: webex,
|
|
425
|
-
}
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
files = files?.items ?? files;
|
|
429
|
-
if (files) {
|
|
430
|
-
files.forEach((file) => share.add(file));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return share;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
export default ShareActivity;
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {EventEmitter} from 'events';
|
|
6
|
+
|
|
7
|
+
import SCR from 'node-scr';
|
|
8
|
+
import {proxyEvents, transferEvents} from '@webex/common';
|
|
9
|
+
import {WebexPlugin} from '@webex/webex-core';
|
|
10
|
+
import {filter, map, pick, some} from 'lodash';
|
|
11
|
+
import {detectFileType, processImage} from '@webex/helper-image';
|
|
12
|
+
import sha256 from 'crypto-js/sha256';
|
|
13
|
+
|
|
14
|
+
export const EMITTER_SYMBOL = Symbol('EMITTER_SYMBOL');
|
|
15
|
+
export const FILE_SYMBOL = Symbol('FILE_SYMBOL');
|
|
16
|
+
const PROMISE_SYMBOL = Symbol('PROMISE_SYMBOL');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @class
|
|
20
|
+
*/
|
|
21
|
+
const ShareActivity = WebexPlugin.extend({
|
|
22
|
+
getSymbols() {
|
|
23
|
+
return {
|
|
24
|
+
file: FILE_SYMBOL,
|
|
25
|
+
emitter: EMITTER_SYMBOL,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
namespace: 'Conversation',
|
|
30
|
+
|
|
31
|
+
derived: {
|
|
32
|
+
target: {
|
|
33
|
+
deps: ['conversation'],
|
|
34
|
+
fn() {
|
|
35
|
+
return this.conversation;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
session: {
|
|
41
|
+
claimedFileType: 'string',
|
|
42
|
+
conversation: {
|
|
43
|
+
required: true,
|
|
44
|
+
type: 'object',
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
content: 'string',
|
|
48
|
+
|
|
49
|
+
clientTempId: 'string',
|
|
50
|
+
|
|
51
|
+
displayName: 'string',
|
|
52
|
+
|
|
53
|
+
enableThumbnails: {
|
|
54
|
+
default: true,
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
hiddenSpaceUrl: 'object',
|
|
59
|
+
|
|
60
|
+
mentions: 'object',
|
|
61
|
+
|
|
62
|
+
spaceUrl: 'object',
|
|
63
|
+
|
|
64
|
+
uploads: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
default() {
|
|
67
|
+
return new Map();
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
initialize(attrs, options) {
|
|
73
|
+
Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
|
|
74
|
+
|
|
75
|
+
if (attrs && attrs.conversation) {
|
|
76
|
+
this.spaceUrl = Promise.resolve(
|
|
77
|
+
attrs.conversation._spaceUrl ||
|
|
78
|
+
this._retrieveSpaceUrl(`${attrs.conversation.url}/space`).then((url) => {
|
|
79
|
+
attrs.conversation._spaceUrl = url;
|
|
80
|
+
|
|
81
|
+
return url;
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
this.hiddenSpaceUrl = Promise.resolve(
|
|
85
|
+
attrs.conversation._hiddenSpaceUrl ||
|
|
86
|
+
this._retrieveSpaceUrl(`${attrs.conversation.url}/space/hidden`).then((url) => {
|
|
87
|
+
attrs.conversation._hiddenSpaceUrl = url;
|
|
88
|
+
|
|
89
|
+
return url;
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Adds an additional GIF to the share activity
|
|
97
|
+
* Different from regular add to skip uploading to webex files service
|
|
98
|
+
* @param {File} gif
|
|
99
|
+
* @param {File} gif.image // thumbnail
|
|
100
|
+
* @param {Object} options
|
|
101
|
+
* @param {Object} options.actions
|
|
102
|
+
* @returns {Promise}
|
|
103
|
+
*/
|
|
104
|
+
addGif(gif, options) {
|
|
105
|
+
let gifToAdd = this.uploads.get(gif);
|
|
106
|
+
|
|
107
|
+
// If the gif already exists, then don't do anything
|
|
108
|
+
if (gifToAdd) {
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
gifToAdd = {
|
|
113
|
+
displayName: gif.name,
|
|
114
|
+
fileSize: gif.size || gif.byteLength || gif.length,
|
|
115
|
+
mimeType: gif.type,
|
|
116
|
+
url: 'https://giphy.com',
|
|
117
|
+
objectType: 'file',
|
|
118
|
+
height: gif.height,
|
|
119
|
+
width: gif.width,
|
|
120
|
+
image: {
|
|
121
|
+
height: gif.image.height,
|
|
122
|
+
width: gif.image.width,
|
|
123
|
+
url: 'https://giphy.com',
|
|
124
|
+
},
|
|
125
|
+
[FILE_SYMBOL]: gif,
|
|
126
|
+
...pick(options, 'actions'),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
this.uploads.set(gif, gifToAdd);
|
|
130
|
+
|
|
131
|
+
/* Instead of encryptBinary, which produces a encrypted version of
|
|
132
|
+
* the file for upload and a SCR (contains info needed to encrypt the
|
|
133
|
+
* SCR itself and the displayName), we directly create an SCR.
|
|
134
|
+
* Because we are skipping uploading, the encrypted file is not needed.
|
|
135
|
+
*/
|
|
136
|
+
return SCR.create()
|
|
137
|
+
.then((scr) => {
|
|
138
|
+
scr.loc = gif.url;
|
|
139
|
+
gifToAdd.scr = scr;
|
|
140
|
+
|
|
141
|
+
return SCR.create();
|
|
142
|
+
})
|
|
143
|
+
.then((thumbnailScr) => {
|
|
144
|
+
thumbnailScr.loc = gif.image.url;
|
|
145
|
+
gifToAdd.image.scr = thumbnailScr;
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Adds an additional file to the share and begins submitting it to webex
|
|
151
|
+
* files
|
|
152
|
+
* @param {File} file
|
|
153
|
+
* @param {Object} options
|
|
154
|
+
* @param {Object} options.actions
|
|
155
|
+
* @returns {EventEmittingPromise}
|
|
156
|
+
*/
|
|
157
|
+
add(file, options) {
|
|
158
|
+
options = options || {};
|
|
159
|
+
options.claimedFileType = file.name.substring(file.name.lastIndexOf('.'));
|
|
160
|
+
let upload = this.uploads.get(file);
|
|
161
|
+
|
|
162
|
+
if (upload) {
|
|
163
|
+
return upload[PROMISE_SYMBOL];
|
|
164
|
+
}
|
|
165
|
+
const emitter = new EventEmitter();
|
|
166
|
+
|
|
167
|
+
upload = {
|
|
168
|
+
displayName: file.name,
|
|
169
|
+
fileSize: file.size || file.byteLength || file.length,
|
|
170
|
+
mimeType: file.type,
|
|
171
|
+
objectType: 'file',
|
|
172
|
+
[EMITTER_SYMBOL]: emitter,
|
|
173
|
+
[FILE_SYMBOL]: file,
|
|
174
|
+
...pick(options, 'actions'),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
this.uploads.set(file, upload);
|
|
178
|
+
const promise = detectFileType(file, this.logger)
|
|
179
|
+
.then((type) => {
|
|
180
|
+
upload.mimeType = type;
|
|
181
|
+
|
|
182
|
+
return processImage({
|
|
183
|
+
file,
|
|
184
|
+
type,
|
|
185
|
+
thumbnailMaxWidth: this.config.thumbnailMaxWidth,
|
|
186
|
+
thumbnailMaxHeight: this.config.thumbnailMaxHeight,
|
|
187
|
+
enableThumbnails: this.enableThumbnails,
|
|
188
|
+
logger: this.logger,
|
|
189
|
+
});
|
|
190
|
+
})
|
|
191
|
+
.then((imageData) => {
|
|
192
|
+
const main = this.webex.internal.encryption
|
|
193
|
+
.encryptBinary(file)
|
|
194
|
+
.then(({scr, cdata}) => {
|
|
195
|
+
upload.scr = scr;
|
|
196
|
+
|
|
197
|
+
return Promise.all([cdata, this.spaceUrl]);
|
|
198
|
+
})
|
|
199
|
+
.then(([cdata, spaceUrl]) => {
|
|
200
|
+
const uploadPromise = this._upload(cdata, `${spaceUrl}/upload_sessions`, options);
|
|
201
|
+
|
|
202
|
+
transferEvents('progress', uploadPromise, emitter);
|
|
203
|
+
|
|
204
|
+
return uploadPromise;
|
|
205
|
+
})
|
|
206
|
+
.then((metadata) => {
|
|
207
|
+
upload.url = upload.scr.loc = metadata.downloadUrl;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let thumb;
|
|
211
|
+
|
|
212
|
+
if (imageData) {
|
|
213
|
+
const [thumbnail, fileDimensions, thumbnailDimensions] = imageData;
|
|
214
|
+
|
|
215
|
+
Object.assign(upload, fileDimensions);
|
|
216
|
+
|
|
217
|
+
if (thumbnail && thumbnailDimensions) {
|
|
218
|
+
upload.image = thumbnailDimensions;
|
|
219
|
+
thumb = this.webex.internal.encryption
|
|
220
|
+
.encryptBinary(thumbnail)
|
|
221
|
+
.then(({scr, cdata}) => {
|
|
222
|
+
upload.image.scr = scr;
|
|
223
|
+
|
|
224
|
+
return Promise.all([cdata, this.hiddenSpaceUrl]);
|
|
225
|
+
})
|
|
226
|
+
.then(([cdata, spaceUrl]) => this._upload(cdata, `${spaceUrl}/upload_sessions`))
|
|
227
|
+
.then((metadata) => {
|
|
228
|
+
upload.image.url = upload.image.scr.loc = metadata.downloadUrl;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return Promise.all([main, thumb]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
upload[PROMISE_SYMBOL] = promise;
|
|
237
|
+
|
|
238
|
+
proxyEvents(emitter, promise);
|
|
239
|
+
|
|
240
|
+
return promise;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Fetches the files from the share
|
|
245
|
+
* @returns {Array}
|
|
246
|
+
*/
|
|
247
|
+
getFiles() {
|
|
248
|
+
const files = [];
|
|
249
|
+
|
|
250
|
+
for (const [key] of this.uploads) {
|
|
251
|
+
files.push(this.uploads.get(key)[FILE_SYMBOL]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return files;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @param {File} file
|
|
259
|
+
* @param {string} uri
|
|
260
|
+
* @param {Object} uploadOptions - Optional object adding additional params to request body
|
|
261
|
+
* @private
|
|
262
|
+
* @returns {Promise}
|
|
263
|
+
*/
|
|
264
|
+
_upload(file, uri, uploadOptions) {
|
|
265
|
+
const fileSize = file.length || file.size || file.byteLength;
|
|
266
|
+
const fileHash = sha256(file).toString();
|
|
267
|
+
const {role, claimedFileType} = uploadOptions ?? {};
|
|
268
|
+
const initializeBody = {fileSize, claimedFileType, ...(role && {role})};
|
|
269
|
+
|
|
270
|
+
return this.webex.upload({
|
|
271
|
+
uri,
|
|
272
|
+
file,
|
|
273
|
+
qs: {
|
|
274
|
+
transcode: true,
|
|
275
|
+
},
|
|
276
|
+
phases: {
|
|
277
|
+
initialize: {
|
|
278
|
+
body: initializeBody,
|
|
279
|
+
},
|
|
280
|
+
upload: {
|
|
281
|
+
$url(session) {
|
|
282
|
+
return session.uploadUrl;
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
finalize: {
|
|
286
|
+
$uri(session) {
|
|
287
|
+
return session.finishUploadUrl;
|
|
288
|
+
},
|
|
289
|
+
body: {fileSize, fileHash},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Removes the specified file from the share (Does not currently delete the
|
|
297
|
+
* uploaded file)
|
|
298
|
+
* @param {File} file
|
|
299
|
+
* @returns {Promise}
|
|
300
|
+
*/
|
|
301
|
+
remove(file) {
|
|
302
|
+
this.uploads.delete(file);
|
|
303
|
+
|
|
304
|
+
// Returns a promise for future-proofiness.
|
|
305
|
+
return Promise.resolve();
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @private
|
|
310
|
+
* @returns {Promise<Object>}
|
|
311
|
+
*/
|
|
312
|
+
prepare() {
|
|
313
|
+
if (!this.uploads.size) {
|
|
314
|
+
throw new Error('Cannot submit a share activity without at least one file');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const activity = {
|
|
318
|
+
verb: 'share',
|
|
319
|
+
object: {
|
|
320
|
+
objectType: 'content',
|
|
321
|
+
displayName: this.object && this.object.displayName ? this.object.displayName : undefined,
|
|
322
|
+
content: this.object && this.object.content ? this.object.content : undefined,
|
|
323
|
+
mentions: this.object && this.object.mentions ? this.object.mentions : undefined,
|
|
324
|
+
files: {
|
|
325
|
+
items: [],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
clientTempId: this.clientTempId,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const promises = [];
|
|
332
|
+
|
|
333
|
+
this.uploads.forEach((item) => {
|
|
334
|
+
activity.object.files.items.push(item);
|
|
335
|
+
promises.push(item[PROMISE_SYMBOL]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
activity.object.contentCategory = this._determineContentCategory(activity.object.files.items);
|
|
339
|
+
|
|
340
|
+
return Promise.all(promises).then(() => activity);
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {Array} items
|
|
345
|
+
* @param {string} mimeType
|
|
346
|
+
* @private
|
|
347
|
+
* @returns {boolean}
|
|
348
|
+
*/
|
|
349
|
+
_itemContainsActionWithMimeType(items, mimeType) {
|
|
350
|
+
return some(items.map((item) => some(item.actions, {mimeType})));
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @param {Array} items
|
|
355
|
+
* @private
|
|
356
|
+
* @returns {string}
|
|
357
|
+
*/
|
|
358
|
+
_determineContentCategory(items) {
|
|
359
|
+
// determine if the items contain an image
|
|
360
|
+
if (this._itemContainsActionWithMimeType(items, 'application/x-cisco-webex-whiteboard')) {
|
|
361
|
+
return 'documents';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const mimeTypes = filter(map(items, 'mimeType'));
|
|
365
|
+
|
|
366
|
+
if (mimeTypes.length !== items.length) {
|
|
367
|
+
return 'documents';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const contentCategory = mimeTypes[0].split('/').shift();
|
|
371
|
+
|
|
372
|
+
if (contentCategory !== 'video' && contentCategory !== 'image') {
|
|
373
|
+
return 'documents';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const mimeType of mimeTypes) {
|
|
377
|
+
if (mimeType.split('/').shift() !== contentCategory) {
|
|
378
|
+
return 'documents';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return `${contentCategory}s`;
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param {string} uri
|
|
387
|
+
* @returns {Promise}
|
|
388
|
+
*/
|
|
389
|
+
_retrieveSpaceUrl(uri) {
|
|
390
|
+
return this.webex
|
|
391
|
+
.request({
|
|
392
|
+
method: 'PUT',
|
|
393
|
+
uri,
|
|
394
|
+
})
|
|
395
|
+
.then((res) => res.body.spaceUrl);
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Instantiates a ShareActivity
|
|
401
|
+
* @param {Object} conversation
|
|
402
|
+
* @param {ShareActivity|Object|array} object
|
|
403
|
+
* @param {ProxyWebex} webex
|
|
404
|
+
* @returns {ShareActivity}
|
|
405
|
+
*/
|
|
406
|
+
ShareActivity.create = function create(conversation, object, webex) {
|
|
407
|
+
if (object instanceof ShareActivity) {
|
|
408
|
+
return object;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let files;
|
|
412
|
+
|
|
413
|
+
if (object?.object?.files) {
|
|
414
|
+
files = object.object.files;
|
|
415
|
+
Reflect.deleteProperty(object.object, 'files');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const share = new ShareActivity(
|
|
419
|
+
{
|
|
420
|
+
conversation,
|
|
421
|
+
...object,
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
parent: webex,
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
files = files?.items ?? files;
|
|
429
|
+
if (files) {
|
|
430
|
+
files.forEach((file) => share.add(file));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return share;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export default ShareActivity;
|