Compare commits

..
2 Commits
Author SHA1 Message Date
esefandClaude Opus 5 93e22a8364 statchannel: ignore steam's bogus zero player counts
Steam's GetNumberOfCurrentPlayers occasionally answers 200 with
player_count: 0 for a valid appid. Both Strinova stat channels were
renamed to "0" on the same 09:48 tick on 2026-10-20 (the only zero in
~300 renames over 3 days), and the name sat wrong until the next
10-minute tick.

Skip the rename on a non-positive count so the channel keeps its last
good value, and log it so the next one is visible. !strinova had the
same bug and would reply "0 players online right now".

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-20 13:12:57 +03:00
esefandClaude Opus 5.5 d514601651 firebase_website: drop the sysinfo sync to Firestore
The website bot status tab was replaced by the VEGA status page, so
nothing reads sysinfo/server anymore. get_db() stays for calendar and
bucketlist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 15:23:41 +03:00
5 changed files with 9 additions and 189 deletions

No files matched your search

+3
View File
@@ -31,6 +31,9 @@ class StatChannel(commands.Cog):
async with session.get('https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=1282270') as r: async with session.get('https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=1282270') as r:
data = await r.json() data = await r.json()
count = data['response']['player_count'] count = data['response']['player_count']
if not isinstance(count, int) or count <= 0:
print(f'stat channel update skipped (channel {channel_id}): steam returned {count!r}')
return
channel = self.bot.get_channel(channel_id) channel = self.bot.get_channel(channel_id)
if channel: if channel:
await channel.edit(name=f'Strinova Players From Steam: {count:,}') await channel.edit(name=f'Strinova Players From Steam: {count:,}')
+3
View File
@@ -117,6 +117,9 @@ class SysInfo(commands.Cog):
async with session.get('https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=1282270') as r: async with session.get('https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=1282270') as r:
data = await r.json() data = await r.json()
count = data['response']['player_count'] count = data['response']['player_count']
if not isinstance(count, int) or count <= 0:
await ctx.reply('🎮 Strinova — Steam is not returning a player count right now, try again in a bit')
return
await ctx.reply(f'🎮 Strinova — {count:,} players online right now') await ctx.reply(f'🎮 Strinova — {count:,} players online right now')
+3 -20
View File
@@ -1,35 +1,18 @@
"""firebase_website — Bot → Firestore sync using the Admin SDK. """firebase_website — Bot → Firestore access using the Admin SDK.
Requires GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service-account JSON. Requires GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service-account JSON.
This package is loaded as a Discord extension purely for lifecycle hooks Loaded as a Discord extension only so missing credentials fail at startup.
(setup / teardown). It does not register a Cog — it starts a background The calendar and bucketlist cogs use get_db().
SysInfoSync task instead.
Usage from main.py: Usage from main.py:
await bot.load_extension("firebase_website") await bot.load_extension("firebase_website")
""" """
###################################################################### ######################################################################
import logging
from discord.ext import commands from discord.ext import commands
from .config import get_db # noqa: re‑export for calendar, bucketlist, etc. from .config import get_db # noqa: re‑export for calendar, bucketlist, etc.
from .sysinfo_sync import SysInfoSync
log = logging.getLogger(__name__)
_sync: SysInfoSync | None = None
async def setup(bot: commands.Bot) -> None: async def setup(bot: commands.Bot) -> None:
global _sync
get_db() # fail fast if credentials are missing get_db() # fail fast if credentials are missing
_sync = SysInfoSync()
_sync.start()
async def teardown(bot: commands.Bot) -> None:
global _sync
if _sync is not None:
await _sync.stop()
-1
View File
@@ -8,7 +8,6 @@ from google.cloud.firestore import AsyncClient
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
PROJECT_ID = "esef-514bf" PROJECT_ID = "esef-514bf"
SYSINFO_INTERVAL = 10 # seconds between pushes
_db: AsyncClient | None = None _db: AsyncClient | None = None
-168
View File
@@ -1,168 +0,0 @@
"""Push system stats (CPU, RAM, disk, uptime) to Firestore via Admin SDK."""
######################################################################
import asyncio
import logging
import re
import time
import psutil
from google.cloud import firestore
from .config import get_db, SYSINFO_INTERVAL
log = logging.getLogger(__name__)
FASTFETCH_CACHE_SECS = 300 # refresh fastfetch every 5 minutes
VIRTUAL_NIC_PREFIXES = ("lo", "docker", "br-", "veth", "tailscale", "tun", "wg", "virbr")
def _fmt_size(b: int) -> str:
"""Bytes → human-readable string (auto-scales B → TB)."""
for unit in ("B", "KB", "MB", "GB", "TB"):
if abs(b) < 1024:
return f"{b:.1f} {unit}"
b /= 1024
return f"{b:.1f} PB"
def _fmt_duration(seconds: int) -> str:
days, r = divmod(seconds, 86400)
hours, r = divmod(r, 3600)
minutes, _ = divmod(r, 60)
return f"{days}d {hours}h {minutes}m" if days else f"{hours}h {minutes}m"
class SysInfoSync:
"""Background task: collect system stats and push to Firestore."""
def __init__(self) -> None:
self._task: asyncio.Task | None = None
self._fastfetch: str = ""
self._fastfetch_ts: float = 0
# ── public ──────────────────────────────────────────────────────────
def start(self) -> None:
if self._task is not None:
return
self._task = asyncio.create_task(self._run())
log.info("SysInfo → Firestore sync started (every %ds)", SYSINFO_INTERVAL)
async def stop(self) -> None:
if self._task is None:
return
self._task.cancel()
self._task = None
log.info("SysInfo → Firestore sync stopped")
# turns the dot on the website red
try:
await get_db().collection("sysinfo").document("server").set(
{"online": False, "updatedAt": firestore.SERVER_TIMESTAMP}, merge=True)
except Exception:
log.warning("Could not mark sysinfo offline", exc_info=True)
# ── loop ────────────────────────────────────────────────────────────
async def _run(self) -> None:
while True:
try:
await self._push()
except asyncio.CancelledError:
return
except Exception:
log.warning("SysInfo push failed", exc_info=True)
await asyncio.sleep(SYSINFO_INTERVAL)
# ── push ────────────────────────────────────────────────────────────
async def _push(self) -> None:
stats = await self._collect_stats()
fetch = await self._get_fastfetch()
payload = {
"online": True,
"cpu": stats["cpu"],
"ram": stats["ram"],
"ramUsed": stats["ram_used"],
"ramTotal": stats["ram_total"],
"disk": stats["disk"],
"diskUsed": stats["disk_used"],
"diskTotal": stats["disk_total"],
"swap": stats["swap"],
"swapUsed": stats["swap_used"],
"swapTotal": stats["swap_total"],
"uptime": stats["uptime"],
"botUptime": stats["bot_uptime"],
"processes": stats["processes"],
"load1": stats["load1"],
"load5": stats["load5"],
"load15": stats["load15"],
"netSent": stats["net_sent"],
"netRecv": stats["net_recv"],
"fetch": fetch,
"updatedAt": firestore.SERVER_TIMESTAMP,
}
db = get_db()
await db.collection("sysinfo").document("server").set(payload, merge=True)
# ── collectors ──────────────────────────────────────────────────────
async def _collect_stats(self) -> dict:
"""Gather all psutil stats; offloads blocking calls to a thread."""
cpu = await asyncio.to_thread(psutil.cpu_percent, interval=1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
swap = psutil.swap_memory()
load1, load5, load15 = psutil.getloadavg()
nics = psutil.net_io_counters(pernic=True)
physical = [c for name, c in nics.items() if not name.startswith(VIRTUAL_NIC_PREFIXES)]
net_sent = sum(c.bytes_sent for c in physical)
net_recv = sum(c.bytes_recv for c in physical)
now = time.time()
uptime_seconds = int(now - psutil.boot_time())
bot_uptime_seconds = int(now - psutil.Process().create_time())
return {
"cpu": round(cpu, 1),
"ram": round(mem.percent, 1),
"ram_used": _fmt_size(mem.used),
"ram_total": _fmt_size(mem.total),
"disk": round(disk.percent, 1),
"disk_used": _fmt_size(disk.used),
"disk_total": _fmt_size(disk.total),
"swap": round(swap.percent, 1),
"swap_used": _fmt_size(swap.used),
"swap_total": _fmt_size(swap.total),
"uptime": _fmt_duration(uptime_seconds),
"bot_uptime": _fmt_duration(bot_uptime_seconds),
"processes": len(psutil.pids()),
"load1": round(load1, 1),
"load5": round(load5, 1),
"load15": round(load15, 1),
"net_sent": _fmt_size(net_sent),
"net_recv": _fmt_size(net_recv),
}
async def _get_fastfetch(self) -> str:
"""Return cached fastfetch output; refreshed every 5 minutes."""
now = time.monotonic()
if self._fastfetch and (now - self._fastfetch_ts) < FASTFETCH_CACHE_SECS:
return self._fastfetch
try:
proc = await asyncio.create_subprocess_exec(
"fastfetch", "--pipe", "--logo", "none",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
clean = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", stdout.decode())
lines = [line for line in clean.strip().splitlines() if line.strip()]
self._fastfetch = "\n".join(lines[:15])
self._fastfetch_ts = now
except Exception:
if not self._fastfetch:
self._fastfetch = ""
return self._fastfetch