Bot Version 1.0.0

This commit is contained in:
2026-04-12 21:27:34 -07:00
parent f3203f3b3d
commit 2a3891936e
6 changed files with 604 additions and 0 deletions

206
app/bot.py Normal file
View File

@@ -0,0 +1,206 @@
#app.bot
import discord
import aiohttp
from datetime import datetime, timezone, timedelta
from discord import app_commands
from discord.ext import tasks
from .config import settings
from .db import init_db, insert_check, fetch_checks_since,fetch_month_checks
from .utils import (
check_site, summarize_counts, get_site_names,
render_bar, compute_uptime, format_detection_reason,
)
TOKEN = settings.discord_secret_key
# Convert Pydantic models → plain dicts once so the rest of bot.py is unchanged.
MONITORED_SITES = [site.to_dict() for site in settings.monitored_sites]
# SITE_CHOICES stays exactly the same — it reads from MONITORED_SITES:
SITE_CHOICES = [
app_commands.Choice(name=site["name"], value=site["name"])
for site in MONITORED_SITES
]
intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
session: aiohttp.ClientSession | None = None
@client.event
async def on_ready():
global session
init_db()
if session is None:
session = aiohttp.ClientSession(headers={"User-Agent": "VoteUptimeBot/1.0"})
if not poll_sites.is_running():
poll_sites.start()
await tree.sync()
print(f"Logged in as {client.user}")
@tasks.loop(minutes=15)
async def poll_sites():
now = datetime.now(timezone.utc).isoformat()
for site in MONITORED_SITES:
result = await check_site(session, site)
# Combine notes + detection_reason into the notes field so the DB
# schema doesn't need a migration. error_type already carries reason.
notes = result["notes"]
if result.get("detection_reason") and not notes:
notes = format_detection_reason(result["detection_reason"])
insert_check(
site_name=site["name"],
checked_at=now,
http_status=result["http_status"],
latency_ms=result["latency_ms"],
result=result["result"],
error_type=result["error_type"], # now carries detection_reason too
notes=notes,
)
@poll_sites.before_loop
async def before_poll_sites():
await client.wait_until_ready()
class SiteNameTransformer(app_commands.Transformer):
async def transform(self, interaction: discord.Interaction, value: str) -> str:
if value not in get_site_names(MONITORED_SITES):
raise app_commands.AppCommandError(f"Unknown site: {value}")
return value
uptime_group = app_commands.Group(name="uptime", description="Uptime tools")
tree.add_command(uptime_group)
@uptime_group.command(name="now", description="Show current configured sites")
async def uptime_now(interaction: discord.Interaction):
lines = []
for site in MONITORED_SITES:
rows = fetch_checks_since(
site["name"], datetime.now(timezone.utc) - timedelta(hours=1)
)
if not rows:
lines.append(f"**{site['name']}**: no recent data")
continue
checked_at, result, http_status, latency_ms, error_type = rows[-1]
# Emoji prefix for quick scanning
emoji = {"up": "🟩", "degraded": "🟨", "down": "🟥"}.get(result, "")
detail = f"{result.upper()} | status={http_status} | latency={latency_ms}ms"
if error_type:
detail += f" | reason={format_detection_reason(error_type)}"
lines.append(f"{emoji} **{site['name']}**: {detail}")
await interaction.response.send_message("\n".join(lines))
SITE_CHOICES = [
app_commands.Choice(name=site["name"], value=site["name"])
for site in MONITORED_SITES
]
@uptime_group.command(name="day", description="Last 24 hours in 15-minute bars")
@app_commands.describe(site="Site name")
@app_commands.choices(site=SITE_CHOICES)
async def uptime_day(
interaction: discord.Interaction,
site: app_commands.Transform[str, SiteNameTransformer],
):
since = datetime.now(timezone.utc) - timedelta(hours=24)
rows = fetch_checks_since(site, since)
results = [row[1] for row in rows]
bar = render_bar(results[-96:]) if results else ""
uptime = compute_uptime(results)
up, degraded, down = summarize_counts(results)
msg = (
f"**{site}** last 24h\n"
f"{bar}\n"
f"Uptime: **{uptime:.2f}%**\n"
f"Up: {up} | Degraded: {degraded} | Down: {down}"
)
await interaction.response.send_message(msg)
@uptime_group.command(name="month", description="Current month summary")
@app_commands.describe(site="Site name")
@app_commands.choices(site=SITE_CHOICES)
async def uptime_month(
interaction: discord.Interaction,
site: app_commands.Transform[str, SiteNameTransformer],
):
now = datetime.now(timezone.utc)
rows = fetch_month_checks(site, now.year, now.month)
by_day: dict[str, list[str]] = {}
for checked_at, result, *_ in rows:
day_key = checked_at[:10]
by_day.setdefault(day_key, []).append(result)
day_bars = []
for day in sorted(by_day.keys()):
pct = compute_uptime(by_day[day])
if pct >= 99:
day_bars.append("🟩")
elif pct >= 95:
day_bars.append("🟨")
else:
day_bars.append("🟥")
all_results = [row[1] for row in rows]
uptime = compute_uptime(all_results)
up, degraded, down = summarize_counts(all_results)
msg = (
f"**{site}** {now.year}-{now.month:02d}\n"
f"{''.join(day_bars) if day_bars else ''}\n"
f"Uptime: **{uptime:.2f}%**\n"
f"Up: {up} | Degraded: {degraded} | Down: {down}"
)
await interaction.response.send_message(msg)
@uptime_group.command(name="summarize", description="Summarize current month for all sites")
async def uptime_summarize(interaction: discord.Interaction):
now = datetime.now(timezone.utc)
lines = [f"**Monthly summary for {now.year}-{now.month:02d}**"]
for site in MONITORED_SITES:
rows = fetch_month_checks(site["name"], now.year, now.month)
results = [row[1] for row in rows]
uptime = compute_uptime(results)
up, degraded, down = summarize_counts(results)
lines.append(
f"{site['name']}: uptime={uptime:.2f}% | up={up} | degraded={degraded} | down={down}"
)
await interaction.response.send_message("\n".join(lines))
@tree.command(name="incident", description="Placeholder incident review command")
async def incident(interaction: discord.Interaction):
await interaction.response.send_message("Incident review command placeholder.")
@tree.command(name="hello", description="Say hello")
async def hello(interaction: discord.Interaction):
await interaction.response.send_message("Hello, world!")
@tree.command(name="add", description="Add two numbers")
async def add(interaction: discord.Interaction, a: float, b: float):
await interaction.response.send_message(f"Sum: {a + b}")
client.run(TOKEN)