Files
thelounge/server/models/msg.ts
Max Leiter 8ca7bca70b ircv3: draft/multiline (#5092)
Closes #5052

Multi-line input is currently sent as one PRIVMSG per line, so a paste
arrives as N unrelated messages. On networks that support
[draft/multiline](https://ircv3.net/specs/extensions/multiline) it can
be sent as a single BATCH and stays one message everywhere.

## Summary

Sending goes through `sayMultiline()` in `plugins/inputs/msg.ts` when
the network advertises limits we can respect (`multilineLimits()`); if
the batch is over max-bytes/max-lines the send falls back to individual
messages, and the local echo follows whichever path was actually taken
so the sender sees what everyone else got. `Client.input()` stops
splitting on newlines when the target network has the cap. Inbound
batches arrive from irc-framework already reassembled and are flagged
`multiline` on the `Msg`, which drives line-by-line rendering (`parse()`
joins lines with `<br>`), one entry per message in the text logs, and a
first-non-empty-line notification preview. `MULTILINE_*` standard
replies get readable text.

No pin bump: `master` already points at the irc-framework commit that
carries multiline (kiwiirc/irc-framework#414).

## Test Plan

- New unit tests for the send path (batch, fallback on max-bytes, no
limits advertised, blank lines), the text log format, `parse()`, and the
standard replies.
- Ran the server against a local IRCd advertising `draft/multiline` and
confirmed on the wire: a 3-line message goes out as one `BATCH …
draft/multiline` with three frames; with the cap advertised bare (no
limits) or with `max-bytes` too small it degrades to three plain
PRIVMSGs and the local echo degrades with it; a CRLF paste sends clean
frames and echoes without stray `\r`; an inbound batch from another user
renders as one message.
- Not verified: rendering was checked through unit tests rather than in
a browser.
2026-07-20 14:45:45 -07:00

104 lines
2.1 KiB
TypeScript

import _ from "lodash";
import {MessageType, LinkPreview, UserInMessage} from "../../shared/types/msg";
class Msg {
from!: UserInMessage;
id!: number;
msgid?: string;
replyTo?: string;
replyToNick?: string;
replyToText?: string;
previews!: LinkPreview[];
text!: string;
type!: MessageType;
self!: boolean;
time!: Date;
hostmask!: string;
target!: UserInMessage;
// TODO: new_nick is only on MessageType.NICK,
// we should probably make Msgs that extend this class and use those
// throughout. I'll leave any similar fields below.
new_nick!: string;
highlight?: boolean;
showInActive?: boolean;
new_ident!: string;
new_host!: string;
ctcpMessage!: string;
command!: string;
invitedYou!: boolean;
gecos!: string;
account!: boolean;
// these are all just for error:
error!: string;
nick!: string;
channel!: string;
reason!: string;
raw_modes!: any;
when!: Date;
whois!: any;
users!: string[];
statusmsgGroup?: string;
params!: string[];
multiline?: boolean;
constructor(attr?: Partial<Msg>) {
// Some properties need to be copied in the Msg object instead of referenced
if (attr) {
["from", "target"].forEach((prop) => {
if (attr[prop]) {
this[prop] = {
mode: attr[prop].mode,
nick: attr[prop].nick,
};
}
});
}
_.defaults(this, attr, {
from: {},
id: 0,
previews: [],
text: "",
type: MessageType.MESSAGE,
self: false,
});
if (this.time) {
this.time = new Date(this.time);
} else {
this.time = new Date();
}
}
findPreview(link: string) {
return this.previews.find((preview) => preview.link === link);
}
isLoggable() {
if (this.type === MessageType.TOPIC) {
// Do not log topic that is sent on channel join
return !!this.from.nick;
}
switch (this.type) {
case MessageType.MONOSPACE_BLOCK:
case MessageType.ERROR:
case MessageType.TOPIC_SET_BY:
case MessageType.MODE_CHANNEL:
case MessageType.MODE_USER:
case MessageType.RAW:
case MessageType.WHOIS:
case MessageType.PLUGIN:
return false;
default:
return true;
}
}
}
export default Msg;
export type Message = Msg;