Files
thelounge/test/plugins/messageStorage/textTest.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

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"
);
});
});