@lazyneoaz/nkxchat 1.0.1 → 1.0.3
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 +1 -1
- package/src/apis/listenMqtt.js +80 -9
- package/src/apis/sendEffect.js +8 -182
- package/src/utils/autoReLogin.js +3 -1
- package/src/utils/constants.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/nkxchat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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.",
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -3,6 +3,7 @@ const utils = require('../utils');
|
|
|
3
3
|
const mqtt = require('mqtt');
|
|
4
4
|
const WebSocket = require('ws');
|
|
5
5
|
const duplexify = require('duplexify');
|
|
6
|
+
const { PassThrough, Writable } = require('stream');
|
|
6
7
|
const HttpsProxyAgent = require('https-proxy-agent');
|
|
7
8
|
const EventEmitter = require('events');
|
|
8
9
|
const { parseDelta } = require('./mqttDeltaValue');
|
|
@@ -188,21 +189,91 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
188
189
|
const wsOpts = {
|
|
189
190
|
headers: options.wsOptions.headers,
|
|
190
191
|
origin: options.wsOptions.origin,
|
|
191
|
-
perMessageDeflate: false,
|
|
192
192
|
};
|
|
193
193
|
if (options.wsOptions.agent) wsOpts.agent = options.wsOptions.agent;
|
|
194
|
+
|
|
194
195
|
const ws = new WebSocket(host, wsOpts);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
|
|
197
|
+
// Custom writable — sends chunks directly through the WebSocket socket.send()
|
|
198
|
+
// rather than using createWebSocketStream, which avoids perMessageDeflate
|
|
199
|
+
// negotiation conflicts and stream buffering edge cases.
|
|
200
|
+
let wsTarget = null;
|
|
201
|
+
let proxyEnded = false;
|
|
202
|
+
const proxy = new Writable({
|
|
203
|
+
autoDestroy: true,
|
|
204
|
+
write(chunk, _enc, cb) {
|
|
205
|
+
if (proxyEnded || this.destroyed) return cb();
|
|
206
|
+
const sock = wsTarget;
|
|
207
|
+
if (sock && sock.readyState === WebSocket.OPEN) {
|
|
208
|
+
try { sock.send(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), cb); }
|
|
209
|
+
catch (e) { cb(e); }
|
|
210
|
+
} else { cb(); }
|
|
211
|
+
},
|
|
212
|
+
final(cb) {
|
|
213
|
+
proxyEnded = true;
|
|
214
|
+
const sock = wsTarget;
|
|
215
|
+
wsTarget = null;
|
|
216
|
+
if (sock && (sock.readyState === WebSocket.CONNECTING || sock.readyState === WebSocket.OPEN)) {
|
|
217
|
+
try { sock.terminate ? sock.terminate() : sock.close(); } catch (_) {}
|
|
218
|
+
}
|
|
219
|
+
cb();
|
|
220
|
+
}
|
|
200
221
|
});
|
|
201
|
-
|
|
222
|
+
|
|
223
|
+
const readable = new PassThrough();
|
|
224
|
+
const stream = new duplexify(undefined, undefined, { end: false, autoDestroy: true });
|
|
225
|
+
|
|
226
|
+
const toBuffer = (data) => {
|
|
227
|
+
if (Buffer.isBuffer(data)) return data;
|
|
228
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
229
|
+
if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
230
|
+
return Buffer.from(String(data));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
let closed = false;
|
|
234
|
+
const cleanup = () => {
|
|
235
|
+
if (closed) return;
|
|
236
|
+
closed = true;
|
|
237
|
+
proxyEnded = true;
|
|
238
|
+
wsTarget = null;
|
|
239
|
+
try { ws.removeAllListeners(); } catch (_) {}
|
|
240
|
+
try { if (ws.readyState === WebSocket.OPEN) ws.terminate ? ws.terminate() : ws.close(); } catch (_) {}
|
|
241
|
+
readable.end();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
ws.on('open', () => {
|
|
245
|
+
if (closed) return;
|
|
246
|
+
wsTarget = ws;
|
|
247
|
+
stream.setWritable(proxy);
|
|
248
|
+
stream.setReadable(readable);
|
|
249
|
+
stream.emit('connect');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
ws.on('message', (data) => {
|
|
253
|
+
if (closed) return;
|
|
254
|
+
readable.write(toBuffer(data));
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
ws.on('error', (err) => {
|
|
258
|
+
cleanup();
|
|
259
|
+
stream.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
ws.on('close', () => {
|
|
263
|
+
cleanup();
|
|
264
|
+
stream.end();
|
|
265
|
+
if (!stream.destroyed) stream.destroy();
|
|
266
|
+
});
|
|
267
|
+
|
|
202
268
|
ws.on('unexpected-response', (_req, res) => {
|
|
203
|
-
|
|
269
|
+
cleanup();
|
|
270
|
+
stream.destroy(new Error(`WebSocket unexpected response: ${res.statusCode}`));
|
|
204
271
|
});
|
|
205
|
-
|
|
272
|
+
|
|
273
|
+
stream.on('finish', cleanup);
|
|
274
|
+
stream.on('close', cleanup);
|
|
275
|
+
|
|
276
|
+
return stream;
|
|
206
277
|
}
|
|
207
278
|
|
|
208
279
|
const mqttClient = new mqtt.Client(buildMqttStream, options);
|
package/src/apis/sendEffect.js
CHANGED
|
@@ -40,131 +40,6 @@ 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
|
-
|
|
168
43
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
169
44
|
/**
|
|
170
45
|
* Lists all available send effects.
|
|
@@ -214,68 +89,19 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
214
89
|
const effectTag = resolveEffect(effectName);
|
|
215
90
|
utils.log('sendEffect', `Effect "${effectTag}" → thread ${threadID}`);
|
|
216
91
|
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
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 ────────────────────────────────────────────────────
|
|
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.
|
|
241
95
|
if (!ctx.mqttClient) {
|
|
242
96
|
throw new Error('MQTT is not connected. Call api.listenMqtt() first.');
|
|
243
97
|
}
|
|
244
98
|
|
|
245
|
-
const
|
|
99
|
+
const msg = typeof message === 'string'
|
|
100
|
+
? { body: message, effect: effectTag }
|
|
101
|
+
: { ...message, effect: effectTag };
|
|
246
102
|
|
|
247
|
-
await
|
|
248
|
-
|
|
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
|
-
});
|
|
103
|
+
const result = await api.sendMessageMqtt(msg, threadID);
|
|
104
|
+
return callback(null, { ...result, effect: effectTag, method: 'mqtt' });
|
|
279
105
|
|
|
280
106
|
} catch (err) {
|
|
281
107
|
utils.error('sendEffect', err.message || err);
|
package/src/utils/autoReLogin.js
CHANGED
|
@@ -102,6 +102,8 @@ class AutoReLoginManager {
|
|
|
102
102
|
const setOptionsModel = require('../engine/models/setOptions');
|
|
103
103
|
const buildAPIModel = require('../engine/models/buildAPI');
|
|
104
104
|
|
|
105
|
+
const fbLinkFunc = typeof fbLink === 'function' ? fbLink : () => fbLink;
|
|
106
|
+
|
|
105
107
|
await new Promise((resolve, reject) => {
|
|
106
108
|
loginHelperModel(
|
|
107
109
|
this.credentials,
|
|
@@ -126,7 +128,7 @@ class AutoReLoginManager {
|
|
|
126
128
|
setOptionsModel,
|
|
127
129
|
buildAPIModel,
|
|
128
130
|
api,
|
|
129
|
-
|
|
131
|
+
fbLinkFunc,
|
|
130
132
|
ERROR_RETRIEVING
|
|
131
133
|
);
|
|
132
134
|
});
|
package/src/utils/constants.js
CHANGED
|
@@ -194,7 +194,7 @@ function getFrom(str, startToken, endToken) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function makeParsable(html) {
|
|
197
|
-
const withoutForLoop = html.replace(
|
|
197
|
+
const withoutForLoop = html.replace(/^\s*for\s*\(;;\);\s*/i, "");
|
|
198
198
|
const maybeMultipleObjects = withoutForLoop.split(/\}\r\n *\{/);
|
|
199
199
|
if (maybeMultipleObjects.length === 1) return maybeMultipleObjects;
|
|
200
200
|
|