@rithien/discord_bridge 0.3.3 → 0.3.5

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
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.5 - rithien fork
4
+
5
+ - **Fixed lost JOIN/LEAVE (and other actions) after a transient controller disconnect.** `onControllerConnectionEvent` flushed the offline `messageQueue` only on `connect`, but clusterio emits `resume` after a brief link drop (`connect` only happens on a new session, e.g. controller restart). Actions buffered during the drop window stayed stuck in the queue until the next full `connect` — symptom: missing `joined`/`left` messages on Discord, especially after chat inactivity (an idle host↔controller link drops unnoticed and resumes). Now flushes on `connect` **or** `resume` (same fix already shipped in comfy_adapter as FIX31 #2). This was the discord-bridge analog of #2 that was never ported.
6
+ - **Added Discord gateway lifecycle handlers** (`error`, `shardError`, `shardDisconnect`, `shardReconnecting`, `shardResume`, `invalidated`). Previously none were attached, so client/shard errors were swallowed (no log trace) and an `invalidated` session left the bot dead until a controller restart. `invalidated` now re-logs in via `connect()` (guarded against a reconnect loop); the rest add observability for diagnosing gateway drops. (discord.js v14 already auto-reconnects shards and `channel.send` goes over REST, so this is mainly recovery + visibility, not the primary fix.)
7
+ - **Wrapped `channel.send` in `handleInstanceAction` in try/catch** — a transient REST/permission/rate-limit error no longer silently drops the action or throws an unhandled rejection out of the event handler (consistent with the existing `.catch` in `onHostConnectionEvent`/`onInstanceStatusChanged`). Losses are now logged.
8
+
9
+ ## v0.3.4 - rithien fork
10
+
11
+ - `discordMessage` no longer assumes `message.member` is present (it can be `null` in discord.js v14 — author left the guild, partial/cache miss, gateway didn't deliver the member); falls back to the always-present `message.author`, so Discord→Factorio chat is no longer silently dropped with a `TypeError` (FIX31 #31).
12
+ - `fetchChannels` handles channel errors per-channel (log + continue) instead of re-throwing any `code !== 10003`; a single inaccessible channel (Missing Access/Permissions, network) no longer aborts plugin `init()` and leaves the bridge half-built (FIX31 #32).
13
+ - Added `allowedMentions: { parse: [] }` to instance status (`onInstanceStatusChanged`) and host connection (`onHostConnectionEvent`) notifications — previously only player-action messages set it. An instance/host name containing `@everyone`/`@here`/a role mention no longer triggers a real ping (FIX31 #33).
14
+ - Bounded the offline `messageQueue` (cap 1000, FIFO drop-oldest with a one-time warn) so a long controller disconnect during active play cannot grow it without limit nor flood Discord with the whole backlog on reconnect (FIX31 #30).
15
+
16
+ ## v0.3.3 - rithien fork
17
+
18
+ - Added `allowedMentions: { parse: [] }` to player-action `channel.send` calls so chat relayed from Factorio containing `@everyone`/`@here`/role mentions does not actually ping on Discord (security fix K3).
19
+
3
20
  ## v0.3.2 - rithien fork
4
21
 
5
22
  - Switched `handleDiscordChatEvent` from `/sc game.print('${text}')` to `/fp ${text}` (custom command exposed by the `factorio-polska` scenario in `commands/fp.lua`). Avoids the persistent "Cheats have been enabled" flag set by `/silent-command`, which permanently disables achievements on the save. Pair with `@rithien/factorio-polska` scenario v0.1.0+ (defines `/fp`). Without that scenario, the chat bridge will fail silently (RCON returns "Unknown command: fp"). To keep working on a vanilla scenario, pin to v0.3.1.
package/controller.js CHANGED
@@ -12,6 +12,7 @@ class ControllerPlugin extends BaseControllerPlugin {
12
12
  this.channelByInstance = new Map();
13
13
  this.instancesByChannel = new Map();
14
14
  this.fallbackChannel = null;
15
+ this.reconnecting = false; // guard przed pętlą reloginu po evencie "invalidated"
15
16
 
16
17
  this.controller.handle(InstanceActionEvent, this.handleInstanceAction.bind(this));
17
18
  await this.connect();
@@ -44,6 +45,39 @@ class ControllerPlugin extends BaseControllerPlugin {
44
45
  this.discordMessage(message).catch(err => { this.logger.error(`Unexpected error:\n${err.stack}`); });
45
46
  });
46
47
 
48
+ // Handlery cyklu życia gatewaya. Wcześniej żaden nie był podpięty — błędy klienta/shardu
49
+ // były połykane (brak śladu w logach), a "invalidated" zostawiał bota martwego do restartu
50
+ // kontrolera. discord.js v14 sam rekonektuje shardy (channel.send i tak idzie przez REST,
51
+ // odporny na blipy gatewaya) — te handlery służą głównie OBSERWOWALNOŚCI + recovery z invalidated.
52
+ this.client.on("error", (err) => {
53
+ this.logger.error(`Discord client error:\n${err?.stack ?? err}`);
54
+ });
55
+ this.client.on("shardError", (err) => {
56
+ this.logger.error(`Discord shard websocket error:\n${err?.stack ?? err}`);
57
+ });
58
+ this.client.on("shardDisconnect", (event, shardId) => {
59
+ this.logger.warn(`Discord shard ${shardId} disconnected (code ${event?.code}) — discord.js spróbuje reconnect`);
60
+ });
61
+ this.client.on("shardReconnecting", (shardId) => {
62
+ this.logger.info(`Discord shard ${shardId} reconnecting`);
63
+ });
64
+ this.client.on("shardResume", (shardId) => {
65
+ this.logger.info(`Discord shard ${shardId} resumed`);
66
+ });
67
+ this.client.on("invalidated", () => {
68
+ // Sesja nieodwracalnie unieważniona — discord.js zatrzymuje klienta i NIE wznawia sam.
69
+ // Bez reloginu bot zostaje martwy do restartu kontrolera. connect() robi destroy + nowy
70
+ // login; flaga reconnecting blokuje pętlę gdyby invalidated przyszło wielokrotnie.
71
+ this.logger.error("Discord session invalidated — ponowne logowanie");
72
+ if (this.reconnecting) {
73
+ return;
74
+ }
75
+ this.reconnecting = true;
76
+ this.connect()
77
+ .catch(err => { this.logger.error(`Relogin po invalidated nie powiódł się:\n${err?.stack ?? err}`); })
78
+ .finally(() => { this.reconnecting = false; });
79
+ });
80
+
47
81
  this.logger.info("Logging in to Discord");
48
82
  try {
49
83
  await this.client.login(token);
@@ -98,10 +132,15 @@ class ControllerPlugin extends BaseControllerPlugin {
98
132
  this.logger.error(`Channel ID ${id} was not found`);
99
133
  }
100
134
  } catch (err) {
101
- if (err.code !== 10003) { // Unknown channel
102
- throw err;
135
+ // FIX31 #32: odporność per-kanał. Re-throw wywracał fetchChannels() poza connect()/init()
136
+ // (fetchChannels jest wołane poza try/catch logowania), zostawiając most w stanie
137
+ // częściowo zbudowanym. Błędy inne niż 10003 to m.in. 50001 (Missing Access),
138
+ // 50013 (Missing Permissions) i sieciowe — logujemy i kontynuujemy pętlę.
139
+ if (err.code === 10003) { // Unknown channel
140
+ this.logger.error(`Channel ID ${id} was not found`);
141
+ } else {
142
+ this.logger.error(`Failed to fetch channel ID ${id}, skipping:\n${err.stack}`);
103
143
  }
104
- this.logger.error(`Channel ID ${id} was not found`);
105
144
  }
106
145
  }
107
146
 
@@ -157,10 +196,15 @@ class ControllerPlugin extends BaseControllerPlugin {
157
196
  return;
158
197
  }
159
198
 
199
+ // FIX31 #31: message.member bywa null (autor nie jest już członkiem gildii, partial/cache
200
+ // miss, gateway nie dostarczył member) — sięgamy po zawsze obecny message.author.
201
+ const displayName = message.member?.displayName ?? message.author.displayName ?? message.author.username;
202
+ const tag = message.author.tag ?? message.author.username;
203
+
160
204
  const template = this.controller.config.get("discord_bridge.discord_template");
161
205
  const content = template.replace(/(__display_name__|__username__|__content__)/g, (sub) => ({
162
- "__display_name__": message.member.displayName,
163
- "__username__": message.member.user.tag,
206
+ "__display_name__": displayName,
207
+ "__username__": tag,
164
208
  "__content__": message.cleanContent,
165
209
  }[sub]));
166
210
 
@@ -204,7 +248,12 @@ class ControllerPlugin extends BaseControllerPlugin {
204
248
  return;
205
249
  }
206
250
 
207
- await channel.send(this.formatMessage(template, hostName, instanceName, ""));
251
+ // allowedMentions: nazwa instancji/hosta trafia do szablonu — bez tego @everyone/@here w nazwie
252
+ // realnie pinguje (FIX31 #33, spójnie z akcjami graczy).
253
+ await channel.send({
254
+ content: this.formatMessage(template, hostName, instanceName, ""),
255
+ allowedMentions: { parse: [] },
256
+ });
208
257
  }
209
258
 
210
259
  onHostConnectionEvent(hostConnection, event) {
@@ -218,7 +267,8 @@ class ControllerPlugin extends BaseControllerPlugin {
218
267
  return;
219
268
  }
220
269
  const message = this.formatMessage(template, hostName, "", "");
221
- this.fallbackChannel.send(message).catch(
270
+ // allowedMentions: nazwa hosta trafia do szablonu — blokujemy ping z @everyone/@here (FIX31 #33).
271
+ this.fallbackChannel.send({ content: message, allowedMentions: { parse: [] } }).catch(
222
272
  err => { this.logger.error(`Unexpected error:\n${err.stack}`); }
223
273
  );
224
274
  }
@@ -261,7 +311,14 @@ class ControllerPlugin extends BaseControllerPlugin {
261
311
  }
262
312
  const hostName = this.getHostName(instance.config.get("instance.assigned_host"));
263
313
  const message = this.formatMessage(template, hostName, instanceName, content);
264
- await channel.send({ content: message, allowedMentions: { parse: [] } });
314
+ // try/catch: przejściowy błąd REST/permission/rate-limit nie może cicho zgubić akcji ani
315
+ // rzucić unhandled rejection z handlera eventu (spójnie z .catch w onHostConnectionEvent /
316
+ // onInstanceStatusChanged). Strata jest teraz widoczna w logach.
317
+ try {
318
+ await channel.send({ content: message, allowedMentions: { parse: [] } });
319
+ } catch (err) {
320
+ this.logger.error(`Failed to send ${action} to Discord:\n${err?.stack ?? err}`);
321
+ }
265
322
  }
266
323
  }
267
324
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "main.js": "static/main.42d03a9854919d3e8189.js",
3
- "discord_bridge.js": "static/discord_bridge.eb399a233189cc9de727.js",
3
+ "discord_bridge.js": "static/discord_bridge.19d9629215eba402e78a.js",
4
4
  "static/info_js.js": "static/info_js.131defcd9652f0e24d4a.js",
5
- "static/package_json.js": "static/package_json.eb40f2bb2cb67c86f9d0.js"
5
+ "static/package_json.js": "static/package_json.84f9beada3e5dfdb2a77.js"
6
6
  }
@@ -123,7 +123,7 @@ __webpack_require__.d(exports, {
123
123
  /******/ // This function allow to reference async chunks
124
124
  /******/ __webpack_require__.u = (chunkId) => {
125
125
  /******/ // return url for filenames based on template
126
- /******/ return "static/" + chunkId + "." + {"info_js":"131defcd9652f0e24d4a","package_json":"eb40f2bb2cb67c86f9d0"}[chunkId] + ".js";
126
+ /******/ return "static/" + chunkId + "." + {"info_js":"131defcd9652f0e24d4a","package_json":"84f9beada3e5dfdb2a77"}[chunkId] + ".js";
127
127
  /******/ };
128
128
  /******/ })();
129
129
  /******/
@@ -15,7 +15,7 @@
15
15
  \**********************/
16
16
  (module) {
17
17
 
18
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@rithien/discord_bridge","version":"0.3.3","description":"Clusterio plugin bridging chat between instances and Discord, with per-instance channel routing","main":"info.js","scripts":{"test":"echo \\"Error: no test specified\\" && exit 1","prepare":"webpack-cli --env production"},"keywords":["clusterio","clusterio-plugin","factorio","discord"],"author":"rithien <jacek@zaluzje.bialystok.pl>","license":"MIT","peerDependencies":{"@clusterio/lib":"^2.0.0-alpha.14"},"devDependencies":{"@clusterio/lib":"^2.0.0-alpha.14","@clusterio/web_ui":"^2.0.0-alpha.14","webpack":"^5.88.2","webpack-cli":"^5.1.4","webpack-merge":"^5.9.0"},"dependencies":{"discord.js":"^14.14.1"},"publishConfig":{"access":"public"}}');
18
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@rithien/discord_bridge","version":"0.3.5","description":"Clusterio plugin bridging chat between instances and Discord, with per-instance channel routing","main":"info.js","scripts":{"test":"echo \\"Error: no test specified\\" && exit 1","prepare":"webpack-cli --env production"},"keywords":["clusterio","clusterio-plugin","factorio","discord"],"author":"rithien <jacek@zaluzje.bialystok.pl>","license":"MIT","peerDependencies":{"@clusterio/lib":"^2.0.0-alpha.14"},"devDependencies":{"@clusterio/lib":"^2.0.0-alpha.14","@clusterio/web_ui":"^2.0.0-alpha.14","webpack":"^5.88.2","webpack-cli":"^5.1.4","webpack-merge":"^5.9.0"},"dependencies":{"discord.js":"^14.14.1"},"publishConfig":{"access":"public"}}');
19
19
 
20
20
  /***/ }
21
21
 
package/instance.js CHANGED
@@ -3,6 +3,11 @@ const { BaseInstancePlugin } = require("@clusterio/host");
3
3
 
4
4
  const { InstanceActionEvent, DiscordChatEvent } = require("./info.js");
5
5
 
6
+ // Twardy limit bufora akcji na czas rozłączenia z kontrolerem (FIX31 #30). Przy długim rozłączeniu
7
+ // i aktywnej rozgrywce (join/leave/chat) kolejka rosłaby bez ograniczeń (OOM hosta), a po reconnekcie
8
+ // cała zaległość zalałaby kanał Discord naraz. FIFO: po przekroczeniu odrzucamy najstarsze.
9
+ const MAX_MESSAGE_QUEUE = 1000;
10
+
6
11
 
7
12
  /**
8
13
  * Removes gps and train tags from messags
@@ -14,15 +19,22 @@ function removeTags(content) {
14
19
  class InstancePlugin extends BaseInstancePlugin {
15
20
  async init() {
16
21
  this.messageQueue = [];
22
+ this.queueOverflowWarned = false;
17
23
  this.instance.handle(DiscordChatEvent, this.handleDiscordChatEvent.bind(this));
18
24
  }
19
25
 
20
26
  onControllerConnectionEvent(event) {
21
- if (event === "connect") {
27
+ // clusterio emituje connect/drop/resume/close. Po krótkim lagu sieci leci "resume"
28
+ // (wznowienie sesji), NIE "connect" (ten tylko przy nowej sesji, np. restart kontrolera).
29
+ // Bez wariantu "resume" akcje z okna dropu (JOIN/LEAVE/CHAT) zostawały uwięzione w kolejce
30
+ // aż do następnego pełnego connect — objaw: gubione join/leave, zwłaszcza po bezczynności
31
+ // (ciche łącze pada niezauważone). Analog naprawionego FIX31 #2 w comfy_adapter.
32
+ if ((event === "connect" || event === "resume") && this.messageQueue.length > 0) {
22
33
  for (let [action, content] of this.messageQueue) {
23
34
  this.sendChat(action, content);
24
35
  }
25
36
  this.messageQueue = [];
37
+ this.queueOverflowWarned = false;
26
38
  }
27
39
  }
28
40
 
@@ -53,6 +65,13 @@ class InstancePlugin extends BaseInstancePlugin {
53
65
  this.sendChat(output.action, output.message);
54
66
  } else {
55
67
  this.messageQueue.push([output.action, output.message]);
68
+ if (this.messageQueue.length > MAX_MESSAGE_QUEUE) {
69
+ this.messageQueue.shift(); // odrzuć najstarsze (FIX31 #30)
70
+ if (!this.queueOverflowWarned) {
71
+ this.logger.warn(`discord_bridge: messageQueue przekroczyła ${MAX_MESSAGE_QUEUE} akcji (kontroler rozłączony) — odrzucam najstarsze`);
72
+ this.queueOverflowWarned = true;
73
+ }
74
+ }
56
75
  }
57
76
  }
58
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rithien/discord_bridge",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Clusterio plugin bridging chat between instances and Discord, with per-instance channel routing",
5
5
  "main": "info.js",
6
6
  "scripts": {