security audit
This commit is contained in:
1 parent
19e2db7fd4
commit
f16d848e42
7 files changed
+115
-116
No files matched your search
@@ -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
@@ -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}.")
|
||||||
|
|||||||
@@ -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=""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
this is what u get from google lol
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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 ""
|
||||||
Reference in new issue
Block a user