215 lines
7.0 KiB
JavaScript
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)
|
|
};
|
|
}
|
|
}
|