@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,35 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class FeatureToggleClient {
6
+ constructor() {
7
+ this.featureToggleUrl = configuration.get('FEATURE_TOGGLE_URL');
8
+
9
+ this.featureToggleNames = [
10
+ 'CONNECTIONS-SOCIAL-GOVERNANCE',
11
+ 'conversations-googlebusiness-display',
12
+ 'CONVERSATIONS_FACEBOOK_MENTIONS',
13
+ ];
14
+ }
15
+
16
+ async get(companyId, jwt) {
17
+ try {
18
+ const result = await superagent
19
+ .post(`${this.featureToggleUrl}/treatments/${companyId}`)
20
+ .set('Authorization', jwt)
21
+ .send({
22
+ features: this.featureToggleNames,
23
+ attributes: {},
24
+ })
25
+ .timeout(5000);
26
+
27
+ return result.body.treatments;
28
+ } catch (error) {
29
+ logger.error(
30
+ `Failed requesting Feature Toggle service for feature toggle get `,
31
+ { error, companyId }
32
+ );
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,117 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class IdentityServicesClient {
6
+ constructor({ companyId, services }) {
7
+ this.companyId = companyId;
8
+ this.identityServicesUrl = configuration.get('IDENTITY_SERVICES_URL');
9
+
10
+ this.authService = services.auth;
11
+ }
12
+
13
+ async getById(userId, jwt, retries = 0) {
14
+ let user;
15
+ try {
16
+ let resignedToken = await this.authService.getResignedToken(jwt);
17
+ user = await superagent
18
+ .get(`${this.identityServicesUrl}/users/${userId}`)
19
+ .set('Authorization', resignedToken)
20
+ .set('x-client-name', configuration.get('CLIENT_NAME_HEADER'))
21
+ .then((result) => result.body.user);
22
+ } catch (error) {
23
+ if (retries <= 3) {
24
+ logger.error(
25
+ `getById Failed requesting Identity Services for userId ${userId} retry ${retries}`,
26
+ error
27
+ );
28
+ return this.getById(userId, jwt, ++retries);
29
+ } else {
30
+ logger.error(
31
+ `getById Failed requesting Identity Services app settings for userId ${userId} complete failure`,
32
+ error
33
+ );
34
+ }
35
+ return;
36
+ }
37
+
38
+ return user;
39
+ }
40
+
41
+ async getAllUsers(jwt, retries = 0) {
42
+ let users;
43
+ try {
44
+ let resignedToken = await this.authService.getResignedToken(jwt);
45
+ users = await superagent
46
+ .get(
47
+ `${this.identityServicesUrl}/companies/${this.companyId}/users`
48
+ )
49
+ .set('Authorization', resignedToken)
50
+ .set('x-client-name', configuration.get('CLIENT_NAME_HEADER'))
51
+ .then((result) => result.body.map((record) => record.user));
52
+ } catch (error) {
53
+ if (retries <= 3) {
54
+ logger.error(
55
+ `getAllUsers Failed requesting Identity Services for ${this.companyId} retrying ${retries}`,
56
+ error
57
+ );
58
+ return await this.getAllUsers(jwt, ++retries);
59
+ } else {
60
+ logger.error(
61
+ `getAllUsers Failed requesting Identity Services app settings for companyId ${companyId} complete failure`,
62
+ error
63
+ );
64
+ }
65
+ return;
66
+ }
67
+
68
+ return users;
69
+ }
70
+
71
+ async getUsersAppSettings({ userIds, settingNames, jwt }, retries = 0) {
72
+ if (!settingNames) {
73
+ return {};
74
+ } else {
75
+ let appSettings;
76
+ try {
77
+ let resignedToken = await this.authService.getResignedToken(
78
+ jwt
79
+ );
80
+ appSettings = await superagent
81
+ .post(
82
+ `${this.identityServicesUrl}/users/app_settings/batch_get`
83
+ )
84
+ .set('Authorization', resignedToken)
85
+ .set(
86
+ 'x-client-name',
87
+ configuration.get('CLIENT_NAME_HEADER')
88
+ )
89
+ .send({
90
+ userIds,
91
+ settingNames: settingNames,
92
+ })
93
+ .then((result) => result.body);
94
+ } catch (error) {
95
+ if (retries <= 3) {
96
+ logger.error(
97
+ `getUsersAppSettings Failed requesting Identity Services app settings for userIds ${userIds} retrying ${retries}`,
98
+ error
99
+ );
100
+ return await this.getUsersAppSettings(
101
+ { userIds, settingNames, jwt },
102
+ ++retries
103
+ );
104
+ } else {
105
+ logger.error(
106
+ `getUsersAppSettings Failed requesting Identity Services app settings for userIds ${userIds} complete failure`,
107
+ error
108
+ );
109
+ }
110
+
111
+ return;
112
+ }
113
+
114
+ return appSettings;
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,496 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+ import { removePrefix } from '../../lib/externalId.helpers.js';
4
+ import assert from 'assert';
5
+ import { CredentialsApiClient } from '../http/credentialsApi.client.js';
6
+
7
+ const INSTAGRAM_URL = 'https://graph.facebook.com';
8
+ export class InstagramApiClient {
9
+ constructor() {
10
+ this.credentialsAPI = new CredentialsApiClient();
11
+ }
12
+
13
+ async validate(accessToken, payload) {
14
+ const { inReplyToId, text } = payload;
15
+ assert(accessToken, 'AccessToken is required.');
16
+ assert(inReplyToId, 'inReplyToId is required.');
17
+ assert(text, 'text is required.');
18
+ }
19
+
20
+ async requestApi(
21
+ apiUrl,
22
+ accessToken,
23
+ text,
24
+ documentId,
25
+ data,
26
+ discussionType = 're',
27
+ adCampaign = undefined
28
+ ) {
29
+ let response = {};
30
+ try {
31
+ let queryStringArgs = { access_token: accessToken };
32
+ if (adCampaign) {
33
+ queryStringArgs.ad_id = adCampaign.adAccountId;
34
+ }
35
+ if (text) {
36
+ queryStringArgs[discussionType === 'dm' ? 'text' : 'message'] =
37
+ text;
38
+ }
39
+
40
+ response = await superagent
41
+ .post(apiUrl)
42
+ .set('Accept', 'application/json')
43
+ .set('Content-Type', 'application/json')
44
+ .query(queryStringArgs)
45
+ .send(data);
46
+ } catch (err) {
47
+ let errorText = '';
48
+ if (
49
+ err &&
50
+ err.response &&
51
+ err.response.body &&
52
+ err.response.body.error
53
+ ) {
54
+ errorText =
55
+ 'Failed to call instagram api for documentId ${documentId}: ${err.response.body.error.message}';
56
+ logger.error(errorText);
57
+ } else {
58
+ errorText =
59
+ 'Failed to call instagram api for documentId ${documentId}';
60
+ logger.error(errorText, err);
61
+ }
62
+ throw new Error(errorText);
63
+ }
64
+
65
+ if (response.status !== 200) {
66
+ logger.error(
67
+ `Failed to call instagram api for documentId ${documentId}`,
68
+ response.body
69
+ );
70
+ let error = new Error(
71
+ `Failed to call instagram api for documentId ${documentId}`
72
+ );
73
+ error.code = response.status;
74
+ throw error;
75
+ }
76
+ logger.debug('Instagram Response status', response.status);
77
+ logger.debug('Instagram Response body', response.body);
78
+ logger.debug('Instagram Response body.id', response.body.id);
79
+ return response;
80
+ }
81
+
82
+ async getLikes(accessToken, payload) {
83
+ let response;
84
+ const { externalId, documentId } = payload;
85
+
86
+ logger.debug(
87
+ `Starting to call instagram getPost api for documentId ${documentId}`,
88
+ { payload }
89
+ );
90
+ try {
91
+ response = await this.getPost(
92
+ `${INSTAGRAM_URL}/${removePrefix(externalId)}`,
93
+ accessToken,
94
+ 'like_count',
95
+ documentId
96
+ );
97
+
98
+ logger.info(
99
+ `Native Instagram API getPost Response for documentId ${documentId}`,
100
+ { status: response.status, ok: response.ok }
101
+ );
102
+ return response.body;
103
+ } catch (err) {
104
+ logger.error('Error getLikes Instagram', { payload });
105
+ return 0;
106
+ }
107
+ }
108
+
109
+ async mentionRequestApi(
110
+ apiUrl,
111
+ accessToken,
112
+ replayObject,
113
+ text,
114
+ documentId
115
+ ) {
116
+ let response = {};
117
+ try {
118
+ response = await superagent
119
+ .post(apiUrl)
120
+ .set('Accept', 'application/json')
121
+ .set('Content-Type', 'application/json')
122
+ .query({ access_token: accessToken })
123
+ .query(replayObject)
124
+ .query({ message: text })
125
+ .send();
126
+ } catch (err) {
127
+ let errorText = '';
128
+ if (
129
+ err &&
130
+ err.response &&
131
+ err.response.body &&
132
+ err.response.body.error
133
+ ) {
134
+ errorText = `Failed to call instagram api for documentId ${documentId}: ${err.response.body.error.message}`;
135
+ logger.error(errorText);
136
+ } else {
137
+ errorText = `Failed to call instagram api for documentId ${documentId}`;
138
+ logger.error(errorText, err);
139
+ }
140
+ throw new Error(errorText);
141
+ }
142
+
143
+ if (response.status !== 200) {
144
+ logger.error(
145
+ `Failed to call instagram api for documentId ${documentId}`,
146
+ response.body
147
+ );
148
+ let error = new Error(
149
+ `Failed to call instagram api for documentId ${documentId}`
150
+ );
151
+ error.code = response.status;
152
+ throw error;
153
+ }
154
+ logger.debug('Instagram Response status', response.status);
155
+ logger.debug('Instagram Response body', response.body);
156
+ logger.debug('Instagram Response body.id', response.body.id);
157
+ return response;
158
+ }
159
+
160
+ async hideApi(apiUrl, accessToken, hideStatus, documentId) {
161
+ let response = {};
162
+ try {
163
+ response = await superagent
164
+ .post(apiUrl)
165
+ .set('Accept', 'application/json')
166
+ .set('Content-Type', 'application/json')
167
+ .query({ access_token: accessToken })
168
+ .query({ hide: hideStatus })
169
+ .send();
170
+ } catch (err) {
171
+ let errorText = '';
172
+ if (err && err.response && err.response.body) {
173
+ errorText = `Failed to call instagram api for documentId ${documentId}: ${err.response.body.error.message}`;
174
+ logger.error(errorText, err.response.body.error);
175
+ } else {
176
+ errorText = `Failed to call instagram api for documentId ${documentId}`;
177
+ logger.error(errorText, err);
178
+ }
179
+ throw new Error(errorText);
180
+ }
181
+
182
+ if (response.status !== 200) {
183
+ logger.error(
184
+ `Failed to call instagram api for documentId ${documentId}`,
185
+ response.body
186
+ );
187
+ let error = new Error(
188
+ `Failed to call instagram api for documentId ${documentId}`
189
+ );
190
+ error.code = response.status;
191
+ throw error;
192
+ }
193
+ logger.debug('Instagram Response status', response.status);
194
+ logger.debug('Instagram Response body', response.body);
195
+ logger.debug('Instagram Response body.id', response.body.id);
196
+ return response;
197
+ }
198
+
199
+ async getPost(apiUrl, accessToken, fields, documentId) {
200
+ let response = {};
201
+ try {
202
+ response = await superagent
203
+ .get(apiUrl)
204
+ .set('Accept', 'application/json')
205
+ .set('Content-Type', 'application/json')
206
+ .query({ access_token: accessToken })
207
+ .query({ fields: fields })
208
+ .send();
209
+ } catch (err) {
210
+ let errorText = '';
211
+ if (err && err.response && err.response.body) {
212
+ errorText = `Failed to call instagram api for documentId ${documentId}: ${err.response.body.error.message}`;
213
+ logger.error(errorText, err.response.body.error);
214
+ } else {
215
+ errorText = `Failed to call instagram api for documentId ${documentId}`;
216
+ logger.error(errorText, err);
217
+ }
218
+ throw new Error(errorText);
219
+ }
220
+
221
+ if (response.status !== 200) {
222
+ logger.error(
223
+ `Failed to call instagram api for documentId ${documentId}`,
224
+ response.body
225
+ );
226
+ let error = new Error(
227
+ `Failed to call instagram api for documentId ${documentId}`
228
+ );
229
+ error.code = response.status;
230
+ throw error;
231
+ }
232
+ logger.debug('Instagram Response status', response.status);
233
+ logger.debug('Instagram Response body', response.body);
234
+ logger.debug('Instagram Response body.id', response.body.id);
235
+ return response;
236
+ }
237
+
238
+ async comment(accessToken, payload) {
239
+ let response;
240
+ const { inReplyToId, text, documentId, isMention, sourceId } = payload;
241
+
242
+ logger.debug(
243
+ `Starting to call instagram comment api for documentId ${documentId}`,
244
+ payload
245
+ );
246
+
247
+ this.validate(accessToken, payload);
248
+
249
+ if (isMention) {
250
+ response = await this.mentionRequestApi(
251
+ `${INSTAGRAM_URL}/${sourceId}/mentions`,
252
+ accessToken,
253
+ { media_id: removePrefix(inReplyToId) },
254
+ text,
255
+ documentId
256
+ );
257
+ } else {
258
+ response = await this.requestApi(
259
+ `${INSTAGRAM_URL}/${removePrefix(inReplyToId)}/comments`,
260
+ accessToken,
261
+ text,
262
+ documentId,
263
+ undefined,
264
+ 'qt'
265
+ );
266
+ }
267
+
268
+ logger.info(
269
+ `Native Intagram API Publish Comment Response for documentId ${documentId}`,
270
+ { status: response.status, ok: response.ok }
271
+ );
272
+ return response.body;
273
+ }
274
+
275
+ async reply(accessToken, payload) {
276
+ let response;
277
+ const {
278
+ inReplyToId,
279
+ text,
280
+ documentId,
281
+ isMention,
282
+ sourceId,
283
+ threadId,
284
+ adCampaign,
285
+ } = payload;
286
+
287
+ logger.debug(
288
+ `Starting to call instagram comment api for documentId ${documentId}`,
289
+ payload
290
+ );
291
+
292
+ this.validate(accessToken, payload);
293
+
294
+ if (isMention) {
295
+ response = await this.mentionRequestApi(
296
+ `${INSTAGRAM_URL}/${sourceId}/mentions`,
297
+ accessToken,
298
+ {
299
+ media_id: removePrefix(threadId),
300
+ comment_id: removePrefix(inReplyToId),
301
+ },
302
+ text,
303
+ documentId
304
+ );
305
+ } else {
306
+ response = await this.requestApi(
307
+ `${INSTAGRAM_URL}/${removePrefix(inReplyToId)}/replies`,
308
+ accessToken,
309
+ text,
310
+ documentId,
311
+ undefined,
312
+ 're',
313
+ adCampaign
314
+ );
315
+ }
316
+
317
+ logger.info(
318
+ `Native Instagram API Publish Reply Response for documentId ${documentId}`,
319
+ { status: response.status, ok: response.ok }
320
+ );
321
+ return response.body;
322
+ }
323
+
324
+ async publish(document) {
325
+ const {
326
+ documentId,
327
+ body: {
328
+ content: { text },
329
+ },
330
+ appData: { isMention },
331
+ metaData: {
332
+ discussionType,
333
+ source: { id: sourceId },
334
+ inReplyTo: { id: inReplyToId, profileId, threadId },
335
+ adCampaign,
336
+ },
337
+ systemData: {
338
+ connectionsCredential: credentialId,
339
+ policies: { storage: { privateTo: companyId } = {} } = {},
340
+ } = {},
341
+ } = document;
342
+
343
+ let payload = {
344
+ documentId,
345
+ credentialId,
346
+ text,
347
+ companyId,
348
+ inReplyToId,
349
+ profileId,
350
+ isMention,
351
+ sourceId,
352
+ threadId,
353
+ adCampaign,
354
+ };
355
+
356
+ let updatedDocument;
357
+ let apiResponse;
358
+ try {
359
+ const token = await this.credentialsAPI.getToken(
360
+ payload.credentialId,
361
+ payload.companyId
362
+ );
363
+ logger.debug(
364
+ `finished fetching token for instagram for ${documentId} on company ${companyId} `,
365
+ payload
366
+ );
367
+
368
+ switch (discussionType) {
369
+ case 'qt':
370
+ apiResponse = await this.comment(token.token, payload);
371
+ break;
372
+ case 're':
373
+ apiResponse = await this.reply(token.token, payload);
374
+ break;
375
+ case 'dm':
376
+ // get parent account
377
+ const parentCredentialId =
378
+ await this.credentialsAPI.getCredentialIdByAccountId(
379
+ token.parentAccountId,
380
+ 'facebook'
381
+ );
382
+
383
+ if (!parentCredentialId) {
384
+ throw 'Parent account id not found';
385
+ }
386
+
387
+ // get parent
388
+ const parentCredential = await this.credentialsAPI.getToken(
389
+ parentCredentialId,
390
+ payload.companyId
391
+ );
392
+
393
+ if (!parentCredential) {
394
+ throw 'Parent account not found';
395
+ }
396
+
397
+ apiResponse = await this.privateMessage(
398
+ parentCredential.token,
399
+ payload
400
+ );
401
+ break;
402
+ default:
403
+ throw new Error('Unsupported discussion type');
404
+ }
405
+ } catch (err) {
406
+ logger.error(documentId + ' - exception details ' + err, err);
407
+ }
408
+
409
+ return apiResponse;
410
+ }
411
+
412
+ async hide(accessToken, payload) {
413
+ let response;
414
+ const { externalId, documentId } = payload;
415
+
416
+ logger.debug(
417
+ `Starting to call instagram hide api for documentId ${documentId}`,
418
+ { payload }
419
+ );
420
+
421
+ response = await this.hideApi(
422
+ `${INSTAGRAM_URL}/${removePrefix(externalId)}`,
423
+ accessToken,
424
+ true,
425
+ documentId
426
+ );
427
+
428
+ logger.info(
429
+ `Native Instagram API Hide Response for documentId ${documentId}`,
430
+ { status: response.status, ok: response.ok }
431
+ );
432
+ return response.body;
433
+ }
434
+
435
+ async unhide(accessToken, payload) {
436
+ let response;
437
+ const { externalId, documentId } = payload;
438
+
439
+ logger.debug(
440
+ `Starting to call instagram unhide api for documentId ${documentId}`,
441
+ payload
442
+ );
443
+
444
+ response = await this.hideApi(
445
+ `${INSTAGRAM_URL}/${removePrefix(externalId)}`,
446
+ accessToken,
447
+ false,
448
+ documentId
449
+ );
450
+
451
+ logger.info(
452
+ `Native Instagram API Unhide Response for documentId ${documentId}`,
453
+ { status: response.status, ok: response.ok }
454
+ );
455
+ return response.body;
456
+ }
457
+
458
+ async privateMessage(accessToken, payload) {
459
+ let response;
460
+ const { profileId, text, documentId, isMention, sourceId, threadId } =
461
+ payload;
462
+
463
+ logger.debug(
464
+ `Starting to call instagram private message api for documentId ${documentId}`,
465
+ payload
466
+ );
467
+
468
+ this.validate(accessToken, payload);
469
+
470
+ response = await this.requestApi(
471
+ `${INSTAGRAM_URL}/v10.0/me/messages`,
472
+ accessToken,
473
+ // this sends in query string leaving out :shrug:
474
+ undefined,
475
+ documentId,
476
+ {
477
+ recipient: {
478
+ // not :100: this is right
479
+ id: removePrefix(profileId),
480
+ },
481
+ message: {
482
+ text,
483
+ },
484
+ // this apparently allows up to 7 days after message 🤔
485
+ tag: 'HUMAN_AGENT',
486
+ },
487
+ 'dm'
488
+ );
489
+
490
+ logger.info(
491
+ `Native Instagram API Publish Private Message Response for documentId ${documentId}`,
492
+ { status: response.status, ok: response.ok }
493
+ );
494
+ return response.body;
495
+ }
496
+ }