Files
glchat/internal/store/bans.go
T

153 lines
4.9 KiB
Go
Raw Normal View History

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)
}