hardlounge/src/plugins/messageStorage/sqlite.js

277 lines
6.4 KiB
JavaScript
Raw Normal View History

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");
const fs = require("fs");
const Helper = require("../../helper");
const Msg = require("../../models/msg");
2017-11-28 17:56:53 +00:00
let sqlite3;
try {
sqlite3 = require("sqlite3");
} catch (e) {
Helper.config.messageStorage = Helper.config.messageStorage.filter((item) => item !== "sqlite");
2019-07-17 09:33:59 +00:00
log.error(
"Unable to load node-sqlite3 module. See https://github.com/mapbox/node-sqlite3/wiki/Binaries"
);
}
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 {
constructor(client) {
this.client = client;
2017-11-28 17:56:53 +00:00
this.isEnabled = false;
}
enable() {
const logsPath = Helper.getUserLogsPath();
const sqlitePath = path.join(logsPath, `${this.client.name}.sqlite3`);
2017-11-28 17:56:53 +00:00
try {
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;
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
});
}
close(callback) {
if (!this.isEnabled) {
return;
}
this.isEnabled = false;
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
}
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) {
if (!this.isEnabled || Helper.config.maxHistory === 0) {
2017-11-28 17:56:53 +00:00
return Promise.resolve([]);
}
// If unlimited history is specified, load 100k messages
const limit = Helper.config.maxHistory < 0 ? 100000 : Helper.config.maxHistory;
2017-11-28 17:56:53 +00:00
return new Promise((resolve, reject) => {
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(
rows
.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;
})
.reverse()
);
2017-11-28 17:56:53 +00:00
}
2019-07-17 09:33:59 +00:00
)
);
2017-11-28 17:56:53 +00:00
});
}
2019-12-31 16:21:34 +00:00
search(query) {
if (!this.isEnabled) {
return Promise.resolve([]);
}
let select =
2020-03-07 12:56:50 +00:00
'SELECT msg, type, time, channel FROM messages WHERE type = "message" AND (json_extract(msg, "$.text") LIKE ?';
const params = [`%${query.searchTerm}%`];
if (query.searchNicks) {
select += ' OR json_extract(msg, "$.from.nick") LIKE ?)';
params.push(query.searchTerm);
} else {
select += ")";
}
if (query.networkUuid) {
select += " AND network = ? ";
params.push(query.networkUuid);
}
2019-12-31 16:21:34 +00:00
if (query.channelName) {
select += " AND channel = ? ";
params.push(query.channelName);
}
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,
2020-03-07 12:56:50 +00:00
results: parseRowsToMessages(query.offset, rows),
2019-12-31 16:21:34 +00:00
};
resolve(response);
}
});
});
}
canProvideMessages() {
return this.isEnabled;
}
2017-11-28 17:56:53 +00:00
}
module.exports = MessageStorage;
2019-12-31 16:21:34 +00:00
2020-03-07 12:56:50 +00:00
function parseRowsToMessages(id, rows) {
const messages = [];
for (const row of rows) {
const msg = JSON.parse(row.msg);
msg.time = row.time;
msg.type = row.type;
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
}