425 lines
13 KiB
TypeScript
425 lines
13 KiB
TypeScript
import { ButtonStyle, ChannelType, Client, ClientPresenceStatusData, CommandInteraction, GatewayIntentBits, InteractionReplyOptions, Message, Partials, Presence } from "discord.js";
|
|
import { readdirSync } from "fs";
|
|
import { CClient, CUser, HelpServer, Komand, KomandNaExport, KomandRaw, ListenerFunkce, Modul, SMessage, SRecord, SuperListenerFunkce } from "./utils/types";
|
|
import { adminLog, areStatusesSame, formatCas, log, nabidni, oddiakritikovat, prefix, rand, messageReply } from "./utils/utils.js";
|
|
import levenshtein from "js-levenshtein";
|
|
import { emouty } from "./utils/emotes";
|
|
import { lidiCoMajDenimPremium, setClient } from "./utils/denim-Spravce";
|
|
|
|
const ints = GatewayIntentBits;
|
|
const client = new Client({
|
|
partials: [Partials.Channel],
|
|
intents: [ints.Guilds, ints.GuildVoiceStates, ints.GuildPresences, ints.GuildMessages, ints.DirectMessages, ints.MessageContent]
|
|
}) as CClient;
|
|
const DJ_PREFIX = "dj:";
|
|
const modulFolder = `${__dirname}/modules/`;
|
|
const eventy: SRecord<ListenerFunkce<any>[]> = {};
|
|
const superEventy: SRecord<SuperListenerFunkce<any>[]> = {};
|
|
const kuldan_log: SRecord<SRecord<number>> = {};
|
|
const komandyNaPoslani: KomandNaExport[] = [];
|
|
const helpServer: HelpServer = require("./utils/helpServer");
|
|
|
|
client.on("error", err => {
|
|
log("errorek", err);
|
|
adminLog(client, "nejaka chyba na klijentovi", err);
|
|
});
|
|
|
|
client.on("shardError", err => {
|
|
log(err);
|
|
adminLog(client, "šárd ejrr??", err);
|
|
});
|
|
|
|
process.on("unhandledRejection", (reason, promise) => {
|
|
let message: string;
|
|
|
|
if (reason instanceof Error) {
|
|
message = `${reason.name}: ${reason.message}\n${reason.stack}`;
|
|
} else {
|
|
message = String(reason);
|
|
}
|
|
|
|
if (message.includes("Connect Timeout Error") && message.includes("10000ms")) {
|
|
// Klasika
|
|
log(new Error("Discord timeout..."));
|
|
return;
|
|
}
|
|
|
|
log(new Error("Unhandled Rejection in app:"), reason, promise);
|
|
|
|
adminLog(client, "Unhandled Rejection", message);
|
|
});
|
|
|
|
process.on("uncaughtException", (err) => {
|
|
log(new Error("Uncaught Exception in app:"), err);
|
|
|
|
let message: string | Error;
|
|
if (err instanceof Error) {
|
|
message = err;
|
|
} else {
|
|
message = String(err);
|
|
}
|
|
|
|
adminLog(client, "Uncaught Exception", message);
|
|
});
|
|
|
|
client.komandy = {};
|
|
client.aliasy = {};
|
|
setClient(client);
|
|
|
|
function getFirstArg(fn: KomandRaw["run"]) {
|
|
if (typeof fn != "function") return;
|
|
return fn.toString()
|
|
.match(/(?:function\s.*?)?\(([^)]*)\)|\w+ =>/)![1]
|
|
?.split(",")[1]
|
|
?.trim();
|
|
}
|
|
|
|
const runEvent = async (name: string, args: unknown[]) => {
|
|
for (const listener of superEventy[name] || []) {
|
|
if (!listener) continue; // [after] pozice v superEventy arrayi se dají nastavovat a teoreticky může nastat mezera
|
|
try {
|
|
if (await listener(...args)) return true; // if listener returns true, it means, we shouldn't continue with execustion
|
|
} catch (e) {
|
|
if (process.env.dieOnError) throw e;
|
|
log("error pri spusteni super listeneru", e as Error);
|
|
adminLog(client, "error pri supereventu", e as Error);
|
|
}
|
|
}
|
|
|
|
eventy[name]?.forEach(listener => {
|
|
try {
|
|
listener(...args);
|
|
} catch (e) {
|
|
if (process.env.dieOnError) throw e;
|
|
log("error pri spousteni eventu", e as Error);
|
|
adminLog(client, "error pri eventu", e as Error);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Loading of modules
|
|
readdirSync(modulFolder).forEach(soubor => {
|
|
if (!soubor.endsWith(".js")) return;
|
|
const modul: Modul = require(`${modulFolder}${soubor}`);
|
|
log(`Loaded: ${soubor.slice(0, -3)}`);
|
|
|
|
modul.client = client;
|
|
for (const klic in modul) {
|
|
const prefix = /^(?<s>super_)?on_(?<h>.+)/.exec(klic);
|
|
if (prefix) {
|
|
const groups = prefix.groups!;
|
|
const ev = groups.s ? superEventy : eventy;
|
|
if (!ev[groups.h]) {
|
|
ev[groups.h] = [];
|
|
if (!["messageCreate", "userPresenceUpdate"].includes(groups.h)) client.on(groups.h, (...args) => void runEvent(groups.h, args));
|
|
}
|
|
|
|
// Bohužel typescript je tupá píča a musíme mu vysvětlit, že to vlastně jde
|
|
const n = modul[klic as Exclude<keyof Modul, "client" | "more_komandy">]!;
|
|
if (typeof n == "object") {
|
|
const prev = ev[groups.h][n.pos];
|
|
ev[groups.h][n.pos] = n.fun;
|
|
if (prev) ev[groups.h].push(prev);
|
|
}
|
|
else ev[groups.h].push(n);
|
|
} else if (klic == "more_komandy") {
|
|
const n = modul[klic]!;
|
|
Object.keys(n).forEach(cmdName => {
|
|
const value = n[cmdName];
|
|
const toCoExportuju: KomandNaExport = { name: cmdName };
|
|
let hide = false;
|
|
if (typeof value !== "object") {
|
|
client.komandy[cmdName] = { run: value };
|
|
toCoExportuju.arg = getFirstArg(value);
|
|
} else {
|
|
client.komandy[cmdName] = { run: value.run, slashRun: value.slashRun, cd: value.cd, DMUnsafe: value.DMUnsafe, premium: value.premium };
|
|
Object.assign(toCoExportuju, { als: value.als, arg: value.arg ?? getFirstArg(value.run) });
|
|
hide = !!value.hidden;
|
|
value.als?.forEach(al => client.aliasy[al] = cmdName);
|
|
}
|
|
if (!hide) komandyNaPoslani.push(toCoExportuju);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
helpServer.komandy = komandyNaPoslani;
|
|
|
|
const maKuldan = (id: string, komand: string, kuldan_komandu: number) => {
|
|
if (!kuldan_log[komand]) kuldan_log[komand] = {};
|
|
|
|
const cas_ted = Date.now() / 1000;
|
|
const rozdil = cas_ted - kuldan_log[komand][id];
|
|
if (rozdil < kuldan_komandu) return kuldan_komandu - rozdil;
|
|
|
|
kuldan_log[komand][id] = cas_ted;
|
|
return 0;
|
|
};
|
|
|
|
function assume(cond: boolean, msg: string) {
|
|
if (!cond) throw msg;
|
|
}
|
|
|
|
function prase(expression: string, i: number, terminators: string[], args: string[]): [string, number, string?] {
|
|
let ret = "";
|
|
let escaped = false;
|
|
|
|
while (i < expression.length) {
|
|
const char = expression[i];
|
|
i++;
|
|
|
|
if (escaped) {
|
|
escaped = false;
|
|
ret += char;
|
|
continue;
|
|
}
|
|
|
|
switch (char) {
|
|
case "~": // eskejp
|
|
escaped = true;
|
|
break;
|
|
|
|
case "[": { // RNG vyber
|
|
let option: string, term: string | undefined;
|
|
const options: string[] = [];
|
|
|
|
do {
|
|
[option, i, term] = prase(expression, i, ["|", "]"], args);
|
|
options.push(option);
|
|
} while (term == "|");
|
|
|
|
if (options.length > 0)
|
|
ret += options[rand(options.length)];
|
|
|
|
break;
|
|
}
|
|
|
|
case "<": { // RNG cislo
|
|
let min: string, max: string;
|
|
[min, i] = prase(expression, i, [","], args);
|
|
[max, i] = prase(expression, i, [">"], args);
|
|
|
|
const imin = parseInt(min);
|
|
const imax = parseInt(max);
|
|
|
|
assume(!Number.isNaN(min), "RNG cislo: Min neni integer");
|
|
assume(!Number.isNaN(max), "RNG cislo: Max neni integer");
|
|
|
|
ret += rand(imax - imin + 1) + imin;
|
|
|
|
break;
|
|
}
|
|
|
|
case "{": {// ARG
|
|
let argId: string;
|
|
[argId, i] = prase(expression, i, ["}"], args);
|
|
|
|
if (argId === "*") { // join args
|
|
ret += args.join(" ");
|
|
} else if (argId === "#") { // number of args
|
|
ret += args.length;
|
|
} else { // arg id #
|
|
const numArgId = parseInt(argId);
|
|
assume(!Number.isNaN(numArgId) && numArgId > 0 && numArgId <= args.length, "ARG: Neplatnej index argumentu");
|
|
ret += args[numArgId - 1];
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
for (const terminator of terminators) {
|
|
if (terminator == char)
|
|
return [ret, i, char];
|
|
}
|
|
|
|
ret += char;
|
|
break;
|
|
}
|
|
}
|
|
|
|
assume(terminators.length == 0, "Neocekavanej konec vejrazu");
|
|
|
|
return [ret, expression.length - 1];
|
|
}
|
|
|
|
function renderMessage(expression: string, args: string[]) {
|
|
if (!expression.toLowerCase().startsWith(DJ_PREFIX))
|
|
return expression;
|
|
|
|
expression = expression.substring(DJ_PREFIX.length);
|
|
|
|
try {
|
|
return prase(expression, 0, [], args)[0];
|
|
} catch (ex) {
|
|
return `Chyba: ${ex}`;
|
|
}
|
|
}
|
|
|
|
function handle(e: unknown, mesOrInt: SMessage | CommandInteraction) {
|
|
if (process.env.dieOnError) throw e;
|
|
log("error pri spousteni akce", e as Error);
|
|
const admin = process.env.adminID;
|
|
|
|
const txt = `pri spousteni thohoto komandu nastala chyba ${admin ? `<@${admin}> uz?` : ""}`;
|
|
if (mesOrInt instanceof Message) return void messageReply(mesOrInt, txt);
|
|
mesOrInt.reply(txt);
|
|
}
|
|
|
|
async function runKomand(mes: SMessage, cmd: Komand, cmdName: string, arg: string): Promise<unknown>;
|
|
async function runKomand(interaction: CommandInteraction, cmd: Komand, cmdName: string): Promise<unknown>;
|
|
async function runKomand(mesOrInt: SMessage | CommandInteraction, cmd: Komand, cmdName: string, arg?: string) {
|
|
const jeMes = mesOrInt instanceof Message;
|
|
|
|
if (jeMes && cmd.slashRun) {
|
|
const nazev = `${prefix}${cmdName}`;
|
|
const id = client.slashCommandy[nazev];
|
|
return void messageReply(mesOrInt, `tuto ejenom sleh komand </${nazev}:${id}>`);
|
|
}
|
|
|
|
if (cmd.cd && jeMes) {
|
|
const zbyva = Math.round(maKuldan(mesOrInt.author.id, cmdName, cmd.cd));
|
|
if (zbyva) return messageReply(mesOrInt, `si kkt vole maz kuldan jeste ${formatCas(zbyva)}`);
|
|
}
|
|
|
|
if (cmd.premium && !lidiCoMajDenimPremium.includes(jeMes ? mesOrInt.author.id : mesOrInt.member?.user.id || "")) {
|
|
return messageReply(mesOrInt as SMessage, "sorka bracho tuto je koamnd jenom pro curaki co platy");
|
|
}
|
|
|
|
const akce = cmd.run ?? cmd.slashRun!;
|
|
|
|
if (typeof akce == "string") {
|
|
const res = renderMessage(akce, arg?.split(" ") || []);
|
|
if (jeMes) return messageReply(mesOrInt, res);
|
|
return mesOrInt.reply(res);
|
|
}
|
|
|
|
try {
|
|
// Je to absolutní fekálie, ale alespoň to je kompilovatelný narozdíl od předchozí verze (commit 08fde76 a předchozí)
|
|
const result = await akce(mesOrInt as never, arg || "");
|
|
if (!result) return;
|
|
|
|
if (jeMes) return messageReply(mesOrInt, result)
|
|
.catch(e => handle(e, mesOrInt));
|
|
// další feklo
|
|
mesOrInt.reply(result as InteractionReplyOptions);
|
|
} catch (e) {
|
|
handle(e, mesOrInt);
|
|
}
|
|
}
|
|
|
|
client.on("messageCreate", async mes => {
|
|
if (process.env.ignoreMess) return;
|
|
|
|
const [mesPrefix, komandSDiakritikou, ...args] = mes.content.split(/\s/);
|
|
|
|
const prefixDiff = levenshtein(oddiakritikovat(mesPrefix.toLowerCase()), prefix);
|
|
if (prefixDiff > 0) {
|
|
if (!await runEvent("messageCreate", [mes]) && prefixDiff <= 1 && mes.author.id != client.user!.id) messageReply(mes, `${prefix}*`);
|
|
return;
|
|
}
|
|
|
|
if (!komandSDiakritikou) {
|
|
if (!await runEvent("messageCreate", [mes])) messageReply(mes, "coe voe");
|
|
return;
|
|
}
|
|
|
|
const komandBez = oddiakritikovat(komandSDiakritikou).toLowerCase();
|
|
let cmdName = komandBez;
|
|
|
|
const aliasCelej = client.aliasy[komandBez];
|
|
if (aliasCelej) {
|
|
const [aliasCmdName, ...aliasArgs] = aliasCelej.split(" ");
|
|
cmdName = aliasCmdName;
|
|
args.unshift(...aliasArgs);
|
|
}
|
|
|
|
const celArgs = args.join(" ");
|
|
|
|
if (await runEvent("messageCreate", [mes, cmdName])) return;
|
|
|
|
const komand = client.komandy[cmdName];
|
|
if (mes.channel.type == ChannelType.DM && komand?.DMUnsafe) return void messageReply(mes, "tuten komand bohuzel v sz nefunkuje");
|
|
|
|
if (komand) return void runKomand(mes, komand, cmdName, celArgs);
|
|
|
|
// neměl jsi na mysli?
|
|
const slova: string[] = [];
|
|
[...Object.keys(client.komandy), ...Object.keys(client.aliasy)].forEach(cmd => {
|
|
const distance = levenshtein(cmd, cmdName);
|
|
if (distance <= Math.ceil(cmdName.length * 0.3)) slova.push(cmd);
|
|
});
|
|
if (!slova.length) return void messageReply(mes, "co to znamena ti gadzovko");
|
|
|
|
const nabidka = slova.sort().slice(0, 5);
|
|
|
|
nabidni({
|
|
nabidka,
|
|
channel: mes.channel,
|
|
zpravec: { content: "nemnel sy na misli:" },
|
|
onCollect: (i, lookupId, radek) => {
|
|
if (i.member?.user.id != mes.author.id) return;
|
|
const komand = nabidka[lookupId];
|
|
const cmdName = client.aliasy[komand] ?? komand;
|
|
const cmd = client.komandy[cmdName];
|
|
radek.components.forEach((btn, i) => btn.setDisabled() && (i == lookupId && btn.setStyle(ButtonStyle.Success)));
|
|
i.update({ content: `ok vole ${emouty.d3k}`, components: [radek] });
|
|
|
|
if (!cmd) return void messageReply(mes, "bohuzel negdo sy ze mi dela prel");
|
|
|
|
if (mes.channel.type == ChannelType.DM && cmd?.DMUnsafe) return void messageReply(mes, "tuten komand bohuzel v sz nefunkuje");
|
|
|
|
runKomand(mes, cmd, cmdName, celArgs);
|
|
},
|
|
konecnaZprava: "pozde"
|
|
});
|
|
});
|
|
|
|
function alertPokudNestandardni(client: CClient, aft: Presence) {
|
|
const presence = aft.clientStatus;
|
|
|
|
for (const pres in presence) {
|
|
if (["web", "mobile", "desktop"].includes(pres)) {
|
|
continue;
|
|
}
|
|
|
|
// Máme jinej
|
|
adminLog(client, `zahlidnul jsem zvlastni status "${pres}".\n\`\`\`${JSON.stringify(presence)}\`\`\`\nod uzivatele ${aft.userId} ${aft.user}`);
|
|
}
|
|
}
|
|
|
|
// Simulation of userPresenceUpdate event
|
|
client.on("presenceUpdate", async (bef, aft) => {
|
|
const user = aft.user as CUser | null;
|
|
if (!user) return;
|
|
if (!user.presence) user.presence = {};
|
|
|
|
if (areStatusesSame(aft.clientStatus, user.presence)) return;
|
|
|
|
alertPokudNestandardni(client, aft);
|
|
|
|
const naCo = aft.clientStatus || {};
|
|
user.presence = naCo;
|
|
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
if (!areStatusesSame(naCo, user.presence)) {
|
|
return;
|
|
}
|
|
|
|
runEvent("userPresenceUpdate", [bef, aft]);
|
|
});
|
|
|
|
client.on("interactionCreate", async int => {
|
|
if (process.env.ignoreMess || !int.isChatInputCommand()) return;
|
|
|
|
const cmdName = int.commandName.slice(prefix.length);
|
|
|
|
const command = client.komandy[cmdName];
|
|
if (!command) return void int.reply(`komand nenalezen ${emouty.lukiw}`);
|
|
|
|
runKomand(int, command, cmdName);
|
|
});
|
|
|
|
client.login(process.env.token);
|