package store import ( "context" "database/sql" "errors" "strings" "time" ) // Баны сервера (AGENT.md 7.17): бан переживает исключение участника и // запрещает повторный вход по приглашению до снятия. // GuildBan — запись о бане. type GuildBan struct { UserID uint64 Username string DisplayName string ActorID *uint64 ActorName string Reason string CreatedAt time.Time } // BanGuildMember добавляет бан: повторный бан обновляет причину и автора. func (s *Store) BanGuildMember(ctx context.Context, guildID, userID, actorID uint64, reason string) error { _, err := s.writer.ExecContext(ctx, ` INSERT INTO guild_bans (guild_id, user_id, actor_id, reason, created_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT (guild_id, user_id) DO UPDATE SET actor_id = excluded.actor_id, reason = excluded.reason, created_at = excluded.created_at`, int64(guildID), int64(userID), int64(actorID), reason, s.Now()) return err } // UnbanGuildMember снимает бан: отсутствие бана — не ошибка (идемпотентность). func (s *Store) UnbanGuildMember(ctx context.Context, guildID, userID uint64) error { _, err := s.writer.ExecContext(ctx, `DELETE FROM guild_bans WHERE guild_id = ? AND user_id = ?`, int64(guildID), int64(userID)) return err } // IsGuildBanned сообщает, забанен ли пользователь на сервере. func (s *Store) IsGuildBanned(ctx context.Context, guildID, userID uint64) (bool, error) { var exists int err := s.reader.QueryRowContext(ctx, `SELECT 1 FROM guild_bans WHERE guild_id = ? AND user_id = ?`, int64(guildID), int64(userID)).Scan(&exists) if errors.Is(err, sql.ErrNoRows) { return false, nil } if err != nil { return false, err } return true, nil } // ListGuildBans перечисляет баны сервера: сначала свежие. func (s *Store) ListGuildBans(ctx context.Context, guildID uint64, limit int) ([]GuildBan, error) { if limit <= 0 || limit > 200 { limit = 100 } rows, err := s.reader.QueryContext(ctx, ` SELECT b.user_id, u.username, COALESCE(u.display_name, ''), b.actor_id, COALESCE(a.username, ''), b.reason, b.created_at FROM guild_bans b JOIN users u ON u.id = b.user_id LEFT JOIN users a ON a.id = b.actor_id WHERE b.guild_id = ? ORDER BY b.created_at DESC, b.user_id DESC LIMIT ?`, int64(guildID), limit) if err != nil { return nil, err } defer rows.Close() bans := make([]GuildBan, 0, limit) for rows.Next() { var ( ban GuildBan userID int64 actorID sql.NullInt64 createdAt string ) if err := rows.Scan(&userID, &ban.Username, &ban.DisplayName, &actorID, &ban.ActorName, &ban.Reason, &createdAt); err != nil { return nil, err } ban.UserID = uint64(userID) if actorID.Valid { value := uint64(actorID.Int64) ban.ActorID = &value } ban.CreatedAt = parseTimestamp(createdAt) bans = append(bans, ban) } return bans, rows.Err() } // AuditFilter — фильтры журнала аудита (AGENT.md 7.18). type AuditFilter struct { Action string ActorID uint64 TargetID uint64 BeforeID uint64 Limit int } // ListAuditLog отдаёт журнал сервера с фильтрами по действию, автору и цели. func (s *Store) ListAuditLog(ctx context.Context, guildID uint64, limit int) ([]AuditEntry, error) { return s.ListAuditLogFiltered(ctx, guildID, AuditFilter{Limit: limit}) } // ListAuditLogFiltered отдаёт журнал сервера с учётом фильтров: пустые // значения означают «без ограничения», BeforeID — постраничный просмотр. func (s *Store) ListAuditLogFiltered(ctx context.Context, guildID uint64, filter AuditFilter) ([]AuditEntry, error) { limit := filter.Limit if limit <= 0 || limit > 200 { limit = 50 } query := strings.Builder{} query.WriteString(` SELECT id, guild_id, actor_id, actor_instance_admin, action, target_type, target_id, reason, changes_json, created_at FROM audit_log WHERE guild_id = ?`) args := []any{int64(guildID)} if filter.Action != "" { query.WriteString(" AND action = ?") args = append(args, filter.Action) } if filter.ActorID != 0 { query.WriteString(" AND actor_id = ?") args = append(args, int64(filter.ActorID)) } if filter.TargetID != 0 { query.WriteString(" AND target_id = ?") args = append(args, int64(filter.TargetID)) } if filter.BeforeID != 0 { query.WriteString(" AND id < ?") args = append(args, int64(filter.BeforeID)) } query.WriteString(" ORDER BY id DESC LIMIT ?") args = append(args, limit) rows, err := s.reader.QueryContext(ctx, query.String(), args...) if err != nil { return nil, err } return scanAuditEntries(rows, limit) }