package store import ( "context" "time" ) // VoiceState — присутствие пользователя в голосовой комнате (AGENT.md 7.14). type VoiceState struct { UserID uint64 GuildID uint64 ChannelID uint64 SessionID string SelfMute bool SelfDeaf bool ServerMute bool ServerDeaf bool Camera bool Screen bool JoinedAt time.Time UpdatedAt time.Time } // VoiceStateParams — параметры записи голосового состояния. type VoiceStateParams struct { UserID uint64 GuildID uint64 ChannelID uint64 SessionID string SelfMute bool SelfDeaf bool ServerMute bool ServerDeaf bool Camera bool Screen bool } const voiceStateColumns = `user_id, guild_id, channel_id, session_id, self_mute, self_deaf, server_mute, server_deaf, camera, screen, joined_at, updated_at` // UpsertVoiceState записывает состояние: пользователь входит в комнату или // обновляет флаги (микрофон, камера, шаринг экрана). func (s *Store) UpsertVoiceState(ctx context.Context, params VoiceStateParams) (*VoiceState, error) { _, err := s.writer.ExecContext(ctx, ` INSERT INTO voice_states (user_id, guild_id, channel_id, session_id, self_mute, self_deaf, server_mute, server_deaf, camera, screen, joined_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (user_id, guild_id) DO UPDATE SET channel_id = excluded.channel_id, session_id = excluded.session_id, self_mute = excluded.self_mute, self_deaf = excluded.self_deaf, server_mute = excluded.server_mute, server_deaf = excluded.server_deaf, camera = excluded.camera, screen = excluded.screen, updated_at = excluded.updated_at`, int64(params.UserID), int64(params.GuildID), int64(params.ChannelID), params.SessionID, boolToInt(params.SelfMute), boolToInt(params.SelfDeaf), boolToInt(params.ServerMute), boolToInt(params.ServerDeaf), boolToInt(params.Camera), boolToInt(params.Screen), s.Now(), s.Now()) if err != nil { return nil, mapError(err) } return s.GetVoiceState(ctx, params.GuildID, params.UserID) } // GetVoiceState возвращает состояние пользователя в сервере. func (s *Store) GetVoiceState(ctx context.Context, guildID, userID uint64) (*VoiceState, error) { row := s.reader.QueryRowContext(ctx, `SELECT `+voiceStateColumns+` FROM voice_states WHERE guild_id = ? AND user_id = ?`, int64(guildID), int64(userID)) return scanVoiceState(row) } // ListVoiceStates отдаёт состояния всех участников сервера. func (s *Store) ListVoiceStates(ctx context.Context, guildID uint64) ([]VoiceState, error) { rows, err := s.reader.QueryContext(ctx, `SELECT `+voiceStateColumns+` FROM voice_states WHERE guild_id = ? ORDER BY joined_at`, int64(guildID)) if err != nil { return nil, err } defer rows.Close() states := make([]VoiceState, 0, 8) for rows.Next() { state, err := scanVoiceState(rows) if err != nil { return nil, err } states = append(states, *state) } return states, rows.Err() } // CountChannelVoiceStates считает участников комнаты (лимит user_limit). func (s *Store) CountChannelVoiceStates(ctx context.Context, channelID uint64) (int, error) { var count int err := s.reader.QueryRowContext(ctx, `SELECT COUNT(*) FROM voice_states WHERE channel_id = ?`, int64(channelID)).Scan(&count) return count, err } // DeleteVoiceState убирает пользователя из голосовой комнаты. func (s *Store) DeleteVoiceState(ctx context.Context, guildID, userID uint64) error { _, err := s.writer.ExecContext(ctx, `DELETE FROM voice_states WHERE guild_id = ? AND user_id = ?`, int64(guildID), int64(userID)) return err } // DeleteVoiceStatesForChannel убирает всех из комнаты (при её удалении). func (s *Store) DeleteVoiceStatesForChannel(ctx context.Context, channelID uint64) error { _, err := s.writer.ExecContext(ctx, `DELETE FROM voice_states WHERE channel_id = ?`, int64(channelID)) return err } func scanVoiceState(scanner interface{ Scan(...any) error }) (*VoiceState, error) { var ( state VoiceState selfMute, selfDeaf int serverMute, serverDeaf int camera, screen int joinedAt, updatedAt string ) err := scanner.Scan(&state.UserID, &state.GuildID, &state.ChannelID, &state.SessionID, &selfMute, &selfDeaf, &serverMute, &serverDeaf, &camera, &screen, &joinedAt, &updatedAt) if err != nil { return nil, mapError(err) } state.SelfMute = selfMute == 1 state.SelfDeaf = selfDeaf == 1 state.ServerMute = serverMute == 1 state.ServerDeaf = serverDeaf == 1 state.Camera = camera == 1 state.Screen = screen == 1 state.JoinedAt = parseTimestamp(joinedAt) state.UpdatedAt = parseTimestamp(updatedAt) return &state, nil }