@lazyneoaz/nkxchat 1.0.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/nkxchat",
3
- "version": "1.0.3",
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",
@@ -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
- globalCallback = callback || function(error, message) {
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
+ };
@@ -40,6 +40,131 @@ function resolveEffect(name) {
40
40
  return EFFECTS[key] ? EFFECTS[key].tag : name.toUpperCase();
41
41
  }
42
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 = utils.generateOfflineThreadingID();
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: 0,
117
+ send_type: 1,
118
+ sync_group: 1,
119
+ text: body,
120
+ initiating_source: 1,
121
+ skip_url_preview_gen: 0,
122
+ lightweight_action_attached: {
123
+ messaging_tag: 'fb.messaging.effects.' + effectTag,
124
+ },
125
+ };
126
+
127
+ if (typeof msgObj === 'object' && msgObj && msgObj.sticker) {
128
+ sendPayload.send_type = 2;
129
+ sendPayload.sticker_id = msgObj.sticker;
130
+ sendPayload.text = null;
131
+ }
132
+
133
+ const request_id = ++ctx.wsReqNumber;
134
+ const content = {
135
+ app_id: '2220391788200892',
136
+ payload: JSON.stringify({
137
+ tasks: [
138
+ {
139
+ label: '46',
140
+ payload: JSON.stringify(sendPayload),
141
+ queue_name: tid,
142
+ task_id: ++ctx.wsTaskNumber,
143
+ failure_count: null,
144
+ },
145
+ {
146
+ label: '21',
147
+ payload: JSON.stringify({
148
+ thread_id: tid,
149
+ last_read_watermark_ts: timestamp,
150
+ sync_group: 1,
151
+ }),
152
+ queue_name: tid,
153
+ task_id: ++ctx.wsTaskNumber,
154
+ failure_count: null,
155
+ },
156
+ ],
157
+ epoch_id,
158
+ version_id: '6120284488008082',
159
+ data_trace_id: null,
160
+ }),
161
+ request_id,
162
+ type: 3,
163
+ };
164
+
165
+ return { content, request_id, otid, timestamp };
166
+ }
167
+
43
168
  module.exports = function (defaultFuncs, api, ctx) {
44
169
  /**
45
170
  * Lists all available send effects.
@@ -89,19 +214,68 @@ module.exports = function (defaultFuncs, api, ctx) {
89
214
  const effectTag = resolveEffect(effectName);
90
215
  utils.log('sendEffect', `Effect "${effectTag}" → thread ${threadID}`);
91
216
 
92
- // Effects only work via MQTT (label-46 task). The legacy /messaging/send/
93
- // HTTP endpoint silently drops lightweight_action_attached fields and
94
- // delivers a plain message with no animation.
217
+ // ── Try HTTP first ───────────────────────────────────────────────────
218
+ try {
219
+ const { form, messageAndOTID, timestamp } = buildForm(effectTag, message, threadID, ctx);
220
+
221
+ const resData = await defaultFuncs
222
+ .post('https://www.facebook.com/messaging/send/', ctx.jar, form, { ...ctx, requestThreadID: String(threadID) })
223
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
224
+
225
+ if (!resData) throw new Error('Empty response from messaging/send');
226
+ if (resData.error) throw new Error(JSON.stringify(resData));
227
+
228
+ const actions = (resData.payload && resData.payload.actions) || [];
229
+ const msgInfo = actions.reduce((p, v) => ({
230
+ threadID: v.thread_fbid || p.threadID,
231
+ messageID: v.message_id || p.messageID,
232
+ timestamp: v.timestamp || p.timestamp,
233
+ }), { threadID, messageID: messageAndOTID, timestamp });
234
+
235
+ return callback(null, { ...msgInfo, effect: effectTag, method: 'http' });
236
+ } catch (httpErr) {
237
+ utils.warn('sendEffect', `HTTP failed: ${httpErr.message}. Falling back to MQTT.`);
238
+ }
239
+
240
+ // ── MQTT fallback ────────────────────────────────────────────────────
95
241
  if (!ctx.mqttClient) {
96
242
  throw new Error('MQTT is not connected. Call api.listenMqtt() first.');
97
243
  }
98
244
 
99
- const msg = typeof message === 'string'
100
- ? { body: message, effect: effectTag }
101
- : { ...message, effect: effectTag };
245
+ const { content, request_id, otid, timestamp } = buildMqttPayload(effectTag, message, threadID, ctx);
102
246
 
103
- const result = await api.sendMessageMqtt(msg, threadID);
104
- return callback(null, { ...result, effect: effectTag, method: 'mqtt' });
247
+ await new Promise((res, rej) => {
248
+ let done = false;
249
+ const timer = setTimeout(() => {
250
+ if (done) return;
251
+ done = true;
252
+ ctx.mqttClient.removeListener('message', onMsg);
253
+ rej(new Error('MQTT effect send timeout'));
254
+ }, 15000);
255
+
256
+ const onMsg = (topic, raw) => {
257
+ if (topic !== '/ls_resp') return;
258
+ let parsed;
259
+ try { parsed = JSON.parse(raw.toString()); parsed.payload = JSON.parse(parsed.payload); } catch { return; }
260
+ if (parsed.request_id !== request_id) return;
261
+ if (done) return;
262
+ done = true;
263
+ clearTimeout(timer);
264
+ ctx.mqttClient.removeListener('message', onMsg);
265
+ callback(null, { threadID, messageID: otid, timestamp, effect: effectTag, method: 'mqtt' });
266
+ res();
267
+ };
268
+
269
+ ctx.mqttClient.on('message', onMsg);
270
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, (err) => {
271
+ if (err && !done) {
272
+ done = true;
273
+ clearTimeout(timer);
274
+ ctx.mqttClient.removeListener('message', onMsg);
275
+ rej(err);
276
+ }
277
+ });
278
+ });
105
279
 
106
280
  } catch (err) {
107
281
  utils.error('sendEffect', err.message || err);