package store import ( "context" "database/sql" "time" ) // Invite — приглашение на сервер (AGENT.md 7.9). type Invite struct { Code string GuildID uint64 ChannelID *uint64 CreatorID *uint64 MaxUses int Uses int MaxAgeSec int ExpiresAt *time.Time CreatedAt time.Time } // CreateInviteParams — параметры приглашения. type CreateInviteParams struct { Code string GuildID uint64 ChannelID *uint64 CreatorID uint64 MaxUses int MaxAgeSec int } const inviteColumns = `code, guild_id, channel_id, creator_id, max_uses, uses, max_age_sec, expires_at, created_at` // CreateInvite сохраняет приглашение с кодом, лимитом использований и сроком. func (s *Store) CreateInvite(ctx context.Context, params CreateInviteParams) (*Invite, error) { var expiresAt any if params.MaxAgeSec > 0 { expiresAt = s.Timestamp(time.Now().UTC().Add(time.Duration(params.MaxAgeSec) * time.Second)) } _, err := s.writer.ExecContext(ctx, ` INSERT INTO invites (code, guild_id, channel_id, creator_id, max_uses, uses, max_age_sec, expires_at, created_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?)`, params.Code, int64(params.GuildID), nullableID(params.ChannelID), int64(params.CreatorID), params.MaxUses, params.MaxAgeSec, expiresAt, s.Now()) if err != nil { return nil, mapError(err) } return s.GetInvite(ctx, params.Code) } func (s *Store) GetInvite(ctx context.Context, code string) (*Invite, error) { row := s.reader.QueryRowContext(ctx, `SELECT `+inviteColumns+` FROM invites WHERE code = ?`, code) return scanInvite(row) } // ListGuildInvites возвращает приглашения сервера (для панели управления). func (s *Store) ListGuildInvites(ctx context.Context, guildID uint64, limit int) ([]Invite, error) { if limit <= 0 || limit > 200 { limit = 50 } rows, err := s.reader.QueryContext(ctx, ` SELECT `+inviteColumns+` FROM invites WHERE guild_id = ? ORDER BY created_at DESC LIMIT ?`, int64(guildID), limit) if err != nil { return nil, err } defer rows.Close() invites := make([]Invite, 0, limit) for rows.Next() { invite, err := scanInvite(rows) if err != nil { return nil, err } invites = append(invites, *invite) } return invites, rows.Err() } // UseInvite атомарно увеличивает счётчик использований, проверяя лимит и срок. // Возвращает ErrNotFound, если приглашение исчерпано или истекло (AGENT.md 7.9). func (s *Store) UseInvite(ctx context.Context, code string) (*Invite, error) { result, err := s.writer.ExecContext(ctx, ` UPDATE invites SET uses = uses + 1 WHERE code = ? AND (max_uses = 0 OR uses < max_uses) AND (expires_at IS NULL OR expires_at > ?)`, code, s.Now()) if err != nil { return nil, err } if affected, err := result.RowsAffected(); err == nil && affected == 0 { return nil, ErrNotFound } return s.GetInvite(ctx, code) } func (s *Store) DeleteInvite(ctx context.Context, code string) error { result, err := s.writer.ExecContext(ctx, `DELETE FROM invites WHERE code = ?`, code) if err != nil { return err } if affected, err := result.RowsAffected(); err == nil && affected == 0 { return ErrNotFound } return nil } // ListPublicGuilds возвращает публичные серверы для каталога (AGENT.md 7.20). func (s *Store) ListPublicGuilds(ctx context.Context, limit int) ([]Guild, error) { if limit <= 0 || limit > 100 { limit = 50 } rows, err := s.reader.QueryContext(ctx, ` SELECT `+guildColumns+` FROM guilds WHERE is_discoverable = 1 ORDER BY id LIMIT ?`, limit) if err != nil { return nil, err } defer rows.Close() guilds := make([]Guild, 0, limit) for rows.Next() { guild, err := scanGuild(rows) if err != nil { return nil, err } guilds = append(guilds, *guild) } return guilds, rows.Err() } func scanInvite(scanner interface{ Scan(...any) error }) (*Invite, error) { var ( invite Invite channelID sql.NullInt64 creatorID sql.NullInt64 expiresAt sql.NullString createdAt string ) err := scanner.Scan(&invite.Code, &invite.GuildID, &channelID, &creatorID, &invite.MaxUses, &invite.Uses, &invite.MaxAgeSec, &expiresAt, &createdAt) if err != nil { return nil, mapError(err) } invite.ChannelID = optionalID(channelID) invite.CreatorID = optionalID(creatorID) if expiresAt.Valid { value := parseTimestamp(expiresAt.String) invite.ExpiresAt = &value } invite.CreatedAt = parseTimestamp(createdAt) return &invite, nil }