feat(moderation): баны сервера, тайм-ауты и фильтры журнала аудита

Фаза 4 (AGENT.md 7.17, 7.18): модерация участников.

- миграция 00011: таблица `guild_bans` (бан переживает исключение участника);
- `PUT /guilds/{id}/bans/{user_id}` — бан с причиной и права `BAN_MEMBERS`:
  исключает участника, отключает его от голосовой комнаты, пишет системное
  сообщение, аудит и события `MEMBER_REMOVE`/`GUILD_DELETE`;
- `DELETE /guilds/{id}/bans/{user_id}` — снятие бана, `GET .../bans` — список
  с автором и причиной (только модераторам);
- вход по приглашению для забаненного отклоняется с `guild.banned`;
- тайм-аут уже был в схеме, но не работал: кэш прав комнат не сбрасывался при
  изменениях сервера, поэтому участник с тайм-аутом продолжал писать. Ключ
  кэша комнат теперь включает сервер, `InvalidateGuild` чистит и его;
- тайм-аут уходит в `MEMBER_UPDATE` (и снимается событием), истёкшие
  тайм-ауты не показываются в профиле участника;
- журнал аудита получил фильтры `action`, `actor_id`, `target_id`, `before_id`;
- тесты: полный цикл бана (включая запрет входа и разбан), права и иерархия,
  блокировка сообщений тайм-аутом, запрет тайм-аута на себя, фильтры аудита.
This commit is contained in:
2026-09-20 22:38:48 +03:00
parent cc16c18b13
commit 4fb3509128
10 changed files with 756 additions and 28 deletions
+15 -5
View File
@@ -21,6 +21,9 @@ type guildKey struct {
}
type channelKey struct {
// guildID нужен, чтобы сброс сервера (роли, тайм-аут, оверрайды) сбрасывал
// и кэш его комнат: иначе участник с тайм-аутом продолжал писать.
guildID uint64
channelID uint64
userID uint64
}
@@ -69,10 +72,10 @@ func (c *Cache) PutGuild(guildID, userID uint64, value Resolved) {
c.evictLocked()
}
func (c *Cache) GetChannel(channelID, userID uint64) (Resolved, bool) {
func (c *Cache) GetChannel(guildID, channelID, userID uint64) (Resolved, bool) {
c.mu.Lock()
defer c.mu.Unlock()
element, ok := c.channels[channelKey{channelID: channelID, userID: userID}]
element, ok := c.channels[channelKey{guildID: guildID, channelID: channelID, userID: userID}]
if !ok {
return Resolved{}, false
}
@@ -80,10 +83,10 @@ func (c *Cache) GetChannel(channelID, userID uint64) (Resolved, bool) {
return element.Value.(*cacheEntry).value, true
}
func (c *Cache) PutChannel(channelID, userID uint64, value Resolved) {
func (c *Cache) PutChannel(guildID, channelID, userID uint64, value Resolved) {
c.mu.Lock()
defer c.mu.Unlock()
key := channelKey{channelID: channelID, userID: userID}
key := channelKey{guildID: guildID, channelID: channelID, userID: userID}
if element, ok := c.channels[key]; ok {
element.Value.(*cacheEntry).value = value
c.order.MoveToFront(element)
@@ -94,7 +97,8 @@ func (c *Cache) PutChannel(channelID, userID uint64, value Resolved) {
c.evictLocked()
}
// InvalidateGuild сбрасывает права всех пользователей сервера.
// InvalidateGuild сбрасывает права всех пользователей сервера, включая кэш
// его комнат: роли, тайм-аут и оверрайды влияют и на права в комнатах.
func (c *Cache) InvalidateGuild(guildID uint64) {
c.mu.Lock()
defer c.mu.Unlock()
@@ -104,6 +108,12 @@ func (c *Cache) InvalidateGuild(guildID uint64) {
delete(c.items, key)
}
}
for key, element := range c.channels {
if key.guildID == guildID {
c.order.Remove(element)
delete(c.channels, key)
}
}
}
// InvalidateChannel сбрасывает кэш комнаты.
+4 -4
View File
@@ -8,7 +8,7 @@ import (
// Channel вычисляет права пользователя в конкретной комнате: базовые права
// сервера, затем оверрайды ролей, затем оверрайд пользователя (AGENT.md 6.2).
func (c *Calculator) Channel(ctx context.Context, guildID, channelID, userID uint64, instanceAdmin bool) (Resolved, error) {
if cached, ok := c.cache.GetChannel(channelID, userID); ok {
if cached, ok := c.cache.GetChannel(guildID, channelID, userID); ok {
cached.IsInstanceAdmin = instanceAdmin
return cached, nil
}
@@ -20,11 +20,11 @@ func (c *Calculator) Channel(ctx context.Context, guildID, channelID, userID uin
resolved.Channel = resolved.Guild
if !resolved.IsMember && !resolved.IsInstanceAdmin {
// Не участник не видит ничего, даже если есть персональный оверрайд.
c.cache.PutChannel(channelID, userID, resolved)
c.cache.PutChannel(guildID, channelID, userID, resolved)
return resolved, nil
}
if resolved.IsInstanceAdmin || resolved.Guild.Has(Administrator) {
c.cache.PutChannel(channelID, userID, resolved)
c.cache.PutChannel(guildID, channelID, userID, resolved)
return resolved, nil
}
@@ -78,6 +78,6 @@ func (c *Calculator) Channel(ctx context.Context, guildID, channelID, userID uin
}
}
resolved.Channel = (computed &^ userDeny) | userAllow
c.cache.PutChannel(channelID, userID, resolved)
c.cache.PutChannel(guildID, channelID, userID, resolved)
return resolved, nil
}