@meltwater/conversations-api-services 1.0.3 → 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.3",
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",
@@ -394,7 +394,7 @@ export class FacebookApiClient {
394
394
  const {
395
395
  documentId,
396
396
  appData: { hidden: hiddenOnNative },
397
- metaData: { externalId: externalId },
397
+ metaData: { externalId },
398
398
  systemData: {
399
399
  connectionsCredential: credentialId,
400
400
  policies: { storage: { privateTo: companyId } = {} } = {},
@@ -82,7 +82,12 @@ export class LinkedInApiClient {
82
82
 
83
83
  this.logger.info(
84
84
  `Native Linkedin API Like Comment Response for documentId ${documentId}`,
85
- { response, likedByUser }
85
+ {
86
+ ok: response.ok,
87
+ status: response.status,
88
+ message: JSON.parse(response.text),
89
+ likedByUser,
90
+ }
86
91
  );
87
92
  } catch (err) {
88
93
  this.logger.error(documentId + ' - exception details', {
@@ -141,12 +146,20 @@ export class LinkedInApiClient {
141
146
 
142
147
  this.logger.info(
143
148
  `Native Linkedin API Delete Comment Response for documentId ${documentId}`,
144
- { response }
149
+ { status: response.status, ok: response.ok }
145
150
  );
146
151
  } catch (err) {
147
- this.logger.error(documentId + ' - exception details', {
148
- err,
149
- });
152
+ if (err && err.response && err.response.text) {
153
+ this.logger.error(
154
+ `Call to linkedin api with documentId ${documentId} failed`,
155
+ { message: JSON.parse(err.response.text) }
156
+ );
157
+ } else {
158
+ this.logger.error(
159
+ `Call to linkedin api with documentId ${documentId} failed`,
160
+ err
161
+ );
162
+ }
150
163
  throw err;
151
164
  }
152
165
  return response;
@@ -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
  };