@lazyneoaz/nkxchat 1.0.2 → 1.0.4
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/package.json +2 -1
- package/src/apis/listenMqtt.js +88 -1
- package/src/apis/scheduler.js +129 -0
- package/src/apis/sendMessage.js +147 -159
- package/src/apis/sendMessageMqtt.js +190 -174
- package/src/apis/sendTypingIndicator.js +68 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/nkxchat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"types": "src/types/index.d.ts",
|
|
6
6
|
"description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"nkxchat"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@dongdev/fca-unofficial": "^4.0.3",
|
|
42
43
|
"axios": "^1.13.5",
|
|
43
44
|
"axios-cookiejar-support": "^4.0.7",
|
|
44
45
|
"bluebird": "^3.7.2",
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -593,9 +593,72 @@ function mqttConf(ctx, overrides) {
|
|
|
593
593
|
return ctx._mqttOpt;
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
+
function createMiddlewareSystem() {
|
|
597
|
+
const stack = [];
|
|
598
|
+
let nextId = 0;
|
|
599
|
+
function use(nameOrFn, fn) {
|
|
600
|
+
let name, middlewareFn;
|
|
601
|
+
if (typeof nameOrFn === "string" && typeof fn === "function") {
|
|
602
|
+
name = nameOrFn; middlewareFn = fn;
|
|
603
|
+
} else if (typeof nameOrFn === "function") {
|
|
604
|
+
middlewareFn = nameOrFn; name = `middleware_${nextId++}`;
|
|
605
|
+
} else throw new Error("Middleware must be a function or (name, function)");
|
|
606
|
+
const entry = { name, fn: middlewareFn, enabled: true };
|
|
607
|
+
stack.push(entry);
|
|
608
|
+
return function remove() {
|
|
609
|
+
const i = stack.indexOf(entry);
|
|
610
|
+
if (i !== -1) stack.splice(i, 1);
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function remove(identifier) {
|
|
614
|
+
const i = typeof identifier === "string"
|
|
615
|
+
? stack.findIndex(e => e.name === identifier)
|
|
616
|
+
: stack.findIndex(e => e.fn === identifier);
|
|
617
|
+
if (i !== -1) { stack.splice(i, 1); return true; }
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
function clear() { stack.length = 0; }
|
|
621
|
+
function list() { return stack.filter(e => e.enabled).map(e => e.name); }
|
|
622
|
+
function setEnabled(name, enabled) {
|
|
623
|
+
const e = stack.find(e => e.name === name);
|
|
624
|
+
if (e) { e.enabled = enabled; return true; }
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
function process(event, finalCallback) {
|
|
628
|
+
const active = stack.filter(e => e.enabled);
|
|
629
|
+
if (!active.length) return finalCallback(null, event);
|
|
630
|
+
let idx = 0;
|
|
631
|
+
function next(err) {
|
|
632
|
+
if (err && err !== false && err !== null) return finalCallback(err, null);
|
|
633
|
+
if (err === false || err === null) return finalCallback(null, null);
|
|
634
|
+
if (idx >= active.length) return finalCallback(null, event);
|
|
635
|
+
const mw = active[idx++];
|
|
636
|
+
try {
|
|
637
|
+
const r = mw.fn(event, next);
|
|
638
|
+
if (r && typeof r.then === "function") r.then(() => next()).catch(e => next(e));
|
|
639
|
+
else if (r === false || r === null) finalCallback(null, null);
|
|
640
|
+
} catch (e) { next(e); }
|
|
641
|
+
}
|
|
642
|
+
next();
|
|
643
|
+
}
|
|
644
|
+
function wrapCallback(callback) {
|
|
645
|
+
return function(err, event) {
|
|
646
|
+
if (err) return callback(err, null);
|
|
647
|
+
if (!event) return callback(null, null);
|
|
648
|
+
process(event, (mwErr, processed) => {
|
|
649
|
+
if (mwErr) return callback(mwErr, null);
|
|
650
|
+
if (processed === null) return;
|
|
651
|
+
callback(null, processed);
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
return { use, remove, clear, list, setEnabled, process, wrapCallback, get count() { return stack.filter(e => e.enabled).length; } };
|
|
656
|
+
}
|
|
657
|
+
|
|
596
658
|
module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
597
659
|
const identity = () => {};
|
|
598
660
|
let globalCallback = identity;
|
|
661
|
+
if (!ctx._middleware) ctx._middleware = createMiddlewareSystem();
|
|
599
662
|
|
|
600
663
|
function emitAuthError(reason, detail) {
|
|
601
664
|
try { if (ctx._autoCycleTimer) clearTimeout(ctx._autoCycleTimer); } catch (_) { }
|
|
@@ -930,7 +993,7 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
930
993
|
|
|
931
994
|
const msgEmitter = new MessageEmitter();
|
|
932
995
|
|
|
933
|
-
|
|
996
|
+
const baseCallback = callback || function(error, message) {
|
|
934
997
|
if (error) {
|
|
935
998
|
utils.error("MQTT", "Emit error");
|
|
936
999
|
return msgEmitter.emit("error", error);
|
|
@@ -941,6 +1004,12 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
941
1004
|
msgEmitter.emit("message", message);
|
|
942
1005
|
};
|
|
943
1006
|
|
|
1007
|
+
globalCallback = ctx._middleware && ctx._middleware.count
|
|
1008
|
+
? ctx._middleware.wrapCallback(baseCallback)
|
|
1009
|
+
: baseCallback;
|
|
1010
|
+
|
|
1011
|
+
ctx._emitter = msgEmitter;
|
|
1012
|
+
|
|
944
1013
|
ctx._listeningActive = true;
|
|
945
1014
|
ctx._lastListenCallback = callback || null;
|
|
946
1015
|
|
|
@@ -990,6 +1059,24 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
990
1059
|
|
|
991
1060
|
api.stopListening = msgEmitter.stopListening;
|
|
992
1061
|
api.stopListeningAsync = msgEmitter.stopListeningAsync;
|
|
1062
|
+
|
|
1063
|
+
api.useMiddleware = function(nameOrFn, fn) {
|
|
1064
|
+
const remove = ctx._middleware.use(nameOrFn, fn);
|
|
1065
|
+
globalCallback = ctx._middleware.wrapCallback(baseCallback || identity);
|
|
1066
|
+
return remove;
|
|
1067
|
+
};
|
|
1068
|
+
api.removeMiddleware = function(identifier) {
|
|
1069
|
+
const ok = ctx._middleware.remove(identifier);
|
|
1070
|
+
if (!ctx._middleware.count) globalCallback = baseCallback || identity;
|
|
1071
|
+
return ok;
|
|
1072
|
+
};
|
|
1073
|
+
api.clearMiddleware = function() {
|
|
1074
|
+
ctx._middleware.clear();
|
|
1075
|
+
globalCallback = baseCallback || identity;
|
|
1076
|
+
};
|
|
1077
|
+
api.listMiddleware = function() { return ctx._middleware.list(); };
|
|
1078
|
+
api.setMiddlewareEnabled = function(name, enabled) { return ctx._middleware.setEnabled(name, enabled); };
|
|
1079
|
+
|
|
993
1080
|
return msgEmitter;
|
|
994
1081
|
};
|
|
995
1082
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
4
|
+
const scheduledMessages = new Map();
|
|
5
|
+
let nextId = 1;
|
|
6
|
+
|
|
7
|
+
function toTimestamp(when) {
|
|
8
|
+
if (when instanceof Date) return when.getTime();
|
|
9
|
+
if (typeof when === "number") return when;
|
|
10
|
+
if (typeof when === "string") return new Date(when).getTime();
|
|
11
|
+
return NaN;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function scheduleMessage(message, threadID, when, options) {
|
|
15
|
+
options = options || {};
|
|
16
|
+
const timestamp = toTimestamp(when);
|
|
17
|
+
if (isNaN(timestamp)) throw new Error("Invalid 'when'. Must be Date, number (ms timestamp), or ISO string.");
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (timestamp <= now) throw new Error("Scheduled time must be in the future.");
|
|
20
|
+
|
|
21
|
+
const id = `scheduled_${nextId++}_${now}`;
|
|
22
|
+
const delay = timestamp - now;
|
|
23
|
+
|
|
24
|
+
const scheduled = {
|
|
25
|
+
id,
|
|
26
|
+
message,
|
|
27
|
+
threadID,
|
|
28
|
+
timestamp,
|
|
29
|
+
createdAt: now,
|
|
30
|
+
options: {
|
|
31
|
+
replyMessageID: options.replyMessageID || null,
|
|
32
|
+
callback: options.callback || null,
|
|
33
|
+
},
|
|
34
|
+
cancelled: false,
|
|
35
|
+
timeout: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
scheduled.timeout = setTimeout(() => {
|
|
39
|
+
if (scheduled.cancelled) return;
|
|
40
|
+
const sendFn = api.sendMessage || api.sendMessageMqtt;
|
|
41
|
+
if (!sendFn) return;
|
|
42
|
+
Promise.resolve(
|
|
43
|
+
sendFn(message, threadID, scheduled.options.callback || (() => {}), scheduled.options.replyMessageID)
|
|
44
|
+
).then(() => {
|
|
45
|
+
scheduledMessages.delete(id);
|
|
46
|
+
}).catch(() => {
|
|
47
|
+
scheduledMessages.delete(id);
|
|
48
|
+
});
|
|
49
|
+
}, delay);
|
|
50
|
+
|
|
51
|
+
scheduledMessages.set(id, scheduled);
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function cancelScheduledMessage(id) {
|
|
56
|
+
const s = scheduledMessages.get(id);
|
|
57
|
+
if (!s || s.cancelled) return false;
|
|
58
|
+
clearTimeout(s.timeout);
|
|
59
|
+
s.cancelled = true;
|
|
60
|
+
scheduledMessages.delete(id);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getScheduledMessage(id) {
|
|
65
|
+
const s = scheduledMessages.get(id);
|
|
66
|
+
if (!s || s.cancelled) return null;
|
|
67
|
+
return {
|
|
68
|
+
id: s.id,
|
|
69
|
+
message: s.message,
|
|
70
|
+
threadID: s.threadID,
|
|
71
|
+
timestamp: s.timestamp,
|
|
72
|
+
createdAt: s.createdAt,
|
|
73
|
+
options: { ...s.options },
|
|
74
|
+
timeUntilSend: s.timestamp - Date.now(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function listScheduledMessages() {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
return Array.from(scheduledMessages.values())
|
|
81
|
+
.filter(s => !s.cancelled)
|
|
82
|
+
.map(s => ({
|
|
83
|
+
id: s.id,
|
|
84
|
+
message: s.message,
|
|
85
|
+
threadID: s.threadID,
|
|
86
|
+
timestamp: s.timestamp,
|
|
87
|
+
createdAt: s.createdAt,
|
|
88
|
+
options: { ...s.options },
|
|
89
|
+
timeUntilSend: s.timestamp - now,
|
|
90
|
+
}))
|
|
91
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cancelAllScheduledMessages() {
|
|
95
|
+
let count = 0;
|
|
96
|
+
for (const id of Array.from(scheduledMessages.keys())) {
|
|
97
|
+
if (cancelScheduledMessage(id)) count++;
|
|
98
|
+
}
|
|
99
|
+
return count;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getScheduledCount() {
|
|
103
|
+
return scheduledMessages.size;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cleanupInterval = setInterval(() => {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
for (const [id, s] of scheduledMessages.entries()) {
|
|
109
|
+
if (s.cancelled || s.timestamp < now) scheduledMessages.delete(id);
|
|
110
|
+
}
|
|
111
|
+
}, 5 * 60 * 1000);
|
|
112
|
+
|
|
113
|
+
function destroy() {
|
|
114
|
+
clearInterval(cleanupInterval);
|
|
115
|
+
return cancelAllScheduledMessages();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ctx._scheduler = { destroy };
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
scheduleMessage,
|
|
122
|
+
cancelScheduledMessage,
|
|
123
|
+
getScheduledMessage,
|
|
124
|
+
listScheduledMessages,
|
|
125
|
+
cancelAllScheduledMessages,
|
|
126
|
+
getScheduledCount,
|
|
127
|
+
destroy,
|
|
128
|
+
};
|
|
129
|
+
};
|
package/src/apis/sendMessage.js
CHANGED
|
@@ -13,38 +13,26 @@ const allowedProperties = {
|
|
|
13
13
|
mentions: true,
|
|
14
14
|
location: true,
|
|
15
15
|
effect: true,
|
|
16
|
+
replyToMessage: true,
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
module.exports = (defaultFuncs, api, ctx) => {
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
const cache = getThreadCache();
|
|
28
|
-
if (Object.prototype.hasOwnProperty.call(cache, tid)) return !!cache[tid];
|
|
29
|
-
try {
|
|
30
|
-
const info = await api.getThreadInfo(tid);
|
|
31
|
-
cache[tid] = !!info.isGroup;
|
|
32
|
-
return !!info.isGroup;
|
|
33
|
-
} catch (_) {
|
|
34
|
-
const fallback = tid.length >= 16;
|
|
35
|
-
cache[tid] = fallback;
|
|
36
|
-
return fallback;
|
|
37
|
-
}
|
|
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;
|
|
38
28
|
}
|
|
39
29
|
|
|
40
30
|
function detectAttachmentType(attachment) {
|
|
41
|
-
const
|
|
42
|
-
const ext =
|
|
43
|
-
|
|
31
|
+
const p = attachment.path || '';
|
|
32
|
+
const ext = p.toLowerCase().split('.').pop();
|
|
44
33
|
const audioTypes = ['mp3', 'wav', 'aac', 'm4a', 'ogg', 'opus', 'flac'];
|
|
45
34
|
const videoTypes = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv', 'flv'];
|
|
46
35
|
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
|
|
47
|
-
|
|
48
36
|
if (audioTypes.includes(ext)) return { voice_clip: "true" };
|
|
49
37
|
if (videoTypes.includes(ext)) return { video: "true" };
|
|
50
38
|
if (imageTypes.includes(ext)) return { image: "true" };
|
|
@@ -63,7 +51,6 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
63
51
|
{},
|
|
64
52
|
{ ...ctx, requestThreadID: threadIDHint }
|
|
65
53
|
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
66
|
-
|
|
67
54
|
if (oksir.error) throw new Error(JSON.stringify(oksir));
|
|
68
55
|
return oksir.payload.metadata[0];
|
|
69
56
|
}
|
|
@@ -82,24 +69,122 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
82
69
|
return uploads;
|
|
83
70
|
}
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
ctx.jar,
|
|
89
|
-
{ image_height: 960, image_width: 960, uri: url }
|
|
90
|
-
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
91
|
-
if (!resData || resData.error || !resData.payload) throw new Error("Invalid url");
|
|
92
|
-
return resData.payload.share_data.share_params;
|
|
72
|
+
function getThreadCache() {
|
|
73
|
+
if (!ctx.threadTypeCache) ctx.threadTypeCache = Object.create(null);
|
|
74
|
+
return ctx.threadTypeCache;
|
|
93
75
|
}
|
|
94
76
|
|
|
95
|
-
async function
|
|
96
|
-
if (utils.getType(
|
|
97
|
-
|
|
98
|
-
|
|
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";
|
|
99
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];
|
|
100
186
|
form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
|
|
101
187
|
form["client_thread_id"] = "root:" + messageAndOTID;
|
|
102
|
-
utils.log("sendMessage", "Sending message to multiple users: " + threadID);
|
|
103
188
|
} else {
|
|
104
189
|
if (isSingleUser) {
|
|
105
190
|
form["specific_to_list[0]"] = "fbid:" + threadID;
|
|
@@ -110,7 +195,6 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
110
195
|
form["thread_fbid"] = threadID;
|
|
111
196
|
}
|
|
112
197
|
}
|
|
113
|
-
|
|
114
198
|
if (ctx.globalOptions.pageID) {
|
|
115
199
|
form["author"] = "fbid:" + ctx.globalOptions.pageID;
|
|
116
200
|
form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
|
|
@@ -131,14 +215,10 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
131
215
|
|
|
132
216
|
if (!resData) throw new Error("Send message failed.");
|
|
133
217
|
if (resData.error) {
|
|
134
|
-
if (resData.error === 1545012)
|
|
135
|
-
utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
|
|
136
|
-
}
|
|
137
|
-
// Check for suspension signals in error
|
|
218
|
+
if (resData.error === 1545012) utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
|
|
138
219
|
globalAntiSuspension.detectSuspensionSignal(String(resData.error) + ' ' + JSON.stringify(resData));
|
|
139
220
|
throw new Error(JSON.stringify(resData));
|
|
140
221
|
}
|
|
141
|
-
|
|
142
222
|
const messageInfo = resData.payload.actions.reduce((p, v) => {
|
|
143
223
|
return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
|
|
144
224
|
}, null);
|
|
@@ -182,11 +262,11 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
182
262
|
return callback(new Error("MessageID should be of type string and not " + threadIDType + "."));
|
|
183
263
|
}
|
|
184
264
|
|
|
185
|
-
if (!ctx.validator.isValidMessage(msg)) {
|
|
265
|
+
if (ctx.validator && !ctx.validator.isValidMessage(msg)) {
|
|
186
266
|
return callback(new Error("Invalid message content"));
|
|
187
267
|
}
|
|
188
268
|
const threadIDs = Array.isArray(threadID) ? threadID : [threadID];
|
|
189
|
-
if (!ctx.validator.validateIDArray(threadIDs, ctx.validator.isValidThreadID)) {
|
|
269
|
+
if (ctx.validator && !ctx.validator.validateIDArray(threadIDs, ctx.validator.isValidThreadID)) {
|
|
190
270
|
return callback(new Error("Invalid thread ID(s)"));
|
|
191
271
|
}
|
|
192
272
|
|
|
@@ -198,122 +278,18 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
198
278
|
}
|
|
199
279
|
|
|
200
280
|
try {
|
|
201
|
-
|
|
202
|
-
let form = {
|
|
203
|
-
client: "mercury",
|
|
204
|
-
action_type: "ma-type:user-generated-message",
|
|
205
|
-
author: "fbid:" + ctx.userID,
|
|
206
|
-
timestamp: Date.now(),
|
|
207
|
-
timestamp_absolute: "Today",
|
|
208
|
-
timestamp_relative: utils.generateTimestampRelative(),
|
|
209
|
-
timestamp_time_passed: "0",
|
|
210
|
-
is_unread: false,
|
|
211
|
-
is_cleared: false,
|
|
212
|
-
is_forward: false,
|
|
213
|
-
is_filtered_content: false,
|
|
214
|
-
is_filtered_content_bh: false,
|
|
215
|
-
is_filtered_content_account: false,
|
|
216
|
-
is_filtered_content_quasar: false,
|
|
217
|
-
is_filtered_content_invalid_app: false,
|
|
218
|
-
is_spoof_warning: false,
|
|
219
|
-
source: "source:chat:web",
|
|
220
|
-
"source_tags[0]": "source:chat",
|
|
221
|
-
...(msg.body && { body: msg.body }),
|
|
222
|
-
html_body: false,
|
|
223
|
-
ui_push_phase: "V3",
|
|
224
|
-
status: "0",
|
|
225
|
-
offline_threading_id: messageAndOTID,
|
|
226
|
-
message_id: messageAndOTID,
|
|
227
|
-
threading_id: utils.generateThreadingID(ctx.clientID),
|
|
228
|
-
"ephemeral_ttl_mode:": "0",
|
|
229
|
-
manual_retry_cnt: "0",
|
|
230
|
-
has_attachment: !!(msg.attachment || msg.url || msg.sticker),
|
|
231
|
-
signatureID: utils.getSignatureID(),
|
|
232
|
-
...(replyToMessage && { replied_to_message_id: replyToMessage })
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
if (msg.location) {
|
|
236
|
-
if (!msg.location.latitude || !msg.location.longitude) {
|
|
237
|
-
return callback(new Error("location property needs both latitude and longitude"));
|
|
238
|
-
}
|
|
239
|
-
form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
|
|
240
|
-
form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
|
|
241
|
-
form["location_attachment[is_current_location]"] = !!msg.location.current;
|
|
242
|
-
}
|
|
243
|
-
if (msg.sticker) form["sticker_id"] = msg.sticker;
|
|
244
|
-
if (msg.effect) {
|
|
245
|
-
const effectTag = String(msg.effect).toUpperCase().replace(/[\s\-]+/g, '_');
|
|
246
|
-
const messagingTag = 'fb.messaging.effects.' + effectTag;
|
|
247
|
-
form.has_lightweight_action = true;
|
|
248
|
-
form['lightweight_action_attached[message_id]'] = messageAndOTID;
|
|
249
|
-
form['lightweight_action_attached[messaging_tag]'] = messagingTag;
|
|
250
|
-
}
|
|
251
|
-
if (msg.attachment) {
|
|
252
|
-
form.image_ids = [];
|
|
253
|
-
form.gif_ids = [];
|
|
254
|
-
form.file_ids = [];
|
|
255
|
-
form.video_ids = [];
|
|
256
|
-
form.audio_ids = [];
|
|
257
|
-
if (utils.getType(msg.attachment) !== "Array") msg.attachment = [msg.attachment];
|
|
258
|
-
const files = await uploadAttachment(msg.attachment, threadID);
|
|
259
|
-
files.forEach(file => {
|
|
260
|
-
const type = Object.keys(file)[0];
|
|
261
|
-
form["" + type + "s"].push(file[type]);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
if (msg.url) {
|
|
265
|
-
form["shareable_attachment[share_type]"] = "100";
|
|
266
|
-
const params = await getUrl(msg.url);
|
|
267
|
-
form["shareable_attachment[share_params]"] = params;
|
|
268
|
-
}
|
|
269
|
-
if (msg.emoji) {
|
|
270
|
-
if (!msg.emojiSize) msg.emojiSize = "medium";
|
|
271
|
-
if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
|
|
272
|
-
return callback(new Error("emojiSize property is invalid"));
|
|
273
|
-
}
|
|
274
|
-
if (form.body && form.body !== "") return callback(new Error("body is not empty"));
|
|
275
|
-
form.body = msg.emoji;
|
|
276
|
-
form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
|
|
277
|
-
}
|
|
278
|
-
if (msg.mentions) {
|
|
279
|
-
for (let i = 0; i < msg.mentions.length; i++) {
|
|
280
|
-
const mention = msg.mentions[i];
|
|
281
|
-
const tag = mention.tag;
|
|
282
|
-
if (typeof tag !== "string") return callback(new Error("Mention tags must be strings."));
|
|
283
|
-
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
|
|
284
|
-
if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
|
|
285
|
-
if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
|
|
286
|
-
const id = mention.id || 0;
|
|
287
|
-
const emptyChar = '\u200E';
|
|
288
|
-
form["body"] = emptyChar + msg.body;
|
|
289
|
-
form["profile_xmd[" + i + "][offset]"] = offset + 1;
|
|
290
|
-
form["profile_xmd[" + i + "][length]"] = tag.length;
|
|
291
|
-
form["profile_xmd[" + i + "][id]"] = id;
|
|
292
|
-
form["profile_xmd[" + i + "][type]"] = "p";
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const isSingleUser = !(await isGroupThread(threadID, isGroup));
|
|
281
|
+
await globalAntiSuspension.prepareBeforeMessage(String(Array.isArray(threadID) ? threadID[0] : threadID), msg.body || '');
|
|
297
282
|
|
|
298
|
-
// ── Optimised anti-suspension send flow ───────────────────────────────
|
|
299
|
-
// Step 1: enforce thread throttle (single delay — no stacking).
|
|
300
|
-
await globalAntiSuspension.prepareBeforeMessage(threadID, msg.body || '');
|
|
301
|
-
|
|
302
|
-
// Step 2: start typing indicator BEFORE the typing delay so the delay
|
|
303
|
-
// is "hidden" inside the visible typing indicator — zero extra latency.
|
|
304
283
|
let typingStarted = false;
|
|
305
284
|
let typingTimeout;
|
|
306
|
-
const shouldSimulateTyping = ctx.globalOptions && ctx.globalOptions.simulateTyping && api.sendTypingIndicator;
|
|
285
|
+
const shouldSimulateTyping = ctx.globalOptions && ctx.globalOptions.simulateTyping && api.sendTypingIndicator && ctx.mqttClient && ctx.mqttClient.connected;
|
|
307
286
|
if (shouldSimulateTyping) {
|
|
308
287
|
try {
|
|
309
288
|
await api.sendTypingIndicator(true, threadID);
|
|
310
289
|
typingStarted = true;
|
|
311
|
-
|
|
312
|
-
// Typing delay runs while the indicator is already showing.
|
|
313
290
|
const msgLen = (msg.body || '').length;
|
|
314
291
|
const typingMs = await globalAntiSuspension.simulateTyping(threadID, msgLen);
|
|
315
292
|
await new Promise(resolve => setTimeout(resolve, typingMs));
|
|
316
|
-
|
|
317
293
|
typingTimeout = setTimeout(() => {
|
|
318
294
|
if (typingStarted) {
|
|
319
295
|
try { api.sendTypingIndicator(false, threadID); } catch (_) {}
|
|
@@ -323,21 +299,33 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
323
299
|
} catch (_) {}
|
|
324
300
|
}
|
|
325
301
|
|
|
326
|
-
// Step 3: send.
|
|
327
302
|
try {
|
|
328
|
-
|
|
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
|
+
}
|
|
329
312
|
callback(null, result);
|
|
330
|
-
} catch (
|
|
331
|
-
|
|
332
|
-
if (api.sendMessageMqtt) {
|
|
313
|
+
} catch (sendErr) {
|
|
314
|
+
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
315
|
+
if (mqttReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
|
|
333
316
|
try {
|
|
334
317
|
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
335
318
|
callback(null, mqttRes);
|
|
336
|
-
} catch (
|
|
337
|
-
callback(
|
|
319
|
+
} catch (_mqttErr) {
|
|
320
|
+
callback(sendErr);
|
|
338
321
|
}
|
|
339
322
|
} else {
|
|
340
|
-
|
|
323
|
+
try {
|
|
324
|
+
const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
325
|
+
callback(null, httpRes);
|
|
326
|
+
} catch (_httpErr) {
|
|
327
|
+
callback(sendErr);
|
|
328
|
+
}
|
|
341
329
|
}
|
|
342
330
|
} finally {
|
|
343
331
|
if (typingTimeout) clearTimeout(typingTimeout);
|
|
@@ -16,240 +16,256 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
16
16
|
return { file: "true" };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
async function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!utils.isReadableStream(attachments[i])) {
|
|
25
|
-
throw { error: "Attachment should be a readable stream and not " + utils.getType(attachments[i]) + "." };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (i > 0) {
|
|
29
|
-
await globalAntiSuspension.addSmartDelay();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
var form = {
|
|
33
|
-
upload_1024: attachments[i],
|
|
34
|
-
...detectAttachmentType(attachments[i]),
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const upload = await defaultFuncs
|
|
38
|
-
.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, { ...ctx, requestThreadID: String(ctx._lastThreadHint || "") })
|
|
39
|
-
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
40
|
-
.then(resData => {
|
|
41
|
-
if (resData.error) throw resData;
|
|
42
|
-
return resData.payload.metadata[0];
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
uploads.push(upload);
|
|
19
|
+
async function uploadAttachments(attachments) {
|
|
20
|
+
const uploads = [];
|
|
21
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
22
|
+
if (!utils.isReadableStream(attachments[i])) {
|
|
23
|
+
throw new Error("Attachment should be a readable stream and not " + utils.getType(attachments[i]) + ".");
|
|
46
24
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
if (i > 0) {
|
|
26
|
+
await globalAntiSuspension.addSmartDelay();
|
|
27
|
+
}
|
|
28
|
+
const form = {
|
|
29
|
+
upload_1024: attachments[i],
|
|
30
|
+
...detectAttachmentType(attachments[i]),
|
|
31
|
+
};
|
|
32
|
+
const upload = await defaultFuncs
|
|
33
|
+
.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, { ...ctx, requestThreadID: String(ctx._lastThreadHint || "") })
|
|
34
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
35
|
+
.then(resData => {
|
|
36
|
+
if (resData.error) throw resData;
|
|
37
|
+
return resData.payload.metadata[0];
|
|
38
|
+
});
|
|
39
|
+
uploads.push(upload);
|
|
51
40
|
}
|
|
41
|
+
return uploads;
|
|
52
42
|
}
|
|
53
43
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (typeof msg === 'object') {
|
|
69
|
-
if (msg.sticker) {
|
|
70
|
-
payload.send_type = 2;
|
|
71
|
-
payload.sticker_id = msg.sticker;
|
|
72
|
-
payload.text = null;
|
|
73
|
-
}
|
|
74
|
-
if (msg.attachment) {
|
|
75
|
-
payload.send_type = 3;
|
|
76
|
-
payload.attachment_fbids = Array.isArray(msg.attachment) ? msg.attachment : [msg.attachment];
|
|
77
|
-
}
|
|
78
|
-
if (msg.effect) {
|
|
79
|
-
const effectTag = String(msg.effect).toUpperCase().replace(/[\s\-]+/g, '_');
|
|
80
|
-
payload.lightweight_action_attached = {
|
|
81
|
-
messaging_tag: 'fb.messaging.effects.' + effectTag,
|
|
82
|
-
};
|
|
44
|
+
function buildMentionData(msg, baseBody) {
|
|
45
|
+
if (!Array.isArray(msg.mentions) || msg.mentions.length === 0) return null;
|
|
46
|
+
const ids = [], offsets = [], lengths = [], types = [];
|
|
47
|
+
let cursor = 0;
|
|
48
|
+
for (const mention of msg.mentions) {
|
|
49
|
+
const rawTag = String(mention.tag || "");
|
|
50
|
+
const displayName = rawTag.replace(/^@+/, "");
|
|
51
|
+
const start = Number.isInteger(mention.fromIndex) ? mention.fromIndex : cursor;
|
|
52
|
+
let index = baseBody.indexOf(rawTag, start);
|
|
53
|
+
let adjustment = 0;
|
|
54
|
+
if (index === -1) {
|
|
55
|
+
index = baseBody.indexOf(displayName, start);
|
|
56
|
+
} else {
|
|
57
|
+
adjustment = rawTag.length - displayName.length;
|
|
83
58
|
}
|
|
59
|
+
if (index < 0) { index = 0; adjustment = 0; }
|
|
60
|
+
const offset = index + adjustment;
|
|
61
|
+
ids.push(String(mention.id || 0));
|
|
62
|
+
offsets.push(offset);
|
|
63
|
+
lengths.push(displayName.length);
|
|
64
|
+
types.push("p");
|
|
65
|
+
cursor = offset + displayName.length;
|
|
84
66
|
}
|
|
85
|
-
return
|
|
67
|
+
return {
|
|
68
|
+
mention_ids: ids.join(","),
|
|
69
|
+
mention_offsets: offsets.join(","),
|
|
70
|
+
mention_lengths: lengths.join(","),
|
|
71
|
+
mention_types: types.join(","),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasLinks(text) {
|
|
76
|
+
return /(https?:\/\/|www\.|t\.me\/|fb\.me\/|youtu\.be\/|facebook\.com\/|youtube\.com\/)/i.test(text);
|
|
86
77
|
}
|
|
87
78
|
|
|
88
79
|
function extractIdsFromPayload(payload) {
|
|
89
80
|
let messageID = null;
|
|
90
81
|
let threadID = null;
|
|
91
82
|
function walk(n) {
|
|
92
|
-
if (Array.isArray(n))
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
for (const x of n) walk(x);
|
|
83
|
+
if (!Array.isArray(n)) return;
|
|
84
|
+
if (n[0] === 5 && (n[1] === "replaceOptimsiticMessage" || n[1] === "replaceOptimisticMessage")) {
|
|
85
|
+
messageID = String(n[3]);
|
|
86
|
+
}
|
|
87
|
+
if (n[0] === 5 && n[1] === "writeCTAIdToThreadsTable") {
|
|
88
|
+
const a = n[2];
|
|
89
|
+
if (Array.isArray(a) && a[0] === 19) threadID = String(a[1]);
|
|
101
90
|
}
|
|
91
|
+
for (const x of n) walk(x);
|
|
102
92
|
}
|
|
103
93
|
walk(payload?.step);
|
|
104
94
|
return { threadID, messageID };
|
|
105
95
|
}
|
|
106
96
|
|
|
107
|
-
function publishWithAck(content, reqID
|
|
97
|
+
function publishWithAck(content, reqID) {
|
|
108
98
|
return new Promise((resolve, reject) => {
|
|
109
99
|
if (!ctx.mqttClient || typeof ctx.mqttClient.on !== "function" || typeof ctx.mqttClient.publish !== "function") {
|
|
110
|
-
|
|
111
|
-
utils.error("sendMessageMqtt", err);
|
|
112
|
-
callback && callback(err);
|
|
113
|
-
return reject(err);
|
|
100
|
+
return reject(new Error("MQTT client is not initialized"));
|
|
114
101
|
}
|
|
115
|
-
|
|
116
102
|
if (typeof ctx.mqttClient.setMaxListeners === "function") {
|
|
117
103
|
ctx.mqttClient.setMaxListeners(0);
|
|
118
104
|
}
|
|
119
|
-
|
|
120
|
-
let
|
|
105
|
+
let settled = false;
|
|
106
|
+
let timer;
|
|
121
107
|
const cleanup = () => {
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
if (timer) clearTimeout(timer);
|
|
111
|
+
ctx.mqttClient.removeListener("message", onMessage);
|
|
125
112
|
};
|
|
126
|
-
const
|
|
113
|
+
const onMessage = (topic, message) => {
|
|
127
114
|
if (topic !== "/ls_resp") return;
|
|
128
|
-
let
|
|
115
|
+
let parsed;
|
|
129
116
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} catch {
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
if (jsonMsg.request_id !== reqID) return;
|
|
136
|
-
const { threadID, messageID } = extractIdsFromPayload(jsonMsg.payload);
|
|
137
|
-
const result = { messageID, threadID };
|
|
117
|
+
parsed = JSON.parse(message.toString());
|
|
118
|
+
if (typeof parsed.payload === "string") parsed.payload = JSON.parse(parsed.payload);
|
|
119
|
+
} catch { return; }
|
|
120
|
+
if (parsed.request_id !== reqID) return;
|
|
121
|
+
const { threadID, messageID } = extractIdsFromPayload(parsed.payload);
|
|
138
122
|
cleanup();
|
|
139
|
-
|
|
140
|
-
resolve(result);
|
|
123
|
+
resolve({ messageID, threadID });
|
|
141
124
|
};
|
|
142
|
-
ctx.mqttClient.on("message",
|
|
143
|
-
ctx.mqttClient.publish("/ls_req", JSON.stringify(content), { qos: 1, retain: false }, err => {
|
|
144
|
-
if (err) {
|
|
145
|
-
cleanup();
|
|
146
|
-
callback && callback(err);
|
|
147
|
-
reject(err);
|
|
148
|
-
}
|
|
125
|
+
ctx.mqttClient.on("message", onMessage);
|
|
126
|
+
ctx.mqttClient.publish("/ls_req", JSON.stringify(content), { qos: 1, retain: false }, (err) => {
|
|
127
|
+
if (err) { cleanup(); reject(err); }
|
|
149
128
|
});
|
|
150
|
-
setTimeout(() => {
|
|
151
|
-
if (
|
|
129
|
+
timer = setTimeout(() => {
|
|
130
|
+
if (settled) return;
|
|
152
131
|
cleanup();
|
|
153
|
-
|
|
154
|
-
callback && callback(err);
|
|
155
|
-
reject(err);
|
|
132
|
+
reject({ error: "Timeout waiting for ACK" });
|
|
156
133
|
}, 15000);
|
|
157
134
|
});
|
|
158
135
|
}
|
|
159
136
|
|
|
160
137
|
return async (msg, threadID, replyToMessage, callback) => {
|
|
161
|
-
if (typeof msg !==
|
|
138
|
+
if (typeof msg !== "string" && typeof msg !== "object") {
|
|
162
139
|
throw new Error("Message should be of type string or object, not " + utils.getType(msg) + ".");
|
|
163
140
|
}
|
|
164
|
-
|
|
165
|
-
if (typeof threadID !== 'string' && typeof threadID !== 'number') {
|
|
141
|
+
if (typeof threadID !== "string" && typeof threadID !== "number") {
|
|
166
142
|
throw new Error("threadID must be a string or number.");
|
|
167
143
|
}
|
|
168
|
-
|
|
169
|
-
if (!callback && typeof threadID === "function") {
|
|
170
|
-
throw new Error("Pass a threadID as a second argument.");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
144
|
if (!callback && typeof replyToMessage === "function") {
|
|
174
145
|
callback = replyToMessage;
|
|
175
146
|
replyToMessage = null;
|
|
176
147
|
}
|
|
177
148
|
|
|
178
|
-
// Apply anti-suspension throttling and volume checks before every MQTT send
|
|
179
149
|
try {
|
|
180
|
-
await globalAntiSuspension.prepareBeforeMessage(String(threadID), typeof msg ===
|
|
181
|
-
} catch (
|
|
182
|
-
utils.warn("sendMessageMqtt", "Anti-suspension check raised:", suspErr && suspErr.message ? suspErr.message : suspErr);
|
|
183
|
-
}
|
|
150
|
+
await globalAntiSuspension.prepareBeforeMessage(String(threadID), typeof msg === "string" ? msg : (msg.body || ""));
|
|
151
|
+
} catch (_) {}
|
|
184
152
|
|
|
185
|
-
const
|
|
153
|
+
const normalized = typeof msg === "string" ? { body: msg } : msg;
|
|
154
|
+
const baseBody = normalized.body != null ? String(normalized.body) : "";
|
|
155
|
+
const epoch = (BigInt(Date.now()) << 22n).toString();
|
|
156
|
+
const requestId = Math.floor(100 + Math.random() * 900);
|
|
186
157
|
const otid = utils.generateOfflineThreadingID();
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
158
|
+
|
|
159
|
+
const payload0 = {
|
|
160
|
+
thread_id: String(threadID),
|
|
161
|
+
otid: otid.toString(),
|
|
162
|
+
source: 2097153,
|
|
163
|
+
send_type: 1,
|
|
164
|
+
sync_group: 1,
|
|
165
|
+
mark_thread_read: 1,
|
|
166
|
+
text: baseBody === "" ? null : baseBody,
|
|
167
|
+
initiating_source: 0,
|
|
168
|
+
skip_url_preview_gen: 0,
|
|
169
|
+
text_has_links: hasLinks(baseBody) ? 1 : 0,
|
|
170
|
+
multitab_env: 0,
|
|
171
|
+
metadata_dataclass: JSON.stringify({ media_accessibility_metadata: { alt_text: null } }),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (normalized.mentions && Array.isArray(normalized.mentions) && normalized.mentions.length > 0) {
|
|
175
|
+
const mentionData = buildMentionData(normalized, baseBody);
|
|
176
|
+
if (mentionData) payload0.mention_data = mentionData;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (normalized.sticker) {
|
|
180
|
+
payload0.send_type = 2;
|
|
181
|
+
payload0.sticker_id = normalized.sticker;
|
|
182
|
+
payload0.text = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (normalized.emoji) {
|
|
186
|
+
payload0.send_type = 1;
|
|
187
|
+
payload0.text = normalized.emoji;
|
|
188
|
+
const sizeMap = { small: 1, medium: 2, large: 3 };
|
|
189
|
+
const emojiSize = normalized.emojiSize;
|
|
190
|
+
payload0.hot_emoji_size = (typeof emojiSize === "number" ? Math.min(3, Math.max(1, emojiSize)) : sizeMap[emojiSize]) || 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (normalized.location && normalized.location.latitude != null && normalized.location.longitude != null) {
|
|
194
|
+
payload0.send_type = 1;
|
|
195
|
+
payload0.location_data = {
|
|
196
|
+
coordinates: { latitude: normalized.location.latitude, longitude: normalized.location.longitude },
|
|
197
|
+
is_current_location: Boolean(normalized.location.current),
|
|
198
|
+
is_live_location: Boolean(normalized.location.live),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (normalized.effect) {
|
|
203
|
+
const effectTag = String(normalized.effect).toUpperCase().replace(/[\s\-]+/g, "_");
|
|
204
|
+
payload0.lightweight_action_attached = {
|
|
205
|
+
message_id: otid.toString(),
|
|
206
|
+
messaging_tag: "fb.messaging.effects." + effectTag,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (replyToMessage || normalized.replyToMessage) {
|
|
211
|
+
const replyId = replyToMessage || normalized.replyToMessage;
|
|
212
|
+
payload0.reply_metadata = {
|
|
213
|
+
reply_source_id: replyId,
|
|
211
214
|
reply_source_type: 1,
|
|
212
215
|
reply_type: 0,
|
|
213
216
|
};
|
|
214
217
|
}
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
if (normalized.attachment) {
|
|
220
|
+
payload0.send_type = 3;
|
|
221
|
+
if (payload0.text === "") payload0.text = null;
|
|
222
|
+
const list = Array.isArray(normalized.attachment) ? normalized.attachment : [normalized.attachment];
|
|
223
|
+
ctx._lastThreadHint = threadID;
|
|
224
|
+
const files = await uploadAttachments(list.filter(a => utils.isReadableStream(a)));
|
|
225
|
+
payload0.attachment_fbids = files.map(f => String(Object.values(f)[0]));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const content = {
|
|
218
229
|
app_id: "2220391788200892",
|
|
219
230
|
payload: {
|
|
220
|
-
tasks
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
231
|
+
tasks: [
|
|
232
|
+
{
|
|
233
|
+
label: "46",
|
|
234
|
+
payload: payload0,
|
|
235
|
+
queue_name: String(threadID),
|
|
236
|
+
task_id: 400,
|
|
237
|
+
failure_count: null,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
label: "21",
|
|
241
|
+
payload: {
|
|
242
|
+
thread_id: String(threadID),
|
|
243
|
+
last_read_watermark_ts: Date.now(),
|
|
244
|
+
sync_group: 1,
|
|
245
|
+
},
|
|
246
|
+
queue_name: String(threadID),
|
|
247
|
+
task_id: 401,
|
|
248
|
+
failure_count: null,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
epoch_id: epoch,
|
|
252
|
+
version_id: "24804310205905615",
|
|
253
|
+
data_trace_id: `#${Buffer.from(String(Math.random())).toString("base64").replace(/=+$/g, "")}`,
|
|
224
254
|
},
|
|
225
|
-
request_id,
|
|
255
|
+
request_id: requestId,
|
|
226
256
|
type: 3,
|
|
227
257
|
};
|
|
228
258
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
ctx._lastThreadHint = threadID;
|
|
232
|
-
const files = await new Promise((resolve, reject) => {
|
|
233
|
-
uploadAttachment(
|
|
234
|
-
Array.isArray(msg.attachment) ? msg.attachment : [msg.attachment],
|
|
235
|
-
(err, files) => {
|
|
236
|
-
if (err) return reject(err);
|
|
237
|
-
return resolve(files);
|
|
238
|
-
}
|
|
239
|
-
);
|
|
240
|
-
});
|
|
241
|
-
form.payload.tasks[0].payload.attachment_fbids = files.map(file => Object.values(file)[0]);
|
|
242
|
-
} catch (err) {
|
|
243
|
-
utils.error("Attachment upload failed:", err);
|
|
244
|
-
throw err;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
259
|
+
content.payload.tasks = content.payload.tasks.map(t => ({ ...t, payload: JSON.stringify(t.payload) }));
|
|
260
|
+
content.payload = JSON.stringify(content.payload);
|
|
247
261
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
try {
|
|
263
|
+
const result = await publishWithAck(content, requestId);
|
|
264
|
+
if (callback) callback(undefined, result);
|
|
265
|
+
return result;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (callback) callback(err);
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
254
270
|
};
|
|
255
271
|
};
|
|
@@ -2,39 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
const utils = require('../utils');
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @param {Object} defaultFuncs
|
|
7
|
-
* @param {Object} api
|
|
8
|
-
* @param {Object} ctx
|
|
9
|
-
*/
|
|
10
5
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
6
|
+
return function sendTypingIndicator(sendTyping, threadID, callback) {
|
|
7
|
+
let resolveFunc, rejectFunc;
|
|
8
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
9
|
+
resolveFunc = resolve;
|
|
10
|
+
rejectFunc = reject;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!callback) {
|
|
14
|
+
callback = (err) => {
|
|
15
|
+
if (err) return rejectFunc(err);
|
|
16
|
+
resolveFunc(true);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!ctx.mqttClient || typeof ctx.mqttClient.publish !== "function") {
|
|
21
|
+
const err = new Error("You can only use sendTypingIndicator after you start listening.");
|
|
22
|
+
callback(err);
|
|
23
|
+
return returnPromise;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const threadIDs = Array.isArray(threadID) ? threadID : [threadID];
|
|
27
|
+
if (!threadIDs.length) {
|
|
28
|
+
const err = new Error("threadID is required");
|
|
29
|
+
callback(err);
|
|
30
|
+
return returnPromise;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
|
|
34
|
+
|
|
35
|
+
function buildPayload(tid) {
|
|
36
|
+
const isGroup = String(tid).length >= 16 ? 1 : 0;
|
|
37
|
+
return {
|
|
38
|
+
app_id: "772021112871879",
|
|
39
|
+
payload: JSON.stringify({
|
|
40
|
+
label: "3",
|
|
41
|
+
payload: JSON.stringify({
|
|
42
|
+
thread_key: Number.parseInt(String(tid), 10),
|
|
43
|
+
is_group_thread: isGroup,
|
|
44
|
+
is_typing: sendTyping ? 1 : 0,
|
|
45
|
+
attribution: 0,
|
|
46
|
+
sync_group: 1,
|
|
47
|
+
thread_type: isGroup ? 2 : 1,
|
|
48
|
+
}),
|
|
49
|
+
version: "8965252033599983",
|
|
50
|
+
}),
|
|
51
|
+
request_id: ++ctx.wsReqNumber,
|
|
52
|
+
type: 4,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const publishes = threadIDs.map(tid =>
|
|
57
|
+
new Promise((resolve, reject) => {
|
|
58
|
+
ctx.mqttClient.publish("/ls_req", JSON.stringify(buildPayload(tid)), { qos: 1 }, (err) => {
|
|
59
|
+
if (err) return reject(err);
|
|
60
|
+
resolve();
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
Promise.all(publishes)
|
|
66
|
+
.then(() => callback(null, true))
|
|
67
|
+
.catch(err => {
|
|
68
|
+
utils.error("sendTypingIndicator", err);
|
|
69
|
+
callback(err);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return returnPromise;
|
|
73
|
+
};
|
|
40
74
|
};
|