thelounge/server/models/chan.ts
Reto Brunner fc9805545b sharedMsg: remove userAway
userAway is purely server side and we don't send it to the client
2024-04-21 15:11:52 +02:00

347 lines
9 KiB
TypeScript

import _ from "lodash";
import log from "../log";
import Config from "../config";
import User from "./user";
import Msg from "./msg";
import storage from "../plugins/storage";
import Client from "../client";
import Network from "./network";
import Prefix from "./prefix";
import {MessageType, SharedMsg} from "../../shared/types/msg";
import {ChanType, SpecialChanType, ChanState} from "../../shared/types/chan";
import {SharedNetworkChan} from "../../shared/types/network";
export type ChanConfig = {
name: string;
key?: string;
muted?: boolean;
type?: string;
};
class Chan {
// TODO: don't force existence, figure out how to make TS infer it.
id!: number;
messages!: Msg[];
name!: string;
key!: string;
topic!: string;
firstUnread!: number;
unread!: number;
highlight!: number;
users!: Map<string, User>;
muted!: boolean;
type!: ChanType;
state!: ChanState;
userAway?: boolean;
special?: SpecialChanType;
data?: any;
closed?: boolean;
num_users?: number;
constructor(attr?: Partial<Chan>) {
_.defaults(this, attr, {
id: 0,
messages: [],
name: "",
key: "",
topic: "",
type: ChanType.CHANNEL,
state: ChanState.PARTED,
firstUnread: 0,
unread: 0,
highlight: 0,
users: new Map(),
muted: false,
});
}
destroy() {
this.dereferencePreviews(this.messages);
}
pushMessage(client: Client, msg: Msg, increasesUnread = false) {
const chanId = this.id;
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: chanId}) !== 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) {
this.unread++;
}
if (msg.highlight) {
this.highlight++;
}
}
client.emit("msg", {chan: chanId, msg, unread: this.unread, highlight: this.highlight});
// Never store messages in public mode as the session
// is completely destroyed when the page gets closed
if (Config.values.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 (Config.values.maxHistory >= 0 && this.messages.length > Config.values.maxHistory) {
const deleted = this.messages.splice(
0,
this.messages.length - Config.values.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 (Config.values.maxHistory > 0) {
this.dereferencePreviews(deleted);
}
}
}
dereferencePreviews(messages: Msg[]) {
if (!Config.values.prefetch || !Config.values.prefetchStorage) {
return;
}
messages.forEach((message) => {
if (message.previews) {
message.previews.forEach((preview) => {
if (preview.thumb) {
storage.dereference(preview.thumb);
preview.thumb = "";
}
});
}
});
}
getSortedUsers(irc?: Network["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];
});
}
findMessage(msgId: number) {
return this.messages.find((message) => message.id === msgId);
}
findUser(nick: string) {
return this.users.get(nick.toLowerCase());
}
getUser(nick: string) {
return this.findUser(nick) || new User({nick}, new Prefix([]));
}
setUser(user: User) {
this.users.set(user.nick.toLowerCase(), user);
}
removeUser(user: 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.
*/
getFilteredClone(
lastActiveChannel?: number | boolean,
lastMessage?: number
): SharedNetworkChan {
let msgs: SharedMsg[];
// If client is reconnecting, only send new messages that client has not seen yet
if (lastMessage && 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
msgs = this.messages.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;
msgs = this.messages.slice(-messagesToSend);
}
return {
id: this.id,
messages: msgs,
totalMessages: this.messages.length,
name: this.name,
key: this.key,
topic: this.topic,
firstUnread: this.firstUnread,
unread: this.unread,
highlight: this.highlight,
muted: this.muted,
type: this.type,
state: this.state,
special: this.special,
data: this.data,
closed: this.closed,
num_users: this.num_users,
};
// TODO: funny array mutation below might need to be reproduced
// static optionalProperties = ["userAway", "special", "data", "closed", "num_users"];
// return Object.keys(this).reduce((newChannel, prop) => {
// if (Chan.optionalProperties.includes(prop)) {
// if (this[prop] !== undefined || (Array.isArray(this[prop]) && this[prop].length)) {
// newChannel[prop] = this[prop];
// }
// }
}
writeUserLog(client: Client, msg: Msg) {
this.messages.push(msg);
// Are there any logs enabled
if (client.messageStorage.length === 0) {
return;
}
const targetChannel: Chan = 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 === MessageType.NOTICE && msg.showInActive) {
targetChannel.name = msg.from.nick || ""; // TODO: check if || works
} 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).catch((e) => log.error(e));
}
}
loadMessages(client: Client, network: 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, () => client.idMsg++)
.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: Error) =>
log.error(`Failed to load messages for ${client.name}: ${err.toString()}`)
);
}
isLoggable() {
return this.type === ChanType.CHANNEL || this.type === ChanType.QUERY;
}
setMuteStatus(muted: boolean) {
this.muted = !!muted;
}
}
function requestZncPlayback(channel: Chan, network: Network, from: number) {
if (!network.irc) {
throw new Error(
`requestZncPlayback: no irc field on network "${network.name}", this is a bug`
);
}
network.irc.raw("ZNC", "*playback", "PLAY", channel.name, from.toString());
}
export default Chan;
export type Channel = Chan;