diff --git a/README.md b/README.md index 96e5ef4..1d67d8d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,12 @@ -# Eris the Bot - -[![Website](https://img.shields.io/website?url=https%3A%2F%2Feris.lisq.eu%2F)](https://eris.lisq.eu/) -![Unique users](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.userCount&label=unique%20users) -![Generated images](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.imageCount&label=images%20generated) -![Processed steps](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.stepCount&label=steps%20processed) -![Painted pixels](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.pixelCount&label=pixels%20painted) +# Nyx the Bot +Fork of Eris the Bot https://eris.lisq.eu Telegram bot for generating images from text. ## Requirements -- [Deno](https://deno.land/) -- [Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui/) +- [Deno](https://deno.land/) (for the bot server) +- [Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui/) (for the worker that generates images) ## Options @@ -20,17 +15,23 @@ You can put these in `.env` file or pass them as environment variables. - `TG_BOT_TOKEN` - Telegram bot token. Get yours from [@BotFather](https://t.me/BotFather). Required. - `DENO_KV_PATH` - [Deno KV](https://deno.land/api?s=Deno.openKv&unstable) database file path. A - temporary file is used by default. + temporary file is used by default. Example: /opt/data/botdata.kv - `LOG_LEVEL` - [Log level](https://deno.land/std@0.201.0/log/mod.ts?s=LogLevels). Default: `INFO`. ## Running 1. Start Eris: `deno task start` -2. Visit [Eris WebUI](http://localhost:5999/) and login via Telegram. +2. Visit [Eris WebUI](http://localhost:8443/) and login via Telegram. 3. Promote yourself to admin in the Eris WebUI. 4. Start Stable Diffusion WebUI: `./webui.sh --api` (in SD WebUI directory) 5. Add a new worker in the Eris WebUI. +## This fork requires the use of webhooks. + +1. You need a reverse Proxy and HTTPS certificate set up that proxies all requests from a domain on port 443 (e.g. nyx.akiru.de) to the backend of this bot (:8443). +2. Change the Webhook URL to your own one in /mod/bot.ts - There are also console log statements you can uncomment for troubleshooting. +3. Make sure the DNS and firewall are set up for the webhook to be reachable, because otherwise the bot fails to start. + ## Codegen The Stable Diffusion API types are auto-generated. To regenerate them, first start your SD WebUI @@ -38,8 +39,8 @@ with `--nowebui --api`, and then run `deno task generate` ## Project structure -- `/api` - Eris API served at `http://localhost:5999/api/`. +- `/api` - Eris API served at `http://localhost:8443/api/`. - `/app` - Queue handling and other core processes. - `/bot` - Handling bot commands and other updates from Telegram API. -- `/ui` - Eris WebUI frontend files served at `http://localhost:5999/`. +- `/ui` - Eris WebUI frontend files served at `http://localhost:8443/`. - `/util` - Utility functions shared by other parts. diff --git a/api/mod.ts b/api/mod.ts index d59b472..c20a30a 100644 --- a/api/mod.ts +++ b/api/mod.ts @@ -2,11 +2,15 @@ import { route } from "reroute"; import { serveSpa } from "serve_spa"; import { api } from "./serveApi.ts"; import { fromFileUrl } from "std/path/mod.ts"; +// New function that handles the webhook +import { handleWebhook } from "../bot/mod.ts"; + export async function serveUi() { - const server = Deno.serve({ port: 5999 }, (request) => + const server = Deno.serve({ port: 8443 }, (request) => route(request, { "/api/*": (request) => api.fetch(request), + "/webhook": handleWebhook, // Create the webhook route handle "/*": (request) => serveSpa(request, { fsRoot: fromFileUrl(new URL("../ui/", import.meta.url)), diff --git a/bot/mod.ts b/bot/mod.ts index 6f7fdda..27b2e8d 100644 --- a/bot/mod.ts +++ b/bot/mod.ts @@ -1,7 +1,7 @@ import { Api, Bot, Context, RawApi, session, SessionFlavor } from "grammy"; import { FileFlavor, hydrateFiles } from "grammy_files"; import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode"; -import { run, sequentialize } from "grammy_runner"; +import { sequentialize } from "grammy_runner"; import { error, info, warning } from "std/log/mod.ts"; import { sessions } from "../api/sessionsRoute.ts"; import { formatUserChat } from "../utils/formatUserChat.ts"; @@ -12,6 +12,16 @@ import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts"; import { pnginfoCommand, pnginfoQuestion } from "./pnginfoCommand.ts"; import { queueCommand } from "./queueCommand.ts"; import { txt2imgCommand, txt2imgQuestion } from "./txt2imgCommand.ts"; +import { setConfig, getConfig } from "../app/config.ts"; + +// Set the new configuration +await setConfig({ maxUserJobs: 1, maxJobs: 500 }); + +// Fetch the updated configuration +const updatedConfig = await getConfig(); + +// Log the updated configuration to the console +console.log("Updated Configuration:", updatedConfig); interface SessionData { chat: ErisChatData; @@ -107,18 +117,38 @@ bot.use(async (ctx, next) => { } }); -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([ - { command: "txt2img", description: "Generate image from text" }, - { command: "img2img", description: "Generate image from image" }, - { command: "pnginfo", description: "Try to extract prompt from raw file" }, - { command: "queue", description: "Show the current queue" }, - { command: "cancel", description: "Cancel all your requests" }, -]); +// Wrap the calls in try-catch for error handling +async function setupBotCommands() { + try { + await bot.api.setMyShortDescription("Generate furry images in '704x704', '576x832', '832x576'. https://ko-fi.com/nyxthebot https://nyx.akiru.de/"); + } catch (err) { + error(`Failed to set short description: ${err.message}`); + } + + try { + await bot.api.setMyDescription( + "I can generate furry images from text. If you want a different size, use 'Size: 576x832' for example." + + "Send /txt2img to generate an image.", + ); + } catch (err) { + error(`Failed to set description: ${err.message}`); + } + + try { + await bot.api.setMyCommands([ + { command: "txt2img", description: "Generate image from text" }, + { command: "img2img", description: "Generate image from image" }, + { command: "pnginfo", description: "Try to extract prompt from raw file" }, + { command: "queue", description: "Show the current queue" }, + { command: "cancel", description: "Cancel all your requests" }, + ]); + } catch (err) { + error(`Failed to set commands: ${err.message}`); + } +} + +// Call the setup function +setupBotCommands(); bot.command("start", async (ctx) => { if (ctx.match) { @@ -173,7 +203,26 @@ bot.command("crash", () => { throw new Error("Crash command used"); }); -export async function runBot() { - const runner = run(bot, { runner: { silent: true } }); - await runner.task(); +// Set up the webhook in the telegram API and initialize the bot +await bot.api.setWebhook('https://nyx.akiru.de/webhook'); +await bot.init(); + +// Function to handle incoming webhook requests +export async function handleWebhook(req: Request): Promise { + try { + const body = await req.json(); + // console.log("Received webhook data:", JSON.stringify(body, null, 2)); + + // Log before processing update + // console.log("Processing update through handleUpdate..."); + await bot.handleUpdate(body); // Process the update + // Log after processing update + // console.log("Update processed successfully."); + + return new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' } }); + } catch (error) { + // Detailed error logging + console.error("Error in handleWebhook:", error); + return new Response("Error", { status: 500 }); + } } diff --git a/main.ts b/main.ts index 43a8d72..99ab22b 100644 --- a/main.ts +++ b/main.ts @@ -4,7 +4,9 @@ import { ConsoleHandler } from "std/log/handlers.ts"; import { LevelName, setup } from "std/log/mod.ts"; import { serveUi } from "./api/mod.ts"; import { runAllTasks } from "./app/mod.ts"; -import { runBot } from "./bot/mod.ts"; +// runbot shouldn't be needed for webhooks? +// import { runBot } from "./bot/mod.ts"; +import "./bot/mod.ts"; const logLevel = Deno.env.get("LOG_LEVEL")?.toUpperCase() as LevelName ?? "INFO"; @@ -20,7 +22,8 @@ setup({ // run parts of the app await Promise.all([ - runBot(), +// runbot shouldn't be needed for webhooks? +// runBot(), runAllTasks(), serveUi(), ]); diff --git a/monitoring/Readme.md b/monitoring/Readme.md new file mode 100644 index 0000000..0d28450 --- /dev/null +++ b/monitoring/Readme.md @@ -0,0 +1 @@ +These scripts exist to automatically restart the bot in case of errors and connectivity issues. They should either be run as systemd services or cronjobs. \ No newline at end of file diff --git a/monitoring/monitoring.sh b/monitoring/monitoring.sh new file mode 100644 index 0000000..84abdbc --- /dev/null +++ b/monitoring/monitoring.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# This script has to be run as systemd service. It checks journalctl output for errors and restarts the bot automatically when they occur too often. + +# Define the regex pattern to search for occurrences of "GrammyError" and "sendMediaGroup" +ERROR_PATTERN="GrammyError.*sendMediaGroup" + +# Initialize the counter for consecutive occurrences +error_count=0 + +# Monitor the journalctl output +journalctl -xe -f | while read line; do + # Check if the line contains the pattern + if echo "$line" | grep -qE "$ERROR_PATTERN"; then + # Increment the error counter + ((error_count++)) + + # Check if the error has occurred 4 times in a row + if [ $error_count -eq 4 ]; then + # Restart the bot service + systemctl restart nyxthebot + # Reset the error counter + error_count=0 + fi + else + # Reset the error counter if the line does not contain the error + error_count=0 + fi +done diff --git a/monitoring/sleepy.sh b/monitoring/sleepy.sh new file mode 100644 index 0000000..2bf0128 --- /dev/null +++ b/monitoring/sleepy.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# When the bot gets eepy sleepy, this script will restart it. It happens, when no jobs come in for multiple minutes. +# The script interfaces with the REST API of the bot and checks if the image count has changed. +# Run this script as cronjob all 4-5 minutes. + +# Define the URL and the stats file path +URL="https://YOUR_URL/api/stats" +STATS_FILE="YOUR_FILE_PATH/stats.txt" + +# Fetch the data and extract imageCount +imageCount=$(curl -s "$URL" | jq '.imageCount') + +# Check if stats file exists and read the last value +if [ -f "$STATS_FILE" ]; then + lastValue=$(tail -n 1 "$STATS_FILE") +else + lastValue="" +fi + +# Save the new value to the file +echo "$imageCount" >> "$STATS_FILE" + +# Keep only the last 2 values in the file +tail -n 2 "$STATS_FILE" > "$STATS_FILE.tmp" && mv "$STATS_FILE.tmp" "$STATS_FILE" + +# Compare the two values and restart the service if they are the same +if [ "$lastValue" == "$imageCount" ]; then + systemctl restart nyxthebot +fi diff --git a/ui/StatsPage.tsx b/ui/StatsPage.tsx index 8c0cabe..c0c3201 100644 --- a/ui/StatsPage.tsx +++ b/ui/StatsPage.tsx @@ -7,7 +7,7 @@ export function StatsPage() { const getGlobalStats = useSWR( ["/stats", {}] as const, (args) => fetchApi(...args).then(handleResponse), - { refreshInterval: 2_000 }, + { refreshInterval: 1_00 }, ); return ( @@ -18,13 +18,13 @@ export function StatsPage() { className="font-bold text-zinc-700 dark:text-zinc-300" value={getGlobalStats.data?.pixelStepCount ?? 0} digits={15} - transitionDurationMs={3_000} + transitionDurationMs={1_00} />

@@ -34,13 +34,13 @@ export function StatsPage() { className="font-bold text-zinc-700 dark:text-zinc-300" value={getGlobalStats.data?.pixelCount ?? 0} digits={15} - transitionDurationMs={3_000} + transitionDurationMs={1_00} />

@@ -51,14 +51,14 @@ export function StatsPage() { className="font-bold text-zinc-700 dark:text-zinc-300" value={getGlobalStats.data?.stepCount ?? 0} digits={9} - transitionDurationMs={3_000} + transitionDurationMs={1_00} />

@@ -68,14 +68,14 @@ export function StatsPage() { className="font-bold text-zinc-700 dark:text-zinc-300" value={getGlobalStats.data?.imageCount ?? 0} digits={9} - transitionDurationMs={3_000} + transitionDurationMs={1_00} />

@@ -86,7 +86,7 @@ export function StatsPage() { className="font-bold text-zinc-700 dark:text-zinc-300" value={getGlobalStats.data?.userCount ?? 0} digits={6} - transitionDurationMs={1_500} + transitionDurationMs={1_00} />