@lordbex/thelounge 4.4.3-blowfish

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 (148) hide show
  1. package/.thelounge_home +1 -0
  2. package/LICENSE +22 -0
  3. package/README.md +95 -0
  4. package/client/index.html.tpl +69 -0
  5. package/dist/defaults/config.js +465 -0
  6. package/dist/package.json +174 -0
  7. package/dist/server/client.js +678 -0
  8. package/dist/server/clientManager.js +220 -0
  9. package/dist/server/command-line/index.js +85 -0
  10. package/dist/server/command-line/install.js +123 -0
  11. package/dist/server/command-line/outdated.js +30 -0
  12. package/dist/server/command-line/start.js +34 -0
  13. package/dist/server/command-line/storage.js +103 -0
  14. package/dist/server/command-line/uninstall.js +40 -0
  15. package/dist/server/command-line/upgrade.js +64 -0
  16. package/dist/server/command-line/users/add.js +67 -0
  17. package/dist/server/command-line/users/edit.js +39 -0
  18. package/dist/server/command-line/users/index.js +17 -0
  19. package/dist/server/command-line/users/list.js +53 -0
  20. package/dist/server/command-line/users/remove.js +37 -0
  21. package/dist/server/command-line/users/reset.js +64 -0
  22. package/dist/server/command-line/utils.js +177 -0
  23. package/dist/server/config.js +138 -0
  24. package/dist/server/helper.js +161 -0
  25. package/dist/server/identification.js +139 -0
  26. package/dist/server/index.js +3 -0
  27. package/dist/server/log.js +35 -0
  28. package/dist/server/models/chan.js +275 -0
  29. package/dist/server/models/msg.js +92 -0
  30. package/dist/server/models/network.js +546 -0
  31. package/dist/server/models/prefix.js +31 -0
  32. package/dist/server/models/user.js +42 -0
  33. package/dist/server/plugins/auth/ldap.js +188 -0
  34. package/dist/server/plugins/auth/local.js +41 -0
  35. package/dist/server/plugins/auth.js +70 -0
  36. package/dist/server/plugins/changelog.js +103 -0
  37. package/dist/server/plugins/clientCertificate.js +115 -0
  38. package/dist/server/plugins/dev-server.js +33 -0
  39. package/dist/server/plugins/inputs/action.js +54 -0
  40. package/dist/server/plugins/inputs/away.js +20 -0
  41. package/dist/server/plugins/inputs/ban.js +45 -0
  42. package/dist/server/plugins/inputs/blow.js +44 -0
  43. package/dist/server/plugins/inputs/connect.js +41 -0
  44. package/dist/server/plugins/inputs/ctcp.js +29 -0
  45. package/dist/server/plugins/inputs/disconnect.js +15 -0
  46. package/dist/server/plugins/inputs/ignore.js +74 -0
  47. package/dist/server/plugins/inputs/ignorelist.js +50 -0
  48. package/dist/server/plugins/inputs/index.js +105 -0
  49. package/dist/server/plugins/inputs/invite.js +31 -0
  50. package/dist/server/plugins/inputs/kick.js +26 -0
  51. package/dist/server/plugins/inputs/kill.js +13 -0
  52. package/dist/server/plugins/inputs/list.js +12 -0
  53. package/dist/server/plugins/inputs/mode.js +55 -0
  54. package/dist/server/plugins/inputs/msg.js +106 -0
  55. package/dist/server/plugins/inputs/mute.js +56 -0
  56. package/dist/server/plugins/inputs/nick.js +55 -0
  57. package/dist/server/plugins/inputs/notice.js +42 -0
  58. package/dist/server/plugins/inputs/part.js +46 -0
  59. package/dist/server/plugins/inputs/quit.js +27 -0
  60. package/dist/server/plugins/inputs/raw.js +13 -0
  61. package/dist/server/plugins/inputs/rejoin.js +25 -0
  62. package/dist/server/plugins/inputs/topic.js +24 -0
  63. package/dist/server/plugins/inputs/whois.js +19 -0
  64. package/dist/server/plugins/irc-events/away.js +59 -0
  65. package/dist/server/plugins/irc-events/cap.js +62 -0
  66. package/dist/server/plugins/irc-events/chghost.js +29 -0
  67. package/dist/server/plugins/irc-events/connection.js +152 -0
  68. package/dist/server/plugins/irc-events/ctcp.js +72 -0
  69. package/dist/server/plugins/irc-events/error.js +80 -0
  70. package/dist/server/plugins/irc-events/help.js +21 -0
  71. package/dist/server/plugins/irc-events/info.js +21 -0
  72. package/dist/server/plugins/irc-events/invite.js +27 -0
  73. package/dist/server/plugins/irc-events/join.js +53 -0
  74. package/dist/server/plugins/irc-events/kick.js +39 -0
  75. package/dist/server/plugins/irc-events/link.js +442 -0
  76. package/dist/server/plugins/irc-events/list.js +47 -0
  77. package/dist/server/plugins/irc-events/message.js +187 -0
  78. package/dist/server/plugins/irc-events/mode.js +124 -0
  79. package/dist/server/plugins/irc-events/modelist.js +67 -0
  80. package/dist/server/plugins/irc-events/motd.js +29 -0
  81. package/dist/server/plugins/irc-events/names.js +21 -0
  82. package/dist/server/plugins/irc-events/nick.js +45 -0
  83. package/dist/server/plugins/irc-events/part.js +35 -0
  84. package/dist/server/plugins/irc-events/quit.js +32 -0
  85. package/dist/server/plugins/irc-events/sasl.js +26 -0
  86. package/dist/server/plugins/irc-events/topic.js +42 -0
  87. package/dist/server/plugins/irc-events/unhandled.js +31 -0
  88. package/dist/server/plugins/irc-events/welcome.js +22 -0
  89. package/dist/server/plugins/irc-events/whois.js +57 -0
  90. package/dist/server/plugins/messageStorage/sqlite.js +454 -0
  91. package/dist/server/plugins/messageStorage/text.js +124 -0
  92. package/dist/server/plugins/packages/index.js +200 -0
  93. package/dist/server/plugins/packages/publicClient.js +66 -0
  94. package/dist/server/plugins/packages/themes.js +61 -0
  95. package/dist/server/plugins/storage.js +88 -0
  96. package/dist/server/plugins/sts.js +85 -0
  97. package/dist/server/plugins/uploader.js +267 -0
  98. package/dist/server/plugins/webpush.js +99 -0
  99. package/dist/server/server.js +857 -0
  100. package/dist/server/storageCleaner.js +131 -0
  101. package/dist/server/utils/fish.js +432 -0
  102. package/dist/shared/irc.js +19 -0
  103. package/dist/shared/linkify.js +81 -0
  104. package/dist/shared/types/chan.js +22 -0
  105. package/dist/shared/types/changelog.js +2 -0
  106. package/dist/shared/types/config.js +2 -0
  107. package/dist/shared/types/mention.js +2 -0
  108. package/dist/shared/types/msg.js +34 -0
  109. package/dist/shared/types/network.js +2 -0
  110. package/dist/shared/types/storage.js +2 -0
  111. package/dist/shared/types/user.js +2 -0
  112. package/dist/webpack.config.js +224 -0
  113. package/index.js +38 -0
  114. package/package.json +174 -0
  115. package/public/audio/pop.wav +0 -0
  116. package/public/css/style.css +12 -0
  117. package/public/css/style.css.map +1 -0
  118. package/public/favicon.ico +0 -0
  119. package/public/fonts/fa-solid-900.woff +0 -0
  120. package/public/fonts/fa-solid-900.woff2 +0 -0
  121. package/public/img/favicon-alerted.ico +0 -0
  122. package/public/img/icon-alerted-black-transparent-bg-72x72px.png +0 -0
  123. package/public/img/icon-alerted-grey-bg-192x192px.png +0 -0
  124. package/public/img/icon-black-transparent-bg.svg +1 -0
  125. package/public/img/logo-grey-bg-120x120px.png +0 -0
  126. package/public/img/logo-grey-bg-152x152px.png +0 -0
  127. package/public/img/logo-grey-bg-167x167px.png +0 -0
  128. package/public/img/logo-grey-bg-180x180px.png +0 -0
  129. package/public/img/logo-grey-bg-192x192px.png +0 -0
  130. package/public/img/logo-grey-bg-512x512px.png +0 -0
  131. package/public/img/logo-grey-bg.svg +1 -0
  132. package/public/img/logo-horizontal-transparent-bg-inverted.svg +1 -0
  133. package/public/img/logo-horizontal-transparent-bg.svg +1 -0
  134. package/public/img/logo-transparent-bg-inverted.svg +1 -0
  135. package/public/img/logo-transparent-bg.svg +1 -0
  136. package/public/img/logo-vertical-transparent-bg-inverted.svg +1 -0
  137. package/public/img/logo-vertical-transparent-bg.svg +1 -0
  138. package/public/js/bundle.js +2 -0
  139. package/public/js/bundle.js.map +1 -0
  140. package/public/js/bundle.vendor.js +3 -0
  141. package/public/js/bundle.vendor.js.LICENSE.txt +18 -0
  142. package/public/js/bundle.vendor.js.map +1 -0
  143. package/public/js/loading-error-handlers.js +1 -0
  144. package/public/robots.txt +2 -0
  145. package/public/service-worker.js +1 -0
  146. package/public/thelounge.webmanifest +53 -0
  147. package/public/themes/default.css +35 -0
  148. package/public/themes/morning.css +183 -0
@@ -0,0 +1,454 @@
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
+ exports.newRollbacks = exports.necessaryMigrations = exports.rollbacks = exports.migrations = exports.currentSchemaVersion = void 0;
7
+ const log_1 = __importDefault(require("../../log"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const config_1 = __importDefault(require("../../config"));
11
+ const msg_1 = __importDefault(require("../../models/msg"));
12
+ const helper_1 = __importDefault(require("../../helper"));
13
+ // TODO; type
14
+ let sqlite3;
15
+ try {
16
+ sqlite3 = require("sqlite3");
17
+ }
18
+ catch (e) {
19
+ config_1.default.values.messageStorage = config_1.default.values.messageStorage.filter((item) => item !== "sqlite");
20
+ log_1.default.error("Unable to load sqlite3 module. See https://github.com/mapbox/node-sqlite3/wiki/Binaries");
21
+ }
22
+ exports.currentSchemaVersion = 1703322560448; // use `new Date().getTime()`
23
+ // Desired schema, adapt to the newest version and add migrations to the array below
24
+ const schema = [
25
+ "CREATE TABLE options (name TEXT, value TEXT, CONSTRAINT name_unique UNIQUE (name))",
26
+ "CREATE TABLE messages (id INTEGER PRIMARY KEY AUTOINCREMENT, network TEXT, channel TEXT, time INTEGER, type TEXT, msg TEXT)",
27
+ `CREATE TABLE migrations (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ version INTEGER NOT NULL UNIQUE,
30
+ rollback_forbidden INTEGER DEFAULT 0 NOT NULL
31
+ )`,
32
+ `CREATE TABLE rollback_steps (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ migration_id INTEGER NOT NULL REFERENCES migrations ON DELETE CASCADE,
35
+ step INTEGER NOT NULL,
36
+ statement TEXT NOT NULL
37
+ )`,
38
+ "CREATE INDEX network_channel ON messages (network, channel)",
39
+ "CREATE INDEX time ON messages (time)",
40
+ "CREATE INDEX msg_type_idx on messages (type)", // needed for efficient storageCleaner queries
41
+ ];
42
+ // the migrations will be executed in an exclusive transaction as a whole
43
+ // add new migrations to the end, with the version being the new 'currentSchemaVersion'
44
+ // write a corresponding down migration into rollbacks
45
+ exports.migrations = [
46
+ {
47
+ version: 1672236339873,
48
+ stmts: [
49
+ "CREATE TABLE messages_new (id INTEGER PRIMARY KEY AUTOINCREMENT, network TEXT, channel TEXT, time INTEGER, type TEXT, msg TEXT)",
50
+ "INSERT INTO messages_new(network, channel, time, type, msg) select network, channel, time, type, msg from messages order by time asc",
51
+ "DROP TABLE messages",
52
+ "ALTER TABLE messages_new RENAME TO messages",
53
+ "CREATE INDEX network_channel ON messages (network, channel)",
54
+ "CREATE INDEX time ON messages (time)",
55
+ ],
56
+ },
57
+ {
58
+ version: 1679743888000,
59
+ stmts: [
60
+ `CREATE TABLE IF NOT EXISTS migrations (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ version INTEGER NOT NULL UNIQUE,
63
+ rollback_forbidden INTEGER DEFAULT 0 NOT NULL
64
+ )`,
65
+ `CREATE TABLE IF NOT EXISTS rollback_steps (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ migration_id INTEGER NOT NULL REFERENCES migrations ON DELETE CASCADE,
68
+ step INTEGER NOT NULL,
69
+ statement TEXT NOT NULL
70
+ )`,
71
+ ],
72
+ },
73
+ {
74
+ version: 1703322560448,
75
+ stmts: ["CREATE INDEX msg_type_idx on messages (type)"],
76
+ },
77
+ ];
78
+ // down migrations need to restore the state of the prior version.
79
+ // rollback can be disallowed by adding rollback_forbidden: true to it
80
+ exports.rollbacks = [
81
+ {
82
+ version: 1672236339873,
83
+ stmts: [], // changes aren't visible, left empty on purpose
84
+ },
85
+ {
86
+ version: 1679743888000,
87
+ stmts: [], // here we can't drop the tables, as we use them in the code, so just leave those in
88
+ },
89
+ {
90
+ version: 1703322560448,
91
+ stmts: ["drop INDEX msg_type_idx"],
92
+ },
93
+ ];
94
+ class Deferred {
95
+ resolve;
96
+ promise;
97
+ constructor() {
98
+ this.promise = new Promise((resolve) => {
99
+ this.resolve = resolve;
100
+ });
101
+ }
102
+ }
103
+ class SqliteMessageStorage {
104
+ isEnabled;
105
+ database;
106
+ initDone;
107
+ userName;
108
+ constructor(userName) {
109
+ this.userName = userName;
110
+ this.isEnabled = false;
111
+ this.initDone = new Deferred();
112
+ }
113
+ async _enable(connection_string) {
114
+ this.database = new sqlite3.Database(connection_string);
115
+ try {
116
+ await this.run_pragmas(); // must be done outside of a transaction
117
+ await this.run_migrations();
118
+ }
119
+ catch (e) {
120
+ this.isEnabled = false;
121
+ throw helper_1.default.catch_to_error("Migration failed", e);
122
+ }
123
+ this.isEnabled = true;
124
+ }
125
+ async enable() {
126
+ const logsPath = config_1.default.getUserLogsPath();
127
+ const sqlitePath = path_1.default.join(logsPath, `${this.userName}.sqlite3`);
128
+ try {
129
+ await promises_1.default.mkdir(logsPath, { recursive: true });
130
+ }
131
+ catch (e) {
132
+ throw helper_1.default.catch_to_error("Unable to create logs directory", e);
133
+ }
134
+ try {
135
+ await this._enable(sqlitePath);
136
+ }
137
+ finally {
138
+ this.initDone.resolve(); // unblock the instance methods
139
+ }
140
+ }
141
+ async setup_new_db() {
142
+ for (const stmt of schema) {
143
+ await this.serialize_run(stmt);
144
+ }
145
+ await this.serialize_run("INSERT INTO options (name, value) VALUES ('schema_version', ?)", exports.currentSchemaVersion.toString());
146
+ }
147
+ async current_version() {
148
+ const have_options = await this.serialize_get("select 1 from sqlite_master where type = 'table' and name = 'options'");
149
+ if (!have_options) {
150
+ return 0;
151
+ }
152
+ const version = await this.serialize_get("SELECT value FROM options WHERE name = 'schema_version'");
153
+ if (version === undefined) {
154
+ // technically shouldn't happen, means something created a schema but didn't populate it
155
+ // we'll try our best to recover
156
+ return 0;
157
+ }
158
+ const storedSchemaVersion = parseInt(version.value, 10);
159
+ return storedSchemaVersion;
160
+ }
161
+ async update_version_in_db() {
162
+ return this.serialize_run("UPDATE options SET value = ? WHERE name = 'schema_version'", exports.currentSchemaVersion.toString());
163
+ }
164
+ async _run_migrations(dbVersion) {
165
+ log_1.default.info(`sqlite messages schema version is out of date (${dbVersion} < ${exports.currentSchemaVersion}). Running migrations.`);
166
+ const to_execute = necessaryMigrations(dbVersion);
167
+ for (const stmt of to_execute.map((m) => m.stmts).flat()) {
168
+ await this.serialize_run(stmt);
169
+ }
170
+ await this.update_version_in_db();
171
+ }
172
+ async run_pragmas() {
173
+ await this.serialize_run("PRAGMA foreign_keys = ON;");
174
+ }
175
+ async run_migrations() {
176
+ const version = await this.current_version();
177
+ if (version > exports.currentSchemaVersion) {
178
+ throw `sqlite messages schema version is higher than expected (${version} > ${exports.currentSchemaVersion}). Is The Lounge out of date?`;
179
+ }
180
+ else if (version === exports.currentSchemaVersion) {
181
+ return; // nothing to do
182
+ }
183
+ await this.serialize_run("BEGIN EXCLUSIVE TRANSACTION");
184
+ try {
185
+ if (version === 0) {
186
+ await this.setup_new_db();
187
+ }
188
+ else {
189
+ await this._run_migrations(version);
190
+ }
191
+ await this.insert_rollback_since(version);
192
+ }
193
+ catch (err) {
194
+ await this.serialize_run("ROLLBACK");
195
+ throw err;
196
+ }
197
+ await this.serialize_run("COMMIT");
198
+ await this.serialize_run("VACUUM");
199
+ }
200
+ // helper method that vacuums the db, meant to be used by migration related cli commands
201
+ async vacuum() {
202
+ await this.serialize_run("VACUUM");
203
+ }
204
+ async close() {
205
+ if (!this.isEnabled) {
206
+ return;
207
+ }
208
+ this.isEnabled = false;
209
+ return new Promise((resolve, reject) => {
210
+ this.database.close((err) => {
211
+ if (err) {
212
+ reject(`Failed to close sqlite database: ${err.message}`);
213
+ return;
214
+ }
215
+ resolve();
216
+ });
217
+ });
218
+ }
219
+ async fetch_rollbacks(since_version) {
220
+ const res = await this.serialize_fetchall(`select version, rollback_forbidden, statement
221
+ from rollback_steps
222
+ join migrations on migrations.id=rollback_steps.migration_id
223
+ where version > ?
224
+ order by version desc, step asc`, since_version);
225
+ const result = [];
226
+ // convert to Rollback[]
227
+ // requires ordering in the sql statement
228
+ for (const raw of res) {
229
+ const last = result.at(-1);
230
+ if (!last || raw.version !== last.version) {
231
+ result.push({
232
+ version: raw.version,
233
+ rollback_forbidden: Boolean(raw.rollback_forbidden),
234
+ stmts: [raw.statement],
235
+ });
236
+ }
237
+ else {
238
+ last.stmts.push(raw.statement);
239
+ }
240
+ }
241
+ return result;
242
+ }
243
+ async delete_migrations_older_than(version) {
244
+ return this.serialize_run("delete from migrations where migrations.version > ?", version);
245
+ }
246
+ async _downgrade_to(version) {
247
+ const _rollbacks = await this.fetch_rollbacks(version);
248
+ if (_rollbacks.length === 0) {
249
+ return version;
250
+ }
251
+ const forbidden = _rollbacks.find((item) => item.rollback_forbidden);
252
+ if (forbidden) {
253
+ throw Error(`can't downgrade past ${forbidden.version}`);
254
+ }
255
+ for (const rollback of _rollbacks) {
256
+ for (const stmt of rollback.stmts) {
257
+ await this.serialize_run(stmt);
258
+ }
259
+ }
260
+ await this.delete_migrations_older_than(version);
261
+ await this.update_version_in_db();
262
+ return version;
263
+ }
264
+ async downgrade_to(version) {
265
+ if (version <= 0) {
266
+ throw Error(`${version} is not a valid version to downgrade to`);
267
+ }
268
+ await this.serialize_run("BEGIN EXCLUSIVE TRANSACTION");
269
+ let new_version;
270
+ try {
271
+ new_version = await this._downgrade_to(version);
272
+ }
273
+ catch (err) {
274
+ await this.serialize_run("ROLLBACK");
275
+ throw err;
276
+ }
277
+ await this.serialize_run("COMMIT");
278
+ return new_version;
279
+ }
280
+ async downgrade() {
281
+ const res = await this.downgrade_to(exports.currentSchemaVersion);
282
+ return res;
283
+ }
284
+ async insert_rollback_since(version) {
285
+ const missing = newRollbacks(version);
286
+ for (const rollback of missing) {
287
+ const migration = await this.serialize_get(`insert into migrations
288
+ (version, rollback_forbidden)
289
+ values (?, ?)
290
+ returning id`, rollback.version, rollback.rollback_forbidden || 0);
291
+ for (const stmt of rollback.stmts) {
292
+ let step = 0;
293
+ await this.serialize_run(`insert into rollback_steps
294
+ (migration_id, step, statement)
295
+ values (?, ?, ?)`, migration.id, step, stmt);
296
+ step++;
297
+ }
298
+ }
299
+ }
300
+ async index(network, channel, msg) {
301
+ await this.initDone.promise;
302
+ if (!this.isEnabled) {
303
+ return;
304
+ }
305
+ const clonedMsg = Object.keys(msg).reduce((newMsg, prop) => {
306
+ // id is regenerated when messages are retrieved
307
+ // previews are not stored because storage is cleared on lounge restart
308
+ // type and time are stored in a separate column
309
+ if (prop !== "id" && prop !== "previews" && prop !== "type" && prop !== "time") {
310
+ newMsg[prop] = msg[prop];
311
+ }
312
+ return newMsg;
313
+ }, {});
314
+ await this.serialize_run("INSERT INTO messages(network, channel, time, type, msg) VALUES(?, ?, ?, ?, ?)", network.uuid, channel.name.toLowerCase(), msg.time.getTime(), msg.type, JSON.stringify(clonedMsg));
315
+ }
316
+ async deleteChannel(network, channel) {
317
+ await this.initDone.promise;
318
+ if (!this.isEnabled) {
319
+ return;
320
+ }
321
+ await this.serialize_run("DELETE FROM messages WHERE network = ? AND channel = ?", network.uuid, channel.name.toLowerCase());
322
+ }
323
+ async getMessages(network, channel, nextID) {
324
+ await this.initDone.promise;
325
+ if (!this.isEnabled || config_1.default.values.maxHistory === 0) {
326
+ return [];
327
+ }
328
+ // If unlimited history is specified, load 100k messages
329
+ const limit = config_1.default.values.maxHistory < 0 ? 100000 : config_1.default.values.maxHistory;
330
+ const rows = await this.serialize_fetchall("SELECT msg, type, time FROM messages WHERE network = ? AND channel = ? ORDER BY time DESC LIMIT ?", network.uuid, channel.name.toLowerCase(), limit);
331
+ return rows.reverse().map((row) => {
332
+ const msg = JSON.parse(row.msg);
333
+ msg.time = row.time;
334
+ msg.type = row.type;
335
+ const newMsg = new msg_1.default(msg);
336
+ newMsg.id = nextID();
337
+ return newMsg;
338
+ });
339
+ }
340
+ async search(query) {
341
+ await this.initDone.promise;
342
+ if (!this.isEnabled) {
343
+ // this should never be hit as messageProvider is checked in client.search()
344
+ throw new Error("search called but sqlite provider not enabled. This is a programming error");
345
+ }
346
+ // Using the '@' character to escape '%' and '_' in patterns.
347
+ const escapedSearchTerm = query.searchTerm.replace(/([%_@])/g, "@$1");
348
+ let select = 'SELECT msg, type, time, network, channel FROM messages WHERE type = "message" AND json_extract(msg, "$.text") LIKE ? ESCAPE \'@\'';
349
+ const params = [`%${escapedSearchTerm}%`];
350
+ if (query.networkUuid) {
351
+ select += " AND network = ? ";
352
+ params.push(query.networkUuid);
353
+ }
354
+ if (query.channelName) {
355
+ select += " AND channel = ? ";
356
+ params.push(query.channelName.toLowerCase());
357
+ }
358
+ const maxResults = 100;
359
+ select += " ORDER BY time DESC LIMIT ? OFFSET ? ";
360
+ params.push(maxResults);
361
+ params.push(query.offset);
362
+ const rows = await this.serialize_fetchall(select, ...params);
363
+ return {
364
+ ...query,
365
+ results: parseSearchRowsToMessages(query.offset, rows).reverse(),
366
+ };
367
+ }
368
+ async deleteMessages(req) {
369
+ await this.initDone.promise;
370
+ let sql = "delete from messages where id in (select id from messages where\n";
371
+ // We roughly get a timestamp from N days before.
372
+ // We don't adjust for daylight savings time or other weird time jumps
373
+ const millisecondsInDay = 24 * 60 * 60 * 1000;
374
+ const deleteBefore = Date.now() - req.olderThanDays * millisecondsInDay;
375
+ sql += `time <= ${deleteBefore}\n`;
376
+ let typeClause = "";
377
+ if (req.messageTypes !== null) {
378
+ typeClause = `type in (${req.messageTypes.map((type) => `'${type}'`).join(",")})\n`;
379
+ }
380
+ if (typeClause) {
381
+ sql += `and ${typeClause}`;
382
+ }
383
+ sql += "order by time asc\n";
384
+ sql += `limit ${req.limit}\n`;
385
+ sql += ")";
386
+ return this.serialize_run(sql);
387
+ }
388
+ canProvideMessages() {
389
+ return this.isEnabled;
390
+ }
391
+ serialize_run(stmt, ...params) {
392
+ return new Promise((resolve, reject) => {
393
+ this.database.serialize(() => {
394
+ this.database.run(stmt, params, function (err) {
395
+ if (err) {
396
+ reject(err);
397
+ return;
398
+ }
399
+ resolve(this.changes); // number of affected rows, `this` is re-bound by sqlite3
400
+ });
401
+ });
402
+ });
403
+ }
404
+ serialize_fetchall(stmt, ...params) {
405
+ return new Promise((resolve, reject) => {
406
+ this.database.serialize(() => {
407
+ this.database.all(stmt, params, (err, rows) => {
408
+ if (err) {
409
+ reject(err);
410
+ return;
411
+ }
412
+ resolve(rows);
413
+ });
414
+ });
415
+ });
416
+ }
417
+ serialize_get(stmt, ...params) {
418
+ return new Promise((resolve, reject) => {
419
+ this.database.serialize(() => {
420
+ this.database.get(stmt, params, (err, row) => {
421
+ if (err) {
422
+ reject(err);
423
+ return;
424
+ }
425
+ resolve(row);
426
+ });
427
+ });
428
+ });
429
+ }
430
+ }
431
+ // TODO: type any
432
+ function parseSearchRowsToMessages(id, rows) {
433
+ const messages = [];
434
+ for (const row of rows) {
435
+ const msg = JSON.parse(row.msg);
436
+ msg.time = row.time;
437
+ msg.type = row.type;
438
+ msg.networkUuid = row.network;
439
+ msg.channelName = row.channel;
440
+ msg.id = id;
441
+ messages.push(new msg_1.default(msg));
442
+ id += 1;
443
+ }
444
+ return messages;
445
+ }
446
+ function necessaryMigrations(since) {
447
+ return exports.migrations.filter((m) => m.version > since);
448
+ }
449
+ exports.necessaryMigrations = necessaryMigrations;
450
+ function newRollbacks(since) {
451
+ return exports.rollbacks.filter((r) => r.version > since);
452
+ }
453
+ exports.newRollbacks = newRollbacks;
454
+ exports.default = SqliteMessageStorage;
@@ -0,0 +1,124 @@
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
+ /* eslint-disable @typescript-eslint/restrict-template-expressions */
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const filenamify_1 = __importDefault(require("filenamify"));
10
+ const config_1 = __importDefault(require("../../config"));
11
+ const msg_1 = require("../../../shared/types/msg");
12
+ class TextFileMessageStorage {
13
+ isEnabled;
14
+ username;
15
+ constructor(username) {
16
+ this.username = username;
17
+ this.isEnabled = false;
18
+ }
19
+ // eslint-disable-next-line @typescript-eslint/require-await
20
+ async enable() {
21
+ this.isEnabled = true;
22
+ }
23
+ // eslint-disable-next-line @typescript-eslint/require-await
24
+ async close() {
25
+ this.isEnabled = false;
26
+ }
27
+ async index(network, channel, msg) {
28
+ if (!this.isEnabled) {
29
+ return;
30
+ }
31
+ const logPath = path_1.default.join(config_1.default.getUserLogsPath(), this.username, TextFileMessageStorage.getNetworkFolderName(network));
32
+ try {
33
+ await promises_1.default.mkdir(logPath, { recursive: true });
34
+ }
35
+ catch (e) {
36
+ throw new Error(`Unable to create logs directory: ${e}`);
37
+ }
38
+ let line = `[${msg.time.toISOString()}] `;
39
+ // message types from src/models/msg.js
40
+ switch (msg.type) {
41
+ case msg_1.MessageType.ACTION:
42
+ // [2014-01-01 00:00:00] * @Arnold is eating cookies
43
+ line += `* ${msg.from.mode}${msg.from.nick} ${msg.text}`;
44
+ break;
45
+ case msg_1.MessageType.JOIN:
46
+ // [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) joined
47
+ line += `*** ${msg.from.nick} (${msg.hostmask}) joined`;
48
+ break;
49
+ case msg_1.MessageType.KICK:
50
+ // [2014-01-01 00:00:00] *** Arnold was kicked by Bernie (Don't steal my cookies!)
51
+ line += `*** ${msg.target.nick} was kicked by ${msg.from.nick} (${msg.text})`;
52
+ break;
53
+ case msg_1.MessageType.MESSAGE:
54
+ // [2014-01-01 00:00:00] <@Arnold> Put that cookie down.. Now!!
55
+ line += `<${msg.from.mode}${msg.from.nick}> ${msg.text}`;
56
+ break;
57
+ case msg_1.MessageType.MODE:
58
+ // [2014-01-01 00:00:00] *** Arnold set mode +o Bernie
59
+ line += `*** ${msg.from.nick} set mode ${msg.text}`;
60
+ break;
61
+ case msg_1.MessageType.NICK:
62
+ // [2014-01-01 00:00:00] *** Arnold changed nick to Bernie
63
+ line += `*** ${msg.from.nick} changed nick to ${msg.new_nick}`;
64
+ break;
65
+ case msg_1.MessageType.NOTICE:
66
+ // [2014-01-01 00:00:00] -Arnold- pssst, I have cookies!
67
+ line += `-${msg.from.nick}- ${msg.text}`;
68
+ break;
69
+ case msg_1.MessageType.PART:
70
+ // [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) left (Bye all!)
71
+ line += `*** ${msg.from.nick} (${msg.hostmask}) left (${msg.text})`;
72
+ break;
73
+ case msg_1.MessageType.QUIT:
74
+ // [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) quit (Connection reset by peer)
75
+ line += `*** ${msg.from.nick} (${msg.hostmask}) quit (${msg.text})`;
76
+ break;
77
+ case msg_1.MessageType.CHGHOST:
78
+ // [2014-01-01 00:00:00] *** Arnold changed host to: new@fancy.host
79
+ line += `*** ${msg.from.nick} changed host to '${msg.new_ident}@${msg.new_host}'`;
80
+ break;
81
+ case msg_1.MessageType.TOPIC:
82
+ // [2014-01-01 00:00:00] *** Arnold changed topic to: welcome everyone!
83
+ line += `*** ${msg.from.nick} changed topic to '${msg.text}'`;
84
+ break;
85
+ default:
86
+ // unhandled events will not be logged
87
+ return;
88
+ }
89
+ line += "\n";
90
+ try {
91
+ await promises_1.default.appendFile(path_1.default.join(logPath, TextFileMessageStorage.getChannelFileName(channel)), line);
92
+ }
93
+ catch (e) {
94
+ throw new Error(`Failed to write user log: ${e}`);
95
+ }
96
+ }
97
+ async deleteChannel() {
98
+ // Not implemented for text log files
99
+ }
100
+ getMessages() {
101
+ // Not implemented for text log files
102
+ // They do not contain enough data to fully re-create message objects
103
+ // Use sqlite storage instead
104
+ return Promise.resolve([]);
105
+ }
106
+ canProvideMessages() {
107
+ return false;
108
+ }
109
+ static getNetworkFolderName(network) {
110
+ // Limit network name in the folder name to 23 characters
111
+ // So we can still fit 12 characters of the uuid for de-duplication
112
+ const networkName = cleanFilename(network.name.substring(0, 23).replace(/ /g, "-"));
113
+ return `${networkName}-${network.uuid.substring(networkName.length + 1)}`;
114
+ }
115
+ static getChannelFileName(channel) {
116
+ return `${cleanFilename(channel.name)}.log`;
117
+ }
118
+ }
119
+ exports.default = TextFileMessageStorage;
120
+ function cleanFilename(name) {
121
+ name = (0, filenamify_1.default)(name, { replacement: "_" });
122
+ name = name.toLowerCase();
123
+ return name;
124
+ }