2025-03-19 11:47:34 +01:00

215 lines
7.0 KiB
JavaScript

import _fs from "fs";
import { fileURLToPath } from "url";
import { dirname } from "path";
import net from "net";
import tls from "tls";
import EventEmitter from "events";
const fs = _fs.promises;
const __dirname = dirname(fileURLToPath(import.meta.url));
const colors = {
white: "00", black: "01", navy: "02", green: "03", red: "04",
brown: "05", purple: "06", orange: "07", yellow: "08",
lightgreen: "09", teal: "10", cyan: "11", blue: "12",
magenta: "13", gray: "14", lightgray: "15"
};
const msgmodes = {
normal: "PRIVMSG {recipient} :{msg}",
action: "PRIVMSG {recipient} :\u0001ACTION {msg}\u0001",
notice: "NOTICE {recipient} :{msg}",
};
const replaceColor = (match, color, text) => {
return colors[color] ? `\x03${colors[color]}${text}\x0F` : text;
};
export default class irc extends EventEmitter {
options;
socket;
_recachetime = 60 * 30;
_cmd = new Map();
server;
emit(event, ...args) {
return super.emit(event, ...args);
}
on(event, listener) {
return super.on(event, listener);
}
constructor(options) {
super();
this.options = {
channels: [],
host: "127.0.0.1",
port: 6667,
ssl: false,
selfSigned: false,
sasl: false,
network: "test",
nickname: "test",
username: "test",
realname: "test",
set: "all",
...options
};
this.server = {
set: this.options.set,
motd: "",
me: {},
channel: new Map(),
user: new Map()
};
return (async () => {
await this.initialize();
return this;
})();
}
async initialize() {
const dir = (await fs.readdir(`${__dirname}/irc`)).filter(f => f.endsWith(".js"));
await Promise.all(dir.map(async (mod) => {
return (await import(`${__dirname}/irc/${mod}`)).default(this);
}));
this.connect();
}
createSocket() {
return this.options.ssl
? tls.connect({
host: this.options.host,
port: this.options.port,
rejectUnauthorized: !this.options.selfSigned,
})
: net.connect({
host: this.options.host,
port: this.options.port,
});
}
connect(reconnect = false) {
if (reconnect)
this.socket = undefined;
this.socket = this.createSocket();
this.socket.on("data", (msg) => this.handleData(msg));
this.socket.on("end", () => this.handleDisconnect());
this.socket.on("error", (err) => this.handleError(err));
this.socket.setEncoding("utf-8");
this.handleConnection();
}
handleConnection() {
this.send(`NICK ${this.options.nickname}`);
this.send(`USER ${this.options.username} 0 * :${this.options.realname}`);
if (this.options.sasl)
this.send("CAP LS");
this.emit("data", "[irc] connected!");
}
handleData(msg) {
msg.split(/\r?\n|\r/).forEach((line) => {
if (line.trim().length > 0) {
const cmd = this.parse(line);
if (this._cmd.has(cmd.command))
this._cmd.get(cmd.command)?.(cmd);
}
});
}
handleDisconnect() {
this.emit("data", ["error", "[irc] stream ended, reconnecting in progress"]);
this.connect(true);
}
handleError(err) {
this.emit("data", ["error", `[irc] socket error: ${err.message}`]);
this.connect(true);
}
join(channels) {
if (!Array.isArray(channels))
channels = [channels];
channels.forEach(e => {
this.send(`JOIN ${e}`);
});
}
part(channel, msg) {
this.send(`PART ${channel} :${msg || ""}`);
}
whois(nick) {
if (!Array.isArray(nick))
nick = [nick];
nick.forEach(e => {
this.send(`WHOIS ${e}`);
});
}
send(data) {
if (this.socket)
this.socket.write(`${data}\n`);
else
this.emit("data", ["info", `[irc] nope: ${data}`]);
}
sendmsg(mode, recipient, msg) {
const messages = Array.isArray(msg) ? msg : msg.split(/\r?\n/);
if (messages.length >= 5)
this.emit("data", ["error", "[irc] too many lines"]);
messages.forEach((e) => {
const formatted = msgmodes[mode]
.replace("{recipient}", recipient)
.replace("{msg}", this.format(e));
this.send(formatted);
});
}
parse(data) {
const [a, ...b] = data.split(/ +:/);
const tmp = a.split(" ").concat(b);
const prefix = data.charAt(0) === ":"
? tmp.shift() ?? null
: null;
const command = tmp.shift();
const params = command.toLowerCase() === "privmsg"
? [tmp.shift(), tmp.join(" :")]
: tmp;
return { prefix, command, params };
}
format(msg) {
return msg
.replace(/\[b\](.*?)\[\/b\]/g, "\x02$1\x02")
.replace(/\[i\](.*?)\[\/i\]/g, "\x1D$1\x1D")
.replace(/\[color=(.*?)](.*?)\[\/color\]/g, replaceColor);
}
parsePrefix(prefix) {
if (!prefix)
return false;
const parsed = /:?(.*)\!(.*)@(.*)/.exec(prefix);
if (!parsed)
return false;
return {
nick: parsed[1],
username: parsed[2],
hostname: parsed[3],
};
}
reply(tmp) {
return {
type: "irc",
network: this.options.network,
channel: tmp.params[0],
channelid: tmp.params[0],
user: {
...this.parsePrefix(tmp.prefix),
account: (() => {
const parsedPrefix = this.parsePrefix(tmp.prefix);
return parsedPrefix && this.server.user.has(parsedPrefix.nick)
? this.server.user.get(parsedPrefix.nick)?.account || false
: false;
})(),
prefix: (tmp.prefix?.charAt(0) === ":"
? tmp.prefix.substring(1)
: tmp.prefix) + `@${this.options.network}`,
},
message: tmp.params[1].replace(/\u0002/g, ""),
time: Math.floor(Date.now() / 1000),
raw: tmp,
reply: (msg) => this.sendmsg("normal", tmp.params[0], msg),
replyAction: (msg) => this.sendmsg("action", tmp.params[0], msg),
replyNotice: (msg) => this.sendmsg("notice", tmp.params[0], msg),
self: this.server,
_chan: this.server.channel.get(tmp.params[0]),
_user: this.server.user,
_cmd: this._cmd,
join: (chan) => this.join(chan),
part: (chan, msg) => this.part(chan, msg),
whois: (user) => this.whois(user),
write: (msg) => this.send(msg)
};
}
}