diff --git a/src/client.js b/src/client.js index cd0359a8..68786b11 100644 --- a/src/client.js +++ b/src/client.js @@ -141,10 +141,23 @@ function Client(manager, name, config = {}) { } } -Client.prototype.createChannel = function (attr) { +Client.prototype.createChannel = function (attr, network) { const chan = new Chan(attr); chan.id = this.idChan++; + if (!chan.isLoggable() || !network) { + return chan; + } + + const messageStorage = this.messageStorage.find((s) => s.canProvideMessages()); + + if (messageStorage) { + messageStorage + .getChannelId(network, chan) + .then((id) => (chan.idStorage = id)) + .catch((err) => log.error(`Failed to get storage channel id: ${err}`)); + } + return chan; }; @@ -177,54 +190,6 @@ Client.prototype.find = function (channelId) { Client.prototype.connect = function (args, isStartup = false) { const client = this; - let channels = []; - - // Get channel id for lobby before creating other channels for nicer ids - const lobbyChannelId = client.idChan++; - - if (args.channels) { - let badName = false; - - args.channels.forEach((chan) => { - if (!chan.name) { - badName = true; - return; - } - - channels.push( - client.createChannel({ - name: chan.name, - key: chan.key || "", - type: chan.type, - }) - ); - }); - - if (badName && client.name) { - log.warn( - "User '" + - client.name + - "' on network '" + - args.name + - "' has an invalid channel which has been ignored" - ); - } - // `join` is kept for backwards compatibility when updating from versions <2.0 - // also used by the "connect" window - } else if (args.join) { - channels = args.join - .replace(/,/g, " ") - .split(/\s+/g) - .map((chan) => { - if (!chan.match(/^[#&!+]/)) { - chan = `#${chan}`; - } - - return client.createChannel({ - name: chan, - }); - }); - } const network = new Network({ uuid: args.uuid, @@ -244,12 +209,60 @@ Client.prototype.connect = function (args, isStartup = false) { saslAccount: String(args.saslAccount || ""), saslPassword: String(args.saslPassword || ""), commands: args.commands || [], - channels: channels, ignoreList: args.ignoreList ? args.ignoreList : [], }); - // Set network lobby channel id - network.channels[0].id = lobbyChannelId; + if (args.channels) { + let badName = false; + + args.channels.forEach((chan) => { + if (!chan.name) { + badName = true; + return; + } + + network.channels.push( + client.createChannel( + { + name: chan.name, + key: chan.key || "", + type: chan.type, + }, + network + ) + ); + }); + + if (badName && client.name) { + log.warn( + "User '" + + client.name + + "' on network '" + + args.name + + "' has an invalid channel which has been ignored" + ); + } + } else if (args.join) { + // `join` is kept for backwards compatibility when updating from versions <2.0 + // also used by the "connect" window + args.join + .replace(/,/g, " ") + .split(/\s+/g) + .forEach((chan) => { + if (!chan.match(/^[#&!+]/)) { + chan = `#${chan}`; + } + + network.channels.push( + client.createChannel( + { + name: chan, + }, + network + ) + ); + }); + } client.networks.push(network); client.emit("network", { @@ -281,7 +294,7 @@ Client.prototype.connect = function (args, isStartup = false) { if (!isStartup) { client.save(); - channels.forEach((channel) => channel.loadMessages(client, network)); + network.channels.forEach((channel) => channel.loadMessages(client, network)); } }; diff --git a/src/models/chan.js b/src/models/chan.js index b0a4fffd..dc6a71ca 100644 --- a/src/models/chan.js +++ b/src/models/chan.js @@ -31,6 +31,7 @@ Chan.State = { function Chan(attr) { _.defaults(this, attr, { id: 0, + idStorage: 0, messages: [], name: "", key: "", @@ -192,7 +193,7 @@ Chan.prototype.getFilteredClone = function (lastActiveChannel, lastMessage) { } newChannel.totalMessages = this[prop].length; - } else { + } else if (prop !== "idStorage") { newChannel[prop] = this[prop]; } diff --git a/src/plugins/inputs/ignore.js b/src/plugins/inputs/ignore.js index b2e2dfd9..69264111 100644 --- a/src/plugins/inputs/ignore.js +++ b/src/plugins/inputs/ignore.js @@ -118,12 +118,15 @@ exports.input = function (network, chan, cmd, args) { let newChan = network.getChannel(chanName); if (typeof newChan === "undefined") { - newChan = client.createChannel({ - type: Chan.Type.SPECIAL, - special: Chan.SpecialType.IGNORELIST, - name: chanName, - data: ignored, - }); + newChan = client.createChannel( + { + type: Chan.Type.SPECIAL, + special: Chan.SpecialType.IGNORELIST, + name: chanName, + data: ignored, + }, + network + ); client.emit("join", { network: network.uuid, chan: newChan.getFilteredClone(true), diff --git a/src/plugins/inputs/msg.js b/src/plugins/inputs/msg.js index 36cf10cb..502bc768 100644 --- a/src/plugins/inputs/msg.js +++ b/src/plugins/inputs/msg.js @@ -63,10 +63,13 @@ exports.input = function (network, chan, cmd, args) { } } - const newChan = this.createChannel({ - type: Chan.Type.QUERY, - name: targetName, - }); + const newChan = this.createChannel( + { + type: Chan.Type.QUERY, + name: targetName, + }, + network + ); this.emit("join", { network: network.uuid, diff --git a/src/plugins/irc-events/join.js b/src/plugins/irc-events/join.js index 79aa9cf4..be6f5dc0 100644 --- a/src/plugins/irc-events/join.js +++ b/src/plugins/irc-events/join.js @@ -11,10 +11,13 @@ module.exports = function (irc, network) { let chan = network.getChannel(data.channel); if (typeof chan === "undefined") { - chan = client.createChannel({ - name: data.channel, - state: Chan.State.JOINED, - }); + chan = client.createChannel( + { + name: data.channel, + state: Chan.State.JOINED, + }, + network + ); client.emit("join", { network: network.uuid, diff --git a/src/plugins/irc-events/list.js b/src/plugins/irc-events/list.js index 2e0abeca..3faba40d 100644 --- a/src/plugins/irc-events/list.js +++ b/src/plugins/irc-events/list.js @@ -34,12 +34,15 @@ module.exports = function (irc, network) { let chan = network.getChannel("Channel List"); if (typeof chan === "undefined") { - chan = client.createChannel({ - type: Chan.Type.SPECIAL, - special: Chan.SpecialType.CHANNELLIST, - name: "Channel List", - data: msg, - }); + chan = client.createChannel( + { + type: Chan.Type.SPECIAL, + special: Chan.SpecialType.CHANNELLIST, + name: "Channel List", + data: msg, + }, + network + ); client.emit("join", { network: network.uuid, diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index a2ff0081..b375604b 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -75,10 +75,13 @@ module.exports = function (irc, network) { showInActive = true; chan = network.channels[0]; } else { - chan = client.createChannel({ - type: Chan.Type.QUERY, - name: target, - }); + chan = client.createChannel( + { + type: Chan.Type.QUERY, + name: target, + }, + network + ); client.emit("join", { network: network.uuid, diff --git a/src/plugins/irc-events/modelist.js b/src/plugins/irc-events/modelist.js index 202a2543..276fe801 100644 --- a/src/plugins/irc-events/modelist.js +++ b/src/plugins/irc-events/modelist.js @@ -50,12 +50,15 @@ module.exports = function (irc, network) { let chan = network.getChannel(chanName); if (typeof chan === "undefined") { - chan = client.createChannel({ - type: Chan.Type.SPECIAL, - special: type, - name: chanName, - data: data, - }); + chan = client.createChannel( + { + type: Chan.Type.SPECIAL, + special: type, + name: chanName, + data: data, + }, + network + ); client.emit("join", { network: network.uuid, chan: chan.getFilteredClone(true), diff --git a/src/plugins/irc-events/whois.js b/src/plugins/irc-events/whois.js index c0ceef77..587df587 100644 --- a/src/plugins/irc-events/whois.js +++ b/src/plugins/irc-events/whois.js @@ -22,10 +22,13 @@ module.exports = function (irc, network) { if (data.error) { chan = network.channels[0]; } else { - chan = client.createChannel({ - type: Chan.Type.QUERY, - name: data.nick, - }); + chan = client.createChannel( + { + type: Chan.Type.QUERY, + name: data.nick, + }, + network + ); client.emit("join", { shouldOpen: true, diff --git a/src/plugins/messageStorage/sqlite.js b/src/plugins/messageStorage/sqlite.js index da26e3b0..5ba8828b 100644 --- a/src/plugins/messageStorage/sqlite.js +++ b/src/plugins/messageStorage/sqlite.js @@ -21,9 +21,12 @@ try { const currentSchemaVersion = 1520239200; const schema = [ - // Schema version #1 + // Tables "CREATE TABLE IF NOT EXISTS options (name TEXT, value TEXT, CONSTRAINT name_unique UNIQUE (name))", + "CREATE TABLE IF NOT EXISTS channels (channel_id INTEGER PRIMARY KEY, network TEXT, channel TEXT, CONSTRAINT unique_channel UNIQUE (network, channel))", "CREATE TABLE IF NOT EXISTS messages (network TEXT, channel TEXT, time INTEGER, type TEXT, msg TEXT)", + + // Indexes "CREATE INDEX IF NOT EXISTS network_channel ON messages (network, channel)", "CREATE INDEX IF NOT EXISTS time ON messages (time)", ]; @@ -116,6 +119,13 @@ class MessageStorage { }); } + /** + * Store a new message in specified channel + * + * @param Network network - Network object where the channel is + * @param Chan channel - Channel object + * @param Msg msg - Message object to store + */ index(network, channel, msg) { if (!this.isEnabled) { return; @@ -144,6 +154,12 @@ class MessageStorage { ); } + /** + * Delete stored all stored messages in a channel + * + * @param Network network - Network object where the channel is + * @param Chan channel - Channel object + */ deleteChannel(network, channel) { if (!this.isEnabled) { return; @@ -158,6 +174,51 @@ class MessageStorage { ); } + /** + * Get the stored channel id, creates one if does not exist. + * + * @param Network network - Network object where the channel is + * @param Chan channel - Channel object + */ + getChannelId(network, channel) { + if (!this.isEnabled) { + return Promise.resolve(0); + } + + const channelName = channel.name.toLowerCase(); + + return new Promise((resolve, reject) => { + this.database.serialize(() => + this.database.get( + "SELECT channel_id FROM channels WHERE network = ? AND channel = ?", + [network.uuid, channelName], + (err, row) => { + if (err) { + return reject(err); + } + + if (row) { + return resolve(row.channel_id); + } + + // This channel was not found, create it and "recursively" call getChannelId again + this.database.run( + "INSERT INTO channels (network, channel) VALUES (?, ?)", + [network.uuid, channelName], + (err2) => { + if (err2) { + return reject(err2); + } + + this.getChannelId(network, channel).then(resolve).catch(reject); + } + ); + } + ) + ); + }); + } + /** * Load messages for given channel on a given network and resolve a promise with loaded messages. * diff --git a/src/plugins/packages/publicClient.js b/src/plugins/packages/publicClient.js index f6507971..16a0c26e 100644 --- a/src/plugins/packages/publicClient.js +++ b/src/plugins/packages/publicClient.js @@ -18,9 +18,10 @@ module.exports = class PublicClient { /** * * @param {Object} attributes + * @param {Network} network */ - createChannel(attributes) { - return this.client.createChannel(attributes); + createChannel(attributes, network) { + return this.client.createChannel(attributes, network); } /** diff --git a/test/plugins/sqlite.js b/test/plugins/sqlite.js index 0971d00f..32fe19f9 100644 --- a/test/plugins/sqlite.js +++ b/test/plugins/sqlite.js @@ -66,6 +66,12 @@ describe("SQLite Message Storage", function () { sql: "CREATE TABLE options (name TEXT, value TEXT, CONSTRAINT name_unique UNIQUE (name))", }, + { + name: "channels", + tbl_name: "channels", + sql: + "CREATE TABLE channels (channel_id INTEGER PRIMARY KEY, network TEXT, channel TEXT, CONSTRAINT unique_channel UNIQUE (network, channel))", + }, { name: "messages", tbl_name: "messages", @@ -80,6 +86,37 @@ describe("SQLite Message Storage", function () { ); }); + it("should create channel id if not exists", function (done) { + const network = { + uuid: "network-uuid", + }; + + store + .getChannelId(network, { + name: "#This-Is-Channel-One", + }) + .then((id) => { + expect(id).to.equal(1); + + store + .getChannelId(network, { + name: "#this-is-channel-ONE", + }) + .then((id2) => { + expect(id2).to.equal(1); + + store + .getChannelId(network, { + name: "#this-is-channel-two", + }) + .then((id3) => { + expect(id3).to.equal(2); + done(); + }); + }); + }); + }); + it("should insert schema version to options table", function (done) { store.database.serialize(() => store.database.get(