Add global role synchronization for Discord with configurable interval
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit introduces a new feature for global role synchronization in Discord, allowing for periodic reconciliation of managed roles. A new environment variable, `DISCORD_GLOBAL_SYNC_INTERVAL`, has been added to configure the synchronization interval, defaulting to 5 minutes. The `RoleWorker` has been updated to schedule global sync jobs, ensuring that missing managed roles are restored and extra assignments are removed without affecting unrelated server roles. Database schema changes support the new synchronization logic, and tests have been added to validate the functionality of the global reconciliation process.
This commit is contained in:
2026-07-19 12:04:33 +03:00
parent e5646ba33d
commit 6b189bfce4
14 changed files with 568 additions and 42 deletions

View File

@@ -10,6 +10,8 @@ Admin UI содержит пошаговый workflow и roster editor с same-r
Добавлен PostgreSQL-backed Discord role worker. После подтверждения составов он создаёт и выдаёт роли по актуальным Team.Name, Captain и slot Tank/Damage/Support, учитывает live-переименование и аварийную замену, а при завершении/отмене/удалении/откате удаляет временные роли. Повтор jobs идемпотентен; guest и отсутствующие в guild игроки сохраняются как предупреждения и не блокируют микс.
При любом RSVP тот же worker создаёт event-specific роли «Зарегистрирован», «Идёт», «Возможно», «Не идёт», выдаёт общую роль ответа и ровно один текущий статус, обновляет названия при rename Event и снимает назначения после удаления RSVP. Registration close роли не удаляет; общий event teardown очищает их вместе с team/captain roles.
В Discord member list отдельными hoisted-группами отображаются роли команд и общая роль регистрации. Team-группы располагаются выше registered-группы; Captain, slot и конкретный RSVP-статус остаются негруппирующими служебными ролями.
Если связанный пользователь зарегистрировался до вступления на Discord-сервер, job повторяет назначение раз в пять минут и автоматически применяет все актуальные роли после его появления в guild.
Добавлен глобальный full reconcile при старте и каждые пять минут: он сравнивает фактические guild roles/members с полным active desired state, пересоздаёт удалённые managed roles, добавляет недостающие и снимает лишние managed assignments, не затрагивая посторонние роли сервера. Для пагинированного member inventory включается Server Members Intent; Gateway listener не используется.
## Подтверждённые требования
@@ -84,6 +86,6 @@ Admin UI содержит пошаговый workflow и roster editor с same-r
## Ближайший следующий шаг
Проверить миграции `006_event_workflow.sql`, `011_discord_role_sync.sql`, `012_discord_rsvp_roles.sql` и `013_discord_role_hoist.sql` с реальной PostgreSQL (`TEST_DATABASE_URL`), затем на staging проверить RSVP transitions, member-list grouping, подтверждение roster, rename/emergency substitution и cleanup Discord-ролей перед первым production-миксом.
Проверить миграции `006_event_workflow.sql` и `011``014` Discord migrations с реальной PostgreSQL (`TEST_DATABASE_URL`), затем на staging проверить RSVP transitions, member-list grouping, global reconcile, rename/emergency substitution и cleanup Discord-ролей перед первым production-миксом.
Публичная вкладка FAQ описывает регистрацию, балансировку, полномочия капитанов, выбор карт, баны героев и подсчёт Bo3.

View File

@@ -120,7 +120,9 @@ Application-слой управляет единым versioned workflow собы
После подтверждения roster PostgreSQL-backed worker синхронизирует Discord-роли по desired state. Он создаёт глобальные `Tank` / `Damage` / `Support`, временную роль с текущим `Team.Name` и отдельную `${Team.Name} Captain`; player ID связывается с Discord user ID через Account. Переименование команды и аварийная замена ставят новый reconcile job. Роли определяются по сохранённым Discord ID, поэтому одинаковые названия и повтор jobs не создают логических дубликатов.
Очередь использует `FOR UPDATE SKIP LOCKED`, timeout и retry/backoff для 429/5xx. Частичная недоступность Discord, guest или отсутствующий в guild пользователь не откатывают доменную команду; предупреждение сохраняется в job. При отмене, завершении, удалении или откате подтверждения event-specific team/captain roles удаляются. Общие ролевые назначения пересчитываются по всем другим активным миксам, поэтому параллельные события не снимают нужную роль. Role worker включается только при наличии `DISCORD_GUILD_ID` и не требует Gateway intents.
Очередь использует `FOR UPDATE SKIP LOCKED`, timeout и retry/backoff для 429/5xx. Частичная недоступность Discord, guest или отсутствующий в guild пользователь не откатывают доменную команду; предупреждение сохраняется в job. Для связанного пользователя вне guild job повторяется раз в пять минут, поэтому после его вступления актуальные назначения появляются без Gateway listener; guest без Discord ID не создаёт бесконечных retries. При отмене, завершении, удалении или откате подтверждения event-specific team/captain roles удаляются. Общие ролевые назначения пересчитываются по всем другим активным миксам, поэтому параллельные события не снимают нужную роль. Role worker включается только при наличии `DISCORD_GUILD_ID`.
При старте и затем раз в `DISCORD_GLOBAL_SYNC_INTERVAL` один bucketed `full_reconcile` job получает фактические guild roles и пагинированный список участников. Удалённые вручную managed roles пересоздаются, свойства name/hoist/position восстанавливаются, недостающие назначения добавляются, а лишние назначения только известных Mixmaker role ID снимаются. Посторонние серверные роли не изменяются. Для member inventory требуется включённый Server Members Intent, но постоянное Gateway-соединение не используется.
Тот же worker ведёт event-specific RSVP-роли независимо от наличия roster: `registered` обозначает явный ответ, а `going` / `maybe` / `not_going` взаимоисключающи и следуют текущей записи RSVP. Изменение RSVP, удаление участника и переименование Event ставят отдельный job. Roster reconcile и RSVP reconcile владеют разными role kinds и не удаляют роли друг друга; общий teardown очищает все event-specific роли.

View File

@@ -28,12 +28,14 @@
Подтверждённый Roster является источником desired state для внешних Discord-ролей. Каждый участник получает временную роль с `Team.Name`, роль своего slot-а Tank/Damage/Support, а капитан — отдельную `${Team.Name} Captain`. Discord role ID и выданные ботом назначения сохраняются отдельно от доменных Team и Account, чтобы повторная синхронизация была идемпотентной.
Team/captain roles принадлежат конкретному Event и удаляются при отмене, завершении, удалении или возврате к редактированию roster. Tank/Damage/Support общие для guild: после cleanup их участники пересчитываются по всем оставшимся активным событиям. Guest и пользователь, отсутствующий в guild, не блокируют подтверждение состава и фиксируются как предупреждение интеграции.
Team/captain roles принадлежат конкретному Event и удаляются при отмене, завершении, удалении или возврате к редактированию roster. Tank/Damage/Support общие для guild: после cleanup их участники пересчитываются по всем оставшимся активным событиям. Guest и пользователь, отсутствующий в guild, не блокируют подтверждение состава и фиксируются как предупреждение интеграции. Для связанного Discord-аккаунта вне guild синхронизация остаётся pending и автоматически выдаёт актуальные роли после его вступления.
Для каждого Event бот также поддерживает четыре временные RSVP-роли: общую `Зарегистрирован: {Event.Name}` для любого явного ответа и взаимоисключающие `Идёт`, `Возможно`, `Не идёт`. Изменение EventRegistration атомарно меняет desired status-role; удаление регистрации снимает все RSVP-назначения. Эти роли сохраняются после закрытия регистрации и удаляются вместе с другими event-specific ролями при завершении, отмене или удалении Event.
В списке участников Discord отдельными группами отображаются только принадлежность к Team и общая регистрация. Роль команды располагается выше registration-role: распределённый игрок показывается под своей командой, а нераспределённый ответивший — в группе регистрации. Ролевой slot, конкретный RSVP-статус и Captain не влияют на группировку.
Mixmaker является источником истины только для Discord role ID, сохранённых как managed. Периодическая полная сверка восстанавливает удалённые managed roles и точный набор их участников, включая снятие лишних назначений. Любые роли сервера вне managed mapping неприкосновенны.
### Жизненный цикл события
Событие проходит серверно контролируемые состояния:

View File

@@ -84,6 +84,7 @@
- обновление анонса, напоминания, публикация составов и результатов;
- [x] идемпотентная синхронизация временных Discord team/captain и Tank/Damage/Support ролей;
- [x] отдельные member-list группы для команд и зарегистрированных участников;
- [x] пятиминутная глобальная сверка и восстановление managed Discord-ролей;
- создание и очистка временных командных голосовых каналов;
- импорт данных и резервное копирование.