@meltwater/conversations-api-services 1.1.22 → 1.1.23
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/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/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/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}`;
|
|
@@ -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}`;
|
|
@@ -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.23",
|
|
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 &&
|
|
@@ -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) {
|