@meltwater/conversations-api-services 1.1.22 → 1.1.24
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.
- package/dist/cjs/data-access/http/facebook.native.js +13 -0
- package/dist/cjs/data-access/http/facebookApi.client.js +13 -0
- package/dist/cjs/data-access/http/linkedInApi.client.js +10 -9
- package/dist/cjs/data-access/http/linkedin.native.js +8 -6
- package/dist/cjs/data-access/http/twitter.native.js +149 -25
- package/dist/cjs/lib/externalId.helpers.js +4 -1
- package/dist/esm/data-access/http/facebook.native.js +13 -0
- package/dist/esm/data-access/http/facebookApi.client.js +13 -0
- package/dist/esm/data-access/http/linkedInApi.client.js +10 -9
- package/dist/esm/data-access/http/linkedin.native.js +8 -6
- package/dist/esm/data-access/http/twitter.native.js +148 -24
- package/dist/esm/lib/externalId.helpers.js +4 -1
- package/package.json +1 -1
- package/src/data-access/http/facebook.native.js +14 -0
- package/src/data-access/http/facebookApi.client.js +14 -0
- package/src/data-access/http/linkedInApi.client.js +11 -9
- package/src/data-access/http/linkedin.native.js +8 -8
- package/src/data-access/http/twitter.native.js +131 -9
- package/src/lib/externalId.helpers.js +4 -1
|
@@ -361,6 +361,19 @@ async function postApi(apiUrl, accessToken, payload, logger) {
|
|
|
361
361
|
}
|
|
362
362
|
response = await request;
|
|
363
363
|
} catch (err) {
|
|
364
|
+
// Handle specific case where content is already marked as spam
|
|
365
|
+
if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
|
|
366
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Content already marked as spam - treating as successful hide operation`, {
|
|
367
|
+
responseBody: JSON.stringify(err.response.body)
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
status: 200,
|
|
371
|
+
body: {
|
|
372
|
+
success: true,
|
|
373
|
+
message: "Content already hidden (marked as spam)"
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
364
377
|
(0, _loggerHelpers.loggerDebug)(logger, `Failed to call facebook api`, err);
|
|
365
378
|
throw err;
|
|
366
379
|
}
|
|
@@ -190,6 +190,19 @@ class FacebookApiClient {
|
|
|
190
190
|
access_token: accessToken
|
|
191
191
|
}).send(payload);
|
|
192
192
|
} catch (err) {
|
|
193
|
+
// Handle specific case where content is already marked as spam
|
|
194
|
+
if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
|
|
195
|
+
loggerChild.info(`Content already marked as spam - treating as successful hide operation for documentId ${documentId}`, {
|
|
196
|
+
responseBody: JSON.stringify(err.response.body)
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
status: 200,
|
|
200
|
+
body: {
|
|
201
|
+
success: true,
|
|
202
|
+
message: "Content already hidden (marked as spam)"
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
193
206
|
let errorText = '';
|
|
194
207
|
if (err && err.response && err.response.body && err.response.body.error) {
|
|
195
208
|
errorText = `Failed to call facebook api for documentId ${documentId}: ${err.response.body.error.message}`;
|
|
@@ -14,6 +14,7 @@ var _logger = _interopRequireDefault(require("../../lib/logger.js"));
|
|
|
14
14
|
var _loggerHelpers = require("../../lib/logger.helpers.js");
|
|
15
15
|
var _linkedinNative = require("./linkedin.native.js");
|
|
16
16
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
|
+
const LINKEDIN_VERSION = '202511';
|
|
17
18
|
class LinkedInApiClient {
|
|
18
19
|
constructor() {
|
|
19
20
|
this.credentialsAPI = new _credentialsApiClient.CredentialsApiClient();
|
|
@@ -63,7 +64,7 @@ class LinkedInApiClient {
|
|
|
63
64
|
response = await _superagent.default.delete(query).timeout(5000).set({
|
|
64
65
|
Authorization: `Bearer ${credential.token}`,
|
|
65
66
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
66
|
-
'LinkedIn-Version':
|
|
67
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
67
68
|
});
|
|
68
69
|
} else {
|
|
69
70
|
loggerChild.debug(`${documentId} - sending to linkedin `, {
|
|
@@ -74,7 +75,7 @@ class LinkedInApiClient {
|
|
|
74
75
|
.set({
|
|
75
76
|
Authorization: `Bearer ${credential.token}`,
|
|
76
77
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
77
|
-
'LinkedIn-Version':
|
|
78
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
78
79
|
}).send(payload);
|
|
79
80
|
}
|
|
80
81
|
loggerChild.info(`Native Linkedin API Like Comment Response for documentId ${documentId}`, {
|
|
@@ -184,7 +185,7 @@ class LinkedInApiClient {
|
|
|
184
185
|
Authorization: `Bearer ${credential.token}`,
|
|
185
186
|
'Content-Type': 'application/json',
|
|
186
187
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
187
|
-
'LinkedIn-Version':
|
|
188
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
188
189
|
}).send(body);
|
|
189
190
|
response = {
|
|
190
191
|
status: 200,
|
|
@@ -213,7 +214,7 @@ class LinkedInApiClient {
|
|
|
213
214
|
Authorization: `Bearer ${credential.token}`,
|
|
214
215
|
'Content-type': 'application/json',
|
|
215
216
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
216
|
-
'LinkedIn-Version':
|
|
217
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
217
218
|
}).send(body);
|
|
218
219
|
response = {
|
|
219
220
|
status: 200,
|
|
@@ -238,7 +239,7 @@ class LinkedInApiClient {
|
|
|
238
239
|
.set({
|
|
239
240
|
Authorization: `Bearer ${token}`,
|
|
240
241
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
241
|
-
'LinkedIn-Version':
|
|
242
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
242
243
|
});
|
|
243
244
|
} catch (error) {
|
|
244
245
|
loggerChild.error('Error in getAllStats', error);
|
|
@@ -292,7 +293,7 @@ class LinkedInApiClient {
|
|
|
292
293
|
.set({
|
|
293
294
|
Authorization: `Bearer ${credential.token}`,
|
|
294
295
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
295
|
-
'LinkedIn-Version':
|
|
296
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
296
297
|
});
|
|
297
298
|
} catch (error) {
|
|
298
299
|
loggerChild.error('Error in getImageMedia', error);
|
|
@@ -332,7 +333,7 @@ class LinkedInApiClient {
|
|
|
332
333
|
.set({
|
|
333
334
|
Authorization: `Bearer ${credential.token}`,
|
|
334
335
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
335
|
-
'LinkedIn-Version':
|
|
336
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
336
337
|
});
|
|
337
338
|
} catch (error) {
|
|
338
339
|
loggerChild.error('Error in getVideoMedia', error);
|
|
@@ -399,7 +400,7 @@ class LinkedInApiClient {
|
|
|
399
400
|
'Content-Type': 'application/octet-stream',
|
|
400
401
|
Authorization: `Bearer ${token}`,
|
|
401
402
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
402
|
-
'LinkedIn-Version':
|
|
403
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
403
404
|
}).send(data).catch(err => {
|
|
404
405
|
_logger.default.error(err);
|
|
405
406
|
throw err;
|
|
@@ -416,7 +417,7 @@ async function getProfile(urn, token) {
|
|
|
416
417
|
}).timeout(5000).set({
|
|
417
418
|
Authorization: `Bearer ${token}`,
|
|
418
419
|
'X-RestLi-Protocol-Version': _configuration.default.get('LINKEDIN_API_VERSION'),
|
|
419
|
-
'LinkedIn-Version':
|
|
420
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
420
421
|
}).then(result => result.body);
|
|
421
422
|
} catch (error) {
|
|
422
423
|
_logger.default.error(`Failed requesting LinkedIn API for profileId ${urn}`, error);
|
|
@@ -25,7 +25,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
25
25
|
const LINKEDIN_API = "https://api.linkedin.com/v2";
|
|
26
26
|
const LINKEDIN_API_REST = "https://api.linkedin.com/rest";
|
|
27
27
|
const LINKEDIN_API_VERSION = "2.0.0";
|
|
28
|
-
const LINKEDIN_VERSION = "
|
|
28
|
+
const LINKEDIN_VERSION = "202511";
|
|
29
29
|
async function like(token, externalId, socialAccountId, logger) {
|
|
30
30
|
let response;
|
|
31
31
|
let payload = {
|
|
@@ -374,16 +374,18 @@ async function getOrganization(urn, token, logger) {
|
|
|
374
374
|
let [,,, id] = urn.split(':');
|
|
375
375
|
let organization;
|
|
376
376
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}).timeout(5000) // 5 seconds
|
|
377
|
+
// Use the newer REST API endpoint instead of deprecated v2/organizations
|
|
378
|
+
organization = await _superagent.default.get(`${LINKEDIN_API_REST}/organizationsLookup?ids=List(${fixedEncodeURIComponent(id)})`).timeout(5000) // 5 seconds
|
|
380
379
|
.set({
|
|
381
380
|
Authorization: `Bearer ${token}`,
|
|
382
381
|
'X-RestLi-Protocol-Version': LINKEDIN_API_VERSION,
|
|
383
382
|
'LinkedIn-Version': LINKEDIN_VERSION
|
|
384
|
-
}).then(result =>
|
|
383
|
+
}).then(result => {
|
|
384
|
+
const res = result.body;
|
|
385
|
+
return res.results[id];
|
|
386
|
+
});
|
|
385
387
|
} catch (error) {
|
|
386
|
-
if (error.response.body.message.includes('ADMIN_ONLY')) {
|
|
388
|
+
if (error.response && error.response.body && error.response.body.message && error.response.body.message.includes('ADMIN_ONLY')) {
|
|
387
389
|
return {
|
|
388
390
|
id: 'private'
|
|
389
391
|
};
|
|
@@ -29,6 +29,112 @@ var _axios = _interopRequireDefault(require("axios"));
|
|
|
29
29
|
var _fs = _interopRequireDefault(require("fs"));
|
|
30
30
|
var _socialTwit = _interopRequireDefault(require("@meltwater/social-twit"));
|
|
31
31
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
32
|
+
const TWITTER_API_CONFIG = {
|
|
33
|
+
v1_1: {
|
|
34
|
+
base_url: 'https://api.twitter.com/1.1'
|
|
35
|
+
},
|
|
36
|
+
v2: {
|
|
37
|
+
base_url: 'https://api.x.com/2',
|
|
38
|
+
default_user_fields: 'id,name,username,profile_image_url,public_metrics,description,created_at,verified,protected'
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Helper function to create v2 API URLs with consistent field selection
|
|
43
|
+
function createTwitterV2Url(endpoint) {
|
|
44
|
+
let additionalParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
45
|
+
const baseUrl = `${TWITTER_API_CONFIG.v2.base_url}/${endpoint}`;
|
|
46
|
+
const defaultFields = {
|
|
47
|
+
'user.fields': TWITTER_API_CONFIG.v2.default_user_fields
|
|
48
|
+
};
|
|
49
|
+
const allParams = {
|
|
50
|
+
...defaultFields,
|
|
51
|
+
...additionalParams
|
|
52
|
+
};
|
|
53
|
+
const queryString = Object.entries(allParams).map(_ref => {
|
|
54
|
+
let [key, value] = _ref;
|
|
55
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
56
|
+
}).join('&');
|
|
57
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Helper function to make v2 API requests with fallback to v1.1
|
|
61
|
+
async function makeTwitterV2Request(v2Endpoint, v1FallbackQuery, token, logger) {
|
|
62
|
+
let requestOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
|
|
63
|
+
// Try v2 API first - it supports OAuth 1.0a for user lookup endpoints and has better rate limits!
|
|
64
|
+
try {
|
|
65
|
+
const v2Url = createTwitterV2Url(v2Endpoint, requestOptions.v2Params);
|
|
66
|
+
const result = await getRequest({
|
|
67
|
+
token,
|
|
68
|
+
uri: v2Url,
|
|
69
|
+
attachUrlPrefix: false,
|
|
70
|
+
convertPayloadToUri: false,
|
|
71
|
+
logger
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
data: result.data?.data,
|
|
76
|
+
source: 'v2'
|
|
77
|
+
};
|
|
78
|
+
} catch (e) {
|
|
79
|
+
(0, _loggerHelpers.loggerWarn)(logger, `Twitter API v2 request failed for ${v2Endpoint}, falling back to v1.1`, e);
|
|
80
|
+
|
|
81
|
+
// Fallback to v1.1 if v2 fails
|
|
82
|
+
if (v1FallbackQuery && requestOptions.enableFallback !== false) {
|
|
83
|
+
try {
|
|
84
|
+
(0, _loggerHelpers.loggerInfo)(logger, `Falling back to v1.1 API for ${v2Endpoint}`);
|
|
85
|
+
const fallbackResult = await (requestOptions.fallbackMethod === 'post' ? postRequest : getRequest)({
|
|
86
|
+
token,
|
|
87
|
+
uri: v1FallbackQuery,
|
|
88
|
+
logger
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
data: fallbackResult.data,
|
|
93
|
+
source: 'v1.1'
|
|
94
|
+
};
|
|
95
|
+
} catch (fallbackError) {
|
|
96
|
+
(0, _loggerHelpers.loggerError)(logger, `Both v2 and v1.1 APIs failed for ${v2Endpoint}`, fallbackError);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: e,
|
|
102
|
+
source: 'failed'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function normalizeUserData(user) {
|
|
107
|
+
let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
|
|
108
|
+
if (!user) return user;
|
|
109
|
+
const normalized = {
|
|
110
|
+
...user
|
|
111
|
+
};
|
|
112
|
+
if (apiVersion === 'v2') {
|
|
113
|
+
normalized.screen_name = user.username;
|
|
114
|
+
normalized.id_str = user.id;
|
|
115
|
+
normalized.profile_image_url_https = user.profile_image_url;
|
|
116
|
+
} else {
|
|
117
|
+
normalized.username = user.screen_name;
|
|
118
|
+
normalized.id = user.id_str;
|
|
119
|
+
normalized.profile_image_url = user.profile_image_url_https;
|
|
120
|
+
}
|
|
121
|
+
return normalized;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Normalizes an array of user data or a single user object
|
|
126
|
+
* @param {Object|Array} users - User object or array of user objects from Twitter API
|
|
127
|
+
* @param {string} apiVersion - API version used ('v1.1' or 'v2')
|
|
128
|
+
* @returns {Object|Array} Normalized user data with both screen_name and username
|
|
129
|
+
*/
|
|
130
|
+
function normalizeUsersData(users) {
|
|
131
|
+
let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
|
|
132
|
+
if (!users) return users;
|
|
133
|
+
if (Array.isArray(users)) {
|
|
134
|
+
return users.map(user => normalizeUserData(user, apiVersion));
|
|
135
|
+
}
|
|
136
|
+
return normalizeUserData(users, apiVersion);
|
|
137
|
+
}
|
|
32
138
|
function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
|
|
33
139
|
if (!token.oauth) {
|
|
34
140
|
token.oauth = (0, _oauth.default)({
|
|
@@ -50,31 +156,49 @@ async function getUserInfoFromHandles(token, handles, logger) {
|
|
|
50
156
|
try {
|
|
51
157
|
const handlesJoin = handles.join(',');
|
|
52
158
|
if (!handlesJoin.length) {
|
|
53
|
-
loggerWarn(logger, `No handles provided to twitterNative getUserInfoFromHandles`);
|
|
159
|
+
(0, _loggerHelpers.loggerWarn)(logger, `No handles provided to twitterNative getUserInfoFromHandles`);
|
|
54
160
|
return [];
|
|
55
161
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
162
|
+
|
|
163
|
+
// Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
|
|
164
|
+
// v2 API supports up to 100 usernames per request vs v1.1's limit
|
|
165
|
+
const result = await makeTwitterV2Request('users/by',
|
|
166
|
+
// v2 endpoint
|
|
167
|
+
`users/lookup.json?screen_name=${handlesJoin}`,
|
|
168
|
+
// v1.1 fallback
|
|
169
|
+
token, logger, {
|
|
170
|
+
v2Params: {
|
|
171
|
+
usernames: handlesJoin
|
|
172
|
+
},
|
|
173
|
+
fallbackMethod: 'post'
|
|
61
174
|
});
|
|
62
|
-
|
|
175
|
+
const normalizedData = normalizeUsersData(result.data, result.source);
|
|
176
|
+
return normalizedData || [];
|
|
63
177
|
} catch (e) {
|
|
64
|
-
(0, _loggerHelpers.loggerError)(logger, `
|
|
178
|
+
(0, _loggerHelpers.loggerError)(logger, `Unexpected error in getUserInfoFromHandles`, e);
|
|
179
|
+
return [];
|
|
65
180
|
}
|
|
66
181
|
}
|
|
67
182
|
async function getUserInfoFromHandle(token, handleId, logger) {
|
|
68
183
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
184
|
+
if (!handleId) {
|
|
185
|
+
(0, _loggerHelpers.loggerWarn)(logger, `Invalid handleId provided to getUserInfoFromHandle: ${JSON.stringify(handleId)}`);
|
|
186
|
+
throw new Error(`Invalid handleId: ${handleId}`);
|
|
187
|
+
}
|
|
188
|
+
// Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
|
|
189
|
+
const result = await makeTwitterV2Request(`users/${(0, _externalIdHelpers.removePrefix)(handleId)}`,
|
|
190
|
+
// v2 endpoint
|
|
191
|
+
`users/show.json?user_id=${(0, _externalIdHelpers.removePrefix)(handleId)}`,
|
|
192
|
+
// v1.1 fallback
|
|
193
|
+
token, logger);
|
|
194
|
+
if (result.success) {
|
|
195
|
+
// Normalize the user data to ensure both screen_name and username are present
|
|
196
|
+
return normalizeUserData(result.data, result.source);
|
|
197
|
+
} else {
|
|
198
|
+
throw result.error || new Error('Failed to get user info from both APIs');
|
|
199
|
+
}
|
|
76
200
|
} catch (e) {
|
|
77
|
-
(0, _loggerHelpers.loggerError)(logger, `Error
|
|
201
|
+
(0, _loggerHelpers.loggerError)(logger, `Error in getUserInfoFromHandle with handleId: ${JSON.stringify(handleId)}`, e);
|
|
78
202
|
}
|
|
79
203
|
}
|
|
80
204
|
async function getDirectMessageImage(token, imageUrl, logger) {
|
|
@@ -403,7 +527,7 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
|
403
527
|
throw err;
|
|
404
528
|
}
|
|
405
529
|
}
|
|
406
|
-
async function postRequest(
|
|
530
|
+
async function postRequest(_ref2) {
|
|
407
531
|
let {
|
|
408
532
|
token,
|
|
409
533
|
uri,
|
|
@@ -411,7 +535,7 @@ async function postRequest(_ref) {
|
|
|
411
535
|
attachUrlPrefix = true,
|
|
412
536
|
convertPayloadToUri = true,
|
|
413
537
|
logger
|
|
414
|
-
} =
|
|
538
|
+
} = _ref2;
|
|
415
539
|
return doRequest({
|
|
416
540
|
requestMethod: 'post',
|
|
417
541
|
token,
|
|
@@ -422,7 +546,7 @@ async function postRequest(_ref) {
|
|
|
422
546
|
logger
|
|
423
547
|
});
|
|
424
548
|
}
|
|
425
|
-
async function getRequest(
|
|
549
|
+
async function getRequest(_ref3) {
|
|
426
550
|
let {
|
|
427
551
|
token,
|
|
428
552
|
uri,
|
|
@@ -430,7 +554,7 @@ async function getRequest(_ref2) {
|
|
|
430
554
|
attachUrlPrefix = true,
|
|
431
555
|
convertPayloadToUri = true,
|
|
432
556
|
logger
|
|
433
|
-
} =
|
|
557
|
+
} = _ref3;
|
|
434
558
|
return doRequest({
|
|
435
559
|
requestMethod: 'get',
|
|
436
560
|
payload,
|
|
@@ -441,7 +565,7 @@ async function getRequest(_ref2) {
|
|
|
441
565
|
logger
|
|
442
566
|
});
|
|
443
567
|
}
|
|
444
|
-
async function deleteRequest(
|
|
568
|
+
async function deleteRequest(_ref4) {
|
|
445
569
|
let {
|
|
446
570
|
token,
|
|
447
571
|
uri,
|
|
@@ -449,7 +573,7 @@ async function deleteRequest(_ref3) {
|
|
|
449
573
|
attachUrlPrefix = false,
|
|
450
574
|
convertPayloadToUri = false,
|
|
451
575
|
logger
|
|
452
|
-
} =
|
|
576
|
+
} = _ref4;
|
|
453
577
|
return doRequest({
|
|
454
578
|
requestMethod: 'delete',
|
|
455
579
|
token,
|
|
@@ -465,7 +589,7 @@ function fixedEncodeURIComponent(str) {
|
|
|
465
589
|
return '%' + c.charCodeAt(0).toString(16);
|
|
466
590
|
});
|
|
467
591
|
}
|
|
468
|
-
async function doRequest(
|
|
592
|
+
async function doRequest(_ref5) {
|
|
469
593
|
let {
|
|
470
594
|
requestMethod,
|
|
471
595
|
token,
|
|
@@ -474,7 +598,7 @@ async function doRequest(_ref4) {
|
|
|
474
598
|
attachUrlPrefix = true,
|
|
475
599
|
convertPayloadToUri = true,
|
|
476
600
|
logger
|
|
477
|
-
} =
|
|
601
|
+
} = _ref5;
|
|
478
602
|
if (payload && convertPayloadToUri) {
|
|
479
603
|
uri = uri + (uri.endsWith('?') ? '' : '?') + Object.keys(payload).map(key => {
|
|
480
604
|
return fixedEncodeURIComponent(key) + '=' + fixedEncodeURIComponent(payload[key]);
|
|
@@ -513,7 +637,7 @@ async function doRequest(_ref4) {
|
|
|
513
637
|
});
|
|
514
638
|
} catch (e) {
|
|
515
639
|
if (e.response.status == 503) {
|
|
516
|
-
loggerWarn(logger, `Twitter API 503 error - Service Unavailable`, {
|
|
640
|
+
(0, _loggerHelpers.loggerWarn)(logger, `Twitter API 503 error - Service Unavailable`, {
|
|
517
641
|
url,
|
|
518
642
|
convertPayloadToUri,
|
|
519
643
|
payload: JSON.stringify(payload)
|
|
@@ -6,7 +6,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.removePrefix = removePrefix;
|
|
7
7
|
exports.twitterPrefixCheck = twitterPrefixCheck;
|
|
8
8
|
function removePrefix(document) {
|
|
9
|
-
|
|
9
|
+
if (!document || typeof document !== 'string') {
|
|
10
|
+
return document; // Return as-is if not a valid string
|
|
11
|
+
}
|
|
12
|
+
return document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
|
|
10
13
|
}
|
|
11
14
|
function twitterPrefixCheck(socialOriginType, value) {
|
|
12
15
|
let result = value;
|
|
@@ -342,6 +342,19 @@ async function postApi(apiUrl, accessToken, payload, logger) {
|
|
|
342
342
|
}
|
|
343
343
|
response = await request;
|
|
344
344
|
} catch (err) {
|
|
345
|
+
// Handle specific case where content is already marked as spam
|
|
346
|
+
if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
|
|
347
|
+
loggerInfo(logger, `Content already marked as spam - treating as successful hide operation`, {
|
|
348
|
+
responseBody: JSON.stringify(err.response.body)
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
status: 200,
|
|
352
|
+
body: {
|
|
353
|
+
success: true,
|
|
354
|
+
message: "Content already hidden (marked as spam)"
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
345
358
|
loggerDebug(logger, `Failed to call facebook api`, err);
|
|
346
359
|
throw err;
|
|
347
360
|
}
|
|
@@ -181,6 +181,19 @@ export class FacebookApiClient {
|
|
|
181
181
|
access_token: accessToken
|
|
182
182
|
}).send(payload);
|
|
183
183
|
} catch (err) {
|
|
184
|
+
// Handle specific case where content is already marked as spam
|
|
185
|
+
if (err?.response?.body?.error?.error_subcode === 1446036 && err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request") {
|
|
186
|
+
loggerChild.info(`Content already marked as spam - treating as successful hide operation for documentId ${documentId}`, {
|
|
187
|
+
responseBody: JSON.stringify(err.response.body)
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
status: 200,
|
|
191
|
+
body: {
|
|
192
|
+
success: true,
|
|
193
|
+
message: "Content already hidden (marked as spam)"
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
184
197
|
let errorText = '';
|
|
185
198
|
if (err && err.response && err.response.body && err.response.body.error) {
|
|
186
199
|
errorText = `Failed to call facebook api for documentId ${documentId}: ${err.response.body.error.message}`;
|
|
@@ -4,6 +4,7 @@ import { CredentialsApiClient } from '../http/credentialsApi.client.js';
|
|
|
4
4
|
import logger from '../../lib/logger.js';
|
|
5
5
|
import { MeltwaterAttributes } from '../../lib/logger.helpers.js';
|
|
6
6
|
import { getOrganization as getOrganizationNative, getOrganizations as getOrganizationsNative, deleteMessage as deleteMessageNative } from './linkedin.native.js';
|
|
7
|
+
const LINKEDIN_VERSION = '202511';
|
|
7
8
|
export class LinkedInApiClient {
|
|
8
9
|
constructor() {
|
|
9
10
|
this.credentialsAPI = new CredentialsApiClient();
|
|
@@ -53,7 +54,7 @@ export class LinkedInApiClient {
|
|
|
53
54
|
response = await superagent.delete(query).timeout(5000).set({
|
|
54
55
|
Authorization: `Bearer ${credential.token}`,
|
|
55
56
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
56
|
-
'LinkedIn-Version':
|
|
57
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
57
58
|
});
|
|
58
59
|
} else {
|
|
59
60
|
loggerChild.debug(`${documentId} - sending to linkedin `, {
|
|
@@ -64,7 +65,7 @@ export class LinkedInApiClient {
|
|
|
64
65
|
.set({
|
|
65
66
|
Authorization: `Bearer ${credential.token}`,
|
|
66
67
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
67
|
-
'LinkedIn-Version':
|
|
68
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
68
69
|
}).send(payload);
|
|
69
70
|
}
|
|
70
71
|
loggerChild.info(`Native Linkedin API Like Comment Response for documentId ${documentId}`, {
|
|
@@ -174,7 +175,7 @@ export class LinkedInApiClient {
|
|
|
174
175
|
Authorization: `Bearer ${credential.token}`,
|
|
175
176
|
'Content-Type': 'application/json',
|
|
176
177
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
177
|
-
'LinkedIn-Version':
|
|
178
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
178
179
|
}).send(body);
|
|
179
180
|
response = {
|
|
180
181
|
status: 200,
|
|
@@ -203,7 +204,7 @@ export class LinkedInApiClient {
|
|
|
203
204
|
Authorization: `Bearer ${credential.token}`,
|
|
204
205
|
'Content-type': 'application/json',
|
|
205
206
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
206
|
-
'LinkedIn-Version':
|
|
207
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
207
208
|
}).send(body);
|
|
208
209
|
response = {
|
|
209
210
|
status: 200,
|
|
@@ -228,7 +229,7 @@ export class LinkedInApiClient {
|
|
|
228
229
|
.set({
|
|
229
230
|
Authorization: `Bearer ${token}`,
|
|
230
231
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
231
|
-
'LinkedIn-Version':
|
|
232
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
232
233
|
});
|
|
233
234
|
} catch (error) {
|
|
234
235
|
loggerChild.error('Error in getAllStats', error);
|
|
@@ -282,7 +283,7 @@ export class LinkedInApiClient {
|
|
|
282
283
|
.set({
|
|
283
284
|
Authorization: `Bearer ${credential.token}`,
|
|
284
285
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
285
|
-
'LinkedIn-Version':
|
|
286
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
286
287
|
});
|
|
287
288
|
} catch (error) {
|
|
288
289
|
loggerChild.error('Error in getImageMedia', error);
|
|
@@ -322,7 +323,7 @@ export class LinkedInApiClient {
|
|
|
322
323
|
.set({
|
|
323
324
|
Authorization: `Bearer ${credential.token}`,
|
|
324
325
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
325
|
-
'LinkedIn-Version':
|
|
326
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
326
327
|
});
|
|
327
328
|
} catch (error) {
|
|
328
329
|
loggerChild.error('Error in getVideoMedia', error);
|
|
@@ -389,7 +390,7 @@ export class LinkedInApiClient {
|
|
|
389
390
|
'Content-Type': 'application/octet-stream',
|
|
390
391
|
Authorization: `Bearer ${token}`,
|
|
391
392
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
392
|
-
'LinkedIn-Version':
|
|
393
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
393
394
|
}).send(data).catch(err => {
|
|
394
395
|
logger.error(err);
|
|
395
396
|
throw err;
|
|
@@ -405,7 +406,7 @@ export async function getProfile(urn, token) {
|
|
|
405
406
|
}).timeout(5000).set({
|
|
406
407
|
Authorization: `Bearer ${token}`,
|
|
407
408
|
'X-RestLi-Protocol-Version': configuration.get('LINKEDIN_API_VERSION'),
|
|
408
|
-
'LinkedIn-Version':
|
|
409
|
+
'LinkedIn-Version': LINKEDIN_VERSION
|
|
409
410
|
}).then(result => result.body);
|
|
410
411
|
} catch (error) {
|
|
411
412
|
logger.error(`Failed requesting LinkedIn API for profileId ${urn}`, error);
|
|
@@ -3,7 +3,7 @@ import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.j
|
|
|
3
3
|
const LINKEDIN_API = "https://api.linkedin.com/v2";
|
|
4
4
|
const LINKEDIN_API_REST = "https://api.linkedin.com/rest";
|
|
5
5
|
const LINKEDIN_API_VERSION = "2.0.0";
|
|
6
|
-
const LINKEDIN_VERSION = "
|
|
6
|
+
const LINKEDIN_VERSION = "202511";
|
|
7
7
|
export async function like(token, externalId, socialAccountId, logger) {
|
|
8
8
|
let response;
|
|
9
9
|
let payload = {
|
|
@@ -352,16 +352,18 @@ export async function getOrganization(urn, token, logger) {
|
|
|
352
352
|
let [,,, id] = urn.split(':');
|
|
353
353
|
let organization;
|
|
354
354
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}).timeout(5000) // 5 seconds
|
|
355
|
+
// Use the newer REST API endpoint instead of deprecated v2/organizations
|
|
356
|
+
organization = await superagent.get(`${LINKEDIN_API_REST}/organizationsLookup?ids=List(${fixedEncodeURIComponent(id)})`).timeout(5000) // 5 seconds
|
|
358
357
|
.set({
|
|
359
358
|
Authorization: `Bearer ${token}`,
|
|
360
359
|
'X-RestLi-Protocol-Version': LINKEDIN_API_VERSION,
|
|
361
360
|
'LinkedIn-Version': LINKEDIN_VERSION
|
|
362
|
-
}).then(result =>
|
|
361
|
+
}).then(result => {
|
|
362
|
+
const res = result.body;
|
|
363
|
+
return res.results[id];
|
|
364
|
+
});
|
|
363
365
|
} catch (error) {
|
|
364
|
-
if (error.response.body.message.includes('ADMIN_ONLY')) {
|
|
366
|
+
if (error.response && error.response.body && error.response.body.message && error.response.body.message.includes('ADMIN_ONLY')) {
|
|
365
367
|
return {
|
|
366
368
|
id: 'private'
|
|
367
369
|
};
|
|
@@ -1,11 +1,117 @@
|
|
|
1
1
|
import superagent from 'superagent';
|
|
2
2
|
import { removePrefix } from '../../lib/externalId.helpers.js';
|
|
3
|
-
import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.js';
|
|
3
|
+
import { loggerDebug, loggerError, loggerInfo, loggerWarn } from '../../lib/logger.helpers.js';
|
|
4
4
|
import OAuth from 'oauth-1.0a';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import Twit from '@meltwater/social-twit';
|
|
9
|
+
const TWITTER_API_CONFIG = {
|
|
10
|
+
v1_1: {
|
|
11
|
+
base_url: 'https://api.twitter.com/1.1'
|
|
12
|
+
},
|
|
13
|
+
v2: {
|
|
14
|
+
base_url: 'https://api.x.com/2',
|
|
15
|
+
default_user_fields: 'id,name,username,profile_image_url,public_metrics,description,created_at,verified,protected'
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Helper function to create v2 API URLs with consistent field selection
|
|
20
|
+
function createTwitterV2Url(endpoint) {
|
|
21
|
+
let additionalParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
22
|
+
const baseUrl = `${TWITTER_API_CONFIG.v2.base_url}/${endpoint}`;
|
|
23
|
+
const defaultFields = {
|
|
24
|
+
'user.fields': TWITTER_API_CONFIG.v2.default_user_fields
|
|
25
|
+
};
|
|
26
|
+
const allParams = {
|
|
27
|
+
...defaultFields,
|
|
28
|
+
...additionalParams
|
|
29
|
+
};
|
|
30
|
+
const queryString = Object.entries(allParams).map(_ref => {
|
|
31
|
+
let [key, value] = _ref;
|
|
32
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
33
|
+
}).join('&');
|
|
34
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Helper function to make v2 API requests with fallback to v1.1
|
|
38
|
+
async function makeTwitterV2Request(v2Endpoint, v1FallbackQuery, token, logger) {
|
|
39
|
+
let requestOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
|
|
40
|
+
// Try v2 API first - it supports OAuth 1.0a for user lookup endpoints and has better rate limits!
|
|
41
|
+
try {
|
|
42
|
+
const v2Url = createTwitterV2Url(v2Endpoint, requestOptions.v2Params);
|
|
43
|
+
const result = await getRequest({
|
|
44
|
+
token,
|
|
45
|
+
uri: v2Url,
|
|
46
|
+
attachUrlPrefix: false,
|
|
47
|
+
convertPayloadToUri: false,
|
|
48
|
+
logger
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
data: result.data?.data,
|
|
53
|
+
source: 'v2'
|
|
54
|
+
};
|
|
55
|
+
} catch (e) {
|
|
56
|
+
loggerWarn(logger, `Twitter API v2 request failed for ${v2Endpoint}, falling back to v1.1`, e);
|
|
57
|
+
|
|
58
|
+
// Fallback to v1.1 if v2 fails
|
|
59
|
+
if (v1FallbackQuery && requestOptions.enableFallback !== false) {
|
|
60
|
+
try {
|
|
61
|
+
loggerInfo(logger, `Falling back to v1.1 API for ${v2Endpoint}`);
|
|
62
|
+
const fallbackResult = await (requestOptions.fallbackMethod === 'post' ? postRequest : getRequest)({
|
|
63
|
+
token,
|
|
64
|
+
uri: v1FallbackQuery,
|
|
65
|
+
logger
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
data: fallbackResult.data,
|
|
70
|
+
source: 'v1.1'
|
|
71
|
+
};
|
|
72
|
+
} catch (fallbackError) {
|
|
73
|
+
loggerError(logger, `Both v2 and v1.1 APIs failed for ${v2Endpoint}`, fallbackError);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: e,
|
|
79
|
+
source: 'failed'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function normalizeUserData(user) {
|
|
84
|
+
let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
|
|
85
|
+
if (!user) return user;
|
|
86
|
+
const normalized = {
|
|
87
|
+
...user
|
|
88
|
+
};
|
|
89
|
+
if (apiVersion === 'v2') {
|
|
90
|
+
normalized.screen_name = user.username;
|
|
91
|
+
normalized.id_str = user.id;
|
|
92
|
+
normalized.profile_image_url_https = user.profile_image_url;
|
|
93
|
+
} else {
|
|
94
|
+
normalized.username = user.screen_name;
|
|
95
|
+
normalized.id = user.id_str;
|
|
96
|
+
normalized.profile_image_url = user.profile_image_url_https;
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Normalizes an array of user data or a single user object
|
|
103
|
+
* @param {Object|Array} users - User object or array of user objects from Twitter API
|
|
104
|
+
* @param {string} apiVersion - API version used ('v1.1' or 'v2')
|
|
105
|
+
* @returns {Object|Array} Normalized user data with both screen_name and username
|
|
106
|
+
*/
|
|
107
|
+
function normalizeUsersData(users) {
|
|
108
|
+
let apiVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'v1.1';
|
|
109
|
+
if (!users) return users;
|
|
110
|
+
if (Array.isArray(users)) {
|
|
111
|
+
return users.map(user => normalizeUserData(user, apiVersion));
|
|
112
|
+
}
|
|
113
|
+
return normalizeUserData(users, apiVersion);
|
|
114
|
+
}
|
|
9
115
|
export function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
|
|
10
116
|
if (!token.oauth) {
|
|
11
117
|
token.oauth = OAuth({
|
|
@@ -30,28 +136,46 @@ export async function getUserInfoFromHandles(token, handles, logger) {
|
|
|
30
136
|
loggerWarn(logger, `No handles provided to twitterNative getUserInfoFromHandles`);
|
|
31
137
|
return [];
|
|
32
138
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
139
|
+
|
|
140
|
+
// Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
|
|
141
|
+
// v2 API supports up to 100 usernames per request vs v1.1's limit
|
|
142
|
+
const result = await makeTwitterV2Request('users/by',
|
|
143
|
+
// v2 endpoint
|
|
144
|
+
`users/lookup.json?screen_name=${handlesJoin}`,
|
|
145
|
+
// v1.1 fallback
|
|
146
|
+
token, logger, {
|
|
147
|
+
v2Params: {
|
|
148
|
+
usernames: handlesJoin
|
|
149
|
+
},
|
|
150
|
+
fallbackMethod: 'post'
|
|
38
151
|
});
|
|
39
|
-
|
|
152
|
+
const normalizedData = normalizeUsersData(result.data, result.source);
|
|
153
|
+
return normalizedData || [];
|
|
40
154
|
} catch (e) {
|
|
41
|
-
loggerError(logger, `
|
|
155
|
+
loggerError(logger, `Unexpected error in getUserInfoFromHandles`, e);
|
|
156
|
+
return [];
|
|
42
157
|
}
|
|
43
158
|
}
|
|
44
159
|
export async function getUserInfoFromHandle(token, handleId, logger) {
|
|
45
160
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
161
|
+
if (!handleId) {
|
|
162
|
+
loggerWarn(logger, `Invalid handleId provided to getUserInfoFromHandle: ${JSON.stringify(handleId)}`);
|
|
163
|
+
throw new Error(`Invalid handleId: ${handleId}`);
|
|
164
|
+
}
|
|
165
|
+
// Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
|
|
166
|
+
const result = await makeTwitterV2Request(`users/${removePrefix(handleId)}`,
|
|
167
|
+
// v2 endpoint
|
|
168
|
+
`users/show.json?user_id=${removePrefix(handleId)}`,
|
|
169
|
+
// v1.1 fallback
|
|
170
|
+
token, logger);
|
|
171
|
+
if (result.success) {
|
|
172
|
+
// Normalize the user data to ensure both screen_name and username are present
|
|
173
|
+
return normalizeUserData(result.data, result.source);
|
|
174
|
+
} else {
|
|
175
|
+
throw result.error || new Error('Failed to get user info from both APIs');
|
|
176
|
+
}
|
|
53
177
|
} catch (e) {
|
|
54
|
-
loggerError(logger, `Error
|
|
178
|
+
loggerError(logger, `Error in getUserInfoFromHandle with handleId: ${JSON.stringify(handleId)}`, e);
|
|
55
179
|
}
|
|
56
180
|
}
|
|
57
181
|
export async function getDirectMessageImage(token, imageUrl, logger) {
|
|
@@ -380,7 +504,7 @@ async function publishTweet(token, payload, query, isDirectMessage, logger) {
|
|
|
380
504
|
throw err;
|
|
381
505
|
}
|
|
382
506
|
}
|
|
383
|
-
async function postRequest(
|
|
507
|
+
async function postRequest(_ref2) {
|
|
384
508
|
let {
|
|
385
509
|
token,
|
|
386
510
|
uri,
|
|
@@ -388,7 +512,7 @@ async function postRequest(_ref) {
|
|
|
388
512
|
attachUrlPrefix = true,
|
|
389
513
|
convertPayloadToUri = true,
|
|
390
514
|
logger
|
|
391
|
-
} =
|
|
515
|
+
} = _ref2;
|
|
392
516
|
return doRequest({
|
|
393
517
|
requestMethod: 'post',
|
|
394
518
|
token,
|
|
@@ -399,7 +523,7 @@ async function postRequest(_ref) {
|
|
|
399
523
|
logger
|
|
400
524
|
});
|
|
401
525
|
}
|
|
402
|
-
async function getRequest(
|
|
526
|
+
async function getRequest(_ref3) {
|
|
403
527
|
let {
|
|
404
528
|
token,
|
|
405
529
|
uri,
|
|
@@ -407,7 +531,7 @@ async function getRequest(_ref2) {
|
|
|
407
531
|
attachUrlPrefix = true,
|
|
408
532
|
convertPayloadToUri = true,
|
|
409
533
|
logger
|
|
410
|
-
} =
|
|
534
|
+
} = _ref3;
|
|
411
535
|
return doRequest({
|
|
412
536
|
requestMethod: 'get',
|
|
413
537
|
payload,
|
|
@@ -418,7 +542,7 @@ async function getRequest(_ref2) {
|
|
|
418
542
|
logger
|
|
419
543
|
});
|
|
420
544
|
}
|
|
421
|
-
async function deleteRequest(
|
|
545
|
+
async function deleteRequest(_ref4) {
|
|
422
546
|
let {
|
|
423
547
|
token,
|
|
424
548
|
uri,
|
|
@@ -426,7 +550,7 @@ async function deleteRequest(_ref3) {
|
|
|
426
550
|
attachUrlPrefix = false,
|
|
427
551
|
convertPayloadToUri = false,
|
|
428
552
|
logger
|
|
429
|
-
} =
|
|
553
|
+
} = _ref4;
|
|
430
554
|
return doRequest({
|
|
431
555
|
requestMethod: 'delete',
|
|
432
556
|
token,
|
|
@@ -442,7 +566,7 @@ function fixedEncodeURIComponent(str) {
|
|
|
442
566
|
return '%' + c.charCodeAt(0).toString(16);
|
|
443
567
|
});
|
|
444
568
|
}
|
|
445
|
-
async function doRequest(
|
|
569
|
+
async function doRequest(_ref5) {
|
|
446
570
|
let {
|
|
447
571
|
requestMethod,
|
|
448
572
|
token,
|
|
@@ -451,7 +575,7 @@ async function doRequest(_ref4) {
|
|
|
451
575
|
attachUrlPrefix = true,
|
|
452
576
|
convertPayloadToUri = true,
|
|
453
577
|
logger
|
|
454
|
-
} =
|
|
578
|
+
} = _ref5;
|
|
455
579
|
if (payload && convertPayloadToUri) {
|
|
456
580
|
uri = uri + (uri.endsWith('?') ? '' : '?') + Object.keys(payload).map(key => {
|
|
457
581
|
return fixedEncodeURIComponent(key) + '=' + fixedEncodeURIComponent(payload[key]);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export function removePrefix(document) {
|
|
2
|
-
|
|
2
|
+
if (!document || typeof document !== 'string') {
|
|
3
|
+
return document; // Return as-is if not a valid string
|
|
4
|
+
}
|
|
5
|
+
return document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
|
|
3
6
|
}
|
|
4
7
|
export function twitterPrefixCheck(socialOriginType, value) {
|
|
5
8
|
let result = value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meltwater/conversations-api-services",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.24",
|
|
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",
|
|
@@ -545,6 +545,20 @@ async function postApi(
|
|
|
545
545
|
|
|
546
546
|
response = await request;
|
|
547
547
|
} catch (err) {
|
|
548
|
+
// Handle specific case where content is already marked as spam
|
|
549
|
+
if (
|
|
550
|
+
err?.response?.body?.error?.error_subcode === 1446036 &&
|
|
551
|
+
err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request"
|
|
552
|
+
) {
|
|
553
|
+
loggerInfo(logger, `Content already marked as spam - treating as successful hide operation`, {
|
|
554
|
+
responseBody: JSON.stringify(err.response.body),
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
status: 200,
|
|
558
|
+
body: { success: true, message: "Content already hidden (marked as spam)" }
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
548
562
|
loggerDebug(logger, `Failed to call facebook api`, err);
|
|
549
563
|
throw err;
|
|
550
564
|
}
|
|
@@ -232,6 +232,20 @@ export class FacebookApiClient {
|
|
|
232
232
|
.query({ access_token: accessToken })
|
|
233
233
|
.send(payload);
|
|
234
234
|
} catch (err) {
|
|
235
|
+
// Handle specific case where content is already marked as spam
|
|
236
|
+
if (
|
|
237
|
+
err?.response?.body?.error?.error_subcode === 1446036 &&
|
|
238
|
+
err?.response?.body?.error?.error_user_title === "Duplicate Mark Spam Request"
|
|
239
|
+
) {
|
|
240
|
+
loggerChild.info(`Content already marked as spam - treating as successful hide operation for documentId ${documentId}`, {
|
|
241
|
+
responseBody: JSON.stringify(err.response.body),
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
status: 200,
|
|
245
|
+
body: { success: true, message: "Content already hidden (marked as spam)" }
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
235
249
|
let errorText = '';
|
|
236
250
|
if (
|
|
237
251
|
err &&
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
deleteMessage as deleteMessageNative,
|
|
10
10
|
} from './linkedin.native.js'
|
|
11
11
|
|
|
12
|
+
const LINKEDIN_VERSION = '202511';
|
|
13
|
+
|
|
12
14
|
export class LinkedInApiClient {
|
|
13
15
|
constructor() {
|
|
14
16
|
this.credentialsAPI = new CredentialsApiClient();
|
|
@@ -71,7 +73,7 @@ export class LinkedInApiClient {
|
|
|
71
73
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
72
74
|
'LINKEDIN_API_VERSION'
|
|
73
75
|
),
|
|
74
|
-
'LinkedIn-Version':
|
|
76
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
75
77
|
});
|
|
76
78
|
} else {
|
|
77
79
|
loggerChild.debug(`${documentId} - sending to linkedin `, {
|
|
@@ -86,7 +88,7 @@ export class LinkedInApiClient {
|
|
|
86
88
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
87
89
|
'LINKEDIN_API_VERSION'
|
|
88
90
|
),
|
|
89
|
-
'LinkedIn-Version':
|
|
91
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
90
92
|
})
|
|
91
93
|
.send(payload);
|
|
92
94
|
}
|
|
@@ -221,7 +223,7 @@ export class LinkedInApiClient {
|
|
|
221
223
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
222
224
|
'LINKEDIN_API_VERSION'
|
|
223
225
|
),
|
|
224
|
-
'LinkedIn-Version':
|
|
226
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
225
227
|
})
|
|
226
228
|
.send(body);
|
|
227
229
|
|
|
@@ -272,7 +274,7 @@ export class LinkedInApiClient {
|
|
|
272
274
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
273
275
|
'LINKEDIN_API_VERSION'
|
|
274
276
|
),
|
|
275
|
-
'LinkedIn-Version':
|
|
277
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
276
278
|
})
|
|
277
279
|
.send(body);
|
|
278
280
|
|
|
@@ -312,7 +314,7 @@ export class LinkedInApiClient {
|
|
|
312
314
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
313
315
|
'LINKEDIN_API_VERSION'
|
|
314
316
|
),
|
|
315
|
-
'LinkedIn-Version':
|
|
317
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
316
318
|
});
|
|
317
319
|
} catch (error) {
|
|
318
320
|
loggerChild.error('Error in getAllStats', error);
|
|
@@ -380,7 +382,7 @@ export class LinkedInApiClient {
|
|
|
380
382
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
381
383
|
'LINKEDIN_API_VERSION'
|
|
382
384
|
),
|
|
383
|
-
'LinkedIn-Version':
|
|
385
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
384
386
|
});
|
|
385
387
|
} catch (error) {
|
|
386
388
|
loggerChild.error('Error in getImageMedia', error);
|
|
@@ -439,7 +441,7 @@ export class LinkedInApiClient {
|
|
|
439
441
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
440
442
|
'LINKEDIN_API_VERSION'
|
|
441
443
|
),
|
|
442
|
-
'LinkedIn-Version':
|
|
444
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
443
445
|
});
|
|
444
446
|
} catch (error) {
|
|
445
447
|
loggerChild.error('Error in getVideoMedia', error);
|
|
@@ -513,7 +515,7 @@ export class LinkedInApiClient {
|
|
|
513
515
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
514
516
|
'LINKEDIN_API_VERSION'
|
|
515
517
|
),
|
|
516
|
-
'LinkedIn-Version':
|
|
518
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
517
519
|
})
|
|
518
520
|
.send(data)
|
|
519
521
|
.catch((err) => {
|
|
@@ -540,7 +542,7 @@ export async function getProfile(urn, token) {
|
|
|
540
542
|
'X-RestLi-Protocol-Version': configuration.get(
|
|
541
543
|
'LINKEDIN_API_VERSION'
|
|
542
544
|
),
|
|
543
|
-
'LinkedIn-Version':
|
|
545
|
+
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
544
546
|
})
|
|
545
547
|
.then((result) => result.body);
|
|
546
548
|
} catch (error) {
|
|
@@ -5,7 +5,7 @@ import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.j
|
|
|
5
5
|
const LINKEDIN_API = "https://api.linkedin.com/v2";
|
|
6
6
|
const LINKEDIN_API_REST = "https://api.linkedin.com/rest";
|
|
7
7
|
const LINKEDIN_API_VERSION = "2.0.0";
|
|
8
|
-
const LINKEDIN_VERSION = "
|
|
8
|
+
const LINKEDIN_VERSION = "202511";
|
|
9
9
|
|
|
10
10
|
export async function like(token, externalId, socialAccountId, logger) {
|
|
11
11
|
let response;
|
|
@@ -474,21 +474,21 @@ export async function getOrganization(urn, token, logger) {
|
|
|
474
474
|
|
|
475
475
|
let organization;
|
|
476
476
|
try {
|
|
477
|
+
// Use the newer REST API endpoint instead of deprecated v2/organizations
|
|
477
478
|
organization = await superagent
|
|
478
|
-
.get(`${
|
|
479
|
-
.query({
|
|
480
|
-
projection:
|
|
481
|
-
'(id,localizedName,vanityName,logoV2(original~:playableStreams))',
|
|
482
|
-
})
|
|
479
|
+
.get(`${LINKEDIN_API_REST}/organizationsLookup?ids=List(${fixedEncodeURIComponent(id)})`)
|
|
483
480
|
.timeout(5000) // 5 seconds
|
|
484
481
|
.set({
|
|
485
482
|
Authorization: `Bearer ${token}`,
|
|
486
483
|
'X-RestLi-Protocol-Version': LINKEDIN_API_VERSION,
|
|
487
484
|
'LinkedIn-Version': LINKEDIN_VERSION,
|
|
488
485
|
})
|
|
489
|
-
.then((result) =>
|
|
486
|
+
.then((result) => {
|
|
487
|
+
const res = result.body;
|
|
488
|
+
return res.results[id];
|
|
489
|
+
});
|
|
490
490
|
} catch (error) {
|
|
491
|
-
if (error.response.body.message.includes('ADMIN_ONLY')) {
|
|
491
|
+
if (error.response && error.response.body && error.response.body.message && error.response.body.message.includes('ADMIN_ONLY')) {
|
|
492
492
|
return { id: 'private' };
|
|
493
493
|
}
|
|
494
494
|
|
|
@@ -1,12 +1,105 @@
|
|
|
1
1
|
import superagent from 'superagent';
|
|
2
2
|
import { removePrefix } from '../../lib/externalId.helpers.js';
|
|
3
|
-
import { loggerDebug, loggerError, loggerInfo } from '../../lib/logger.helpers.js';
|
|
3
|
+
import { loggerDebug, loggerError, loggerInfo, loggerWarn } from '../../lib/logger.helpers.js';
|
|
4
4
|
import OAuth from 'oauth-1.0a';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import Twit from '@meltwater/social-twit';
|
|
9
9
|
|
|
10
|
+
const TWITTER_API_CONFIG = {
|
|
11
|
+
v1_1: {
|
|
12
|
+
base_url: 'https://api.twitter.com/1.1',
|
|
13
|
+
},
|
|
14
|
+
v2: {
|
|
15
|
+
base_url: 'https://api.x.com/2',
|
|
16
|
+
default_user_fields: 'id,name,username,profile_image_url,public_metrics,description,created_at,verified,protected'
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Helper function to create v2 API URLs with consistent field selection
|
|
21
|
+
function createTwitterV2Url(endpoint, additionalParams = {}) {
|
|
22
|
+
const baseUrl = `${TWITTER_API_CONFIG.v2.base_url}/${endpoint}`;
|
|
23
|
+
const defaultFields = { 'user.fields': TWITTER_API_CONFIG.v2.default_user_fields };
|
|
24
|
+
const allParams = { ...defaultFields, ...additionalParams };
|
|
25
|
+
|
|
26
|
+
const queryString = Object.entries(allParams)
|
|
27
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
28
|
+
.join('&');
|
|
29
|
+
|
|
30
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Helper function to make v2 API requests with fallback to v1.1
|
|
34
|
+
async function makeTwitterV2Request(v2Endpoint, v1FallbackQuery, token, logger, requestOptions = {}) {
|
|
35
|
+
// Try v2 API first - it supports OAuth 1.0a for user lookup endpoints and has better rate limits!
|
|
36
|
+
try {
|
|
37
|
+
const v2Url = createTwitterV2Url(v2Endpoint, requestOptions.v2Params);
|
|
38
|
+
|
|
39
|
+
const result = await getRequest({
|
|
40
|
+
token,
|
|
41
|
+
uri: v2Url,
|
|
42
|
+
attachUrlPrefix: false,
|
|
43
|
+
convertPayloadToUri: false,
|
|
44
|
+
logger
|
|
45
|
+
});
|
|
46
|
+
return { success: true, data: result.data?.data, source: 'v2' };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
loggerWarn(logger, `Twitter API v2 request failed for ${v2Endpoint}, falling back to v1.1`, e);
|
|
49
|
+
|
|
50
|
+
// Fallback to v1.1 if v2 fails
|
|
51
|
+
if (v1FallbackQuery && requestOptions.enableFallback !== false) {
|
|
52
|
+
try {
|
|
53
|
+
loggerInfo(logger, `Falling back to v1.1 API for ${v2Endpoint}`);
|
|
54
|
+
const fallbackResult = await (requestOptions.fallbackMethod === 'post' ? postRequest : getRequest)({
|
|
55
|
+
token,
|
|
56
|
+
uri: v1FallbackQuery,
|
|
57
|
+
logger
|
|
58
|
+
});
|
|
59
|
+
return { success: true, data: fallbackResult.data, source: 'v1.1' };
|
|
60
|
+
} catch (fallbackError) {
|
|
61
|
+
loggerError(logger, `Both v2 and v1.1 APIs failed for ${v2Endpoint}`, fallbackError);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { success: false, error: e, source: 'failed' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeUserData(user, apiVersion = 'v1.1') {
|
|
70
|
+
if (!user) return user;
|
|
71
|
+
|
|
72
|
+
const normalized = { ...user };
|
|
73
|
+
|
|
74
|
+
if (apiVersion === 'v2') {
|
|
75
|
+
normalized.screen_name = user.username;
|
|
76
|
+
normalized.id_str = user.id;
|
|
77
|
+
normalized.profile_image_url_https = user.profile_image_url;
|
|
78
|
+
} else {
|
|
79
|
+
normalized.username = user.screen_name;
|
|
80
|
+
normalized.id = user.id_str;
|
|
81
|
+
normalized.profile_image_url = user.profile_image_url_https;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalizes an array of user data or a single user object
|
|
89
|
+
* @param {Object|Array} users - User object or array of user objects from Twitter API
|
|
90
|
+
* @param {string} apiVersion - API version used ('v1.1' or 'v2')
|
|
91
|
+
* @returns {Object|Array} Normalized user data with both screen_name and username
|
|
92
|
+
*/
|
|
93
|
+
function normalizeUsersData(users, apiVersion = 'v1.1') {
|
|
94
|
+
if (!users) return users;
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(users)) {
|
|
97
|
+
return users.map(user => normalizeUserData(user, apiVersion));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return normalizeUserData(users, apiVersion);
|
|
101
|
+
}
|
|
102
|
+
|
|
10
103
|
|
|
11
104
|
|
|
12
105
|
export function addOAuthToToken(token, TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET) {
|
|
@@ -37,21 +130,50 @@ export async function getUserInfoFromHandles(token, handles, logger) {
|
|
|
37
130
|
loggerWarn(logger,`No handles provided to twitterNative getUserInfoFromHandles`);
|
|
38
131
|
return [];
|
|
39
132
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
133
|
+
|
|
134
|
+
// Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
|
|
135
|
+
// v2 API supports up to 100 usernames per request vs v1.1's limit
|
|
136
|
+
const result = await makeTwitterV2Request(
|
|
137
|
+
'users/by', // v2 endpoint
|
|
138
|
+
`users/lookup.json?screen_name=${handlesJoin}`, // v1.1 fallback
|
|
139
|
+
token,
|
|
140
|
+
logger,
|
|
141
|
+
{
|
|
142
|
+
v2Params: { usernames: handlesJoin },
|
|
143
|
+
fallbackMethod: 'post'
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const normalizedData = normalizeUsersData(result.data, result.source);
|
|
148
|
+
return normalizedData || [];
|
|
43
149
|
} catch (e) {
|
|
44
|
-
loggerError(logger
|
|
150
|
+
loggerError(logger, `Unexpected error in getUserInfoFromHandles`, e);
|
|
151
|
+
return [];
|
|
45
152
|
}
|
|
46
153
|
}
|
|
47
154
|
|
|
48
155
|
export async function getUserInfoFromHandle(token, handleId, logger) {
|
|
49
156
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
157
|
+
if (!handleId) {
|
|
158
|
+
loggerWarn(logger, `Invalid handleId provided to getUserInfoFromHandle: ${JSON.stringify(handleId)}`);
|
|
159
|
+
throw new Error(`Invalid handleId: ${handleId}`);
|
|
160
|
+
}
|
|
161
|
+
// Use Twitter API v2 for much better rate limits (900 req/15min vs v1.1 limits)
|
|
162
|
+
const result = await makeTwitterV2Request(
|
|
163
|
+
`users/${removePrefix(handleId)}`, // v2 endpoint
|
|
164
|
+
`users/show.json?user_id=${removePrefix(handleId)}`, // v1.1 fallback
|
|
165
|
+
token,
|
|
166
|
+
logger
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (result.success) {
|
|
170
|
+
// Normalize the user data to ensure both screen_name and username are present
|
|
171
|
+
return normalizeUserData(result.data, result.source);
|
|
172
|
+
} else {
|
|
173
|
+
throw result.error || new Error('Failed to get user info from both APIs');
|
|
174
|
+
}
|
|
53
175
|
} catch (e) {
|
|
54
|
-
loggerError(logger
|
|
176
|
+
loggerError(logger, `Error in getUserInfoFromHandle with handleId: ${JSON.stringify(handleId)}`, e);
|
|
55
177
|
}
|
|
56
178
|
}
|
|
57
179
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export function removePrefix(document) {
|
|
2
|
-
|
|
2
|
+
if (!document || typeof document !== 'string') {
|
|
3
|
+
return document; // Return as-is if not a valid string
|
|
4
|
+
}
|
|
5
|
+
return document.replace(/id\:[a-zA-Z]+\.(com|net):/g, '');
|
|
3
6
|
}
|
|
4
7
|
|
|
5
8
|
export function twitterPrefixCheck(socialOriginType, value) {
|