2016-10-09 19:14:02 +00:00
|
|
|
"use strict";
|
|
|
|
|
2018-01-11 11:33:36 +00:00
|
|
|
const _ = require("lodash");
|
2018-06-15 20:31:06 +00:00
|
|
|
const log = require("../log");
|
2018-01-11 11:33:36 +00:00
|
|
|
const Helper = require("../helper");
|
2017-11-08 21:10:17 +00:00
|
|
|
const User = require("./user");
|
2018-06-12 11:37:22 +00:00
|
|
|
const Msg = require("./msg");
|
2017-07-06 15:33:09 +00:00
|
|
|
const storage = require("../plugins/storage");
|
2014-09-13 21:29:45 +00:00
|
|
|
|
|
|
|
module.exports = Chan;
|
|
|
|
|
|
|
|
Chan.Type = {
|
|
|
|
CHANNEL: "channel",
|
|
|
|
LOBBY: "lobby",
|
2016-03-09 20:04:07 +00:00
|
|
|
QUERY: "query",
|
|
|
|
SPECIAL: "special",
|
2014-09-13 21:29:45 +00:00
|
|
|
};
|
|
|
|
|
2018-07-10 09:16:24 +00:00
|
|
|
Chan.SpecialType = {
|
|
|
|
BANLIST: "list_bans",
|
2019-04-14 11:44:44 +00:00
|
|
|
INVITELIST: "list_invites",
|
2018-07-10 09:16:24 +00:00
|
|
|
CHANNELLIST: "list_channels",
|
|
|
|
IGNORELIST: "list_ignored",
|
|
|
|
};
|
|
|
|
|
2018-02-13 10:30:26 +00:00
|
|
|
Chan.State = {
|
|
|
|
PARTED: 0,
|
|
|
|
JOINED: 1,
|
|
|
|
};
|
|
|
|
|
2014-09-13 21:29:45 +00:00
|
|
|
function Chan(attr) {
|
2016-10-02 07:37:37 +00:00
|
|
|
_.defaults(this, attr, {
|
2018-04-27 10:16:23 +00:00
|
|
|
id: 0,
|
2014-09-13 21:29:45 +00:00
|
|
|
messages: [],
|
|
|
|
name: "",
|
2017-04-01 08:33:17 +00:00
|
|
|
key: "",
|
2014-10-10 20:05:25 +00:00
|
|
|
topic: "",
|
2014-09-13 21:29:45 +00:00
|
|
|
type: Chan.Type.CHANNEL,
|
2018-02-13 10:30:26 +00:00
|
|
|
state: Chan.State.PARTED,
|
2016-05-13 10:23:05 +00:00
|
|
|
firstUnread: 0,
|
2014-09-21 16:48:01 +00:00
|
|
|
unread: 0,
|
2017-09-03 15:57:07 +00:00
|
|
|
highlight: 0,
|
2017-11-16 20:32:03 +00:00
|
|
|
users: new Map(),
|
2016-10-02 07:37:37 +00:00
|
|
|
});
|
2014-09-13 21:29:45 +00:00
|
|
|
}
|
|
|
|
|
2017-08-11 12:02:58 +00:00
|
|
|
Chan.prototype.destroy = function() {
|
|
|
|
this.dereferencePreviews(this.messages);
|
|
|
|
};
|
|
|
|
|
2016-09-25 06:41:10 +00:00
|
|
|
Chan.prototype.pushMessage = function(client, msg, increasesUnread) {
|
2018-03-05 00:59:16 +00:00
|
|
|
const chan = this.id;
|
|
|
|
const obj = {chan, msg};
|
2016-09-25 06:41:10 +00:00
|
|
|
|
2018-04-27 10:16:23 +00:00
|
|
|
msg.id = client.idMsg++;
|
|
|
|
|
2016-09-25 06:41:10 +00:00
|
|
|
// If this channel is open in any of the clients, do not increase unread counter
|
2018-03-05 00:59:16 +00:00
|
|
|
const isOpen = _.find(client.attachedClients, {openChannel: chan}) !== undefined;
|
2016-09-25 06:41:10 +00:00
|
|
|
|
2018-06-11 11:29:57 +00:00
|
|
|
if (msg.self) {
|
|
|
|
// reset counters/markers when receiving self-/echo-message
|
|
|
|
this.unread = 0;
|
2018-07-17 08:41:12 +00:00
|
|
|
this.firstUnread = msg.id;
|
2018-06-11 11:29:57 +00:00
|
|
|
this.highlight = 0;
|
|
|
|
} else if (!isOpen) {
|
|
|
|
if (!this.firstUnread) {
|
|
|
|
this.firstUnread = msg.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (increasesUnread || msg.highlight) {
|
|
|
|
obj.unread = ++this.unread;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (msg.highlight) {
|
|
|
|
obj.highlight = ++this.highlight;
|
|
|
|
}
|
2016-09-25 06:41:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
client.emit("msg", obj);
|
2016-04-19 10:28:07 +00:00
|
|
|
|
|
|
|
// Never store messages in public mode as the session
|
|
|
|
// is completely destroyed when the page gets closed
|
2016-06-08 09:26:24 +00:00
|
|
|
if (Helper.config.public) {
|
2016-04-19 10:28:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-11-28 17:56:53 +00:00
|
|
|
this.writeUserLog(client, msg);
|
2017-09-14 07:41:50 +00:00
|
|
|
|
2016-06-08 09:26:24 +00:00
|
|
|
if (Helper.config.maxHistory >= 0 && this.messages.length > Helper.config.maxHistory) {
|
2017-07-06 15:33:09 +00:00
|
|
|
const deleted = this.messages.splice(0, this.messages.length - Helper.config.maxHistory);
|
|
|
|
|
2017-08-11 12:02:58 +00:00
|
|
|
// If maxHistory is 0, image would be dereferenced before client had a chance to retrieve it,
|
|
|
|
// so for now, just don't implement dereferencing for this edge case.
|
2018-10-03 11:54:41 +00:00
|
|
|
if (Helper.config.maxHistory > 0) {
|
2017-08-11 12:02:58 +00:00
|
|
|
this.dereferencePreviews(deleted);
|
2017-07-06 15:33:09 +00:00
|
|
|
}
|
2016-04-19 10:20:18 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-08-11 12:02:58 +00:00
|
|
|
Chan.prototype.dereferencePreviews = function(messages) {
|
2018-10-03 11:54:41 +00:00
|
|
|
if (!Helper.config.prefetch || !Helper.config.prefetchStorage) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-08-11 12:02:58 +00:00
|
|
|
messages.forEach((message) => {
|
2018-10-03 11:54:41 +00:00
|
|
|
if (message.previews) {
|
|
|
|
message.previews.forEach((preview) => {
|
|
|
|
if (preview.thumb) {
|
|
|
|
storage.dereference(preview.thumb);
|
|
|
|
preview.thumb = null;
|
|
|
|
}
|
|
|
|
});
|
2017-08-11 12:02:58 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2017-11-16 20:32:03 +00:00
|
|
|
Chan.prototype.getSortedUsers = function(irc) {
|
2018-02-07 09:56:49 +00:00
|
|
|
const users = Array.from(this.users.values());
|
|
|
|
|
|
|
|
if (!irc || !irc.network || !irc.network.options || !irc.network.options.PREFIX) {
|
|
|
|
return users;
|
|
|
|
}
|
|
|
|
|
2018-01-11 11:33:36 +00:00
|
|
|
const userModeSortPriority = {};
|
2016-10-12 07:55:40 +00:00
|
|
|
irc.network.options.PREFIX.forEach((prefix, index) => {
|
2016-03-14 11:44:06 +00:00
|
|
|
userModeSortPriority[prefix.symbol] = index;
|
|
|
|
});
|
|
|
|
|
|
|
|
userModeSortPriority[""] = 99; // No mode is lowest
|
|
|
|
|
2017-11-16 20:32:03 +00:00
|
|
|
return users.sort(function(a, b) {
|
2016-03-14 11:44:06 +00:00
|
|
|
if (a.mode === b.mode) {
|
2017-06-01 18:54:46 +00:00
|
|
|
return a.nick.toLowerCase() < b.nick.toLowerCase() ? -1 : 1;
|
2016-03-14 11:44:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
|
|
|
|
});
|
2014-09-13 21:29:45 +00:00
|
|
|
};
|
|
|
|
|
2017-07-24 06:01:25 +00:00
|
|
|
Chan.prototype.findMessage = function(msgId) {
|
|
|
|
return this.messages.find((message) => message.id === msgId);
|
|
|
|
};
|
|
|
|
|
2017-07-10 10:56:37 +00:00
|
|
|
Chan.prototype.findUser = function(nick) {
|
2017-11-16 20:32:03 +00:00
|
|
|
return this.users.get(nick.toLowerCase());
|
2017-07-10 10:56:37 +00:00
|
|
|
};
|
|
|
|
|
2017-11-08 21:10:17 +00:00
|
|
|
Chan.prototype.getUser = function(nick) {
|
2018-03-05 00:59:16 +00:00
|
|
|
return this.findUser(nick) || new User({nick});
|
2017-11-08 21:10:17 +00:00
|
|
|
};
|
|
|
|
|
2017-11-16 20:32:03 +00:00
|
|
|
Chan.prototype.setUser = function(user) {
|
|
|
|
this.users.set(user.nick.toLowerCase(), user);
|
|
|
|
};
|
|
|
|
|
|
|
|
Chan.prototype.removeUser = function(user) {
|
|
|
|
this.users.delete(user.nick.toLowerCase());
|
|
|
|
};
|
|
|
|
|
2017-11-29 19:54:09 +00:00
|
|
|
/**
|
|
|
|
* Get a clean clone of this channel that will be sent to the client.
|
|
|
|
* This function performs manual cloning of channel object for
|
|
|
|
* better control of performance and memory usage.
|
|
|
|
*
|
|
|
|
* @param {(int|bool)} lastActiveChannel - Last known active user channel id (needed to control how many messages are sent)
|
|
|
|
* If true, channel is assumed active.
|
|
|
|
* @param {int} lastMessage - Last message id seen by active client to avoid sending duplicates.
|
|
|
|
*/
|
|
|
|
Chan.prototype.getFilteredClone = function(lastActiveChannel, lastMessage) {
|
|
|
|
return Object.keys(this).reduce((newChannel, prop) => {
|
|
|
|
if (prop === "users") {
|
|
|
|
// Do not send users, client requests updated user list whenever needed
|
|
|
|
newChannel[prop] = [];
|
|
|
|
} else if (prop === "messages") {
|
|
|
|
// If client is reconnecting, only send new messages that client has not seen yet
|
|
|
|
if (lastMessage > -1) {
|
2017-12-23 09:36:52 +00:00
|
|
|
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
|
2018-02-21 17:48:22 +00:00
|
|
|
// See https://github.com/thelounge/thelounge/issues/1883
|
2017-11-29 19:54:09 +00:00
|
|
|
newChannel[prop] = this[prop]
|
|
|
|
.filter((m) => m.id > lastMessage)
|
2017-12-23 09:36:52 +00:00
|
|
|
.slice(-100);
|
2018-07-10 16:51:45 +00:00
|
|
|
newChannel.moreHistoryAvailable = this[prop].length > 100;
|
2017-11-29 19:54:09 +00:00
|
|
|
} else {
|
2017-12-23 09:36:52 +00:00
|
|
|
// If channel is active, send up to 100 last messages, for all others send just 1
|
|
|
|
// Client will automatically load more messages whenever needed based on last seen messages
|
2018-07-10 16:51:45 +00:00
|
|
|
const messagesToSend = lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
|
2017-12-23 09:36:52 +00:00
|
|
|
|
2018-07-10 16:51:45 +00:00
|
|
|
newChannel[prop] = this[prop].slice(-messagesToSend);
|
|
|
|
newChannel.moreHistoryAvailable = this[prop].length > messagesToSend;
|
2017-11-29 19:54:09 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
newChannel[prop] = this[prop];
|
|
|
|
}
|
|
|
|
|
|
|
|
return newChannel;
|
|
|
|
}, {});
|
2014-09-13 21:29:45 +00:00
|
|
|
};
|
2017-09-14 07:41:50 +00:00
|
|
|
|
2017-11-28 17:56:53 +00:00
|
|
|
Chan.prototype.writeUserLog = function(client, msg) {
|
|
|
|
this.messages.push(msg);
|
|
|
|
|
2018-04-17 08:06:08 +00:00
|
|
|
// Are there any logs enabled
|
|
|
|
if (client.messageStorage.length === 0) {
|
2017-11-28 17:56:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-12 11:37:22 +00:00
|
|
|
let targetChannel = this;
|
|
|
|
|
2017-11-28 17:56:53 +00:00
|
|
|
// Is this particular message or channel loggable
|
|
|
|
if (!msg.isLoggable() || !this.isLoggable()) {
|
2018-06-12 11:37:22 +00:00
|
|
|
// Because notices are nasty and can be shown in active channel on the client
|
|
|
|
// if there is no open query, we want to always log notices in the sender's name
|
|
|
|
if (msg.type === Msg.Type.NOTICE && msg.showInActive) {
|
|
|
|
targetChannel = {
|
|
|
|
name: msg.from.nick,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
}
|
2017-11-28 17:56:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Find the parent network where this channel is in
|
2017-09-14 07:41:50 +00:00
|
|
|
const target = client.find(this.id);
|
|
|
|
|
2017-09-29 07:37:27 +00:00
|
|
|
if (!target) {
|
2017-11-28 17:56:53 +00:00
|
|
|
return;
|
2017-09-29 07:37:27 +00:00
|
|
|
}
|
|
|
|
|
2018-04-17 08:06:08 +00:00
|
|
|
for (const messageStorage of client.messageStorage) {
|
2018-06-12 11:37:22 +00:00
|
|
|
messageStorage.index(target.network, targetChannel, msg);
|
2017-11-28 17:56:53 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Chan.prototype.loadMessages = function(client, network) {
|
2018-04-17 08:06:08 +00:00
|
|
|
if (!this.isLoggable()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const messageStorage = client.messageStorage.find((s) => s.canProvideMessages());
|
|
|
|
|
|
|
|
if (!messageStorage) {
|
2017-11-28 17:56:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-17 08:06:08 +00:00
|
|
|
messageStorage
|
2017-11-28 17:56:53 +00:00
|
|
|
.getMessages(network, this)
|
|
|
|
.then((messages) => {
|
|
|
|
if (messages.length === 0) {
|
2018-02-02 21:38:25 +00:00
|
|
|
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
|
|
|
requestZncPlayback(this, network, 0);
|
|
|
|
}
|
|
|
|
|
2017-11-28 17:56:53 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.messages.unshift(...messages);
|
|
|
|
|
|
|
|
if (!this.firstUnread) {
|
|
|
|
this.firstUnread = messages[messages.length - 1].id;
|
|
|
|
}
|
|
|
|
|
|
|
|
client.emit("more", {
|
|
|
|
chan: this.id,
|
|
|
|
messages: messages.slice(-100),
|
2019-02-13 10:23:17 +00:00
|
|
|
moreHistoryAvailable: messages.length > 100,
|
2017-11-28 17:56:53 +00:00
|
|
|
});
|
2018-02-02 21:38:25 +00:00
|
|
|
|
|
|
|
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
|
|
|
|
const from = Math.floor(messages[messages.length - 1].time.getTime() / 1000);
|
|
|
|
|
|
|
|
requestZncPlayback(this, network, from);
|
|
|
|
}
|
2017-11-28 17:56:53 +00:00
|
|
|
})
|
|
|
|
.catch((err) => log.error(`Failed to load messages: ${err}`));
|
|
|
|
};
|
|
|
|
|
|
|
|
Chan.prototype.isLoggable = function() {
|
|
|
|
return this.type === Chan.Type.CHANNEL || this.type === Chan.Type.QUERY;
|
|
|
|
};
|
2018-02-02 21:38:25 +00:00
|
|
|
|
|
|
|
function requestZncPlayback(channel, network, from) {
|
|
|
|
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
|
|
|
|
}
|