@lazyneoaz/metachat 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.
- package/LICENSE +3 -0
- package/README.md +199 -0
- package/index.js +2 -0
- package/package.json +86 -0
- package/src/apis/addExternalModule.js +24 -0
- package/src/apis/addUserToGroup.js +108 -0
- package/src/apis/changeAdminStatus.js +148 -0
- package/src/apis/changeArchivedStatus.js +61 -0
- package/src/apis/changeAvatar.js +103 -0
- package/src/apis/changeBio.js +69 -0
- package/src/apis/changeBlockedStatus.js +54 -0
- package/src/apis/changeGroupImage.js +136 -0
- package/src/apis/changeThreadColor.js +116 -0
- package/src/apis/changeThreadEmoji.js +53 -0
- package/src/apis/comment.js +207 -0
- package/src/apis/createAITheme.js +129 -0
- package/src/apis/createNewGroup.js +79 -0
- package/src/apis/createPoll.js +73 -0
- package/src/apis/deleteMessage.js +44 -0
- package/src/apis/deleteThread.js +52 -0
- package/src/apis/editMessage.js +70 -0
- package/src/apis/emoji.js +124 -0
- package/src/apis/enableAutoSaveAppState.js +69 -0
- package/src/apis/fetchThemeData.js +113 -0
- package/src/apis/follow.js +81 -0
- package/src/apis/forwardAttachment.js +195 -0
- package/src/apis/forwardMessage.js +52 -0
- package/src/apis/friend.js +243 -0
- package/src/apis/gcmember.js +122 -0
- package/src/apis/gcname.js +123 -0
- package/src/apis/gcrule.js +119 -0
- package/src/apis/getAccess.js +111 -0
- package/src/apis/getBotInfo.js +88 -0
- package/src/apis/getBotInitialData.js +43 -0
- package/src/apis/getEmojiUrl.js +40 -0
- package/src/apis/getFriendsList.js +79 -0
- package/src/apis/getMessage.js +423 -0
- package/src/apis/getTheme.js +123 -0
- package/src/apis/getThemeInfo.js +116 -0
- package/src/apis/getThemePictures.js +87 -0
- package/src/apis/getThreadColors.js +119 -0
- package/src/apis/getThreadHistory.js +239 -0
- package/src/apis/getThreadInfo.js +271 -0
- package/src/apis/getThreadList.js +236 -0
- package/src/apis/getThreadPictures.js +58 -0
- package/src/apis/getUserID.js +117 -0
- package/src/apis/getUserInfo.js +513 -0
- package/src/apis/getUserInfoV2.js +146 -0
- package/src/apis/handleFriendRequest.js +66 -0
- package/src/apis/handleMessageRequest.js +50 -0
- package/src/apis/httpGet.js +63 -0
- package/src/apis/httpPost.js +89 -0
- package/src/apis/httpPostFormData.js +69 -0
- package/src/apis/listenMqtt.js +1081 -0
- package/src/apis/listenSpeed.js +178 -0
- package/src/apis/logout.js +63 -0
- package/src/apis/markAsDelivered.js +47 -0
- package/src/apis/markAsRead.js +82 -0
- package/src/apis/markAsReadAll.js +40 -0
- package/src/apis/markAsSeen.js +70 -0
- package/src/apis/mqttDeltaValue.js +252 -0
- package/src/apis/muteThread.js +45 -0
- package/src/apis/nickname.js +132 -0
- package/src/apis/notes.js +163 -0
- package/src/apis/pinMessage.js +150 -0
- package/src/apis/produceMetaTheme.js +160 -0
- package/src/apis/realtime.js +182 -0
- package/src/apis/refreshFb_dtsg.js +94 -0
- package/src/apis/removeUserFromGroup.js +117 -0
- package/src/apis/resolvePhotoUrl.js +58 -0
- package/src/apis/scheduler.js +129 -0
- package/src/apis/searchForThread.js +154 -0
- package/src/apis/sendEffect.js +311 -0
- package/src/apis/sendMessage.js +341 -0
- package/src/apis/sendMessageMqtt.js +271 -0
- package/src/apis/sendTypingIndicator.js +74 -0
- package/src/apis/setMessageReaction.js +27 -0
- package/src/apis/setMessageReactionMqtt.js +61 -0
- package/src/apis/setPostReaction.js +118 -0
- package/src/apis/setThreadTheme.js +210 -0
- package/src/apis/setThreadThemeMqtt.js +94 -0
- package/src/apis/setTitle.js +26 -0
- package/src/apis/share.js +106 -0
- package/src/apis/shareContact.js +66 -0
- package/src/apis/stickers.js +257 -0
- package/src/apis/story.js +181 -0
- package/src/apis/theme.js +233 -0
- package/src/apis/unfriend.js +47 -0
- package/src/apis/unsendMessage.js +17 -0
- package/src/apis/uploadAttachment.js +87 -0
- package/src/database/appStateBackup.js +189 -0
- package/src/database/models/index.js +56 -0
- package/src/database/models/thread.js +31 -0
- package/src/database/models/user.js +32 -0
- package/src/database/threadData.js +101 -0
- package/src/database/userData.js +90 -0
- package/src/engine/client.js +92 -0
- package/src/engine/models/buildAPI.js +118 -0
- package/src/engine/models/loginHelper.js +492 -0
- package/src/engine/models/setOptions.js +88 -0
- package/src/types/index.d.ts +498 -0
- package/src/utils/antiSuspension.js +516 -0
- package/src/utils/auth-helpers.js +149 -0
- package/src/utils/autoReLogin.js +239 -0
- package/src/utils/axios.js +368 -0
- package/src/utils/cache.js +54 -0
- package/src/utils/clients.js +279 -0
- package/src/utils/constants.js +525 -0
- package/src/utils/formatters/data/formatAttachment.js +370 -0
- package/src/utils/formatters/data/formatDelta.js +109 -0
- package/src/utils/formatters/index.js +159 -0
- package/src/utils/formatters/value/formatCookie.js +91 -0
- package/src/utils/formatters/value/formatDate.js +36 -0
- package/src/utils/formatters/value/formatID.js +16 -0
- package/src/utils/formatters.js +1369 -0
- package/src/utils/headers.js +235 -0
- package/src/utils/index.js +153 -0
- package/src/utils/monitoring.js +333 -0
- package/src/utils/rateLimiter.js +251 -0
- package/src/utils/tokenRefresh.js +285 -0
- package/src/utils/user-agents.js +238 -0
- package/src/utils/validation.js +157 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Facebook Messenger send effects — visual animations that play when a message is sent/received.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* api.sendEffect.fire('Hello!', threadID)
|
|
10
|
+
* api.sendEffect.send('love', 'Hello!', threadID)
|
|
11
|
+
* api.sendEffect.list() → [{tag, aliases, emoji}, ...]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const EFFECTS = {
|
|
15
|
+
fire: { tag: 'FIRE', emoji: '🔥', label: 'Fire' },
|
|
16
|
+
love: { tag: 'LOVE', emoji: '❤️', label: 'Love' },
|
|
17
|
+
heart: { tag: 'LOVE', emoji: '❤️', label: 'Love' },
|
|
18
|
+
hearts: { tag: 'LOVE', emoji: '❤️', label: 'Love' },
|
|
19
|
+
confetti: { tag: 'CELEBRATION', emoji: '🎉', label: 'Confetti' },
|
|
20
|
+
celebration: { tag: 'CELEBRATION', emoji: '🎉', label: 'Confetti' },
|
|
21
|
+
wand: { tag: 'WAND', emoji: '🪄', label: 'Wand' },
|
|
22
|
+
magic: { tag: 'WAND', emoji: '🪄', label: 'Wand' },
|
|
23
|
+
fireworks: { tag: 'FIREWORKS', emoji: '🎆', label: 'Fireworks' },
|
|
24
|
+
shooting_star: { tag: 'SHOOTING_STAR', emoji: '🌠', label: 'Shooting Star' },
|
|
25
|
+
stars: { tag: 'SHOOTING_STAR', emoji: '🌠', label: 'Shooting Star' },
|
|
26
|
+
starstruck: { tag: 'SHOOTING_STAR', emoji: '🌠', label: 'Shooting Star' },
|
|
27
|
+
balloons: { tag: 'BIRTHDAY', emoji: '🎈', label: 'Balloons' },
|
|
28
|
+
birthday: { tag: 'BIRTHDAY', emoji: '🎈', label: 'Balloons' },
|
|
29
|
+
horror: { tag: 'HORROR', emoji: '👻', label: 'Horror' },
|
|
30
|
+
halloween: { tag: 'HORROR', emoji: '👻', label: 'Horror' },
|
|
31
|
+
las_vegas: { tag: 'LAS_VEGAS', emoji: '🎰', label: 'Las Vegas' },
|
|
32
|
+
vegas: { tag: 'LAS_VEGAS', emoji: '🎰', label: 'Las Vegas' },
|
|
33
|
+
anniversary: { tag: 'ANNIVERSARY', emoji: '💍', label: 'Anniversary' },
|
|
34
|
+
spotlight: { tag: 'SPOTLIGHT', emoji: '✨', label: 'Spotlight' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function resolveEffect(name) {
|
|
38
|
+
if (!name) return null;
|
|
39
|
+
const key = String(name).toLowerCase().replace(/[\s\-]+/g, '_');
|
|
40
|
+
return EFFECTS[key] ? EFFECTS[key].tag : name.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildForm(effectTag, msgObj, threadID, ctx) {
|
|
44
|
+
const messageAndOTID = utils.generateOfflineThreadingID();
|
|
45
|
+
const timestamp = Date.now();
|
|
46
|
+
const messagingTag = 'fb.messaging.effects.' + effectTag;
|
|
47
|
+
const body = typeof msgObj === 'string' ? msgObj : (msgObj.body || '');
|
|
48
|
+
|
|
49
|
+
const form = {
|
|
50
|
+
client: 'mercury',
|
|
51
|
+
action_type: 'ma-type:user-generated-message',
|
|
52
|
+
author: 'fbid:' + ctx.userID,
|
|
53
|
+
timestamp,
|
|
54
|
+
timestamp_absolute: 'Today',
|
|
55
|
+
timestamp_relative: utils.generateTimestampRelative(),
|
|
56
|
+
timestamp_time_passed:'0',
|
|
57
|
+
is_unread: false,
|
|
58
|
+
is_cleared: false,
|
|
59
|
+
is_forward: false,
|
|
60
|
+
is_filtered_content: false,
|
|
61
|
+
is_filtered_content_bh: false,
|
|
62
|
+
is_filtered_content_account: false,
|
|
63
|
+
is_filtered_content_quasar: false,
|
|
64
|
+
is_filtered_content_invalid_app: false,
|
|
65
|
+
is_spoof_warning: false,
|
|
66
|
+
source: 'source:chat:web',
|
|
67
|
+
'source_tags[0]': 'source:chat',
|
|
68
|
+
body,
|
|
69
|
+
html_body: false,
|
|
70
|
+
ui_push_phase: 'V3',
|
|
71
|
+
status: '0',
|
|
72
|
+
offline_threading_id: messageAndOTID,
|
|
73
|
+
message_id: messageAndOTID,
|
|
74
|
+
threading_id: utils.generateThreadingID(ctx.clientID),
|
|
75
|
+
'ephemeral_ttl_mode:':'0',
|
|
76
|
+
manual_retry_cnt: '0',
|
|
77
|
+
has_attachment: !!(msgObj && msgObj.sticker),
|
|
78
|
+
signatureID: utils.getSignatureID(),
|
|
79
|
+
has_lightweight_action: true,
|
|
80
|
+
'lightweight_action_attached[message_id]': messageAndOTID,
|
|
81
|
+
'lightweight_action_attached[messaging_tag]': messagingTag,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (msgObj && msgObj.sticker) {
|
|
85
|
+
form.sticker_id = msgObj.sticker;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof msgObj === 'object' && msgObj.reply_to_message_id) {
|
|
89
|
+
form.replied_to_message_id = msgObj.reply_to_message_id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tid = String(threadID);
|
|
93
|
+
const isGroup = tid.length >= 16;
|
|
94
|
+
if (isGroup) {
|
|
95
|
+
form.thread_fbid = tid;
|
|
96
|
+
} else {
|
|
97
|
+
form['specific_to_list[0]'] = 'fbid:' + tid;
|
|
98
|
+
form['specific_to_list[1]'] = 'fbid:' + ctx.userID;
|
|
99
|
+
form.other_user_fbid = tid;
|
|
100
|
+
form.client_thread_id = 'root:' + messageAndOTID;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { form, messageAndOTID, timestamp, messagingTag };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildMqttPayload(effectTag, msgObj, threadID, ctx) {
|
|
107
|
+
const otid = utils.generateOfflineThreadingID();
|
|
108
|
+
const epoch_id = (BigInt(Date.now()) << 22n).toString();
|
|
109
|
+
const timestamp = Date.now();
|
|
110
|
+
const body = typeof msgObj === 'string' ? msgObj : (msgObj && msgObj.body || '');
|
|
111
|
+
const tid = String(threadID);
|
|
112
|
+
|
|
113
|
+
const sendPayload = {
|
|
114
|
+
thread_id: tid,
|
|
115
|
+
otid: otid.toString(),
|
|
116
|
+
source: 2097153,
|
|
117
|
+
send_type: 1,
|
|
118
|
+
sync_group: 1,
|
|
119
|
+
text: body || null,
|
|
120
|
+
initiating_source: 0,
|
|
121
|
+
mark_thread_read: 1,
|
|
122
|
+
multitab_env: 0,
|
|
123
|
+
skip_url_preview_gen: 0,
|
|
124
|
+
metadata_dataclass: JSON.stringify({ media_accessibility_metadata: { alt_text: null } }),
|
|
125
|
+
lightweight_action_attached: {
|
|
126
|
+
message_id: otid.toString(),
|
|
127
|
+
messaging_tag: 'fb.messaging.effects.' + effectTag,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (typeof msgObj === 'object' && msgObj && msgObj.sticker) {
|
|
132
|
+
sendPayload.send_type = 2;
|
|
133
|
+
sendPayload.sticker_id = msgObj.sticker;
|
|
134
|
+
sendPayload.text = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof ctx.wsReqNumber !== 'number') ctx.wsReqNumber = 0;
|
|
138
|
+
const request_id = ++ctx.wsReqNumber;
|
|
139
|
+
|
|
140
|
+
const content = {
|
|
141
|
+
app_id: '2220391788200892',
|
|
142
|
+
payload: JSON.stringify({
|
|
143
|
+
tasks: [
|
|
144
|
+
{
|
|
145
|
+
label: '46',
|
|
146
|
+
payload: JSON.stringify(sendPayload),
|
|
147
|
+
queue_name: tid,
|
|
148
|
+
task_id: 400,
|
|
149
|
+
failure_count: null,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: '21',
|
|
153
|
+
payload: JSON.stringify({
|
|
154
|
+
thread_id: tid,
|
|
155
|
+
last_read_watermark_ts: timestamp,
|
|
156
|
+
sync_group: 1,
|
|
157
|
+
}),
|
|
158
|
+
queue_name: tid,
|
|
159
|
+
task_id: 401,
|
|
160
|
+
failure_count: null,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
epoch_id,
|
|
164
|
+
version_id: '24804310205905615',
|
|
165
|
+
data_trace_id: `#${Buffer.from(String(Math.random())).toString('base64').replace(/=+$/, '')}`,
|
|
166
|
+
}),
|
|
167
|
+
request_id,
|
|
168
|
+
type: 3,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return { content, request_id, otid, timestamp };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
175
|
+
/**
|
|
176
|
+
* Lists all available send effects.
|
|
177
|
+
* @returns {Array<{tag, label, emoji, aliases}>}
|
|
178
|
+
*/
|
|
179
|
+
function list() {
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const result = [];
|
|
182
|
+
for (const [alias, { tag, label, emoji }] of Object.entries(EFFECTS)) {
|
|
183
|
+
if (!seen.has(tag)) {
|
|
184
|
+
seen.add(tag);
|
|
185
|
+
result.push({
|
|
186
|
+
tag,
|
|
187
|
+
label,
|
|
188
|
+
emoji,
|
|
189
|
+
aliases: Object.entries(EFFECTS).filter(([, v]) => v.tag === tag).map(([k]) => k),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Sends a message with a visual effect animation.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} effectName Effect alias or tag (e.g. 'fire', 'LOVE', 'confetti')
|
|
200
|
+
* @param {string|object} message Message text or object ({ body, sticker, reply_to_message_id })
|
|
201
|
+
* @param {string|number} threadID Thread / conversation ID
|
|
202
|
+
* @param {Function} [callback] Optional callback(err, { messageID, threadID, effect, timestamp })
|
|
203
|
+
* @returns {Promise}
|
|
204
|
+
*/
|
|
205
|
+
async function send(effectName, message, threadID, callback) {
|
|
206
|
+
let resolveFunc, rejectFunc;
|
|
207
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
208
|
+
resolveFunc = resolve;
|
|
209
|
+
rejectFunc = reject;
|
|
210
|
+
});
|
|
211
|
+
if (!callback) {
|
|
212
|
+
callback = (err, data) => { if (err) return rejectFunc(err); resolveFunc(data); };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
if (!effectName) return callback(new Error('effectName is required.'));
|
|
217
|
+
if (message === undefined || message === null) return callback(new Error('message is required.'));
|
|
218
|
+
if (!threadID) return callback(new Error('threadID is required.'));
|
|
219
|
+
|
|
220
|
+
const effectTag = resolveEffect(effectName);
|
|
221
|
+
utils.log('sendEffect', `Effect "${effectTag}" → thread ${threadID}`);
|
|
222
|
+
|
|
223
|
+
// ── MQTT-first (effects require MQTT; HTTP messaging/send strips them) ──
|
|
224
|
+
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
225
|
+
|
|
226
|
+
if (mqttReady) {
|
|
227
|
+
const { content, request_id, otid, timestamp } = buildMqttPayload(effectTag, message, threadID, ctx);
|
|
228
|
+
|
|
229
|
+
await new Promise((res, rej) => {
|
|
230
|
+
let done = false;
|
|
231
|
+
const timer = setTimeout(() => {
|
|
232
|
+
if (done) return;
|
|
233
|
+
done = true;
|
|
234
|
+
ctx.mqttClient.removeListener('message', onMsg);
|
|
235
|
+
rej(new Error('MQTT effect send timeout'));
|
|
236
|
+
}, 15000);
|
|
237
|
+
|
|
238
|
+
const onMsg = (topic, raw) => {
|
|
239
|
+
if (topic !== '/ls_resp') return;
|
|
240
|
+
let parsed;
|
|
241
|
+
try { parsed = JSON.parse(raw.toString()); parsed.payload = JSON.parse(parsed.payload); } catch { return; }
|
|
242
|
+
if (parsed.request_id !== request_id) return;
|
|
243
|
+
if (done) return;
|
|
244
|
+
done = true;
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
ctx.mqttClient.removeListener('message', onMsg);
|
|
247
|
+
callback(null, { threadID, messageID: otid.toString(), timestamp, effect: effectTag, method: 'mqtt' });
|
|
248
|
+
res();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (typeof ctx.mqttClient.setMaxListeners === 'function') ctx.mqttClient.setMaxListeners(0);
|
|
252
|
+
ctx.mqttClient.on('message', onMsg);
|
|
253
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, (err) => {
|
|
254
|
+
if (err && !done) {
|
|
255
|
+
done = true;
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
ctx.mqttClient.removeListener('message', onMsg);
|
|
258
|
+
rej(err);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
return returnPromise;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── HTTP fallback (last resort — effects may not render on all clients) ─
|
|
266
|
+
utils.warn('sendEffect', 'MQTT not connected, falling back to HTTP (effects may not animate).');
|
|
267
|
+
const { form, messageAndOTID, timestamp: httpTs } = buildForm(effectTag, message, threadID, ctx);
|
|
268
|
+
|
|
269
|
+
const resData = await defaultFuncs
|
|
270
|
+
.post('https://www.facebook.com/messaging/send/', ctx.jar, form, { ...ctx, requestThreadID: String(threadID) })
|
|
271
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
272
|
+
|
|
273
|
+
if (!resData) throw new Error('Empty response from messaging/send');
|
|
274
|
+
if (resData.error) throw new Error(JSON.stringify(resData));
|
|
275
|
+
|
|
276
|
+
const actions = (resData.payload && resData.payload.actions) || [];
|
|
277
|
+
const msgInfo = actions.reduce((p, v) => ({
|
|
278
|
+
threadID: v.thread_fbid || p.threadID,
|
|
279
|
+
messageID: v.message_id || p.messageID,
|
|
280
|
+
timestamp: v.timestamp || p.timestamp,
|
|
281
|
+
}), { threadID, messageID: messageAndOTID, timestamp: httpTs });
|
|
282
|
+
|
|
283
|
+
return callback(null, { ...msgInfo, effect: effectTag, method: 'http' });
|
|
284
|
+
|
|
285
|
+
} catch (err) {
|
|
286
|
+
utils.error('sendEffect', err.message || err);
|
|
287
|
+
callback(err instanceof Error ? err : new Error(String(err.message || err)));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return returnPromise;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Convenience shortcuts ──────────────────────────────────────────────────
|
|
294
|
+
const shortcuts = {};
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
for (const [alias, { tag }] of Object.entries(EFFECTS)) {
|
|
297
|
+
if (!seen.has(tag)) {
|
|
298
|
+
seen.add(tag);
|
|
299
|
+
const _tag = tag;
|
|
300
|
+
shortcuts[alias] = (message, threadID, callback) => send(_tag, message, threadID, callback);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
list,
|
|
306
|
+
send,
|
|
307
|
+
effects: EFFECTS,
|
|
308
|
+
resolveEffect,
|
|
309
|
+
...shortcuts,
|
|
310
|
+
};
|
|
311
|
+
};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
const { globalAntiSuspension } = require('../utils/antiSuspension');
|
|
5
|
+
|
|
6
|
+
const allowedProperties = {
|
|
7
|
+
attachment: true,
|
|
8
|
+
url: true,
|
|
9
|
+
sticker: true,
|
|
10
|
+
emoji: true,
|
|
11
|
+
emojiSize: true,
|
|
12
|
+
body: true,
|
|
13
|
+
mentions: true,
|
|
14
|
+
location: true,
|
|
15
|
+
effect: true,
|
|
16
|
+
replyToMessage: true,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
20
|
+
async function getUrl(url) {
|
|
21
|
+
const resData = await defaultFuncs.post(
|
|
22
|
+
"https://www.facebook.com/message_share_attachment/fromURI/",
|
|
23
|
+
ctx.jar,
|
|
24
|
+
{ image_height: 960, image_width: 960, uri: url }
|
|
25
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
26
|
+
if (!resData || resData.error || !resData.payload) throw new Error("Invalid url");
|
|
27
|
+
return resData.payload.share_data.share_params;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function detectAttachmentType(attachment) {
|
|
31
|
+
const p = attachment.path || '';
|
|
32
|
+
const ext = p.toLowerCase().split('.').pop();
|
|
33
|
+
const audioTypes = ['mp3', 'wav', 'aac', 'm4a', 'ogg', 'opus', 'flac'];
|
|
34
|
+
const videoTypes = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv', 'flv'];
|
|
35
|
+
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
|
|
36
|
+
if (audioTypes.includes(ext)) return { voice_clip: "true" };
|
|
37
|
+
if (videoTypes.includes(ext)) return { video: "true" };
|
|
38
|
+
if (imageTypes.includes(ext)) return { image: "true" };
|
|
39
|
+
return { file: "true" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function uploadSingleAttachment(attachment, threadIDHint) {
|
|
43
|
+
if (!utils.isReadableStream(attachment)) {
|
|
44
|
+
throw new Error("Attachment should be a readable stream and not " + utils.getType(attachment) + ".");
|
|
45
|
+
}
|
|
46
|
+
const uploadType = detectAttachmentType(attachment);
|
|
47
|
+
const oksir = await defaultFuncs.postFormData(
|
|
48
|
+
"https://upload.facebook.com/ajax/mercury/upload.php",
|
|
49
|
+
ctx.jar,
|
|
50
|
+
{ upload_1024: attachment, ...uploadType },
|
|
51
|
+
{},
|
|
52
|
+
{ ...ctx, requestThreadID: threadIDHint }
|
|
53
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
54
|
+
if (oksir.error) throw new Error(JSON.stringify(oksir));
|
|
55
|
+
return oksir.payload.metadata[0];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function uploadAttachment(attachments, threadIDHint) {
|
|
59
|
+
const CONCURRENT_UPLOADS = 2;
|
|
60
|
+
const uploads = [];
|
|
61
|
+
for (let i = 0; i < attachments.length; i += CONCURRENT_UPLOADS) {
|
|
62
|
+
const batch = attachments.slice(i, i + CONCURRENT_UPLOADS);
|
|
63
|
+
const results = await Promise.all(batch.map(a => uploadSingleAttachment(a, threadIDHint)));
|
|
64
|
+
uploads.push(...results);
|
|
65
|
+
if (i + CONCURRENT_UPLOADS < attachments.length) {
|
|
66
|
+
await globalAntiSuspension.addSmartDelay();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return uploads;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getThreadCache() {
|
|
73
|
+
if (!ctx.threadTypeCache) ctx.threadTypeCache = Object.create(null);
|
|
74
|
+
return ctx.threadTypeCache;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function isGroupThread(threadID, explicitIsGroup) {
|
|
78
|
+
if (utils.getType(explicitIsGroup) === "Boolean") return !!explicitIsGroup;
|
|
79
|
+
const tid = threadID.toString();
|
|
80
|
+
const cache = getThreadCache();
|
|
81
|
+
if (Object.prototype.hasOwnProperty.call(cache, tid)) return !!cache[tid];
|
|
82
|
+
try {
|
|
83
|
+
const info = await api.getThreadInfo(tid);
|
|
84
|
+
cache[tid] = !!info.isGroup;
|
|
85
|
+
return !!info.isGroup;
|
|
86
|
+
} catch (_) {
|
|
87
|
+
const fallback = tid.length >= 16;
|
|
88
|
+
cache[tid] = fallback;
|
|
89
|
+
return fallback;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function sendViaHttp(msg, threadID, replyToMessage, isGroup) {
|
|
94
|
+
const isSingleUser = !(await isGroupThread(threadID, isGroup));
|
|
95
|
+
let messageAndOTID = utils.generateOfflineThreadingID();
|
|
96
|
+
let form = {
|
|
97
|
+
client: "mercury",
|
|
98
|
+
action_type: "ma-type:user-generated-message",
|
|
99
|
+
author: "fbid:" + ctx.userID,
|
|
100
|
+
timestamp: Date.now(),
|
|
101
|
+
timestamp_absolute: "Today",
|
|
102
|
+
timestamp_relative: utils.generateTimestampRelative(),
|
|
103
|
+
timestamp_time_passed: "0",
|
|
104
|
+
is_unread: false,
|
|
105
|
+
is_cleared: false,
|
|
106
|
+
is_forward: false,
|
|
107
|
+
is_filtered_content: false,
|
|
108
|
+
is_filtered_content_bh: false,
|
|
109
|
+
is_filtered_content_account: false,
|
|
110
|
+
is_filtered_content_quasar: false,
|
|
111
|
+
is_filtered_content_invalid_app: false,
|
|
112
|
+
is_spoof_warning: false,
|
|
113
|
+
source: "source:chat:web",
|
|
114
|
+
"source_tags[0]": "source:chat",
|
|
115
|
+
...(msg.body && { body: msg.body }),
|
|
116
|
+
html_body: false,
|
|
117
|
+
ui_push_phase: "V3",
|
|
118
|
+
status: "0",
|
|
119
|
+
offline_threading_id: messageAndOTID,
|
|
120
|
+
message_id: messageAndOTID,
|
|
121
|
+
threading_id: utils.generateThreadingID(ctx.clientID),
|
|
122
|
+
"ephemeral_ttl_mode:": "0",
|
|
123
|
+
manual_retry_cnt: "0",
|
|
124
|
+
has_attachment: !!(msg.attachment || msg.url || msg.sticker),
|
|
125
|
+
signatureID: utils.getSignatureID(),
|
|
126
|
+
...(replyToMessage && { replied_to_message_id: replyToMessage })
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (msg.location) {
|
|
130
|
+
if (!msg.location.latitude || !msg.location.longitude) throw new Error("location property needs both latitude and longitude");
|
|
131
|
+
form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
|
|
132
|
+
form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
|
|
133
|
+
form["location_attachment[is_current_location]"] = !!msg.location.current;
|
|
134
|
+
}
|
|
135
|
+
if (msg.sticker) form["sticker_id"] = msg.sticker;
|
|
136
|
+
if (msg.effect) {
|
|
137
|
+
const effectTag = String(msg.effect).toUpperCase().replace(/[\s\-]+/g, '_');
|
|
138
|
+
form.has_lightweight_action = true;
|
|
139
|
+
form['lightweight_action_attached[message_id]'] = messageAndOTID;
|
|
140
|
+
form['lightweight_action_attached[messaging_tag]'] = 'fb.messaging.effects.' + effectTag;
|
|
141
|
+
}
|
|
142
|
+
if (msg.attachment) {
|
|
143
|
+
form.image_ids = [];
|
|
144
|
+
form.gif_ids = [];
|
|
145
|
+
form.file_ids = [];
|
|
146
|
+
form.video_ids = [];
|
|
147
|
+
form.audio_ids = [];
|
|
148
|
+
if (utils.getType(msg.attachment) !== "Array") msg.attachment = [msg.attachment];
|
|
149
|
+
const files = await uploadAttachment(msg.attachment, threadID);
|
|
150
|
+
files.forEach(file => {
|
|
151
|
+
const type = Object.keys(file)[0];
|
|
152
|
+
form["" + type + "s"].push(file[type]);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (msg.url) {
|
|
156
|
+
form["shareable_attachment[share_type]"] = "100";
|
|
157
|
+
const params = await getUrl(msg.url);
|
|
158
|
+
form["shareable_attachment[share_params]"] = params;
|
|
159
|
+
}
|
|
160
|
+
if (msg.emoji) {
|
|
161
|
+
if (!msg.emojiSize) msg.emojiSize = "medium";
|
|
162
|
+
if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") throw new Error("emojiSize property is invalid");
|
|
163
|
+
if (form.body && form.body !== "") throw new Error("body is not empty");
|
|
164
|
+
form.body = msg.emoji;
|
|
165
|
+
form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
|
|
166
|
+
}
|
|
167
|
+
if (msg.mentions) {
|
|
168
|
+
for (let i = 0; i < msg.mentions.length; i++) {
|
|
169
|
+
const mention = msg.mentions[i];
|
|
170
|
+
const tag = mention.tag;
|
|
171
|
+
if (typeof tag !== "string") throw new Error("Mention tags must be strings.");
|
|
172
|
+
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
|
|
173
|
+
if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
|
|
174
|
+
const id = mention.id || 0;
|
|
175
|
+
const emptyChar = '\u200E';
|
|
176
|
+
form["body"] = emptyChar + msg.body;
|
|
177
|
+
form["profile_xmd[" + i + "][offset]"] = offset + 1;
|
|
178
|
+
form["profile_xmd[" + i + "][length]"] = tag.length;
|
|
179
|
+
form["profile_xmd[" + i + "][id]"] = id;
|
|
180
|
+
form["profile_xmd[" + i + "][type]"] = "p";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (utils.getType(threadID) === "Array") {
|
|
185
|
+
for (let i = 0; i < threadID.length; i++) form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
|
|
186
|
+
form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
|
|
187
|
+
form["client_thread_id"] = "root:" + messageAndOTID;
|
|
188
|
+
} else {
|
|
189
|
+
if (isSingleUser) {
|
|
190
|
+
form["specific_to_list[0]"] = "fbid:" + threadID;
|
|
191
|
+
form["specific_to_list[1]"] = "fbid:" + ctx.userID;
|
|
192
|
+
form["other_user_fbid"] = threadID;
|
|
193
|
+
form["client_thread_id"] = "root:" + messageAndOTID;
|
|
194
|
+
} else {
|
|
195
|
+
form["thread_fbid"] = threadID;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (ctx.globalOptions.pageID) {
|
|
199
|
+
form["author"] = "fbid:" + ctx.globalOptions.pageID;
|
|
200
|
+
form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
|
|
201
|
+
form["creator_info[creatorID]"] = ctx.userID;
|
|
202
|
+
form["creator_info[creatorType]"] = "direct_admin";
|
|
203
|
+
form["creator_info[labelType]"] = "sent_message";
|
|
204
|
+
form["creator_info[pageID]"] = ctx.globalOptions.pageID;
|
|
205
|
+
form["request_user_id"] = ctx.globalOptions.pageID;
|
|
206
|
+
form["creator_info[profileURI]"] = "https://www.facebook.com/profile.php?id=" + ctx.userID;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const resData = await defaultFuncs.post(
|
|
210
|
+
"https://www.facebook.com/messaging/send/",
|
|
211
|
+
ctx.jar,
|
|
212
|
+
form,
|
|
213
|
+
{ ...ctx, requestThreadID: threadID }
|
|
214
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
215
|
+
|
|
216
|
+
if (!resData) throw new Error("Send message failed.");
|
|
217
|
+
if (resData.error) {
|
|
218
|
+
if (resData.error === 1545012) utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
|
|
219
|
+
globalAntiSuspension.detectSuspensionSignal(String(resData.error) + ' ' + JSON.stringify(resData));
|
|
220
|
+
throw new Error(JSON.stringify(resData));
|
|
221
|
+
}
|
|
222
|
+
const messageInfo = resData.payload.actions.reduce((p, v) => {
|
|
223
|
+
return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
|
|
224
|
+
}, null);
|
|
225
|
+
return messageInfo;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return async (msg, threadID, callback, replyToMessage, isGroup) => {
|
|
229
|
+
if (!callback && (utils.getType(threadID) === "Function" || utils.getType(threadID) === "AsyncFunction")) {
|
|
230
|
+
throw new Error("Pass a threadID as a second argument.");
|
|
231
|
+
}
|
|
232
|
+
if (!replyToMessage && utils.getType(callback) === "String") {
|
|
233
|
+
replyToMessage = callback;
|
|
234
|
+
callback = undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let resolveFunc = () => {};
|
|
238
|
+
let rejectFunc = () => {};
|
|
239
|
+
let returnPromise = new Promise((resolve, reject) => {
|
|
240
|
+
resolveFunc = resolve;
|
|
241
|
+
rejectFunc = reject;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!callback) {
|
|
245
|
+
callback = (err, data) => {
|
|
246
|
+
if (err) return rejectFunc(err);
|
|
247
|
+
resolveFunc(data);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let msgType = utils.getType(msg);
|
|
252
|
+
let threadIDType = utils.getType(threadID);
|
|
253
|
+
let messageIDType = utils.getType(replyToMessage);
|
|
254
|
+
|
|
255
|
+
if (msgType !== "String" && msgType !== "Object") {
|
|
256
|
+
return callback(new Error("Message should be of type string or object and not " + msgType + "."));
|
|
257
|
+
}
|
|
258
|
+
if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") {
|
|
259
|
+
return callback(new Error("ThreadID should be of type number, string, or array and not " + threadIDType + "."));
|
|
260
|
+
}
|
|
261
|
+
if (replyToMessage && messageIDType !== 'String') {
|
|
262
|
+
return callback(new Error("MessageID should be of type string and not " + threadIDType + "."));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (ctx.validator && !ctx.validator.isValidMessage(msg)) {
|
|
266
|
+
return callback(new Error("Invalid message content"));
|
|
267
|
+
}
|
|
268
|
+
const threadIDs = Array.isArray(threadID) ? threadID : [threadID];
|
|
269
|
+
if (ctx.validator && !ctx.validator.validateIDArray(threadIDs, ctx.validator.isValidThreadID)) {
|
|
270
|
+
return callback(new Error("Invalid thread ID(s)"));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (msgType === "String") msg = { body: msg };
|
|
274
|
+
|
|
275
|
+
let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
|
|
276
|
+
if (disallowedProperties.length > 0) {
|
|
277
|
+
return callback(new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`"));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
await globalAntiSuspension.prepareBeforeMessage(String(Array.isArray(threadID) ? threadID[0] : threadID), msg.body || '');
|
|
282
|
+
|
|
283
|
+
let typingStarted = false;
|
|
284
|
+
let typingTimeout;
|
|
285
|
+
const shouldSimulateTyping = ctx.globalOptions && ctx.globalOptions.simulateTyping && api.sendTypingIndicator && ctx.mqttClient && ctx.mqttClient.connected;
|
|
286
|
+
if (shouldSimulateTyping) {
|
|
287
|
+
try {
|
|
288
|
+
await api.sendTypingIndicator(true, threadID);
|
|
289
|
+
typingStarted = true;
|
|
290
|
+
const msgLen = (msg.body || '').length;
|
|
291
|
+
const typingMs = await globalAntiSuspension.simulateTyping(threadID, msgLen);
|
|
292
|
+
await new Promise(resolve => setTimeout(resolve, typingMs));
|
|
293
|
+
typingTimeout = setTimeout(() => {
|
|
294
|
+
if (typingStarted) {
|
|
295
|
+
try { api.sendTypingIndicator(false, threadID); } catch (_) {}
|
|
296
|
+
typingStarted = false;
|
|
297
|
+
}
|
|
298
|
+
}, 10000);
|
|
299
|
+
} catch (_) {}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
let result;
|
|
304
|
+
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
305
|
+
const isMultiRecipient = Array.isArray(threadID);
|
|
306
|
+
|
|
307
|
+
if (mqttReady && !isMultiRecipient && api.sendMessageMqtt) {
|
|
308
|
+
result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
309
|
+
} else {
|
|
310
|
+
result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
311
|
+
}
|
|
312
|
+
callback(null, result);
|
|
313
|
+
} catch (sendErr) {
|
|
314
|
+
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
315
|
+
if (mqttReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
|
|
316
|
+
try {
|
|
317
|
+
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
318
|
+
callback(null, mqttRes);
|
|
319
|
+
} catch (_mqttErr) {
|
|
320
|
+
callback(sendErr);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
try {
|
|
324
|
+
const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
325
|
+
callback(null, httpRes);
|
|
326
|
+
} catch (_httpErr) {
|
|
327
|
+
callback(sendErr);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} finally {
|
|
331
|
+
if (typingTimeout) clearTimeout(typingTimeout);
|
|
332
|
+
if (typingStarted) {
|
|
333
|
+
try { await api.sendTypingIndicator(false, threadID); } catch (_) {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
callback(err);
|
|
338
|
+
}
|
|
339
|
+
return returnPromise;
|
|
340
|
+
};
|
|
341
|
+
};
|