forked from pinks/eris
1
0
Fork 0
nyx/bot/mod.ts

145 lines
4.7 KiB
TypeScript
Raw Normal View History

2023-09-11 20:43:12 +00:00
import { Grammy, GrammyAutoQuote, GrammyFiles, GrammyParseMode, Log } from "../deps.ts";
2023-09-13 09:50:22 +00:00
import { formatUserChat } from "../common/utils.ts";
2023-09-10 18:56:17 +00:00
import { session, SessionFlavor } from "./session.ts";
import { queueCommand } from "./queueCommand.ts";
2023-09-10 23:59:33 +00:00
import { txt2imgCommand, txt2imgQuestion } from "./txt2imgCommand.ts";
2023-09-11 20:43:12 +00:00
import { pnginfoCommand, pnginfoQuestion } from "./pnginfoCommand.ts";
2023-09-12 01:57:44 +00:00
import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts";
2023-09-10 18:56:17 +00:00
export const logger = () => Log.getLogger();
2023-09-11 17:07:46 +00:00
type WithRetryApi<T extends Grammy.RawApi> = {
[M in keyof T]: T[M] extends (args: infer P, ...rest: infer A) => infer R
? (args: P extends object ? P & { maxAttempts?: number } : P, ...rest: A) => R
: T[M];
};
2023-09-11 20:43:12 +00:00
export type Context =
& GrammyFiles.FileFlavor<GrammyParseMode.ParseModeFlavor<Grammy.Context>>
& SessionFlavor;
2023-09-11 17:07:46 +00:00
export const bot = new Grammy.Bot<Context, Grammy.Api<WithRetryApi<Grammy.RawApi>>>(
Deno.env.get("TG_BOT_TOKEN") ?? "",
);
2023-09-10 18:56:17 +00:00
bot.use(GrammyAutoQuote.autoQuote);
bot.use(GrammyParseMode.hydrateReply);
bot.use(session);
2023-09-11 20:43:12 +00:00
bot.api.config.use(GrammyFiles.hydrateFiles(bot.token));
2023-09-10 18:56:17 +00:00
2023-09-13 00:38:09 +00:00
// Automatically cancel requests after 30 seconds
bot.api.config.use(async (prev, method, payload, signal) => {
const controller = new AbortController();
let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
2023-09-14 22:40:12 +00:00
// TODO: this sometimes throws with "can't abort a locked stream" and crashes whole process
2023-09-13 00:38:09 +00:00
controller.abort();
}, 30 * 1000);
signal?.addEventListener("abort", () => {
controller.abort();
});
try {
2023-09-14 22:40:12 +00:00
const result = await prev(method, payload, controller.signal);
return result;
2023-09-13 00:38:09 +00:00
} finally {
clearTimeout(timeout);
if (timedOut) {
logger().warning(`${method} timed out`);
}
}
});
2023-09-10 18:56:17 +00:00
// Automatically retry bot requests if we get a "too many requests" or telegram internal error
bot.api.config.use(async (prev, method, payload, signal) => {
2023-09-11 17:07:46 +00:00
const maxAttempts = payload && ("maxAttempts" in payload) ? payload.maxAttempts ?? 3 : 3;
2023-09-10 18:56:17 +00:00
let attempt = 0;
while (true) {
attempt++;
const result = await prev(method, payload, signal);
if (
result.ok ||
![429, 500].includes(result.error_code) ||
2023-09-11 17:07:46 +00:00
attempt >= maxAttempts
2023-09-10 18:56:17 +00:00
) {
return result;
}
2023-09-11 17:07:46 +00:00
logger().warning(
2023-09-11 20:48:38 +00:00
`${method} (attempt ${attempt}) failed: ${result.error_code} ${result.description}`,
2023-09-11 17:07:46 +00:00
);
2023-09-10 18:56:17 +00:00
const retryAfterMs = (result.parameters?.retry_after ?? (attempt * 5)) * 1000;
await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
}
});
2023-09-11 20:43:12 +00:00
bot.catch((err) => {
2023-09-11 20:48:38 +00:00
logger().error(
`Handling update from ${formatUserChat(err.ctx)} failed: ${err.name} ${err.message}`,
);
2023-09-11 20:43:12 +00:00
});
2023-09-10 18:56:17 +00:00
// if error happened, try to reply to the user with the error
bot.use(async (ctx, next) => {
try {
await next();
} catch (err) {
try {
await ctx.reply(`Handling update failed: ${err}`, {
reply_to_message_id: ctx.message?.message_id,
});
} catch {
throw err;
}
}
});
bot.api.setMyShortDescription("I can generate furry images from text");
bot.api.setMyDescription(
"I can generate furry images from text. " +
"Send /txt2img to generate an image.",
);
bot.api.setMyCommands([
2023-09-12 01:57:44 +00:00
{ command: "txt2img", description: "Generate an image from text" },
{ command: "img2img", description: "Generate an image based on another image" },
2023-09-11 20:43:12 +00:00
{ command: "pnginfo", description: "Show generation parameters of an image" },
2023-09-10 18:56:17 +00:00
{ command: "queue", description: "Show the current queue" },
]);
bot.command("start", (ctx) => ctx.reply("Hello! Use the /txt2img command to generate an image"));
bot.command("txt2img", txt2imgCommand);
2023-09-11 16:03:07 +00:00
bot.use(txt2imgQuestion.middleware());
2023-09-12 01:57:44 +00:00
bot.command("img2img", img2imgCommand);
bot.use(img2imgQuestion.middleware());
2023-09-10 18:56:17 +00:00
2023-09-11 20:43:12 +00:00
bot.command("pnginfo", pnginfoCommand);
bot.use(pnginfoQuestion.middleware());
2023-09-10 18:56:17 +00:00
bot.command("queue", queueCommand);
bot.command("pause", (ctx) => {
if (!ctx.from?.username) return;
const config = ctx.session.global;
if (!config.adminUsernames.includes(ctx.from.username)) return;
if (config.pausedReason != null) {
return ctx.reply(`Already paused: ${config.pausedReason}`);
}
config.pausedReason = ctx.match ?? "No reason given";
logger().warning(`Bot paused by ${ctx.from.first_name} because ${config.pausedReason}`);
return ctx.reply("Paused");
});
bot.command("resume", (ctx) => {
if (!ctx.from?.username) return;
const config = ctx.session.global;
if (!config.adminUsernames.includes(ctx.from.username)) return;
if (config.pausedReason == null) return ctx.reply("Already running");
config.pausedReason = null;
logger().info(`Bot resumed by ${ctx.from.first_name}`);
return ctx.reply("Resumed");
});
bot.command("crash", () => {
throw new Error("Crash command used");
});