@dongdev/fca-unofficial 2.0.21 → 2.0.23

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/CHANGELOG.md CHANGED
@@ -98,3 +98,9 @@ Too lazy to write changelog, sorry! (will write changelog in the next release, t
98
98
 
99
99
  ## v2.0.20 - 2025-10-08
100
100
  - Hotfix / auto bump
101
+
102
+ ## v2.0.21 - 2025-10-08
103
+ - Hotfix / auto bump
104
+
105
+ ## v2.0.22 - 2025-10-09
106
+ - Hotfix / auto bump
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongdev/fca-unofficial",
3
- "version": "2.0.21",
3
+ "version": "2.0.23",
4
4
  "description": "Unofficial Facebook Chat API for Node.js - Interact with Facebook Messenger programmatically",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,64 +1,46 @@
1
1
  "use strict";
2
2
 
3
- const log = require("npmlog");
4
- const { get } = require("../../utils/request");
5
- const { getType } = require("../../utils/format");
6
- module.exports = function(defaultFuncs, api, ctx) {
7
- return function httpGet(url, form, customHeader, callback, notAPI) {
8
- let resolveFunc = function() {};
9
- let rejectFunc = function() {};
3
+ const { getType } = require("../../utils/format.js");
4
+ const { get } = require("../../utils/request.js");
10
5
 
11
- const returnPromise = new Promise(function(resolve, reject) {
6
+ const httpGetFactory = function (defaultFuncs, api, ctx) {
7
+ return function httpGet(url, form, callback, notAPI) {
8
+ let resolveFunc = () => { };
9
+ let rejectFunc = () => { };
10
+
11
+ const returnPromise = new Promise((resolve, reject) => {
12
12
  resolveFunc = resolve;
13
13
  rejectFunc = reject;
14
14
  });
15
15
 
16
16
  if (
17
- getType(form) == "Function" ||
18
- getType(form) == "AsyncFunction"
17
+ !callback &&
18
+ (getType(form) === "Function" || getType(form) === "AsyncFunction")
19
19
  ) {
20
20
  callback = form;
21
21
  form = {};
22
22
  }
23
23
 
24
- if (
25
- getType(customHeader) == "Function" ||
26
- getType(customHeader) == "AsyncFunction"
27
- ) {
28
- callback = customHeader;
29
- customHeader = {};
30
- }
31
-
32
- customHeader = customHeader || {};
24
+ form = form || {};
33
25
 
34
26
  callback =
35
27
  callback ||
36
- function(err, data) {
28
+ function (err, data) {
37
29
  if (err) return rejectFunc(err);
38
30
  resolveFunc(data);
39
31
  };
40
32
 
41
- if (notAPI) {
42
- get(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
43
- .then(function(resData) {
44
- callback(null, resData.data.toString());
45
- })
46
- .catch(function(err) {
47
- log.error("httpGet", err);
48
- return callback(err);
49
- });
50
- } else {
51
- defaultFuncs
52
- .get(url, ctx.jar, form, null, customHeader)
53
- .then(function(resData) {
54
- callback(null, resData.data.toString());
55
- })
56
- .catch(function(err) {
57
- log.error("httpGet", err);
58
- return callback(err);
59
- });
60
- }
33
+ const executor = notAPI ? get : defaultFuncs.get;
34
+
35
+ executor(url, ctx.jar, form)
36
+ .then((resData) => callback(null, resData.data))
37
+ .catch(function (err) {
38
+ console.error("httpGet", err);
39
+ return callback(err);
40
+ });
61
41
 
62
42
  return returnPromise;
63
43
  };
64
44
  };
45
+
46
+ module.exports = httpGetFactory;
@@ -1,64 +1,52 @@
1
1
  "use strict";
2
2
 
3
- const log = require("npmlog");
4
3
  const { post } = require("../../utils/request");
5
4
  const { getType } = require("../../utils/format");
6
- module.exports = function(defaultFuncs, api, ctx) {
7
- return function httpPost(url, form, customHeader, callback, notAPI) {
8
- let resolveFunc = function() {};
9
- let rejectFunc = function() {};
10
5
 
11
- const returnPromise = new Promise(function(resolve, reject) {
6
+ const httpPostFactory = function (defaultFuncs, api, ctx) {
7
+ return function httpPost(url, form, callback, notAPI) {
8
+ let resolveFunc = () => { };
9
+ let rejectFunc = () => { };
10
+
11
+ const returnPromise = new Promise((resolve, reject) => {
12
12
  resolveFunc = resolve;
13
13
  rejectFunc = reject;
14
14
  });
15
15
 
16
16
  if (
17
- getType(form) == "Function" ||
18
- getType(form) == "AsyncFunction"
17
+ !callback &&
18
+ (getType(form) === "Function" || getType(form) === "AsyncFunction")
19
19
  ) {
20
20
  callback = form;
21
21
  form = {};
22
22
  }
23
23
 
24
- if (
25
- getType(customHeader) == "Function" ||
26
- getType(customHeader) == "AsyncFunction"
27
- ) {
28
- callback = customHeader;
29
- customHeader = {};
30
- }
31
-
32
- customHeader = customHeader || {};
24
+ form = form || {};
33
25
 
34
26
  callback =
35
27
  callback ||
36
- function(err, data) {
28
+ function (err, data) {
37
29
  if (err) return rejectFunc(err);
38
30
  resolveFunc(data);
39
31
  };
40
32
 
41
- if (notAPI) {
42
- post(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
43
- .then(function(resData) {
44
- callback(null, resData.data.toString());
45
- })
46
- .catch(function(err) {
47
- log.error("httpPost", err);
48
- return callback(err);
49
- });
50
- } else {
51
- defaultFuncs
52
- .post(url, ctx.jar, form, {}, customHeader)
53
- .then(function(resData) {
54
- callback(null, resData.data.toString());
55
- })
56
- .catch(function(err) {
57
- log.error("httpPost", err);
58
- return callback(err);
59
- });
60
- }
33
+ const executor = notAPI ? post : defaultFuncs.post;
34
+
35
+ executor(url, ctx.jar, form, ctx.globalOptions)
36
+ .then((resData) => {
37
+ let data = resData.data;
38
+ if (typeof data === "object") {
39
+ data = JSON.stringify(data, null, 2);
40
+ }
41
+ callback(null, data);
42
+ })
43
+ .catch((err) => {
44
+ console.error("httpPost", err);
45
+ return callback(err);
46
+ });
61
47
 
62
48
  return returnPromise;
63
49
  };
64
50
  };
51
+
52
+ module.exports = httpPostFactory;
@@ -1,41 +1,43 @@
1
1
  "use strict";
2
2
  const { formatID } = require("../../../utils/format");
3
+
4
+ "use strict";
3
5
  module.exports = function createListenMqtt(deps) {
4
- const { WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger } = deps;
6
+ const { WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy,
7
+ topics, parseDelta, getTaskResponseData, logger
8
+ } = deps;
9
+
5
10
  return function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
11
+
12
+ function scheduleReconnect(delayMs) {
13
+ const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
14
+ const ms = typeof delayMs === "number" ? delayMs : d;
15
+ if (ctx._reconnectTimer) return; // debounce
16
+ logger(`mqtt will reconnect in ${ms}ms`, "warn");
17
+ ctx._reconnectTimer = setTimeout(() => {
18
+ ctx._reconnectTimer = null;
19
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
20
+ }, ms);
21
+ }
22
+ function isEndingLikeError(msg) {
23
+ return /No subscription existed|client disconnecting|socket hang up|ECONNRESET/i.test(msg || "");
24
+ }
25
+
6
26
  const chatOn = ctx.globalOptions.online;
7
- const foreground = false;
8
27
  const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
28
+ const GUID = utils.getGUID();
9
29
  const username = {
10
- u: ctx.userID,
11
- s: sessionID,
12
- chat_on: chatOn,
13
- fg: foreground,
14
- d: ctx.clientId,
15
- ct: "websocket",
16
- aid: 219994525426954,
17
- aids: null,
18
- mqtt_sid: "",
19
- cp: 3,
20
- ecp: 10,
21
- st: [],
22
- pm: [],
23
- dc: "",
24
- no_auto_fg: true,
25
- gas: null,
26
- pack: [],
27
- p: null,
28
- php_override: ""
30
+ u: ctx.userID, s: sessionID, chat_on: chatOn, fg: false, d: GUID,
31
+ ct: "websocket", aid: 219994525426954, aids: null, mqtt_sid: "",
32
+ cp: 3, ecp: 10, st: [], pm: [], dc: "", no_auto_fg: true, gas: null, pack: [], p: null, php_override: ""
29
33
  };
34
+
30
35
  const cookies = api.getCookies();
31
36
  let host;
32
- if (ctx.mqttEndpoint) {
33
- host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${ctx.clientId}`;
34
- } else if (ctx.region) {
35
- host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLowerCase()}&sid=${sessionID}&cid=${ctx.clientId}`;
36
- } else {
37
- host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}&cid=${ctx.clientId}`;
38
- }
37
+ if (ctx.mqttEndpoint) host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${GUID}`;
38
+ else if (ctx.region) host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLowerCase()}&sid=${sessionID}&cid=${ctx.clientId}`;
39
+ else host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}&cid=${GUID}`;
40
+
39
41
  const options = {
40
42
  clientId: "mqttwsclient",
41
43
  protocolId: "MQIsdp",
@@ -46,7 +48,7 @@ module.exports = function createListenMqtt(deps) {
46
48
  headers: {
47
49
  Cookie: cookies,
48
50
  Origin: "https://www.facebook.com",
49
- "User-Agent": ctx.globalOptions.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
51
+ "User-Agent": ctx.globalOptions.userAgent || "Mozilla/5.0",
50
52
  Referer: "https://www.facebook.com/",
51
53
  Host: "edge-chat.facebook.com",
52
54
  Connection: "Upgrade",
@@ -64,115 +66,124 @@ module.exports = function createListenMqtt(deps) {
64
66
  },
65
67
  keepalive: 30,
66
68
  reschedulePings: true,
67
- reconnectPeriod: 1000,
69
+ reconnectPeriod: 0,
68
70
  connectTimeout: 5000
69
71
  };
70
72
  if (ctx.globalOptions.proxy !== undefined) {
71
73
  const agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
72
74
  options.wsOptions.agent = agent;
73
75
  }
74
- ctx.mqttClient = new mqtt.Client(() => buildStream(options, new WebSocket(host, options.wsOptions), buildProxy()), options);
76
+
77
+ ctx.mqttClient = new mqtt.Client(
78
+ () => buildStream(options, new WebSocket(host, options.wsOptions), buildProxy()),
79
+ options
80
+ );
75
81
  const mqttClient = ctx.mqttClient;
76
82
  global.mqttClient = mqttClient;
83
+
84
+
77
85
  mqttClient.on("error", function (err) {
78
86
  const msg = String(err && err.message ? err.message : err || "");
79
- if (ctx._ending && /No subscription existed/i.test(msg)) {
80
- logger("MQTT ignore unsubscribe error during shutdown", "warn");
87
+
88
+ if ((ctx._ending || ctx._cycling) && isEndingLikeError(msg)) {
89
+ logger(`mqtt expected during shutdown: ${msg}`, "info");
81
90
  return;
82
91
  }
83
- logger(`MQTT error: ${msg}`, "error");
84
- // mqttClient.end();
85
- // logger("MQTT autoReconnect listenMqtt() in 2000ms", "warn");
86
- // setTimeout(() => listenMqtt(defaultFuncs, api, ctx, globalCallback), 2000);
87
- process.exit(1);
92
+ logger(`mqtt error: ${msg}`, "error");
93
+ try { mqttClient.end(true); } catch (_) { }
94
+ if (ctx._ending || ctx._cycling) return;
95
+
96
+ if (ctx.globalOptions.autoReconnect) {
97
+ scheduleReconnect();
98
+ } else {
99
+
100
+ globalCallback({ type: "stop_listen", error: msg || "Connection refused: Server unavailable" }, null);
101
+ }
88
102
  });
103
+
89
104
  mqttClient.on("connect", function () {
90
105
  if (process.env.OnStatus === undefined) {
91
106
  logger("fca-unoffcial premium", "info");
92
107
  process.env.OnStatus = true;
93
108
  }
94
- topics.forEach(topicsub => mqttClient.subscribe(topicsub));
95
- let topic;
96
- const queue = { sync_api_version: 11, max_deltas_able_to_process: 100, delta_batch_size: 500, encoding: "JSON", entity_fbid: ctx.userID, initial_titan_sequence_id: ctx.lastSeqId, device_params: null };
97
- if (ctx.syncToken) {
98
- topic = "/messenger_sync_get_diffs";
99
- queue.last_seq_id = ctx.lastSeqId;
100
- queue.sync_token = ctx.syncToken;
101
- } else {
102
- topic = "/messenger_sync_create_queue";
103
- queue.initial_titan_sequence_id = ctx.lastSeqId;
104
- queue.device_params = null;
105
- }
109
+ ctx._cycling = false;
110
+
111
+ topics.forEach(t => mqttClient.subscribe(t));
112
+
113
+
114
+ const queue = {
115
+ sync_api_version: 11, max_deltas_able_to_process: 100, delta_batch_size: 500,
116
+ encoding: "JSON", entity_fbid: ctx.userID, initial_titan_sequence_id: ctx.lastSeqId, device_params: null
117
+ };
118
+ const topic = ctx.syncToken ? "/messenger_sync_get_diffs" : "/messenger_sync_create_queue";
119
+ if (ctx.syncToken) { queue.last_seq_id = ctx.lastSeqId; queue.sync_token = ctx.syncToken; }
106
120
  mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
107
121
  mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
108
122
  mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
123
+ const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
109
124
  const rTimeout = setTimeout(function () {
110
- mqttClient.end();
111
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
125
+ logger(`mqtt t_ms timeout, cycling in ${d}ms`, "warn");
126
+ try { mqttClient.end(true); } catch (_) { }
127
+ scheduleReconnect(d);
112
128
  }, 5000);
129
+
113
130
  ctx.tmsWait = function () {
114
131
  clearTimeout(rTimeout);
115
- ctx.globalOptions.emitReady ? globalCallback({ type: "ready", error: null }) : "";
132
+ if (ctx.globalOptions.emitReady) globalCallback({ type: "ready", error: null });
116
133
  delete ctx.tmsWait;
117
134
  };
118
135
  });
119
- mqttClient.on("message", function (topic, message, _packet) {
136
+
137
+ mqttClient.on("message", function (topic, message) {
120
138
  try {
121
139
  let jsonMessage = Buffer.isBuffer(message) ? Buffer.from(message).toString() : message;
122
- try {
123
- jsonMessage = JSON.parse(jsonMessage);
124
- } catch (e) {
125
- jsonMessage = {};
126
- }
140
+ try { jsonMessage = JSON.parse(jsonMessage); } catch (_) { jsonMessage = {}; }
141
+
127
142
  if (jsonMessage.type === "jewel_requests_add") {
128
143
  globalCallback(null, { type: "friend_request_received", actorFbId: jsonMessage.from.toString(), timestamp: Date.now().toString() });
129
144
  } else if (jsonMessage.type === "jewel_requests_remove_old") {
130
145
  globalCallback(null, { type: "friend_request_cancel", actorFbId: jsonMessage.from.toString(), timestamp: Date.now().toString() });
131
146
  } else if (topic === "/t_ms") {
132
- if (ctx.tmsWait && typeof ctx.tmsWait == "function") {
133
- ctx.tmsWait();
134
- }
147
+ if (ctx.tmsWait && typeof ctx.tmsWait == "function") ctx.tmsWait();
135
148
  if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
136
149
  ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
137
150
  ctx.syncToken = jsonMessage.syncToken;
138
151
  }
139
- if (jsonMessage.lastIssuedSeqId) {
140
- ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
141
- }
142
- for (const i in jsonMessage.deltas) {
143
- const delta = jsonMessage.deltas[i];
144
- parseDelta(defaultFuncs, api, ctx, globalCallback, { delta });
152
+ if (jsonMessage.lastIssuedSeqId) ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
153
+ for (const dlt of (jsonMessage.deltas || [])) {
154
+ parseDelta(defaultFuncs, api, ctx, globalCallback, { delta: dlt });
145
155
  }
146
156
  } else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
147
- const typ = { type: "typ", isTyping: !!jsonMessage.state, from: jsonMessage.sender_fbid.toString(), threadID: formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString()) };
157
+ const typ = {
158
+ type: "typ",
159
+ isTyping: !!jsonMessage.state,
160
+ from: jsonMessage.sender_fbid.toString(),
161
+ threadID: formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
162
+ };
148
163
  globalCallback(null, typ);
149
164
  } else if (topic === "/orca_presence") {
150
165
  if (!ctx.globalOptions.updatePresence) {
151
- for (const i in jsonMessage.list) {
152
- const data = jsonMessage.list[i];
153
- const userID = data["u"];
154
- const presence = { type: "presence", userID: userID.toString(), timestamp: data["l"] * 1000, statuses: data["p"] };
166
+ for (const data of (jsonMessage.list || [])) {
167
+ const presence = { type: "presence", userID: String(data.u), timestamp: data.l * 1000, statuses: data.p };
155
168
  globalCallback(null, presence);
156
169
  }
157
170
  }
158
- } else if (topic == "/ls_resp") {
171
+ } else if (topic === "/ls_resp") {
159
172
  const parsedPayload = JSON.parse(jsonMessage.payload);
160
173
  const reqID = jsonMessage.request_id;
161
174
  if (ctx["tasks"].has(reqID)) {
162
175
  const taskData = ctx["tasks"].get(reqID);
163
176
  const { type: taskType, callback: taskCallback } = taskData;
164
177
  const taskRespData = getTaskResponseData(taskType, parsedPayload);
165
- if (taskRespData == null) {
166
- taskCallback("error", null);
167
- } else {
168
- taskCallback(null, Object.assign({ type: taskType, reqID: reqID }, taskRespData));
169
- }
178
+ if (taskRespData == null) taskCallback("error", null);
179
+ else taskCallback(null, Object.assign({ type: taskType, reqID }, taskRespData));
170
180
  }
171
181
  }
172
182
  } catch (ex) {
173
- return;
183
+ logger(`mqtt message parse error: ${ex && ex.message ? ex.message : ex}`, "error");
174
184
  }
175
185
  });
186
+
176
187
  mqttClient.on("close", function () { });
177
188
  mqttClient.on("disconnect", () => { });
178
189
  };
@@ -15,44 +15,54 @@ const getTaskResponseData = require("./core/getTaskResponseData");
15
15
  const parseDelta = createParseDelta({ markDelivery, parseAndCheckLogin });
16
16
  const listenMqtt = createListenMqtt({ WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy, topics, parseDelta, getTaskResponseData, logger });
17
17
  const getSeqIDFactory = createGetSeqID({ parseAndCheckLogin, listenMqtt, logger });
18
- module.exports = function (defaultFuncs, api, ctx) {
18
+
19
+ const MQTT_DEFAULTS = { cycleMs: 60 * 60 * 1000, reconnectDelayMs: 2000, autoReconnect: true, reconnectAfterStop: false };
20
+ function mqttConf(ctx, overrides) {
21
+ ctx._mqttOpt = Object.assign({}, MQTT_DEFAULTS, ctx._mqttOpt || {}, overrides || {});
22
+ if (typeof ctx._mqttOpt.autoReconnect === "boolean") ctx.globalOptions.autoReconnect = ctx._mqttOpt.autoReconnect;
23
+ return ctx._mqttOpt;
24
+ }
25
+
26
+ module.exports = function (defaultFuncs, api, ctx, opts) {
19
27
  const identity = function () { };
20
28
  let globalCallback = identity;
29
+ let conf = mqttConf(ctx, opts);
30
+
21
31
  function getSeqIDWrapper() {
22
32
  const form = {
23
- av: ctx.userID,
33
+ av: ctx.globalOptions.pageID,
24
34
  queries: JSON.stringify({
25
- o0: { doc_id: "3336396659757871", query_params: { limit: 1, before: null, tags: ["INBOX"], includeDeliveryReceipts: false, includeSeqID: true } }
35
+ o0: {
36
+ doc_id: "3336396659757871",
37
+ query_params: { limit: 1, before: null, tags: ["INBOX"], includeDeliveryReceipts: false, includeSeqID: true }
38
+ }
26
39
  })
27
40
  };
28
- logger("MQTT getSeqID call", "info");
29
- return getSeqIDFactory(defaultFuncs, api, ctx, globalCallback, form).then(() => {
30
- logger("MQTT getSeqID done", "info");
31
- }).catch(e => {
32
- logger(`MQTT getSeqID error: ${e && e.message ? e.message : e}`, "error");
33
- });
41
+ logger("mqtt getSeqID call", "info");
42
+ return getSeqIDFactory(defaultFuncs, api, ctx, globalCallback, form)
43
+ .then(() => { logger("mqtt getSeqID done", "info"); ctx._cycling = false; })
44
+ .catch(e => { logger(`mqtt getSeqID error: ${e && e.message ? e.message : e}`, "error"); });
34
45
  }
46
+
35
47
  function isConnected() {
36
48
  return !!(ctx.mqttClient && ctx.mqttClient.connected);
37
49
  }
50
+
38
51
  function unsubAll(cb) {
39
52
  if (!isConnected()) return cb && cb();
40
53
  let pending = topics.length;
41
54
  if (!pending) return cb && cb();
42
- let done = false;
55
+ let fired = false;
43
56
  topics.forEach(t => {
44
- ctx.mqttClient.unsubscribe(t, err => {
45
- const msg = String(err && err.message ? err.message : err || "");
46
- if (msg && /No subscription existed/i.test(msg)) err = null;
47
- if (--pending === 0 && !done) {
48
- done = true;
49
- cb && cb();
50
- }
57
+ ctx.mqttClient.unsubscribe(t, () => {
58
+ if (--pending === 0 && !fired) { fired = true; cb && cb(); }
51
59
  });
52
60
  });
53
61
  }
62
+
54
63
  function endQuietly(next) {
55
64
  const finish = () => {
65
+ try { ctx.mqttClient && ctx.mqttClient.removeAllListeners(); } catch (_) { }
56
66
  ctx.mqttClient = undefined;
57
67
  ctx.lastSeqId = null;
58
68
  ctx.syncToken = undefined;
@@ -62,71 +72,77 @@ module.exports = function (defaultFuncs, api, ctx) {
62
72
  };
63
73
  try {
64
74
  if (ctx.mqttClient) {
65
- if (isConnected()) {
66
- try { ctx.mqttClient.publish("/browser_close", "{}"); } catch (_) { }
67
- }
75
+ if (isConnected()) { try { ctx.mqttClient.publish("/browser_close", "{}"); } catch (_) { } }
68
76
  ctx.mqttClient.end(true, finish);
69
77
  } else finish();
70
- } catch (_) {
71
- finish();
72
- }
78
+ } catch (_) { finish(); }
73
79
  }
80
+
74
81
  function delayedReconnect() {
75
- logger("MQTT reconnect in 2000ms", "info");
76
- setTimeout(() => getSeqIDWrapper(), 2000);
82
+ const d = conf.reconnectDelayMs;
83
+ logger(`mqtt reconnect in ${d}ms`, "info");
84
+ setTimeout(() => getSeqIDWrapper(), d);
77
85
  }
86
+
78
87
  function forceCycle() {
88
+ if (ctx._cycling) return; // đừng cycle chồng
89
+ ctx._cycling = true;
79
90
  ctx._ending = true;
80
- logger("MQTT force cycle begin", "warn");
81
- unsubAll(() => {
82
- endQuietly(() => {
83
- delayedReconnect();
84
- });
85
- });
91
+ logger("mqtt force cycle begin", "warn");
92
+ unsubAll(() => endQuietly(() => delayedReconnect()));
86
93
  }
94
+
87
95
  return function (callback) {
88
96
  class MessageEmitter extends EventEmitter {
89
97
  stopListening(callback2) {
90
98
  const cb = callback2 || function () { };
91
- logger("MQTT stop requested", "info");
99
+ logger("mqtt stop requested", "info");
92
100
  globalCallback = identity;
101
+
93
102
  if (ctx._autoCycleTimer) {
94
103
  clearInterval(ctx._autoCycleTimer);
95
104
  ctx._autoCycleTimer = null;
96
- logger("MQTT auto-cycle cleared", "info");
105
+ logger("mqtt auto-cycle cleared", "info");
97
106
  }
107
+
98
108
  ctx._ending = true;
99
- unsubAll(() => {
100
- endQuietly(() => {
101
- logger("MQTT stopped", "info");
102
- cb();
103
- delayedReconnect();
104
- });
105
- });
109
+ unsubAll(() => endQuietly(() => {
110
+ logger("mqtt stopped", "info");
111
+ cb();
112
+ conf = mqttConf(ctx, conf);
113
+ if (conf.reconnectAfterStop) delayedReconnect();
114
+ }));
106
115
  }
107
116
  async stopListeningAsync() {
108
117
  return new Promise(resolve => { this.stopListening(resolve); });
109
118
  }
110
119
  }
120
+
111
121
  const msgEmitter = new MessageEmitter();
112
122
  globalCallback = callback || function (error, message) {
113
- if (error) { logger("MQTT emit error", "error"); return msgEmitter.emit("error", error); }
123
+ if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
114
124
  msgEmitter.emit("message", message);
115
125
  };
126
+
127
+ conf = mqttConf(ctx, conf);
116
128
  if (!ctx.firstListen) ctx.lastSeqId = null;
117
129
  ctx.syncToken = undefined;
118
130
  ctx.t_mqttCalled = false;
119
- if (ctx._autoCycleTimer) {
120
- clearInterval(ctx._autoCycleTimer);
121
- ctx._autoCycleTimer = null;
131
+
132
+ if (ctx._autoCycleTimer) { clearInterval(ctx._autoCycleTimer); ctx._autoCycleTimer = null; }
133
+ if (conf.cycleMs && conf.cycleMs > 0) {
134
+ ctx._autoCycleTimer = setInterval(forceCycle, conf.cycleMs);
135
+ logger(`mqtt auto-cycle enabled ${conf.cycleMs}ms`, "info");
136
+ } else {
137
+ logger("mqtt auto-cycle disabled", "info");
122
138
  }
123
- ctx._autoCycleTimer = setInterval(forceCycle, 60 * 60 * 1000);
124
- logger("MQTT auto-cycle enabled 3600000ms", "info");
139
+
125
140
  if (!ctx.firstListen || !ctx.lastSeqId) getSeqIDWrapper();
126
141
  else {
127
- logger("MQTT starting listenMqtt", "info");
142
+ logger("mqtt starting listenMqtt", "info");
128
143
  listenMqtt(defaultFuncs, api, ctx, globalCallback);
129
144
  }
145
+
130
146
  api.stopListening = msgEmitter.stopListening;
131
147
  api.stopListeningAsync = msgEmitter.stopListeningAsync;
132
148
  return msgEmitter;