From f16d848e42979fa31a0bf666d4a349831d4921ba Mon Sep 17 00:00:00 2001 From: esefxdz Date: Mon, 13 Jul 2026 11:01:42 +0300 Subject: [PATCH] security audit --- .gitignore | 2 + cogs/calendar.py | 93 ++++++++++++++---------------- credentials_test.env | 3 + firebase_service_account_test.json | 1 + firebase_website/__init__.py | 36 ++++++++++-- firebase_website/config.py | 11 ---- firebase_website/sysinfo_sync.py | 85 +++++++++++---------------- 7 files changed, 115 insertions(+), 116 deletions(-) create mode 100644 firebase_service_account_test.json diff --git a/.gitignore b/.gitignore index f40baa5..763f57c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ __pycache__/ ssh_update.py ssh_*.py blue_archive/ba_students.json +firebase-service-account.json +firebase_service_account.json diff --git a/cogs/calendar.py b/cogs/calendar.py index 99d5f99..0288bdb 100644 --- a/cogs/calendar.py +++ b/cogs/calendar.py @@ -1,39 +1,36 @@ -"""Calendar cog — modal-based booking, country-aware timezones.""" +"""Calendar cog — modal-based booking, Firestore Admin SDK.""" ###################################################################### import logging import re from datetime import datetime, timezone, timedelta -import aiohttp import discord from discord.ext import commands +from google.cloud.firestore import AsyncClient +from firebase_website import get_db from .calendar_data import COUNTRY_TZ log = logging.getLogger(__name__) -FIRESTORE_CALENDAR = ( - "https://firestore.googleapis.com/v1/projects/esef-514bf" - "/databases/(default)/documents/calendar_events" -) - DATE_RE = re.compile(r"^\d{2}-\d{2}-\d{4}$") TIME_RE = re.compile(r"^\d{2}:\d{2}$") +COLLECTION = "calendar_events" +TR_TZ = timezone(timedelta(hours=3)) + + def _find_offset(country: str) -> float | None: """Look up a country's UTC offset. Case-insensitive fuzzy match.""" c = country.strip().lower() if not c: return None - # Exact match for name, offset in COUNTRY_TZ.items(): if c == name.lower(): return offset - # Starts with for name, offset in COUNTRY_TZ.items(): if name.lower().startswith(c): return offset - # Substring anywhere for name, offset in COUNTRY_TZ.items(): if c in name.lower(): return offset @@ -98,8 +95,7 @@ class BookModal(discord.ui.Modal, title="Book an Event"): offset = _find_offset(country) if offset is None: return await interaction.response.send_message( - f"Unknown country. Try: Turkey, UK, US East, Japan, etc.\n" - f"Type a country name from the list.", + "Unknown country. Try: Turkey, UK, US East, Japan, etc.", ephemeral=True, ) @@ -110,29 +106,29 @@ class BookModal(discord.ui.Modal, title="Book an Event"): ).replace(tzinfo=tz) utc_ts = local_dt.astimezone(timezone.utc).isoformat() - payload = { - "fields": { - "title": {"stringValue": title}, - "date": {"stringValue": f"{year}-{month}-{day}"}, - "time": {"stringValue": time_str}, - "country": {"stringValue": country}, - "utc": {"stringValue": utc_ts}, - "description": {"stringValue": description}, - "createdBy": {"stringValue": str(interaction.user)}, - } + event_data = { + "title": title, + "date": f"{year}-{month}-{day}", + "time": time_str, + "country": country, + "utc": utc_ts, + "description": description, + "createdBy": str(interaction.user), } - async with aiohttp.ClientSession() as session: - async with session.post(FIRESTORE_CALENDAR, json=payload) as resp: - if resp.status in (200, 201): - desc = f" — {description}" if description else "" - await interaction.response.send_message( - f"Booked **{date} at {time_str} ({country})**: **{title}**{desc}" - ) - else: - await interaction.response.send_message( - "Failed to save. Try again.", ephemeral=True - ) + try: + db = get_db() + await db.collection(COLLECTION).add(event_data) + except Exception: + log.exception("Failed to add calendar event") + return await interaction.response.send_message( + "Failed to save. Try again.", ephemeral=True + ) + + desc = f" — {description}" if description else "" + await interaction.response.send_message( + f"Booked **{date} at {time_str} ({country})**: **{title}**{desc}" + ) class BookButton(discord.ui.View): @@ -164,23 +160,22 @@ class Calendar(commands.Cog): day, month, year = date.split("-") iso_date = f"{year}-{month}-{day}" - deleted = False - async with aiohttp.ClientSession() as session: - async with session.get(FIRESTORE_CALENDAR) as resp: - if resp.status != 200: - return await ctx.reply("Could not fetch events.") - data = await resp.json() + try: + db = get_db() + docs = ( + await db.collection(COLLECTION) + .where("date", "==", iso_date) + .where("title", "==", title.strip()) + .get() + ) + except Exception: + log.exception("Failed to query calendar events") + return await ctx.reply("Could not fetch events.") - for doc in data.get("documents", []): - fields = doc.get("fields", {}) - d = fields.get("date", {}).get("stringValue", "") - t = fields.get("title", {}).get("stringValue", "") - if d == iso_date and t.lower() == title.strip().lower(): - doc_name = doc["name"].split("/")[-1] - async with session.delete(f"{FIRESTORE_CALENDAR}/{doc_name}") as dresp: - if dresp.status == 200: - deleted = True - break + deleted = False + for doc in docs: + await doc.reference.delete() + deleted = True if deleted: await ctx.reply(f"Removed **{title}** from {date}.") diff --git a/credentials_test.env b/credentials_test.env index 3981a82..07490b9 100644 --- a/credentials_test.env +++ b/credentials_test.env @@ -22,6 +22,9 @@ OWNER_ID="your discord servers owner id here" DEEPSEEK_API=lol you thought +# firebase service account (for Admin SDK) +GOOGLE_APPLICATION_CREDENTIALS="/home/esef/discordbot/firebase-service-account.json" + #mis ids member speicific stuff MELLO="" FEROX="" diff --git a/firebase_service_account_test.json b/firebase_service_account_test.json new file mode 100644 index 0000000..665892d --- /dev/null +++ b/firebase_service_account_test.json @@ -0,0 +1 @@ +this is what u get from google lol \ No newline at end of file diff --git a/firebase_website/__init__.py b/firebase_website/__init__.py index e7dae7c..14a0c0d 100644 --- a/firebase_website/__init__.py +++ b/firebase_website/__init__.py @@ -1,29 +1,53 @@ -"""firebase_website — Bot → Firestore sync for the esefos.netlify.app dashboard. +"""firebase_website — Bot → Firestore sync using the Admin SDK. -Add integrations here as they grow: - sysinfo_sync — CPU, RAM, disk, uptime pushed every 10 s - (future) — gacha stats, now-playing, radio listeners, … +Requires GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service-account JSON. Usage from main.py: await bot.load_extension("firebase_website") + +Provides: + get_db() → google.cloud.firestore.AsyncClient (shared, lazy-init) """ ###################################################################### +import logging +import os + from discord.ext import commands +from google.cloud.firestore import AsyncClient from .sysinfo_sync import SysInfoSync +log = logging.getLogger(__name__) + +_db: AsyncClient | None = None _sync: SysInfoSync | None = None +def get_db() -> AsyncClient: + """Return the shared Async Firestore client, creating it on first call.""" + global _db + if _db is None: + creds_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS") + if creds_path is None: + raise RuntimeError( + "GOOGLE_APPLICATION_CREDENTIALS environment variable is not set. " + "Point it to your service-account JSON file." + ) + _db = AsyncClient() + log.info("Firestore Admin SDK initialised (project %s)", _db.project) + return _db + + async def setup(bot: commands.Bot) -> None: - """Called by bot.load_extension(). Starts the sysinfo sync loop.""" + """Start the sysinfo sync loop.""" global _sync + get_db() # early init — fails fast if credentials are missing _sync = SysInfoSync() _sync.start() async def teardown(bot: commands.Bot) -> None: - """Called on bot unload / shutdown. Stops all sync tasks.""" + """Stop the sysinfo sync loop.""" global _sync if _sync is not None: _sync.stop() diff --git a/firebase_website/config.py b/firebase_website/config.py index ec43244..9a6e8a6 100644 --- a/firebase_website/config.py +++ b/firebase_website/config.py @@ -1,18 +1,7 @@ """Firebase project config — shared by all website integrations.""" ###################################################################### -# Your Firebase project (same one the website uses in comments.js) PROJECT_ID = "esef-514bf" -API_KEY = "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0" - -# Firestore REST API base URL -FIRESTORE_BASE = ( - f"https://firestore.googleapis.com/v1/projects/{PROJECT_ID}" - f"/databases/(default)/documents" -) - -# Document paths -DOC_SYSINFO = f"{FIRESTORE_BASE}/sysinfo/server" # How often system stats are pushed (seconds) SYSINFO_INTERVAL = 10 diff --git a/firebase_website/sysinfo_sync.py b/firebase_website/sysinfo_sync.py index a14a002..bc6be5e 100644 --- a/firebase_website/sysinfo_sync.py +++ b/firebase_website/sysinfo_sync.py @@ -1,14 +1,14 @@ -"""Push system stats (CPU, RAM, disk, uptime) to Firestore on a loop.""" +"""Push system stats (CPU, RAM, disk, uptime) to Firestore via Admin SDK.""" ###################################################################### import asyncio import logging import re import time -import aiohttp import psutil -from .config import DOC_SYSINFO, SYSINFO_INTERVAL +from . import get_db +from .config import SYSINFO_INTERVAL log = logging.getLogger(__name__) @@ -27,19 +27,16 @@ class SysInfoSync: def __init__(self) -> None: self._task: asyncio.Task | None = None - self._tick = 0 # ── public API ────────────────────────────────────────────────────────── def start(self) -> None: - """Launch the background sync loop.""" if self._task is not None: return self._task = asyncio.create_task(self._run()) log.info("SysInfo → Firestore sync started (every %ds)", SYSINFO_INTERVAL) def stop(self) -> None: - """Cancel the background sync loop.""" if self._task is None: return self._task.cancel() @@ -49,69 +46,57 @@ class SysInfoSync: # ── internals ─────────────────────────────────────────────────────────── async def _run(self) -> None: - """Main loop — collect stats, push, sleep, repeat.""" - async with aiohttp.ClientSession() as session: - while True: - try: - await self._push(session) - except asyncio.CancelledError: - return - except Exception: - log.debug("SysInfo Firestore push failed", exc_info=True) - await asyncio.sleep(SYSINFO_INTERVAL) + while True: + try: + await self._push() + except asyncio.CancelledError: + return + except Exception: + log.debug("SysInfo Firestore push failed", exc_info=True) + await asyncio.sleep(SYSINFO_INTERVAL) - async def _push(self, session: aiohttp.ClientSession) -> None: - """Collect current stats and PATCH the Firestore document.""" + async def _push(self) -> None: cpu = psutil.cpu_percent(interval=1) mem = psutil.virtual_memory() disk = psutil.disk_usage("/") uptime_seconds = int(time.time() - psutil.boot_time()) - # uptime → "3d 12h 5m" or "5h 23m" days, r = divmod(uptime_seconds, 86400) hours, r = divmod(r, 3600) minutes, _ = divmod(r, 60) - if days > 0: - uptime_str = f"{days}d {hours}h {minutes}m" - else: - uptime_str = f"{hours}h {minutes}m" + uptime_str = f"{days}d {hours}h {minutes}m" if days > 0 else f"{hours}h {minutes}m" load1, load5, load15 = psutil.getloadavg() net = psutil.net_io_counters() swap = psutil.swap_memory() payload = { - "fields": { - "online": {"booleanValue": True}, - "cpu": {"doubleValue": round(cpu, 1)}, - "ram": {"doubleValue": round(mem.percent, 1)}, - "ramUsed": {"stringValue": f"{mem.used // (1024 ** 2)}MB"}, - "ramTotal": {"stringValue": f"{mem.total // (1024 ** 2)}MB"}, - "disk": {"doubleValue": round(disk.percent, 1)}, - "diskUsed": {"stringValue": f"{disk.used // (1024 ** 3)}GB"}, - "diskTotal": {"stringValue": f"{disk.total // (1024 ** 3)}GB"}, - "swap": {"doubleValue": round(swap.percent, 1)}, - "swapUsed": {"stringValue": f"{swap.used // (1024 ** 2)}MB"}, - "swapTotal": {"stringValue": f"{swap.total // (1024 ** 2)}MB"}, - "uptime": {"stringValue": uptime_str}, - "load1": {"doubleValue": round(load1, 1)}, - "load5": {"doubleValue": round(load5, 1)}, - "load15": {"doubleValue": round(load15, 1)}, - "netSent": {"stringValue": _fmt_bytes(net.bytes_sent)}, - "netRecv": {"stringValue": _fmt_bytes(net.bytes_recv)}, - "processes": {"integerValue": len(psutil.pids())}, - "fetch": {"stringValue": await self._get_fastfetch()}, - } + "online": True, + "cpu": round(cpu, 1), + "ram": round(mem.percent, 1), + "ramUsed": f"{mem.used // (1024 ** 2)}MB", + "ramTotal": f"{mem.total // (1024 ** 2)}MB", + "disk": round(disk.percent, 1), + "diskUsed": f"{disk.used // (1024 ** 3)}GB", + "diskTotal": f"{disk.total // (1024 ** 3)}GB", + "swap": round(swap.percent, 1), + "swapUsed": f"{swap.used // (1024 ** 2)}MB", + "swapTotal": f"{swap.total // (1024 ** 2)}MB", + "uptime": uptime_str, + "load1": round(load1, 1), + "load5": round(load5, 1), + "load15": round(load15, 1), + "netSent": _fmt_bytes(net.bytes_sent), + "netRecv": _fmt_bytes(net.bytes_recv), + "processes": len(psutil.pids()), + "fetch": await self._get_fastfetch(), } - async with session.patch(DOC_SYSINFO, json=payload) as resp: - if resp.status not in (200, 201): - body = await resp.text() - log.debug("Firestore PATCH %d: %s", resp.status, body[:200]) + db = get_db() + await db.collection("sysinfo").document("server").set(payload, merge=True) @staticmethod async def _get_fastfetch() -> str: - """Run fastfetch --pipe --logo none, strip ANSI, return first 15 lines.""" try: proc = await asyncio.create_subprocess_exec( "fastfetch", "--pipe", "--logo", "none", @@ -120,7 +105,7 @@ class SysInfoSync: ) stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5) clean = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]|\[[0-9]+[A-Z]", "", stdout.decode()) - lines = [l for l in clean.strip().splitlines() if l.strip()] + lines = [line for line in clean.strip().splitlines() if line.strip()] return "\n".join(lines[:15]) except Exception: return ""