@meltwater/conversations-api-services 1.0.4 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltwater/conversations-api-services",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Repository to contain all conversations api services shared across our services",
5
5
  "type": "module",
6
6
  "main": "src/data-access/index.js",
@@ -0,0 +1,489 @@
1
+ import superagent from 'superagent';
2
+ import { removePrefix } from '../../lib/externalId.helpers.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class TikTokApiClient {
6
+ constructor({ services }) {
7
+ this.tiktokURL = configuration.get('TIKTOK_API_URL');
8
+ this.tokenService = services.token;
9
+ this.logger = services.logger;
10
+ }
11
+
12
+ async getAuthorization(document) {
13
+ const {
14
+ documentId,
15
+ systemData: { connectionsCredential: credentialId } = {},
16
+ } = document;
17
+ try {
18
+ const token = await this.tokenService.getByCredentialId(
19
+ credentialId
20
+ );
21
+ return `${token.token}`;
22
+ } catch (error) {
23
+ this.logger.error(
24
+ `${documentId} - error getting tiktok token - ${error.code}`,
25
+ error
26
+ );
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ async sendPost({
32
+ paramString = '',
33
+ headers = {},
34
+ document,
35
+ postData = undefined,
36
+ }) {
37
+ let response = {};
38
+ try {
39
+ response = await superagent
40
+ .post(this.tiktokURL + paramString)
41
+ .set('Accept', 'application/json')
42
+ .set('Content-Type', 'application/json')
43
+ .set('Access-Token', await this.getAuthorization(document))
44
+ .send(postData);
45
+ } catch (err) {
46
+ if (
47
+ err &&
48
+ err.response &&
49
+ err.response.body &&
50
+ err.response.body.error
51
+ ) {
52
+ this.logger.error(
53
+ `Failed to call tiktok api for paramString ${paramString}: ${err.response.body.error.message}`
54
+ );
55
+ } else {
56
+ this.logger.error(
57
+ `Failed to call tiktok api for paramString ${paramString}`,
58
+ err
59
+ );
60
+ }
61
+
62
+ throw err;
63
+ }
64
+
65
+ return response;
66
+ }
67
+
68
+ // assumes batch calls are using the same credential
69
+ async sendRequest({ paramString = '', headers = {}, document }) {
70
+ let response = {};
71
+ try {
72
+ response = await superagent
73
+ .get(this.tiktokURL + paramString)
74
+ .set('Accept', 'application/json')
75
+ .set('Content-Type', 'application/json')
76
+ .set('Access-Token', await this.getAuthorization(document))
77
+ .send();
78
+ } catch (err) {
79
+ if (
80
+ err &&
81
+ err.response &&
82
+ err.response.body &&
83
+ err.response.body.error
84
+ ) {
85
+ this.logger.error(
86
+ `Failed to call tiktok api for paramString ${paramString}: ${err.response.body.error.message}`
87
+ );
88
+ } else {
89
+ this.logger.error(
90
+ `Failed to call tiktok api for paramString ${paramString}`,
91
+ err
92
+ );
93
+ }
94
+
95
+ throw err;
96
+ }
97
+
98
+ if (response.status !== 200) {
99
+ this.logger.error(
100
+ `Failed to call tiktok api for documentId ${documentId}`,
101
+ response.body
102
+ );
103
+ let error = new Error(
104
+ `Failed to call tiktok api for documentId ${documentId}`
105
+ );
106
+ error.code = response.status;
107
+ throw error;
108
+ }
109
+
110
+ return response.body;
111
+ }
112
+
113
+ async getChannelInfoForDocuments(documents) {
114
+ const channelInfo = await this.getStatsForDocuments(
115
+ documents,
116
+ 'metaData.authors.0.authorInfo.externalId',
117
+ {
118
+ og: ['video/list/'],
119
+ }
120
+ );
121
+ return channelInfo;
122
+ }
123
+
124
+ async getStatsForDocuments(
125
+ documents,
126
+ externalIdFrom = 'metaData.externalId',
127
+ paramsByType = {
128
+ og: ['video/list/'],
129
+ }
130
+ ) {
131
+ // for batching separate og and comment/reply requests per credential
132
+ const documentsByTypeAndCredentials = documents.reduce(
133
+ (retObj, document, index) => {
134
+ const paramArray =
135
+ paramsByType[document.metaData.discussionType];
136
+ if (paramArray) {
137
+ paramArray.forEach((paramString) => {
138
+ let discussionTypeBucket =
139
+ retObj[
140
+ `${paramString}${document.systemData.connectionsCredential}`
141
+ ];
142
+ if (!discussionTypeBucket) {
143
+ discussionTypeBucket = {
144
+ paramString,
145
+ document,
146
+ externalIds: [],
147
+ externalIdToDocumentIndex: {},
148
+ };
149
+ retObj[
150
+ `${paramString}${document.systemData.connectionsCredential}`
151
+ ] = discussionTypeBucket;
152
+ }
153
+ const externalIdTrimmed = removePrefix(
154
+ // javascript weirdness to get around '.' notation
155
+ externalIdFrom
156
+ .split('.')
157
+ .reduce((a, b) => a[b], document)
158
+ );
159
+ discussionTypeBucket.externalIds.push(
160
+ externalIdTrimmed
161
+ );
162
+ discussionTypeBucket.externalIdToDocumentIndex[
163
+ externalIdTrimmed
164
+ ] = index;
165
+ });
166
+ }
167
+ return retObj;
168
+ },
169
+ {}
170
+ );
171
+
172
+ const sortedResults = Array(documents.length).fill();
173
+ await Promise.all(
174
+ Object.values(documentsByTypeAndCredentials).map(
175
+ async (documentsByTypeAndCredential) => {
176
+ if (documentsByTypeAndCredential.externalIds.length) {
177
+ const {
178
+ metaData: {
179
+ inReplyTo: { profileId: business_id },
180
+ },
181
+ } = documentsByTypeAndCredential.document;
182
+ const trimmedBusinessId = removePrefix(business_id);
183
+ const requestResult = await this.sendRequest({
184
+ paramString: `${
185
+ documentsByTypeAndCredential.paramString
186
+ }?business_id=${trimmedBusinessId}&fields=["item_id","create_time","thumbnail_url","share_url","embed_url","caption","video_views","likes","comments","shares"]&filters=${JSON.stringify(
187
+ {
188
+ video_ids:
189
+ documentsByTypeAndCredential.externalIds,
190
+ }
191
+ )}`,
192
+ document: documentsByTypeAndCredential.document,
193
+ });
194
+ requestResult.data.videos.forEach(
195
+ ({
196
+ item_id,
197
+ create_time,
198
+ thumbnail_url,
199
+ share_url,
200
+ embed_url,
201
+ caption,
202
+ video_views,
203
+ likes,
204
+ comments,
205
+ shares,
206
+ ...args
207
+ }) => {
208
+ const documentIndex =
209
+ documentsByTypeAndCredential
210
+ .externalIdToDocumentIndex[item_id];
211
+ sortedResults[documentIndex] = {
212
+ ...sortedResults[documentIndex],
213
+ item_id,
214
+ create_time,
215
+ thumbnail_url,
216
+ externalId:
217
+ documents[documentIndex].metaData
218
+ .externalId,
219
+ share_url,
220
+ embed_url,
221
+ caption,
222
+ video_views,
223
+ likes,
224
+ comments,
225
+ shares,
226
+ };
227
+ }
228
+ );
229
+ }
230
+ }
231
+ )
232
+ );
233
+ return sortedResults;
234
+ }
235
+
236
+ async publish(message, videoId, markMessageAsCompleteDocumentId) {
237
+ const {
238
+ metaData: { discussionType },
239
+ } = message;
240
+ let publishedMessage;
241
+
242
+ switch (discussionType) {
243
+ case 'qt':
244
+ publishedMessage = await this.insertComment(message);
245
+ break;
246
+ case 're':
247
+ publishedMessage = await this.insertReply(
248
+ videoId,
249
+ message,
250
+ markMessageAsCompleteDocumentId
251
+ );
252
+ break;
253
+ }
254
+
255
+ return publishedMessage;
256
+ }
257
+
258
+ async insertComment(document) {
259
+ const {
260
+ body: {
261
+ content: { text: text },
262
+ },
263
+ metaData: {
264
+ source: { id: business_id },
265
+ inReplyTo: { id: video_id },
266
+ },
267
+ } = document;
268
+ const { body: publishedMessage } =
269
+ (await this.sendPost({
270
+ paramString: 'comment/create/',
271
+ postData: {
272
+ business_id,
273
+ video_id,
274
+ text,
275
+ },
276
+ document,
277
+ })) || {};
278
+ return publishedMessage;
279
+ }
280
+
281
+ async insertReply(videoId, document, markMessageAsCompleteDocumentId) {
282
+ const {
283
+ body: {
284
+ content: { text: text },
285
+ },
286
+ metaData: {
287
+ inReplyTo: { id: comment_id, profileId: business_id },
288
+ },
289
+ } = document;
290
+
291
+ const { body: publishedMessage } =
292
+ (await this.sendPost({
293
+ paramString: 'comment/reply/create/',
294
+ postData: {
295
+ business_id: document.metaData.source.id,
296
+ comment_id: markMessageAsCompleteDocumentId || comment_id,
297
+ video_id: removePrefix(videoId),
298
+ text,
299
+ },
300
+ document,
301
+ })) || {};
302
+
303
+ return publishedMessage;
304
+ }
305
+
306
+ async toggleLike(document) {
307
+ const {
308
+ enrichments: {
309
+ socialScores: { tt_liked_by_user: likedByUser },
310
+ },
311
+ } = document;
312
+
313
+ try {
314
+ let likeResponse = await (!likedByUser
315
+ ? this.like(document)
316
+ : this.unlike(document));
317
+ return likeResponse;
318
+ } catch (error) {
319
+ this.logger.error(`${document} - error recieved - ${error.code}`, error);
320
+ throw error;
321
+ }
322
+ }
323
+
324
+ async toggleHide(document) {
325
+ const {
326
+ documentId,
327
+ appData: { hidden: hiddenOnNative },
328
+ systemData: {
329
+ policies: { storage: { privateTo: companyId } = {} } = {},
330
+ } = {},
331
+ } = document;
332
+ let hideResponse;
333
+ let notificationPayload = document;
334
+
335
+ try {
336
+ if (hiddenOnNative) {
337
+ hideResponse = await this.hide(document);
338
+ } else {
339
+ hideResponse = await this.unhide(document);
340
+ }
341
+
342
+ notificationPayload.appData.hideStatus = {
343
+ code: hideResponse.success === true ? 200 : 500,
344
+ message:
345
+ hideResponse.success === true
346
+ ? 'Success'
347
+ : 'Failed to Hide',
348
+ };
349
+ this.logger.debug(
350
+ `Notification payload for ${documentId} on company ${companyId} `,
351
+ notificationPayload
352
+ );
353
+ } catch (error) {
354
+ this.logger.error(
355
+ `${documentId} - error recieved - ${error.code}`,
356
+ error
357
+ );
358
+ // message failed ot send, mark as such
359
+ notificationPayload.appData.hideStatus = {
360
+ code: 405,
361
+ message: 'Exception Occurred',
362
+ };
363
+ }
364
+ setTimeout(() => {
365
+ PushRepository.publish(
366
+ `${companyId}-${documentId}`,
367
+ notificationPayload
368
+ );
369
+ }, 5000);
370
+ }
371
+
372
+ async like(document) {
373
+ const {
374
+ metaData: {
375
+ externalId: comment_id,
376
+ source: { id: sourceId },
377
+ },
378
+ } = document;
379
+ const { body: publishedMessage } =
380
+ (await this.sendPost({
381
+ paramString: 'comment/like/',
382
+ postData: {
383
+ business_id: removePrefix(sourceId),
384
+ comment_id: removePrefix(comment_id),
385
+ action: 'LIKE',
386
+ },
387
+ document,
388
+ })) || {};
389
+ return publishedMessage;
390
+ }
391
+
392
+ async unlike(document) {
393
+ const {
394
+ metaData: {
395
+ inReplyTo: { id: comment_id },
396
+ source: { id: sourceId },
397
+ },
398
+ } = document;
399
+ const { body: publishedMessage } =
400
+ (await this.sendPost({
401
+ paramString: 'comment/like/',
402
+ postData: {
403
+ business_id: removePrefix(sourceId),
404
+ comment_id: removePrefix(comment_id),
405
+ action: 'unlike',
406
+ },
407
+ document,
408
+ })) || {};
409
+ return publishedMessage;
410
+ }
411
+
412
+ async hide(document) {
413
+ const {
414
+ metaData: {
415
+ inReplyTo: { id: comment_id, profileId: business_id },
416
+ },
417
+ } = document;
418
+ const { body: publishedMessage } =
419
+ (await this.sendPost({
420
+ paramString: 'comment/hide/',
421
+ postData: {
422
+ business_id,
423
+ comment_id,
424
+ action: 'HIDE',
425
+ },
426
+ document,
427
+ })) || {};
428
+ return publishedMessage;
429
+ }
430
+
431
+ async unhide(document) {
432
+ const {
433
+ metaData: {
434
+ inReplyTo: { id: comment_id, profileId: business_id },
435
+ },
436
+ } = document;
437
+ const { body: publishedMessage } =
438
+ (await this.sendPost({
439
+ paramString: 'comment/hide/',
440
+ postData: {
441
+ business_id,
442
+ comment_id,
443
+ action: 'UNHIDE',
444
+ },
445
+ document,
446
+ })) || {};
447
+ return publishedMessage;
448
+ }
449
+
450
+ async deleteComment(document) {
451
+ const {
452
+ metaData: {
453
+ inReplyTo: { id: comment_id, profileId: business_id },
454
+ },
455
+ } = document;
456
+ const { body: publishedMessage } =
457
+ (await this.sendPost({
458
+ paramString: 'comment/delete/',
459
+ postData: {
460
+ business_id,
461
+ comment_id,
462
+ },
463
+ document,
464
+ })) || {};
465
+ return publishedMessage;
466
+ }
467
+
468
+ async getProfile(business_id, document) {
469
+ const fields = '["username", "display_name", "profile_image"]';
470
+ const profile =
471
+ (await this.sendRequest({
472
+ paramString: `get/?business_id=${business_id}&fields=${fields}`,
473
+ document,
474
+ })) || {};
475
+ return profile;
476
+ }
477
+
478
+ async getPostData(business_id, video_id, document) {
479
+ const fields =
480
+ '["item_id", "thumbnail_url", "create_time", "username", "display_name", "profile_image"]';
481
+ const video_ids = `["${video_id}"]`;
482
+ const profile =
483
+ (await this.sendRequest({
484
+ paramString: `video/list?business_id=${business_id}&fields=${fields}&filters={"video_ids":${video_ids}}`,
485
+ document,
486
+ })) || {};
487
+ return profile;
488
+ }
489
+ }
@@ -10,6 +10,7 @@ import { InstagramApiClient } from './http/instagramApi.client.js';
10
10
  import { InstagramVideoClient } from './http/InstagramVideoClient.js';
11
11
  import { IRClient } from './http/ir.client.js';
12
12
  import { LinkedInApiClient } from './http/linkedInApi.client.js';
13
+ import { TikTokApiClient } from './http/tiktokApi.client.js';
13
14
  import { MasfClient } from './http/masf.client.js';
14
15
  import { WarpZoneApiClient } from './http/WarpZoneApi.client.js';
15
16
 
@@ -26,6 +27,7 @@ export {
26
27
  InstagramVideoClient,
27
28
  IRClient,
28
29
  LinkedInApiClient,
30
+ TikTokApiClient,
29
31
  MasfClient,
30
32
  WarpZoneApiClient,
31
33
  };