@meltwater/conversations-api-services 1.0.24 → 1.0.26

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,561 @@
1
+ import superagent from 'superagent';
2
+ import { removePrefix } from '../../lib/externalId.helpers.js';
3
+ import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.js';
4
+ import OAuth from 'oauth-1.0a';
5
+ import crypto from 'crypto';
6
+ import axios from 'axios';
7
+ import fs from 'fs';
8
+ import Twit from '@meltwater/social-twit';
9
+ export function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
10
+ if (!token.oauth) {
11
+ token.oauth = OAuth({
12
+ consumer: {
13
+ key: TWITTER_CONSUMER_KEY,
14
+ secret: TWITTER_CONSUMER_SECRET
15
+ },
16
+ signature_method: 'HMAC-SHA1',
17
+ hash_function(base_string, key) {
18
+ return crypto.createHmac('sha1', key).update(base_string).digest('base64');
19
+ }
20
+ });
21
+ token.consumer_key = TWITTER_CONSUMER_KEY;
22
+ token.consumer_secret = TWITTER_CONSUMER_SECRET;
23
+ }
24
+ return token;
25
+ }
26
+ export async function getUserInfoFromHandles(token, handles, logger) {
27
+ try {
28
+ let query = `users/lookup.json?screen_name=${handles.join(',')}`;
29
+ let result = await postRequest({
30
+ token,
31
+ uri: query,
32
+ logger
33
+ });
34
+ return result.data;
35
+ } catch (e) {
36
+ loggerError(logger, `Error getting UserInfo`, e);
37
+ }
38
+ }
39
+ export async function getUserInfoFromHandle(token, handleId, logger) {
40
+ try {
41
+ let query = `users/show.json?user_id=${removePrefix(handleId)}`;
42
+ let result = await getRequest({
43
+ token,
44
+ uri: query,
45
+ logger
46
+ });
47
+ return result.data;
48
+ } catch (e) {
49
+ loggerError(logger, `Error getting UserInfo`, e);
50
+ }
51
+ }
52
+ export async function getDirectMessageImage(token, imageUrl, logger) {
53
+ try {
54
+ let result = await getRequest({
55
+ token,
56
+ uri: imageUrl,
57
+ attachUrlPrefix: false,
58
+ logger
59
+ });
60
+ return {
61
+ image: result.data,
62
+ contentType: result.contentType
63
+ };
64
+ } catch (e) {
65
+ loggerError(logger, `Error getting image`, e);
66
+ throw e;
67
+ }
68
+ }
69
+ export async function getCurrentInfo(token, externalId, logger) {
70
+ try {
71
+ const uri = `https://api.twitter.com/2/tweets?ids=${removePrefix(externalId)}&tweet.fields=public_metrics&expansions=author_id`;
72
+ let result = await getRequest({
73
+ token,
74
+ uri,
75
+ attachUrlPrefix: false,
76
+ convertPayloadToUri: false,
77
+ logger
78
+ });
79
+
80
+ // this makes it so 'retweeted' is only true on a retweet that isn't a comment
81
+ // sometimes 'retweeted' is true if user retweeted with comment, sometimes not :sadness:
82
+ result.data.retweeted = !!result.data.current_user_retweet;
83
+ return result.data.data[0].public_metrics;
84
+ } catch (error) {
85
+ loggerDebug(logger, 'Error in twitter getCurrentInfo', error);
86
+ }
87
+ }
88
+ export async function getMentionHandleInfo(token, externalId, logger) {
89
+ try {
90
+ const uri = `https://api.twitter.com/2/tweets/${removePrefix(externalId)}?tweet.fields=entities&expansions=entities.mentions.username,author_id&user.fields=id,name,username,profile_image_url`;
91
+ let result = await getRequest({
92
+ token,
93
+ uri,
94
+ attachUrlPrefix: false,
95
+ convertPayloadToUri: false
96
+ });
97
+ return result.data;
98
+ } catch (error) {
99
+ loggerDebug(logger, 'Error in twitter getMentionHandleInfo', error);
100
+ }
101
+ }
102
+ export async function retweet(token, sourceId, externalId, logger) {
103
+ try {
104
+ let response = await postRequest({
105
+ token,
106
+ uri: `https://api.twitter.com/2/users/${removePrefix(sourceId)}/retweets`,
107
+ payload: {
108
+ tweet_id: removePrefix(externalId)
109
+ },
110
+ attachUrlPrefix: false,
111
+ convertPayloadToUri: false,
112
+ logger
113
+ });
114
+ if (response.data.data.retweeted !== undefined && response.resp.statusCode === 200) {
115
+ return true;
116
+ } else {
117
+ return false;
118
+ }
119
+ } catch (error) {
120
+ loggerDebug(logger, `Error in retweet user: ${sourceId}`, error);
121
+ }
122
+ }
123
+ export async function unRetweet(token, sourceId, externalId, logger) {
124
+ try {
125
+ let response = await deleteRequest({
126
+ token,
127
+ uri: `https://api.twitter.com/2/users/${sourceId}/retweets/${removePrefix(externalId)}`,
128
+ logger
129
+ });
130
+ if (response.data.data.retweeted !== undefined && response.resp.statusCode === 200) {
131
+ return true;
132
+ } else {
133
+ return false;
134
+ }
135
+ } catch (error) {
136
+ loggerDebug(logger, `Error in unretweet user: ${sourceId}`, error, {
137
+ [MeltwaterAttributes.SOCIALEXTERNALID]: externalId
138
+ });
139
+ }
140
+ }
141
+ export async function like(token, externalId, logger) {
142
+ try {
143
+ let response = await postRequest({
144
+ token,
145
+ uri: 'favorites/create.json?id=' + removePrefix(externalId),
146
+ logger
147
+ });
148
+ if (response.data.favorited !== undefined && response.resp.statusCode === 200) {
149
+ return true;
150
+ } else {
151
+ loggerInfo(logger, 'Twitter Error in like user statusCode non 200 ', {
152
+ response: JSON.stringify(response)
153
+ });
154
+ return false;
155
+ }
156
+ } catch (error) {
157
+ loggerDebug(logger, `Twitter Error in like user: ${externalId}`, error);
158
+ throw error;
159
+ }
160
+ }
161
+ export async function unLike(token, externalId, logger) {
162
+ try {
163
+ let response = await postRequest({
164
+ token,
165
+ uri: 'favorites/destroy.json?id=' + removePrefix(externalId),
166
+ logger
167
+ });
168
+ if (response.data.favorited !== undefined && response.resp.statusCode === 200) {
169
+ return true;
170
+ } else {
171
+ return false;
172
+ }
173
+ } catch (error) {
174
+ loggerDebug(logger, `Error in unlike user: ${externalId}`, error);
175
+ throw error;
176
+ }
177
+ }
178
+ export async function followUser(token, profileId, logger) {
179
+ try {
180
+ let response = await postRequest({
181
+ token,
182
+ uri: 'friendships/create.json',
183
+ payload: {
184
+ user_id: removePrefix(profileId),
185
+ follow: true
186
+ },
187
+ logger
188
+ });
189
+ if (response.data.following !== undefined) {
190
+ return true;
191
+ } else {
192
+ return false;
193
+ }
194
+ } catch (error) {
195
+ loggerDebug(logger, `Error in following user: ${profileId}`, error);
196
+ }
197
+ }
198
+ export async function unFollowUser(token, profileId, logger) {
199
+ try {
200
+ let response = await postRequest({
201
+ token,
202
+ uri: 'friendships/destroy.json',
203
+ payload: {
204
+ user_id: removePrefix(profileId)
205
+ },
206
+ logger
207
+ });
208
+ if (response.data.following !== undefined) {
209
+ return true;
210
+ } else {
211
+ return false;
212
+ }
213
+ } catch (error) {
214
+ loggerDebug(logger, `Error in unfollowing user: ${profileId}`, error);
215
+ }
216
+ }
217
+ export async function userFollowStatus(token, profileId, logger) {
218
+ try {
219
+ let response = await getRequest({
220
+ token,
221
+ uri: 'friendships/lookup.json',
222
+ payload: {
223
+ user_id: removePrefix(profileId)
224
+ },
225
+ logger
226
+ });
227
+ if (response.data.length > 0) {
228
+ if (response.data[0].connections.length > 0) {
229
+ for (var i = 0; i < response.data[0].connections.length; i++) {
230
+ if (response.data[0].connections[i] === 'following') {
231
+ return true;
232
+ }
233
+ }
234
+ return false;
235
+ } else {
236
+ return false;
237
+ }
238
+ }
239
+ } catch (error) {
240
+ loggerDebug(logger, `Error in getting user follow status: ${profileId}`, error);
241
+ if (error.message != 'Bad Request') {
242
+ // if we hit a minor rate limit here, we wait for 5 seconds before attempting again, usually long enough to continue
243
+ await new Promise(r => setTimeout(r, 5000));
244
+ return userFollowStatus(token, profileId, logger);
245
+ }
246
+ }
247
+ }
248
+ export async function getBrandUserRelationship(token, profileId, brandProfileId, logger) {
249
+ try {
250
+ let {
251
+ data: {
252
+ relationship
253
+ } = {}
254
+ } = await getRequest({
255
+ token,
256
+ uri: `friendships/show.json?source_id=${brandProfileId}&target_id=${removePrefix(profileId)}`,
257
+ logger
258
+ });
259
+ return relationship;
260
+ } catch (error) {
261
+ loggerDebug(logger, `Error in getting user brand friendship info: ${profileId} and ${brandProfileId}`, error);
262
+ }
263
+ }
264
+ export async function reply(token, text, attachment, inReplyToId, removeInReplyToId, logger) {
265
+ let mediaId;
266
+ if (attachment) {
267
+ // discussionType only matters if DM or Not
268
+ mediaId = await uploadMedia(attachment, token, 're', logger);
269
+ }
270
+ const payload = {
271
+ text,
272
+ ...(attachment && {
273
+ media: {
274
+ media_ids: [mediaId]
275
+ }
276
+ }),
277
+ reply: {
278
+ in_reply_to_tweet_id: removePrefix(inReplyToId)
279
+ }
280
+ };
281
+ if (removeInReplyToId.length) {
282
+ payload.reply.exclude_reply_user_ids = removeInReplyToId.map(removePrefix);
283
+ }
284
+ let query = 'https://api.twitter.com/2/tweets';
285
+ return publishTweet(token, payload, query, false, logger);
286
+ }
287
+ export async function privateMessage(token, text, attachment, profileId, logger) {
288
+ let query = 'direct_messages/events/new.json';
289
+ let mediaId;
290
+ if (attachment) {
291
+ // discussionType only matters if DM or Not
292
+ mediaId = await uploadMedia(attachment, token, 'dm', logger);
293
+ }
294
+ const payload = {
295
+ event: {
296
+ type: 'message_create',
297
+ message_create: {
298
+ target: {
299
+ recipient_id: removePrefix(profileId)
300
+ },
301
+ message_data: {
302
+ text: text || ' ',
303
+ ...(attachment && {
304
+ attachment: {
305
+ type: 'media',
306
+ media: {
307
+ id: mediaId
308
+ }
309
+ }
310
+ })
311
+ }
312
+ }
313
+ }
314
+ };
315
+ return publishTweet(token, payload, query, true, logger);
316
+ }
317
+ export async function retweetWithComment(token, text, attachment, logger) {
318
+ let mediaId;
319
+ if (attachment) {
320
+ // discussionType only matters if DM or Not
321
+ mediaId = await uploadMedia(attachment, token, 're', logger);
322
+ }
323
+ const payload = {
324
+ text,
325
+ ...(attachment && {
326
+ media: {
327
+ media_ids: [mediaId]
328
+ }
329
+ })
330
+ };
331
+ let query = 'https://api.twitter.com/2/tweets';
332
+ return publishTweet(token, payload, query, false, logger);
333
+ }
334
+ async function publishTweet(token, payload, query, isDirectMessage, logger) {
335
+ try {
336
+ let nativeResponse = await postRequest({
337
+ token,
338
+ uri: query,
339
+ payload,
340
+ convertPayloadToUri: false,
341
+ attachUrlPrefix: isDirectMessage
342
+ });
343
+ if (!isDirectMessage && nativeResponse.resp.statusCode === 201) {
344
+ const uri = `https://api.twitter.com/2/tweets?ids=${nativeResponse.data.data.id}&tweet.fields=public_metrics&expansions=author_id`;
345
+ nativeResponse = await getRequest({
346
+ token,
347
+ uri,
348
+ payload,
349
+ attachUrlPrefix: false,
350
+ convertPayloadToUri: false
351
+ });
352
+ }
353
+ loggerInfo(logger, `finished fetching twitterAPI nativeResponse `, {
354
+ data: JSON.stringify(nativeResponse.data)
355
+ });
356
+ const response = nativeResponse.resp ? {
357
+ statusCode: nativeResponse.resp.statusCode,
358
+ statusMessage: nativeResponse.resp.statusCode === 200 ? 'Success' : 'Unknown Failure',
359
+ data: isDirectMessage ? nativeResponse.data.event : nativeResponse.data.data[0]
360
+ } : {
361
+ statusCode: nativeResponse.statusCode,
362
+ statusMessage: nativeResponse.message
363
+ };
364
+ loggerDebug(logger, `Twitter the data response is`, {
365
+ response: JSON.stringify(response)
366
+ });
367
+ return response;
368
+ } catch (err) {
369
+ loggerError(`Twitter publish exception details`, err);
370
+ throw err;
371
+ }
372
+ }
373
+ async function postRequest(_ref) {
374
+ let {
375
+ token,
376
+ uri,
377
+ payload,
378
+ attachUrlPrefix = true,
379
+ convertPayloadToUri = true,
380
+ logger
381
+ } = _ref;
382
+ return doRequest({
383
+ requestMethod: 'post',
384
+ token,
385
+ convertPayloadToUri,
386
+ payload,
387
+ uri,
388
+ attachUrlPrefix,
389
+ logger
390
+ });
391
+ }
392
+ async function getRequest(_ref2) {
393
+ let {
394
+ token,
395
+ uri,
396
+ payload,
397
+ attachUrlPrefix = true,
398
+ convertPayloadToUri = true,
399
+ logger
400
+ } = _ref2;
401
+ return doRequest({
402
+ requestMethod: 'get',
403
+ payload,
404
+ token,
405
+ uri,
406
+ attachUrlPrefix,
407
+ convertPayloadToUri,
408
+ logger
409
+ });
410
+ }
411
+ async function deleteRequest(_ref3) {
412
+ let {
413
+ token,
414
+ uri,
415
+ payload,
416
+ attachUrlPrefix = false,
417
+ convertPayloadToUri = false,
418
+ logger
419
+ } = _ref3;
420
+ return doRequest({
421
+ requestMethod: 'delete',
422
+ token,
423
+ convertPayloadToUri,
424
+ payload,
425
+ uri,
426
+ attachUrlPrefix,
427
+ logger
428
+ });
429
+ }
430
+ function fixedEncodeURIComponent(str) {
431
+ return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
432
+ return '%' + c.charCodeAt(0).toString(16);
433
+ });
434
+ }
435
+ async function doRequest(_ref4) {
436
+ let {
437
+ requestMethod,
438
+ token,
439
+ uri,
440
+ payload,
441
+ attachUrlPrefix = true,
442
+ convertPayloadToUri = true,
443
+ logger
444
+ } = _ref4;
445
+ if (payload && convertPayloadToUri) {
446
+ uri = uri + (uri.endsWith('?') ? '' : '?') + Object.keys(payload).map(key => {
447
+ return fixedEncodeURIComponent(key) + '=' + fixedEncodeURIComponent(payload[key]);
448
+ }).join('&');
449
+ }
450
+ let url = attachUrlPrefix ? `https://api.twitter.com/1.1/${uri}` : uri;
451
+ return new Promise(async (resolve, reject) => {
452
+ try {
453
+ const {
454
+ Authorization
455
+ } = token.oauth.toHeader(token.oauth.authorize({
456
+ url,
457
+ method: requestMethod,
458
+ data: {}
459
+ }, {
460
+ key: token.token,
461
+ secret: token.tokenSecret
462
+ }));
463
+ const result = await (requestMethod === 'get' ? superagent.get(url).set('Authorization', Authorization) : requestMethod === 'post' ? superagent.post(url).set('Authorization', Authorization).send(payload && !convertPayloadToUri ? payload : undefined) : superagent.del(url).set('Authorization', Authorization));
464
+ loggerDebug(logger, 'Twitter Native Response ', {
465
+ native: {
466
+ //data: result.body,
467
+ contentType: result.headers['content-type']
468
+ },
469
+ methodParams: {
470
+ uri,
471
+ payload: JSON.stringify(payload)
472
+ }
473
+ });
474
+ resolve({
475
+ data: result.body,
476
+ contentType: result.headers['content-type'],
477
+ resp: {
478
+ ...result
479
+ }
480
+ });
481
+ } catch (e) {
482
+ loggerError(logger, `Error in twitter doRequest`, e, {
483
+ url,
484
+ convertPayloadToUri,
485
+ payload: JSON.stringify(payload)
486
+ });
487
+ reject(e);
488
+ }
489
+ });
490
+ }
491
+
492
+ // local
493
+ async function downloadImage(url, fileName) {
494
+ const writer = fs.createWriteStream(fileName);
495
+ const response = await axios({
496
+ url,
497
+ method: 'GET',
498
+ responseType: 'stream'
499
+ });
500
+ response.data = response.data;
501
+ response.data.pipe(writer);
502
+ return new Promise((resolve, reject) => {
503
+ writer.on('finish', resolve);
504
+ writer.on('error', reject);
505
+ });
506
+ }
507
+
508
+ // local
509
+ function generateFilePath(mimeType) {
510
+ let dir = `${__dirname}/temporaryTwitterMedia`;
511
+ if (!fs.existsSync(dir)) {
512
+ fs.mkdirSync(dir);
513
+ }
514
+ let randomFileName = Math.floor(Math.random() * 100);
515
+ let fileExtension = mimeType.split('/')[1];
516
+ if (mimeType === 'video/quicktime') {
517
+ fileExtension = 'mov';
518
+ }
519
+ return `${dir}/${randomFileName}.${fileExtension}`;
520
+ }
521
+ // local
522
+ async function uploadMedia(attachment, token, discussionType, logger) {
523
+ return new Promise(async (resolve, reject) => {
524
+ let filePath = generateFilePath(attachment.mimeType);
525
+ await downloadImage(attachment.url, filePath);
526
+ const T = new Twit({
527
+ consumer_key: token.consumer_key,
528
+ consumer_secret: token.consumer_secret,
529
+ access_token: token.token,
530
+ access_token_secret: token.tokenSecret
531
+ });
532
+ let mediaCategory = discussionType === 'dm' ? 'dm' : 'tweet';
533
+ try {
534
+ T.postMediaChunked({
535
+ file_path: filePath
536
+ }, mediaCategory, function (err, data, response) {
537
+ if (err) {
538
+ reject(err);
539
+ }
540
+ resolve(data.media_id_string);
541
+ removeMedia(filePath, logger);
542
+ });
543
+ } catch (e) {
544
+ loggerError(logger, `Failed posting media`, e);
545
+ // this is just a safety precaution
546
+ removeMedia(filePath, logger);
547
+ }
548
+ });
549
+ }
550
+ function removeMedia(file, logger) {
551
+ try {
552
+ fs.unlink(file, function (err) {
553
+ if (err) {
554
+ loggerError(logger, `Failed removing ${file}`, err);
555
+ }
556
+ });
557
+ } catch (e) {
558
+ loggerError(logger, `failed trying to remove media ${file} it may have already been removed`);
559
+ }
560
+ }
561
+ ;
@@ -20,6 +20,8 @@ import * as FacebookNative from './http/facebook.native.js';
20
20
  import * as TiktokNative from './http/tiktok.native.js';
21
21
  import * as YoutubeNative from './http/youtube.native.js';
22
22
  import * as LinkedinNative from './http/linkedin.native.js';
23
+ import * as TwitterNative from './http/twitter.native.js';
24
+ import * as InstagramNative from './http/instagram.native.js';
23
25
  const DocumentHelperFunctions = {
24
26
  ...messageHelpers,
25
27
  ...applicationTagFunctions,
@@ -29,4 +31,4 @@ const LinkedInHelpers = {
29
31
  getOrganization,
30
32
  getProfile
31
33
  };
32
- export { FacebookNative, TiktokNative, YoutubeNative, LinkedinNative, awsS3Client, assetManagerTvmRepository, CompanyApiClient, CredentialsApiClient, EntitlementsApiClient, FacebookApiClient, FeatureToggleClient, IdentityServicesClient, InstagramApiClient, InstagramVideoClient, IRClient, LinkedInApiClient, TikTokApiClient, MasfClient, WarpZoneApiClient, DocumentHelperFunctions, LinkedInHelpers };
34
+ export { InstagramNative, FacebookNative, TiktokNative, YoutubeNative, LinkedinNative, TwitterNative, awsS3Client, assetManagerTvmRepository, CompanyApiClient, CredentialsApiClient, EntitlementsApiClient, FacebookApiClient, FeatureToggleClient, IdentityServicesClient, InstagramApiClient, InstagramVideoClient, IRClient, LinkedInApiClient, TikTokApiClient, MasfClient, WarpZoneApiClient, DocumentHelperFunctions, LinkedInHelpers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meltwater/conversations-api-services",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Repository to contain all conversations api services shared across our services",
5
5
  "main": "dist/cjs/data-access/index.js",
6
6
  "module": "dist/esm/data-access/index.js",
@@ -30,6 +30,9 @@
30
30
  },
31
31
  "homepage": "https://github.com/meltwater/conversations-api-services#readme",
32
32
  "dependencies": {
33
+ "axios": "^1.1.3",
34
+ "oauth-1.0a": "^2.2.6",
35
+ "@meltwater/social-twit": "^2.2.12",
33
36
  "@elastic/ecs-winston-format": "^1.5.3",
34
37
  "@hapi/joi": "^17.1.1",
35
38
  "@meltwater/date-range": "^3.6.0",