security audit

This commit is contained in:
esef committed 2026-07-13 11:01:42 +03:00
1 parent 19e2db7fd4
commit f16d848e42
7 files changed
+115 -116

No files matched your search

+2
View File
@@ -15,3 +15,5 @@ __pycache__/
ssh_update.py ssh_update.py
ssh_*.py ssh_*.py
blue_archive/ba_students.json blue_archive/ba_students.json
firebase-service-account.json
firebase_service_account.json
+44 -49
View File
@@ -1,39 +1,36 @@
"""Calendar cog — modal-based booking, country-aware timezones.""" """Calendar cog — modal-based booking, Firestore Admin SDK."""
###################################################################### ######################################################################
import logging import logging
import re import re
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import aiohttp
import discord import discord
from discord.ext import commands from discord.ext import commands
from google.cloud.firestore import AsyncClient
from firebase_website import get_db
from .calendar_data import COUNTRY_TZ from .calendar_data import COUNTRY_TZ
log = logging.getLogger(__name__) 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}$") DATE_RE = re.compile(r"^\d{2}-\d{2}-\d{4}$")
TIME_RE = re.compile(r"^\d{2}:\d{2}$") 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: def _find_offset(country: str) -> float | None:
"""Look up a country's UTC offset. Case-insensitive fuzzy match.""" """Look up a country's UTC offset. Case-insensitive fuzzy match."""
c = country.strip().lower() c = country.strip().lower()
if not c: if not c:
return None return None
# Exact match
for name, offset in COUNTRY_TZ.items(): for name, offset in COUNTRY_TZ.items():
if c == name.lower(): if c == name.lower():
return offset return offset
# Starts with
for name, offset in COUNTRY_TZ.items(): for name, offset in COUNTRY_TZ.items():
if name.lower().startswith(c): if name.lower().startswith(c):
return offset return offset
# Substring anywhere
for name, offset in COUNTRY_TZ.items(): for name, offset in COUNTRY_TZ.items():
if c in name.lower(): if c in name.lower():
return offset return offset
@@ -98,8 +95,7 @@ class BookModal(discord.ui.Modal, title="Book an Event"):
offset = _find_offset(country) offset = _find_offset(country)
if offset is None: if offset is None:
return await interaction.response.send_message( return await interaction.response.send_message(
f"Unknown country. Try: Turkey, UK, US East, Japan, etc.\n" "Unknown country. Try: Turkey, UK, US East, Japan, etc.",
f"Type a country name from the list.",
ephemeral=True, ephemeral=True,
) )
@@ -110,29 +106,29 @@ class BookModal(discord.ui.Modal, title="Book an Event"):
).replace(tzinfo=tz) ).replace(tzinfo=tz)
utc_ts = local_dt.astimezone(timezone.utc).isoformat() utc_ts = local_dt.astimezone(timezone.utc).isoformat()
payload = { event_data = {
"fields": { "title": title,
"title": {"stringValue": title}, "date": f"{year}-{month}-{day}",
"date": {"stringValue": f"{year}-{month}-{day}"}, "time": time_str,
"time": {"stringValue": time_str}, "country": country,
"country": {"stringValue": country}, "utc": utc_ts,
"utc": {"stringValue": utc_ts}, "description": description,
"description": {"stringValue": description}, "createdBy": str(interaction.user),
"createdBy": {"stringValue": str(interaction.user)},
}
} }
async with aiohttp.ClientSession() as session: try:
async with session.post(FIRESTORE_CALENDAR, json=payload) as resp: db = get_db()
if resp.status in (200, 201): await db.collection(COLLECTION).add(event_data)
desc = f" — {description}" if description else "" except Exception:
await interaction.response.send_message( log.exception("Failed to add calendar event")
f"Booked **{date} at {time_str} ({country})**: **{title}**{desc}" return await interaction.response.send_message(
) "Failed to save. Try again.", ephemeral=True
else: )
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): class BookButton(discord.ui.View):
@@ -164,23 +160,22 @@ class Calendar(commands.Cog):
day, month, year = date.split("-") day, month, year = date.split("-")
iso_date = f"{year}-{month}-{day}" iso_date = f"{year}-{month}-{day}"
deleted = False try:
async with aiohttp.ClientSession() as session: db = get_db()
async with session.get(FIRESTORE_CALENDAR) as resp: docs = (
if resp.status != 200: await db.collection(COLLECTION)
return await ctx.reply("Could not fetch events.") .where("date", "==", iso_date)
data = await resp.json() .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", []): deleted = False
fields = doc.get("fields", {}) for doc in docs:
d = fields.get("date", {}).get("stringValue", "") await doc.reference.delete()
t = fields.get("title", {}).get("stringValue", "") deleted = True
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
if deleted: if deleted:
await ctx.reply(f"Removed **{title}** from {date}.") await ctx.reply(f"Removed **{title}** from {date}.")
+3
View File
@@ -22,6 +22,9 @@ OWNER_ID="your discord servers owner id here"
DEEPSEEK_API=lol you thought 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 #mis ids member speicific stuff
MELLO="" MELLO=""
FEROX="" FEROX=""
+1
View File
@@ -0,0 +1 @@
this is what u get from google lol
+30 -6
View File
@@ -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: Requires GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service-account JSON.
sysinfo_sync — CPU, RAM, disk, uptime pushed every 10 s
(future) — gacha stats, now-playing, radio listeners, …
Usage from main.py: Usage from main.py:
await bot.load_extension("firebase_website") 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 discord.ext import commands
from google.cloud.firestore import AsyncClient
from .sysinfo_sync import SysInfoSync from .sysinfo_sync import SysInfoSync
log = logging.getLogger(__name__)
_db: AsyncClient | None = None
_sync: SysInfoSync | 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: async def setup(bot: commands.Bot) -> None:
"""Called by bot.load_extension(). Starts the sysinfo sync loop.""" """Start the sysinfo sync loop."""
global _sync global _sync
get_db() # early init — fails fast if credentials are missing
_sync = SysInfoSync() _sync = SysInfoSync()
_sync.start() _sync.start()
async def teardown(bot: commands.Bot) -> None: async def teardown(bot: commands.Bot) -> None:
"""Called on bot unload / shutdown. Stops all sync tasks.""" """Stop the sysinfo sync loop."""
global _sync global _sync
if _sync is not None: if _sync is not None:
_sync.stop() _sync.stop()
-11
View File
@@ -1,18 +1,7 @@
"""Firebase project config — shared by all website integrations.""" """Firebase project config — shared by all website integrations."""
###################################################################### ######################################################################
# Your Firebase project (same one the website uses in comments.js)
PROJECT_ID = "esef-514bf" 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) # How often system stats are pushed (seconds)
SYSINFO_INTERVAL = 10 SYSINFO_INTERVAL = 10
+35 -50
View File
@@ -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 asyncio
import logging import logging
import re import re
import time import time
import aiohttp
import psutil import psutil
from .config import DOC_SYSINFO, SYSINFO_INTERVAL from . import get_db
from .config import SYSINFO_INTERVAL
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -27,19 +27,16 @@ class SysInfoSync:
def __init__(self) -> None: def __init__(self) -> None:
self._task: asyncio.Task | None = None self._task: asyncio.Task | None = None
self._tick = 0
# ── public API ────────────────────────────────────────────────────────── # ── public API ──────────────────────────────────────────────────────────
def start(self) -> None: def start(self) -> None:
"""Launch the background sync loop."""
if self._task is not None: if self._task is not None:
return return
self._task = asyncio.create_task(self._run()) self._task = asyncio.create_task(self._run())
log.info("SysInfo → Firestore sync started (every %ds)", SYSINFO_INTERVAL) log.info("SysInfo → Firestore sync started (every %ds)", SYSINFO_INTERVAL)
def stop(self) -> None: def stop(self) -> None:
"""Cancel the background sync loop."""
if self._task is None: if self._task is None:
return return
self._task.cancel() self._task.cancel()
@@ -49,69 +46,57 @@ class SysInfoSync:
# ── internals ─────────────────────────────────────────────────────────── # ── internals ───────────────────────────────────────────────────────────
async def _run(self) -> None: async def _run(self) -> None:
"""Main loop — collect stats, push, sleep, repeat.""" while True:
async with aiohttp.ClientSession() as session: try:
while True: await self._push()
try: except asyncio.CancelledError:
await self._push(session) return
except asyncio.CancelledError: except Exception:
return log.debug("SysInfo Firestore push failed", exc_info=True)
except Exception: await asyncio.sleep(SYSINFO_INTERVAL)
log.debug("SysInfo Firestore push failed", exc_info=True)
await asyncio.sleep(SYSINFO_INTERVAL)
async def _push(self, session: aiohttp.ClientSession) -> None: async def _push(self) -> None:
"""Collect current stats and PATCH the Firestore document."""
cpu = psutil.cpu_percent(interval=1) cpu = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
disk = psutil.disk_usage("/") disk = psutil.disk_usage("/")
uptime_seconds = int(time.time() - psutil.boot_time()) uptime_seconds = int(time.time() - psutil.boot_time())
# uptime → "3d 12h 5m" or "5h 23m"
days, r = divmod(uptime_seconds, 86400) days, r = divmod(uptime_seconds, 86400)
hours, r = divmod(r, 3600) hours, r = divmod(r, 3600)
minutes, _ = divmod(r, 60) minutes, _ = divmod(r, 60)
if days > 0: uptime_str = f"{days}d {hours}h {minutes}m" if days > 0 else f"{hours}h {minutes}m"
uptime_str = f"{days}d {hours}h {minutes}m"
else:
uptime_str = f"{hours}h {minutes}m"
load1, load5, load15 = psutil.getloadavg() load1, load5, load15 = psutil.getloadavg()
net = psutil.net_io_counters() net = psutil.net_io_counters()
swap = psutil.swap_memory() swap = psutil.swap_memory()
payload = { payload = {
"fields": { "online": True,
"online": {"booleanValue": True}, "cpu": round(cpu, 1),
"cpu": {"doubleValue": round(cpu, 1)}, "ram": round(mem.percent, 1),
"ram": {"doubleValue": round(mem.percent, 1)}, "ramUsed": f"{mem.used // (1024 ** 2)}MB",
"ramUsed": {"stringValue": f"{mem.used // (1024 ** 2)}MB"}, "ramTotal": f"{mem.total // (1024 ** 2)}MB",
"ramTotal": {"stringValue": f"{mem.total // (1024 ** 2)}MB"}, "disk": round(disk.percent, 1),
"disk": {"doubleValue": round(disk.percent, 1)}, "diskUsed": f"{disk.used // (1024 ** 3)}GB",
"diskUsed": {"stringValue": f"{disk.used // (1024 ** 3)}GB"}, "diskTotal": f"{disk.total // (1024 ** 3)}GB",
"diskTotal": {"stringValue": f"{disk.total // (1024 ** 3)}GB"}, "swap": round(swap.percent, 1),
"swap": {"doubleValue": round(swap.percent, 1)}, "swapUsed": f"{swap.used // (1024 ** 2)}MB",
"swapUsed": {"stringValue": f"{swap.used // (1024 ** 2)}MB"}, "swapTotal": f"{swap.total // (1024 ** 2)}MB",
"swapTotal": {"stringValue": f"{swap.total // (1024 ** 2)}MB"}, "uptime": uptime_str,
"uptime": {"stringValue": uptime_str}, "load1": round(load1, 1),
"load1": {"doubleValue": round(load1, 1)}, "load5": round(load5, 1),
"load5": {"doubleValue": round(load5, 1)}, "load15": round(load15, 1),
"load15": {"doubleValue": round(load15, 1)}, "netSent": _fmt_bytes(net.bytes_sent),
"netSent": {"stringValue": _fmt_bytes(net.bytes_sent)}, "netRecv": _fmt_bytes(net.bytes_recv),
"netRecv": {"stringValue": _fmt_bytes(net.bytes_recv)}, "processes": len(psutil.pids()),
"processes": {"integerValue": len(psutil.pids())}, "fetch": await self._get_fastfetch(),
"fetch": {"stringValue": await self._get_fastfetch()},
}
} }
async with session.patch(DOC_SYSINFO, json=payload) as resp: db = get_db()
if resp.status not in (200, 201): await db.collection("sysinfo").document("server").set(payload, merge=True)
body = await resp.text()
log.debug("Firestore PATCH %d: %s", resp.status, body[:200])
@staticmethod @staticmethod
async def _get_fastfetch() -> str: async def _get_fastfetch() -> str:
"""Run fastfetch --pipe --logo none, strip ANSI, return first 15 lines."""
try: try:
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
"fastfetch", "--pipe", "--logo", "none", "fastfetch", "--pipe", "--logo", "none",
@@ -120,7 +105,7 @@ class SysInfoSync:
) )
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5) 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()) 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]) return "\n".join(lines[:15])
except Exception: except Exception:
return "" return ""