@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 +55 -1
- package/package.json +1 -1
- package/src/apis/gcmember.js +17 -10
- package/src/apis/gcrule.js +14 -9
- package/src/apis/getAccess.js +3 -1
- package/src/apis/getBotInitialData.js +1 -1
- package/src/apis/getThreadInfo.js +1 -1
- package/src/apis/markAsSeen.js +7 -9
- package/src/apis/nickname.js +6 -3
- package/src/apis/sendEffect.js +4 -4
- package/src/app/MessengerBot.js +239 -0
- package/src/app/broadcast.js +48 -0
- package/src/app/config.js +97 -0
- package/src/app/createFcaClient.js +179 -0
- package/src/app/state.js +117 -0
- package/src/app/threadSync.js +106 -0
- package/src/app/updateCheck.js +92 -0
- package/src/engine/client.js +76 -28
- package/src/utils/antiSuspension.js +9 -9
- package/src/utils/auth-helpers.js +17 -1
- package/src/utils/rateLimiter.js +1 -1
package/index.js
CHANGED
|
@@ -1,2 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
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.
|
|
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.",
|
package/src/apis/gcmember.js
CHANGED
|
@@ -43,24 +43,30 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
if (!validActions.includes(action)) {
|
|
46
|
-
|
|
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
|
-
|
|
50
|
+
_callback(null, { type: "error_gc", error: "userIDs is required." });
|
|
51
|
+
return returnPromise;
|
|
50
52
|
}
|
|
51
53
|
if (!threadID) {
|
|
52
|
-
|
|
54
|
+
_callback(null, { type: "error_gc", error: "threadID is required." });
|
|
55
|
+
return returnPromise;
|
|
53
56
|
}
|
|
54
57
|
if (!ctx.mqttClient) {
|
|
55
|
-
|
|
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
|
-
|
|
64
|
+
_callback(null, { type: "error_gc", error: "Could not retrieve thread information." });
|
|
65
|
+
return returnPromise;
|
|
61
66
|
}
|
|
62
67
|
if (threadInfo.isGroup === false) {
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/apis/gcrule.js
CHANGED
|
@@ -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
|
-
|
|
44
|
+
_callback(null, { type: "error_gc_rule", error: `Invalid action. Must be one of: ${validActions.join(", ")}` });
|
|
45
|
+
return returnPromise;
|
|
45
46
|
}
|
|
46
|
-
if (!userID)
|
|
47
|
-
if (!threadID)
|
|
48
|
-
if (!ctx.mqttClient)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
_callback(null, { type: "error_gc_rule", error: err.message || "An unknown error occurred." });
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
return returnPromise;
|
package/src/apis/getAccess.js
CHANGED
|
@@ -52,7 +52,9 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
52
52
|
Origin: url
|
|
53
53
|
})
|
|
54
54
|
.then(function (res) {
|
|
55
|
-
var
|
|
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)
|
|
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 ||
|
|
266
|
+
threadInfos[threadInfo.threadID || threadIDs[i]] = threadInfo;
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
package/src/apis/markAsSeen.js
CHANGED
|
@@ -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
|
|
14
|
-
* @param {Function} [callback] -
|
|
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
|
|
34
|
-
if (err)
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
+
callback(err);
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
return returnPromise;
|
package/src/apis/nickname.js
CHANGED
|
@@ -70,14 +70,17 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
70
70
|
participantID = participantID || ctx.userID;
|
|
71
71
|
|
|
72
72
|
if (!threadID) {
|
|
73
|
-
|
|
73
|
+
_callback(new Error("threadID is required to set a nickname."));
|
|
74
|
+
return returnPromise;
|
|
74
75
|
}
|
|
75
76
|
if (typeof nickname !== 'string') {
|
|
76
|
-
|
|
77
|
+
_callback(new Error("nickname must be a string."));
|
|
78
|
+
return returnPromise;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
if (!ctx.mqttClient) {
|
|
80
|
-
|
|
82
|
+
_callback(new Error("Not connected to MQTT"));
|
|
83
|
+
return returnPromise;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
ctx.wsReqNumber += 1;
|
package/src/apis/sendEffect.js
CHANGED
|
@@ -213,9 +213,9 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
try {
|
|
216
|
-
if (!effectName)
|
|
217
|
-
if (message === undefined || message === null)
|
|
218
|
-
if (!threadID)
|
|
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
|
-
|
|
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 };
|