Initial sanitary RepoRadar release
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
main.py — RepoRadar entrypoint.
|
||||
|
||||
A standalone Discord bot that watches a self-hosted/hosted git server (Gitea,
|
||||
Forgejo, GitLab or GitHub) and drops the *newest* projects into Discord with
|
||||
descriptions, community voting and admin-curated highlights.
|
||||
|
||||
Nextcord 2.6 boot pattern (learned the hard way):
|
||||
* Construct the Bot *inside* the running asyncio loop (avoids dual-loop
|
||||
"heartbeat blocked" deadlocks).
|
||||
* `setup_hook` is NOT called with `bot.start()` in 2.6 — load extensions
|
||||
manually in our own `setup()` before `bot.start()`.
|
||||
* `add_cog()` attaches commands to the cog but does NOT push them into the
|
||||
bot's rollout set — iterate each cog's `application_commands` and call
|
||||
`add_application_command(cmd, use_rollout=True)` with the guild id.
|
||||
* Bot is NOT an async context manager — use try/finally + `await bot.close()`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import signal
|
||||
|
||||
import nextcord
|
||||
from nextcord.ext import commands
|
||||
|
||||
from config import settings
|
||||
from utils.git_api import GitClient
|
||||
from utils.helpers import Store
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, settings.log_level, logging.INFO),
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
)
|
||||
log = logging.getLogger("reporadar")
|
||||
|
||||
EXTENSIONS = ("cogs.gitscan",)
|
||||
|
||||
|
||||
class RepoRadar(commands.Bot):
|
||||
"""The RepoRadar bot — owns the git client and the SQLite store."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
intents = nextcord.Intents.default()
|
||||
super().__init__(intents=intents)
|
||||
self.git = GitClient()
|
||||
self.store = Store(settings.db_path)
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Initialise resources and load + register cogs (called before start)."""
|
||||
settings.ensure_dirs()
|
||||
await self.store.init()
|
||||
await self.git.start()
|
||||
|
||||
for ext in EXTENSIONS:
|
||||
self.load_extension(ext)
|
||||
log.info("Loaded extension %s", ext)
|
||||
|
||||
# Push each cog's slash commands into the rollout set (Nextcord 2.6 quirk).
|
||||
registered = 0
|
||||
for cog in self.cogs.values():
|
||||
for cmd in cog.application_commands:
|
||||
if settings.guild_id:
|
||||
cmd.guild_ids_to_rollout.add(settings.guild_id)
|
||||
self.add_application_command(cmd, use_rollout=True)
|
||||
registered += 1
|
||||
log.info("Registered %d application command(s)", registered)
|
||||
|
||||
async def on_ready(self) -> None:
|
||||
log.info("Logged in as %s (id=%s)", self.user, self.user.id if self.user else "?")
|
||||
await self.change_presence(
|
||||
activity=nextcord.Activity(
|
||||
type=nextcord.ActivityType.watching,
|
||||
name="git servers for fresh drops ☣",
|
||||
)
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
log.info("Shutting down — closing git session…")
|
||||
await self.git.close()
|
||||
await super().close()
|
||||
|
||||
|
||||
async def runner() -> None:
|
||||
if not settings.discord_token:
|
||||
raise SystemExit("DISCORD_TOKEN is not set. Configure it in .env before starting.")
|
||||
|
||||
# Construct the bot INSIDE the running loop (Nextcord 2.6 requirement).
|
||||
bot = RepoRadar()
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
stop = asyncio.Event()
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
try:
|
||||
loop.add_signal_handler(sig, stop.set)
|
||||
except NotImplementedError:
|
||||
# Windows event loop doesn't support signal handlers — ignore.
|
||||
pass
|
||||
|
||||
await bot.setup()
|
||||
|
||||
async def _shutdown_watcher() -> None:
|
||||
await stop.wait()
|
||||
log.info("Shutdown signal received")
|
||||
if not bot.is_closed():
|
||||
await bot.close()
|
||||
|
||||
watcher = asyncio.create_task(_shutdown_watcher())
|
||||
try:
|
||||
await bot.start(settings.discord_token)
|
||||
finally:
|
||||
stop.set()
|
||||
watcher.cancel()
|
||||
if not bot.is_closed():
|
||||
await bot.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(runner())
|
||||
except (KeyboardInterrupt, SystemExit) as exc:
|
||||
log.info("RepoRadar stopped: %s", exc)
|
||||
Reference in New Issue
Block a user