feat: users, administrator role, selective access to server, login screen, admin dashboard with user and access management
This commit is contained in:
20 files changed
+762
-72
No files matched your search
@@ -1,16 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import argparse
|
||||
import getpass
|
||||
|
||||
import psycopg
|
||||
|
||||
from zyrarbot.application import Application
|
||||
from zyrarbot.ui.api.auth import hash_password
|
||||
from zyrarbot.storage.adapters.postgres.database import PostgresDatabase
|
||||
|
||||
|
||||
def create_admin(username: str, password: str) -> None:
|
||||
database = PostgresDatabase(
|
||||
host=os.environ.get("POSTGRES_HOST", "localhost"),
|
||||
port=int(os.environ.get("POSTGRES_PORT", "5432")),
|
||||
database=os.environ.get("POSTGRES_DB", "zyrarbot"),
|
||||
user=os.environ.get("POSTGRES_USER", "zyrarbot"),
|
||||
password=os.environ.get("POSTGRES_PASSWORD", "zyrarbot"),
|
||||
)
|
||||
with database.connect() as connection:
|
||||
connection.execute("INSERT INTO users(username,password_hash,is_admin) VALUES (%s,%s,TRUE)",
|
||||
(username, hash_password(password)))
|
||||
print(f"Utworzono administratora: {username}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="ŻyrarBOT")
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
sub = subparsers.add_parser("create-admin", help="Utwórz konto administratora")
|
||||
sub.add_argument("username")
|
||||
sub.add_argument("--password")
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
||||
)
|
||||
if args.command == "create-admin":
|
||||
password = args.password if args.password is not None else getpass.getpass("Hasło administratora: ")
|
||||
if not password:
|
||||
parser.error("Hasło nie może być puste")
|
||||
try:
|
||||
create_admin(args.username, password)
|
||||
except psycopg.errors.UniqueViolation:
|
||||
parser.error("Użytkownik o tej nazwie już istnieje")
|
||||
return
|
||||
asyncio.run(Application().run())
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from zyrarbot.storage.adapters.filesystem.uploads import UploadsRepository
|
||||
from zyrarbot.storage.adapters.postgres.persistence import PostgresPersistence
|
||||
from zyrarbot.storage.ports.persistence import Persistence
|
||||
from zyrarbot.ui.api.factory import mount_api
|
||||
from zyrarbot.ui.api.auth import AuthRepository
|
||||
from zyrarbot.ui.bot.client import MusicBot
|
||||
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
||||
|
||||
@@ -74,6 +75,18 @@ class Application:
|
||||
|
||||
self.web = FastAPI(title="ŻyrarBOT")
|
||||
self.web.state.restart = self.schedule_restart
|
||||
self.web.state.persistence = self.persistence
|
||||
self.web.state.auth = AuthRepository(self.persistence.database)
|
||||
|
||||
@self.web.middleware("http")
|
||||
async def require_panel_login(request, call_next):
|
||||
if request.url.path.startswith("/api/") and request.url.path not in {
|
||||
"/api/health", "/api/auth/login"
|
||||
}:
|
||||
if self.web.state.auth.user_for_token(request.cookies.get("zyrarbot_session")) is None:
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse({"detail": "Wymagane logowanie"}, status_code=401)
|
||||
return await call_next(request)
|
||||
broadcaster = mount_api(
|
||||
self.web,
|
||||
settings=self.settings,
|
||||
@@ -111,6 +124,11 @@ class Application:
|
||||
"""Serve the SPA for a direct link to a guild view."""
|
||||
return FileResponse(os.path.join(self.settings.static_dir, "index.html"))
|
||||
|
||||
@self.web.get("/admin/{route_path:path}", include_in_schema=False)
|
||||
async def frontend_route(route_path: str) -> FileResponse:
|
||||
"""Serve the SPA for nested frontend routes such as admin pages."""
|
||||
return FileResponse(os.path.join(self.settings.static_dir, "index.html"))
|
||||
|
||||
self.web.mount(
|
||||
"/", StaticFiles(directory=self.settings.static_dir, html=True), name="static"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import psycopg
|
||||
|
||||
|
||||
class UsersMigration:
|
||||
version = 2
|
||||
name = "users_and_access"
|
||||
|
||||
def apply(self, connection: psycopg.Connection) -> None:
|
||||
connection.execute("""
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
connection.execute("""
|
||||
CREATE TABLE user_sessions (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
)
|
||||
""")
|
||||
connection.execute("""
|
||||
CREATE TABLE user_guild_access (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
guild_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (user_id, guild_id)
|
||||
)
|
||||
""")
|
||||
|
||||
def rollback(self, connection: psycopg.Connection) -> None:
|
||||
connection.execute("DROP TABLE user_guild_access")
|
||||
connection.execute("DROP TABLE user_sessions")
|
||||
connection.execute("DROP TABLE users")
|
||||
@@ -5,5 +5,6 @@ from importlib import import_module
|
||||
InitialSchemaMigration = import_module(
|
||||
".0001_initial_schema", __name__
|
||||
).InitialSchemaMigration
|
||||
UsersMigration = import_module(".0002_users", __name__).UsersMigration
|
||||
|
||||
MIGRATIONS = (InitialSchemaMigration(),)
|
||||
MIGRATIONS = (InitialSchemaMigration(), UsersMigration())
|
||||
@@ -0,0 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
from zyrarbot.storage.adapters.postgres.database import PostgresDatabase
|
||||
|
||||
SESSION_DAYS = 14
|
||||
|
||||
|
||||
def hash_password(password: str, salt: bytes | None = None) -> str:
|
||||
salt = salt or secrets.token_bytes(16)
|
||||
digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 310_000)
|
||||
return f"{salt.hex()}:{digest.hex()}"
|
||||
|
||||
|
||||
def verify_password(password: str, encoded: str) -> bool:
|
||||
try:
|
||||
salt, expected = encoded.split(":", 1)
|
||||
actual = hash_password(password, bytes.fromhex(salt)).split(":", 1)[1]
|
||||
return hmac.compare_digest(actual, expected)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
class AuthRepository:
|
||||
def __init__(self, database: PostgresDatabase):
|
||||
self.database = database
|
||||
|
||||
def login(self, username: str, password: str) -> tuple[str, dict] | None:
|
||||
with self.database.connect() as connection:
|
||||
row = connection.execute(
|
||||
"SELECT id, username, password_hash, is_admin FROM users WHERE username=%s",
|
||||
(username,),
|
||||
).fetchone()
|
||||
if not row or not verify_password(password, row[2]):
|
||||
return None
|
||||
token = secrets.token_urlsafe(32)
|
||||
connection.execute(
|
||||
"INSERT INTO user_sessions(token_hash,user_id,expires_at) VALUES (%s,%s,%s)",
|
||||
(hashlib.sha256(token.encode()).hexdigest(), row[0],
|
||||
datetime.now(timezone.utc) + timedelta(days=SESSION_DAYS)),
|
||||
)
|
||||
return token, {"id": row[0], "username": row[1], "is_admin": row[3]}
|
||||
|
||||
def user_for_token(self, token: str | None) -> dict | None:
|
||||
if not token:
|
||||
return None
|
||||
with self.database.connect() as connection:
|
||||
row = connection.execute("""
|
||||
SELECT u.id, u.username, u.is_admin FROM user_sessions s
|
||||
JOIN users u ON u.id=s.user_id
|
||||
WHERE s.token_hash=%s AND s.expires_at > CURRENT_TIMESTAMP
|
||||
""", (hashlib.sha256(token.encode()).hexdigest(),)).fetchone()
|
||||
return {"id": row[0], "username": row[1], "is_admin": row[2]} if row else None
|
||||
|
||||
def logout(self, token: str | None) -> None:
|
||||
if token:
|
||||
with self.database.connect() as connection:
|
||||
connection.execute("DELETE FROM user_sessions WHERE token_hash=%s",
|
||||
(hashlib.sha256(token.encode()).hexdigest(),))
|
||||
|
||||
def guild_ids(self, user: dict, available: list[int]) -> list[int]:
|
||||
if user["is_admin"]:
|
||||
return available
|
||||
with self.database.connect() as connection:
|
||||
rows = connection.execute("SELECT guild_id FROM user_guild_access WHERE user_id=%s",
|
||||
(user["id"],)).fetchall()
|
||||
allowed = {row[0] for row in rows}
|
||||
return [guild_id for guild_id in available if guild_id in allowed]
|
||||
|
||||
|
||||
def current_user(request: Request) -> dict:
|
||||
user = request.app.state.auth.user_for_token(request.cookies.get("zyrarbot_session"))
|
||||
if user is None:
|
||||
raise HTTPException(status_code=401, detail="Wymagane logowanie")
|
||||
return user
|
||||
|
||||
|
||||
def require_admin(request: Request) -> dict:
|
||||
user = current_user(request)
|
||||
if not user["is_admin"]:
|
||||
raise HTTPException(status_code=403, detail="Wymagane uprawnienia administratora")
|
||||
return user
|
||||
@@ -24,11 +24,11 @@ class ConnectionManager:
|
||||
self._connections.pop(websocket, None)
|
||||
|
||||
async def broadcast(
|
||||
self, message: dict[str, Any] | Callable[[int | None], dict[str, Any]]
|
||||
self, message: dict[str, Any] | Callable[..., dict[str, Any]]
|
||||
) -> None:
|
||||
for connection, guild_id in list(self._connections.items()):
|
||||
try:
|
||||
payload = message(guild_id) if callable(message) else message
|
||||
payload = message(connection, guild_id) if callable(message) else message
|
||||
await connection.send_json(payload)
|
||||
except Exception: # noqa: BLE001 - zerwane połączenie po prostu usuwamy
|
||||
self.disconnect(connection)
|
||||
@@ -59,9 +59,16 @@ def get_socket_status_service(websocket: WebSocket) -> StatusService:
|
||||
def get_active_guild(request: Request, guild_id: int | None = Query(default=None)) -> discord.Guild:
|
||||
"""Zwraca serwer wskazany przez panel lub pierwszy znany serwer."""
|
||||
guilds = get_guilds(request)
|
||||
guild = next((item for item in guilds.guilds() if item.id == guild_id), None)
|
||||
user = request.app.state.auth.user_for_token(request.cookies.get("zyrarbot_session"))
|
||||
permitted = request.app.state.auth.guild_ids(user, [item.id for item in guilds.guilds()]) if user else []
|
||||
if guild_id is not None and guild_id not in permitted:
|
||||
raise HTTPException(status_code=403, detail="Brak dostępu do tego serwera")
|
||||
guild = next((item for item in guilds.guilds() if item.id == guild_id and item.id in permitted), None)
|
||||
if guild_id is None:
|
||||
guild = guilds.active_guild()
|
||||
active = guilds.active_guild()
|
||||
guild = active if active and active.id in permitted else next(
|
||||
(item for item in guilds.guilds() if item.id in permitted), None
|
||||
)
|
||||
if guild is None:
|
||||
raise HTTPException(status_code=404, detail="Unknown Discord server")
|
||||
return guild
|
||||
@@ -9,6 +9,7 @@ from zyrarbot.storage.ports.guild_config import GuildConfigRepository
|
||||
from zyrarbot.storage.ports.uploads import UploadsRepository
|
||||
from zyrarbot.ui.api.connection_manager import ConnectionManager
|
||||
from zyrarbot.ui.api.routers import (
|
||||
auth_router,
|
||||
playback_router,
|
||||
queue_router,
|
||||
status_router,
|
||||
@@ -21,6 +22,7 @@ from zyrarbot.ui.api.status_service import StatusService
|
||||
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
||||
|
||||
ROUTERS = (
|
||||
auth_router.router,
|
||||
status_router.router,
|
||||
playback_router.router,
|
||||
queue_router.router,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zyrarbot.ui.api.auth import current_user, require_admin
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
class Credentials(BaseModel):
|
||||
username: str = Field(min_length=1, max_length=80)
|
||||
password: str = Field(min_length=1)
|
||||
|
||||
|
||||
class UserCreate(Credentials):
|
||||
guild_ids: list[int] = Field(default_factory=list)
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
guild_ids: list[int]
|
||||
|
||||
|
||||
class UserRoleUpdate(BaseModel):
|
||||
is_admin: bool
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(body: Credentials, request: Request, response: Response) -> dict:
|
||||
result = request.app.state.auth.login(body.username, body.password)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=401, detail="Nieprawidłowa nazwa użytkownika lub hasło")
|
||||
token, user = result
|
||||
response.set_cookie("zyrarbot_session", token, httponly=True, samesite="lax",
|
||||
secure=request.url.scheme == "https", max_age=14 * 86400, path="/")
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request, response: Response) -> dict:
|
||||
request.app.state.auth.logout(request.cookies.get("zyrarbot_session"))
|
||||
response.delete_cookie("zyrarbot_session", path="/")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def me(user: dict = Depends(current_user)) -> dict:
|
||||
return user
|
||||
|
||||
|
||||
def _available_guilds(request: Request) -> list[dict]:
|
||||
return [{"guild_id": str(g.id), "guild_name": g.name} for g in request.app.state.guilds.guilds()]
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def list_users(request: Request, _: dict = Depends(require_admin)) -> list[dict]:
|
||||
with request.app.state.persistence.database.connect() as connection:
|
||||
rows = connection.execute("SELECT id,username,is_admin FROM users ORDER BY username").fetchall()
|
||||
assignments = connection.execute("SELECT user_id,guild_id FROM user_guild_access").fetchall()
|
||||
by_user: dict[int, list[str]] = {}
|
||||
for user_id, guild_id in assignments:
|
||||
by_user.setdefault(user_id, []).append(str(guild_id))
|
||||
return [{"id": row[0], "username": row[1], "is_admin": row[2],
|
||||
"guild_ids": by_user.get(row[0], [])} for row in rows]
|
||||
|
||||
|
||||
@router.get("/guilds")
|
||||
async def list_guilds(request: Request, _: dict = Depends(require_admin)) -> list[dict]:
|
||||
return _available_guilds(request)
|
||||
|
||||
|
||||
@router.post("/users")
|
||||
async def create_user(body: UserCreate, request: Request, _: dict = Depends(require_admin)) -> dict:
|
||||
from zyrarbot.ui.api.auth import hash_password
|
||||
available = {g.id for g in request.app.state.guilds.guilds()}
|
||||
if not set(body.guild_ids) <= available:
|
||||
raise HTTPException(status_code=400, detail="Nieznany serwer")
|
||||
try:
|
||||
with request.app.state.persistence.database.connect() as connection:
|
||||
row = connection.execute("INSERT INTO users(username,password_hash) VALUES (%s,%s) RETURNING id",
|
||||
(body.username, hash_password(body.password))).fetchone()
|
||||
for guild_id in set(body.guild_ids):
|
||||
connection.execute("INSERT INTO user_guild_access(user_id,guild_id) VALUES (%s,%s)",
|
||||
(row[0], guild_id))
|
||||
except Exception as error:
|
||||
if getattr(error, "sqlstate", None) == "23505":
|
||||
raise HTTPException(status_code=409, detail="Taka nazwa użytkownika już istnieje") from error
|
||||
raise
|
||||
return {"id": row[0], "username": body.username, "is_admin": False,
|
||||
"guild_ids": [str(g) for g in set(body.guild_ids)]}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/guilds")
|
||||
async def update_access(user_id: int, body: UserUpdate, request: Request,
|
||||
_: dict = Depends(require_admin)) -> dict:
|
||||
available = {g.id for g in request.app.state.guilds.guilds()}
|
||||
if not set(body.guild_ids) <= available:
|
||||
raise HTTPException(status_code=400, detail="Nieznany serwer")
|
||||
with request.app.state.persistence.database.connect() as connection:
|
||||
exists = connection.execute("SELECT 1 FROM users WHERE id=%s AND is_admin=FALSE",
|
||||
(user_id,)).fetchone()
|
||||
if not exists:
|
||||
raise HTTPException(status_code=404, detail="Nie znaleziono użytkownika")
|
||||
connection.execute("DELETE FROM user_guild_access WHERE user_id=%s", (user_id,))
|
||||
for guild_id in set(body.guild_ids):
|
||||
connection.execute("INSERT INTO user_guild_access(user_id,guild_id) VALUES (%s,%s)",
|
||||
(user_id, guild_id))
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/role")
|
||||
async def update_role(user_id: int, body: UserRoleUpdate, request: Request,
|
||||
admin: dict = Depends(require_admin)) -> dict:
|
||||
if user_id == admin["id"] and not body.is_admin:
|
||||
raise HTTPException(status_code=400, detail="Nie możesz odebrać uprawnień administratora własnemu kontu")
|
||||
with request.app.state.persistence.database.connect() as connection:
|
||||
row = connection.execute(
|
||||
"UPDATE users SET is_admin=%s WHERE id=%s RETURNING id,username,is_admin",
|
||||
(body.is_admin, user_id),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Nie znaleziono użytkownika")
|
||||
return {"id": row[0], "username": row[1], "is_admin": row[2]}
|
||||
@@ -8,16 +8,22 @@ from fastapi import APIRouter, Depends, Request
|
||||
from zyrarbot.music.music_player import MusicPlayer
|
||||
from zyrarbot.ui.api.dependencies import get_active_guild, get_player, get_status_service
|
||||
from zyrarbot.ui.api.status_service import StatusService
|
||||
from zyrarbot.ui.api.auth import current_user, require_admin
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["status"])
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_status(
|
||||
request: Request,
|
||||
user: dict = Depends(current_user),
|
||||
guild: discord.Guild = Depends(get_active_guild),
|
||||
status: StatusService = Depends(get_status_service),
|
||||
) -> dict[str, Any]:
|
||||
return status.snapshot(guild.id)
|
||||
snapshot = status.snapshot(guild.id)
|
||||
permitted = request.app.state.auth.guild_ids(user, [g.id for g in request.app.state.guilds.guilds()])
|
||||
snapshot["guilds"] = [item for item in snapshot["guilds"] if int(item["guild_id"]) in permitted]
|
||||
return snapshot
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
@@ -26,7 +32,7 @@ async def get_health(player: MusicPlayer = Depends(get_player)) -> dict[str, Any
|
||||
|
||||
|
||||
@router.post("/restart")
|
||||
async def restart_bot(request: Request) -> dict[str, Any]:
|
||||
async def restart_bot(request: Request, _: dict = Depends(require_admin)) -> dict[str, Any]:
|
||||
restart_action = getattr(request.app.state, "restart", None)
|
||||
if restart_action:
|
||||
restart_action()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import discord
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from zyrarbot.music.music_player import MusicPlayer
|
||||
from zyrarbot.music.notification_channel_resolver import NotificationChannelResolver
|
||||
@@ -16,6 +16,7 @@ from zyrarbot.ui.api.dependencies import (
|
||||
from zyrarbot.ui.api.requests.connect_request import ConnectRequest
|
||||
from zyrarbot.ui.api.requests.notification_channel_request import NotificationChannelRequest
|
||||
from zyrarbot.ui.api.status_broadcaster import StatusBroadcaster
|
||||
from zyrarbot.ui.api.auth import current_user
|
||||
from zyrarbot.ui.bot.guild_resolver import GuildResolver
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["voice"])
|
||||
@@ -50,6 +51,8 @@ async def set_notification_channel(
|
||||
@router.post("/connect")
|
||||
async def connect(
|
||||
request: ConnectRequest,
|
||||
http_request: Request,
|
||||
user: dict = Depends(current_user),
|
||||
guilds: GuildResolver = Depends(get_guilds),
|
||||
player: MusicPlayer = Depends(get_player),
|
||||
broadcaster: StatusBroadcaster = Depends(get_broadcaster),
|
||||
@@ -62,6 +65,11 @@ async def connect(
|
||||
channel = guilds.channel(channel_id)
|
||||
if not isinstance(channel, discord.VoiceChannel):
|
||||
raise HTTPException(status_code=404, detail="Voice channel not found")
|
||||
allowed = http_request.app.state.auth.guild_ids(
|
||||
user, [item.id for item in guilds.guilds()]
|
||||
)
|
||||
if channel.guild.id not in allowed:
|
||||
raise HTTPException(status_code=403, detail="Brak dostępu do tego serwera")
|
||||
|
||||
if channel.guild.me and not channel.permissions_for(channel.guild.me).connect:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -20,9 +20,22 @@ async def websocket_endpoint(
|
||||
status: StatusService = Depends(get_socket_status_service),
|
||||
guild_id: int | None = Query(default=None),
|
||||
) -> None:
|
||||
user = websocket.app.state.auth.user_for_token(websocket.cookies.get("zyrarbot_session"))
|
||||
if user is None:
|
||||
await websocket.close(code=4401)
|
||||
return
|
||||
available = [g.id for g in websocket.app.state.guilds.guilds()]
|
||||
permitted = websocket.app.state.auth.guild_ids(user, available)
|
||||
if guild_id is not None and guild_id not in permitted:
|
||||
await websocket.close(code=4403)
|
||||
return
|
||||
if guild_id is None:
|
||||
guild_id = permitted[0] if permitted else None
|
||||
await connections.connect(websocket, guild_id)
|
||||
try:
|
||||
await websocket.send_json(status.snapshot(guild_id))
|
||||
initial = status.snapshot(guild_id)
|
||||
initial["guilds"] = [item for item in initial["guilds"] if int(item["guild_id"]) in permitted]
|
||||
await websocket.send_json(initial)
|
||||
while True:
|
||||
# Klient nic nie wysyła - czekamy wyłącznie na rozłączenie.
|
||||
await websocket.receive_text()
|
||||
|
||||
@@ -24,10 +24,21 @@ class StatusBroadcaster:
|
||||
if not self._connections.has_listeners:
|
||||
return
|
||||
try:
|
||||
await self._connections.broadcast(self._status.snapshot)
|
||||
await self._connections.broadcast(self._snapshot_for_connection)
|
||||
except Exception as error: # noqa: BLE001 - rozgłaszanie nie może ubić pętli
|
||||
logger.error("Błąd rozgłaszania stanu: %s", error)
|
||||
|
||||
def _snapshot_for_connection(self, connection, guild_id: int | None) -> dict:
|
||||
payload = self._status.snapshot(guild_id)
|
||||
user = connection.app.state.auth.user_for_token(
|
||||
connection.cookies.get("zyrarbot_session")
|
||||
)
|
||||
allowed = connection.app.state.auth.guild_ids(
|
||||
user, [guild.id for guild in connection.app.state.guilds.guilds()]
|
||||
) if user else []
|
||||
payload["guilds"] = [item for item in payload["guilds"] if int(item["guild_id"]) in allowed]
|
||||
return payload
|
||||
|
||||
def start(self) -> None:
|
||||
if self._task is None or self._task.done():
|
||||
self._task = asyncio.create_task(self._loop())
|
||||
|
||||
Reference in new issue
Block a user