@lazyneoaz/metachat 1.0.9 → 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.
package/index.js CHANGED
@@ -1,2 +1,56 @@
1
1
  "use strict";
2
- module.exports = require('./src/engine/client');
2
+
3
+ const { login, loginAsync, loginLegacy, DEFAULT_OPTIONS } = require('./src/engine/client');
4
+
5
+ const { MessengerBot, MessengerContext, createMessengerBot } = require('./src/app/MessengerBot');
6
+ const { createFcaClient, attachClientFacade, createMessagesDomain, createThreadsDomain, createUsersDomain, createAccountDomain, createRealtimeDomain, createHttpDomain, createSchedulerDomain } = require('./src/app/createFcaClient');
7
+ const { defaultConfig, loadConfig, resolveConfig, writeConfigTemplate } = require('./src/app/config');
8
+ const { broadcast } = require('./src/app/broadcast');
9
+ const { attachThreadInfoRealtimeSync } = require('./src/app/threadSync');
10
+ const { checkForPackageUpdate, runConfiguredUpdateCheck } = require('./src/app/updateCheck');
11
+ const { createDefaultContext, createFcaState, createApiFacade, createRequestHelper } = require('./src/app/state');
12
+
13
+ const { normalizeCookieHeaderString, setJarFromPairs } = require('./src/utils/formatters/value/formatCookie');
14
+ const { createAuthCore } = require('./src/utils/auth-helpers');
15
+
16
+ module.exports = login;
17
+
18
+ module.exports.login = login;
19
+ module.exports.loginAsync = loginAsync;
20
+ module.exports.loginLegacy = loginLegacy;
21
+ module.exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
22
+
23
+ module.exports.MessengerBot = MessengerBot;
24
+ module.exports.MessengerContext = MessengerContext;
25
+ module.exports.createMessengerBot = createMessengerBot;
26
+
27
+ module.exports.createFcaClient = createFcaClient;
28
+ module.exports.attachClientFacade = attachClientFacade;
29
+
30
+ module.exports.createMessagesDomain = createMessagesDomain;
31
+ module.exports.createThreadsDomain = createThreadsDomain;
32
+ module.exports.createUsersDomain = createUsersDomain;
33
+ module.exports.createAccountDomain = createAccountDomain;
34
+ module.exports.createRealtimeDomain = createRealtimeDomain;
35
+ module.exports.createHttpDomain = createHttpDomain;
36
+ module.exports.createSchedulerDomain = createSchedulerDomain;
37
+
38
+ module.exports.defaultConfig = defaultConfig;
39
+ module.exports.loadConfig = loadConfig;
40
+ module.exports.resolveConfig = resolveConfig;
41
+ module.exports.writeConfigTemplate = writeConfigTemplate;
42
+
43
+ module.exports.broadcast = broadcast;
44
+ module.exports.attachThreadInfoRealtimeSync = attachThreadInfoRealtimeSync;
45
+
46
+ module.exports.checkForPackageUpdate = checkForPackageUpdate;
47
+ module.exports.runConfiguredUpdateCheck = runConfiguredUpdateCheck;
48
+
49
+ module.exports.createDefaultContext = createDefaultContext;
50
+ module.exports.createFcaState = createFcaState;
51
+ module.exports.createApiFacade = createApiFacade;
52
+ module.exports.createRequestHelper = createRequestHelper;
53
+
54
+ module.exports.normalizeCookieHeaderString = normalizeCookieHeaderString;
55
+ module.exports.setJarFromPairs = setJarFromPairs;
56
+ module.exports.createAuthCore = createAuthCore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
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.",
@@ -43,24 +43,30 @@ module.exports = function (defaultFuncs, api, ctx) {
43
43
 
44
44
 
45
45
  if (!validActions.includes(action)) {
46
- return _callback(null, { type: "error_gc", error: `Invalid action. Must be one of: ${validActions.join(", ")}` });
46
+ _callback(null, { type: "error_gc", error: `Invalid action. Must be one of: ${validActions.join(", ")}` });
47
+ return returnPromise;
47
48
  }
48
49
  if (!userIDs || userIDs.length === 0) {
49
- return _callback(null, { type: "error_gc", error: "userIDs is required." });
50
+ _callback(null, { type: "error_gc", error: "userIDs is required." });
51
+ return returnPromise;
50
52
  }
51
53
  if (!threadID) {
52
- return _callback(null, { type: "error_gc", error: "threadID is required." });
54
+ _callback(null, { type: "error_gc", error: "threadID is required." });
55
+ return returnPromise;
53
56
  }
54
57
  if (!ctx.mqttClient) {
55
- return _callback(null, { type: "error_gc", error: "Not connected to MQTT" });
58
+ _callback(null, { type: "error_gc", error: "Not connected to MQTT" });
59
+ return returnPromise;
56
60
  }
57
61
 
58
62
  const threadInfo = await api.getThreadInfo(threadID);
59
63
  if (!threadInfo) {
60
- return _callback(null, { type: "error_gc", error: "Could not retrieve thread information." });
64
+ _callback(null, { type: "error_gc", error: "Could not retrieve thread information." });
65
+ return returnPromise;
61
66
  }
62
67
  if (threadInfo.isGroup === false) {
63
- return _callback(null, { type: "error_gc", error: "This feature is only for group chats, not private messages." });
68
+ _callback(null, { type: "error_gc", error: "This feature is only for group chats, not private messages." });
69
+ return returnPromise;
64
70
  }
65
71
 
66
72
  const currentMembers = threadInfo.participantIDs;
@@ -74,7 +80,8 @@ module.exports = function (defaultFuncs, api, ctx) {
74
80
  if (action === 'add') {
75
81
  const usersToAdd = usersToModify.filter(id => !currentMembers.includes(id));
76
82
  if (usersToAdd.length === 0) {
77
- return _callback(null, { type: "error_gc", error: "All specified users are already in the group." });
83
+ _callback(null, { type: "error_gc", error: "All specified users are already in the group." });
84
+ return returnPromise;
78
85
  }
79
86
  finalUsers = usersToAdd;
80
87
  queryPayload = { thread_key: parseInt(threadID), contact_ids: finalUsers.map(id => parseInt(id)), sync_group: 1 };
@@ -83,7 +90,8 @@ module.exports = function (defaultFuncs, api, ctx) {
83
90
  } else { // action is 'remove'
84
91
  const userToRemove = usersToModify[0];
85
92
  if (!currentMembers.includes(userToRemove)) {
86
- return _callback(null, { type: "error_gc", error: `User with ID ${userToRemove} is not in this group chat.` });
93
+ _callback(null, { type: "error_gc", error: `User with ID ${userToRemove} is not in this group chat.` });
94
+ return returnPromise;
87
95
  }
88
96
  finalUsers = [userToRemove];
89
97
  queryPayload = { thread_id: threadID, contact_id: userToRemove, sync_group: 1 };
@@ -113,8 +121,7 @@ module.exports = function (defaultFuncs, api, ctx) {
113
121
  return _callback(null, gcmemberInfo);
114
122
  });
115
123
  } catch (err) {
116
-
117
- return _callback(null, { type: "error_gc", error: err.message || "An unknown error occurred." });
124
+ _callback(null, { type: "error_gc", error: err.message || "An unknown error occurred." });
118
125
  }
119
126
 
120
127
  return returnPromise;
@@ -41,18 +41,21 @@ module.exports = function (defaultFuncs, api, ctx) {
41
41
  action = action ? action.toLowerCase() : "";
42
42
 
43
43
  if (!validActions.includes(action)) {
44
- return _callback(null, { type: "error_gc_rule", error: `Invalid action. Must be one of: ${validActions.join(", ")}` });
44
+ _callback(null, { type: "error_gc_rule", error: `Invalid action. Must be one of: ${validActions.join(", ")}` });
45
+ return returnPromise;
45
46
  }
46
- if (!userID) return _callback(null, { type: "error_gc_rule", error: "userID is required." });
47
- if (!threadID) return _callback(null, { type: "error_gc_rule", error: "threadID is required." });
48
- if (!ctx.mqttClient) return _callback(null, { type: "error_gc_rule", error: "Not connected to MQTT" });
47
+ if (!userID) { _callback(null, { type: "error_gc_rule", error: "userID is required." }); return returnPromise; }
48
+ if (!threadID) { _callback(null, { type: "error_gc_rule", error: "threadID is required." }); return returnPromise; }
49
+ if (!ctx.mqttClient) { _callback(null, { type: "error_gc_rule", error: "Not connected to MQTT" }); return returnPromise; }
49
50
 
50
51
  const threadInfo = await api.getThreadInfo(threadID);
51
52
  if (!threadInfo) {
52
- return _callback(null, { type: "error_gc_rule", error: "Could not retrieve thread information." });
53
+ _callback(null, { type: "error_gc_rule", error: "Could not retrieve thread information." });
54
+ return returnPromise;
53
55
  }
54
56
  if (threadInfo.isGroup === false) {
55
- return _callback(null, { type: "error_gc_rule", error: "This feature is only for group chats." });
57
+ _callback(null, { type: "error_gc_rule", error: "This feature is only for group chats." });
58
+ return returnPromise;
56
59
  }
57
60
 
58
61
  const adminIDs = threadInfo.adminIDs || [];
@@ -60,11 +63,13 @@ module.exports = function (defaultFuncs, api, ctx) {
60
63
 
61
64
  if (action === 'admin') {
62
65
  if (isCurrentlyAdmin) {
63
- return _callback(null, { type: "error_gc_rule", error: `User is already an admin.` });
66
+ _callback(null, { type: "error_gc_rule", error: `User is already an admin.` });
67
+ return returnPromise;
64
68
  }
65
69
  } else { // action is 'unadmin'
66
70
  if (!isCurrentlyAdmin) {
67
- return _callback(null, { type: "error_gc_rule", error: `User is not an admin.` });
71
+ _callback(null, { type: "error_gc_rule", error: `User is not an admin.` });
72
+ return returnPromise;
68
73
  }
69
74
  }
70
75
 
@@ -111,7 +116,7 @@ module.exports = function (defaultFuncs, api, ctx) {
111
116
  });
112
117
 
113
118
  } catch (err) {
114
- return _callback(null, { type: "error_gc_rule", error: err.message || "An unknown error occurred." });
119
+ _callback(null, { type: "error_gc_rule", error: err.message || "An unknown error occurred." });
115
120
  }
116
121
 
117
122
  return returnPromise;
@@ -52,7 +52,9 @@ module.exports = function (defaultFuncs, api, ctx) {
52
52
  Origin: url
53
53
  })
54
54
  .then(function (res) {
55
- var { payload } = JSON.parse(res.body.split(';').pop() || '{}');
55
+ var parsed;
56
+ try { parsed = JSON.parse(res.body.split(';').pop() || '{}'); } catch(e) { parsed = {}; }
57
+ var { payload } = parsed;
56
58
  if (payload && !payload.codeConfirmed)
57
59
  throw {
58
60
  error: 'submitCode',
@@ -23,7 +23,7 @@ module.exports = (defaultFuncs, api, ctx) => {
23
23
  customUserAgent: utils.windowsUserAgent
24
24
  }, (err, data) => {
25
25
 
26
- if (err) throw err;
26
+ if (err) return callback(err);
27
27
  const profileMatch = data.match(/"CurrentUserInitialData",\[\],\{(.*?)\},(.*?)\]/);
28
28
  if (profileMatch && profileMatch[1]){
29
29
  const accountJson = JSON.parse(`{${profileMatch[1]}}`);
@@ -263,7 +263,7 @@ module.exports = function (defaultFuncs, api, ctx) {
263
263
 
264
264
  const threadInfo = formatThreadGraphQLResponse(responseData.data);
265
265
  if (threadInfo) {
266
- threadInfos[threadInfo.threadID || threadID[threadID.length - 1 - i]] = threadInfo;
266
+ threadInfos[threadInfo.threadID || threadIDs[i]] = threadInfo;
267
267
  }
268
268
  }
269
269
 
@@ -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;
@@ -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;
@@ -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);
@@ -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 };