@meltwater/conversations-api-services 1.0.0

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.
@@ -0,0 +1,477 @@
1
+ import superagent from 'superagent';
2
+ import configuration from '../../lib/configuration.js';
3
+ import logger from '../../lib/logger.js';
4
+ import { removePrefix } from '../../lib/externalId.helpers.js';
5
+ import { CredentialsApiClient } from '../http/credentialsApi.client.js';
6
+
7
+ export class TikTokApiClient {
8
+ constructor() {
9
+ this.tiktokURL = configuration.get('TIKTOK_API_URL');
10
+ this.credentialsAPI = new CredentialsApiClient();
11
+ }
12
+
13
+ async getAuthorization(document) {
14
+ const {
15
+ documentId,
16
+ systemData: {
17
+ connectionsCredential: credentialId,
18
+ policies: { storage: { privateTo: companyId } = {} } = {},
19
+ } = {},
20
+ } = document;
21
+
22
+ try {
23
+ const credential = await this.credentialsAPI.getToken(
24
+ credentialId,
25
+ companyId
26
+ );
27
+ return `${credential.token}`;
28
+ } catch (error) {
29
+ logger.error(
30
+ `${documentId} - error getting tiktok token - ${error.code}`,
31
+ error
32
+ );
33
+ }
34
+ }
35
+
36
+ async sendPost({
37
+ paramString = '',
38
+ headers = {},
39
+ document,
40
+ postData = undefined,
41
+ }) {
42
+ let response = {};
43
+ try {
44
+ response = await superagent
45
+ .post(this.tiktokURL + paramString)
46
+ .set('Accept', 'application/json')
47
+ .set('Content-Type', 'application/json')
48
+ .set('Access-Token', await this.getAuthorization(document))
49
+ .send(postData);
50
+ } catch (err) {
51
+ let errorText = '';
52
+ if (
53
+ err &&
54
+ err.response &&
55
+ err.response.body &&
56
+ err.response.body.error
57
+ ) {
58
+ errorText = `Failed to call tiktok api for paramString ${paramString}: ${err.response.body.error.message}`;
59
+ logger.error(errorText);
60
+ } else {
61
+ errorText = `Failed to call tiktok api for paramString ${paramString}`;
62
+ logger.error(errorText, err);
63
+ }
64
+
65
+ throw new Error(errorText);
66
+ }
67
+
68
+ return response;
69
+ }
70
+
71
+ // assumes batch calls are using the same credential
72
+ async sendRequest({ paramString = '', headers = {}, document }) {
73
+ let response = {};
74
+ try {
75
+ response = await superagent
76
+ .get(this.tiktokURL + paramString)
77
+ .set('Accept', 'application/json')
78
+ .set('Content-Type', 'application/json')
79
+ .set('Access-Token', await this.getAuthorization(document))
80
+ .send();
81
+ } catch (err) {
82
+ let errorText = '';
83
+ if (
84
+ err &&
85
+ err.response &&
86
+ err.response.body &&
87
+ err.response.body.error
88
+ ) {
89
+ errorText = `Failed to call tiktok api for paramString ${paramString}: ${err.response.body.error.message}`;
90
+ logger.error(errorText);
91
+ } else {
92
+ errorText = `Failed to call tiktok api for paramString ${paramString}`;
93
+ logger.error(errorText, err);
94
+ }
95
+
96
+ throw new Error(errorText);
97
+ }
98
+
99
+ if (response.status !== 200) {
100
+ logger.error(
101
+ `Failed to call tiktok api for documentId ${documentId}`,
102
+ response.body
103
+ );
104
+ let error = new Error(
105
+ `Failed to call tiktok api for documentId ${documentId}`
106
+ );
107
+ error.code = response.status;
108
+ throw error;
109
+ }
110
+
111
+ return response.body;
112
+ }
113
+
114
+ async getChannelInfoForDocuments(documents) {
115
+ const channelInfo = await this.getStatsForDocuments(
116
+ documents,
117
+ 'metaData.authors.0.authorInfo.externalId',
118
+ {
119
+ og: ['video/list/'],
120
+ }
121
+ );
122
+ return channelInfo;
123
+ }
124
+
125
+ async getStatsForDocuments(
126
+ documents,
127
+ externalIdFrom = 'metaData.externalId',
128
+ paramsByType = {
129
+ og: ['video/list/'],
130
+ }
131
+ ) {
132
+ // for batching separate og and comment/reply requests per credential
133
+ const documentsByTypeAndCredentials = documents.reduce(
134
+ (retObj, document, index) => {
135
+ const paramArray =
136
+ paramsByType[document.metaData.discussionType];
137
+ if (paramArray) {
138
+ paramArray.forEach((paramString) => {
139
+ let discussionTypeBucket =
140
+ retObj[
141
+ `${paramString}${document.systemData.connectionsCredential}`
142
+ ];
143
+ if (!discussionTypeBucket) {
144
+ discussionTypeBucket = {
145
+ paramString,
146
+ document,
147
+ externalIds: [],
148
+ externalIdToDocumentIndex: {},
149
+ };
150
+ retObj[
151
+ `${paramString}${document.systemData.connectionsCredential}`
152
+ ] = discussionTypeBucket;
153
+ }
154
+ const externalIdTrimmed = removePrefix(
155
+ // javascript weirdness to get around '.' notation
156
+ externalIdFrom
157
+ .split('.')
158
+ .reduce((a, b) => a[b], document)
159
+ );
160
+ discussionTypeBucket.externalIds.push(
161
+ externalIdTrimmed
162
+ );
163
+ discussionTypeBucket.externalIdToDocumentIndex[
164
+ externalIdTrimmed
165
+ ] = index;
166
+ });
167
+ }
168
+ return retObj;
169
+ },
170
+ {}
171
+ );
172
+
173
+ const sortedResults = Array(documents.length).fill();
174
+ await Promise.all(
175
+ Object.values(documentsByTypeAndCredentials).map(
176
+ async (documentsByTypeAndCredential) => {
177
+ if (documentsByTypeAndCredential.externalIds.length) {
178
+ const {
179
+ metaData: {
180
+ inReplyTo: { profileId: business_id },
181
+ },
182
+ } = documentsByTypeAndCredential.document;
183
+ const trimmedBusinessId = removePrefix(business_id);
184
+ const requestResult = await this.sendRequest({
185
+ paramString: `${
186
+ documentsByTypeAndCredential.paramString
187
+ }?business_id=${trimmedBusinessId}&fields=["item_id","create_time","thumbnail_url","share_url","embed_url","caption","video_views","likes","comments","shares"]&filters=${JSON.stringify(
188
+ {
189
+ video_ids:
190
+ documentsByTypeAndCredential.externalIds,
191
+ }
192
+ )}`,
193
+ document: documentsByTypeAndCredential.document,
194
+ });
195
+ requestResult.data.videos.forEach(
196
+ ({
197
+ item_id,
198
+ create_time,
199
+ thumbnail_url,
200
+ share_url,
201
+ embed_url,
202
+ caption,
203
+ video_views,
204
+ likes,
205
+ comments,
206
+ shares,
207
+ ...args
208
+ }) => {
209
+ const documentIndex =
210
+ documentsByTypeAndCredential
211
+ .externalIdToDocumentIndex[item_id];
212
+ sortedResults[documentIndex] = {
213
+ ...sortedResults[documentIndex],
214
+ item_id,
215
+ create_time,
216
+ thumbnail_url,
217
+ externalId:
218
+ documents[documentIndex].metaData
219
+ .externalId,
220
+ share_url,
221
+ embed_url,
222
+ caption,
223
+ video_views,
224
+ likes,
225
+ comments,
226
+ shares,
227
+ };
228
+ }
229
+ );
230
+ }
231
+ }
232
+ )
233
+ );
234
+ return sortedResults;
235
+ }
236
+
237
+ async publish(message, videoId, markMessageAsCompleteDocumentId) {
238
+ const {
239
+ metaData: { discussionType },
240
+ } = message;
241
+ let publishedMessage;
242
+
243
+ switch (discussionType) {
244
+ case 'qt':
245
+ publishedMessage = await this.insertComment(message);
246
+ break;
247
+ case 're':
248
+ publishedMessage = await this.insertReply(
249
+ videoId,
250
+ message,
251
+ markMessageAsCompleteDocumentId
252
+ );
253
+ break;
254
+ }
255
+
256
+ return publishedMessage;
257
+ }
258
+
259
+ async insertComment(document) {
260
+ const {
261
+ body: {
262
+ content: { text: text },
263
+ },
264
+ metaData: {
265
+ source: { id: business_id },
266
+ inReplyTo: { id: video_id },
267
+ },
268
+ } = document;
269
+ const { body: publishedMessage } =
270
+ (await this.sendPost({
271
+ paramString: 'comment/create/',
272
+ postData: {
273
+ business_id,
274
+ video_id,
275
+ text,
276
+ },
277
+ document,
278
+ })) || {};
279
+ return publishedMessage;
280
+ }
281
+
282
+ async insertReply(videoId, document, markMessageAsCompleteDocumentId) {
283
+ const {
284
+ body: {
285
+ content: { text: text },
286
+ },
287
+ metaData: {
288
+ inReplyTo: { id: comment_id, profileId: business_id },
289
+ },
290
+ } = document;
291
+
292
+ const { body: publishedMessage } =
293
+ (await this.sendPost({
294
+ paramString: 'comment/reply/create/',
295
+ postData: {
296
+ business_id: document.metaData.source.id,
297
+ comment_id: markMessageAsCompleteDocumentId || comment_id,
298
+ video_id: removePrefix(videoId),
299
+ text,
300
+ },
301
+ document,
302
+ })) || {};
303
+
304
+ return publishedMessage;
305
+ }
306
+
307
+ async toggleLike(document) {
308
+ const {
309
+ enrichments: {
310
+ socialScores: { tt_liked_by_user: likedByUser },
311
+ },
312
+ } = document;
313
+
314
+ try {
315
+ let likeResponse = await (!likedByUser
316
+ ? this.like(document)
317
+ : this.unlike(document));
318
+ return likeResponse;
319
+ } catch (error) {
320
+ logger.error(`${document} - error recieved - ${error.code}`, error);
321
+ throw error;
322
+ }
323
+ }
324
+
325
+ async toggleHide(document) {
326
+ const {
327
+ documentId,
328
+ appData: { hidden: hiddenOnNative },
329
+ } = document;
330
+
331
+ let response;
332
+ try {
333
+ if (hiddenOnNative) {
334
+ response = await this.hide(document);
335
+ } else {
336
+ response = await this.unhide(document);
337
+ }
338
+ logger.info(
339
+ `Native Tiktok API Hide Response for documentId ${documentId}`,
340
+ { status: response.status, ok: response.ok }
341
+ );
342
+
343
+ if (response.message === 'OK') {
344
+ return response;
345
+ }
346
+ } catch (error) {
347
+ logger.error(
348
+ `${documentId} - error recieved - ${error.code}`,
349
+ error
350
+ );
351
+ }
352
+ }
353
+
354
+ async like(document) {
355
+ const {
356
+ metaData: {
357
+ externalId: comment_id,
358
+ source: { id: sourceId },
359
+ },
360
+ } = document;
361
+ const { body: publishedMessage } =
362
+ (await this.sendPost({
363
+ paramString: 'comment/like/',
364
+ postData: {
365
+ business_id: removePrefix(sourceId),
366
+ comment_id: removePrefix(comment_id),
367
+ action: 'LIKE',
368
+ },
369
+ document,
370
+ })) || {};
371
+ return publishedMessage;
372
+ }
373
+
374
+ async unlike(document) {
375
+ const {
376
+ metaData: {
377
+ inReplyTo: { id: comment_id },
378
+ source: { id: sourceId },
379
+ },
380
+ } = document;
381
+ const { body: publishedMessage } =
382
+ (await this.sendPost({
383
+ paramString: 'comment/like/',
384
+ postData: {
385
+ business_id: removePrefix(sourceId),
386
+ comment_id: removePrefix(comment_id),
387
+ action: 'unlike',
388
+ },
389
+ document,
390
+ })) || {};
391
+ return publishedMessage;
392
+ }
393
+
394
+ async hide(document) {
395
+ const {
396
+ metaData: {
397
+ externalId: comment_id,
398
+ source: { id: sourceId },
399
+ inReplyTo: { id: video_id },
400
+ },
401
+ } = document;
402
+ const { body: publishedMessage } =
403
+ (await this.sendPost({
404
+ paramString: 'comment/hide/',
405
+ postData: {
406
+ business_id: removePrefix(sourceId),
407
+ comment_id: removePrefix(comment_id),
408
+ video_id: removePrefix(video_id),
409
+ action: 'HIDE',
410
+ },
411
+ document,
412
+ })) || {};
413
+ return publishedMessage;
414
+ }
415
+
416
+ async unhide(document) {
417
+ const {
418
+ metaData: {
419
+ externalId: comment_id,
420
+ source: { id: sourceId },
421
+ inReplyTo: { id: video_id },
422
+ },
423
+ } = document;
424
+ const { body: publishedMessage } =
425
+ (await this.sendPost({
426
+ paramString: 'comment/hide/',
427
+ postData: {
428
+ business_id: removePrefix(sourceId),
429
+ comment_id: removePrefix(comment_id),
430
+ video_id: removePrefix(video_id),
431
+ action: 'UNHIDE',
432
+ },
433
+ document,
434
+ })) || {};
435
+ return publishedMessage;
436
+ }
437
+
438
+ async deleteComment(document) {
439
+ const {
440
+ metaData: {
441
+ inReplyTo: { id: comment_id, profileId: business_id },
442
+ },
443
+ } = document;
444
+ const { body: publishedMessage } =
445
+ (await this.sendPost({
446
+ paramString: 'comment/delete/',
447
+ postData: {
448
+ business_id,
449
+ comment_id,
450
+ },
451
+ document,
452
+ })) || {};
453
+ return publishedMessage;
454
+ }
455
+
456
+ async getProfile(business_id, document) {
457
+ const fields = '["username", "display_name", "profile_image"]';
458
+ const profile =
459
+ (await this.sendRequest({
460
+ paramString: `get/?business_id=${business_id}&fields=${fields}`,
461
+ document,
462
+ })) || {};
463
+ return profile;
464
+ }
465
+
466
+ async getPostData(business_id, video_id, document) {
467
+ const fields =
468
+ '["item_id", "thumbnail_url", "create_time", "username", "display_name", "profile_image"]';
469
+ const video_ids = `["${video_id}"]`;
470
+ const profile =
471
+ (await this.sendRequest({
472
+ paramString: `video/list?business_id=${business_id}&fields=${fields}&filters={"video_ids":${video_ids}}`,
473
+ document,
474
+ })) || {};
475
+ return profile;
476
+ }
477
+ }
@@ -0,0 +1,33 @@
1
+ import { awsS3Client } from './http/amazonS3.js';
2
+ import { assetManagerTvmRepository } from './http/asset-manager-tvm.client.js';
3
+ import { CompanyApiClient } from './http/companiesApi.client.js';
4
+ import { CredentialsApiClient } from './http/credentialsApi.client.js';
5
+ import { EntitlementsApiClient } from './http/entitlementsApi.client.js';
6
+ import { FacebookApiClient } from './http/facebookApi.client.js';
7
+ import { FeatureToggleClient } from './http/featureToggleApi.client.js';
8
+ import { IdentityServicesClient } from './http/identityServices.client.js';
9
+ import { InstagramApiClient } from './http/instagramApi.client.js';
10
+ import { InstagramVideoClient } from './http/InstagramVideoClient.js';
11
+ import { IRClient } from './http/ir.client.js';
12
+ import { LinkedInApiClient } from './http/linkedInApi.client.js';
13
+ import { TikTokApiClient } from './http/tiktokApi.client.js';
14
+ import { MasfClient } from './http/masf.client.js';
15
+ import { WarpZoneApiClient } from './http/WarpZoneApi.client.js';
16
+
17
+ export {
18
+ awsS3Client,
19
+ assetManagerTvmRepository,
20
+ CompanyApiClient,
21
+ CredentialsApiClient,
22
+ EntitlementsApiClient,
23
+ FacebookApiClient,
24
+ FeatureToggleClient,
25
+ IdentityServicesClient,
26
+ InstagramApiClient,
27
+ InstagramVideoClient,
28
+ IRClient,
29
+ LinkedInApiClient,
30
+ TikTokApiClient,
31
+ MasfClient,
32
+ WarpZoneApiClient,
33
+ };
@@ -0,0 +1,22 @@
1
+ export function parseApplicationTags(tags) {
2
+ const parsedTags = {};
3
+ if (!tags) {
4
+ return parsedTags;
5
+ }
6
+
7
+ tags.map((tag) => {
8
+ const [key, value] = tag.split('=');
9
+ parsedTags[key] = value;
10
+ });
11
+
12
+ return parsedTags;
13
+ }
14
+
15
+ export function checkApplicationTagsForUserLikes(applicationTags) {
16
+ const appTags = parseApplicationTags(applicationTags);
17
+ if (appTags.userLikes === 'true') {
18
+ return true;
19
+ } else {
20
+ return false;
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ export default {
2
+ get: (varName) => {
3
+ const value = process.env[varName];
4
+
5
+ if (value === 'true') return true;
6
+ if (value === 'false') return false;
7
+
8
+ return value;
9
+ },
10
+ };
@@ -0,0 +1,6 @@
1
+ export default {
2
+ MARK_AS_TODO: 'MARK_AS_TODO',
3
+ MARK_AS_COMPLETE: 'MARK_AS_COMPLETE',
4
+ USER_STARTED_RESPONDING: 'USER_STARTED_RESPONDING',
5
+ USER_STOPPED_RESPONDING: 'USER_STOPPED_RESPONDING',
6
+ };
@@ -0,0 +1,15 @@
1
+ export function removePrefix(document) {
2
+ return document && document.replace(/id\:[a-zA-Z]+\.com:/g, '');
3
+ }
4
+
5
+ export function twitterPrefixCheck(socialOriginType, value) {
6
+ let result = value;
7
+
8
+ if (socialOriginType === 'twitter') {
9
+ if (!value.includes('id:twitter.com:')) {
10
+ result = 'id:twitter.com:' + value;
11
+ }
12
+ }
13
+
14
+ return result;
15
+ }
@@ -0,0 +1,100 @@
1
+ import logger from '../lib/logger.js';
2
+ import { IRClient } from '../data-access/http/ir.clien.jst';
3
+ import XRunes from '@meltwater/xrunes';
4
+ import xRunesCore from '@meltwater/xrunes-core';
5
+
6
+ class HiddenCommentHelper {
7
+ constructor({ company, user, traceId }) {
8
+ this.irClient = new IRClient({ company, user, traceId });
9
+
10
+ this.xRunes = new XRunes();
11
+ this.xRunes.registerLibrary(xRunesCore);
12
+ }
13
+
14
+ shouldHandle(message) {
15
+ return message.metaData.discussionType === 'qt';
16
+ }
17
+
18
+ async hideChildren(message, markHidden) {
19
+ let {
20
+ body: { publishDate: { date: publishDate } = {} } = {},
21
+ metaData: { externalId, source: { socialOriginType } = {} } = {},
22
+ systemData: {
23
+ connectionsCredential: credentialId,
24
+ policies: { storage: { privateTo: companyId } = {} } = {},
25
+ } = {},
26
+ } = message;
27
+
28
+ let operation;
29
+ let query = {
30
+ type: 'x:match',
31
+ matchQuery: {
32
+ type: 'all',
33
+ allQueries: [
34
+ {
35
+ type: 'x:dateRange',
36
+ field: 'body.publishDate.date',
37
+ from: publishDate,
38
+ },
39
+ {
40
+ type: 'term',
41
+ field: 'metaData.inReplyTo.externalId',
42
+ value: externalId,
43
+ },
44
+ ],
45
+ },
46
+ };
47
+
48
+ if (markHidden) {
49
+ operation = 'addToSet';
50
+ query.notMatchQuery = {
51
+ type: 'term',
52
+ field: 'metaData.applicationTags',
53
+ value: 'parentHidden',
54
+ };
55
+ } else {
56
+ operation = 'removeFromSet';
57
+ query.matchQuery.allQueries.push({
58
+ type: 'term',
59
+ field: 'metaData.applicationTags',
60
+ value: 'parentHidden',
61
+ });
62
+ }
63
+
64
+ let runes = {
65
+ query,
66
+ transformers: [
67
+ {
68
+ type: 'idml',
69
+ script: `action = "update" target = "revision" revisionGroup = "${companyId}" documentId = id operations = [{"operation": "${operation}", "value": ["parentHidden"], "fieldPath": "metaData.applicationTags"}]`,
70
+ },
71
+ ],
72
+ overlayGroups: [companyId],
73
+ fields: ['id'],
74
+ outputConfiguration: {
75
+ type: 's3',
76
+ region: 'eu-west-1',
77
+ roleArn: process.env.EXPORT_API_ROLE_ARN,
78
+ externalId: process.env.EXPORT_API_EXTERNAL_ID,
79
+ s3BucketName: process.env.EXPORT_API_S3_BUCKET,
80
+ s3KeyPrefix: process.env.EXPORT_API_S3_PREFIX, // the property is optional, the trailing / isn't
81
+ batchSize: 1000, // the property is optional, must be >= 100 and <= 20000. Default is 20000
82
+ },
83
+ metaData: {
84
+ companyId,
85
+ },
86
+ };
87
+
88
+ runes.query = await this.xRunes.render(runes.query);
89
+
90
+ logger.debug(`export api query: ${JSON.stringify(runes.query)}`);
91
+
92
+ let results = await this.irClient.export(runes);
93
+
94
+ logger.debug(`export api result: ${JSON.stringify(results)}`);
95
+
96
+ return results;
97
+ }
98
+ }
99
+
100
+ export default HiddenCommentHelper;
@@ -0,0 +1,14 @@
1
+ import winston from 'winston';
2
+
3
+ const logger = winston.createLogger({
4
+ level: process.env.LOG_LEVEL || 'info',
5
+ format: winston.format.json(),
6
+ defaultMeta: {},
7
+ transports: [
8
+ new winston.transports.Console({
9
+ format: winston.format.json(),
10
+ }),
11
+ ],
12
+ });
13
+
14
+ export default logger;