mirror of
https://github.com/thelounge/thelounge.git
synced 2026-07-30 22:55:50 -04:00
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.
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
import {expect} from "chai";
|
|
|
|
import msgInput from "../../../server/plugins/inputs/msg";
|
|
import Chan from "../../../server/models/chan";
|
|
import {ChanType} from "../../../shared/types/chan";
|
|
|
|
type Sent = {type: "say" | "multiline"; target: string; text: string};
|
|
|
|
function setup({limits}: {limits: {maxBytes: number; maxLines: number | null} | null}) {
|
|
const sent: Sent[] = [];
|
|
const echoed: Array<{message: string; multiline?: boolean}> = [];
|
|
const chan = new Chan({name: "#thelounge", type: ChanType.CHANNEL});
|
|
|
|
const irc = {
|
|
user: {nick: "alice", username: "alice", host: "localhost"},
|
|
network: {
|
|
cap: {isEnabled: (cap: string) => cap === "draft/multiline"},
|
|
multilineLimits: () => limits,
|
|
extractTargetGroup: () => undefined,
|
|
},
|
|
say(target: string, text: string) {
|
|
sent.push({type: "say", target, text});
|
|
},
|
|
sayMultiline(target: string, lines: string[]) {
|
|
if (limits && lines.join("\n").length > limits.maxBytes) {
|
|
const error = new Error("too long") as Error & {code: string};
|
|
error.code = "MULTILINE_MAX_BYTES";
|
|
throw error;
|
|
}
|
|
|
|
sent.push({type: "multiline", target, text: lines.join("\n")});
|
|
},
|
|
emit(_event: string, data: {message: string; multiline?: boolean}) {
|
|
echoed.push(data);
|
|
},
|
|
};
|
|
|
|
const network = {
|
|
irc,
|
|
serverOptions: {supportsReply: false},
|
|
getChannel: (name: string) => (name === chan.name ? chan : undefined),
|
|
};
|
|
|
|
const send = (text: string) =>
|
|
msgInput.input.call({} as any, network as any, chan, "say", text.split(" "), undefined);
|
|
|
|
return {send, sent, echoed};
|
|
}
|
|
|
|
describe("msg input", function () {
|
|
it("sends a multiline message as a single batch", function () {
|
|
const {send, sent, echoed} = setup({limits: {maxBytes: 4096, maxLines: null}});
|
|
|
|
send("one\ntwo");
|
|
|
|
expect(sent).to.deep.equal([{type: "multiline", target: "#thelounge", text: "one\ntwo"}]);
|
|
expect(echoed).to.have.lengthOf(1);
|
|
expect(echoed[0].message).to.equal("one\ntwo");
|
|
expect(echoed[0].multiline).to.equal(true);
|
|
});
|
|
|
|
it("sends one message per line when the network advertises no limits", function () {
|
|
const {send, sent, echoed} = setup({limits: null});
|
|
|
|
send("one\ntwo");
|
|
|
|
expect(sent.map((s) => s.text)).to.deep.equal(["one", "two"]);
|
|
expect(echoed.map((e) => e.message)).to.deep.equal(["one", "two"]);
|
|
expect(echoed.every((e) => e.multiline === undefined)).to.equal(true);
|
|
});
|
|
|
|
it("falls back to individual messages when the batch is over max-bytes", function () {
|
|
const {send, sent, echoed} = setup({limits: {maxBytes: 4, maxLines: null}});
|
|
|
|
send("one\ntwo");
|
|
|
|
// The echo has to match what actually went out, not what we attempted
|
|
expect(sent.map((s) => s.type)).to.deep.equal(["say", "say"]);
|
|
expect(echoed.map((e) => e.message)).to.deep.equal(["one", "two"]);
|
|
expect(echoed.every((e) => e.multiline === undefined)).to.equal(true);
|
|
});
|
|
|
|
it("does not send blank lines", function () {
|
|
const {send, sent, echoed} = setup({limits: null});
|
|
|
|
send("one\n\ntwo\n");
|
|
|
|
expect(sent.map((s) => s.text)).to.deep.equal(["one", "two"]);
|
|
expect(echoed.map((e) => e.message)).to.deep.equal(["one", "two"]);
|
|
});
|
|
|
|
it("keeps sending single line messages unbatched", function () {
|
|
const {send, sent, echoed} = setup({limits: {maxBytes: 4096, maxLines: null}});
|
|
|
|
send("hello");
|
|
|
|
expect(sent).to.deep.equal([{type: "say", target: "#thelounge", text: "hello"}]);
|
|
expect(echoed.map((e) => e.message)).to.deep.equal(["hello"]);
|
|
});
|
|
});
|