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:
@@ -363,8 +363,12 @@ func (s *Server) registerGuildCoreRoutes(api huma.API) {
|
||||
Tags: []string{"Guilds"},
|
||||
Security: security,
|
||||
}, func(ctx context.Context, input *struct {
|
||||
GuildID string `path:"guild_id"`
|
||||
Limit int `query:"limit" default:"50" minimum:"1" maximum:"200"`
|
||||
GuildID string `path:"guild_id"`
|
||||
Limit int `query:"limit" default:"50" minimum:"1" maximum:"200"`
|
||||
Action string `query:"action" maxLength:"64"`
|
||||
ActorID string `query:"actor_id"`
|
||||
TargetID string `query:"target_id"`
|
||||
BeforeID string `query:"before_id"`
|
||||
},
|
||||
) (*auditListOutput, error) {
|
||||
user, _, err := requireUser(ctx)
|
||||
@@ -375,7 +379,25 @@ func (s *Server) registerGuildCoreRoutes(api huma.API) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := s.store.ListAuditLog(ctx, guildID, input.Limit)
|
||||
filter := store.AuditFilter{Limit: input.Limit, Action: strings.TrimSpace(input.Action)}
|
||||
for _, field := range []struct {
|
||||
raw string
|
||||
apply func(uint64)
|
||||
}{
|
||||
{input.ActorID, func(value uint64) { filter.ActorID = value }},
|
||||
{input.TargetID, func(value uint64) { filter.TargetID = value }},
|
||||
{input.BeforeID, func(value uint64) { filter.BeforeID = value }},
|
||||
} {
|
||||
if strings.TrimSpace(field.raw) == "" {
|
||||
continue
|
||||
}
|
||||
parsed, err := parseID("filter", field.raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
field.apply(parsed)
|
||||
}
|
||||
entries, err := s.store.ListAuditLogFiltered(ctx, guildID, filter)
|
||||
if err != nil {
|
||||
return nil, humaError(err)
|
||||
}
|
||||
@@ -786,11 +808,20 @@ func (s *Server) registerMemberRoutes(api huma.API) {
|
||||
return nil, humaError(err)
|
||||
}
|
||||
s.invalidateGuild(guildID)
|
||||
s.dispatchGuildEvent(guildID, "MEMBER_UPDATE", map[string]any{
|
||||
payload := map[string]any{
|
||||
"guild_id": formatSnowflake(guildID),
|
||||
"user_id": formatSnowflake(targetID),
|
||||
"nickname": nickname,
|
||||
})
|
||||
}
|
||||
// Тайм-аут показываем в событии: клиенты обновляют состояние без F5.
|
||||
if timeoutUntil != nil {
|
||||
if timeoutUntil.After(time.Now().UTC()) {
|
||||
payload["timeout_until"] = timeoutUntil.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
payload["timeout_until"] = nil
|
||||
}
|
||||
}
|
||||
s.dispatchGuildEvent(guildID, "MEMBER_UPDATE", payload)
|
||||
return newOKOutput(), nil
|
||||
})
|
||||
|
||||
@@ -1254,7 +1285,8 @@ func memberFromProfile(profile store.GuildMemberProfile) memberPayload {
|
||||
if profile.AvatarFileID != nil {
|
||||
payload.AvatarFileID = formatSnowflake(*profile.AvatarFileID)
|
||||
}
|
||||
if profile.TimeoutUntil != nil {
|
||||
// Истёкший тайм-аут не показываем: он уже не действует.
|
||||
if profile.TimeoutUntil != nil && profile.TimeoutUntil.After(time.Now().UTC()) {
|
||||
payload.TimeoutUntil = profile.TimeoutUntil.UTC().Format(time.RFC3339)
|
||||
}
|
||||
for _, roleID := range profile.RoleIDs {
|
||||
|
||||
Reference in New Issue
Block a user