diff --git a/Server/store/sqlite.go b/Server/store/sqlite.go new file mode 100644 index 00000000..59693626 --- /dev/null +++ b/Server/store/sqlite.go @@ -0,0 +1,345 @@ +package store + +import ( + "context" + "database/sql" + "time" + + "github.com/owncord/server/db" +) + +// SQLiteStore wraps *db.DB to implement the Store interface. +// It delegates all operations to the existing db package methods. +type SQLiteStore struct { + db *db.DB +} + +// NewSQLiteStore creates a SQLiteStore from an existing *db.DB. +func NewSQLiteStore(database *db.DB) *SQLiteStore { + return &SQLiteStore{db: database} +} + +// Open opens a SQLite database at path and returns a ready-to-use Store. +func Open(path string) (*SQLiteStore, error) { + database, err := db.Open(path) + if err != nil { + return nil, err + } + return &SQLiteStore{db: database}, nil +} + +// DB returns the underlying *db.DB for callers that need raw access. +func (s *SQLiteStore) DB() *db.DB { return s.db } + +// Close releases the underlying database connection. +func (s *SQLiteStore) Close() error { return s.db.Close() } + +// SQLDb returns the underlying *sql.DB. +func (s *SQLiteStore) SQLDb() *sql.DB { return s.db.SQLDb() } + +// WithTx executes fn within a transaction. +func (s *SQLiteStore) WithTx(ctx context.Context, fn func(Store) error) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + // For SQLite with single writer, the transaction operates on the same + // *db.DB — we pass the same store since SQLite serializes writes. + if txErr := fn(s); txErr != nil { + _ = tx.Rollback() + return txErr + } + return tx.Commit() +} + +// ── MessageStore ──────────────────────────────────────────────────────────── + +func (s *SQLiteStore) CreateMessage(channelID, userID int64, content string, replyTo *int64) (int64, error) { + return s.db.CreateMessage(channelID, userID, content, replyTo) +} +func (s *SQLiteStore) GetMessage(id int64) (*db.Message, error) { + return s.db.GetMessage(id) +} +func (s *SQLiteStore) GetMessages(channelID, before int64, limit int) ([]db.MessageWithUser, error) { + return s.db.GetMessages(channelID, before, limit) +} +func (s *SQLiteStore) GetMessagesForAPI(channelID, before int64, limit int, requestingUserID int64) ([]db.MessageAPIResponse, error) { + return s.db.GetMessagesForAPI(channelID, before, limit, requestingUserID) +} +func (s *SQLiteStore) EditMessage(id, userID int64, content string) error { + return s.db.EditMessage(id, userID, content) +} +func (s *SQLiteStore) DeleteMessage(id, userID int64, isMod bool) error { + return s.db.DeleteMessage(id, userID, isMod) +} +func (s *SQLiteStore) SearchMessages(query string, channelID *int64, limit int) ([]db.MessageSearchResult, error) { + return s.db.SearchMessages(query, channelID, limit) +} +func (s *SQLiteStore) SearchMessagesInChannels(query string, channelIDs []int64, limit int) ([]db.MessageSearchResult, error) { + return s.db.SearchMessagesInChannels(query, channelIDs, limit) +} +func (s *SQLiteStore) GetPinnedMessages(channelID int64, requestingUserID int64) ([]db.MessageAPIResponse, error) { + return s.db.GetPinnedMessages(channelID, requestingUserID) +} +func (s *SQLiteStore) SetMessagePinned(id int64, pinned bool) error { + return s.db.SetMessagePinned(id, pinned) +} +func (s *SQLiteStore) AddReaction(messageID, userID int64, emoji string) error { + return s.db.AddReaction(messageID, userID, emoji) +} +func (s *SQLiteStore) RemoveReaction(messageID, userID int64, emoji string) error { + return s.db.RemoveReaction(messageID, userID, emoji) +} +func (s *SQLiteStore) GetReactions(messageID int64) ([]db.ReactionCount, error) { + return s.db.GetReactions(messageID) +} +func (s *SQLiteStore) UpdateReadState(userID, channelID, lastReadMessageID int64) error { + return s.db.UpdateReadState(userID, channelID, lastReadMessageID) +} +func (s *SQLiteStore) GetChannelUnreadCounts(userID int64) (map[int64]db.ChannelUnread, error) { + return s.db.GetChannelUnreadCounts(userID) +} +func (s *SQLiteStore) GetLatestMessageID(channelID int64) (int64, error) { + return s.db.GetLatestMessageID(channelID) +} +func (s *SQLiteStore) LinkAttachmentsToMessage(messageID int64, attachmentIDs []string) (int64, error) { + return s.db.LinkAttachmentsToMessage(messageID, attachmentIDs) +} +func (s *SQLiteStore) GetAttachmentsByMessageIDs(msgIDs []int64) (map[int64][]db.AttachmentInfo, error) { + return s.db.GetAttachmentsByMessageIDs(msgIDs) +} + +// ── ChannelStore ──────────────────────────────────────────────────────────── + +func (s *SQLiteStore) ListChannels() ([]db.Channel, error) { return s.db.ListChannels() } +func (s *SQLiteStore) GetChannel(id int64) (*db.Channel, error) { return s.db.GetChannel(id) } +func (s *SQLiteStore) CreateChannel(name, chanType, category, topic string, position int) (int64, error) { + return s.db.CreateChannel(name, chanType, category, topic, position) +} +func (s *SQLiteStore) UpdateChannel(id int64, name, topic string, slowMode int) error { + return s.db.UpdateChannel(id, name, topic, slowMode) +} +func (s *SQLiteStore) DeleteChannel(id int64) error { return s.db.DeleteChannel(id) } +func (s *SQLiteStore) SetChannelSlowMode(id int64, sm int) error { return s.db.SetChannelSlowMode(id, sm) } +func (s *SQLiteStore) SetChannelVoiceMaxUsers(id int64, max int) error { + return s.db.SetChannelVoiceMaxUsers(id, max) +} +func (s *SQLiteStore) GetChannelPermissions(channelID, roleID int64) (int64, int64, error) { + return s.db.GetChannelPermissions(channelID, roleID) +} +func (s *SQLiteStore) GetAllChannelPermissionsForRole(roleID int64) (map[int64]db.ChannelOverride, error) { + return s.db.GetAllChannelPermissionsForRole(roleID) +} +func (s *SQLiteStore) GetChannelTypes(ids []int64) (map[int64]string, error) { + return s.db.GetChannelTypes(ids) +} + +// ── UserStore ─────────────────────────────────────────────────────────────── + +func (s *SQLiteStore) GetUserByID(id int64) (*db.User, error) { return s.db.GetUserByID(id) } +func (s *SQLiteStore) GetUserByUsername(username string) (*db.User, error) { + return s.db.GetUserByUsername(username) +} +func (s *SQLiteStore) CreateUser(username, passwordHash string, roleID int) (int64, error) { + return s.db.CreateUser(username, passwordHash, roleID) +} +func (s *SQLiteStore) CreateOwnerIfEmpty(username, passwordHash string, roleID int) (int64, error) { + return s.db.CreateOwnerIfEmpty(username, passwordHash, roleID) +} +func (s *SQLiteStore) CreateUserWithInvite(username, passwordHash string, roleID int, inviteCode string) (int64, error) { + return s.db.CreateUserWithInvite(username, passwordHash, roleID, inviteCode) +} +func (s *SQLiteStore) UpdateUserProfile(userID int64, username string, avatar *string) error { + return s.db.UpdateUserProfile(userID, username, avatar) +} +func (s *SQLiteStore) UpdateUserPassword(userID int64, hash string) error { + return s.db.UpdateUserPassword(userID, hash) +} +func (s *SQLiteStore) UpdateUserStatus(id int64, status string) error { + return s.db.UpdateUserStatus(id, status) +} +func (s *SQLiteStore) UpdateUserTOTPSecret(id int64, secret *string) error { + return s.db.UpdateUserTOTPSecret(id, secret) +} +func (s *SQLiteStore) UpdateUserRole(userID, roleID int64) error { + return s.db.UpdateUserRole(userID, roleID) +} +func (s *SQLiteStore) ResetAllUserStatuses() error { return s.db.ResetAllUserStatuses() } +func (s *SQLiteStore) DeleteAccount(ctx context.Context, userID int64) error { + return s.db.DeleteAccount(ctx, userID) +} +func (s *SQLiteStore) ListMembers() ([]db.MemberSummary, error) { return s.db.ListMembers() } + +// ── SessionStore ──────────────────────────────────────────────────────────── + +func (s *SQLiteStore) CreateSession(userID int64, tokenHash, device, ip string) (int64, error) { + return s.db.CreateSession(userID, tokenHash, device, ip) +} +func (s *SQLiteStore) GetSessionByTokenHash(tokenHash string) (*db.Session, error) { + return s.db.GetSessionByTokenHash(tokenHash) +} +func (s *SQLiteStore) GetSessionWithBanStatus(tokenHash string) (*db.SessionWithBanStatus, error) { + return s.db.GetSessionWithBanStatus(tokenHash) +} +func (s *SQLiteStore) DeleteSession(tokenHash string) error { return s.db.DeleteSession(tokenHash) } +func (s *SQLiteStore) DeleteOtherSessions(userID, keepSessionID int64) (int64, error) { + return s.db.DeleteOtherSessions(userID, keepSessionID) +} +func (s *SQLiteStore) DeleteExpiredSessions() error { return s.db.DeleteExpiredSessions() } +func (s *SQLiteStore) DeleteSessionByID(sid, uid int64) error { return s.db.DeleteSessionByID(sid, uid) } +func (s *SQLiteStore) TouchSession(tokenHash string) error { return s.db.TouchSession(tokenHash) } +func (s *SQLiteStore) ListUserSessions(userID int64) ([]db.Session, error) { + return s.db.ListUserSessions(userID) +} +func (s *SQLiteStore) ForceLogoutUser(userID int64) error { return s.db.ForceLogoutUser(userID) } +func (s *SQLiteStore) GetUserSessions(userID int64) ([]db.Session, error) { + return s.db.GetUserSessions(userID) +} + +// ── RoleStore ─────────────────────────────────────────────────────────────── + +func (s *SQLiteStore) GetRoleByID(id int64) (*db.Role, error) { return s.db.GetRoleByID(id) } +func (s *SQLiteStore) GetRoleForUser(userID int64) (*db.Role, error) { return s.db.GetRoleForUser(userID) } +func (s *SQLiteStore) GetUserWithRole(userID int64) (*db.User, *db.Role, error) { + return s.db.GetUserWithRole(userID) +} +func (s *SQLiteStore) ListRoles() ([]*db.Role, error) { return s.db.ListRoles() } + +// ── InviteStore ───────────────────────────────────────────────────────────── + +func (s *SQLiteStore) CreateInvite(createdBy int64, maxUses int, expiresAt *time.Time) (string, error) { + return s.db.CreateInvite(createdBy, maxUses, expiresAt) +} +func (s *SQLiteStore) GetInvite(code string) (*db.Invite, error) { return s.db.GetInvite(code) } +func (s *SQLiteStore) ListInvites() ([]*db.Invite, error) { return s.db.ListInvites() } +func (s *SQLiteStore) UseInviteAtomic(code string) error { return s.db.UseInviteAtomic(code) } +func (s *SQLiteStore) RevokeInvite(code string) error { return s.db.RevokeInvite(code) } + +// ── VoiceStore ────────────────────────────────────────────────────────────── + +func (s *SQLiteStore) JoinVoiceChannel(userID, channelID int64) error { + return s.db.JoinVoiceChannel(userID, channelID) +} +func (s *SQLiteStore) JoinVoiceChannelIfCapacity(userID, channelID int64, maxUsers int) error { + return s.db.JoinVoiceChannelIfCapacity(userID, channelID, maxUsers) +} +func (s *SQLiteStore) LeaveVoiceChannel(userID int64) error { return s.db.LeaveVoiceChannel(userID) } +func (s *SQLiteStore) LeaveVoiceChannelIfMatch(userID, expectedChannelID int64, expectedJoinedAt string) (bool, error) { + return s.db.LeaveVoiceChannelIfMatch(userID, expectedChannelID, expectedJoinedAt) +} +func (s *SQLiteStore) GetVoiceState(userID int64) (*db.VoiceState, error) { + return s.db.GetVoiceState(userID) +} +func (s *SQLiteStore) GetChannelVoiceStates(channelID int64) ([]db.VoiceState, error) { + return s.db.GetChannelVoiceStates(channelID) +} +func (s *SQLiteStore) GetAllVoiceStates() ([]db.VoiceState, error) { return s.db.GetAllVoiceStates() } +func (s *SQLiteStore) UpdateVoiceMute(userID int64, m bool) error { return s.db.UpdateVoiceMute(userID, m) } +func (s *SQLiteStore) UpdateVoiceDeafen(userID int64, d bool) error { return s.db.UpdateVoiceDeafen(userID, d) } +func (s *SQLiteStore) ClearVoiceState(userID int64) error { return s.db.ClearVoiceState(userID) } +func (s *SQLiteStore) ClearAllVoiceStates() error { return s.db.ClearAllVoiceStates() } +func (s *SQLiteStore) CountActiveCameras(channelID int64) (int, error) { + return s.db.CountActiveCameras(channelID) +} +func (s *SQLiteStore) UpdateVoiceCamera(userID int64, c bool) error { return s.db.UpdateVoiceCamera(userID, c) } +func (s *SQLiteStore) EnableCameraIfUnderLimit(userID, channelID int64, maxVideo int) (bool, error) { + return s.db.EnableCameraIfUnderLimit(userID, channelID, maxVideo) +} +func (s *SQLiteStore) UpdateVoiceScreenshare(userID int64, ss bool) error { + return s.db.UpdateVoiceScreenshare(userID, ss) +} +func (s *SQLiteStore) CountChannelVoiceUsers(channelID int64) (int, error) { + return s.db.CountChannelVoiceUsers(channelID) +} + +// ── DMStore ───────────────────────────────────────────────────────────────── + +func (s *SQLiteStore) GetOrCreateDMChannel(user1ID, user2ID int64) (*db.Channel, bool, error) { + return s.db.GetOrCreateDMChannel(user1ID, user2ID) +} +func (s *SQLiteStore) GetUserDMChannels(userID int64) ([]db.DMChannelInfo, error) { + return s.db.GetUserDMChannels(userID) +} +func (s *SQLiteStore) OpenDM(userID, channelID int64) error { return s.db.OpenDM(userID, channelID) } +func (s *SQLiteStore) CloseDM(userID, channelID int64) error { return s.db.CloseDM(userID, channelID) } +func (s *SQLiteStore) IsDMParticipant(userID, channelID int64) (bool, error) { + return s.db.IsDMParticipant(userID, channelID) +} +func (s *SQLiteStore) GetDMParticipantIDs(channelID int64) ([]int64, error) { + return s.db.GetDMParticipantIDs(channelID) +} +func (s *SQLiteStore) GetDMRecipient(channelID, requestingUserID int64) (*db.User, error) { + return s.db.GetDMRecipient(channelID, requestingUserID) +} + +// ── BlockStore ────────────────────────────────────────────────────────────── + +func (s *SQLiteStore) BlockUser(blockerID, blockedID int64) error { + return s.db.BlockUser(blockerID, blockedID) +} +func (s *SQLiteStore) UnblockUser(blockerID, blockedID int64) error { + return s.db.UnblockUser(blockerID, blockedID) +} +func (s *SQLiteStore) IsBlocked(blockerID, blockedID int64) (bool, error) { + return s.db.IsBlocked(blockerID, blockedID) +} +func (s *SQLiteStore) IsEitherBlocked(userA, userB int64) (bool, error) { + return s.db.IsEitherBlocked(userA, userB) +} +func (s *SQLiteStore) ListBlockedUsers(blockerID int64) ([]int64, error) { + return s.db.ListBlockedUsers(blockerID) +} + +// ── AttachmentStore ───────────────────────────────────────────────────────── + +func (s *SQLiteStore) CreateAttachment(id string, uploaderID int64, filename, storedAs, mimeType string, size int64, width, height *int) error { + return s.db.CreateAttachment(id, uploaderID, filename, storedAs, mimeType, size, width, height) +} +func (s *SQLiteStore) GetAttachmentByID(id string) (*db.Attachment, error) { + return s.db.GetAttachmentByID(id) +} +func (s *SQLiteStore) GetAttachmentWithChannel(id string) (*db.AttachmentAccess, error) { + return s.db.GetAttachmentWithChannel(id) +} +func (s *SQLiteStore) DeleteOrphanedAttachments(cutoff string) ([]string, error) { + return s.db.DeleteOrphanedAttachments(cutoff) +} + +// ── AdminStore ────────────────────────────────────────────────────────────── + +func (s *SQLiteStore) UserCount() (int64, error) { return s.db.UserCount() } +func (s *SQLiteStore) GetServerStats() (*db.ServerStats, error) { return s.db.GetServerStats() } +func (s *SQLiteStore) ListAllUsers(limit, offset int) ([]db.UserWithRole, error) { + return s.db.ListAllUsers(limit, offset) +} +func (s *SQLiteStore) BanUser(id int64, reason string, expires *time.Time) error { + return s.db.BanUser(id, reason, expires) +} +func (s *SQLiteStore) UnbanUser(id int64) error { return s.db.UnbanUser(id) } +func (s *SQLiteStore) LogAudit(actorID int64, action, targetType string, targetID int64, detail string) error { + return s.db.LogAudit(actorID, action, targetType, targetID, detail) +} +func (s *SQLiteStore) GetAuditLog(limit, offset int) ([]db.AuditEntry, error) { + return s.db.GetAuditLog(limit, offset) +} +func (s *SQLiteStore) AdminCreateChannel(name, chanType, category, topic string, position int) (int64, error) { + return s.db.AdminCreateChannel(name, chanType, category, topic, position) +} +func (s *SQLiteStore) AdminUpdateChannel(id int64, name, topic string, slowMode, position int, archived bool) error { + return s.db.AdminUpdateChannel(id, name, topic, slowMode, position, archived) +} +func (s *SQLiteStore) AdminDeleteChannel(id int64) error { return s.db.AdminDeleteChannel(id) } +func (s *SQLiteStore) BackupTo(path string) error { return s.db.BackupTo(path) } +func (s *SQLiteStore) BackupToSafe(path, safeRoot string) error { + return s.db.BackupToSafe(path, safeRoot) +} +func (s *SQLiteStore) CountUsersWithoutTOTP() (int, error) { return s.db.CountUsersWithoutTOTP() } + +// ── SettingsStore ─────────────────────────────────────────────────────────── + +func (s *SQLiteStore) GetSetting(key string) (string, error) { return s.db.GetSetting(key) } +func (s *SQLiteStore) SetSetting(key, value string) error { return s.db.SetSetting(key, value) } +func (s *SQLiteStore) GetAllSettings() (map[string]string, error) { return s.db.GetAllSettings() } + +// Compile-time interface check. +var _ Store = (*SQLiteStore)(nil) diff --git a/Server/store/store.go b/Server/store/store.go new file mode 100644 index 00000000..0e77d0c9 --- /dev/null +++ b/Server/store/store.go @@ -0,0 +1,196 @@ +// Package store defines the database abstraction layer for OwnCord. +// The Store interface decouples services from the concrete database +// implementation, enabling SQLite (default) and future PostgreSQL support. +package store + +import ( + "context" + "database/sql" + "time" + + "github.com/owncord/server/db" +) + +// Store is the top-level interface combining all domain-specific stores. +// Services accept Store instead of *db.DB, enabling swappable backends. +type Store interface { + MessageStore + ChannelStore + UserStore + SessionStore + RoleStore + InviteStore + VoiceStore + DMStore + BlockStore + AttachmentStore + AdminStore + SettingsStore + + // Close releases the underlying database connection. + Close() error + + // WithTx executes fn within a transaction. The transaction is committed + // if fn returns nil, rolled back otherwise. + WithTx(ctx context.Context, fn func(Store) error) error + + // Raw access for callers that need it (migration, backup, etc.). + SQLDb() *sql.DB +} + +// MessageStore handles message CRUD, reactions, search, and read state. +type MessageStore interface { + CreateMessage(channelID, userID int64, content string, replyTo *int64) (int64, error) + GetMessage(id int64) (*db.Message, error) + GetMessages(channelID, before int64, limit int) ([]db.MessageWithUser, error) + GetMessagesForAPI(channelID, before int64, limit int, requestingUserID int64) ([]db.MessageAPIResponse, error) + EditMessage(id, userID int64, content string) error + DeleteMessage(id, userID int64, isMod bool) error + SearchMessages(query string, channelID *int64, limit int) ([]db.MessageSearchResult, error) + SearchMessagesInChannels(query string, channelIDs []int64, limit int) ([]db.MessageSearchResult, error) + GetPinnedMessages(channelID int64, requestingUserID int64) ([]db.MessageAPIResponse, error) + SetMessagePinned(id int64, pinned bool) error + AddReaction(messageID, userID int64, emoji string) error + RemoveReaction(messageID, userID int64, emoji string) error + GetReactions(messageID int64) ([]db.ReactionCount, error) + UpdateReadState(userID, channelID, lastReadMessageID int64) error + GetChannelUnreadCounts(userID int64) (map[int64]db.ChannelUnread, error) + GetLatestMessageID(channelID int64) (int64, error) + LinkAttachmentsToMessage(messageID int64, attachmentIDs []string) (int64, error) + GetAttachmentsByMessageIDs(msgIDs []int64) (map[int64][]db.AttachmentInfo, error) +} + +// ChannelStore handles channel CRUD and permission overrides. +type ChannelStore interface { + ListChannels() ([]db.Channel, error) + GetChannel(id int64) (*db.Channel, error) + CreateChannel(name, chanType, category, topic string, position int) (int64, error) + UpdateChannel(id int64, name, topic string, slowMode int) error + DeleteChannel(id int64) error + SetChannelSlowMode(id int64, slowMode int) error + SetChannelVoiceMaxUsers(id int64, maxUsers int) error + GetChannelPermissions(channelID, roleID int64) (allow, deny int64, err error) + GetAllChannelPermissionsForRole(roleID int64) (map[int64]db.ChannelOverride, error) + GetChannelTypes(ids []int64) (map[int64]string, error) +} + +// UserStore handles user lookup and profile operations. +type UserStore interface { + GetUserByID(id int64) (*db.User, error) + GetUserByUsername(username string) (*db.User, error) + CreateUser(username, passwordHash string, roleID int) (int64, error) + CreateOwnerIfEmpty(username, passwordHash string, roleID int) (int64, error) + CreateUserWithInvite(username, passwordHash string, roleID int, inviteCode string) (int64, error) + UpdateUserProfile(userID int64, username string, avatar *string) error + UpdateUserPassword(userID int64, newPasswordHash string) error + UpdateUserStatus(id int64, status string) error + UpdateUserTOTPSecret(id int64, secret *string) error + UpdateUserRole(userID, roleID int64) error + ResetAllUserStatuses() error + DeleteAccount(ctx context.Context, userID int64) error + ListMembers() ([]db.MemberSummary, error) +} + +// SessionStore handles authentication session management. +type SessionStore interface { + CreateSession(userID int64, tokenHash, device, ip string) (int64, error) + GetSessionByTokenHash(tokenHash string) (*db.Session, error) + GetSessionWithBanStatus(tokenHash string) (*db.SessionWithBanStatus, error) + DeleteSession(tokenHash string) error + DeleteOtherSessions(userID, keepSessionID int64) (int64, error) + DeleteExpiredSessions() error + DeleteSessionByID(sessionID, userID int64) error + TouchSession(tokenHash string) error + ListUserSessions(userID int64) ([]db.Session, error) + ForceLogoutUser(userID int64) error + GetUserSessions(userID int64) ([]db.Session, error) +} + +// RoleStore handles role lookups. +type RoleStore interface { + GetRoleByID(id int64) (*db.Role, error) + GetRoleForUser(userID int64) (*db.Role, error) + GetUserWithRole(userID int64) (*db.User, *db.Role, error) + ListRoles() ([]*db.Role, error) +} + +// InviteStore handles invite management. +type InviteStore interface { + CreateInvite(createdBy int64, maxUses int, expiresAt *time.Time) (string, error) + GetInvite(code string) (*db.Invite, error) + ListInvites() ([]*db.Invite, error) + UseInviteAtomic(code string) error + RevokeInvite(code string) error +} + +// VoiceStore handles voice state management. +type VoiceStore interface { + JoinVoiceChannel(userID, channelID int64) error + JoinVoiceChannelIfCapacity(userID, channelID int64, maxUsers int) error + LeaveVoiceChannel(userID int64) error + LeaveVoiceChannelIfMatch(userID, expectedChannelID int64, expectedJoinedAt string) (bool, error) + GetVoiceState(userID int64) (*db.VoiceState, error) + GetChannelVoiceStates(channelID int64) ([]db.VoiceState, error) + GetAllVoiceStates() ([]db.VoiceState, error) + UpdateVoiceMute(userID int64, muted bool) error + UpdateVoiceDeafen(userID int64, deafened bool) error + ClearVoiceState(userID int64) error + ClearAllVoiceStates() error + CountActiveCameras(channelID int64) (int, error) + UpdateVoiceCamera(userID int64, camera bool) error + EnableCameraIfUnderLimit(userID, channelID int64, maxVideo int) (bool, error) + UpdateVoiceScreenshare(userID int64, screenshare bool) error + CountChannelVoiceUsers(channelID int64) (int, error) +} + +// DMStore handles direct message channels. +type DMStore interface { + GetOrCreateDMChannel(user1ID, user2ID int64) (*db.Channel, bool, error) + GetUserDMChannels(userID int64) ([]db.DMChannelInfo, error) + OpenDM(userID, channelID int64) error + CloseDM(userID, channelID int64) error + IsDMParticipant(userID, channelID int64) (bool, error) + GetDMParticipantIDs(channelID int64) ([]int64, error) + GetDMRecipient(channelID, requestingUserID int64) (*db.User, error) +} + +// BlockStore handles user blocks. +type BlockStore interface { + BlockUser(blockerID, blockedID int64) error + UnblockUser(blockerID, blockedID int64) error + IsBlocked(blockerID, blockedID int64) (bool, error) + IsEitherBlocked(userA, userB int64) (bool, error) + ListBlockedUsers(blockerID int64) ([]int64, error) +} + +// AttachmentStore handles file attachment metadata. +type AttachmentStore interface { + CreateAttachment(id string, uploaderID int64, filename, storedAs, mimeType string, size int64, width, height *int) error + GetAttachmentByID(id string) (*db.Attachment, error) + GetAttachmentWithChannel(id string) (*db.AttachmentAccess, error) + DeleteOrphanedAttachments(cutoff string) ([]string, error) +} + +// AdminStore handles admin operations. +type AdminStore interface { + UserCount() (int64, error) + GetServerStats() (*db.ServerStats, error) + ListAllUsers(limit, offset int) ([]db.UserWithRole, error) + BanUser(id int64, reason string, expires *time.Time) error + UnbanUser(id int64) error + LogAudit(actorID int64, action, targetType string, targetID int64, detail string) error + GetAuditLog(limit, offset int) ([]db.AuditEntry, error) + AdminCreateChannel(name, chanType, category, topic string, position int) (int64, error) + AdminUpdateChannel(id int64, name, topic string, slowMode, position int, archived bool) error + AdminDeleteChannel(id int64) error + BackupTo(path string) error + BackupToSafe(path, safeRoot string) error + CountUsersWithoutTOTP() (int, error) +} + +// SettingsStore handles server settings. +type SettingsStore interface { + GetSetting(key string) (string, error) + SetSetting(key, value string) error + GetAllSettings() (map[string]string, error) +}