@meltwater/conversations-api-services 1.0.23 → 1.0.25

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