@lordbex/thelounge 4.4.4-blowfish → 4.5.0-blowfish-pre

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.
Files changed (140) hide show
  1. package/README.md +2 -2
  2. package/dist/defaults/config.js +31 -2
  3. package/dist/package.json +93 -91
  4. package/dist/server/client.js +188 -194
  5. package/dist/server/clientManager.js +65 -63
  6. package/dist/server/command-line/index.js +44 -43
  7. package/dist/server/command-line/install.js +37 -70
  8. package/dist/server/command-line/outdated.js +12 -17
  9. package/dist/server/command-line/start.js +25 -26
  10. package/dist/server/command-line/storage.js +26 -31
  11. package/dist/server/command-line/uninstall.js +16 -23
  12. package/dist/server/command-line/upgrade.js +20 -26
  13. package/dist/server/command-line/users/add.js +33 -40
  14. package/dist/server/command-line/users/edit.js +18 -24
  15. package/dist/server/command-line/users/index.js +12 -16
  16. package/dist/server/command-line/users/list.js +11 -39
  17. package/dist/server/command-line/users/remove.js +16 -22
  18. package/dist/server/command-line/users/reset.js +34 -35
  19. package/dist/server/command-line/utils.js +231 -87
  20. package/dist/server/config.js +61 -52
  21. package/dist/server/helper.js +29 -28
  22. package/dist/server/identification.js +39 -34
  23. package/dist/server/index.js +1 -3
  24. package/dist/server/log.js +19 -16
  25. package/dist/server/models/chan.js +36 -33
  26. package/dist/server/models/msg.js +15 -19
  27. package/dist/server/models/network.js +88 -86
  28. package/dist/server/models/prefix.js +4 -7
  29. package/dist/server/models/user.js +5 -10
  30. package/dist/server/path-helper.js +8 -0
  31. package/dist/server/plugins/auth/ldap.js +177 -112
  32. package/dist/server/plugins/auth/local.js +10 -15
  33. package/dist/server/plugins/auth.js +6 -35
  34. package/dist/server/plugins/changelog.js +30 -27
  35. package/dist/server/plugins/clientCertificate.js +33 -37
  36. package/dist/server/plugins/dev-server.js +15 -21
  37. package/dist/server/plugins/inputs/action.js +9 -14
  38. package/dist/server/plugins/inputs/away.js +1 -3
  39. package/dist/server/plugins/inputs/ban.js +9 -14
  40. package/dist/server/plugins/inputs/blow.js +9 -14
  41. package/dist/server/plugins/inputs/connect.js +5 -10
  42. package/dist/server/plugins/inputs/ctcp.js +7 -12
  43. package/dist/server/plugins/inputs/disconnect.js +1 -3
  44. package/dist/server/plugins/inputs/ignore.js +23 -29
  45. package/dist/server/plugins/inputs/ignorelist.js +12 -18
  46. package/dist/server/plugins/inputs/index.js +8 -34
  47. package/dist/server/plugins/inputs/invite.js +7 -12
  48. package/dist/server/plugins/inputs/kick.js +7 -12
  49. package/dist/server/plugins/inputs/kill.js +1 -3
  50. package/dist/server/plugins/inputs/list.js +1 -3
  51. package/dist/server/plugins/inputs/mode.js +10 -15
  52. package/dist/server/plugins/inputs/msg.js +13 -18
  53. package/dist/server/plugins/inputs/mute.js +9 -15
  54. package/dist/server/plugins/inputs/nick.js +9 -14
  55. package/dist/server/plugins/inputs/notice.js +5 -7
  56. package/dist/server/plugins/inputs/part.js +11 -16
  57. package/dist/server/plugins/inputs/quit.js +7 -13
  58. package/dist/server/plugins/inputs/rainbow.js +55 -0
  59. package/dist/server/plugins/inputs/raw.js +1 -3
  60. package/dist/server/plugins/inputs/rejoin.js +7 -12
  61. package/dist/server/plugins/inputs/topic.js +7 -12
  62. package/dist/server/plugins/inputs/whois.js +1 -3
  63. package/dist/server/plugins/irc-events/away.js +14 -20
  64. package/dist/server/plugins/irc-events/cap.js +16 -22
  65. package/dist/server/plugins/irc-events/chghost.js +14 -13
  66. package/dist/server/plugins/irc-events/connection.js +61 -63
  67. package/dist/server/plugins/irc-events/ctcp.js +22 -28
  68. package/dist/server/plugins/irc-events/error.js +20 -26
  69. package/dist/server/plugins/irc-events/help.js +7 -13
  70. package/dist/server/plugins/irc-events/info.js +7 -13
  71. package/dist/server/plugins/irc-events/invite.js +7 -13
  72. package/dist/server/plugins/irc-events/join.js +30 -27
  73. package/dist/server/plugins/irc-events/kick.js +21 -17
  74. package/dist/server/plugins/irc-events/link.js +75 -96
  75. package/dist/server/plugins/irc-events/list.js +23 -26
  76. package/dist/server/plugins/irc-events/message.js +46 -52
  77. package/dist/server/plugins/irc-events/mode.js +66 -63
  78. package/dist/server/plugins/irc-events/modelist.js +29 -35
  79. package/dist/server/plugins/irc-events/motd.js +10 -16
  80. package/dist/server/plugins/irc-events/names.js +3 -6
  81. package/dist/server/plugins/irc-events/nick.js +26 -23
  82. package/dist/server/plugins/irc-events/part.js +19 -15
  83. package/dist/server/plugins/irc-events/quit.js +17 -14
  84. package/dist/server/plugins/irc-events/sasl.js +9 -15
  85. package/dist/server/plugins/irc-events/spgroups.js +38 -0
  86. package/dist/server/plugins/irc-events/spjoin.js +52 -0
  87. package/dist/server/plugins/irc-events/topic.js +12 -18
  88. package/dist/server/plugins/irc-events/unhandled.js +12 -12
  89. package/dist/server/plugins/irc-events/welcome.js +7 -13
  90. package/dist/server/plugins/irc-events/whois.js +20 -24
  91. package/dist/server/plugins/massEventAggregator.js +214 -0
  92. package/dist/server/plugins/messageStorage/sqlite.js +322 -141
  93. package/dist/server/plugins/messageStorage/text.js +21 -26
  94. package/dist/server/plugins/packages/index.js +105 -74
  95. package/dist/server/plugins/packages/publicClient.js +7 -16
  96. package/dist/server/plugins/packages/themes.js +11 -16
  97. package/dist/server/plugins/storage.js +28 -33
  98. package/dist/server/plugins/sts.js +12 -17
  99. package/dist/server/plugins/uploader.js +40 -43
  100. package/dist/server/plugins/webpush.js +23 -51
  101. package/dist/server/server.js +318 -271
  102. package/dist/server/storageCleaner.js +29 -37
  103. package/dist/server/utils/fish.js +7 -14
  104. package/dist/shared/irc.js +3 -6
  105. package/dist/shared/linkify.js +7 -14
  106. package/dist/shared/types/chan.js +6 -9
  107. package/dist/shared/types/changelog.js +1 -2
  108. package/dist/shared/types/config.js +1 -2
  109. package/dist/shared/types/mention.js +1 -2
  110. package/dist/shared/types/msg.js +3 -5
  111. package/dist/shared/types/network.js +1 -2
  112. package/dist/shared/types/storage.js +1 -2
  113. package/dist/shared/types/user.js +1 -2
  114. package/index.js +14 -10
  115. package/package.json +93 -91
  116. package/public/css/style.css +9 -6
  117. package/public/css/style.css.map +1 -1
  118. package/public/fonts/font-awesome/fa-brands-400.ttf +0 -0
  119. package/public/fonts/font-awesome/fa-brands-400.woff2 +0 -0
  120. package/public/fonts/font-awesome/fa-duotone-900.ttf +0 -0
  121. package/public/fonts/font-awesome/fa-duotone-900.woff2 +0 -0
  122. package/public/fonts/font-awesome/fa-light-300.ttf +0 -0
  123. package/public/fonts/font-awesome/fa-light-300.woff2 +0 -0
  124. package/public/fonts/font-awesome/fa-regular-400.ttf +0 -0
  125. package/public/fonts/font-awesome/fa-regular-400.woff2 +0 -0
  126. package/public/fonts/font-awesome/fa-solid-900.ttf +0 -0
  127. package/public/fonts/font-awesome/fa-solid-900.woff2 +0 -0
  128. package/public/fonts/font-awesome/fa-thin-100.ttf +0 -0
  129. package/public/fonts/font-awesome/fa-thin-100.woff2 +0 -0
  130. package/public/fonts/font-awesome/fa-v4compatibility.ttf +0 -0
  131. package/public/fonts/font-awesome/fa-v4compatibility.woff2 +0 -0
  132. package/public/js/bundle.js +1 -1
  133. package/public/js/bundle.js.map +1 -1
  134. package/public/js/bundle.vendor.js +1 -1
  135. package/public/js/bundle.vendor.js.LICENSE.txt +24 -6
  136. package/public/js/bundle.vendor.js.map +1 -1
  137. package/public/service-worker.js +1 -1
  138. package/public/themes/default.css +1 -1
  139. package/public/themes/morning.css +1 -1
  140. package/dist/webpack.config.js +0 -224
@@ -1,13 +1,7 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const msg_1 = __importDefault(require("../../models/msg"));
7
- const msg_2 = require("../../../shared/types/msg");
8
- exports.default = (function (irc, network) {
9
- const client = this;
10
- irc.on("part", function (data) {
1
+ import Msg from "../../models/msg.js";
2
+ import { MessageType } from "../../../shared/types/msg.js";
3
+ export default (function (irc, network) {
4
+ irc.on("part", (data) => {
11
5
  if (!data.channel) {
12
6
  return;
13
7
  }
@@ -16,20 +10,30 @@ exports.default = (function (irc, network) {
16
10
  return;
17
11
  }
18
12
  const user = chan.getUser(data.nick);
19
- const msg = new msg_1.default({
20
- type: msg_2.MessageType.PART,
13
+ const msg = new Msg({
14
+ type: MessageType.PART,
21
15
  time: data.time,
22
16
  text: data.message || "",
23
17
  hostmask: data.ident + "@" + data.hostname,
24
18
  from: user,
25
19
  self: data.nick === irc.user.nick,
26
20
  });
27
- chan.pushMessage(client, msg);
21
+ // Self parts should not be buffered and need special handling
28
22
  if (data.nick === irc.user.nick) {
29
- client.part(network, chan);
23
+ chan.pushMessage(this, msg);
24
+ this.part(network, chan);
25
+ return;
30
26
  }
31
- else {
27
+ // User list update callback - executed regardless of buffering
28
+ const updateUserList = () => {
32
29
  chan.removeUser(user);
30
+ };
31
+ // Try to process through mass event aggregator
32
+ const wasBuffered = this.massEventAggregator.processMessage(network, chan, msg, updateUserList);
33
+ if (!wasBuffered) {
34
+ // Not in mass event mode - process normally
35
+ chan.pushMessage(this, msg);
36
+ updateUserList();
33
37
  }
34
38
  });
35
39
  });
@@ -1,27 +1,30 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const msg_1 = __importDefault(require("../../models/msg"));
7
- const msg_2 = require("../../../shared/types/msg");
8
- exports.default = (function (irc, network) {
9
- const client = this;
10
- irc.on("quit", function (data) {
1
+ import Msg from "../../models/msg.js";
2
+ import { MessageType } from "../../../shared/types/msg.js";
3
+ export default (function (irc, network) {
4
+ irc.on("quit", (data) => {
11
5
  network.channels.forEach((chan) => {
12
6
  const user = chan.findUser(data.nick);
13
7
  if (typeof user === "undefined") {
14
8
  return;
15
9
  }
16
- const msg = new msg_1.default({
10
+ const msg = new Msg({
17
11
  time: data.time,
18
- type: msg_2.MessageType.QUIT,
12
+ type: MessageType.QUIT,
19
13
  text: data.message || "",
20
14
  hostmask: data.ident + "@" + data.hostname,
21
15
  from: user,
22
16
  });
23
- chan.pushMessage(client, msg);
24
- chan.removeUser(user);
17
+ // User list update callback - executed regardless of buffering
18
+ const updateUserList = () => {
19
+ chan.removeUser(user);
20
+ };
21
+ // Try to process through mass event aggregator
22
+ const wasBuffered = this.massEventAggregator.processMessage(network, chan, msg, updateUserList);
23
+ if (!wasBuffered) {
24
+ // Not in mass event mode - process normally
25
+ chan.pushMessage(this, msg);
26
+ updateUserList();
27
+ }
25
28
  });
26
29
  // If user with the nick we are trying to keep has quit, try to get this nick
27
30
  if (network.keepNick === data.nick) {
@@ -1,26 +1,20 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const msg_1 = __importDefault(require("../../models/msg"));
7
- const msg_2 = require("../../../shared/types/msg");
8
- exports.default = (function (irc, network) {
9
- const client = this;
1
+ import Msg from "../../models/msg.js";
2
+ import { MessageType } from "../../../shared/types/msg.js";
3
+ export default (function (irc, network) {
10
4
  irc.on("loggedin", (data) => {
11
5
  const lobby = network.getLobby();
12
- const msg = new msg_1.default({
13
- type: msg_2.MessageType.LOGIN,
6
+ const msg = new Msg({
7
+ type: MessageType.LOGIN,
14
8
  text: "Logged in as: " + data.account,
15
9
  });
16
- lobby.pushMessage(client, msg, true);
10
+ lobby.pushMessage(this, msg, true);
17
11
  });
18
12
  irc.on("loggedout", () => {
19
13
  const lobby = network.getLobby();
20
- const msg = new msg_1.default({
21
- type: msg_2.MessageType.LOGOUT,
14
+ const msg = new Msg({
15
+ type: MessageType.LOGOUT,
22
16
  text: "Logged out",
23
17
  });
24
- lobby.pushMessage(client, msg, true);
18
+ lobby.pushMessage(this, msg, true);
25
19
  });
26
20
  });
@@ -0,0 +1,38 @@
1
+ import log from "../../log.js";
2
+ export default (function (irc, network) {
3
+ // Handle SPGROUPS command from seedpool/enhanced capable servers
4
+ // Format: :SeedServ SPGROUPS #channel :{"groups":[{"name":"Sysop","users":["admin1"]}, ...]}
5
+ irc.on("unknown command", (command) => {
6
+ if (command.command !== "SPGROUPS") {
7
+ return;
8
+ }
9
+ const channelName = command.params[0];
10
+ const jsonPayload = command.params[1];
11
+ if (!channelName || !jsonPayload) {
12
+ log.warn("SPGROUPS: Missing channel or payload");
13
+ return;
14
+ }
15
+ const chan = network.getChannel(channelName);
16
+ if (!chan) {
17
+ log.warn(`SPGROUPS: Channel ${channelName} not found`);
18
+ return;
19
+ }
20
+ try {
21
+ const data = JSON.parse(jsonPayload);
22
+ if (!data.groups || !Array.isArray(data.groups)) {
23
+ log.warn("SPGROUPS: Invalid payload format, expected {groups: [...]}");
24
+ return;
25
+ }
26
+ // Store groups on the channel, sorted by position (highest first)
27
+ chan.groups = data.groups.sort((a, b) => b.position - a.position);
28
+ // Emit to client
29
+ this.emit("channel:groups", {
30
+ chan: chan.id,
31
+ groups: chan.groups,
32
+ });
33
+ }
34
+ catch (err) {
35
+ log.error(`SPGROUPS: Failed to parse JSON payload: ${String(err)}`);
36
+ }
37
+ });
38
+ });
@@ -0,0 +1,52 @@
1
+ import log from "../../log.js";
2
+ export default (function (irc, network) {
3
+ // Handle SPJOIN command from seedpool/enhanced capable servers
4
+ // Format: :SeedServ SPJOIN #channel nickname :GroupName
5
+ irc.on("unknown command", (command) => {
6
+ if (command.command !== "SPJOIN") {
7
+ return;
8
+ }
9
+ const channelName = command.params[0];
10
+ const nickname = command.params[1];
11
+ const groupName = command.params[2];
12
+ if (!channelName || !nickname || !groupName) {
13
+ log.warn("SPJOIN: Missing channel, nickname, or group");
14
+ return;
15
+ }
16
+ const chan = network.getChannel(channelName);
17
+ if (!chan) {
18
+ log.warn(`SPJOIN: Channel ${channelName} not found`);
19
+ return;
20
+ }
21
+ if (!chan.groups) {
22
+ chan.groups = [];
23
+ }
24
+ // Remove user from any existing group (in case of group change)
25
+ for (const group of chan.groups) {
26
+ const lowerUsers = group.users.map((u) => u.toLowerCase());
27
+ const userIndex = lowerUsers.indexOf(nickname.toLowerCase());
28
+ if (userIndex !== -1) {
29
+ group.users.splice(userIndex, 1);
30
+ }
31
+ }
32
+ // Find the target group or create it
33
+ let targetGroup = chan.groups.find((g) => g.name === groupName);
34
+ if (!targetGroup) {
35
+ // Find the lowest existing position and go below it
36
+ const lowestPosition = chan.groups.length > 0 ? Math.min(...chan.groups.map((g) => g.position)) - 1 : 0;
37
+ targetGroup = { name: groupName, position: lowestPosition, users: [] };
38
+ chan.groups.push(targetGroup);
39
+ }
40
+ // Add user to the group
41
+ if (!targetGroup.users.map((u) => u.toLowerCase()).includes(nickname.toLowerCase())) {
42
+ targetGroup.users.push(nickname);
43
+ }
44
+ // Sort groups by position (highest first) before emitting
45
+ chan.groups.sort((a, b) => b.position - a.position);
46
+ // Emit updated groups to client
47
+ this.emit("channel:groups", {
48
+ chan: chan.id,
49
+ groups: chan.groups,
50
+ });
51
+ });
52
+ });
@@ -1,42 +1,36 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const msg_1 = __importDefault(require("../../models/msg"));
7
- const msg_2 = require("../../../shared/types/msg");
8
- exports.default = (function (irc, network) {
9
- const client = this;
10
- irc.on("topic", function (data) {
1
+ import Msg from "../../models/msg.js";
2
+ import { MessageType } from "../../../shared/types/msg.js";
3
+ export default (function (irc, network) {
4
+ irc.on("topic", (data) => {
11
5
  const chan = network.getChannel(data.channel);
12
6
  if (typeof chan === "undefined") {
13
7
  return;
14
8
  }
15
- const msg = new msg_1.default({
9
+ const msg = new Msg({
16
10
  time: data.time,
17
- type: msg_2.MessageType.TOPIC,
11
+ type: MessageType.TOPIC,
18
12
  from: data.nick && chan.getUser(data.nick),
19
13
  text: data.topic,
20
14
  self: data.nick === irc.user.nick,
21
15
  });
22
- chan.pushMessage(client, msg);
16
+ chan.pushMessage(this, msg);
23
17
  chan.topic = data.topic;
24
- client.emit("topic", {
18
+ this.emit("topic", {
25
19
  chan: chan.id,
26
20
  topic: chan.topic,
27
21
  });
28
22
  });
29
- irc.on("topicsetby", function (data) {
23
+ irc.on("topicsetby", (data) => {
30
24
  const chan = network.getChannel(data.channel);
31
25
  if (typeof chan === "undefined") {
32
26
  return;
33
27
  }
34
- const msg = new msg_1.default({
35
- type: msg_2.MessageType.TOPIC_SET_BY,
28
+ const msg = new Msg({
29
+ type: MessageType.TOPIC_SET_BY,
36
30
  from: chan.getUser(data.nick),
37
31
  when: new Date(data.when * 1000),
38
32
  self: data.nick === irc.user.nick,
39
33
  });
40
- chan.pushMessage(client, msg);
34
+ chan.pushMessage(this, msg);
41
35
  });
42
36
  });
@@ -1,13 +1,13 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const msg_1 = __importDefault(require("../../models/msg"));
7
- const msg_2 = require("../../../shared/types/msg");
8
- exports.default = (function (irc, network) {
9
- const client = this;
10
- irc.on("unknown command", function (command) {
1
+ import Msg from "../../models/msg.js";
2
+ import { MessageType } from "../../../shared/types/msg.js";
3
+ // Commands handled by other plugins - don't display as unhandled
4
+ const handledCommands = new Set(["SPGROUPS", "SPJOIN"]);
5
+ export default (function (irc, network) {
6
+ irc.on("unknown command", (command) => {
7
+ // Skip commands that are handled by other plugins
8
+ if (handledCommands.has(command.command)) {
9
+ return;
10
+ }
11
11
  let target = network.getLobby();
12
12
  // Do not display users own name
13
13
  if (command.params.length > 0 && command.params[0] === network.irc.user.nick) {
@@ -22,8 +22,8 @@ exports.default = (function (irc, network) {
22
22
  target = channel;
23
23
  }
24
24
  }
25
- target.pushMessage(client, new msg_1.default({
26
- type: msg_2.MessageType.UNHANDLED,
25
+ target.pushMessage(this, new Msg({
26
+ type: MessageType.UNHANDLED,
27
27
  command: command.command,
28
28
  params: command.params,
29
29
  }), true);
@@ -1,20 +1,14 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const msg_1 = __importDefault(require("../../models/msg"));
7
- exports.default = (function (irc, network) {
8
- const client = this;
9
- irc.on("registered", function (data) {
1
+ import Msg from "../../models/msg.js";
2
+ export default (function (irc, network) {
3
+ irc.on("registered", (data) => {
10
4
  network.setNick(data.nick);
11
5
  const lobby = network.getLobby();
12
- const msg = new msg_1.default({
6
+ const msg = new Msg({
13
7
  text: "You're now known as " + data.nick,
14
8
  });
15
- lobby.pushMessage(client, msg);
16
- client.save();
17
- client.emit("nick", {
9
+ lobby.pushMessage(this, msg);
10
+ this.save();
11
+ this.emit("nick", {
18
12
  network: network.uuid,
19
13
  nick: data.nick,
20
14
  });
@@ -1,17 +1,13 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const msg_1 = __importDefault(require("../../models/msg"));
7
- const msg_2 = require("../../../shared/types/msg");
8
- const chan_1 = require("../../../shared/types/chan");
9
- exports.default = (function (irc, network) {
10
- const client = this;
11
- irc.on("whois", handleWhois);
1
+ import Msg from "../../models/msg.js";
2
+ import { MessageType } from "../../../shared/types/msg.js";
3
+ import { ChanType } from "../../../shared/types/chan.js";
4
+ export default (function (irc, network) {
5
+ irc.on("whois", (data) => {
6
+ handleWhois.call(this, data);
7
+ });
12
8
  irc.on("whowas", (data) => {
13
9
  data.whowas = true;
14
- handleWhois(data);
10
+ handleWhois.call(this, data);
15
11
  });
16
12
  function handleWhois(data) {
17
13
  let chan = network.getChannel(data.nick);
@@ -21,37 +17,37 @@ exports.default = (function (irc, network) {
21
17
  chan = network.getLobby();
22
18
  }
23
19
  else {
24
- chan = client.createChannel({
25
- type: chan_1.ChanType.QUERY,
20
+ chan = this.createChannel({
21
+ type: ChanType.QUERY,
26
22
  name: data.nick,
27
23
  });
28
- client.emit("join", {
24
+ this.emit("join", {
29
25
  network: network.uuid,
30
26
  chan: chan.getFilteredClone(true),
31
27
  shouldOpen: true,
32
28
  index: network.addChannel(chan),
33
29
  });
34
- chan.loadMessages(client, network);
35
- client.save();
30
+ chan.loadMessages(this, network);
31
+ this.save();
36
32
  }
37
33
  }
38
34
  let msg;
39
35
  if (data.error) {
40
- msg = new msg_1.default({
41
- type: msg_2.MessageType.ERROR,
36
+ msg = new Msg({
37
+ type: MessageType.ERROR,
42
38
  text: "No such nick: " + data.nick,
43
39
  });
44
40
  }
45
41
  else {
46
42
  // Absolute datetime in milliseconds since nick is idle
47
- data.idleTime = Date.now() - data.idle * 1000;
43
+ data.idleTime = Date.now() - (data.idle ?? 0) * 1000;
48
44
  // Absolute datetime in milliseconds when nick logged on.
49
- data.logonTime = data.logon * 1000;
50
- msg = new msg_1.default({
51
- type: msg_2.MessageType.WHOIS,
45
+ data.logonTime = (data.logon ?? 0) * 1000;
46
+ msg = new Msg({
47
+ type: MessageType.WHOIS,
52
48
  whois: data,
53
49
  });
54
50
  }
55
- chan.pushMessage(client, msg);
51
+ chan.pushMessage(this, msg);
56
52
  }
57
53
  });
@@ -0,0 +1,214 @@
1
+ import Msg from "../models/msg.js";
2
+ import Config from "../config.js";
3
+ import log from "../log.js";
4
+ import { MessageType } from "../../shared/types/msg.js";
5
+ import { condensedTypes } from "../../shared/irc.js";
6
+ class MassEventAggregator {
7
+ channelStates = new Map();
8
+ client;
9
+ constructor(client) {
10
+ this.client = client;
11
+ }
12
+ getOrCreateState(chanId) {
13
+ if (!this.channelStates.has(chanId)) {
14
+ this.channelStates.set(chanId, {
15
+ isActive: false,
16
+ buffer: [],
17
+ preBuffer: [],
18
+ startTime: null,
19
+ cooldownTimer: null,
20
+ maxDurationTimer: null,
21
+ recentTimestamps: [],
22
+ });
23
+ }
24
+ return this.channelStates.get(chanId);
25
+ }
26
+ /**
27
+ * Process an incoming status message.
28
+ * Returns true if the message was buffered (mass event active).
29
+ * Returns false if the message should be sent normally.
30
+ */
31
+ processMessage(network, chan, msg, userUpdateCallback) {
32
+ // Check if mass event detection is enabled
33
+ if (!Config.values.massEventDetection?.enable) {
34
+ return false;
35
+ }
36
+ // Only process condensable message types
37
+ if (!condensedTypes.has(msg.type)) {
38
+ return false;
39
+ }
40
+ // Never buffer self messages or highlights
41
+ if (msg.self || msg.highlight) {
42
+ return false;
43
+ }
44
+ const config = Config.values.massEventDetection;
45
+ const state = this.getOrCreateState(chan.id);
46
+ const now = Date.now();
47
+ // Update recent timestamps (sliding window)
48
+ state.recentTimestamps.push(now);
49
+ const windowStart = now - config.windowMs;
50
+ state.recentTimestamps = state.recentTimestamps.filter((t) => t > windowStart);
51
+ // Check if we should activate mass event mode
52
+ if (!state.isActive) {
53
+ // Track message in preBuffer for accurate counting when activation happens
54
+ state.preBuffer.push({ msg, timestamp: now });
55
+ // Clean old messages from preBuffer (keep only within window)
56
+ state.preBuffer = state.preBuffer.filter((m) => m.timestamp > windowStart);
57
+ if (state.recentTimestamps.length >= config.threshold) {
58
+ log.debug(`MassEvent: ACTIVATING for ${chan.name} (${state.recentTimestamps.length} msgs, ${state.preBuffer.length} in preBuffer)`);
59
+ // Move preBuffer to main buffer when activating
60
+ state.buffer = [...state.preBuffer];
61
+ state.preBuffer = [];
62
+ this.activateMassEvent(state, chan, network, now);
63
+ // Execute user update callback
64
+ if (userUpdateCallback) {
65
+ userUpdateCallback();
66
+ }
67
+ // Reset cooldown timer
68
+ this.resetCooldownTimer(state, chan, network);
69
+ return true; // Message was captured in preBuffer, now in main buffer
70
+ }
71
+ return false; // Not in mass event mode, process normally
72
+ }
73
+ // We're in mass event mode - buffer the message
74
+ state.buffer.push({ msg, timestamp: now });
75
+ // Execute user update callback (for real-time user list updates)
76
+ if (userUpdateCallback) {
77
+ userUpdateCallback();
78
+ }
79
+ // Reset cooldown timer
80
+ this.resetCooldownTimer(state, chan, network);
81
+ return true;
82
+ }
83
+ activateMassEvent(state, chan, network, now) {
84
+ state.isActive = true;
85
+ state.startTime = now;
86
+ const config = Config.values.massEventDetection;
87
+ // Set maximum duration timer
88
+ state.maxDurationTimer = setTimeout(() => {
89
+ this.endMassEvent(state, chan, network);
90
+ }, config.maxDurationMs);
91
+ }
92
+ resetCooldownTimer(state, chan, network) {
93
+ if (state.cooldownTimer) {
94
+ clearTimeout(state.cooldownTimer);
95
+ }
96
+ const config = Config.values.massEventDetection;
97
+ state.cooldownTimer = setTimeout(() => {
98
+ this.endMassEvent(state, chan, network);
99
+ }, config.cooldownMs);
100
+ }
101
+ endMassEvent(state, chan, network) {
102
+ if (!state.isActive) {
103
+ return;
104
+ }
105
+ log.debug(`MassEvent: ENDING for ${chan.name} (${state.buffer.length} buffered msgs)`);
106
+ // Clear timers
107
+ if (state.cooldownTimer) {
108
+ clearTimeout(state.cooldownTimer);
109
+ state.cooldownTimer = null;
110
+ }
111
+ if (state.maxDurationTimer) {
112
+ clearTimeout(state.maxDurationTimer);
113
+ state.maxDurationTimer = null;
114
+ }
115
+ // Generate summary
116
+ const summary = this.generateSummary(state);
117
+ log.debug(`MassEvent: Summary - joins=${summary.joins} parts=${summary.parts} quits=${summary.quits}`);
118
+ // Create summary message
119
+ const summaryMsg = new Msg({
120
+ type: MessageType.MASS_EVENT,
121
+ time: new Date(),
122
+ massEventSummary: summary,
123
+ });
124
+ // Push summary message through normal channel
125
+ chan.pushMessage(this.client, summaryMsg);
126
+ // Reset state
127
+ state.isActive = false;
128
+ state.buffer = [];
129
+ state.preBuffer = [];
130
+ state.startTime = null;
131
+ state.recentTimestamps = [];
132
+ // Refresh user list if configured
133
+ if (Config.values.massEventDetection.refreshNamesAfter && network.irc) {
134
+ network.irc.raw("NAMES", chan.name);
135
+ }
136
+ }
137
+ generateSummary(state) {
138
+ const now = Date.now();
139
+ const summary = {
140
+ joins: 0,
141
+ parts: 0,
142
+ quits: 0,
143
+ modes: 0,
144
+ nicks: 0,
145
+ kicks: 0,
146
+ chghosts: 0,
147
+ away: 0,
148
+ back: 0,
149
+ duration: now - (state.startTime || now),
150
+ startTime: new Date(state.startTime || now),
151
+ endTime: new Date(),
152
+ };
153
+ for (const { msg } of state.buffer) {
154
+ switch (msg.type) {
155
+ case MessageType.JOIN:
156
+ summary.joins++;
157
+ break;
158
+ case MessageType.PART:
159
+ summary.parts++;
160
+ break;
161
+ case MessageType.QUIT:
162
+ summary.quits++;
163
+ break;
164
+ case MessageType.MODE:
165
+ // Count individual mode changes
166
+ const modeText = msg.text || "";
167
+ const modeChanges = modeText
168
+ .split(" ")[0]
169
+ .split("")
170
+ .filter((c) => c !== "+" && c !== "-").length;
171
+ summary.modes += modeChanges || 1;
172
+ break;
173
+ case MessageType.NICK:
174
+ summary.nicks++;
175
+ break;
176
+ case MessageType.KICK:
177
+ summary.kicks++;
178
+ break;
179
+ case MessageType.CHGHOST:
180
+ summary.chghosts++;
181
+ break;
182
+ case MessageType.AWAY:
183
+ summary.away++;
184
+ break;
185
+ case MessageType.BACK:
186
+ summary.back++;
187
+ break;
188
+ }
189
+ }
190
+ return summary;
191
+ }
192
+ /**
193
+ * Clean up state for a channel (e.g., when leaving)
194
+ */
195
+ cleanup(chanId) {
196
+ const state = this.channelStates.get(chanId);
197
+ if (state) {
198
+ if (state.cooldownTimer) {
199
+ clearTimeout(state.cooldownTimer);
200
+ }
201
+ if (state.maxDurationTimer) {
202
+ clearTimeout(state.maxDurationTimer);
203
+ }
204
+ this.channelStates.delete(chanId);
205
+ }
206
+ }
207
+ /**
208
+ * Check if mass event is active for a channel
209
+ */
210
+ isActive(chanId) {
211
+ return this.channelStates.get(chanId)?.isActive || false;
212
+ }
213
+ }
214
+ export default MassEventAggregator;