@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,624 @@
1
+ import superagent from 'superagent';
2
+ import configuration from '../../lib/configuration.js';
3
+ import { CredentialsApiClient } from '../http/credentialsApi.client.js';
4
+ import logger from '../../lib/logger.js';
5
+
6
+ export class LinkedInApiClient {
7
+ constructor() {
8
+ this.credentialsAPI = new CredentialsApiClient();
9
+ }
10
+
11
+ async likeMessage(document) {
12
+ const {
13
+ documentId,
14
+ systemData: {
15
+ connectionsCredential: credentialId,
16
+ policies: {
17
+ storage: { privateTo: companyId },
18
+ },
19
+ },
20
+ metaData: {
21
+ externalId,
22
+ source: { id: socialAccountId },
23
+ },
24
+ enrichments: {
25
+ socialScores: { li_liked_by_user: likedByUser },
26
+ },
27
+ } = document;
28
+
29
+ let response;
30
+ let payload = { actor: socialAccountId, object: externalId };
31
+ let query = likedByUser
32
+ ? `${configuration.get(
33
+ 'LINKEDIN_API'
34
+ )}/socialActions/${fixedEncodeURIComponent(externalId)}/likes`
35
+ : `${configuration.get(
36
+ 'LINKEDIN_API'
37
+ )}/socialActions/${fixedEncodeURIComponent(
38
+ externalId
39
+ )}/likes/${fixedEncodeURIComponent(
40
+ payload.actor
41
+ )}?actor=${fixedEncodeURIComponent(payload.actor)}`;
42
+
43
+ try {
44
+ let credential = await this.credentialsAPI.getToken(
45
+ credentialId,
46
+ companyId
47
+ );
48
+ if (!likedByUser) {
49
+ logger.debug(`${documentId} - trying Delete `, {
50
+ query,
51
+ payload,
52
+ });
53
+ response = await superagent
54
+ .delete(query)
55
+ .timeout(5000)
56
+ .set({
57
+ Authorization: `Bearer ${credential.token}`,
58
+ 'X-RestLi-Protocol-Version': configuration.get(
59
+ 'LINKEDIN_API_VERSION'
60
+ ),
61
+ 'LinkedIn-Version': '202305',
62
+ });
63
+ } else {
64
+ logger.debug(`${documentId} - sending to linkedin `, {
65
+ query,
66
+ payload,
67
+ });
68
+ response = await superagent
69
+ .post(query)
70
+ .timeout(5000) // 5 seconds
71
+ .set({
72
+ Authorization: `Bearer ${credential.token}`,
73
+ 'X-RestLi-Protocol-Version': configuration.get(
74
+ 'LINKEDIN_API_VERSION'
75
+ ),
76
+ 'LinkedIn-Version': '202305',
77
+ })
78
+ .send(payload);
79
+ }
80
+
81
+ logger.info(
82
+ `Native Linkedin API Like Comment Response for documentId ${documentId}`,
83
+ {
84
+ ok: response.ok,
85
+ status: response.status,
86
+ message: JSON.parse(response.text),
87
+ likedByUser,
88
+ }
89
+ );
90
+ } catch (err) {
91
+ logger.error(documentId + ' - exception details', {
92
+ err,
93
+ });
94
+ throw err;
95
+ }
96
+ return response.connection || response;
97
+ }
98
+
99
+ async deleteMessage(document) {
100
+ const {
101
+ documentId,
102
+ systemData: {
103
+ connectionsCredential: credentialId,
104
+ policies: {
105
+ storage: { privateTo: companyId },
106
+ },
107
+ },
108
+ metaData: {
109
+ externalId,
110
+ source: { id: actorURN },
111
+ inReplyTo: { id: inReplyToId },
112
+ },
113
+ } = document;
114
+
115
+ const [, comment] = externalId.split(',');
116
+ const commentId = comment.split(')')[0];
117
+
118
+ let response;
119
+ let query = `${configuration.get(
120
+ 'LINKEDIN_API'
121
+ )}/socialActions/${fixedEncodeURIComponent(
122
+ inReplyToId
123
+ )}/comments/${commentId}?actor=${fixedEncodeURIComponent(actorURN)}`;
124
+
125
+ try {
126
+ let credential = await this.credentialsAPI.getToken(
127
+ credentialId,
128
+ companyId
129
+ );
130
+
131
+ logger.debug(`${documentId} - trying Delete `, {
132
+ query,
133
+ });
134
+ response = await superagent
135
+ .delete(query)
136
+ .timeout(5000)
137
+ .set({
138
+ Authorization: `Bearer ${credential.token}`,
139
+ 'X-RestLi-Protocol-Version': configuration.get(
140
+ 'LINKEDIN_API_VERSION'
141
+ ),
142
+ 'LinkedIn-Version': '202305',
143
+ });
144
+
145
+ logger.info(
146
+ `Native Linkedin API Delete Comment Response for documentId ${documentId}`,
147
+ { status: response.status, ok: response.ok }
148
+ );
149
+ } catch (err) {
150
+ let errorText = '';
151
+ if (err && err.response && err.response.text) {
152
+ errorText = `Call to linkedin api with documentId ${documentId} failed`;
153
+ logger.error(errorText, {
154
+ message: JSON.parse(err.response.text),
155
+ });
156
+ } else {
157
+ errorText = `Call to linkedin api with documentId ${documentId} failed`;
158
+ logger.error(errorText, err);
159
+ }
160
+ throw new Error(errorText);
161
+ }
162
+ return response;
163
+ }
164
+
165
+ async publishComment(document, mediaId) {
166
+ // command
167
+ const {
168
+ documentId,
169
+ systemData: {
170
+ connectionsCredential: credentialId,
171
+ policies: {
172
+ storage: { privateTo: companyId },
173
+ },
174
+ },
175
+ body: {
176
+ content: { text: messageText },
177
+ },
178
+ metaData: {
179
+ discussionType,
180
+ inReplyTo: { id: parentUrn },
181
+ source: { id: socialAccountId },
182
+ },
183
+ } = document;
184
+ let messageType = discussionType === 're' ? 'reply' : 'comment';
185
+
186
+ // for now this is needed to make the urns from the images api work until the docs are updated to reflect how to use these new ids.
187
+ if (mediaId) {
188
+ mediaId = mediaId.replace(
189
+ 'urn:li:image:',
190
+ 'urn:li:digitalmediaAsset:'
191
+ );
192
+ }
193
+
194
+ let body = {
195
+ actor: socialAccountId,
196
+ message: {
197
+ attributes: [],
198
+ text: messageText,
199
+ },
200
+ };
201
+ if (messageType === 'reply') {
202
+ body.parentComment = parentUrn;
203
+ }
204
+ if (mediaId) {
205
+ body.content = [
206
+ {
207
+ entity: {
208
+ digitalmediaAsset: mediaId,
209
+ },
210
+ type: 'IMAGE',
211
+ },
212
+ ];
213
+ }
214
+
215
+ let query = `${configuration.get(
216
+ 'LINKEDIN_API'
217
+ )}/socialActions/${fixedEncodeURIComponent(parentUrn)}/comments`;
218
+
219
+ // data-listener -> linkedin_api.client sendComment
220
+ let response, updatedDocument;
221
+ try {
222
+ let credential = await this.credentialsAPI.getToken(
223
+ credentialId,
224
+ companyId
225
+ );
226
+
227
+ let response = await superagent
228
+ .post(query)
229
+ .timeout(5000)
230
+ .set({
231
+ Authorization: `Bearer ${credential.token}`,
232
+ 'Content-Type': 'application/json',
233
+ 'X-RestLi-Protocol-Version': configuration.get(
234
+ 'LINKEDIN_API_VERSION'
235
+ ),
236
+ 'LinkedIn-Version': '202305',
237
+ })
238
+ .send(body);
239
+
240
+ response = {
241
+ status: 200,
242
+ message: JSON.parse(response.text),
243
+ };
244
+ } catch (err) {
245
+ logger.error(
246
+ documentId + ' - exception linkedin publish ' + err,
247
+ err
248
+ );
249
+ }
250
+
251
+ return response;
252
+ }
253
+
254
+ async getMediaUploadURL(
255
+ companyId,
256
+ credentialId,
257
+ socialAccountId,
258
+ documentId
259
+ ) {
260
+ let query = `${configuration.get(
261
+ 'LINKEDIN_API'
262
+ )}/images?action=initializeUpload`;
263
+ let body = {
264
+ initializeUploadRequest: {
265
+ owner: socialAccountId,
266
+ },
267
+ };
268
+
269
+ let response;
270
+ try {
271
+ let credential = await this.credentialsAPI.getToken(
272
+ credentialId,
273
+ companyId
274
+ );
275
+
276
+ response = await superagent
277
+ .post(query)
278
+ .timeout(5000)
279
+ .set({
280
+ Authorization: `Bearer ${credential.token}`,
281
+ 'Content-type': 'application/json',
282
+ 'X-RestLi-Protocol-Version': configuration.get(
283
+ 'LINKEDIN_API_VERSION'
284
+ ),
285
+ 'LinkedIn-Version': '202305',
286
+ })
287
+ .send(body);
288
+
289
+ response = {
290
+ status: 200,
291
+ message: JSON.parse(response.text),
292
+ };
293
+ return response;
294
+ } catch (err) {
295
+ logger.error(
296
+ documentId + ' - exception linkedin media register ' + err,
297
+ err
298
+ );
299
+ }
300
+ }
301
+
302
+ async getProfile(urn, token) {
303
+ let [, , , id] = urn.split(':');
304
+
305
+ let profile;
306
+ try {
307
+ profile = await superagent
308
+ .get(`${configuration.get('LINKEDIN_API')}/people/(id:${id})`)
309
+ .query({
310
+ projection:
311
+ '(id,localizedFirstName,localizedLastName,vanityName,profilePicture(displayImage~:playableStreams))',
312
+ })
313
+ .timeout(5000)
314
+ .set({
315
+ Authorization: `Bearer ${token}`,
316
+ 'X-RestLi-Protocol-Version': configuration.get(
317
+ 'LINKEDIN_API_VERSION'
318
+ ),
319
+ 'LinkedIn-Version': '202305',
320
+ })
321
+ .then((result) => result.body);
322
+ } catch (error) {
323
+ logger.error(
324
+ `Failed requesting LinkedIn API for profileId ${urn}`,
325
+ error
326
+ );
327
+ return;
328
+ }
329
+
330
+ logger.info(`Finished requesting LinkedIn API for profileId ${urn}`);
331
+
332
+ return profile;
333
+ }
334
+
335
+ async getOrganization(urn, token) {
336
+ let [, , , id] = urn.split(':');
337
+
338
+ let organization;
339
+ try {
340
+ organization = await superagent
341
+ .get(`${configuration.get('LINKEDIN_API')}/organizations/${id}`)
342
+ .query({
343
+ projection:
344
+ '(id,localizedName,vanityName,logoV2(original~:playableStreams))',
345
+ })
346
+ .timeout(5000) // 5 seconds
347
+ .set({
348
+ Authorization: `Bearer ${token}`,
349
+ 'X-RestLi-Protocol-Version': configuration.get(
350
+ 'LINKEDIN_API_VERSION'
351
+ ),
352
+ 'LinkedIn-Version': '202305',
353
+ })
354
+ .then((result) => result.body);
355
+ } catch (error) {
356
+ logger.error(
357
+ `Failed requesting LinkedIn API for organizationId ${urn}`,
358
+ error
359
+ );
360
+ }
361
+
362
+ logger.info(
363
+ `Finished requesting LinkedIn API for organizationId ${urn}`
364
+ );
365
+
366
+ return organization;
367
+ }
368
+
369
+ async getAllStats(externalId, token) {
370
+ let response = {};
371
+
372
+ try {
373
+ response = await superagent
374
+ .get(
375
+ configuration.get('LINKEDIN_API') +
376
+ '/socialMetadata/' +
377
+ fixedEncodeURIComponent(externalId)
378
+ )
379
+ .timeout(5000) // 5 seconds
380
+ .set({
381
+ Authorization: `Bearer ${token}`,
382
+ 'X-RestLi-Protocol-Version': configuration.get(
383
+ 'LINKEDIN_API_VERSION'
384
+ ),
385
+ 'LinkedIn-Version': '202305',
386
+ });
387
+ } catch (error) {
388
+ logger.error('Error in getAllStats ' + error, error);
389
+ }
390
+
391
+ let { commentSummary, reactionSummaries } = response.body || {};
392
+ if (commentSummary && reactionSummaries) {
393
+ return { commentSummary, reactionSummaries };
394
+ } else {
395
+ logger.error(
396
+ `Invalid response getting all Linkedin Stats for externalId = ${externalId} ` +
397
+ response,
398
+ response
399
+ );
400
+ return {};
401
+ }
402
+ }
403
+
404
+ async getSocialStats(externalId, token) {
405
+ try {
406
+ // get all stats (likes are not to be trusted) seems to only work
407
+ // for og posts.
408
+ const { commentSummary, reactionSummaries } =
409
+ await this.getAllStats(externalId, token);
410
+
411
+ return {
412
+ commentSummary,
413
+ reactionSummaries,
414
+ };
415
+ } catch (error) {
416
+ logger.error('Error in getSocialStats: ' + error, error);
417
+ }
418
+ }
419
+
420
+ async getImageMedia(mediaId, credentialId, companyId) {
421
+ let response = {};
422
+
423
+ let credential = await this.credentialsAPI.getToken(
424
+ credentialId,
425
+ companyId
426
+ );
427
+
428
+ if (mediaId && mediaId.includes('digitalmediaAsset')) {
429
+ mediaId = mediaId.replace(
430
+ 'urn:li:digitalmediaAsset:',
431
+ 'urn:li:image:'
432
+ );
433
+ }
434
+
435
+ try {
436
+ response = await superagent
437
+ .get(
438
+ configuration.get('LINKEDIN_API') +
439
+ '/images/' +
440
+ fixedEncodeURIComponent(mediaId)
441
+ )
442
+ .timeout(5000) // 5 seconds
443
+ .set({
444
+ Authorization: `Bearer ${credential.token}`,
445
+ 'X-RestLi-Protocol-Version': configuration.get(
446
+ 'LINKEDIN_API_VERSION'
447
+ ),
448
+ 'LinkedIn-Version': '202305',
449
+ });
450
+ } catch (error) {
451
+ logger.error('Error in getImageMedia ' + error, error);
452
+ }
453
+
454
+ let { downloadUrl } = response.body || {};
455
+ if (downloadUrl) {
456
+ return { downloadUrl };
457
+ } else {
458
+ logger.error(
459
+ `Invalid response getting Linkedin media for mediaId = ${mediaId} ` +
460
+ response,
461
+ response
462
+ );
463
+ return {};
464
+ }
465
+ }
466
+
467
+ async getVideoMedia(mediaId, credentialId, companyId) {
468
+ let response = {};
469
+ let tempValue;
470
+
471
+ let credential = await this.credentialsAPI.getToken(
472
+ credentialId,
473
+ companyId
474
+ );
475
+
476
+ if (mediaId && mediaId.includes('digitalmediaMediaArtifact')) {
477
+ tempValue = mediaId.split('digitalmediaMediaArtifact:(')[1];
478
+ tempValue = tempValue.split(
479
+ ',urn:li:digitalmediaMediaArtifactClass'
480
+ )[0];
481
+ mediaId = tempValue;
482
+ }
483
+
484
+ if (mediaId && mediaId.includes('digitalmediaAsset')) {
485
+ mediaId = mediaId.replace(
486
+ 'urn:li:digitalmediaAsset:',
487
+ 'urn:li:video:'
488
+ );
489
+ }
490
+
491
+ try {
492
+ response = await superagent
493
+ .get(
494
+ configuration.get('LINKEDIN_API') +
495
+ '/videos/' +
496
+ fixedEncodeURIComponent(mediaId)
497
+ )
498
+ .timeout(5000) // 5 seconds
499
+ .set({
500
+ Authorization: `Bearer ${credential.token}`,
501
+ 'X-RestLi-Protocol-Version': configuration.get(
502
+ 'LINKEDIN_API_VERSION'
503
+ ),
504
+ 'LinkedIn-Version': '202305',
505
+ });
506
+ } catch (error) {
507
+ logger.error('Error in getVideoMedia ' + error, error);
508
+ }
509
+
510
+ let { thumbnail, downloadUrl } = response.body || {};
511
+ if (downloadUrl) {
512
+ return { thumbnail, downloadUrl };
513
+ } else {
514
+ logger.error(
515
+ `Invalid response getting Linkedin media for mediaId = ${mediaId} ` +
516
+ response,
517
+ response
518
+ );
519
+ return {};
520
+ }
521
+ }
522
+
523
+ async getMedia(mediaId, type, credentialId, companyId) {
524
+ // needed due to issue with polled data
525
+ if (type.includes('video') && mediaId.includes('image')) {
526
+ type = 'image/jpeg';
527
+ }
528
+
529
+ try {
530
+ if (type.includes('image')) {
531
+ const { downloadUrl } = await this.getImageMedia(
532
+ mediaId,
533
+ credentialId,
534
+ companyId
535
+ );
536
+
537
+ return {
538
+ downloadUrl,
539
+ };
540
+ } else if (type.includes('video')) {
541
+ const { thumbnail, downloadUrl } = await this.getVideoMedia(
542
+ mediaId,
543
+ credentialId,
544
+ companyId
545
+ );
546
+
547
+ return {
548
+ thumbnail,
549
+ downloadUrl,
550
+ };
551
+ }
552
+
553
+ return {};
554
+ } catch (error) {
555
+ logger.error('Error in getMedia: ' + error, error);
556
+ }
557
+ }
558
+
559
+ async getLikedByUser(
560
+ externalId,
561
+ authorExternalId,
562
+ credentialId,
563
+ companyId
564
+ ) {
565
+ let result;
566
+ try {
567
+ let credential = await this.credentialsAPI.getToken(
568
+ credentialId,
569
+ companyId
570
+ );
571
+
572
+ result = await superagent
573
+ .get(
574
+ `${configuration.get(
575
+ 'LINKEDIN_API'
576
+ )}/reactions/(actor:${fixedEncodeURIComponent(
577
+ authorExternalId
578
+ )},entity:${fixedEncodeURIComponent(externalId)})`
579
+ )
580
+ .set({
581
+ Authorization: `Bearer ${credential.token}`,
582
+ 'X-RestLi-Protocol-Version': configuration.get(
583
+ 'LINKEDIN_API_VERSION'
584
+ ),
585
+ 'LinkedIn-Version': '202305',
586
+ })
587
+ .then((result) => result.body);
588
+ } catch (error) {
589
+ logger.error('Error in getLikesByUser: ' + error, error);
590
+ return false;
591
+ }
592
+ return true;
593
+ }
594
+
595
+ async uploadMedia(query) {
596
+ const { socialAccountId, url, token, data } = query;
597
+
598
+ logger.info(
599
+ `Starting to upload media to linkedin for ${socialAccountId}`,
600
+ { url: query.url }
601
+ );
602
+
603
+ return await superagent
604
+ .put(url)
605
+ .set({
606
+ Accept: '*/*',
607
+ 'Content-Type': 'application/octet-stream',
608
+ Authorization: `Bearer ${token}`,
609
+ 'X-RestLi-Protocol-Version': configuration.get(
610
+ 'LINKEDIN_API_VERSION'
611
+ ),
612
+ 'LinkedIn-Version': '202305',
613
+ })
614
+ .send(data)
615
+ .catch((err) => {
616
+ logger.error(err);
617
+ throw err;
618
+ });
619
+ }
620
+ }
621
+
622
+ function fixedEncodeURIComponent(str) {
623
+ return encodeURIComponent(str).replace('(', '%28').replace(')', '%29');
624
+ }
@@ -0,0 +1,93 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class MasfClient {
6
+ constructor() {
7
+ this.searchServicesUrlMasf = configuration.get(
8
+ 'MASF_SEARCH_SERVICE_URL'
9
+ );
10
+ this.searchServicesVersionMasf = configuration.get(
11
+ 'MASF_SEARCH_SERVICE_VERSION'
12
+ );
13
+ this.authorListVersionMasf = 'v3';
14
+ }
15
+
16
+ async getSearches({ companyId, token }) {
17
+ try {
18
+ let uri = `${this.searchServicesUrlMasf}/${this.searchServicesVersionMasf}/company/${companyId}/searches`;
19
+ return (
20
+ await superagent.get(uri).set({
21
+ Authorization: `${token}`,
22
+ })
23
+ ).body;
24
+ } catch (error) {
25
+ logger.error(
26
+ `Failed requesting Search Services for search list `,
27
+ error
28
+ );
29
+ }
30
+ }
31
+
32
+ async getAuthorsLists({ companyId, token }) {
33
+ try {
34
+ let uri = `${this.searchServicesUrlMasf}/v3/companies/${companyId}/authorlists`;
35
+ return (
36
+ await superagent.get(uri).set({
37
+ Authorization: `${token}`,
38
+ })
39
+ ).body.sort(({ name: n1 }, { name: n2 }) => {
40
+ if (n1.toUpperCase() < n2.toUpperCase()) {
41
+ return -1;
42
+ }
43
+ if (n1.toUpperCase() > n2.toUpperCase()) {
44
+ return 1;
45
+ }
46
+ return 0;
47
+ });
48
+ } catch (error) {
49
+ logger.error(
50
+ `Failed requesting Search Services for author list `,
51
+ error
52
+ );
53
+ }
54
+ }
55
+
56
+ async getAuthorsList({ companyId, authorListId, token }) {
57
+ try {
58
+ let uri = `${this.searchServicesUrlMasf}/${this.authorListVersionMasf}/companies/${companyId}/authorlists/${authorListId}`;
59
+ return (
60
+ await superagent.get(uri).set({
61
+ Authorization: `${token}`,
62
+ })
63
+ ).body;
64
+ } catch (error) {
65
+ logger.error(
66
+ `Failed requesting Search Services for author list `,
67
+ error
68
+ );
69
+ }
70
+ }
71
+
72
+ async getRune({ searchId, token }) {
73
+ let rune;
74
+ try {
75
+ let uri = `${this.searchServicesUrlMasf}/${this.searchServicesVersionMasf}/search/getSavedSearchAsRunes/${searchId}`;
76
+ rune = await superagent.get(uri).set({
77
+ Authorization: `${token}`,
78
+ });
79
+ } catch (error) {
80
+ logger.error(
81
+ `Failed requesting Search Services for search list `,
82
+ error
83
+ );
84
+ return;
85
+ }
86
+
87
+ logger.info(
88
+ `Finished requesting Search Services for rune ${this.searchServicesUrlMasf} searchId: ${searchId}`
89
+ );
90
+
91
+ return rune.body;
92
+ }
93
+ }