2017-11-28 17:56:53 +00:00
|
|
|
"use strict";
|
|
|
|
|
2018-06-15 20:31:06 +00:00
|
|
|
const log = require("../../log");
|
2017-11-28 17:56:53 +00:00
|
|
|
const path = require("path");
|
2020-03-16 11:53:48 +00:00
|
|
|
const fs = require("fs");
|
2022-05-01 19:12:39 +00:00
|
|
|
const Config = require("../../config");
|
2018-04-17 08:06:08 +00:00
|
|
|
const Msg = require("../../models/msg");
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2018-04-26 09:11:38 +00:00
|
|
|
let sqlite3;
|
|
|
|
|
|
|
|
try {
|
|
|
|
sqlite3 = require("sqlite3");
|
|
|
|
} catch (e) {
|
2022-05-01 19:12:39 +00:00
|
|
|
Config.values.messageStorage = Config.values.messageStorage.filter((item) => item !== "sqlite");
|
2018-04-26 09:11:38 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
log.error(
|
2022-03-12 15:39:03 +00:00
|
|
|
"Unable to load sqlite3 module. See https://github.com/mapbox/node-sqlite3/wiki/Binaries"
|
2019-07-17 09:33:59 +00:00
|
|
|
);
|
2018-04-26 09:11:38 +00:00
|
|
|
}
|
|
|
|
|
2018-03-02 08:42:12 +00:00
|
|
|
const currentSchemaVersion = 1520239200;
|
2017-11-28 17:56:53 +00:00
|
|
|
|
|
|
|
const schema = [
|
|
|
|
// Schema version #1
|
|
|
|
"CREATE TABLE IF NOT EXISTS options (name TEXT, value TEXT, CONSTRAINT name_unique UNIQUE (name))",
|
|
|
|
"CREATE TABLE IF NOT EXISTS messages (network TEXT, channel TEXT, time INTEGER, type TEXT, msg TEXT)",
|
|
|
|
"CREATE INDEX IF NOT EXISTS network_channel ON messages (network, channel)",
|
|
|
|
"CREATE INDEX IF NOT EXISTS time ON messages (time)",
|
|
|
|
];
|
|
|
|
|
|
|
|
class MessageStorage {
|
2018-04-27 10:16:23 +00:00
|
|
|
constructor(client) {
|
|
|
|
this.client = client;
|
2017-11-28 17:56:53 +00:00
|
|
|
this.isEnabled = false;
|
|
|
|
}
|
|
|
|
|
2018-04-17 08:06:08 +00:00
|
|
|
enable() {
|
2022-05-01 19:12:39 +00:00
|
|
|
const logsPath = Config.getUserLogsPath();
|
2018-04-17 08:06:08 +00:00
|
|
|
const sqlitePath = path.join(logsPath, `${this.client.name}.sqlite3`);
|
2017-11-28 17:56:53 +00:00
|
|
|
|
|
|
|
try {
|
2020-03-16 11:53:48 +00:00
|
|
|
fs.mkdirSync(logsPath, {recursive: true});
|
2017-11-28 17:56:53 +00:00
|
|
|
} catch (e) {
|
|
|
|
log.error("Unable to create logs directory", e);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.isEnabled = true;
|
|
|
|
|
2018-03-10 16:49:16 +00:00
|
|
|
this.database = new sqlite3.Database(sqlitePath);
|
2017-11-28 17:56:53 +00:00
|
|
|
this.database.serialize(() => {
|
|
|
|
schema.forEach((line) => this.database.run(line));
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
this.database.get(
|
|
|
|
"SELECT value FROM options WHERE name = 'schema_version'",
|
|
|
|
(err, row) => {
|
|
|
|
if (err) {
|
|
|
|
return log.error(`Failed to retrieve schema version: ${err}`);
|
|
|
|
}
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
// New table
|
|
|
|
if (row === undefined) {
|
|
|
|
this.database.serialize(() =>
|
|
|
|
this.database.run(
|
|
|
|
"INSERT INTO options (name, value) VALUES ('schema_version', ?)",
|
|
|
|
currentSchemaVersion
|
|
|
|
)
|
|
|
|
);
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
const storedSchemaVersion = parseInt(row.value, 10);
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
if (storedSchemaVersion === currentSchemaVersion) {
|
|
|
|
return;
|
|
|
|
}
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
if (storedSchemaVersion > currentSchemaVersion) {
|
|
|
|
return log.error(
|
|
|
|
`sqlite messages schema version is higher than expected (${storedSchemaVersion} > ${currentSchemaVersion}). Is The Lounge out of date?`
|
|
|
|
);
|
|
|
|
}
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
log.info(
|
|
|
|
`sqlite messages schema version is out of date (${storedSchemaVersion} < ${currentSchemaVersion}). Running migrations if any.`
|
|
|
|
);
|
2017-11-28 17:56:53 +00:00
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
this.database.serialize(() =>
|
|
|
|
this.database.run(
|
|
|
|
"UPDATE options SET value = ? WHERE name = 'schema_version'",
|
|
|
|
currentSchemaVersion
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
2017-11-28 17:56:53 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-03-10 16:49:16 +00:00
|
|
|
close(callback) {
|
|
|
|
if (!this.isEnabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-14 06:45:52 +00:00
|
|
|
this.isEnabled = false;
|
|
|
|
|
2018-03-10 16:49:16 +00:00
|
|
|
this.database.close((err) => {
|
|
|
|
if (err) {
|
|
|
|
log.error(`Failed to close sqlite database: ${err}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (callback) {
|
|
|
|
callback(err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-11-28 17:56:53 +00:00
|
|
|
index(network, channel, msg) {
|
|
|
|
if (!this.isEnabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const clonedMsg = Object.keys(msg).reduce((newMsg, prop) => {
|
|
|
|
// id is regenerated when messages are retrieved
|
|
|
|
// previews are not stored because storage is cleared on lounge restart
|
|
|
|
// type and time are stored in a separate column
|
|
|
|
if (prop !== "id" && prop !== "previews" && prop !== "type" && prop !== "time") {
|
|
|
|
newMsg[prop] = msg[prop];
|
|
|
|
}
|
|
|
|
|
|
|
|
return newMsg;
|
|
|
|
}, {});
|
|
|
|
|
2019-07-17 09:33:59 +00:00
|
|
|
this.database.serialize(() =>
|
|
|
|
this.database.run(
|
|
|
|
"INSERT INTO messages(network, channel, time, type, msg) VALUES(?, ?, ?, ?, ?)",
|
|
|
|
network.uuid,
|
|
|
|
channel.name.toLowerCase(),
|
|
|
|
msg.time.getTime(),
|
|
|
|
msg.type,
|
|
|
|
JSON.stringify(clonedMsg)
|
|
|
|
)
|
|
|
|
);
|
2017-11-28 17:56:53 +00:00
|
|
|
}
|
|
|
|
|
2020-01-30 08:52:29 +00:00
|
|
|
deleteChannel(network, channel) {
|
|
|
|
if (!this.isEnabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.database.serialize(() =>
|
|
|
|
this.database.run(
|
|
|
|
"DELETE FROM messages WHERE network = ? AND channel = ?",
|
|
|
|
network.uuid,
|
|
|
|
channel.name.toLowerCase()
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-11-28 17:56:53 +00:00
|
|
|
/**
|
|
|
|
* Load messages for given channel on a given network and resolve a promise with loaded messages.
|
|
|
|
*
|
|
|
|
* @param Network network - Network object where the channel is
|
|
|
|
* @param Chan channel - Channel object for which to load messages for
|
|
|
|
*/
|
|
|
|
getMessages(network, channel) {
|
2022-05-01 19:12:39 +00:00
|
|
|
if (!this.isEnabled || Config.values.maxHistory === 0) {
|
2017-11-28 17:56:53 +00:00
|
|
|
return Promise.resolve([]);
|
|
|
|
}
|
|
|
|
|
2018-06-11 11:08:05 +00:00
|
|
|
// If unlimited history is specified, load 100k messages
|
2022-05-01 19:12:39 +00:00
|
|
|
const limit = Config.values.maxHistory < 0 ? 100000 : Config.values.maxHistory;
|
2018-06-11 11:08:05 +00:00
|
|
|
|
2017-11-28 17:56:53 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2020-02-15 09:50:07 +00:00
|
|
|
this.database.serialize(() =>
|
2019-07-17 09:33:59 +00:00
|
|
|
this.database.all(
|
|
|
|
"SELECT msg, type, time FROM messages WHERE network = ? AND channel = ? ORDER BY time DESC LIMIT ?",
|
|
|
|
[network.uuid, channel.name.toLowerCase(), limit],
|
|
|
|
(err, rows) => {
|
|
|
|
if (err) {
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
resolve(
|
2020-04-28 14:45:27 +00:00
|
|
|
rows.reverse().map((row) => {
|
|
|
|
const msg = JSON.parse(row.msg);
|
|
|
|
msg.time = row.time;
|
|
|
|
msg.type = row.type;
|
|
|
|
|
|
|
|
const newMsg = new Msg(msg);
|
|
|
|
newMsg.id = this.client.idMsg++;
|
|
|
|
|
|
|
|
return newMsg;
|
|
|
|
})
|
2019-07-17 09:33:59 +00:00
|
|
|
);
|
2017-11-28 17:56:53 +00:00
|
|
|
}
|
2019-07-17 09:33:59 +00:00
|
|
|
)
|
|
|
|
);
|
2017-11-28 17:56:53 +00:00
|
|
|
});
|
|
|
|
}
|
2018-04-17 08:06:08 +00:00
|
|
|
|
2019-12-31 16:21:34 +00:00
|
|
|
search(query) {
|
|
|
|
if (!this.isEnabled) {
|
|
|
|
return Promise.resolve([]);
|
|
|
|
}
|
|
|
|
|
2022-04-12 00:49:13 +00:00
|
|
|
// Using the '@' character to escape '%' and '_' in patterns.
|
|
|
|
const escapedSearchTerm = query.searchTerm.replace(/([%_@])/g, "@$1");
|
|
|
|
|
2019-12-31 16:21:34 +00:00
|
|
|
let select =
|
2022-04-12 00:49:13 +00:00
|
|
|
'SELECT msg, type, time, network, channel FROM messages WHERE type = "message" AND json_extract(msg, "$.text") LIKE ? ESCAPE \'@\'';
|
|
|
|
const params = [`%${escapedSearchTerm}%`];
|
2020-03-07 12:56:50 +00:00
|
|
|
|
|
|
|
if (query.networkUuid) {
|
|
|
|
select += " AND network = ? ";
|
|
|
|
params.push(query.networkUuid);
|
|
|
|
}
|
2019-12-31 16:21:34 +00:00
|
|
|
|
|
|
|
if (query.channelName) {
|
|
|
|
select += " AND channel = ? ";
|
2020-06-03 11:17:53 +00:00
|
|
|
params.push(query.channelName.toLowerCase());
|
2019-12-31 16:21:34 +00:00
|
|
|
}
|
|
|
|
|
2020-03-07 12:56:50 +00:00
|
|
|
const maxResults = 100;
|
|
|
|
|
|
|
|
select += " ORDER BY time DESC LIMIT ? OFFSET ? ";
|
|
|
|
params.push(maxResults);
|
|
|
|
query.offset = parseInt(query.offset, 10) || 0;
|
|
|
|
params.push(query.offset);
|
2019-12-31 16:21:34 +00:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.database.all(select, params, (err, rows) => {
|
|
|
|
if (err) {
|
|
|
|
reject(err);
|
|
|
|
} else {
|
|
|
|
const response = {
|
|
|
|
searchTerm: query.searchTerm,
|
|
|
|
target: query.channelName,
|
|
|
|
networkUuid: query.networkUuid,
|
|
|
|
offset: query.offset,
|
2021-10-28 07:48:11 +00:00
|
|
|
results: parseSearchRowsToMessages(query.offset, rows).reverse(),
|
2019-12-31 16:21:34 +00:00
|
|
|
};
|
|
|
|
resolve(response);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-04-17 08:06:08 +00:00
|
|
|
canProvideMessages() {
|
|
|
|
return this.isEnabled;
|
|
|
|
}
|
2017-11-28 17:56:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = MessageStorage;
|
2019-12-31 16:21:34 +00:00
|
|
|
|
2020-04-24 07:20:40 +00:00
|
|
|
function parseSearchRowsToMessages(id, rows) {
|
2020-03-07 12:56:50 +00:00
|
|
|
const messages = [];
|
|
|
|
|
|
|
|
for (const row of rows) {
|
|
|
|
const msg = JSON.parse(row.msg);
|
|
|
|
msg.time = row.time;
|
|
|
|
msg.type = row.type;
|
2020-04-24 07:20:40 +00:00
|
|
|
msg.networkUuid = row.network;
|
|
|
|
msg.channelName = row.channel;
|
2020-03-07 12:56:50 +00:00
|
|
|
msg.id = id;
|
|
|
|
messages.push(new Msg(msg));
|
|
|
|
id += 1;
|
|
|
|
}
|
2019-12-31 16:21:34 +00:00
|
|
|
|
2020-03-07 12:56:50 +00:00
|
|
|
return messages;
|
2019-12-31 16:21:34 +00:00
|
|
|
}
|