typescript schmypescript

This commit is contained in:
2025-03-18 09:55:57 +01:00
parent f7ac8ae5cd
commit 52d79e0763
52 changed files with 1948 additions and 891 deletions

216
dist/clients/irc.js vendored Normal file
View File

@ -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)
};
}
}

20
dist/clients/irc/cap.js vendored Normal file
View File

@ -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");
});
};

12
dist/clients/irc/invite.js vendored Normal file
View File

@ -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);
}
});
};

5
dist/clients/irc/join.js vendored Normal file
View File

@ -0,0 +1,5 @@
export default (bot) => {
bot._cmd.set("JOIN", (msg) => {
bot.send(`WHO ${msg.params[0]}`);
});
};

12
dist/clients/irc/motd.js vendored Normal file
View File

@ -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]);
});
};

13
dist/clients/irc/msg.js vendored Normal file
View File

@ -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]]);
});
};

8
dist/clients/irc/nick.js vendored Normal file
View File

@ -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);
});
};

5
dist/clients/irc/part.js vendored Normal file
View File

@ -0,0 +1,5 @@
export default (bot) => {
bot._cmd.set("PART", (msg) => {
delete bot.server.user[msg.params[0]];
});
};

5
dist/clients/irc/ping.js vendored Normal file
View File

@ -0,0 +1,5 @@
export default (bot) => {
bot._cmd.set("PING", (msg) => {
bot.send(`PONG ${msg.params.join('')}`);
});
};

6
dist/clients/irc/pwdreq.js vendored Normal file
View File

@ -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}`);
});
};

6
dist/clients/irc/welcome.js vendored Normal file
View File

@ -0,0 +1,6 @@
export default (bot) => {
bot._cmd.set("001", (msg) => {
bot.join(bot.options.channels);
bot.emit("data", ["connected", msg.params[1]]);
});
};

17
dist/clients/irc/who.js vendored Normal file
View File

@ -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 = [];
});
};

62
dist/clients/irc/whois.js vendored Normal file
View File

@ -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);
});
};

200
dist/clients/slack.js vendored Normal file
View File

@ -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");
}
}

159
dist/clients/tg.js vendored Normal file
View File

@ -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("&lt;")
.split(">").join("&gt;")
.replace(/\[b\](.*?)\[\/b\]/gsm, "<b>$1</b>")
.replace(/\[i\](.*?)\[\/i\]/gsm, "<i>$1</i>")
.replace(/\[color=(.*?)](.*?)\[\/color\]/gsm, "$2")
.replace(/\[pre\](.*?)\[\/pre\]/gsm, "<pre>$1</pre>");
}
}