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.
100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import {expect} from "chai";
|
|
|
|
import Msg from "../../../server/models/msg";
|
|
import Network from "../../../server/models/network";
|
|
import Chan from "../../../server/models/chan";
|
|
import {MessageType} from "../../../shared/types/msg";
|
|
import Config from "../../../server/config";
|
|
import TextFileMessageStorage from "../../../server/plugins/messageStorage/text";
|
|
|
|
describe("TextFileMessageStorage", function () {
|
|
let tmpHome: string;
|
|
let originalHome: string;
|
|
|
|
beforeEach(function () {
|
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "tl-textstorage-"));
|
|
originalHome = Config.getHomePath();
|
|
Config.setHome(tmpHome);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Config.setHome(originalHome);
|
|
fs.rmSync(tmpHome, {recursive: true, force: true});
|
|
});
|
|
|
|
function logPathFor(network: Network, channel: Chan) {
|
|
return path.join(
|
|
Config.getUserLogsPath(),
|
|
"alice",
|
|
TextFileMessageStorage.getNetworkFolderName(network),
|
|
TextFileMessageStorage.getChannelFileName(channel)
|
|
);
|
|
}
|
|
|
|
it("writes a single line for a normal message", function () {
|
|
const storage = new TextFileMessageStorage("alice");
|
|
storage.enable();
|
|
|
|
const network = new Network({name: "freenode"});
|
|
const channel = new Chan({name: "#test"});
|
|
const msg = new Msg({
|
|
type: MessageType.MESSAGE,
|
|
text: "hello world",
|
|
from: {nick: "bob", mode: ""},
|
|
time: new Date("2026-05-01T12:00:00.000Z"),
|
|
});
|
|
|
|
storage.index(network, channel, msg);
|
|
|
|
const contents = fs.readFileSync(logPathFor(network, channel), "utf8");
|
|
expect(contents).to.equal("[2026-05-01T12:00:00.000Z] <bob> hello world\n");
|
|
});
|
|
|
|
it("re-prefixes continuation lines so each row stays parseable", function () {
|
|
const storage = new TextFileMessageStorage("alice");
|
|
storage.enable();
|
|
|
|
const network = new Network({name: "freenode"});
|
|
const channel = new Chan({name: "#test"});
|
|
const msg = new Msg({
|
|
type: MessageType.MESSAGE,
|
|
text: "line one\nline two\nline three",
|
|
from: {nick: "bob", mode: ""},
|
|
time: new Date("2026-05-01T12:00:00.000Z"),
|
|
});
|
|
|
|
storage.index(network, channel, msg);
|
|
|
|
const contents = fs.readFileSync(logPathFor(network, channel), "utf8");
|
|
expect(contents).to.equal(
|
|
"[2026-05-01T12:00:00.000Z] <bob> line one\n" +
|
|
"[2026-05-01T12:00:00.000Z] | line two\n" +
|
|
"[2026-05-01T12:00:00.000Z] | line three\n"
|
|
);
|
|
});
|
|
|
|
it("preserves embedded newlines in ACTION messages", function () {
|
|
const storage = new TextFileMessageStorage("alice");
|
|
storage.enable();
|
|
|
|
const network = new Network({name: "freenode"});
|
|
const channel = new Chan({name: "#test"});
|
|
const msg = new Msg({
|
|
type: MessageType.ACTION,
|
|
text: "first\nsecond",
|
|
from: {nick: "bob", mode: "@"},
|
|
time: new Date("2026-05-01T12:00:00.000Z"),
|
|
});
|
|
|
|
storage.index(network, channel, msg);
|
|
|
|
const contents = fs.readFileSync(logPathFor(network, channel), "utf8");
|
|
expect(contents).to.equal(
|
|
"[2026-05-01T12:00:00.000Z] * @bob first\n" + "[2026-05-01T12:00:00.000Z] | second\n"
|
|
);
|
|
});
|
|
});
|