diff --git a/dist/clients/irc.js b/dist/clients/irc.js
new file mode 100644
index 0000000..208b3d1
--- /dev/null
+++ b/dist/clients/irc.js
@@ -0,0 +1,216 @@
+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;
+ 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();
+ }
+ connect(reconnect = false) {
+ if (reconnect)
+ this.socket = undefined;
+ if (this.options.ssl) {
+ this.socket = tls.connect({
+ host: this.options.host,
+ port: this.options.port,
+ rejectUnauthorized: !this.options.selfSigned,
+ }, () => this.handleConnection());
+ }
+ else {
+ this.socket = net.connect({
+ host: this.options.host,
+ port: this.options.port,
+ }, () => this.handleConnection());
+ }
+ if (!this.socket)
+ throw new Error("Socket konnte nicht initialisiert werden.");
+ this.socket.setEncoding("utf-8");
+ this.socket.on("data", (msg) => {
+ console.log("Received data:", msg);
+ this.handleData(msg);
+ });
+ this.socket.on("end", () => {
+ this.handleDisconnect();
+ });
+ this.socket.on("error", (err) => {
+ this.handleError(err);
+ });
+ }
+ 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)
+ };
+ }
+}
diff --git a/dist/clients/irc/cap.js b/dist/clients/irc/cap.js
new file mode 100644
index 0000000..3f8d80a
--- /dev/null
+++ b/dist/clients/irc/cap.js
@@ -0,0 +1,20 @@
+export default (bot) => {
+ bot._cmd.set("CAP", (msg) => {
+ switch (msg.params[1]) {
+ case "LS":
+ bot.send(`CAP REQ :${msg.params[2]}`);
+ break;
+ case "ACK":
+ bot.send("AUTHENTICATE PLAIN");
+ break;
+ }
+ });
+ bot._cmd.set("AUTHENTICATE", (msg) => {
+ if (msg.params[0].match(/\+/)) {
+ bot.send(`AUTHENTICATE ${Buffer.from(bot.username + "\u0000" + bot.username + "\u0000" + bot.options.password).toString("base64")}`);
+ }
+ });
+ bot._cmd.set("900", (msg) => {
+ bot.send("CAP END");
+ });
+};
diff --git a/dist/clients/irc/invite.js b/dist/clients/irc/invite.js
new file mode 100644
index 0000000..23827a8
--- /dev/null
+++ b/dist/clients/irc/invite.js
@@ -0,0 +1,12 @@
+export default (bot) => {
+ bot._cmd.set("INVITE", (msg) => {
+ const user = bot.parsePrefix(msg.prefix);
+ const channel = msg.params[1];
+ if (!bot.server.channel.has(channel)) {
+ bot.join(channel);
+ setTimeout(() => {
+ bot.send(`PRIVMSG ${channel} :Hi. Wurde von ${user.nick} eingeladen.`);
+ }, 1000);
+ }
+ });
+};
diff --git a/dist/clients/irc/join.js b/dist/clients/irc/join.js
new file mode 100644
index 0000000..27b1936
--- /dev/null
+++ b/dist/clients/irc/join.js
@@ -0,0 +1,5 @@
+export default (bot) => {
+ bot._cmd.set("JOIN", (msg) => {
+ bot.send(`WHO ${msg.params[0]}`);
+ });
+};
diff --git a/dist/clients/irc/motd.js b/dist/clients/irc/motd.js
new file mode 100644
index 0000000..7abe556
--- /dev/null
+++ b/dist/clients/irc/motd.js
@@ -0,0 +1,12 @@
+export default (bot) => {
+ bot._cmd.set("372", (msg) => {
+ bot.server.motd += `${msg.params[1]}\n`;
+ });
+ bot._cmd.set("375", (msg) => {
+ bot.server.motd = `${msg.params[1]}\n`;
+ });
+ bot._cmd.set("376", (msg) => {
+ bot.server.motd += `${msg.params[1]}\n`;
+ bot.emit("data", ["motd", bot.server.motd]);
+ });
+};
diff --git a/dist/clients/irc/msg.js b/dist/clients/irc/msg.js
new file mode 100644
index 0000000..ef0a432
--- /dev/null
+++ b/dist/clients/irc/msg.js
@@ -0,0 +1,13 @@
+export default (bot) => {
+ bot._cmd.set("PRIVMSG", (msg) => {
+ if (msg.params[1] === "\u0001VERSION\u0001")
+ return bot.emit("data", ["ctcp:version", bot.reply(msg)]);
+ else if (msg.params[1].match(/^\u0001PING .*\u0001/i))
+ return bot.emit("data", ["ctcp:ping", bot.reply(msg)]);
+ else
+ bot.emit("data", ["message", bot.reply(msg)]);
+ });
+ bot._cmd.set("NOTICE", (msg) => {
+ bot.emit("data", ["notice", msg.params[1]]);
+ });
+};
diff --git a/dist/clients/irc/nick.js b/dist/clients/irc/nick.js
new file mode 100644
index 0000000..1cd74ed
--- /dev/null
+++ b/dist/clients/irc/nick.js
@@ -0,0 +1,8 @@
+export default (bot) => {
+ bot._cmd.set("NICK", (msg) => {
+ const prefix = bot.parsePrefix(msg.prefix);
+ if (bot.server.user.has(prefix.nick))
+ bot.server.user.delete(prefix.nick);
+ bot.whois(msg.params[0], true);
+ });
+};
diff --git a/dist/clients/irc/part.js b/dist/clients/irc/part.js
new file mode 100644
index 0000000..93a128c
--- /dev/null
+++ b/dist/clients/irc/part.js
@@ -0,0 +1,5 @@
+export default (bot) => {
+ bot._cmd.set("PART", (msg) => {
+ delete bot.server.user[msg.params[0]];
+ });
+};
diff --git a/dist/clients/irc/ping.js b/dist/clients/irc/ping.js
new file mode 100644
index 0000000..8c295ed
--- /dev/null
+++ b/dist/clients/irc/ping.js
@@ -0,0 +1,5 @@
+export default (bot) => {
+ bot._cmd.set("PING", (msg) => {
+ bot.send(`PONG ${msg.params.join('')}`);
+ });
+};
diff --git a/dist/clients/irc/pwdreq.js b/dist/clients/irc/pwdreq.js
new file mode 100644
index 0000000..199d205
--- /dev/null
+++ b/dist/clients/irc/pwdreq.js
@@ -0,0 +1,6 @@
+export default (bot) => {
+ bot._cmd.set("464", (msg) => {
+ if (bot.options.password.length > 0 && !bot.options.sasl)
+ bot.send(`PASS ${bot.options.password}`);
+ });
+};
diff --git a/dist/clients/irc/welcome.js b/dist/clients/irc/welcome.js
new file mode 100644
index 0000000..0c9f851
--- /dev/null
+++ b/dist/clients/irc/welcome.js
@@ -0,0 +1,6 @@
+export default (bot) => {
+ bot._cmd.set("001", (msg) => {
+ bot.join(bot.options.channels);
+ bot.emit("data", ["connected", msg.params[1]]);
+ });
+};
diff --git a/dist/clients/irc/who.js b/dist/clients/irc/who.js
new file mode 100644
index 0000000..79f3369
--- /dev/null
+++ b/dist/clients/irc/who.js
@@ -0,0 +1,17 @@
+const max = 400;
+let whois = [];
+let chan;
+export default (bot) => {
+ bot._cmd.set("352", (msg) => {
+ chan = msg.params[1];
+ whois.push(msg.params[5]);
+ });
+ bot._cmd.set("315", (msg) => {
+ bot.server.channel.set(chan, whois);
+ whois = [...new Set(whois)];
+ Array(Math.ceil(whois.length / 10)).fill(undefined).map(() => whois.splice(0, 10)).forEach((l) => {
+ bot.whois(l);
+ });
+ whois = [];
+ });
+};
diff --git a/dist/clients/irc/whois.js b/dist/clients/irc/whois.js
new file mode 100644
index 0000000..9275c32
--- /dev/null
+++ b/dist/clients/irc/whois.js
@@ -0,0 +1,62 @@
+export default (bot) => {
+ bot._cmd.set("307", (msg) => {
+ let tmpuser = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.account = msg.params[1];
+ tmpuser.registered = true;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+ bot._cmd.set("311", (msg) => {
+ let tmpuser = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.nickname = msg.params[1];
+ tmpuser.username = msg.params[2];
+ tmpuser.hostname = msg.params[3];
+ tmpuser.realname = msg.params[5];
+ tmpuser.prefix = `${msg.params[1]}!${msg.params[2]}@${msg.params[3]}`;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+ bot._cmd.set("313", (msg) => {
+ let tmpuser = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.oper = true;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+ bot._cmd.set("318", (msg) => {
+ let tmpuser = bot.server.user.get(msg.params[1]) || {};
+ tmpuser = {
+ nick: tmpuser.nick || false,
+ nickname: tmpuser.nickname || false,
+ username: tmpuser.username || false,
+ hostname: tmpuser.hostname || false,
+ realname: tmpuser.realname || false,
+ account: tmpuser.account || false,
+ prefix: tmpuser.prefix || false,
+ registered: tmpuser.registered || false,
+ oper: tmpuser.oper || false,
+ channels: tmpuser.channels || [],
+ cached: Math.floor(Date.now() / 1000),
+ };
+ bot.server.user.set(msg.params[1], tmpuser);
+ if (msg.params[0] === msg.params[1]) {
+ bot.server.me = tmpuser;
+ bot.server.user.delete(msg.params[1]);
+ }
+ });
+ bot._cmd.set("319", (msg) => {
+ let tmpchan = new Map();
+ let tmpuser = bot.server.user.get(msg.params[1]) || {};
+ if (tmpuser.channels)
+ tmpchan = new Map(tmpuser.channels);
+ const chans = msg.params[2].trim().split(" ");
+ chans.forEach((chan) => {
+ const [flags, name] = chan.split("#");
+ tmpchan.set(`#${name}`, flags);
+ });
+ tmpuser.channels = tmpchan;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+ bot._cmd.set("330", (msg) => {
+ let tmpuser = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.account = msg.params[2];
+ tmpuser.registered = true;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+};
diff --git a/dist/clients/slack.js b/dist/clients/slack.js
new file mode 100644
index 0000000..ef6c56c
--- /dev/null
+++ b/dist/clients/slack.js
@@ -0,0 +1,200 @@
+import https from "node:https";
+import url from "node:url";
+import EventEmitter from "node:events";
+import fetch from "flumm-fetch";
+export default class slack extends EventEmitter {
+ options;
+ token;
+ api = "https://slack.com/api";
+ interval = null;
+ server;
+ constructor(options) {
+ super();
+ this.options = {
+ set: "all",
+ ...options,
+ };
+ this.token = this.options.token;
+ this.server = {
+ set: this.options.set,
+ channel: new Map(),
+ user: new Map(),
+ wss: {
+ url: null,
+ socket: null,
+ },
+ me: {},
+ };
+ return (async () => {
+ await this.connect();
+ return this;
+ })();
+ }
+ async connect() {
+ const response = await fetch(`${this.api}/rtm.start?token=${this.token}`);
+ const res = await response.json();
+ if (!res.ok) {
+ this.emit("data", ["error", res.description || "Connection failed"]);
+ return;
+ }
+ res.channels?.forEach(channel => {
+ this.server.channel.set(channel.id, channel.name);
+ });
+ res.users?.forEach(user => {
+ this.server.user.set(user.id, {
+ account: user.name,
+ nickname: user.real_name,
+ });
+ });
+ if (res.url) {
+ this.server.wss.url = url.parse(res.url);
+ this.initializeWebSocket();
+ }
+ else
+ this.emit("data", ["error", "No WebSocket URL provided"]);
+ }
+ initializeWebSocket() {
+ https.get({
+ hostname: this.server.wss.url?.host,
+ path: this.server.wss.url?.path,
+ port: 443,
+ headers: {
+ Upgrade: "websocket",
+ Connection: "Upgrade",
+ "Sec-WebSocket-Version": 13,
+ "Sec-WebSocket-Key": Buffer.from(Array(16)
+ .fill(0)
+ .map(() => Math.round(Math.random() * 0xff))).toString("base64"),
+ }
+ }, () => { })
+ .on("upgrade", (_, sock) => {
+ this.server.wss.socket = sock;
+ this.server.wss.socket.setEncoding("utf-8");
+ this.handleWebSocketEvents();
+ })
+ .on("error", err => {
+ this.emit("data", ["error", `Failed to establish WebSocket: ${err.message}`]);
+ });
+ }
+ handleWebSocketEvents() {
+ if (!this.server.wss.socket)
+ return;
+ this.interval = setInterval(async () => await this.ping(), 30000);
+ this.server.wss.socket.on("data", async (data) => {
+ try {
+ const parsedData = this.parseData(data);
+ if (parsedData?.type === "message") {
+ await Promise.all([
+ this.getChannel(parsedData.channel),
+ this.getUser(parsedData.user),
+ ]);
+ this.emit("data", ["message", this.reply(parsedData)]);
+ }
+ }
+ catch (err) {
+ this.emit("data", ["error", err]);
+ }
+ });
+ this.server.wss.socket.on("end", async () => {
+ this.emit("data", ["debug", "WebSocket stream ended"]);
+ await this.reconnect();
+ });
+ this.server.wss.socket.on("error", async (err) => {
+ this.emit("data", ["error", err.message]);
+ await this.reconnect();
+ });
+ }
+ async reconnect() {
+ this.server.wss.url = null;
+ this.server.wss.socket = null;
+ if (this.interval)
+ clearInterval(this.interval);
+ this.emit("data", ["info", "reconnecting slack"]);
+ await this.connect();
+ }
+ async getChannel(channelId) {
+ if (this.server.channel.has(channelId))
+ return this.server.channel.get(channelId);
+ const res = await (await fetch(`${this.api}/conversations.info?channel=${channelId}&token=${this.token}`)).json();
+ if (!res.channel)
+ throw new Error("Channel not found");
+ this.server.channel.set(channelId, res.channel.name);
+ return res.channel.name;
+ }
+ async getUser(userId) {
+ if (this.server.user.has(userId))
+ return this.server.user.get(userId);
+ const res = await (await fetch(`${this.api}/users.info?user=${userId}&token=${this.token}`)).json();
+ if (!res.user)
+ throw new Error("User not found");
+ const user = { account: res.user.name, nickname: res.user.real_name };
+ this.server.user.set(userId, user);
+ return user;
+ }
+ async send(channel, text) {
+ const message = Array.isArray(text) ? text.join("\n") : text;
+ const formatted = message.includes("\n") ? "```" + message + "```" : message;
+ await this.write({
+ type: "message",
+ channel: channel,
+ text: this.format(formatted),
+ });
+ }
+ async ping() {
+ await this.write({ type: "ping" });
+ }
+ async write(json) {
+ const msg = JSON.stringify(json);
+ const payload = Buffer.from(msg);
+ if (payload.length > 2 ** 14) {
+ this.emit("data", ["error", "message too long, slack limit reached"]);
+ return;
+ }
+ if (!this.server.wss.socket) {
+ await this.reconnect();
+ return;
+ }
+ try {
+ this.server.wss.socket.cork();
+ this.server.wss.socket.write(payload);
+ this.server.wss.socket.uncork();
+ }
+ catch (err) {
+ console.error(err);
+ await this.reconnect();
+ }
+ }
+ reply(tmp) {
+ return {
+ type: "slack",
+ network: "Slack",
+ channel: this.server.channel.get(tmp.channel),
+ channelid: tmp.channel,
+ user: this.server.user.get(tmp.user),
+ self: this.server,
+ message: tmp.text,
+ time: ~~(Date.now() / 1000),
+ raw: tmp,
+ reply: (msg) => this.send(tmp.channel, msg),
+ replyAction: (msg) => this.send(tmp.channel, `[i]${msg}[/i]`),
+ replyNotice: (msg) => this.send(tmp.channel, msg),
+ };
+ }
+ parseData(data) {
+ try {
+ const json = JSON.parse(data.toString());
+ return json;
+ }
+ catch (err) {
+ this.emit("data", ["error", "failed to parse data"]);
+ return undefined;
+ }
+ }
+ format(msg) {
+ return msg.toString()
+ .replace(/\[b\](.*?)\[\/b\]/g, "*$1*")
+ .replace(/\[s\](.*?)\[\/s\]/g, "~$1~")
+ .replace(/\[i\](.*?)\[\/i\]/g, "_$1_")
+ .replace(/\[color=(.*?)](.*?)\[\/color\]/g, "$2");
+ }
+}
diff --git a/dist/clients/tg.js b/dist/clients/tg.js
new file mode 100644
index 0000000..3724efa
--- /dev/null
+++ b/dist/clients/tg.js
@@ -0,0 +1,159 @@
+import fetch from "flumm-fetch";
+import EventEmitter from "events";
+const allowedFiles = ["audio", "video", "photo", "document"];
+export default class tg extends EventEmitter {
+ options;
+ token;
+ api;
+ lastUpdate = 0;
+ lastMessage = 0;
+ poller = null;
+ server = {
+ set: "",
+ channel: new Map(),
+ user: new Map(),
+ me: {},
+ };
+ constructor(options) {
+ super();
+ this.options = {
+ pollrate: 1000,
+ set: "all",
+ ...options,
+ };
+ this.token = this.options.token;
+ this.api = `https://api.telegram.org/bot${this.token}`;
+ this.server.set = this.options.set;
+ return (async () => {
+ await this.connect();
+ await this.poll();
+ return this;
+ })();
+ }
+ async connect() {
+ const res = await (await fetch(`${this.api}/getMe`)).json();
+ if (!res.ok)
+ throw this.emit("data", ["error", res.description ?? "Unknown error"]);
+ this.server.me = {
+ nickname: res.result.first_name,
+ username: res.result.username,
+ account: res.result.id.toString(),
+ prefix: `${res.result.username}!${res.result.id.toString()}`,
+ id: res.result.id.toString(),
+ };
+ }
+ async getFile(fileId) {
+ const res = await (await fetch(`${this.api}/getFile?file_id=${fileId}`)).json();
+ if (!res.ok)
+ return false;
+ return `https://api.telegram.org/file/bot${this.token}/${res.result?.file_path}`;
+ }
+ async poll() {
+ try {
+ const updates = await this.fetchUpdates();
+ if (!updates || updates.length === 0)
+ return;
+ this.lastUpdate = updates[updates.length - 1].update_id + 1;
+ for (const update of updates)
+ await this.processUpdate(update);
+ }
+ catch (err) {
+ await this.handlePollError(err);
+ }
+ finally {
+ setTimeout(() => this.poll(), this.options.pollrate);
+ }
+ }
+ async fetchUpdates() {
+ const res = await (await fetch(`${this.api}/getUpdates?offset=${this.lastUpdate}&allowed_updates=message`)).json();
+ if (!res.ok)
+ throw new Error(res.description || "Failed to fetch updates");
+ return res.result || [];
+ }
+ async processUpdate(update) {
+ if (update.message)
+ await this.handleMessage(update.message);
+ else if (update.callback_query)
+ this.emit("data", ["callback_query", this.reply(update.callback_query.message, update.callback_query)]);
+ else if (update.inline_query)
+ this.emit("data", ["inline_query", update.inline_query]);
+ }
+ async handleMessage(message) {
+ if (message.date >= Math.floor(Date.now() / 1000) - 10 &&
+ message.message_id !== this.lastMessage) {
+ this.lastMessage = message.message_id;
+ if (!this.server.user.has(message.from.username || message.from.first_name)) {
+ this.server.user.set(message.from.username || message.from.first_name, {
+ nick: message.from.first_name,
+ username: message.from.username,
+ account: message.from.id.toString(),
+ prefix: `${message.from.username}!${message.from.id.toString()}@Telegram`,
+ id: message.from.id,
+ });
+ }
+ try {
+ const fileKey = Object.keys(message).find(key => allowedFiles.includes(key));
+ if (fileKey) {
+ let media = message[fileKey];
+ if (fileKey === "photo")
+ media = message[fileKey][message[fileKey].length - 1];
+ message.media = await this.getFile(media.file_id);
+ message.text = message.caption;
+ delete message[fileKey];
+ }
+ }
+ catch { }
+ this.emit("data", ["message", this.reply(message)]);
+ }
+ }
+ async handlePollError(err) {
+ if (!err.type)
+ this.emit("data", ["error", "tg timed out lol"]);
+ else if (err.type === "tg")
+ this.emit("data", ["error", err.message]);
+ await this.connect();
+ }
+ reply(tmp, opt = {}) {
+ return {
+ type: "tg",
+ network: "Telegram",
+ channel: tmp.chat?.title,
+ channelid: tmp.chat?.id,
+ user: {
+ prefix: `${tmp.from.username}!${tmp.from.id}@Telegram`,
+ nick: tmp.from.first_name,
+ username: tmp.from.username,
+ account: tmp.from.id.toString(),
+ },
+ self: this.server,
+ message: tmp.text,
+ time: tmp.date,
+ raw: tmp,
+ media: tmp.media || null,
+ reply: async (msg, opt = {}) => await this.send(tmp.chat.id, msg, tmp.message_id, opt),
+ replyAction: async (msg, opt = {}) => await this.send(tmp.chat.id, `Action ${msg}`, tmp.message_id, opt),
+ };
+ }
+ async send(chatId, msg, reply = null, opt = {}) {
+ const body = {
+ chat_id: chatId,
+ text: this.format(msg),
+ parse_mode: "HTML",
+ ...opt,
+ };
+ if (reply)
+ body["reply_to_message_id"] = reply;
+ const opts = { method: "POST", body };
+ return await (await fetch(`${this.api}/sendMessage`, opts)).json();
+ }
+ format(msg) {
+ return msg.toString()
+ .split("&").join("&")
+ .split("<").join("<")
+ .split(">").join(">")
+ .replace(/\[b\](.*?)\[\/b\]/gsm, "$1")
+ .replace(/\[i\](.*?)\[\/i\]/gsm, "$1")
+ .replace(/\[color=(.*?)](.*?)\[\/color\]/gsm, "$2")
+ .replace(/\[pre\](.*?)\[\/pre\]/gsm, "
$1
");
+ }
+}
diff --git a/dist/index.js b/dist/index.js
new file mode 100644
index 0000000..9677993
--- /dev/null
+++ b/dist/index.js
@@ -0,0 +1,48 @@
+import fs from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname } from "node:path";
+import EventEmitter from "node:events";
+const __dirname = dirname(fileURLToPath(import.meta.url));
+export default class Cuffeo extends EventEmitter {
+ clients = [];
+ libs = {};
+ emit(event, ...args) {
+ return super.emit(event, ...args);
+ }
+ on(event, listener) {
+ return super.on(event, listener);
+ }
+ constructor(cfg) {
+ super();
+ return (async () => {
+ this.libs = await this.loadLibs();
+ this.clients = await this.registerClients(cfg);
+ return this;
+ })();
+ }
+ async loadLibs() {
+ const clientFiles = await fs.promises.readdir(`${__dirname}/clients`);
+ const modules = await Promise.all(clientFiles
+ .filter(f => f.endsWith(".js"))
+ .map(async (client) => {
+ const lib = (await import(`./clients/${client}`)).default;
+ return { [lib.name]: lib };
+ }));
+ return modules.reduce((a, b) => ({ ...a, ...b }), {});
+ }
+ async registerClients(cfg) {
+ return Promise.all(cfg
+ .filter(e => e.enabled)
+ .map(async (srv) => {
+ if (!Object.keys(this.libs).includes(srv.type))
+ throw new Error(`unsupported client: ${srv.type}`);
+ const client = {
+ name: srv.network,
+ type: srv.type,
+ client: await new this.libs[srv.type](srv),
+ };
+ client.client.on("data", ([type, data]) => this.emit(type, data));
+ return client;
+ }));
+ }
+}
diff --git a/package.json b/package.json
index 6cb0edc..84be041 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,12 @@
{
"name": "cuffeo",
- "version": "1.2.2",
+ "version": "2.0.0",
"description": "A multi-protocol chatbot library with nearly zero dependencies.",
- "main": "src/index.mjs",
- "scripts": {},
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsc",
+ "test": "node src/test.mjs"
+ },
"repository": {
"type": "git",
"url": "gitea@git.lat:keinBot/cuffeo.git"
@@ -16,11 +19,16 @@
],
"author": "Flummi & jkhsjdhjs",
"license": "MIT",
+ "type": "module",
"dependencies": {
"flumm-fetch": "^1.0.1"
},
"bugs": {
"url": "https://git.lat/keinBot/cuffeo/issues"
},
- "homepage": "https://git.lat/keinBot/cuffeo"
+ "homepage": "https://git.lat/keinBot/cuffeo",
+ "devDependencies": {
+ "@types/node": "^22.13.10",
+ "typescript": "^5.8.2"
+ }
}
diff --git a/src/clients/discord.mjs.unused b/src/clients/discord.mjs.unused
deleted file mode 100644
index f7ee12e..0000000
--- a/src/clients/discord.mjs.unused
+++ /dev/null
@@ -1,77 +0,0 @@
-import Discord from "discord.js";
-import EventEmitter from "events";
-
-export class discord extends EventEmitter {
- constructor(options) {
- super();
- this.options = options || {};
- this.token = options.token || null;
- this.set = this.options.set || "all";
- this.network = "discord";
-
- this.bot = new Discord.Client();
- this.bot.login(this.token);
-
- this.server = {
- set: this.set,
- channel: new Map(),
- user: new Map(),
- me: {}
- };
-
- this.bot.on("ready", () => {
- this.server.me = {
- nickname: this.bot.user.username,
- username: this.bot.user.username,
- account: this.bot.user.id.toString(),
- prefix: `${this.bot.user.username}!${this.bot.user.id.toString()}`,
- id: this.bot.user.id.toString()
- };
- });
-
- this.bot.on("message", msg => {
- if(msg.author.id !== this.server.me.id)
- this.emit("data", ["message", this.reply(msg)]);
- });
- }
- reply(tmp) {
- return {
- type: "discord",
- network: "Discord",
- channel: tmp.channel.name,
- channelid: tmp.channel.id,
- user: {
- prefix: `${tmp.author.username}!${tmp.author.id}`,
- nick: tmp.author.username,
- username: tmp.author.username,
- account: tmp.author.id.toString()
- },
- message: tmp.content,
- time: ~~(Date.now() / 1000),
- self: this.server,
- reply: msg => this.send(tmp, this.format(msg)),
- replyAction: msg => this.send(tmp, this.format(`*${msg}*`), "normal"),
- replyNotice: msg => this.send(tmp, this.format(msg))
- };
- }
- send(r, msg, mode = "blah") {
- switch(mode) {
- case "normal":
- r.channel.send(msg);
- break;
- default:
- r.reply(msg);
- break;
- }
- }
- sendmsg(mode, recipient, msg) {
- this.bot.channels.get(recipient).send(msg);
- }
- format(msg) {
- return msg.toString()
- .replace(/\[b\](.*?)\[\/b\]/g, "**$1**") // bold
- .replace(/\[i\](.*?)\[\/i\]/g, "*$1*") // italic
- .replace(/\[color=(.*?)](.*?)\[\/color\]/g, "$2")
- ;
- }
-};
diff --git a/src/clients/irc.mjs b/src/clients/irc.mjs
deleted file mode 100644
index 1ef1a56..0000000
--- a/src/clients/irc.mjs
+++ /dev/null
@@ -1,184 +0,0 @@
-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", "black", "navy",
- "green", "red", "brown",
- "purple", "orange", "yellow",
- "lightgreen", "teal", "cyan",
- "blue", "magenta", "gray",
- "lightgray"
-].reduce((a, b, i) => ({...a, ...{[b]: i.toString().padStart(2, 0)}}), {});
-
-const msgmodes = {
- normal: "PRIVMSG {recipient} :{msg}",
- action: "PRIVMSG {recipient} :\u0001ACTION {msg}\u0001",
- notice: "NOTICE {recipient} :{msg}"
-};
-
-const replaceColor = (match, color, text) => {
- if (colors.hasOwnProperty(color))
- return `\x03${colors[color]}${text}\x0F`;
- return text;
-};
-
-export default class irc extends EventEmitter {
- constructor(options) {
- super();
- this.options = options || {};
- this.options.channels = this.options.channels || [];
- this.options.host = this.options.host || "127.0.0.1";
- this.options.port = this.options.port || 6667;
- this.options.ssl = this.options.ssl || false;
- this.options.selfSigned = this.options.selfSigned || false;
- this.options.sasl = this.options.sasl || false;
- this.network = this.options.network || "test";
- this.nickname = this.options.nickname || "test";
- this.username = this.options.username || "test";
- this.realname = this.options.realname || "test";
- this.channels = this.options.channels || [];
- this.set = this.options.set || "all";
- this._recachetime = 60 * 30; // 30 minutes
- this._cmd = new Map();
-
- this.server = {
- set: this.set,
- motd: "",
- me: {},
- channel: new Map(),
- user: new Map()
- };
-
- return (async () => {
- const dir = (await fs.readdir(`${__dirname}/irc`)).filter(f => f.endsWith(".mjs"));
- await Promise.all(dir.map(async mod => {
- return (await import(`${__dirname}/irc/${mod}`)).default(this);
- }));
- this.connect();
- return this;
- })();
- }
- connect(reconnect = false) {
- if(reconnect)
- this.socket = null;
- this.socket = (this.options.ssl ? tls : net).connect({
- host: this.options.host,
- port: this.options.port,
- rejectUnauthorized: !this.options.selfSigned
- }, () => {
- this.send(`NICK ${this.nickname}`);
- this.send(`USER ${this.username} 0 * : ${this.realname}`);
- if (this.options.sasl)
- this.send("CAP LS");
- this.emit("data", "[irc] connected!");
- });
- this.socket.setEncoding("utf-8");
- this.socket.on("data", msg => {
- msg.split(/\r?\n|\r/).filter(tmp => tmp.length > 0).forEach(tmp => {
- const cmd = this.parse(tmp);
- if (this._cmd.has(cmd.command))
- this._cmd.get(cmd.command)(cmd);
- })
- });
- this.socket.on("end", () => {
- this.connect(true);
- return this.emit("data", ["error", "[irc] stream ended, reconnecting in progress"]);
- });
- }
- send(data) {
- if(this.socket)
- this.socket.write(`${data}\n`);
- else
- this.emit("data", ["info", `[irc] nope: ${data}`]);
- }
- sendmsg(mode, recipient, msg) {
- msg = Array.isArray(msg) ? msg : msg.split(/\r?\n/);
- if (msg.length >= 5)
- return this.emit("data", ["error", "[irc] too many lines"]);
- msg.forEach(e => this.send( msgmodes[mode].replace("{recipient}", recipient).replace("{msg}", this.format(e.toString())) ));
- }
- parse(data, [a, ...b] = data.split(/ +:/), tmp = a.split(" ").concat(b)) {
- const prefix = data.charAt(0) === ":" ? tmp.shift() : null
- , command = tmp.shift()
- , params = command.toLowerCase() === "privmsg" ? [ tmp.shift(), tmp.join(" :") ] : tmp;
- return {
- prefix: prefix,
- command: command,
- params: params
- };
- }
- reply(tmp) {
- return {
- type: "irc",
- network: this.network,
- channel: tmp.params[0],
- channelid: tmp.params[0],
- user: { ...this.parsePrefix(tmp.prefix), ...{
- account: this.server.user.has(this.parsePrefix(tmp.prefix).nick) ? this.server.user.get(this.parsePrefix(tmp.prefix).nick).account : false,
- prefix: (tmp.prefix.charAt(0) === ":" ? tmp.prefix.substring(1) : tmp.prefix) + `@${this.network}`
- }},
- message: tmp.params[1].replace(/\u0002/, ""),
- time: ~~(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),
- socket: this.socket
- };
- }
- join(channel) {
- this.send(`JOIN ${(typeof channel === "object") ? channel.join(",") : channel}`);
- }
- who(channel) {
- this.send(`WHO ${channel}`);
- }
- part(channel, msg = false) {
- this.send(`PART ${(typeof channel === "object") ? channel.join(",") : channel}${msg ? " " + msg : " part"}`);
- }
- whois(userlist, force = false, whois = []) {
- for(const u of (typeof userlist === "object") ? userlist : userlist.split(",")) {
- let tmpuser = { cached: 0 };
- if (this.server.user.has(u) && !force)
- tmpuser = this.server.user.get(u);
- if (tmpuser.cached < ~~(Date.now() / 1000) - this._recachetime)
- whois.push(u);
- }
- whois = whois.filter(e => e.trim());
- if(whois.length === 0)
- return false;
- this.emit("data", ["info", `[irc] whois > ${whois}`]);
- this.send(`WHOIS ${whois.join(',')}`);
- }
- parsePrefix(prefix) {
- prefix = /:?(.*)\!(.*)@(.*)/.exec(prefix);
- if (!prefix)
- return false;
- return {
- nick: prefix[1],
- username: prefix[2],
- hostname: prefix[3]
- };
- }
- format(msg) {
- return msg
- .replace(/\[b\](.*?)\[\/b\]/g, "\x02$1\x02") // bold
- .replace(/\[i\](.*?)\[\/i\]/g, "\x1D$1\x1D") // italic
- .replace(/\[color=(.*?)](.*?)\[\/color\]/g, replaceColor) // colors
- ;
- }
-}
diff --git a/src/clients/irc.ts b/src/clients/irc.ts
new file mode 100644
index 0000000..5f027db
--- /dev/null
+++ b/src/clients/irc.ts
@@ -0,0 +1,279 @@
+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));
+
+type MessageModes = "normal" | "action" | "notice";
+interface IRCOptions {
+ channels?: string[];
+ host?: string;
+ port?: number;
+ ssl?: boolean;
+ selfSigned?: boolean;
+ sasl?: boolean;
+ network?: string;
+ nickname?: string;
+ username?: string;
+ realname?: string;
+ set?: string;
+}
+
+interface ParsedCommand {
+ prefix: string | null;
+ command: string;
+ params: string[];
+}
+
+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: Record = {
+ normal: "PRIVMSG {recipient} :{msg}",
+ action: "PRIVMSG {recipient} :\u0001ACTION {msg}\u0001",
+ notice: "NOTICE {recipient} :{msg}",
+};
+
+const replaceColor = (match: string, color: keyof typeof colors, text: string) => {
+ return colors[color] ? `\x03${colors[color]}${text}\x0F` : text;
+};
+
+export default class irc extends EventEmitter {
+ private options: Required;
+ private socket?: net.Socket | tls.TLSSocket;
+ private _recachetime: number = 60 * 30; // 30 minutes
+ private _cmd: Map void> = new Map();
+ private server: {
+ set: string;
+ motd: string;
+ me: Record;
+ channel: Map;
+ user: Map;
+ };
+
+ constructor(options: IRCOptions) {
+ 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;
+ })() as unknown as irc;
+ }
+
+ 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();
+ }
+
+ connect(reconnect: boolean = false): void {
+ if(reconnect)
+ this.socket = undefined;
+
+ if(this.options.ssl) {
+ this.socket = tls.connect({
+ host: this.options.host,
+ port: this.options.port,
+ rejectUnauthorized: !this.options.selfSigned,
+ }, () => this.handleConnection());
+ }
+ else {
+ this.socket = net.connect({
+ host: this.options.host,
+ port: this.options.port,
+ }, () => this.handleConnection());
+ }
+
+ if(!this.socket)
+ throw new Error("Socket konnte nicht initialisiert werden.");
+
+ this.socket.setEncoding("utf-8");
+ this.socket.on("data", (msg: string) => {
+ console.log("Received data:", msg);
+ this.handleData(msg);
+ });
+ this.socket.on("end", () => {
+ this.handleDisconnect();
+ });
+ this.socket.on("error", (err: Error) => {
+ this.handleError(err);
+ });
+ }
+
+ private handleConnection(): void {
+ 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!");
+ }
+
+ private handleData(msg: string): void {
+ 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);
+ }
+ });
+ }
+
+ private handleDisconnect(): void {
+ this.emit("data", ["error", "[irc] stream ended, reconnecting in progress"]);
+ this.connect(true);
+ }
+
+ private handleError(err: Error): void {
+ this.emit("data", ["error", `[irc] socket error: ${err.message}`]);
+ this.connect(true);
+ }
+
+ private join(channels: string | string[]): void {
+ if(!Array.isArray(channels))
+ channels = [channels];
+ channels.forEach(e => {
+ this.send(`JOIN ${e}`);
+ });
+ }
+
+ private part(channel: string, msg?: string): void {
+ this.send(`PART ${channel} :${msg || ""}`);
+ }
+
+ private whois(nick: string | string[]): void {
+ if(!Array.isArray(nick))
+ nick = [nick];
+ nick.forEach(e => {
+ this.send(`WHOIS ${e}`);
+ });
+ }
+
+ send(data: string) {
+ if(this.socket)
+ this.socket.write(`${data}\n`);
+ else
+ this.emit("data", ["info", `[irc] nope: ${data}`]);
+ }
+
+ sendmsg(mode: MessageModes, recipient: string, msg: string | string[]) {
+ 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: string): ParsedCommand {
+ const [a, ...b] = data.split(/ +:/);
+ const tmp = a.split(" ").concat(b);
+
+ const prefix: string | null = 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 };
+ }
+
+ private format(msg: string): string {
+ return msg
+ .replace(/\[b\](.*?)\[\/b\]/g, "\x02$1\x02") // bold
+ .replace(/\[i\](.*?)\[\/i\]/g, "\x1D$1\x1D") // italic
+ .replace(/\[color=(.*?)](.*?)\[\/color\]/g, replaceColor) // colors
+ ;
+ }
+
+ parsePrefix(prefix: string | null): {
+ nick: string; username: string; hostname: string
+ } | false {
+ if (!prefix) return false; // Null oder undefined behandeln
+
+ const parsed = /:?(.*)\!(.*)@(.*)/.exec(prefix);
+ if (!parsed) return false;
+
+ return {
+ nick: parsed[1],
+ username: parsed[2],
+ hostname: parsed[3],
+ };
+ }
+
+ reply(tmp: ParsedCommand) {
+ 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, ""), // Entfernt Steuerzeichen
+ time: Math.floor(Date.now() / 1000),
+ raw: tmp,
+ reply: (msg: string) => this.sendmsg("normal", tmp.params[0], msg),
+ replyAction: (msg: string) => this.sendmsg("action", tmp.params[0], msg),
+ replyNotice: (msg: string) => 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: string) => this.join(chan),
+ part: (chan: string, msg?: string) => this.part(chan, msg),
+ whois: (user: string) => this.whois(user),
+ write: (msg: string) => this.send(msg)
+ };
+ }
+}
diff --git a/src/clients/irc/cap.mjs b/src/clients/irc/cap.mjs
deleted file mode 100644
index 7f9a560..0000000
--- a/src/clients/irc/cap.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-export default bot => {
- bot._cmd.set("CAP", msg => { // capkram
- switch (msg.params[1]) {
- case "LS": // list
- bot.send(`CAP REQ :${msg.params[2]}`);
- break;
- case "ACK": // success
- bot.send("AUTHENTICATE PLAIN");
- break;
- }
- });
-
- bot._cmd.set("AUTHENTICATE", msg => { // auth
- if (msg.params[0].match(/\+/))
- bot.send(`AUTHENTICATE ${new Buffer(bot.username + "\u0000" + bot.username + "\u0000" + bot.options.password).toString("base64")}`);
- });
-
- bot._cmd.set("900", msg => { // cap end
- bot.send("CAP END");
- });
-};
diff --git a/src/clients/irc/cap.ts b/src/clients/irc/cap.ts
new file mode 100644
index 0000000..8db5e6d
--- /dev/null
+++ b/src/clients/irc/cap.ts
@@ -0,0 +1,26 @@
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("CAP", (msg: Message) => { // CAP Handling
+ switch(msg.params[1]) {
+ case "LS": // List
+ bot.send(`CAP REQ :${msg.params[2]}`);
+ break;
+ case "ACK": // Success
+ bot.send("AUTHENTICATE PLAIN");
+ break;
+ }
+ });
+
+ bot._cmd.set("AUTHENTICATE", (msg: Message) => { // Authentication
+ if(msg.params[0].match(/\+/)) {
+ bot.send(`AUTHENTICATE ${Buffer.from(
+ bot.username + "\u0000" + bot.username + "\u0000" + bot.options.password
+ ).toString("base64")}`);
+ }
+ });
+
+ bot._cmd.set("900", (msg: Message) => { // End CAP
+ bot.send("CAP END");
+ });
+};
diff --git a/src/clients/irc/invite.mjs b/src/clients/irc/invite.mjs
deleted file mode 100644
index 7690ab7..0000000
--- a/src/clients/irc/invite.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-export default bot => {
- bot._cmd.set("INVITE", msg => { // invite
- const user = bot.parsePrefix(msg.prefix);
- const channel = msg.params[1];
-
- if(!bot.server.channel.has(channel)) {
- bot.join(channel);
- setTimeout(() => {
- bot.send(`PRIVMSG ${channel} :Hi. Wurde von ${user.nick} eingeladen.`);
- }, 1000);
- }
- });
-};
\ No newline at end of file
diff --git a/src/clients/irc/invite.ts b/src/clients/irc/invite.ts
new file mode 100644
index 0000000..a6d1211
--- /dev/null
+++ b/src/clients/irc/invite.ts
@@ -0,0 +1,15 @@
+import { Bot, Message, User } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("INVITE", (msg: Message) => {
+ const user: User = bot.parsePrefix(msg.prefix);
+ const channel: string = msg.params[1];
+
+ if(!bot.server.channel.has(channel)) {
+ bot.join(channel);
+ setTimeout(() => {
+ bot.send(`PRIVMSG ${channel} :Hi. Wurde von ${user.nick} eingeladen.`);
+ }, 1000);
+ }
+ });
+};
diff --git a/src/clients/irc/join.mjs b/src/clients/irc/join.mjs
deleted file mode 100644
index 829b87c..0000000
--- a/src/clients/irc/join.mjs
+++ /dev/null
@@ -1,5 +0,0 @@
-export default bot => {
- bot._cmd.set("JOIN", msg => { // join
- bot.send(`WHO ${msg.params[0]}`);
- });
-};
diff --git a/src/clients/irc/join.ts b/src/clients/irc/join.ts
new file mode 100644
index 0000000..7eeb043
--- /dev/null
+++ b/src/clients/irc/join.ts
@@ -0,0 +1,7 @@
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("JOIN", (msg: Message) => { // Handle join
+ bot.send(`WHO ${msg.params[0]}`);
+ });
+};
diff --git a/src/clients/irc/motd.mjs b/src/clients/irc/motd.mjs
deleted file mode 100644
index 44fd517..0000000
--- a/src/clients/irc/motd.mjs
+++ /dev/null
@@ -1,14 +0,0 @@
-export default bot => {
- bot._cmd.set("372", msg => { // motd_entry
- bot.server.motd += `${msg.params[1]}\n`;
- });
-
- bot._cmd.set("375", msg => { // motd_start
- bot.server.motd = `${msg.params[1]}\n`;
- });
-
- bot._cmd.set("376", msg => { // motd_end
- bot.server.motd += `${msg.params[1]}\n`;
- bot.emit("data", ["motd", bot.server.motd]);
- });
-};
diff --git a/src/clients/irc/motd.ts b/src/clients/irc/motd.ts
new file mode 100644
index 0000000..3028a79
--- /dev/null
+++ b/src/clients/irc/motd.ts
@@ -0,0 +1,16 @@
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("372", (msg: Message) => { // MOTD Entry
+ bot.server.motd += `${msg.params[1]}\n`;
+ });
+
+ bot._cmd.set("375", (msg: Message) => { // MOTD Start
+ bot.server.motd = `${msg.params[1]}\n`;
+ });
+
+ bot._cmd.set("376", (msg: Message) => { // MOTD End
+ bot.server.motd += `${msg.params[1]}\n`;
+ bot.emit("data", ["motd", bot.server.motd]);
+ });
+};
diff --git a/src/clients/irc/msg.mjs b/src/clients/irc/msg.ts
similarity index 64%
rename from src/clients/irc/msg.mjs
rename to src/clients/irc/msg.ts
index 0485fba..ca331e3 100644
--- a/src/clients/irc/msg.mjs
+++ b/src/clients/irc/msg.ts
@@ -1,5 +1,7 @@
-export default bot => {
- bot._cmd.set("PRIVMSG", msg => { // privmsg
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("PRIVMSG", (msg: Message) => { // Handle PRIVMSG
if(msg.params[1] === "\u0001VERSION\u0001")
return bot.emit("data", ["ctcp:version", bot.reply(msg)]);
else if(msg.params[1].match(/^\u0001PING .*\u0001/i))
@@ -8,7 +10,7 @@ export default bot => {
bot.emit("data", ["message", bot.reply(msg)]);
});
- bot._cmd.set("NOTICE", msg => { // notice
+ bot._cmd.set("NOTICE", (msg: Message) => { // Handle NOTICE
bot.emit("data", ["notice", msg.params[1]]);
});
};
diff --git a/src/clients/irc/nick.mjs b/src/clients/irc/nick.mjs
deleted file mode 100644
index 94a8645..0000000
--- a/src/clients/irc/nick.mjs
+++ /dev/null
@@ -1,8 +0,0 @@
-export default bot => {
- bot._cmd.set("NICK", msg => { // nickchange
- let prefix = bot.parsePrefix(msg.prefix);
- if (bot.server.user.has(prefix.nick))
- bot.server.user.delete(prefix.nick);
- bot.whois(msg.params[0], true); // force
- });
-};
diff --git a/src/clients/irc/nick.ts b/src/clients/irc/nick.ts
new file mode 100644
index 0000000..f21b73a
--- /dev/null
+++ b/src/clients/irc/nick.ts
@@ -0,0 +1,12 @@
+import { Bot, Message, User } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("NICK", (msg: Message) => { // Handle nick change
+ const prefix: User = bot.parsePrefix(msg.prefix);
+
+ if(bot.server.user.has(prefix.nick))
+ bot.server.user.delete(prefix.nick);
+
+ bot.whois(msg.params[0], true); // Force whois
+ });
+};
diff --git a/src/clients/irc/part.mjs b/src/clients/irc/part.mjs
deleted file mode 100644
index f5f1563..0000000
--- a/src/clients/irc/part.mjs
+++ /dev/null
@@ -1,5 +0,0 @@
-export default bot => {
- bot._cmd.set("PART", msg => { // part
- //delete this.server.user[msg.params[0]];
- });
-};
diff --git a/src/clients/irc/part.ts b/src/clients/irc/part.ts
new file mode 100644
index 0000000..3d1fcf7
--- /dev/null
+++ b/src/clients/irc/part.ts
@@ -0,0 +1,7 @@
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("PART", (msg: Message) => { // Handle part
+ delete bot.server.user[msg.params[0]];
+ });
+};
diff --git a/src/clients/irc/ping.mjs b/src/clients/irc/ping.mjs
deleted file mode 100644
index cec7f1f..0000000
--- a/src/clients/irc/ping.mjs
+++ /dev/null
@@ -1,5 +0,0 @@
-export default bot => {
- bot._cmd.set("PING", msg => { // ping
- bot.send(`PONG ${msg.params.join``}`);
- });
-};
diff --git a/src/clients/irc/ping.ts b/src/clients/irc/ping.ts
new file mode 100644
index 0000000..298b961
--- /dev/null
+++ b/src/clients/irc/ping.ts
@@ -0,0 +1,7 @@
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("PING", (msg: Message) => { // Handle PING
+ bot.send(`PONG ${msg.params.join('')}`);
+ });
+};
diff --git a/src/clients/irc/pwdreq.mjs b/src/clients/irc/pwdreq.mjs
deleted file mode 100644
index 845e58a..0000000
--- a/src/clients/irc/pwdreq.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-export default bot => {
- bot._cmd.set("464", msg => { // motd_entry
- if (bot.options.password.length > 0 && !bot.options.sasl)
- bot.send(`PASS ${bot.options.password}`);
- });
-};
diff --git a/src/clients/irc/pwdreq.ts b/src/clients/irc/pwdreq.ts
new file mode 100644
index 0000000..b5e97e0
--- /dev/null
+++ b/src/clients/irc/pwdreq.ts
@@ -0,0 +1,8 @@
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("464", (msg: Message) => { // Handle password request
+ if(bot.options.password.length > 0 && !bot.options.sasl)
+ bot.send(`PASS ${bot.options.password}`);
+ });
+};
diff --git a/src/clients/irc/welcome.mjs b/src/clients/irc/welcome.mjs
deleted file mode 100644
index d58e026..0000000
--- a/src/clients/irc/welcome.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-export default bot => {
- bot._cmd.set("001", msg => { // welcome
- bot.join(bot.options.channels);
- bot.emit("data", ["connected", msg.params[1]]);
- });
-};
diff --git a/src/clients/irc/welcome.ts b/src/clients/irc/welcome.ts
new file mode 100644
index 0000000..93d14aa
--- /dev/null
+++ b/src/clients/irc/welcome.ts
@@ -0,0 +1,8 @@
+import { Bot, Message } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("001", (msg: Message) => { // Handle welcome
+ bot.join(bot.options.channels);
+ bot.emit("data", ["connected", msg.params[1]]);
+ });
+};
diff --git a/src/clients/irc/who.mjs b/src/clients/irc/who.mjs
deleted file mode 100644
index c6bfa38..0000000
--- a/src/clients/irc/who.mjs
+++ /dev/null
@@ -1,20 +0,0 @@
-const max = 400;
-let whois = [];
-let chan;
-
-export default bot => {
- bot._cmd.set("352", msg => { // who_entry
- chan = msg.params[1];
- whois.push(msg.params[5]);
- });
-
- bot._cmd.set("315", msg => { // who_end
- bot.server.channel.set(chan, whois);
- whois = [...new Set(whois)];
- Array(Math.ceil(whois.length / 10)).fill().map(_ => whois.splice(0, 10)).forEach(l => {
- //console.log(l);
- bot.whois(l);
- });
- whois = [];
- });
-};
diff --git a/src/clients/irc/who.ts b/src/clients/irc/who.ts
new file mode 100644
index 0000000..2303bee
--- /dev/null
+++ b/src/clients/irc/who.ts
@@ -0,0 +1,21 @@
+import { Bot, Message } from "../../types";
+
+const max = 400;
+let whois: string[] = [];
+let chan: string;
+
+export default (bot: Bot) => {
+ bot._cmd.set("352", (msg: Message) => { // Handle WHO entry
+ chan = msg.params[1];
+ whois.push(msg.params[5]);
+ });
+
+ bot._cmd.set("315", (msg: Message) => { // Handle WHO end
+ bot.server.channel.set(chan, whois);
+ whois = [...new Set(whois)];
+ Array(Math.ceil(whois.length / 10)).fill(undefined).map(() => whois.splice(0, 10)).forEach((l: string[]) => {
+ bot.whois(l);
+ });
+ whois = [];
+ });
+};
diff --git a/src/clients/irc/whois.mjs b/src/clients/irc/whois.mjs
deleted file mode 100644
index 876d5f5..0000000
--- a/src/clients/irc/whois.mjs
+++ /dev/null
@@ -1,80 +0,0 @@
-export default bot => {
- bot._cmd.set("307", msg => { // whois_identified (ircd-hybrid)
- let tmpuser = {};
- if (bot.server.user.has(msg.params[1]))
- tmpuser = bot.server.user.get(msg.params[1]);
- tmpuser.account = msg.params[1];
- tmpuser.registered = true;
- bot.server.user.set(msg.params[1], tmpuser);
- });
-
- bot._cmd.set("311", msg => { // whois_userdata
- let tmpuser = {};
- if (bot.server.user.has(msg.params[1]))
- tmpuser = bot.server.user.get(msg.params[1]);
- tmpuser.nickname = msg.params[1];
- tmpuser.username = msg.params[2];
- tmpuser.hostname = msg.params[3];
- tmpuser.realname = msg.params[5];
- tmpuser.prefix = `${msg.params[1]}!${msg.params[2]}@${msg.params[3]}`;
- bot.server.user.set(msg.params[1], tmpuser);
- });
-
- bot._cmd.set("313", msg => { // whois_oper
- let tmpuser = {};
- if (bot.server.user.has(msg.params[1]))
- tmpuser = bot.server.user.get(msg.params[1]);
- tmpuser.oper = true;
- bot.server.user.set(msg.params[1], tmpuser);
- });
-
- bot._cmd.set("318", msg => { // whois_end
- let tmpuser = {};
- //bot.emit("data", ["info", `whois < ${msg.params[1]}`]);
- if (bot.server.user.has(msg.params[1]))
- tmpuser = bot.server.user.get(msg.params[1]);
- tmpuser = {
- nickname: tmpuser.nickname || false,
- username: tmpuser.username || false,
- hostname: tmpuser.hostname || false,
- realname: tmpuser.realname || false,
- account: tmpuser.account || false,
- prefix: tmpuser.prefix || false,
- registered: tmpuser.registered || false,
- oper: tmpuser.oper || false,
- channels: tmpuser.channels || [],
- cached: ~~(Date.now() / 1000)
- };
- bot.server.user.set(msg.params[1], tmpuser);
- if(msg.params[0] == msg.params[1]) {
- bot.server.me = tmpuser;
- bot.server.user.delete(msg.params[1]);
- }
- });
-
- bot._cmd.set("319", msg => { // whois_chanlist
- let tmpchan = new Map()
- , tmpuser = {};
- if (bot.server.user.has(msg.params[1])) {
- tmpuser = bot.server.user.get(msg.params[1]);
- if (tmpuser.channels)
- tmpchan = new Map(tmpuser.channels);
- }
- let chans = msg.params[2].trim().split(" ");
- for (let chan in chans) {
- chan = chans[chan].split("#");
- tmpchan.set(`#${chan[1]}`, chan[0]);
- }
- tmpuser.channels = tmpchan;
- bot.server.user.set(msg.params[1], tmpuser);
- });
-
- bot._cmd.set("330", msg => { // whois_authed_as (snircd)
- let tmpuser = {};
- if (bot.server.user.has(msg.params[1]))
- tmpuser = bot.server.user.get(msg.params[1]);
- tmpuser.account = msg.params[2];
- tmpuser.registered = true;
- bot.server.user.set(msg.params[1], tmpuser);
- });
-};
diff --git a/src/clients/irc/whois.ts b/src/clients/irc/whois.ts
new file mode 100644
index 0000000..44a253c
--- /dev/null
+++ b/src/clients/irc/whois.ts
@@ -0,0 +1,69 @@
+import { Bot, Message, User } from "../../types";
+
+export default (bot: Bot) => {
+ bot._cmd.set("307", (msg: Message) => { // whois_identified (ircd-hybrid)
+ let tmpuser: User = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.account = msg.params[1];
+ tmpuser.registered = true;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+
+ bot._cmd.set("311", (msg: Message) => { // whois_userdata
+ let tmpuser: User = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.nickname = msg.params[1];
+ tmpuser.username = msg.params[2];
+ tmpuser.hostname = msg.params[3];
+ tmpuser.realname = msg.params[5];
+ tmpuser.prefix = `${msg.params[1]}!${msg.params[2]}@${msg.params[3]}`;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+
+ bot._cmd.set("313", (msg: Message) => { // whois_oper
+ let tmpuser: User = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.oper = true;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+
+ bot._cmd.set("318", (msg: Message) => { // whois_end
+ let tmpuser: User = bot.server.user.get(msg.params[1]) || {};
+ tmpuser = {
+ nick: tmpuser.nick || false,
+ nickname: tmpuser.nickname || false,
+ username: tmpuser.username || false,
+ hostname: tmpuser.hostname || false,
+ realname: tmpuser.realname || false,
+ account: tmpuser.account || false,
+ prefix: tmpuser.prefix || false,
+ registered: tmpuser.registered || false,
+ oper: tmpuser.oper || false,
+ channels: tmpuser.channels || [],
+ cached: Math.floor(Date.now() / 1000),
+ };
+ bot.server.user.set(msg.params[1], tmpuser);
+ if(msg.params[0] === msg.params[1]) {
+ bot.server.me = tmpuser;
+ bot.server.user.delete(msg.params[1]);
+ }
+ });
+
+ bot._cmd.set("319", (msg: Message) => { // whois_chanlist
+ let tmpchan = new Map();
+ let tmpuser: User = bot.server.user.get(msg.params[1]) || {};
+ if(tmpuser.channels)
+ tmpchan = new Map(tmpuser.channels);
+ const chans = msg.params[2].trim().split(" ");
+ chans.forEach((chan) => {
+ const [flags, name] = chan.split("#");
+ tmpchan.set(`#${name}`, flags);
+ });
+ tmpuser.channels = tmpchan;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+
+ bot._cmd.set("330", (msg: Message) => { // whois_authed_as (snircd)
+ let tmpuser: User = bot.server.user.get(msg.params[1]) || {};
+ tmpuser.account = msg.params[2];
+ tmpuser.registered = true;
+ bot.server.user.set(msg.params[1], tmpuser);
+ });
+};
diff --git a/src/clients/slack.mjs b/src/clients/slack.mjs
deleted file mode 100644
index c039019..0000000
--- a/src/clients/slack.mjs
+++ /dev/null
@@ -1,217 +0,0 @@
-import https from "https";
-import url from "url";
-import EventEmitter from "events";
-import fetch from "flumm-fetch";
-
-export default class slack extends EventEmitter {
- constructor(options) {
- super();
- this.options = options || {};
- this.token = options.token || null;
- this.set = this.options.set || "all";
- this.network = "Slack";
- this.api = "https://slack.com/api";
- this.socket = null;
- this.interval = null;
- this.server = {
- set: this.set,
- channel: new Map(),
- user: new Map(),
- wss: {
- url: null,
- socket: null
- },
- me: {}
- };
-
- return (async () => {
- await this.connect();
- return this;
- })();
- }
- async connect() {
- const res = await (await fetch(`${this.api}/rtm.start?token=${this.token}`)).json();
-
- if (!res.ok)
- return this.emit("data", [ "error", res.description ]); // more infos
-
- res.channels?.forEach(channel => this.server.channel.set(channel.id, channel.name));
- res.users?.forEach(user => this.server.user.set(user.id, {
- account: user.name,
- nickname: user.real_name
- }));
-
- this.server.wss.url = url.parse(res.url);
-
- https.get({
- hostname: this.server.wss.url.host,
- path: this.server.wss.url.path,
- port: 443,
- headers: {
- "Upgrade": "websocket",
- "Connection": "Upgrade",
- "Sec-WebSocket-Version": 13,
- "Sec-WebSocket-Key": new Buffer.from(Array(16).fill().map(e => Math.round(Math.random() * 0xFF))).toString("base64")
- }
- }).on("upgrade", (_, sock) => {
- this.server.wss.socket = sock;
- this.server.wss.socket.setDefaultEncoding("utf-8");
-
- this.interval = setInterval(async () => await this.ping(), 3e4); // 30 seconds lul
-
- this.server.wss.socket.on("data", async data => {
- try {
- if (data.length < 2)
- throw "payload too short";
- if (data[1] & 0x80)
- throw "we no accept masked data";
- let offset = 2;
- let length = data[1] & 0x7F;
- if (length === 126) {
- offset = 4;
- length = data.readUInt16BE(2);
- }
- else if(length === 127)
- throw "payload too long";
- if (data.length < length + offset)
- throw "payload shorter than given length";
- //console.log(data, offset, length);
- data = JSON.parse(data.slice(offset, length + offset).toString().replace(/\0+$/, "")); // trim null bytes at the end
-
- //console.log(data, data.type);
-
- if (data.type !== "message")
- return false;
-
- await Promise.all([this.getChannel(data.channel), this.getUser(data.user)]);
-
- return this.emit("data", [ "message", this.reply(data) ]);
- }
- catch(err) {
- this.emit("data", [ "error", err ]);
- }
- })
- .on("end", async () => {
- this.emit("data", [ "debug", "stream ended" ]);
- await this.reconnect();
- })
- .on("error", async err => {
- this.emit("data", [ "error", err ]);
- await this.reconnect();
- });
- });
- }
-
- async reconnect() {
- this.server.wss.url = null;
- this.server.wss.socket = null;
- clearTimeout(this.interval);
- this.emit("data", [ "info", "reconnecting slack" ]);
- return await this.connect();
- }
-
- async getChannel(channelId) {
- if (this.server.channel.has(channelId))
- return this.server.channel.get(channelId);
-
- const res = await (await fetch(`${this.api}/conversations.info?channel=${channelId}&token=${this.token}`)).json();
- this.server.channel.set(channelId, res.channel.name);
- return res.channel.name;
- }
-
- async getUser(userId) {
- if (this.server.user.has(userId))
- return this.server.user.get(userId);
-
- const res = await (await fetch(`${this.api}/users.info?user=${userId}&token=${this.token}`)).json();
- this.server.user.set(userId, {
- account: res.user.name,
- nickname: res.user.real_name
- });
- }
-
- async send(channel, text) {
- text = Array.isArray(text) ? text.join("\n") : text;
- text = text.includes("\n") ? "```" + text + "```" : text;
- await this.write({
- type: "message",
- channel: channel,
- text: this.format(text)
- });
- }
-
- async ping() {
- return await this.write({
- type: "ping"
- });
- }
-
- async write(json) {
- const msg = JSON.stringify(json);
-
- const payload = Buffer.from(msg);
-
- if(payload.length > 2 ** 14) // 16KB limit
- throw this.emit("data", [ "error", "message too long, slack limit reached" ]);
-
- let frame_length = 6;
- let frame_payload_length = payload.length;
-
- if(payload.length > 125) {
- frame_length += 2;
- frame_payload_length = 126;
- }
-
- const frame = Buffer.alloc(frame_length);
-
- // set mask bit but leave mask key empty (= 0), so we don't have to mask the message
- frame.writeUInt16BE(0x8180 | frame_payload_length);
-
- if(frame_length > 6)
- frame.writeUInt16BE(payload.length, 2);
-
- if (!this.server.wss.socket)
- await this.reconnect();
-
- try {
- this.server.wss.socket.cork();
- this.server.wss.socket.write(frame);
- this.server.wss.socket.write(Buffer.from(msg));
- this.server.wss.socket.uncork();
- } catch(err) {
- console.error(err);
- }
- }
-
- reply(tmp) {
- return {
- type: "slack",
- network: "Slack",
- channel: this.server.channel.get(tmp.channel), // get channelname
- channelid: tmp.channel,
- user: {
- prefix: `${this.server.user.get(tmp.user).account}!${tmp.user}@${this.network}`, // get username
- nick: this.server.user.get(tmp.user).nickname, // get username
- username: this.server.user.get(tmp.user).nickname, // get username
- account: this.server.user.get(tmp.user).account
- },
- self: this.server,
- message: tmp.text,
- time: ~~(Date.now() / 1000),
- raw: tmp,
- reply: msg => this.send(tmp.channel, msg),
- replyAction: msg => this.send(tmp.channel, `[i]${msg}[/i]`),
- replyNotice: msg => this.send(tmp.channel, msg)
- };
- }
-
- format(msg) {
- return msg.toString()
- .replace(/\[b\](.*?)\[\/b\]/g, "*$1*") // bold
- .replace(/\[s\](.*?)\[\/s\]/g, "~$1~") // strike
- .replace(/\[i\](.*?)\[\/i\]/g, "_$1_") // italic
- .replace(/\[color=(.*?)](.*?)\[\/color\]/g, "$2")
- ;
- }
-
-}
diff --git a/src/clients/slack.ts b/src/clients/slack.ts
new file mode 100644
index 0000000..355f7f6
--- /dev/null
+++ b/src/clients/slack.ts
@@ -0,0 +1,267 @@
+import https from "node:https";
+import net from "node:net";
+import url from "node:url";
+import EventEmitter from "node:events";
+import fetch from "flumm-fetch";
+
+interface SlackOptions {
+ token: string;
+ set?: string;
+}
+
+interface User {
+ account: string;
+ nickname: string;
+}
+
+interface ServerInfo {
+ set: string;
+ channel: Map;
+ user: Map;
+ wss: {
+ url: url.UrlWithStringQuery | null;
+ socket: net.Socket | null;
+ };
+ me: Record;
+}
+
+interface SlackMessage {
+ type: string;
+ channel: string;
+ user: string;
+ text: string;
+}
+
+interface SlackRTMStartResponse {
+ ok: boolean;
+ url?: string;
+ channels?: { id: string; name: string }[];
+ users?: { id: string; name: string; real_name: string }[];
+ description?: string;
+}
+
+export default class slack extends EventEmitter {
+ private options: Required;
+ private token: string;
+ private api: string = "https://slack.com/api";
+ private interval: NodeJS.Timeout | null = null;
+ private server: ServerInfo;
+
+ constructor(options: SlackOptions) {
+ super();
+ this.options = {
+ set: "all",
+ ...options,
+ };
+ this.token = this.options.token;
+ this.server = {
+ set: this.options.set,
+ channel: new Map(),
+ user: new Map(),
+ wss: {
+ url: null,
+ socket: null,
+ },
+ me: {},
+ };
+
+ return (async () => {
+ await this.connect();
+ return this;
+ })() as unknown as slack;
+ }
+
+ async connect(): Promise {
+ const response = await fetch(`${this.api}/rtm.start?token=${this.token}`);
+ const res: SlackRTMStartResponse = await response.json();
+
+ if(!res.ok) {
+ this.emit("data", ["error", res.description || "Connection failed"]);
+ return;
+ }
+
+ res.channels?.forEach(channel => {
+ this.server.channel.set(channel.id, channel.name);
+ });
+ res.users?.forEach(user => {
+ this.server.user.set(user.id, {
+ account: user.name,
+ nickname: user.real_name,
+ });
+ });
+
+ if(res.url) {
+ this.server.wss.url = url.parse(res.url);
+ this.initializeWebSocket();
+ }
+ else
+ this.emit("data", ["error", "No WebSocket URL provided"]);
+ }
+
+ private initializeWebSocket(): void {
+ https.get({
+ hostname: this.server.wss.url?.host,
+ path: this.server.wss.url?.path,
+ port: 443,
+ headers: {
+ Upgrade: "websocket",
+ Connection: "Upgrade",
+ "Sec-WebSocket-Version": 13,
+ "Sec-WebSocket-Key": Buffer.from(
+ Array(16)
+ .fill(0)
+ .map(() => Math.round(Math.random() * 0xff))
+ ).toString("base64"),
+ }}, () => {}
+ )
+ .on("upgrade", (_, sock) => {
+ this.server.wss.socket = sock;
+ this.server.wss.socket.setEncoding("utf-8");
+ this.handleWebSocketEvents();
+ })
+ .on("error", err => {
+ this.emit("data", ["error", `Failed to establish WebSocket: ${err.message}`]);
+ });
+ }
+
+ private handleWebSocketEvents(): void {
+ if(!this.server.wss.socket)
+ return;
+
+ this.interval = setInterval(async () => await this.ping(), 30000);
+
+ this.server.wss.socket.on("data", async (data: Buffer) => {
+ try {
+ const parsedData = this.parseData(data);
+ if(parsedData?.type === "message") {
+ await Promise.all([
+ this.getChannel(parsedData.channel),
+ this.getUser(parsedData.user),
+ ]);
+ this.emit("data", ["message", this.reply(parsedData)]);
+ }
+ }
+ catch(err: any) {
+ this.emit("data", ["error", err]);
+ }
+ });
+
+ this.server.wss.socket.on("end", async () => {
+ this.emit("data", ["debug", "WebSocket stream ended"]);
+ await this.reconnect();
+ });
+
+ this.server.wss.socket.on("error", async (err: Error) => {
+ this.emit("data", ["error", err.message]);
+ await this.reconnect();
+ });
+ }
+
+ async reconnect(): Promise {
+ this.server.wss.url = null;
+ this.server.wss.socket = null;
+ if(this.interval)
+ clearInterval(this.interval);
+ this.emit("data", ["info", "reconnecting slack"]);
+ await this.connect();
+ }
+
+ async getChannel(channelId: string): Promise {
+ if(this.server.channel.has(channelId))
+ return this.server.channel.get(channelId);
+
+ const res = await (await fetch(`${this.api}/conversations.info?channel=${channelId}&token=${this.token}`)).json() as { channel: { name: string } };
+ if(!res.channel)
+ throw new Error("Channel not found");
+ this.server.channel.set(channelId, res.channel.name);
+ return res.channel.name;
+ }
+
+ async getUser(userId: string): Promise {
+ if(this.server.user.has(userId))
+ return this.server.user.get(userId);
+
+ const res = await (await fetch(`${this.api}/users.info?user=${userId}&token=${this.token}`)).json() as { user: { name: string; real_name: string } };
+ if(!res.user)
+ throw new Error("User not found");
+ const user = { account: res.user.name, nickname: res.user.real_name };
+ this.server.user.set(userId, user);
+ return user;
+ }
+
+ async send(channel: string, text: string | string[]): Promise {
+ const message = Array.isArray(text) ? text.join("\n") : text;
+ const formatted = message.includes("\n") ? "```" + message + "```" : message;
+ await this.write({
+ type: "message",
+ channel: channel,
+ text: this.format(formatted),
+ });
+ }
+
+ async ping(): Promise {
+ await this.write({ type: "ping" });
+ }
+
+ async write(json: object): Promise {
+ const msg = JSON.stringify(json);
+ const payload = Buffer.from(msg);
+
+ if(payload.length > 2 ** 14) {
+ this.emit("data", ["error", "message too long, slack limit reached"]);
+ return;
+ }
+
+ if(!this.server.wss.socket) {
+ await this.reconnect();
+ return;
+ }
+
+ try {
+ this.server.wss.socket.cork();
+ this.server.wss.socket.write(payload);
+ this.server.wss.socket.uncork();
+ }
+ catch(err: any) {
+ console.error(err);
+ await this.reconnect();
+ }
+ }
+
+ reply(tmp: SlackMessage): any {
+ return {
+ type: "slack",
+ network: "Slack",
+ channel: this.server.channel.get(tmp.channel),
+ channelid: tmp.channel,
+ user: this.server.user.get(tmp.user),
+ self: this.server,
+ message: tmp.text,
+ time: ~~(Date.now() / 1000),
+ raw: tmp,
+ reply: (msg: string) => this.send(tmp.channel, msg),
+ replyAction: (msg: string) => this.send(tmp.channel, `[i]${msg}[/i]`),
+ replyNotice: (msg: string) => this.send(tmp.channel, msg),
+ };
+ }
+
+ private parseData(data: Buffer): SlackMessage | undefined {
+ try {
+ const json = JSON.parse(data.toString());
+ return json;
+ }
+ catch(err: any) {
+ this.emit("data", ["error", "failed to parse data"]);
+ return undefined;
+ }
+ }
+
+ format(msg: string): string {
+ return msg.toString()
+ .replace(/\[b\](.*?)\[\/b\]/g, "*$1*") // bold
+ .replace(/\[s\](.*?)\[\/s\]/g, "~$1~") // strike
+ .replace(/\[i\](.*?)\[\/i\]/g, "_$1_") // italic
+ .replace(/\[color=(.*?)](.*?)\[\/color\]/g, "$2")
+ ;
+ }
+}
diff --git a/src/clients/tg.mjs b/src/clients/tg.mjs
deleted file mode 100644
index 6ee4e9f..0000000
--- a/src/clients/tg.mjs
+++ /dev/null
@@ -1,183 +0,0 @@
-import fetch from "flumm-fetch";
-import EventEmitter from "events";
-
-const allowedFiles = [ 'audio', 'video', 'photo', 'document' ];
-
-export default class tg extends EventEmitter {
- constructor(options) {
- super();
- this.options = options || {};
- this.token = options.token || null;
- this.options.pollrate = options.pollrate || 1000;
- this.set = this.options.set || "all";
- this.network = "Telegram";
- this.api = `https://api.telegram.org/bot${this.token}`;
- this.lastUpdate = 0;
- this.lastMessage = 0;
- this.poller = null;
- this.server = {
- set: this.set,
- channel: new Map(),
- user: new Map(),
- me: {}
- };
-
- return (async () => {
- await this.connect();
- await this.poll();
- return this;
- })();
- }
- async connect() {
- const res = await (await fetch(`${this.api}/getMe`)).json();
- if (!res.ok)
- throw this.emit("data", ["error", res.description]); // more infos
-
- this.me = res.result;
- this.server.me = {
- nickname: res.result.first_name,
- username: res.result.username,
- account: res.result.id.toString(),
- prefix: `${res.result.username}!${res.result.id.toString()}`,
- id: res.result.id.toString()
- };
- }
- async getFile(file_id) {
- const res = await (await fetch(`${this.api}/getFile?file_id=${file_id}`)).json();
- if(!res.ok)
- return false;
- return `https://api.telegram.org/file/bot${this.token}/${res.result.file_path}`;
- }
- async poll() {
- try {
- const _res = await (await fetch(`${this.api}/getUpdates?offset=${this.lastUpdate}&allowed_updates=message`)).json();
-
- if(!_res.ok)
- throw { type: "tg", message: _res.description};
- if(_res.result.length === 0)
- return
-
- this.lastUpdate = _res.result[_res.result.length - 1].update_id + 1;
-
- _res.result.forEach(async res => {
- if(res.hasOwnProperty("message")) {
- if(res.message?.date >= ~~(Date.now() / 1000) - 10 && res.message?.message_id !== this.lastMessage) {
- this.lastMessage = res.message.message_id;
- if(!this.server.user.has(res.message.from.username || res.message.from.first_name)) {
- this.server.user.set(res.message.from.username || res.message.from.first_name, {
- nick: res.message.from.first_name,
- username: res.message.from.username,
- account: res.message.from.id.toString(),
- prefix: `${res.message.from.username}!${res.message.from.id.toString()}@${this.network}`,
- id: res.message.from.id
- });
- }
-
- try {
- let key;
- if(key = Object.keys(res.message).filter(t => allowedFiles.includes(t))?.[0]) {
- let media = res.message[key];
- if(key === 'photo')
- media = res.message[key][res.message[key].length - 1];
- res.message.media = await this.getFile(media.file_id);
- res.message.text = res.message.caption;
- delete res.message[key];
- }
- } catch {
- // no media files
- }
-
- this.emit("data", ["message", this.reply(res.message)]);
- }
- }
- else if(res.hasOwnProperty("callback_query")) {
- this.emit("data", ["callback_query", {
- ...res.callback_query,
- editMessageText: this.editMessageText.bind(this)
- }]);
- }
- else if(res.hasOwnProperty("inline_query")) {
- this.emit("data", ["inline_query", res.inline_query]);
- }
- });
- }
- catch(err) {
- if(!err.type)
- this.emit("data", ["error", "tg timed out lol"]);
- else if(err.type === "tg")
- this.emit("data", ["error", err.message]);
- await this.connect();
- }
- finally {
- setTimeout(async () => {
- await this.poll();
- }, this.options.pollrate);
- }
- }
- async editMessageText(chat_id, message_id, text, opt = {}) {
- const opts = {
- method: "POST",
- body: {
- chat_id: chat_id,
- message_id: message_id,
- text: text,
- ...opt
- }
- };
- await fetch(`${this.api}/editMessageText`, opts);
- }
- async send(chatid, msg, reply = null, opt = {}) {
- msg = Array.isArray(msg) ? msg.join("\n") : msg;
- if (msg.length === 0 || msg.length > 2048)
- return this.emit("data", ["error", "msg to short or to long lol"]);
- const opts = {
- method: "POST",
- body: {
- chat_id: chatid,
- text: this.format(msg),
- parse_mode: "HTML",
- ...opt
- }
- };
- if (reply)
- opts.body.reply_to_message_id = reply;
- await fetch(`${this.api}/sendMessage`, opts);
- }
- async sendmsg(mode, recipient, msg) {
- await this.send(recipient, msg);
- }
- reply(tmp) {
- return {
- type: "tg",
- network: "Telegram",
- channel: tmp.chat.title,
- channelid: tmp.chat.id,
- user: {
- prefix: `${tmp.from.username}!${tmp.from.id}@${this.network}`,
- nick: tmp.from.first_name,
- username: tmp.from.username,
- account: tmp.from.id.toString()
- },
- self: this.server,
- message: tmp.text,
- time: tmp.date,
- raw: tmp,
- media: tmp.media || null,
- reply: (msg, opt = {}) => this.send(tmp.chat.id, msg, tmp.message_id, opt),
- replyAction: msg => this.send(tmp.chat.id, `Uwe ${msg}`, tmp.message_id),
- replyNotice: msg => this.send(tmp.chat.id, msg, tmp.message_id),
- _user: this.server.user
- };
- }
- format(msg) {
- return msg.toString()
- .split("&").join("&")
- .split("<").join("<")
- .split(">").join(">")
- .replace(/\[b\](.*?)\[\/b\]/gsm, "$1") // bold
- .replace(/\[i\](.*?)\[\/i\]/gsm, "$1") // italic
- .replace(/\[color=(.*?)](.*?)\[\/color\]/gsm, "$2")
- .replace(/\[pre\](.*?)\[\/pre\]/gsm, "$1
")
- ;
- }
-}
diff --git a/src/clients/tg.ts b/src/clients/tg.ts
new file mode 100644
index 0000000..cf6ed6b
--- /dev/null
+++ b/src/clients/tg.ts
@@ -0,0 +1,232 @@
+import fetch from "flumm-fetch";
+import EventEmitter from "events";
+
+const allowedFiles = ["audio", "video", "photo", "document"] as const;
+
+type AllowedFileTypes = typeof allowedFiles[number];
+
+interface TelegramOptions {
+ token: string;
+ pollrate?: number;
+ set?: string;
+}
+
+interface ServerInfo {
+ set: string;
+ channel: Map;
+ user: Map;
+ me: Record;
+}
+
+interface TelegramResponse {
+ ok: boolean;
+ result?: T;
+ description?: string;
+}
+
+interface TelegramMessage {
+ message_id: number;
+ from: {
+ id: number;
+ is_bot: boolean;
+ first_name: string;
+ username?: string;
+ };
+ chat: {
+ id: number;
+ title?: string;
+ type: string;
+ };
+ date: number;
+ text?: string;
+ caption?: string;
+ [key: string]: any;
+}
+
+interface TelegramUpdate {
+ update_id: number;
+ message?: TelegramMessage;
+ callback_query?: any;
+ inline_query?: any;
+}
+
+export default class tg extends EventEmitter {
+ private options: Required;
+ private token: string;
+ private api: string;
+ private lastUpdate: number = 0;
+ private lastMessage: number = 0;
+ private poller: NodeJS.Timeout | null = null;
+ private server: ServerInfo = {
+ set: "",
+ channel: new Map(),
+ user: new Map(),
+ me: {},
+ };
+
+ constructor(options: TelegramOptions) {
+ super();
+ this.options = {
+ pollrate: 1000,
+ set: "all",
+ ...options,
+ };
+ this.token = this.options.token;
+ this.api = `https://api.telegram.org/bot${this.token}`;
+ this.server.set = this.options.set;
+
+ return (async () => {
+ await this.connect();
+ await this.poll();
+ return this;
+ })() as unknown as tg;
+ }
+
+ async connect(): Promise {
+ const res = await (await fetch(`${this.api}/getMe`)).json() as TelegramResponse;
+ if(!res.ok)
+ throw this.emit("data", ["error", res.description ?? "Unknown error"]);
+
+ this.server.me = {
+ nickname: res.result.first_name,
+ username: res.result.username,
+ account: res.result.id.toString(),
+ prefix: `${res.result.username}!${res.result.id.toString()}`,
+ id: res.result.id.toString(),
+ };
+ }
+
+ async getFile(fileId: string): Promise {
+ const res = await (await fetch(`${this.api}/getFile?file_id=${fileId}`)).json() as TelegramResponse<{ file_path: string }>;
+ if(!res.ok)
+ return false;
+ return `https://api.telegram.org/file/bot${this.token}/${res.result?.file_path}`;
+ }
+
+ async poll(): Promise {
+ try {
+ const updates = await this.fetchUpdates();
+ if(!updates || updates.length === 0)
+ return;
+
+ this.lastUpdate = updates[updates.length - 1].update_id + 1;
+
+ for(const update of updates)
+ await this.processUpdate(update);
+ }
+ catch(err: any) {
+ await this.handlePollError(err);
+ }
+ finally {
+ setTimeout(() => this.poll(), this.options.pollrate);
+ }
+ }
+
+ private async fetchUpdates(): Promise {
+ const res = await (await fetch(`${this.api}/getUpdates?offset=${this.lastUpdate}&allowed_updates=message`)).json() as TelegramResponse;
+ if(!res.ok)
+ throw new Error(res.description || "Failed to fetch updates");
+ return res.result || [];
+ }
+
+ private async processUpdate(update: any): Promise {
+ if(update.message)
+ await this.handleMessage(update.message);
+ else if(update.callback_query)
+ this.emit("data", ["callback_query", this.reply(update.callback_query.message, update.callback_query)]);
+ else if(update.inline_query)
+ this.emit("data", ["inline_query", update.inline_query]);
+ }
+
+ private async handleMessage(message: any): Promise {
+ if (
+ message.date >= Math.floor(Date.now() / 1000) - 10 &&
+ message.message_id !== this.lastMessage
+ ) {
+ this.lastMessage = message.message_id;
+
+ if(!this.server.user.has(message.from.username || message.from.first_name)) {
+ this.server.user.set(message.from.username || message.from.first_name, {
+ nick: message.from.first_name,
+ username: message.from.username,
+ account: message.from.id.toString(),
+ prefix: `${message.from.username}!${message.from.id.toString()}@Telegram`,
+ id: message.from.id,
+ });
+ }
+
+ try {
+ const fileKey = Object.keys(message).find(key =>
+ allowedFiles.includes(key as AllowedFileTypes)
+ );
+ if(fileKey) {
+ let media = message[fileKey];
+ if(fileKey === "photo")
+ media = message[fileKey][message[fileKey].length - 1];
+ message.media = await this.getFile(media.file_id);
+ message.text = message.caption;
+ delete message[fileKey];
+ }
+ }
+ catch {}
+
+ this.emit("data", ["message", this.reply(message)]);
+ }
+ }
+
+ private async handlePollError(err: any): Promise {
+ if(!err.type)
+ this.emit("data", ["error", "tg timed out lol"]);
+ else if(err.type === "tg")
+ this.emit("data", ["error", err.message]);
+ await this.connect();
+ }
+
+ reply(tmp: any, opt = {}): any {
+ return {
+ type: "tg",
+ network: "Telegram",
+ channel: tmp.chat?.title,
+ channelid: tmp.chat?.id,
+ user: {
+ prefix: `${tmp.from.username}!${tmp.from.id}@Telegram`,
+ nick: tmp.from.first_name,
+ username: tmp.from.username,
+ account: tmp.from.id.toString(),
+ },
+ self: this.server,
+ message: tmp.text,
+ time: tmp.date,
+ raw: tmp,
+ media: tmp.media || null,
+ reply: async (msg: string, opt = {}) => await this.send(tmp.chat.id, msg, tmp.message_id, opt),
+ replyAction: async (msg: string, opt = {}) => await this.send(tmp.chat.id, `Action ${msg}`, tmp.message_id, opt),
+ };
+ }
+
+ async send(chatId: number, msg: string, reply: number | null = null, opt = {}): Promise {
+ const body: { chat_id: number; text: string; parse_mode: string; reply_to_message_id?: number } = {
+ chat_id: chatId,
+ text: this.format(msg),
+ parse_mode: "HTML",
+ ...opt,
+ };
+ if(reply)
+ body["reply_to_message_id"] = reply;
+
+ const opts = { method: "POST", body };
+ return await (await fetch(`${this.api}/sendMessage`, opts)).json();
+ }
+
+ format(msg: string): string {
+ return msg.toString()
+ .split("&").join("&")
+ .split("<").join("<")
+ .split(">").join(">")
+ .replace(/\[b\](.*?)\[\/b\]/gsm, "$1") // bold
+ .replace(/\[i\](.*?)\[\/i\]/gsm, "$1") // italic
+ .replace(/\[color=(.*?)](.*?)\[\/color\]/gsm, "$2")
+ .replace(/\[pre\](.*?)\[\/pre\]/gsm, "$1
")
+ ;
+ }
+}
diff --git a/src/index.mjs b/src/index.mjs
deleted file mode 100644
index 3baea33..0000000
--- a/src/index.mjs
+++ /dev/null
@@ -1,40 +0,0 @@
-import fs from "fs";
-import { fileURLToPath } from "url";
-import { dirname } from "path";
-import EventEmitter from "events";
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-
-export default class cuffeo extends EventEmitter {
- constructor(cfg) {
- super();
- this.clients = [];
- this.libs = {};
-
- return (async () => {
- this.libs = await this.loadLibs();
- this.clients = await this.registerClients(cfg);
- return this;
- })();
- }
- async loadLibs() {
- return (await (Promise.all((await fs.promises.readdir(`${__dirname}/clients`)).filter(f => f.endsWith(".mjs")).map(async client => {
- const lib = (await import(`./clients/${client}`)).default;
- return { [lib.name]: lib };
- })))).reduce((a, b) => ({ ...a, ...b }));
- }
- async registerClients(cfg) {
- return cfg.filter(e => e.enabled).map(async srv => {
- if(!Object.keys(this.libs).includes(srv.type))
- throw new Error(`not supported client: ${srv.type}`);
-
- const client = {
- name: srv.network,
- type: srv.type,
- client: await new this.libs[srv.type](srv)
- };
- client.client.on("data", ([type, data]) => this.emit(type, data));
- return client;
- });
- }
-};
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..51c7f82
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,82 @@
+import fs from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname } from "node:path";
+import EventEmitter from "node:events";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+interface Config {
+ enabled: boolean;
+ type: string;
+ network: string;
+}
+
+interface Client {
+ name: string;
+ type: string;
+ client: any;
+}
+
+interface CuffeoEvents {
+ data: [string, any];
+ error: [Error];
+}
+
+export default class Cuffeo extends EventEmitter {
+ private clients: Client[] = [];
+ private libs: Record = {};
+
+ emit(event: K, ...args: CuffeoEvents[K]): boolean {
+ return super.emit(event, ...args);
+ }
+
+ on(event: K, listener: (...args: CuffeoEvents[K]) => void): this {
+ return super.on(event, listener);
+ }
+
+ constructor(cfg: Config[]) {
+ super();
+
+ return (async () => {
+ this.libs = await this.loadLibs();
+ this.clients = await this.registerClients(cfg);
+ return this;
+ })() as unknown as Cuffeo;
+ }
+
+ private async loadLibs(): Promise> {
+ const clientFiles = await fs.promises.readdir(`${__dirname}/clients`);
+ const modules = await Promise.all(
+ clientFiles
+ .filter(f => f.endsWith(".js"))
+ .map(async client => {
+ const lib = (await import(`./clients/${client}`)).default;
+ return { [lib.name]: lib };
+ })
+ );
+ return modules.reduce((a, b) => ({ ...a, ...b }), {});
+ }
+
+ private async registerClients(cfg: Config[]): Promise {
+ return Promise.all(
+ cfg
+ .filter(e => e.enabled)
+ .map(async srv => {
+ if(!Object.keys(this.libs).includes(srv.type))
+ throw new Error(`unsupported client: ${srv.type}`);
+
+ const client: Client = {
+ name: srv.network,
+ type: srv.type,
+ client: await new this.libs[srv.type](srv),
+ };
+
+ client.client.on("data", ([type, data]: [keyof CuffeoEvents, any]) =>
+ this.emit(type, data)
+ );
+
+ return client;
+ })
+ );
+ }
+}
diff --git a/src/test.mjs b/src/test.mjs
new file mode 100644
index 0000000..147592e
--- /dev/null
+++ b/src/test.mjs
@@ -0,0 +1,33 @@
+import cuffeo from '../dist/index.js';
+
+const clients = [{
+ "type": "irc",
+ "enabled": false,
+ "network": "n0xy",
+ "host": "irc.n0xy.net",
+ "port": 6697,
+ "ssl": true,
+ "selfSigned": false,
+ "sasl": false,
+ "nickname": "cuffeots",
+ "username": "cuffeots",
+ "realname": "cuffeo",
+ "channels": [
+ "#kbot-dev"
+ ]
+}, {
+ "type": "tg",
+ "enabled": true,
+ "token": "1225044594:AAFii7CRCZsmxo1i7DSVXPgd6IFxtVd1Uig",
+ "pollrate": 1001
+}];
+
+const bot = await new cuffeo(clients);
+bot.on("message", msg => {
+ console.log(msg);
+ msg.reply("pong");
+});
+
+bot.on("data", data => {
+ console.log(data);
+});
diff --git a/src/types.d.ts b/src/types.d.ts
new file mode 100644
index 0000000..890bc15
--- /dev/null
+++ b/src/types.d.ts
@@ -0,0 +1,34 @@
+export interface Message {
+ prefix: string;
+ params: string[];
+}
+
+export interface Bot {
+ username: string;
+ parsePrefix: (prefix: string) => User;
+ _cmd: Map void>;
+ send: (message: string) => void;
+ server: {
+ channel: Map;
+ motd: string;
+ //user: Map;
+ user: {
+ [channel: string]: any
+ };
+ me?: User;
+ };
+ options: {
+ password: string;
+ sasl: boolean;
+ channels: string[];
+ };
+ join: (channel: string | string[]) => void;
+ emit: (event: string, args: any[]) => void;
+ reply: (msg: Message) => string;
+ whois: (nick: string | string[], force?: boolean) => void;
+}
+
+export interface User {
+ nick: string | false;
+ [key: string]: any;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..510781e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "es2024",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "removeComments": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}