@lazyneoaz/metachat 1.0.8 → 1.0.10

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.
@@ -10,8 +10,8 @@ const utils = require('../utils');
10
10
  module.exports = function (defaultFuncs, api, ctx) {
11
11
  /**
12
12
  * Marks all messages as "seen" up to a specific timestamp.
13
- * @param {number} [seen_timestamp=Date.now()] - The timestamp (in milliseconds) up to which messages should be marked as seen. If a function is provided, it's treated as the callback and the timestamp defaults to the current time.
14
- * @param {Function} [callback] - The callback function.
13
+ * @param {number} [seen_timestamp=Date.now()] - The timestamp (in ms) up to which messages should be marked as seen.
14
+ * @param {Function} [callback] - Optional callback function.
15
15
  * @returns {Promise<void>} A Promise that resolves on success or rejects with an error.
16
16
  */
17
17
  return async function markAsSeen(seen_timestamp, callback) {
@@ -30,11 +30,9 @@ module.exports = function (defaultFuncs, api, ctx) {
30
30
  }
31
31
 
32
32
  if (!callback) {
33
- callback = function (err, friendList) {
34
- if (err) {
35
- return rejectFunc(err);
36
- }
37
- resolveFunc(friendList);
33
+ callback = function (err) {
34
+ if (err) return rejectFunc(err);
35
+ resolveFunc();
38
36
  };
39
37
  }
40
38
 
@@ -56,13 +54,13 @@ module.exports = function (defaultFuncs, api, ctx) {
56
54
  throw resData;
57
55
  }
58
56
 
59
- return callback();
57
+ callback(null);
60
58
  } catch (err) {
61
59
  utils.error("markAsSeen", err);
62
60
  if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
63
61
  ctx.loggedIn = false;
64
62
  }
65
- return callback(err);
63
+ callback(err);
66
64
  }
67
65
 
68
66
  return returnPromise;
@@ -183,7 +183,7 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
183
183
  } catch (err) {
184
184
  return;
185
185
  }
186
- if (!ctx.globalOptions.selfListen && fmtEvent2 && fmtEvent2.author && fmtEvent2.author.toString() === ctx.userID) return;
186
+ if (!ctx.globalOptions.selfListenEvent && fmtEvent2 && fmtEvent2.author && fmtEvent2.author.toString() === ctx.userID) return;
187
187
  if (!ctx.loggedIn) return;
188
188
  if (fmtEvent2) globalCallback(null, fmtEvent2);
189
189
  break;
@@ -193,7 +193,7 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
193
193
  var tid = v.delta.threadKey.threadFbId;
194
194
  if (mid && tid) {
195
195
  var form = {
196
- av: ctx.globalOptions.pageID,
196
+ av: ctx.globalOptions.pageID || ctx.userID,
197
197
  queries: JSON.stringify({
198
198
  o0: {
199
199
  doc_id: "2848441488556444",
@@ -70,14 +70,17 @@ module.exports = function (defaultFuncs, api, ctx) {
70
70
  participantID = participantID || ctx.userID;
71
71
 
72
72
  if (!threadID) {
73
- return _callback(new Error("threadID is required to set a nickname."));
73
+ _callback(new Error("threadID is required to set a nickname."));
74
+ return returnPromise;
74
75
  }
75
76
  if (typeof nickname !== 'string') {
76
- return _callback(new Error("nickname must be a string."));
77
+ _callback(new Error("nickname must be a string."));
78
+ return returnPromise;
77
79
  }
78
80
 
79
81
  if (!ctx.mqttClient) {
80
- return _callback(new Error("Not connected to MQTT"));
82
+ _callback(new Error("Not connected to MQTT"));
83
+ return returnPromise;
81
84
  }
82
85
 
83
86
  ctx.wsReqNumber += 1;
@@ -99,7 +102,7 @@ module.exports = function (defaultFuncs, api, ctx) {
99
102
  };
100
103
 
101
104
  const context = {
102
- app_id: ctx.appID,
105
+ app_id: ctx.appID || "2220391788200892",
103
106
  payload: {
104
107
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
105
108
  tasks: [query],
@@ -34,11 +34,13 @@ function extractAndSearchLightspeedRequest(allJsonData, options = {}) {
34
34
  }
35
35
 
36
36
 
37
- try {
38
- fs.writeFileSync(outputFile, JSON.stringify(lightReq, null, 2), "utf8");
39
- utils.log(`pin.js: Saved lightspeed_web_request to ${outputFile}`);
40
- } catch (err) {
41
- utils.error("pin.js: Failed to write lightspeed_web_request.json", err);
37
+ if (outputFile) {
38
+ try {
39
+ fs.writeFileSync(outputFile, JSON.stringify(lightReq, null, 2), "utf8");
40
+ utils.log(`pin.js: Saved lightspeed_web_request to ${outputFile}`);
41
+ } catch (err) {
42
+ utils.error("pin.js: Failed to write lightspeed_web_request.json", err);
43
+ }
42
44
  }
43
45
 
44
46
 
@@ -67,6 +67,7 @@ module.exports = (defaultFuncs, api, ctx) => {
67
67
  }
68
68
  if (jsonMsg.request_id !== reqID) return;
69
69
  responseHandled = true;
70
+ clearTimeout(timeout);
70
71
  ctx.mqttClient.removeListener("message", handleRes);
71
72
  callback(null, { success: true });
72
73
  resolveFunc({ success: true });
@@ -213,9 +213,9 @@ module.exports = function (defaultFuncs, api, ctx) {
213
213
  }
214
214
 
215
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.'));
216
+ if (!effectName) { callback(new Error('effectName is required.')); return returnPromise; }
217
+ if (message === undefined || message === null) { callback(new Error('message is required.')); return returnPromise; }
218
+ if (!threadID) { callback(new Error('threadID is required.')); return returnPromise; }
219
219
 
220
220
  const effectTag = resolveEffect(effectName);
221
221
  utils.log('sendEffect', `Effect "${effectTag}" → thread ${threadID}`);
@@ -280,7 +280,7 @@ module.exports = function (defaultFuncs, api, ctx) {
280
280
  timestamp: v.timestamp || p.timestamp,
281
281
  }), { threadID, messageID: messageAndOTID, timestamp: httpTs });
282
282
 
283
- return callback(null, { ...msgInfo, effect: effectTag, method: 'http' });
283
+ callback(null, { ...msgInfo, effect: effectTag, method: 'http' });
284
284
 
285
285
  } catch (err) {
286
286
  utils.error('sendEffect', err.message || err);
@@ -5,20 +5,21 @@ const utils = require('../utils');
5
5
 
6
6
  module.exports = function (defaultFuncs, api, ctx) {
7
7
  return async (reaction, messageID) => {
8
- if (!reaction) throw new Error("Please enter a valid emoji.");
9
- const defData = await defaultFuncs.postFormData("https://www.facebook.com/webgraphql/mutation/", ctx.jar, {}, {
8
+ if (reaction === undefined || reaction === null) throw new Error("Please enter a valid emoji.");
9
+ const action = reaction === "" ? "REMOVE_REACTION" : "ADD_REACTION";
10
+ const defData = await defaultFuncs.postFormData("https://www.facebook.com/webgraphql/mutation/", ctx.jar, {
10
11
  doc_id: "1491398900900362",
11
12
  variables: JSON.stringify({
12
- data: {
13
- client_mutation_id: ctx.clientMutationId++,
14
- actor_id: ctx.userID,
15
- action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION",
16
- message_id: messageID,
17
- reaction
18
- }
19
- }),
13
+ data: {
14
+ client_mutation_id: ctx.clientMutationId++,
15
+ actor_id: ctx.userID,
16
+ action,
17
+ message_id: messageID,
18
+ reaction
19
+ }
20
+ }),
20
21
  dpr: 1
21
- });
22
+ }, {});
22
23
  const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
23
24
  if (!resData) {
24
25
  throw new Error("setMessageReactionLegacy returned empty object.");
@@ -35,17 +35,17 @@ module.exports = function (defaultFuncs, api, ctx) {
35
35
 
36
36
  // ── Hardcoded colour aliases (fast path — no API round trip needed) ──────
37
37
  const PALETTE = {
38
- blue: "196241301102133",
39
- purple: "370940413392601",
40
- green: "169463077092846",
41
- pink: "230032715012014",
42
- orange: "175615189761153",
43
- red: "2136751179887052",
44
- yellow: "2058653964378557",
45
- teal: "417639218648241",
46
- black: "539927563794799",
47
- white: "2873642392710980",
48
- default: "196241301102133",
38
+ blue: "196241301102133", // DefaultBlue / MessengerBlue
39
+ purple: "234137870477637", // BrightPurple / MediumSlateBlue
40
+ green: "2136751179887052", // FreeSpeechGreen / Green
41
+ pink: "169463077092846", // HotPink / BrilliantRose
42
+ orange: "175615189761153", // Pumpkin / Orange
43
+ red: "2129984390566328", // RadicalRed / Red
44
+ yellow: "174636906462322", // GoldenPoppy / Yellow
45
+ teal: "1928399724138152", // TealBlue / Viking
46
+ aqua: "417639218648241", // Aqua
47
+ black: "271607034185782", // Shadow (darkest solid theme)
48
+ default: "196241301102133", // DefaultBlue
49
49
  };
50
50
 
51
51
  (async function worker() {
@@ -10,7 +10,7 @@ module.exports = function (defaultFuncs, api, ctx) {
10
10
  })
11
11
  const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
12
12
  if (resData.error) {
13
- throw new Error(resData);
13
+ throw new Error(JSON.stringify(resData));
14
14
  }
15
15
  return resData;
16
16
  };
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+
3
+ const EventEmitter = require("events");
4
+
5
+ class MessengerContext {
6
+ constructor(bot, event) {
7
+ this.bot = bot;
8
+ this.event = event;
9
+ }
10
+
11
+ get threadID() { return this.event.threadID; }
12
+ get senderID() { return this.event.senderID; }
13
+ get messageID() { return this.event.messageID; }
14
+ get text() { return (this.event.body || "").trim(); }
15
+ get body() { return this.event.body; }
16
+ get message() { return this.event; }
17
+
18
+ reply(payload, callback) {
19
+ const tid = this.event.threadID;
20
+ if (tid == null) throw new Error("MessengerContext.reply: threadID is missing");
21
+ return this.bot.api.sendMessage(payload, tid, callback);
22
+ }
23
+
24
+ async replyAsync(payload) {
25
+ const r = this.reply(payload);
26
+ if (r && typeof r.then === "function") return r;
27
+ return Promise.resolve(r);
28
+ }
29
+ }
30
+
31
+ function escapeRegex(s) {
32
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
+ }
34
+
35
+ function emitIf(bot, channel, payload) {
36
+ if (bot.listenerCount(channel) > 0) bot.emit(channel, payload);
37
+ }
38
+
39
+ function emitGatewayEvents(bot, event) {
40
+ emitIf(bot, "update", event);
41
+ emitIf(bot, "raw", event);
42
+ const t = event.type;
43
+ if (!t) return;
44
+ if (t === "message" || t === "message_reply") {
45
+ emitIf(bot, "message", event);
46
+ emitIf(bot, "messageCreate", event);
47
+ }
48
+ if (t === "message_reply") {
49
+ emitIf(bot, "message_reply", event);
50
+ } else if (t !== "message") {
51
+ emitIf(bot, t, event);
52
+ }
53
+ switch (t) {
54
+ case "message_reaction":
55
+ emitIf(bot, "messageReactionAdd", event);
56
+ break;
57
+ case "message_unsend":
58
+ emitIf(bot, "messageDelete", event);
59
+ break;
60
+ case "typ":
61
+ emitIf(bot, event.isTyping ? "typingStart" : "typingStop", event);
62
+ break;
63
+ case "event":
64
+ emitIf(bot, "threadUpdate", event);
65
+ break;
66
+ case "ready":
67
+ emitIf(bot, "ready", event);
68
+ emitIf(bot, "shardReady", event);
69
+ break;
70
+ }
71
+ }
72
+
73
+ class MessengerBot extends EventEmitter {
74
+ constructor(api, options = {}) {
75
+ super();
76
+ this.api = api;
77
+ this.options = Object.assign({
78
+ commandPrefix: "/",
79
+ enableComposer: true,
80
+ autoListen: true,
81
+ stopOnSignals: false,
82
+ maxEventListeners: 64,
83
+ }, options);
84
+
85
+ if (this.options.maxEventListeners > 0) {
86
+ this.setMaxListeners(this.options.maxEventListeners);
87
+ }
88
+
89
+ this._middleware = [];
90
+ this._commands = new Map();
91
+ this._hears = [];
92
+ this._catchHandler = null;
93
+ this._mqttHandle = null;
94
+ this._signalHandlers = null;
95
+ this._client = null;
96
+ }
97
+
98
+ get client() {
99
+ if (!this._client) {
100
+ const { createFcaClient } = require("./createFcaClient");
101
+ this._client = createFcaClient(this.api);
102
+ }
103
+ return this._client;
104
+ }
105
+
106
+ use(fn) {
107
+ this._middleware.push(fn);
108
+ return this;
109
+ }
110
+
111
+ command(name, fn) {
112
+ this._commands.set(String(name).toLowerCase(), fn);
113
+ return this;
114
+ }
115
+
116
+ hears(trigger, fn) {
117
+ this._hears.push({ trigger, fn });
118
+ return this;
119
+ }
120
+
121
+ catch(fn) {
122
+ this._catchHandler = fn;
123
+ return this;
124
+ }
125
+
126
+ async _runComposer(event) {
127
+ if (!this.options.enableComposer) return;
128
+ if (event.type !== "message" && event.type !== "message_reply") return;
129
+
130
+ const ctx = new MessengerContext(this, event);
131
+ const prefix = this.options.commandPrefix || "/";
132
+
133
+ const body = event.body || "";
134
+ const trimmed = body.trim();
135
+
136
+ const pipeline = [...this._middleware];
137
+
138
+ if (trimmed.startsWith(prefix)) {
139
+ const withoutPrefix = trimmed.slice(prefix.length);
140
+ const cmdName = withoutPrefix.split(/\s+/)[0].toLowerCase();
141
+ if (this._commands.has(cmdName)) {
142
+ pipeline.push(this._commands.get(cmdName));
143
+ }
144
+ } else {
145
+ for (const { trigger, fn } of this._hears) {
146
+ if (typeof trigger === "string") {
147
+ if (trimmed.toLowerCase().includes(trigger.toLowerCase())) {
148
+ pipeline.push(fn);
149
+ }
150
+ } else if (trigger instanceof RegExp) {
151
+ if (trigger.test(trimmed)) {
152
+ pipeline.push(fn);
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ let idx = 0;
159
+ const next = async () => {
160
+ if (idx >= pipeline.length) return;
161
+ const fn = pipeline[idx++];
162
+ try {
163
+ await fn(ctx, next);
164
+ } catch (err) {
165
+ if (this._catchHandler) {
166
+ this._catchHandler(err, ctx);
167
+ } else {
168
+ this.emit("error", err);
169
+ }
170
+ }
171
+ };
172
+
173
+ await next();
174
+ }
175
+
176
+ startListening() {
177
+ if (this._mqttHandle) return;
178
+ this._mqttHandle = this.api.listenMqtt((err, event) => {
179
+ if (err) {
180
+ this.emit("error", err);
181
+ return;
182
+ }
183
+ emitGatewayEvents(this, event);
184
+ this._runComposer(event).catch((e) => this.emit("error", e));
185
+ });
186
+ }
187
+
188
+ stopListening() {
189
+ if (this._mqttHandle && typeof this._mqttHandle.stopListening === "function") {
190
+ this._mqttHandle.stopListening();
191
+ } else if (this.api && typeof this.api.stopListening === "function") {
192
+ this.api.stopListening();
193
+ }
194
+ this._mqttHandle = null;
195
+ }
196
+
197
+ async stop() {
198
+ if (this._signalHandlers) {
199
+ for (const [sig, handler] of Object.entries(this._signalHandlers)) {
200
+ process.removeListener(sig, handler);
201
+ }
202
+ this._signalHandlers = null;
203
+ }
204
+ this.stopListening();
205
+ this.removeAllListeners();
206
+ }
207
+
208
+ async launch(launchOptions = {}) {
209
+ const opts = Object.assign({}, this.options, launchOptions);
210
+ if (opts.stopOnSignals) {
211
+ const handler = async () => {
212
+ await this.stop();
213
+ process.exit(0);
214
+ };
215
+ this._signalHandlers = { SIGINT: handler, SIGTERM: handler };
216
+ process.once("SIGINT", handler);
217
+ process.once("SIGTERM", handler);
218
+ }
219
+ if (opts.autoListen !== false) {
220
+ this.startListening();
221
+ }
222
+ }
223
+ }
224
+
225
+ async function createMessengerBot(credentials, options = {}) {
226
+ const { login } = require("../engine/client");
227
+ const api = await login(credentials, options);
228
+ const bot = new MessengerBot(api, options);
229
+ if (options.autoListen !== false) {
230
+ bot.startListening();
231
+ }
232
+ return bot;
233
+ }
234
+
235
+ module.exports = {
236
+ MessengerBot,
237
+ MessengerContext,
238
+ createMessengerBot,
239
+ };
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Broadcast a message to multiple threads.
5
+ * @param {object} api - The FCA api object.
6
+ * @param {string|object} message - Message body or message object.
7
+ * @param {string[]} threadIDs - Array of thread IDs to send to.
8
+ * @param {object} [options] - Options.
9
+ * @param {number} [options.delayMs=200] - Delay between sends in ms.
10
+ * @returns {Promise<{sent: string[], failed: string[]}>}
11
+ */
12
+ async function broadcast(api, message, threadIDs, options = {}) {
13
+ if (!api || typeof api.sendMessage !== "function") {
14
+ throw new Error("broadcast: api must be a valid FCA api object");
15
+ }
16
+ if (!Array.isArray(threadIDs) || threadIDs.length === 0) {
17
+ throw new Error("broadcast: threadIDs must be a non-empty array");
18
+ }
19
+
20
+ const delayMs = typeof options.delayMs === "number" ? options.delayMs : 200;
21
+ const sent = [];
22
+ const failed = [];
23
+
24
+ for (const tid of threadIDs) {
25
+ try {
26
+ await new Promise((resolve, reject) => {
27
+ const result = api.sendMessage(message, tid, (err) => {
28
+ if (err) reject(err);
29
+ else resolve();
30
+ });
31
+ if (result && typeof result.then === "function") {
32
+ result.then(resolve).catch(reject);
33
+ }
34
+ });
35
+ sent.push(tid);
36
+ } catch (err) {
37
+ failed.push(tid);
38
+ }
39
+
40
+ if (delayMs > 0 && threadIDs.indexOf(tid) < threadIDs.length - 1) {
41
+ await new Promise((r) => setTimeout(r, delayMs));
42
+ }
43
+ }
44
+
45
+ return { sent, failed };
46
+ }
47
+
48
+ module.exports = { broadcast };
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const CONFIG_FILE = "fca-config.json";
7
+
8
+ const DEFAULT_CONFIG = {
9
+ checkUpdate: {
10
+ enabled: false,
11
+ install: false,
12
+ notifyIfCurrent: false,
13
+ packageName: "@lazyneoaz/metachat",
14
+ registryUrl: "https://registry.npmjs.org",
15
+ timeoutMs: 10000,
16
+ },
17
+ mqtt: {
18
+ enabled: true,
19
+ reconnectInterval: 3600,
20
+ },
21
+ credentials: {
22
+ email: "",
23
+ password: "",
24
+ twofactor: "",
25
+ },
26
+ antiGetInfo: {
27
+ AntiGetThreadInfo: false,
28
+ AntiGetUserInfo: false,
29
+ },
30
+ remoteControl: {
31
+ enabled: false,
32
+ url: "",
33
+ token: "",
34
+ autoReconnect: true,
35
+ },
36
+ };
37
+
38
+ function defaultConfig() {
39
+ return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
40
+ }
41
+
42
+ function loadConfig() {
43
+ const configPath = path.resolve(process.cwd(), CONFIG_FILE);
44
+ let fileConfig = {};
45
+ let created = false;
46
+
47
+ try {
48
+ if (fs.existsSync(configPath)) {
49
+ const raw = fs.readFileSync(configPath, "utf8");
50
+ fileConfig = JSON.parse(raw);
51
+ } else {
52
+ try {
53
+ fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf8");
54
+ created = true;
55
+ } catch (_) {}
56
+ }
57
+ } catch (_) {}
58
+
59
+ const config = resolveConfig(fileConfig);
60
+ return { config, created };
61
+ }
62
+
63
+ function resolveConfig(partial = {}) {
64
+ const base = defaultConfig();
65
+ return deepMerge(base, partial);
66
+ }
67
+
68
+ function deepMerge(target, source) {
69
+ const out = Object.assign({}, target);
70
+ for (const key of Object.keys(source || {})) {
71
+ if (
72
+ source[key] !== null &&
73
+ typeof source[key] === "object" &&
74
+ !Array.isArray(source[key]) &&
75
+ typeof target[key] === "object" &&
76
+ target[key] !== null
77
+ ) {
78
+ out[key] = deepMerge(target[key], source[key]);
79
+ } else {
80
+ out[key] = source[key];
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ function writeConfigTemplate(destPath) {
87
+ const target = destPath || path.resolve(process.cwd(), "fca-config.example.json");
88
+ fs.writeFileSync(target, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf8");
89
+ return target;
90
+ }
91
+
92
+ module.exports = {
93
+ defaultConfig,
94
+ loadConfig,
95
+ resolveConfig,
96
+ writeConfigTemplate,
97
+ };