Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
62f45722c4 | |||
90f43a734e | |||
2d13c865af | |||
ca114f677e | |||
3e1cfe18f9 | |||
52d79e0763 |
93
dist/clients/irc.d.ts
vendored
Normal file
93
dist/clients/irc.d.ts
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
import EventEmitter from "events";
|
||||
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[];
|
||||
}
|
||||
interface IRCEvents {
|
||||
data: [string | [string, any]];
|
||||
error: [string];
|
||||
}
|
||||
export default class irc extends EventEmitter {
|
||||
private options;
|
||||
private socket?;
|
||||
private _recachetime;
|
||||
private _cmd;
|
||||
private server;
|
||||
emit<K extends keyof IRCEvents>(event: K, ...args: IRCEvents[K]): boolean;
|
||||
on<K extends keyof IRCEvents>(event: K, listener: (...args: IRCEvents[K]) => void): this;
|
||||
constructor(options: IRCOptions);
|
||||
initialize(): Promise<void>;
|
||||
private createSocket;
|
||||
connect(reconnect?: boolean): void;
|
||||
private handleConnection;
|
||||
private handleData;
|
||||
private handleDisconnect;
|
||||
private handleError;
|
||||
private join;
|
||||
private part;
|
||||
private whois;
|
||||
send(data: string): void;
|
||||
sendmsg(mode: MessageModes, recipient: string, msg: string | string[]): void;
|
||||
parse(data: string): ParsedCommand;
|
||||
private format;
|
||||
parsePrefix(prefix: string | null): {
|
||||
nick: string;
|
||||
username: string;
|
||||
hostname: string;
|
||||
} | false;
|
||||
reply(tmp: ParsedCommand): {
|
||||
type: string;
|
||||
network: string;
|
||||
channel: string;
|
||||
channelid: string;
|
||||
user: {
|
||||
account: string | boolean;
|
||||
prefix: string;
|
||||
nick?: string | undefined;
|
||||
username?: string | undefined;
|
||||
hostname?: string | undefined;
|
||||
};
|
||||
message: string;
|
||||
time: number;
|
||||
raw: ParsedCommand;
|
||||
reply: (msg: string) => void;
|
||||
replyAction: (msg: string) => void;
|
||||
replyNotice: (msg: string) => void;
|
||||
self: {
|
||||
set: string;
|
||||
motd: string;
|
||||
me: Record<string, unknown>;
|
||||
channel: Map<string, unknown>;
|
||||
user: Map<string, {
|
||||
account?: string;
|
||||
cached: number;
|
||||
}>;
|
||||
};
|
||||
_chan: unknown;
|
||||
_user: Map<string, {
|
||||
account?: string;
|
||||
cached: number;
|
||||
}>;
|
||||
_cmd: Map<string, (cmd: ParsedCommand) => void>;
|
||||
join: (chan: string) => void;
|
||||
part: (chan: string, msg?: string) => void;
|
||||
whois: (user: string) => void;
|
||||
write: (msg: string) => void;
|
||||
};
|
||||
}
|
||||
export {};
|
214
dist/clients/irc.js
vendored
Normal file
214
dist/clients/irc.js
vendored
Normal file
@ -0,0 +1,214 @@
|
||||
import _fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
import net from "net";
|
||||
import tls from "tls";
|
||||
import EventEmitter from "events";
|
||||
const fs = _fs.promises;
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const colors = {
|
||||
white: "00", black: "01", navy: "02", green: "03", red: "04",
|
||||
brown: "05", purple: "06", orange: "07", yellow: "08",
|
||||
lightgreen: "09", teal: "10", cyan: "11", blue: "12",
|
||||
magenta: "13", gray: "14", lightgray: "15"
|
||||
};
|
||||
const msgmodes = {
|
||||
normal: "PRIVMSG {recipient} :{msg}",
|
||||
action: "PRIVMSG {recipient} :\u0001ACTION {msg}\u0001",
|
||||
notice: "NOTICE {recipient} :{msg}",
|
||||
};
|
||||
const replaceColor = (match, color, text) => {
|
||||
return colors[color] ? `\x03${colors[color]}${text}\x0F` : text;
|
||||
};
|
||||
export default class irc extends EventEmitter {
|
||||
options;
|
||||
socket;
|
||||
_recachetime = 60 * 30;
|
||||
_cmd = new Map();
|
||||
server;
|
||||
emit(event, ...args) {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
on(event, listener) {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
constructor(options) {
|
||||
super();
|
||||
this.options = {
|
||||
channels: [],
|
||||
host: "127.0.0.1",
|
||||
port: 6667,
|
||||
ssl: false,
|
||||
selfSigned: false,
|
||||
sasl: false,
|
||||
network: "test",
|
||||
nickname: "test",
|
||||
username: "test",
|
||||
realname: "test",
|
||||
set: "all",
|
||||
...options
|
||||
};
|
||||
this.server = {
|
||||
set: this.options.set,
|
||||
motd: "",
|
||||
me: {},
|
||||
channel: new Map(),
|
||||
user: new Map()
|
||||
};
|
||||
return (async () => {
|
||||
await this.initialize();
|
||||
return this;
|
||||
})();
|
||||
}
|
||||
async initialize() {
|
||||
const dir = (await fs.readdir(`${__dirname}/irc`)).filter(f => f.endsWith(".js"));
|
||||
await Promise.all(dir.map(async (mod) => {
|
||||
return (await import(`${__dirname}/irc/${mod}`)).default(this);
|
||||
}));
|
||||
this.connect();
|
||||
}
|
||||
createSocket() {
|
||||
return this.options.ssl
|
||||
? tls.connect({
|
||||
host: this.options.host,
|
||||
port: this.options.port,
|
||||
rejectUnauthorized: !this.options.selfSigned,
|
||||
})
|
||||
: net.connect({
|
||||
host: this.options.host,
|
||||
port: this.options.port,
|
||||
});
|
||||
}
|
||||
connect(reconnect = false) {
|
||||
if (reconnect)
|
||||
this.socket = undefined;
|
||||
this.socket = this.createSocket();
|
||||
this.socket.on("data", (msg) => this.handleData(msg));
|
||||
this.socket.on("end", () => this.handleDisconnect());
|
||||
this.socket.on("error", (err) => this.handleError(err));
|
||||
this.socket.setEncoding("utf-8");
|
||||
this.handleConnection();
|
||||
}
|
||||
handleConnection() {
|
||||
this.send(`NICK ${this.options.nickname}`);
|
||||
this.send(`USER ${this.options.username} 0 * :${this.options.realname}`);
|
||||
if (this.options.sasl)
|
||||
this.send("CAP LS");
|
||||
this.emit("data", "[irc] connected!");
|
||||
}
|
||||
handleData(msg) {
|
||||
msg.split(/\r?\n|\r/).forEach((line) => {
|
||||
if (line.trim().length > 0) {
|
||||
const cmd = this.parse(line);
|
||||
if (this._cmd.has(cmd.command))
|
||||
this._cmd.get(cmd.command)?.(cmd);
|
||||
}
|
||||
});
|
||||
}
|
||||
handleDisconnect() {
|
||||
this.emit("data", ["error", "[irc] stream ended, reconnecting in progress"]);
|
||||
this.connect(true);
|
||||
}
|
||||
handleError(err) {
|
||||
this.emit("data", ["error", `[irc] socket error: ${err.message}`]);
|
||||
this.connect(true);
|
||||
}
|
||||
join(channels) {
|
||||
if (!Array.isArray(channels))
|
||||
channels = [channels];
|
||||
channels.forEach(e => {
|
||||
this.send(`JOIN ${e}`);
|
||||
});
|
||||
}
|
||||
part(channel, msg) {
|
||||
this.send(`PART ${channel} :${msg || ""}`);
|
||||
}
|
||||
whois(nick) {
|
||||
if (!Array.isArray(nick))
|
||||
nick = [nick];
|
||||
nick.forEach(e => {
|
||||
this.send(`WHOIS ${e}`);
|
||||
});
|
||||
}
|
||||
send(data) {
|
||||
if (this.socket)
|
||||
this.socket.write(`${data}\n`);
|
||||
else
|
||||
this.emit("data", ["info", `[irc] nope: ${data}`]);
|
||||
}
|
||||
sendmsg(mode, recipient, msg) {
|
||||
const messages = Array.isArray(msg) ? msg : msg.split(/\r?\n/);
|
||||
if (messages.length >= 5)
|
||||
this.emit("data", ["error", "[irc] too many lines"]);
|
||||
messages.forEach((e) => {
|
||||
const formatted = msgmodes[mode]
|
||||
.replace("{recipient}", recipient)
|
||||
.replace("{msg}", this.format(e));
|
||||
this.send(formatted);
|
||||
});
|
||||
}
|
||||
parse(data) {
|
||||
const [a, ...b] = data.split(/ +:/);
|
||||
const tmp = a.split(" ").concat(b);
|
||||
const prefix = data.charAt(0) === ":"
|
||||
? tmp.shift() ?? null
|
||||
: null;
|
||||
const command = tmp.shift();
|
||||
const params = command.toLowerCase() === "privmsg"
|
||||
? [tmp.shift(), tmp.join(" :")]
|
||||
: tmp;
|
||||
return { prefix, command, params };
|
||||
}
|
||||
format(msg) {
|
||||
return msg
|
||||
.replace(/\[b\](.*?)\[\/b\]/g, "\x02$1\x02")
|
||||
.replace(/\[i\](.*?)\[\/i\]/g, "\x1D$1\x1D")
|
||||
.replace(/\[color=(.*?)](.*?)\[\/color\]/g, replaceColor);
|
||||
}
|
||||
parsePrefix(prefix) {
|
||||
if (!prefix)
|
||||
return false;
|
||||
const parsed = /:?(.*)\!(.*)@(.*)/.exec(prefix);
|
||||
if (!parsed)
|
||||
return false;
|
||||
return {
|
||||
nick: parsed[1],
|
||||
username: parsed[2],
|
||||
hostname: parsed[3],
|
||||
};
|
||||
}
|
||||
reply(tmp) {
|
||||
return {
|
||||
type: "irc",
|
||||
network: this.options.network,
|
||||
channel: tmp.params[0],
|
||||
channelid: tmp.params[0],
|
||||
user: {
|
||||
...this.parsePrefix(tmp.prefix),
|
||||
account: (() => {
|
||||
const parsedPrefix = this.parsePrefix(tmp.prefix);
|
||||
return parsedPrefix && this.server.user.has(parsedPrefix.nick)
|
||||
? this.server.user.get(parsedPrefix.nick)?.account || false
|
||||
: false;
|
||||
})(),
|
||||
prefix: (tmp.prefix?.charAt(0) === ":"
|
||||
? tmp.prefix.substring(1)
|
||||
: tmp.prefix) + `@${this.options.network}`,
|
||||
},
|
||||
message: tmp.params[1].replace(/\u0002/g, ""),
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
raw: tmp,
|
||||
reply: (msg) => this.sendmsg("normal", tmp.params[0], msg),
|
||||
replyAction: (msg) => this.sendmsg("action", tmp.params[0], msg),
|
||||
replyNotice: (msg) => this.sendmsg("notice", tmp.params[0], msg),
|
||||
self: this.server,
|
||||
_chan: this.server.channel.get(tmp.params[0]),
|
||||
_user: this.server.user,
|
||||
_cmd: this._cmd,
|
||||
join: (chan) => this.join(chan),
|
||||
part: (chan, msg) => this.part(chan, msg),
|
||||
whois: (user) => this.whois(user),
|
||||
write: (msg) => this.send(msg)
|
||||
};
|
||||
}
|
||||
}
|
3
dist/clients/irc/cap.d.ts
vendored
Normal file
3
dist/clients/irc/cap.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
20
dist/clients/irc/cap.js
vendored
Normal file
20
dist/clients/irc/cap.js
vendored
Normal 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");
|
||||
});
|
||||
};
|
3
dist/clients/irc/invite.d.ts
vendored
Normal file
3
dist/clients/irc/invite.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
12
dist/clients/irc/invite.js
vendored
Normal file
12
dist/clients/irc/invite.js
vendored
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
3
dist/clients/irc/join.d.ts
vendored
Normal file
3
dist/clients/irc/join.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
5
dist/clients/irc/join.js
vendored
Normal file
5
dist/clients/irc/join.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
export default (bot) => {
|
||||
bot._cmd.set("JOIN", (msg) => {
|
||||
bot.send(`WHO ${msg.params[0]}`);
|
||||
});
|
||||
};
|
3
dist/clients/irc/motd.d.ts
vendored
Normal file
3
dist/clients/irc/motd.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
12
dist/clients/irc/motd.js
vendored
Normal file
12
dist/clients/irc/motd.js
vendored
Normal 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]);
|
||||
});
|
||||
};
|
3
dist/clients/irc/msg.d.ts
vendored
Normal file
3
dist/clients/irc/msg.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
13
dist/clients/irc/msg.js
vendored
Normal file
13
dist/clients/irc/msg.js
vendored
Normal 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]]);
|
||||
});
|
||||
};
|
3
dist/clients/irc/nick.d.ts
vendored
Normal file
3
dist/clients/irc/nick.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
8
dist/clients/irc/nick.js
vendored
Normal file
8
dist/clients/irc/nick.js
vendored
Normal 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);
|
||||
});
|
||||
};
|
3
dist/clients/irc/part.d.ts
vendored
Normal file
3
dist/clients/irc/part.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
5
dist/clients/irc/part.js
vendored
Normal file
5
dist/clients/irc/part.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
export default (bot) => {
|
||||
bot._cmd.set("PART", (msg) => {
|
||||
delete bot.server.user[msg.params[0]];
|
||||
});
|
||||
};
|
3
dist/clients/irc/ping.d.ts
vendored
Normal file
3
dist/clients/irc/ping.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
5
dist/clients/irc/ping.js
vendored
Normal file
5
dist/clients/irc/ping.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
export default (bot) => {
|
||||
bot._cmd.set("PING", (msg) => {
|
||||
bot.send(`PONG ${msg.params.join('')}`);
|
||||
});
|
||||
};
|
3
dist/clients/irc/pwdreq.d.ts
vendored
Normal file
3
dist/clients/irc/pwdreq.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
6
dist/clients/irc/pwdreq.js
vendored
Normal file
6
dist/clients/irc/pwdreq.js
vendored
Normal 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}`);
|
||||
});
|
||||
};
|
3
dist/clients/irc/welcome.d.ts
vendored
Normal file
3
dist/clients/irc/welcome.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
6
dist/clients/irc/welcome.js
vendored
Normal file
6
dist/clients/irc/welcome.js
vendored
Normal 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]]);
|
||||
});
|
||||
};
|
3
dist/clients/irc/who.d.ts
vendored
Normal file
3
dist/clients/irc/who.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
17
dist/clients/irc/who.js
vendored
Normal file
17
dist/clients/irc/who.js
vendored
Normal 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 = [];
|
||||
});
|
||||
};
|
3
dist/clients/irc/whois.d.ts
vendored
Normal file
3
dist/clients/irc/whois.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Bot } from "../../types";
|
||||
declare const _default: (bot: Bot) => void;
|
||||
export default _default;
|
62
dist/clients/irc/whois.js
vendored
Normal file
62
dist/clients/irc/whois.js
vendored
Normal 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);
|
||||
});
|
||||
};
|
44
dist/clients/slack.d.ts
vendored
Normal file
44
dist/clients/slack.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
import EventEmitter from "node:events";
|
||||
interface SlackOptions {
|
||||
token: string;
|
||||
set?: string;
|
||||
}
|
||||
interface User {
|
||||
account: string;
|
||||
nickname: string;
|
||||
}
|
||||
interface SlackMessage {
|
||||
type: string;
|
||||
channel: string;
|
||||
user: string;
|
||||
text: string;
|
||||
}
|
||||
interface SlackEvents {
|
||||
data: [string | [string, any]];
|
||||
error: [string];
|
||||
message: [SlackMessage];
|
||||
}
|
||||
export default class slack extends EventEmitter {
|
||||
private options;
|
||||
private token;
|
||||
private api;
|
||||
private interval;
|
||||
private server;
|
||||
private reconnectAttempts;
|
||||
emit<K extends keyof SlackEvents>(event: K, ...args: SlackEvents[K]): boolean;
|
||||
on<K extends keyof SlackEvents>(event: K, listener: (...args: SlackEvents[K]) => void): this;
|
||||
constructor(options: SlackOptions);
|
||||
connect(): Promise<void>;
|
||||
private initializeWebSocket;
|
||||
private handleWebSocketEvents;
|
||||
reconnect(): Promise<void>;
|
||||
getChannel(channelId: string): Promise<string | undefined>;
|
||||
getUser(userId: string): Promise<User | undefined>;
|
||||
send(channel: string, text: string | string[]): Promise<void>;
|
||||
ping(): Promise<void>;
|
||||
write(json: object): Promise<void>;
|
||||
reply(tmp: SlackMessage): any;
|
||||
private parseData;
|
||||
format(msg: string): string;
|
||||
}
|
||||
export {};
|
209
dist/clients/slack.js
vendored
Normal file
209
dist/clients/slack.js
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
import https from "node:https";
|
||||
import url from "node:url";
|
||||
import EventEmitter from "node:events";
|
||||
export default class slack extends EventEmitter {
|
||||
options;
|
||||
token;
|
||||
api = "https://slack.com/api";
|
||||
interval = null;
|
||||
server;
|
||||
reconnectAttempts = 0;
|
||||
emit(event, ...args) {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
on(event, listener) {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
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.reconnectAttempts = 0;
|
||||
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(), 3e4);
|
||||
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() {
|
||||
if (this.reconnectAttempts >= 5) {
|
||||
this.emit("data", ["error", "Too many reconnect attempts"]);
|
||||
return;
|
||||
}
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(async () => {
|
||||
this.emit("data", ["info", "Reconnecting to Slack"]);
|
||||
await this.connect();
|
||||
}, this.reconnectAttempts * 1e3);
|
||||
}
|
||||
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 {
|
||||
return JSON.parse(data.toString());
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
33
dist/clients/tg.d.ts
vendored
Normal file
33
dist/clients/tg.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
import EventEmitter from "events";
|
||||
interface TelegramOptions {
|
||||
token: string;
|
||||
pollrate?: number;
|
||||
set?: string;
|
||||
}
|
||||
interface TelegramEvents {
|
||||
data: [string | [string, any]];
|
||||
error: [string];
|
||||
}
|
||||
export default class tg extends EventEmitter {
|
||||
private options;
|
||||
private token;
|
||||
private api;
|
||||
private lastUpdate;
|
||||
private lastMessage;
|
||||
private poller;
|
||||
private server;
|
||||
emit<K extends keyof TelegramEvents>(event: K, ...args: TelegramEvents[K]): boolean;
|
||||
on<K extends keyof TelegramEvents>(event: K, listener: (...args: TelegramEvents[K]) => void): this;
|
||||
constructor(options: TelegramOptions);
|
||||
connect(): Promise<void>;
|
||||
getFile(fileId: string): Promise<string | false>;
|
||||
poll(): Promise<void>;
|
||||
private fetchUpdates;
|
||||
private processUpdate;
|
||||
private handleMessage;
|
||||
private handlePollError;
|
||||
reply(tmp: any, opt?: {}): any;
|
||||
send(chatId: number, msg: string, reply?: number | null, opt?: {}): Promise<any>;
|
||||
format(msg: string): string;
|
||||
}
|
||||
export {};
|
170
dist/clients/tg.js
vendored
Normal file
170
dist/clients/tg.js
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
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: {},
|
||||
};
|
||||
emit(event, ...args) {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
on(event, listener) {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
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: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
return await (await fetch(`${this.api}/sendMessage`, opts)).json();
|
||||
}
|
||||
format(msg) {
|
||||
return msg.toString()
|
||||
.split("&").join("&")
|
||||
.split("<").join("<")
|
||||
.split(">").join(">")
|
||||
.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>");
|
||||
}
|
||||
}
|
20
dist/index.d.ts
vendored
Normal file
20
dist/index.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
import EventEmitter from "node:events";
|
||||
interface Config {
|
||||
enabled: boolean;
|
||||
type: string;
|
||||
network: string;
|
||||
}
|
||||
interface CuffeoEvents {
|
||||
data: [string, any];
|
||||
error: [Error];
|
||||
}
|
||||
export default class Cuffeo extends EventEmitter {
|
||||
private clients;
|
||||
private libs;
|
||||
emit<K extends keyof CuffeoEvents>(event: K, ...args: CuffeoEvents[K]): boolean;
|
||||
on<K extends keyof CuffeoEvents>(event: K, listener: (...args: CuffeoEvents[K]) => void): this;
|
||||
constructor(cfg: Config[]);
|
||||
private loadLibs;
|
||||
private registerClients;
|
||||
}
|
||||
export {};
|
48
dist/index.js
vendored
Normal file
48
dist/index.js
vendored
Normal file
@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
19
package.json
19
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,13 @@
|
||||
],
|
||||
"author": "Flummi & jkhsjdhjs",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flumm-fetch": "^1.0.1"
|
||||
},
|
||||
"type": "module",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
;
|
||||
}
|
||||
};
|
@ -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
|
||||
;
|
||||
}
|
||||
}
|
283
src/clients/irc.ts
Normal file
283
src/clients/irc.ts
Normal file
@ -0,0 +1,283 @@
|
||||
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[];
|
||||
}
|
||||
|
||||
interface IRCEvents {
|
||||
data: [string | [string, any]];
|
||||
error: [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<MessageModes, string> = {
|
||||
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<IRCOptions>;
|
||||
private socket?: net.Socket | tls.TLSSocket;
|
||||
private _recachetime: number = 60 * 30; // 30 minutes
|
||||
private _cmd: Map<string, (cmd: ParsedCommand) => void> = new Map();
|
||||
private server: {
|
||||
set: string;
|
||||
motd: string;
|
||||
me: Record<string, unknown>;
|
||||
channel: Map<string, unknown>;
|
||||
user: Map<string, { account?: string; cached: number }>;
|
||||
};
|
||||
|
||||
emit<K extends keyof IRCEvents>(event: K, ...args: IRCEvents[K]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
on<K extends keyof IRCEvents>(event: K, listener: (...args: IRCEvents[K]) => void): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private createSocket(): net.Socket | tls.TLSSocket {
|
||||
return this.options.ssl
|
||||
? tls.connect({
|
||||
host: this.options.host,
|
||||
port: this.options.port,
|
||||
rejectUnauthorized: !this.options.selfSigned,
|
||||
})
|
||||
: net.connect({
|
||||
host: this.options.host,
|
||||
port: this.options.port,
|
||||
});
|
||||
}
|
||||
|
||||
connect(reconnect: boolean = false): void {
|
||||
if(reconnect)
|
||||
this.socket = undefined;
|
||||
|
||||
this.socket = this.createSocket();
|
||||
this.socket.on("data", (msg: string) => this.handleData(msg));
|
||||
this.socket.on("end", () => this.handleDisconnect());
|
||||
this.socket.on("error", (err: Error) => this.handleError(err));
|
||||
this.socket.setEncoding("utf-8");
|
||||
this.handleConnection();
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
};
|
26
src/clients/irc/cap.ts
Normal file
26
src/clients/irc/cap.ts
Normal file
@ -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");
|
||||
});
|
||||
};
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
15
src/clients/irc/invite.ts
Normal file
15
src/clients/irc/invite.ts
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
export default bot => {
|
||||
bot._cmd.set("JOIN", msg => { // join
|
||||
bot.send(`WHO ${msg.params[0]}`);
|
||||
});
|
||||
};
|
7
src/clients/irc/join.ts
Normal file
7
src/clients/irc/join.ts
Normal file
@ -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]}`);
|
||||
});
|
||||
};
|
@ -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]);
|
||||
});
|
||||
};
|
16
src/clients/irc/motd.ts
Normal file
16
src/clients/irc/motd.ts
Normal file
@ -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]);
|
||||
});
|
||||
};
|
@ -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]]);
|
||||
});
|
||||
};
|
@ -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
|
||||
});
|
||||
};
|
12
src/clients/irc/nick.ts
Normal file
12
src/clients/irc/nick.ts
Normal file
@ -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
|
||||
});
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
export default bot => {
|
||||
bot._cmd.set("PART", msg => { // part
|
||||
//delete this.server.user[msg.params[0]];
|
||||
});
|
||||
};
|
7
src/clients/irc/part.ts
Normal file
7
src/clients/irc/part.ts
Normal file
@ -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]];
|
||||
});
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
export default bot => {
|
||||
bot._cmd.set("PING", msg => { // ping
|
||||
bot.send(`PONG ${msg.params.join``}`);
|
||||
});
|
||||
};
|
7
src/clients/irc/ping.ts
Normal file
7
src/clients/irc/ping.ts
Normal file
@ -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('')}`);
|
||||
});
|
||||
};
|
@ -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}`);
|
||||
});
|
||||
};
|
8
src/clients/irc/pwdreq.ts
Normal file
8
src/clients/irc/pwdreq.ts
Normal file
@ -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}`);
|
||||
});
|
||||
};
|
@ -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]]);
|
||||
});
|
||||
};
|
8
src/clients/irc/welcome.ts
Normal file
8
src/clients/irc/welcome.ts
Normal file
@ -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]]);
|
||||
});
|
||||
};
|
@ -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 = [];
|
||||
});
|
||||
};
|
21
src/clients/irc/who.ts
Normal file
21
src/clients/irc/who.ts
Normal file
@ -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 = [];
|
||||
});
|
||||
};
|
@ -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);
|
||||
});
|
||||
};
|
69
src/clients/irc/whois.ts
Normal file
69
src/clients/irc/whois.ts
Normal file
@ -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<string, string>();
|
||||
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);
|
||||
});
|
||||
};
|
@ -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")
|
||||
;
|
||||
}
|
||||
|
||||
}
|
284
src/clients/slack.ts
Normal file
284
src/clients/slack.ts
Normal file
@ -0,0 +1,284 @@
|
||||
import https from "node:https";
|
||||
import net from "node:net";
|
||||
import url from "node:url";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
interface SlackOptions {
|
||||
token: string;
|
||||
set?: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
account: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
interface ServerInfo {
|
||||
set: string;
|
||||
channel: Map<string, string>;
|
||||
user: Map<string, User>;
|
||||
wss: {
|
||||
url: url.UrlWithStringQuery | null;
|
||||
socket: net.Socket | null;
|
||||
};
|
||||
me: Record<string, any>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface SlackEvents {
|
||||
data: [string | [string, any]];
|
||||
error: [string];
|
||||
message: [SlackMessage];
|
||||
}
|
||||
|
||||
export default class slack extends EventEmitter {
|
||||
private options: Required<SlackOptions>;
|
||||
private token: string;
|
||||
private api: string = "https://slack.com/api";
|
||||
private interval: NodeJS.Timeout | null = null;
|
||||
private server: ServerInfo;
|
||||
private reconnectAttempts = 0;
|
||||
|
||||
emit<K extends keyof SlackEvents>(event: K, ...args: SlackEvents[K]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
on<K extends keyof SlackEvents>(event: K, listener: (...args: SlackEvents[K]) => void): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
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<void> {
|
||||
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.reconnectAttempts = 0;
|
||||
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(), 3e4);
|
||||
|
||||
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<void> {
|
||||
if(this.reconnectAttempts >= 5) {
|
||||
this.emit("data", ["error", "Too many reconnect attempts"]);
|
||||
return;
|
||||
}
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(async () => {
|
||||
this.emit("data", ["info", "Reconnecting to Slack"]);
|
||||
await this.connect();
|
||||
}, this.reconnectAttempts * 1e3);
|
||||
}
|
||||
|
||||
async getChannel(channelId: string): Promise<string | undefined> {
|
||||
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<User | undefined> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await this.write({ type: "ping" });
|
||||
}
|
||||
|
||||
async write(json: object): Promise<void> {
|
||||
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 {
|
||||
return JSON.parse(data.toString()) as SlackMessage;
|
||||
}
|
||||
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")
|
||||
;
|
||||
}
|
||||
}
|
@ -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, "<b>$1</b>") // bold
|
||||
.replace(/\[i\](.*?)\[\/i\]/gsm, "<i>$1</i>") // italic
|
||||
.replace(/\[color=(.*?)](.*?)\[\/color\]/gsm, "$2")
|
||||
.replace(/\[pre\](.*?)\[\/pre\]/gsm, "<pre>$1</pre>")
|
||||
;
|
||||
}
|
||||
}
|
250
src/clients/tg.ts
Normal file
250
src/clients/tg.ts
Normal file
@ -0,0 +1,250 @@
|
||||
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<string, any>;
|
||||
user: Map<string, any>;
|
||||
me: Record<string, any>;
|
||||
}
|
||||
|
||||
interface TelegramResponse<T> {
|
||||
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;
|
||||
}
|
||||
|
||||
interface TelegramEvents {
|
||||
data: [string | [string, any]];
|
||||
error: [string];
|
||||
}
|
||||
|
||||
export default class tg extends EventEmitter {
|
||||
private options: Required<TelegramOptions>;
|
||||
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: {},
|
||||
};
|
||||
|
||||
emit<K extends keyof TelegramEvents>(event: K, ...args: TelegramEvents[K]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
on<K extends keyof TelegramEvents>(event: K, listener: (...args: TelegramEvents[K]) => void): this {
|
||||
return super.on(event, listener);
|
||||
}
|
||||
|
||||
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<void> {
|
||||
const res = await (await fetch(`${this.api}/getMe`)).json() as TelegramResponse<any>;
|
||||
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<string | false> {
|
||||
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<void> {
|
||||
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<TelegramUpdate[]> {
|
||||
const res = await (await fetch(`${this.api}/getUpdates?offset=${this.lastUpdate}&allowed_updates=message`)).json() as TelegramResponse<TelegramUpdate[]>;
|
||||
if(!res.ok)
|
||||
throw new Error(res.description || "Failed to fetch updates");
|
||||
return res.result || [];
|
||||
}
|
||||
|
||||
private async processUpdate(update: any): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<any> {
|
||||
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: JSON.stringify(body),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
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, "<b>$1</b>") // bold
|
||||
.replace(/\[i\](.*?)\[\/i\]/gsm, "<i>$1</i>") // italic
|
||||
.replace(/\[color=(.*?)](.*?)\[\/color\]/gsm, "$2")
|
||||
.replace(/\[pre\](.*?)\[\/pre\]/gsm, "<pre>$1</pre>")
|
||||
;
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
82
src/index.ts
Normal file
82
src/index.ts
Normal file
@ -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<string, any> = {};
|
||||
|
||||
emit<K extends keyof CuffeoEvents>(event: K, ...args: CuffeoEvents[K]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
on<K extends keyof CuffeoEvents>(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<Record<string, any>> {
|
||||
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<Client[]> {
|
||||
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;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
33
src/test.mjs
Normal file
33
src/test.mjs
Normal file
@ -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": "xd",
|
||||
"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);
|
||||
});
|
33
src/types.d.ts
vendored
Normal file
33
src/types.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
export interface Message {
|
||||
prefix: string;
|
||||
params: string[];
|
||||
}
|
||||
|
||||
export interface Bot {
|
||||
username: string;
|
||||
parsePrefix: (prefix: string) => User;
|
||||
_cmd: Map<string, (msg: Message) => void>;
|
||||
send: (message: string) => void;
|
||||
server: {
|
||||
channel: Map<string, string[]>;
|
||||
motd: string;
|
||||
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;
|
||||
}
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2024",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user