mirror of
https://github.com/thelounge/thelounge.git
synced 2026-07-31 07:05:52 -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.
145 lines
4.1 KiB
TypeScript
145 lines
4.1 KiB
TypeScript
import {mkdirSync, appendFileSync} from "fs";
|
|
import path from "path";
|
|
import filenamify from "filenamify";
|
|
|
|
import Config from "../../config";
|
|
import {MessageStorage} from "./types";
|
|
import Channel from "../../models/chan";
|
|
import {Message} from "../../models/msg";
|
|
import Network from "../../models/network";
|
|
import {MessageType} from "../../../shared/types/msg";
|
|
|
|
class TextFileMessageStorage implements MessageStorage {
|
|
isEnabled: boolean;
|
|
username: string;
|
|
|
|
constructor(username: string) {
|
|
this.username = username;
|
|
this.isEnabled = false;
|
|
}
|
|
|
|
enable() {
|
|
this.isEnabled = true;
|
|
}
|
|
|
|
close() {
|
|
this.isEnabled = false;
|
|
}
|
|
|
|
index(network: Network, channel: Channel, msg: Message) {
|
|
if (!this.isEnabled) {
|
|
return;
|
|
}
|
|
|
|
const logPath = path.join(
|
|
Config.getUserLogsPath(),
|
|
this.username,
|
|
TextFileMessageStorage.getNetworkFolderName(network)
|
|
);
|
|
|
|
mkdirSync(logPath, {recursive: true});
|
|
|
|
let line = `[${msg.time.toISOString()}] `;
|
|
|
|
// message types from src/models/msg.js
|
|
switch (msg.type) {
|
|
case MessageType.ACTION:
|
|
// [2014-01-01 00:00:00] * @Arnold is eating cookies
|
|
line += `* ${msg.from.mode}${msg.from.nick} ${msg.text}`;
|
|
break;
|
|
case MessageType.JOIN:
|
|
// [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) joined
|
|
line += `*** ${msg.from.nick} (${msg.hostmask}) joined`;
|
|
break;
|
|
case MessageType.KICK:
|
|
// [2014-01-01 00:00:00] *** Arnold was kicked by Bernie (Don't steal my cookies!)
|
|
line += `*** ${msg.target.nick} was kicked by ${msg.from.nick} (${msg.text})`;
|
|
break;
|
|
case MessageType.MESSAGE:
|
|
// [2014-01-01 00:00:00] <@Arnold> Put that cookie down.. Now!!
|
|
line += `<${msg.from.mode}${msg.from.nick}> ${msg.text}`;
|
|
break;
|
|
case MessageType.MODE:
|
|
// [2014-01-01 00:00:00] *** Arnold set mode +o Bernie
|
|
line += `*** ${msg.from.nick} set mode ${msg.text}`;
|
|
break;
|
|
case MessageType.NICK:
|
|
// [2014-01-01 00:00:00] *** Arnold changed nick to Bernie
|
|
line += `*** ${msg.from.nick} changed nick to ${msg.new_nick}`;
|
|
break;
|
|
case MessageType.NOTICE:
|
|
// [2014-01-01 00:00:00] -Arnold- pssst, I have cookies!
|
|
line += `-${msg.from.nick}- ${msg.text}`;
|
|
break;
|
|
case MessageType.PART:
|
|
// [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) left (Bye all!)
|
|
line += `*** ${msg.from.nick} (${msg.hostmask}) left (${msg.text})`;
|
|
break;
|
|
case MessageType.QUIT:
|
|
// [2014-01-01 00:00:00] *** Arnold (~arnold@foo.bar) quit (Connection reset by peer)
|
|
line += `*** ${msg.from.nick} (${msg.hostmask}) quit (${msg.text})`;
|
|
break;
|
|
case MessageType.CHGHOST:
|
|
// [2014-01-01 00:00:00] *** Arnold changed host to: new@fancy.host
|
|
line += `*** ${msg.from.nick} changed host to '${msg.new_ident}@${msg.new_host}'`;
|
|
break;
|
|
case MessageType.TOPIC:
|
|
// [2014-01-01 00:00:00] *** Arnold changed topic to: welcome everyone!
|
|
line += `*** ${msg.from.nick} changed topic to '${msg.text}'`;
|
|
break;
|
|
|
|
default:
|
|
// unhandled events will not be logged
|
|
return;
|
|
}
|
|
|
|
// Split draft/multiline messages into individual lines
|
|
if (line.includes("\n")) {
|
|
line = line.replaceAll("\n", `\n[${msg.time.toISOString()}] | `);
|
|
}
|
|
|
|
line += "\n";
|
|
|
|
appendFileSync(
|
|
path.join(logPath, TextFileMessageStorage.getChannelFileName(channel)),
|
|
line
|
|
);
|
|
}
|
|
|
|
deleteChannel() {
|
|
// Not implemented for text log files
|
|
}
|
|
|
|
getMessages(): Message[] {
|
|
// Not implemented for text log files
|
|
// They do not contain enough data to fully re-create message objects
|
|
// Use sqlite storage instead
|
|
return [];
|
|
}
|
|
|
|
canProvideMessages() {
|
|
return false;
|
|
}
|
|
|
|
static getNetworkFolderName(network: Network) {
|
|
// Limit network name in the folder name to 23 characters
|
|
// So we can still fit 12 characters of the uuid for de-duplication
|
|
const networkName = cleanFilename(network.name.substring(0, 23).replace(/ /g, "-"));
|
|
|
|
return `${networkName}-${network.uuid.substring(networkName.length + 1)}`;
|
|
}
|
|
|
|
static getChannelFileName(channel: Channel) {
|
|
return `${cleanFilename(channel.name)}.log`;
|
|
}
|
|
}
|
|
|
|
export default TextFileMessageStorage;
|
|
|
|
function cleanFilename(name: string) {
|
|
name = filenamify(name, {replacement: "_"});
|
|
name = name.toLowerCase();
|
|
|
|
return name;
|
|
}
|