thelounge/src/models/chan.js

305 lines
7.8 KiB
JavaScript

"use strict";
const _ = require("lodash");
const log = require("../log");
const Helper = require("../helper");
const User = require("./user");
const Msg = require("./msg");
const storage = require("../plugins/storage");
module.exports = Chan;
Chan.Type = {
CHANNEL: "channel",
LOBBY: "lobby",
QUERY: "query",
SPECIAL: "special",
};
Chan.SpecialType = {
BANLIST: "list_bans",
INVITELIST: "list_invites",
CHANNELLIST: "list_channels",
IGNORELIST: "list_ignored",
};
Chan.State = {
PARTED: 0,
JOINED: 1,
};
function Chan(attr) {
_.defaults(this, attr, {
id: 0,
messages: [],
name: "",
key: "",
topic: "",
type: Chan.Type.CHANNEL,
state: Chan.State.PARTED,
firstUnread: 0,
unread: 0,
highlight: 0,
users: new Map(),
muted: false,
});
}
Chan.prototype.destroy = function () {
this.dereferencePreviews(this.messages);
};
Chan.prototype.pushMessage = function (client, msg, increasesUnread) {
const chan = this.id;
const obj = {chan, msg};
msg.id = client.idMsg++;
// If this channel is open in any of the clients, do not increase unread counter
const isOpen = _.find(client.attachedClients, {openChannel: chan}) !== undefined;
if (msg.self) {
// reset counters/markers when receiving self-/echo-message
this.unread = 0;
this.firstUnread = msg.id;
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;
}
}
client.emit("msg", obj);
// Never store messages in public mode as the session
// is completely destroyed when the page gets closed
if (Helper.config.public) {
return;
}
// showInActive is only processed on "msg", don't need it on page reload
if (msg.showInActive) {
delete msg.showInActive;
}
this.writeUserLog(client, msg);
if (Helper.config.maxHistory >= 0 && this.messages.length > Helper.config.maxHistory) {
const deleted = this.messages.splice(0, this.messages.length - Helper.config.maxHistory);
// 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.
if (Helper.config.maxHistory > 0) {
this.dereferencePreviews(deleted);
}
}
};
Chan.prototype.dereferencePreviews = function (messages) {
if (!Helper.config.prefetch || !Helper.config.prefetchStorage) {
return;
}
messages.forEach((message) => {
if (message.previews) {
message.previews.forEach((preview) => {
if (preview.thumb) {
storage.dereference(preview.thumb);
preview.thumb = "";
}
});
}
});
};
Chan.prototype.getSortedUsers = function (irc) {
const users = Array.from(this.users.values());
if (!irc || !irc.network || !irc.network.options || !irc.network.options.PREFIX) {
return users;
}
const userModeSortPriority = {};
irc.network.options.PREFIX.forEach((prefix, index) => {
userModeSortPriority[prefix.symbol] = index;
});
userModeSortPriority[""] = 99; // No mode is lowest
return users.sort(function (a, b) {
if (a.mode === b.mode) {
return a.nick.toLowerCase() < b.nick.toLowerCase() ? -1 : 1;
}
return userModeSortPriority[a.mode] - userModeSortPriority[b.mode];
});
};
Chan.prototype.findMessage = function (msgId) {
return this.messages.find((message) => message.id === msgId);
};
Chan.prototype.findUser = function (nick) {
return this.users.get(nick.toLowerCase());
};
Chan.prototype.getUser = function (nick) {
return this.findUser(nick) || new User({nick});
};
Chan.prototype.setUser = function (user) {
this.users.set(user.nick.toLowerCase(), user);
};
Chan.prototype.removeUser = function (user) {
this.users.delete(user.nick.toLowerCase());
};
/**
* 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) {
// When reconnecting, always send up to 100 messages to prevent message gaps on the client
// See https://github.com/thelounge/thelounge/issues/1883
newChannel[prop] = this[prop].filter((m) => m.id > lastMessage).slice(-100);
} else {
// 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
const messagesToSend =
lastActiveChannel === true || this.id === lastActiveChannel ? 100 : 1;
newChannel[prop] = this[prop].slice(-messagesToSend);
}
newChannel.totalMessages = this[prop].length;
} else {
newChannel[prop] = this[prop];
}
return newChannel;
}, {});
};
Chan.prototype.writeUserLog = function (client, msg) {
this.messages.push(msg);
// Are there any logs enabled
if (client.messageStorage.length === 0) {
return;
}
let targetChannel = this;
// Is this particular message or channel loggable
if (!msg.isLoggable() || !this.isLoggable()) {
// 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;
}
}
// Find the parent network where this channel is in
const target = client.find(this.id);
if (!target) {
return;
}
for (const messageStorage of client.messageStorage) {
messageStorage.index(target.network, targetChannel, msg);
}
};
Chan.prototype.loadMessages = function (client, network) {
if (!this.isLoggable()) {
return;
}
if (!network.irc) {
// Network created, but misconfigured
log.warn(
`Failed to load messages for ${client.name}, network ${network.name} is not initialized.`
);
return;
}
if (!client.messageProvider) {
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
// if we do have a message provider we might be able to only fetch partial history,
// so delay the cap in this case.
requestZncPlayback(this, network, 0);
}
return;
}
client.messageProvider
.getMessages(network, this)
.then((messages) => {
if (messages.length === 0) {
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
requestZncPlayback(this, network, 0);
}
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),
totalMessages: messages.length,
});
if (network.irc.network.cap.isEnabled("znc.in/playback")) {
const from = Math.floor(messages[messages.length - 1].time.getTime() / 1000);
requestZncPlayback(this, network, from);
}
})
.catch((err) => log.error(`Failed to load messages for ${client.name}: ${err}`));
};
Chan.prototype.isLoggable = function () {
return this.type === Chan.Type.CHANNEL || this.type === Chan.Type.QUERY;
};
Chan.prototype.setMuteStatus = function (muted) {
this.muted = !!muted;
};
function requestZncPlayback(channel, network, from) {
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
}