import { randomUUID } from "node:crypto" ;
import { hasIrcControlChars, stripIrcControlChars } from "./control-chars.js" ;
const IRC_TARGET_PATTERN = /^[^\s:]+$/u;
export type ParsedIrcLine = {
raw: string;
prefix?: string;
command: string;
params: string[];
trailing?: string;
};
export type ParsedIrcPrefix = {
nick?: string;
user?: string;
host?: string;
server?: string;
};
export function parseIrcLine(line: string): ParsedIrcLine | null {
const raw = line.replace(/[\r\n]+/g, "" ).trim();
if (!raw) {
return null ;
}
let cursor = raw;
let prefix: string | undefined;
if (cursor.startsWith(":" )) {
const idx = cursor.indexOf(" " );
if (idx <= 1 ) {
return null ;
}
prefix = cursor.slice(1 , idx);
cursor = cursor.slice(idx + 1 ).trimStart();
}
if (!cursor) {
return null ;
}
const firstSpace = cursor.indexOf(" " );
const command = (firstSpace === -1 ? cursor : cursor.slice(0 , firstSpace)).trim();
if (!command) {
return null ;
}
cursor = firstSpace === -1 ? "" : cursor.slice(firstSpace + 1 );
const params: string[] = [];
let trailing: string | undefined;
while (cursor.length > 0 ) {
cursor = cursor.trimStart();
if (!cursor) {
break ;
}
if (cursor.startsWith(":" )) {
trailing = cursor.slice(1 );
break ;
}
const spaceIdx = cursor.indexOf(" " );
if (spaceIdx === -1 ) {
params.push(cursor);
break ;
}
params.push(cursor.slice(0 , spaceIdx));
cursor = cursor.slice(spaceIdx + 1 );
}
return {
raw,
prefix,
command: command.toUpperCase(),
params,
trailing,
};
}
export function parseIrcPrefix(prefix?: string): ParsedIrcPrefix {
if (!prefix) {
return {};
}
const nickPart = prefix.match(/^([^!@]+)!([^@]+)@(.+)$/);
if (nickPart) {
return {
nick: nickPart[1 ],
user: nickPart[2 ],
host: nickPart[3 ],
};
}
const nickHostPart = prefix.match(/^([^@]+)@(.+)$/);
if (nickHostPart) {
return {
nick: nickHostPart[1 ],
host: nickHostPart[2 ],
};
}
if (prefix.includes("!" )) {
const [nick, user] = prefix.split("!" , 2 );
return { nick, user };
}
if (prefix.includes("." )) {
return { server: prefix };
}
return { nick: prefix };
}
function decodeLiteralEscapes(input: string): string {
// Defensive: this is not a full JS string unescaper.
// It's just enough to catch common "\r\n" / "\u0001" style payloads.
return input
.replace(/\\r/g, "\r" )
.replace(/\\n/g, "\n" )
.replace(/\\t/g, "\t" )
.replace(/\\0 /g, "\0" )
.replace(/\\x([0 -9 a-fA-F]{2 })/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16 )))
.replace(/\\u([0 -9 a-fA-F]{4 })/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16 )));
}
export function sanitizeIrcOutboundText(text: string): string {
const decoded = decodeLiteralEscapes(text);
return stripIrcControlChars(decoded.replace(/\r?\n/g, " " )).trim();
}
export function sanitizeIrcTarget(raw: string): string {
const decoded = decodeLiteralEscapes(raw);
if (!decoded) {
throw new Error("IRC target is required" );
}
// Reject any surrounding whitespace instead of trimming it away.
if (decoded !== decoded.trim()) {
throw new Error(`Invalid IRC target: ${raw}`);
}
if (hasIrcControlChars(decoded)) {
throw new Error(`Invalid IRC target: ${raw}`);
}
if (!IRC_TARGET_PATTERN.test(decoded)) {
throw new Error(`Invalid IRC target: ${raw}`);
}
return decoded;
}
export function splitIrcText(text: string, maxChars = 350 ): string[] {
const cleaned = sanitizeIrcOutboundText(text);
if (!cleaned) {
return [];
}
if (cleaned.length <= maxChars) {
return [cleaned];
}
const chunks: string[] = [];
let remaining = cleaned;
while (remaining.length > maxChars) {
let splitAt = remaining.lastIndexOf(" " , maxChars);
if (splitAt < Math.floor(maxChars * 0 .5 )) {
splitAt = maxChars;
}
chunks.push(remaining.slice(0 , splitAt).trim());
remaining = remaining.slice(splitAt).trimStart();
}
if (remaining) {
chunks.push(remaining);
}
return chunks.filter(Boolean );
}
export function makeIrcMessageId() {
return randomUUID();
}
Messung V0.5 in Prozent C=96 H=95 G=95
¤ Dauer der Verarbeitung: 0.13 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland