Keep bot online via Discord gateway

This commit is contained in:
MiTHRAL 2026-05-13 22:37:55 -04:00
parent bd282b4dc3
commit e6fbd670ac
6 changed files with 84 additions and 0 deletions

View file

@ -1,5 +1,6 @@
DISCORD_BOT_TOKEN=replace-with-your-discord-bot-token
DISCORD_CHANNEL_ID=1504278732070981683
DISCORD_GATEWAY_ENABLED=true
ARCHIVE_STATUS_CONFIG=services.json
ARCHIVE_STATUS_STATE=state/status-message.json
CHECK_INTERVAL_SECONDS=60

View file

@ -1,5 +1,6 @@
DISCORD_BOT_TOKEN=replace-with-your-discord-bot-token
DISCORD_CHANNEL_ID=1504278732070981683
DISCORD_GATEWAY_ENABLED=true
ARCHIVE_STATUS_CONFIG=services.json
ARCHIVE_STATUS_STATE=state/status-message.json
CHECK_INTERVAL_SECONDS=60

View file

@ -2,10 +2,12 @@ FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt /app/requirements.txt
COPY --chown=1001:1001 status_bot.py /app/status_bot.py
COPY --chown=1001:1001 dashboard.html /app/dashboard.html
COPY --chown=1001:1001 services.example.json /app/services.json
RUN pip install --no-cache-dir -r /app/requirements.txt
RUN adduser -D -u 1001 -g "" -h /app archive-status
CMD ["python", "/app/status_bot.py"]

View file

@ -39,6 +39,7 @@ cp services.example.json services.json
Edit `.env` and set:
- `DISCORD_BOT_TOKEN`
- `DISCORD_GATEWAY_ENABLED=true`
- `DASHBOARD_USERNAME`
- `DASHBOARD_PASSWORD_HASH`
@ -59,6 +60,8 @@ Dashboard auth includes:
- CSRF token required for write actions
- basic failed-login throttling
The bot keeps a Gateway connection open so Discord shows it as online even when no status refresh is happening.
Edit `services.json` with the URLs you want displayed. Keep private/internal URLs out of Git if they should not be shared.
Preview the Discord embed payload:

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
discord.py>=2.4,<3

View file

@ -4,6 +4,7 @@
from __future__ import annotations
import base64
import asyncio
import hashlib
import hmac
import json
@ -160,6 +161,75 @@ class BotRuntime:
self.last_checked_at: datetime | None = None
class DiscordGatewayManager:
def __init__(self, token: str) -> None:
self.token = token
self.thread: threading.Thread | None = None
self.loop: asyncio.AbstractEventLoop | None = None
self.client: Any = None
self.ready = threading.Event()
self._disconnecting = threading.Event()
def start(self) -> None:
if self.thread is not None:
return
self.thread = threading.Thread(target=self._run, name="discord-gateway", daemon=True)
self.thread.start()
def stop(self) -> None:
self._disconnecting.set()
if self.loop is not None and self.client is not None:
asyncio.run_coroutine_threadsafe(self.client.close(), self.loop)
if self.thread is not None:
self.thread.join(timeout=15)
def _run(self) -> None:
try:
import discord
except ImportError:
print("discord.py is not installed. Install requirements.txt to keep the bot online.", file=sys.stderr, flush=True)
self.ready.set()
return
class GatewayClient(discord.Client):
def __init__(self, manager: DiscordGatewayManager) -> None:
intents = discord.Intents.default()
intents.guilds = True
super().__init__(intents=intents)
self.manager = manager
async def on_ready(self) -> None:
await self.change_presence(status=discord.Status.online)
user = self.user
name = user.name if user is not None else "unknown"
bot_id = user.id if user is not None else "unknown"
print(f"Discord gateway connected as {name} ({bot_id})", flush=True)
self.manager.ready.set()
async def on_disconnect(self) -> None:
self.manager.ready.clear()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.client = GatewayClient(self)
try:
self.loop.run_until_complete(self.client.start(self.token))
except Exception as exc:
if not self._disconnecting.is_set():
print(f"Discord gateway stopped: {exc}", file=sys.stderr, flush=True)
finally:
self.ready.set()
try:
pending = asyncio.all_tasks(self.loop)
for task in pending:
task.cancel()
if pending:
self.loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
finally:
self.loop.close()
def env(name: str, default: str | None = None) -> str:
value = os.getenv(name, default)
if value is None or not value.strip():
@ -894,6 +964,10 @@ def main() -> int:
state_path = Path(env("ARCHIVE_STATUS_STATE", "state/status-message.json"))
interval = int(env("CHECK_INTERVAL_SECONDS", str(DEFAULT_INTERVAL_SECONDS)))
runtime = BotRuntime(token, channel_id, config_path, state_path, dry_run=bool_env("DISCORD_DRY_RUN", False))
gateway = None
if bool_env("DISCORD_GATEWAY_ENABLED", True) and not runtime.dry_run:
gateway = DiscordGatewayManager(token)
gateway.start()
dashboard = maybe_start_dashboard(runtime)
if runtime.dry_run:
@ -933,6 +1007,8 @@ def main() -> int:
if dashboard is not None:
dashboard.shutdown()
if gateway is not None:
gateway.stop()
return 0