Files
NotBigGhostandClaude Opus 5 976b8fc622 Объявления: модель, API игрока и админки, очистка HTML
Объявление администрации показывается игроку окном в свой период, пока игрок
не закроет его («Понятно»). Отметка о закрытии хранится на сервере с номером
версии: правка с «показать заново» поднимает версию, и закрывшие прежнюю
увидят объявление снова — ответ помечен updated («обновлено»). Флаг
show_to_new_players=false прячет объявление от зарегистрировавшихся после
начала показа. Пересекающиеся объявления идут от старого к новому.

Текст приходит HTML-ом из редактора админки и сохраняется только после
очистки по белому списку (b, em, mark и mark.red, p, br): атрибуты
отбрасываются, script/style/svg — вместе с содержимым, текст экранируется
заново. Фронт вставляет только этот HTML.

API: GET /api/announcements/pending, POST /api/announcements/{id}/ack;
админка — список со статусом и счётчиком «закрыли N из M», создание, правка,
«снять с показа», удаление, всё в аудит. SSE-событие announcements активным
игрокам. Миграция 0015 идемпотентная.

Тесты: очистка (XSS-попытки, вложенные div), права, период и порядок,
«новые игроки», повторный показ, снятие, удаление, валидация. #84

Перенесено в main без рейтинга из a0a0e52; миграция 0015 — сразу от 0013
(как в ec0445f).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:57:46 +03:00

47 lines
1.7 KiB
Python

"""Объявления администрации для игрока: что показать сейчас и «Понятно».
Появление нового объявления у открытой вкладки обеспечивает SSE-сигнал
`{type:"announcements"}`; объявление с отложенным началом клиент подхватывает
периодическим перезапросом."""
from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlmodel import Session
from app.auth.deps import get_current_user
from app.db.session import get_session
from app.models import User
from app.schemas import api as s
from app.services import announcement_service
router = APIRouter(prefix="/announcements", tags=["announcements"])
@router.get("/pending", response_model=list[s.AnnouncementRead])
def pending(
session: Session = Depends(get_session),
user: User = Depends(get_current_user),
) -> list[s.AnnouncementRead]:
return [
s.AnnouncementRead(
id=a.id, # type: ignore[arg-type]
title=a.title,
body_html=a.body_html,
revision=a.revision,
updated=updated,
)
for a, updated in announcement_service.pending_for_user(session, user)
]
@router.post("/{announcement_id}/ack", response_model=s.OkResponse)
def acknowledge(
announcement_id: int,
body: s.AnnouncementAck,
session: Session = Depends(get_session),
user: User = Depends(get_current_user),
) -> s.OkResponse:
announcement_service.acknowledge(session, user.id, announcement_id, body.revision) # type: ignore[arg-type]
session.commit()
return s.OkResponse()