Files
thelounge/server/plugins/irc-events/error.ts
Max Leiter 78cadc5ff1 ircv3: monitor and extended-monitor (#5045)
Updated version of #4550. Closes #337, closes #872 (#337 can't be fully
addressed — tracking nick changes isn't really possible atm).

<img width="253" height="149" alt="image"
src="https://github.com/user-attachments/assets/8a961868-0aa9-406b-92e2-f9d4858e2d0d"
/>

Away support:
<img width="214" alt="image"
src="https://user-images.githubusercontent.com/8675906/165882385-6f928c8e-5a0b-4502-8bd3-3cd0bd70e0e0.png">

## Summary

Query windows show whether the person behind them is online, and whether
they are away, using
[MONITOR](https://ircv3.net/specs/extensions/monitor) rather than
polling. `Network` keeps the monitor list: queries are registered in
batched `MONITOR +` commands once RPL_ISUPPORT tells us the server
supports it, and one at a time as new query windows open. The limit
advertised in `MONITOR=<n>` is respected, with anything over it waiting
for a slot; `MONITOR=0` means no monitoring at all, a bare `MONITOR`
means no limit. A nick change on a monitored query swaps the target in
place so the query keeps its slot.

[extended-monitor](https://ircv3.net/specs/extensions/extended-monitor)
is requested too, so away, nick, account and chghost updates arrive for
monitored nicks we share no channel with. Away state for people we do
share a channel with comes from a WHO on join.

The indicator is a dot on the query in the sidebar and next to the nick
in the message bubble, with a tooltip and an `img` role so it isn't
colour-only, and it can be turned off under Appearance.

Known limitation: a server that enforces a monitor limit it doesn't
advertise will reject part of a batch with `ERR_MONLISTFULL`; the user
gets a message in the lobby, but the client's own list isn't reconciled
until reconnect.

## Test Plan

- Unit tests for the monitor list: add, case-insensitive dedupe,
queueing at the limit, removal draining the queue, batching, and the
rename path.
- Ran the server against a local IRCd and watched the wire: opening
queries sends `MONITOR + <nick>` each, a reconnect with four open
queries sends a single `MONITOR + bob,carol,dave,mallory`, 730/731 drive
the online/offline state, a rename sends `MONITOR - bob` then `MONITOR +
bob2`, `MONITOR=0` sends nothing at all, and a bare `MONITOR` behaves as
unlimited.
2026-07-22 09:46:58 -07:00

99 lines
2.6 KiB
TypeScript

import {IrcEventHandler} from "../../client";
import Msg from "../../models/msg";
import Config from "../../config";
import {MessageType} from "../../../shared/types/msg";
export default <IrcEventHandler>function (irc, network) {
const client = this;
irc.on("irc error", function (data) {
if (data.error === "monitor_list_full") {
return;
}
const msg = new Msg({
type: MessageType.ERROR,
error: data.error,
showInActive: true,
nick: data.nick,
channel: data.channel,
reason: data.reason,
command: data.command,
});
let target = network.getLobby();
// If this error is channel specific and a channel
// with this name exists, put this error in that channel
if (data.channel) {
const channel = network.getChannel(data.channel);
if (typeof channel !== "undefined") {
target = channel;
msg.showInActive = false;
}
}
target.pushMessage(client, msg, true);
});
irc.on("nick in use", function (data) {
let message = data.nick + ": " + (data.reason || "Nickname is already in use.");
if (irc.connection.registered === false && !Config.values.public) {
message += " An attempt to use it will be made when this nick quits.";
// Clients usually get nick in use on connect when reconnecting to a network
// after a network failure (like ping timeout), and as a result of that,
// TL will append a random number to the nick.
// keepNick will try to set the original nick name back if it sees a QUIT for that nick.
network.keepNick = irc.user.nick;
}
const lobby = network.getLobby();
const msg = new Msg({
type: MessageType.ERROR,
text: message,
showInActive: true,
});
lobby.pushMessage(client, msg, true);
if (irc.connection.registered === false) {
const nickLen = parseInt(network.irc.network.options.NICKLEN, 10) || 16;
const random = (data.nick || irc.user.nick) + Math.floor(Math.random() * 10);
// Safeguard nick changes up to allowed length
// Some servers may send "nick in use" error even for randomly generated nicks
if (random.length <= nickLen) {
irc.changeNick(random);
}
}
client.emit("nick", {
network: network.uuid,
nick: irc.user.nick,
});
});
irc.on("nick invalid", function (data) {
const lobby = network.getLobby();
const msg = new Msg({
type: MessageType.ERROR,
text: data.nick + ": " + (data.reason || "Nickname is invalid."),
showInActive: true,
});
lobby.pushMessage(client, msg, true);
if (irc.connection.registered === false) {
irc.changeNick(Config.getDefaultNick());
}
client.emit("nick", {
network: network.uuid,
nick: irc.user.nick,
});
});
};