add ModerationService, VoiceService, MemStore, permission tests

- ModerationService: ban/unban with validation and audit logging
- VoiceService: join (with capacity check), leave, mute, deafen,
  camera (with video limit), screenshare (with permission check)
- MemStore: in-memory Store implementation for service unit tests
- Permission tests: cache hit/miss, invalidation, TTL behavior
- Add Moderation and Voice to Services struct

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
This commit is contained in:
Claude
2026-04-05 21:35:20 +00:00
parent 9829325105
commit 0e2d101103
5 changed files with 1162 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
package service
import (
"fmt"
"log/slog"
"time"
"github.com/owncord/server/store"
)
// ModerationService handles user ban/unban operations.
type ModerationService struct {
st store.Store
}
// NewModerationService creates a ModerationService.
func NewModerationService(st store.Store) *ModerationService {
return &ModerationService{st: st}
}
// BanUser bans a target user. Validates the target exists and
// prevents self-banning.
func (s *ModerationService) BanUser(actorID, targetID int64, reason string, expires *time.Time) error {
if targetID <= 0 {
return fmt.Errorf("%w: user_id must be positive", ErrBadRequest)
}
if actorID == targetID {
return fmt.Errorf("%w: cannot ban yourself", ErrBadRequest)
}
target, err := s.st.GetUserByID(targetID)
if err != nil || target == nil {
return fmt.Errorf("%w: user not found", ErrNotFound)
}
if err := s.st.BanUser(targetID, reason, expires); err != nil {
return fmt.Errorf("%w: failed to ban user", ErrInternal)
}
if err := s.st.LogAudit(actorID, "ban", "user", targetID, reason); err != nil {
slog.Error("failed to log audit entry", "error", err)
}
slog.Info("user banned", "actor_id", actorID, "target_id", targetID, "reason", reason)
return nil
}
// UnbanUser removes a ban on a target user.
func (s *ModerationService) UnbanUser(actorID, targetID int64) error {
if targetID <= 0 {
return fmt.Errorf("%w: user_id must be positive", ErrBadRequest)
}
target, err := s.st.GetUserByID(targetID)
if err != nil || target == nil {
return fmt.Errorf("%w: user not found", ErrNotFound)
}
if err := s.st.UnbanUser(targetID); err != nil {
return fmt.Errorf("%w: failed to unban user", ErrInternal)
}
if err := s.st.LogAudit(actorID, "unban", "user", targetID, ""); err != nil {
slog.Error("failed to log audit entry", "error", err)
}
slog.Info("user unbanned", "actor_id", actorID, "target_id", targetID)
return nil
}
+192
View File
@@ -0,0 +1,192 @@
package service
import (
"testing"
"time"
"github.com/owncord/server/db"
"github.com/owncord/server/permissions"
"github.com/owncord/server/store"
)
// newTestPermService creates a PermissionService backed by a MemStore
// pre-populated with a single role and user.
func newTestPermService() (*PermissionService, *store.MemStore) {
ms := store.NewMemStore()
ms.SeedRole(&db.Role{
ID: permissions.MemberRoleID,
Name: "member",
Permissions: permissions.SendMessages | permissions.ReadMessages | permissions.AddReactions,
Position: 1,
})
ms.SeedUserRole(1, permissions.MemberRoleID)
checker := permissions.NewChecker(ms)
return NewPermissionService(ms, checker), ms
}
func TestHasChannelPerm_Allowed(t *testing.T) {
svc, ms := newTestPermService()
ms.SeedChannel(&db.Channel{ID: 10, Name: "general", Type: "text"})
// Member has SendMessages | ReadMessages; no overrides exist, so base role perms apply.
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected user to have SendMessages permission")
}
if !svc.HasChannelPerm(1, 10, permissions.ReadMessages) {
t.Fatal("expected user to have ReadMessages permission")
}
}
func TestHasChannelPerm_Denied(t *testing.T) {
svc, ms := newTestPermService()
ms.SeedChannel(&db.Channel{ID: 10, Name: "general", Type: "text"})
// ManageMessages is NOT in the member role.
if svc.HasChannelPerm(1, 10, permissions.ManageMessages) {
t.Fatal("expected user to NOT have ManageMessages permission")
}
}
func TestHasChannelPerm_OverrideDeny(t *testing.T) {
svc, ms := newTestPermService()
ms.SeedChannel(&db.Channel{ID: 10, Name: "readonly", Type: "text"})
// Deny SendMessages for this channel.
ms.SeedChannelOverride(permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Invalidate so next check re-populates cache.
svc.InvalidateAll()
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected SendMessages to be denied via channel override")
}
// ReadMessages should still be allowed.
if !svc.HasChannelPerm(1, 10, permissions.ReadMessages) {
t.Fatal("expected ReadMessages to remain allowed")
}
}
func TestHasChannelPerm_OverrideAllow(t *testing.T) {
svc, ms := newTestPermService()
ms.SeedChannel(&db.Channel{ID: 10, Name: "special", Type: "text"})
// Allow ManageMessages (not in base role) via channel override.
ms.SeedChannelOverride(permissions.MemberRoleID, 10, permissions.ManageMessages, 0)
svc.InvalidateAll()
if !svc.HasChannelPerm(1, 10, permissions.ManageMessages) {
t.Fatal("expected ManageMessages to be allowed via channel override")
}
}
func TestHasChannelPerm_AdminBypass(t *testing.T) {
ms := store.NewMemStore()
ms.SeedRole(&db.Role{
ID: permissions.AdminRoleID,
Name: "admin",
Permissions: permissions.Administrator,
Position: 90,
})
ms.SeedUserRole(1, permissions.AdminRoleID)
checker := permissions.NewChecker(ms)
svc := NewPermissionService(ms, checker)
ms.SeedChannel(&db.Channel{ID: 10, Name: "locked", Type: "text"})
// Deny everything via override; admin should still bypass.
ms.SeedChannelOverride(permissions.AdminRoleID, 10, 0, permissions.SendMessages|permissions.ReadMessages)
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("admin should bypass all permission checks")
}
if !svc.HasChannelPerm(1, 10, permissions.ManageMessages) {
t.Fatal("admin should bypass all permission checks")
}
}
func TestInvalidateUser_ClearsCacheForUser(t *testing.T) {
svc, ms := newTestPermService()
ms.SeedChannel(&db.Channel{ID: 10, Name: "general", Type: "text"})
// Populate cache.
svc.HasChannelPerm(1, 10, permissions.SendMessages)
// Now add a deny override.
ms.SeedChannelOverride(permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Without invalidation, cache still says allowed.
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected cached value to still allow SendMessages")
}
// After invalidation, should pick up the override.
svc.InvalidateUser(1)
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected SendMessages to be denied after cache invalidation")
}
}
func TestInvalidateAll_ClearsEntireCache(t *testing.T) {
svc, ms := newTestPermService()
// Add a second user.
ms.SeedUserRole(2, permissions.MemberRoleID)
ms.SeedChannel(&db.Channel{ID: 10, Name: "general", Type: "text"})
// Populate cache for both users.
svc.HasChannelPerm(1, 10, permissions.SendMessages)
svc.HasChannelPerm(2, 10, permissions.SendMessages)
// Add deny override.
ms.SeedChannelOverride(permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Both still cached as allowed.
if !svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected cached allow for user 1")
}
if !svc.HasChannelPerm(2, 10, permissions.SendMessages) {
t.Fatal("expected cached allow for user 2")
}
svc.InvalidateAll()
// Both should now see the deny.
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected deny for user 1 after InvalidateAll")
}
if svc.HasChannelPerm(2, 10, permissions.SendMessages) {
t.Fatal("expected deny for user 2 after InvalidateAll")
}
}
func TestPermCacheTTLExpiry(t *testing.T) {
// This test verifies the cache TTL mechanism. We cannot easily wait 30s
// in a unit test, so we verify the structural behavior: after manually
// backdating the populatedAt field the cache should be stale and the
// next check should re-populate from the store.
svc, ms := newTestPermService()
ms.SeedChannel(&db.Channel{ID: 10, Name: "general", Type: "text"})
// Populate cache.
svc.HasChannelPerm(1, 10, permissions.SendMessages)
// Add deny override.
ms.SeedChannelOverride(permissions.MemberRoleID, 10, 0, permissions.SendMessages)
// Manually expire the cache entry by backdating populatedAt.
svc.mu.Lock()
if cp, ok := svc.cache[int64(1)]; ok {
cp.populatedAt = time.Now().Add(-permCacheTTL - time.Second)
}
svc.mu.Unlock()
// The next call should re-populate and pick up the deny.
if svc.HasChannelPerm(1, 10, permissions.SendMessages) {
t.Fatal("expected cache TTL expiry to cause re-population with deny override")
}
}
func TestHasChannelPerm_UnknownUserReturnsFalse(t *testing.T) {
svc, ms := newTestPermService()
ms.SeedChannel(&db.Channel{ID: 10, Name: "general", Type: "text"})
// User 999 has no role assigned.
if svc.HasChannelPerm(999, 10, permissions.SendMessages) {
t.Fatal("expected false for unknown user")
}
}
+4
View File
@@ -20,6 +20,8 @@ type Services struct {
DMs *DMService
Invites *InviteService
Blocks *BlockService
Moderation *ModerationService
Voice *VoiceService
}
// New creates all domain services wired together.
@@ -34,5 +36,7 @@ func New(st store.Store, limiter *auth.RateLimiter) *Services {
DMs: NewDMService(st),
Invites: NewInviteService(st),
Blocks: NewBlockService(st),
Moderation: NewModerationService(st),
Voice: NewVoiceService(st, permSvc),
}
}
+137
View File
@@ -0,0 +1,137 @@
package service
import (
"errors"
"fmt"
"log/slog"
"github.com/owncord/server/db"
"github.com/owncord/server/permissions"
"github.com/owncord/server/store"
)
// VoiceService handles voice state business logic.
type VoiceService struct {
st store.Store
perm *PermissionService
}
// NewVoiceService creates a VoiceService.
func NewVoiceService(st store.Store, perm *PermissionService) *VoiceService {
return &VoiceService{st: st, perm: perm}
}
// JoinChannel validates the channel, checks ConnectVoice permission, and
// joins the user to the voice channel respecting capacity limits.
// Returns the channel on success so callers can access voice config fields.
func (s *VoiceService) JoinChannel(userID, channelID int64) (*db.Channel, error) {
if channelID <= 0 {
return nil, fmt.Errorf("%w: channel_id must be a positive integer", ErrBadRequest)
}
ch, err := s.st.GetChannel(channelID)
if err != nil || ch == nil {
return nil, fmt.Errorf("%w: channel not found", ErrNotFound)
}
if !s.perm.HasChannelPerm(userID, channelID, permissions.ConnectVoice) {
return nil, fmt.Errorf("%w: missing CONNECT_VOICE permission", ErrForbidden)
}
maxUsers := ch.VoiceMaxUsers
if maxUsers > 0 {
if err := s.st.JoinVoiceChannelIfCapacity(userID, channelID, maxUsers); err != nil {
if errors.Is(err, db.ErrChannelFull) {
return nil, fmt.Errorf("%w: voice channel is full", ErrForbidden)
}
slog.Error("VoiceService.JoinChannel JoinVoiceChannelIfCapacity", "err", err, "user_id", userID)
return nil, fmt.Errorf("%w: failed to join voice channel", ErrInternal)
}
} else {
if err := s.st.JoinVoiceChannel(userID, channelID); err != nil {
slog.Error("VoiceService.JoinChannel JoinVoiceChannel", "err", err, "user_id", userID)
return nil, fmt.Errorf("%w: failed to join voice channel", ErrInternal)
}
}
slog.Info("voice join", "user_id", userID, "channel_id", channelID)
return ch, nil
}
// LeaveChannel removes the user from their current voice channel.
func (s *VoiceService) LeaveChannel(userID int64) error {
if err := s.st.LeaveVoiceChannel(userID); err != nil {
slog.Error("VoiceService.LeaveChannel", "err", err, "user_id", userID)
return fmt.Errorf("%w: failed to leave voice channel", ErrInternal)
}
slog.Info("voice leave", "user_id", userID)
return nil
}
// UpdateMute toggles the mute state for the given user.
func (s *VoiceService) UpdateMute(userID int64, muted bool) error {
if err := s.st.UpdateVoiceMute(userID, muted); err != nil {
slog.Error("VoiceService.UpdateMute", "err", err, "user_id", userID)
return fmt.Errorf("%w: failed to update mute state", ErrInternal)
}
slog.Debug("voice mute changed", "user_id", userID, "muted", muted)
return nil
}
// UpdateDeafen toggles the deafen state for the given user.
func (s *VoiceService) UpdateDeafen(userID int64, deafened bool) error {
if err := s.st.UpdateVoiceDeafen(userID, deafened); err != nil {
slog.Error("VoiceService.UpdateDeafen", "err", err, "user_id", userID)
return fmt.Errorf("%w: failed to update deafen state", ErrInternal)
}
slog.Debug("voice deafen changed", "user_id", userID, "deafened", deafened)
return nil
}
// ToggleCamera enables or disables the user's camera. When enabling, it
// enforces the maxVideo limit via an atomic check-and-update. Returns true
// if the camera was successfully enabled (or disabled), false if the video
// limit was reached.
func (s *VoiceService) ToggleCamera(userID, channelID int64, enable bool, maxVideo int) (bool, error) {
if !s.perm.HasChannelPerm(userID, channelID, permissions.UseVideo) {
return false, fmt.Errorf("%w: missing USE_VIDEO permission", ErrForbidden)
}
if enable && maxVideo > 0 {
ok, err := s.st.EnableCameraIfUnderLimit(userID, channelID, maxVideo)
if err != nil {
slog.Error("VoiceService.ToggleCamera EnableCameraIfUnderLimit", "err", err, "user_id", userID)
return false, fmt.Errorf("%w: failed to check video limit", ErrInternal)
}
if !ok {
return false, nil
}
} else {
if err := s.st.UpdateVoiceCamera(userID, enable); err != nil {
slog.Error("VoiceService.ToggleCamera UpdateVoiceCamera", "err", err, "user_id", userID)
return false, fmt.Errorf("%w: failed to update camera state", ErrInternal)
}
}
slog.Debug("voice camera changed", "user_id", userID, "enabled", enable, "channel_id", channelID)
return true, nil
}
// ToggleScreenshare enables or disables the user's screen share after
// checking the ShareScreen permission.
func (s *VoiceService) ToggleScreenshare(userID, channelID int64, enable bool) error {
if !s.perm.HasChannelPerm(userID, channelID, permissions.ShareScreen) {
return fmt.Errorf("%w: missing SHARE_SCREEN permission", ErrForbidden)
}
if err := s.st.UpdateVoiceScreenshare(userID, enable); err != nil {
slog.Error("VoiceService.ToggleScreenshare", "err", err, "user_id", userID)
return fmt.Errorf("%w: failed to update screenshare state", ErrInternal)
}
slog.Debug("voice screenshare changed", "user_id", userID, "enabled", enable, "channel_id", channelID)
return nil
}
+760
View File
@@ -0,0 +1,760 @@
package store
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
"github.com/owncord/server/db"
)
// compile-time interface check
var _ Store = (*MemStore)(nil)
// MemStore is a lightweight in-memory Store implementation for testing.
// Only the methods needed by service-layer tests are implemented;
// everything else panics with a descriptive message.
type MemStore struct {
mu sync.Mutex
// auto-increment counters
nextMsgID int64
nextChannelID int64
channels map[int64]*db.Channel
messages map[int64]*db.Message
users map[int64]*db.User
roles map[int64]*db.Role
// userID -> roleID
userRoles map[int64]int64
// roleID -> channelID -> override
channelOverrides map[int64]map[int64]db.ChannelOverride
// messageID -> userID -> emoji -> bool
reactions map[int64]map[int64]map[string]bool
// channelID -> set of participant userIDs
dmParticipants map[int64]map[int64]bool
// blockerID -> blockedID -> bool (bidirectional checked via IsEitherBlocked)
blocks map[int64]map[int64]bool
// userID -> channelID -> lastReadMessageID
readStates map[int64]map[int64]int64
}
// NewMemStore creates an empty MemStore ready for use.
func NewMemStore() *MemStore {
return &MemStore{
channels: make(map[int64]*db.Channel),
messages: make(map[int64]*db.Message),
users: make(map[int64]*db.User),
roles: make(map[int64]*db.Role),
userRoles: make(map[int64]int64),
channelOverrides: make(map[int64]map[int64]db.ChannelOverride),
reactions: make(map[int64]map[int64]map[string]bool),
dmParticipants: make(map[int64]map[int64]bool),
blocks: make(map[int64]map[int64]bool),
readStates: make(map[int64]map[int64]int64),
}
}
// ---------- helpers for test setup ----------
// SeedChannel inserts a channel directly (for test setup).
func (m *MemStore) SeedChannel(ch *db.Channel) {
m.mu.Lock()
defer m.mu.Unlock()
m.channels[ch.ID] = ch
if ch.ID >= m.nextChannelID {
m.nextChannelID = ch.ID + 1
}
}
// SeedUser inserts a user directly (for test setup).
func (m *MemStore) SeedUser(u *db.User) {
m.mu.Lock()
defer m.mu.Unlock()
m.users[u.ID] = u
}
// SeedRole inserts a role directly (for test setup).
func (m *MemStore) SeedRole(r *db.Role) {
m.mu.Lock()
defer m.mu.Unlock()
m.roles[r.ID] = r
}
// SeedUserRole assigns a role to a user (for test setup).
func (m *MemStore) SeedUserRole(userID, roleID int64) {
m.mu.Lock()
defer m.mu.Unlock()
m.userRoles[userID] = roleID
}
// SeedChannelOverride sets a channel permission override for a role (for test setup).
func (m *MemStore) SeedChannelOverride(roleID, channelID int64, allow, deny int64) {
m.mu.Lock()
defer m.mu.Unlock()
if m.channelOverrides[roleID] == nil {
m.channelOverrides[roleID] = make(map[int64]db.ChannelOverride)
}
m.channelOverrides[roleID][channelID] = db.ChannelOverride{Allow: allow, Deny: deny}
}
// SeedDMParticipant adds a user as a DM participant in a channel (for test setup).
func (m *MemStore) SeedDMParticipant(channelID, userID int64) {
m.mu.Lock()
defer m.mu.Unlock()
if m.dmParticipants[channelID] == nil {
m.dmParticipants[channelID] = make(map[int64]bool)
}
m.dmParticipants[channelID][userID] = true
}
// SeedBlock records a block relationship (for test setup).
func (m *MemStore) SeedBlock(blockerID, blockedID int64) {
m.mu.Lock()
defer m.mu.Unlock()
if m.blocks[blockerID] == nil {
m.blocks[blockerID] = make(map[int64]bool)
}
m.blocks[blockerID][blockedID] = true
}
// ---------- Store interface: top-level ----------
func (m *MemStore) Close() error { return nil }
func (m *MemStore) SQLDb() *sql.DB { return nil }
func (m *MemStore) WithTx(_ context.Context, fn func(Store) error) error { return fn(m) }
// ---------- MessageStore ----------
func (m *MemStore) CreateMessage(channelID, userID int64, content string, replyTo *int64) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.nextMsgID++
id := m.nextMsgID
m.messages[id] = &db.Message{
ID: id,
ChannelID: channelID,
UserID: userID,
Content: content,
ReplyTo: replyTo,
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
return id, nil
}
func (m *MemStore) GetMessage(id int64) (*db.Message, error) {
m.mu.Lock()
defer m.mu.Unlock()
msg, ok := m.messages[id]
if !ok {
return nil, fmt.Errorf("message %d not found", id)
}
// Return a copy to avoid aliasing.
cp := *msg
return &cp, nil
}
func (m *MemStore) GetMessages(_ int64, _ int64, _ int) ([]db.MessageWithUser, error) {
panic("memstore: not implemented: GetMessages")
}
func (m *MemStore) GetMessagesForAPI(_ int64, _ int64, _ int, _ int64) ([]db.MessageAPIResponse, error) {
return []db.MessageAPIResponse{}, nil
}
func (m *MemStore) EditMessage(id, userID int64, content string) error {
m.mu.Lock()
defer m.mu.Unlock()
msg, ok := m.messages[id]
if !ok {
return fmt.Errorf("message %d not found", id)
}
if msg.UserID != userID {
return fmt.Errorf("not message owner")
}
now := time.Now().UTC().Format(time.RFC3339)
msg.Content = content
msg.EditedAt = &now
return nil
}
func (m *MemStore) DeleteMessage(id, userID int64, isMod bool) error {
m.mu.Lock()
defer m.mu.Unlock()
msg, ok := m.messages[id]
if !ok {
return fmt.Errorf("message %d not found", id)
}
if !isMod && msg.UserID != userID {
return fmt.Errorf("not message owner")
}
msg.Deleted = true
return nil
}
func (m *MemStore) SearchMessages(_ string, _ *int64, _ int) ([]db.MessageSearchResult, error) {
return []db.MessageSearchResult{}, nil
}
func (m *MemStore) SearchMessagesInChannels(_ string, _ []int64, _ int) ([]db.MessageSearchResult, error) {
return []db.MessageSearchResult{}, nil
}
func (m *MemStore) GetPinnedMessages(_ int64, _ int64) ([]db.MessageAPIResponse, error) {
return []db.MessageAPIResponse{}, nil
}
func (m *MemStore) SetMessagePinned(_ int64, _ bool) error { return nil }
func (m *MemStore) AddReaction(messageID, userID int64, emoji string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.messages[messageID]; !ok {
return fmt.Errorf("message %d not found", messageID)
}
if m.reactions[messageID] == nil {
m.reactions[messageID] = make(map[int64]map[string]bool)
}
if m.reactions[messageID][userID] == nil {
m.reactions[messageID][userID] = make(map[string]bool)
}
if m.reactions[messageID][userID][emoji] {
return fmt.Errorf("reaction already exists")
}
m.reactions[messageID][userID][emoji] = true
return nil
}
func (m *MemStore) RemoveReaction(messageID, userID int64, emoji string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.reactions[messageID] == nil || m.reactions[messageID][userID] == nil || !m.reactions[messageID][userID][emoji] {
return fmt.Errorf("reaction not found")
}
delete(m.reactions[messageID][userID], emoji)
return nil
}
func (m *MemStore) GetReactions(_ int64) ([]db.ReactionCount, error) {
return []db.ReactionCount{}, nil
}
func (m *MemStore) UpdateReadState(userID, channelID, lastReadMessageID int64) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.readStates[userID] == nil {
m.readStates[userID] = make(map[int64]int64)
}
m.readStates[userID][channelID] = lastReadMessageID
return nil
}
func (m *MemStore) GetChannelUnreadCounts(_ int64) (map[int64]db.ChannelUnread, error) {
return map[int64]db.ChannelUnread{}, nil
}
func (m *MemStore) GetLatestMessageID(channelID int64) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
var latest int64
for _, msg := range m.messages {
if msg.ChannelID == channelID && msg.ID > latest {
latest = msg.ID
}
}
return latest, nil
}
func (m *MemStore) LinkAttachmentsToMessage(_ int64, _ []string) (int64, error) {
return 0, nil
}
func (m *MemStore) GetAttachmentsByMessageIDs(_ []int64) (map[int64][]db.AttachmentInfo, error) {
return map[int64][]db.AttachmentInfo{}, nil
}
// ---------- ChannelStore ----------
func (m *MemStore) ListChannels() ([]db.Channel, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]db.Channel, 0, len(m.channels))
for _, ch := range m.channels {
out = append(out, *ch)
}
return out, nil
}
func (m *MemStore) GetChannel(id int64) (*db.Channel, error) {
m.mu.Lock()
defer m.mu.Unlock()
ch, ok := m.channels[id]
if !ok {
return nil, fmt.Errorf("channel %d not found", id)
}
cp := *ch
return &cp, nil
}
func (m *MemStore) CreateChannel(name, chanType, category, topic string, position int) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.nextChannelID++
id := m.nextChannelID
m.channels[id] = &db.Channel{
ID: id,
Name: name,
Type: chanType,
Category: category,
Topic: topic,
Position: position,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
return id, nil
}
func (m *MemStore) UpdateChannel(_ int64, _, _ string, _ int) error {
panic("memstore: not implemented: UpdateChannel")
}
func (m *MemStore) DeleteChannel(_ int64) error {
panic("memstore: not implemented: DeleteChannel")
}
func (m *MemStore) SetChannelSlowMode(_ int64, _ int) error {
panic("memstore: not implemented: SetChannelSlowMode")
}
func (m *MemStore) SetChannelVoiceMaxUsers(_ int64, _ int) error {
panic("memstore: not implemented: SetChannelVoiceMaxUsers")
}
func (m *MemStore) GetChannelPermissions(channelID, roleID int64) (int64, int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
overrides, ok := m.channelOverrides[roleID]
if !ok {
return 0, 0, nil
}
o, ok := overrides[channelID]
if !ok {
return 0, 0, nil
}
return o.Allow, o.Deny, nil
}
func (m *MemStore) GetAllChannelPermissionsForRole(roleID int64) (map[int64]db.ChannelOverride, error) {
m.mu.Lock()
defer m.mu.Unlock()
overrides, ok := m.channelOverrides[roleID]
if !ok {
return map[int64]db.ChannelOverride{}, nil
}
// Copy map.
out := make(map[int64]db.ChannelOverride, len(overrides))
for k, v := range overrides {
out[k] = v
}
return out, nil
}
func (m *MemStore) GetChannelTypes(_ []int64) (map[int64]string, error) {
panic("memstore: not implemented: GetChannelTypes")
}
// ---------- UserStore ----------
func (m *MemStore) GetUserByID(id int64) (*db.User, error) {
m.mu.Lock()
defer m.mu.Unlock()
u, ok := m.users[id]
if !ok {
return nil, fmt.Errorf("user %d not found", id)
}
cp := *u
return &cp, nil
}
func (m *MemStore) GetUserByUsername(_ string) (*db.User, error) {
panic("memstore: not implemented: GetUserByUsername")
}
func (m *MemStore) CreateUser(_, _ string, _ int) (int64, error) {
panic("memstore: not implemented: CreateUser")
}
func (m *MemStore) CreateOwnerIfEmpty(_, _ string, _ int) (int64, error) {
panic("memstore: not implemented: CreateOwnerIfEmpty")
}
func (m *MemStore) CreateUserWithInvite(_, _ string, _ int, _ string) (int64, error) {
panic("memstore: not implemented: CreateUserWithInvite")
}
func (m *MemStore) UpdateUserProfile(_ int64, _ string, _ *string) error {
panic("memstore: not implemented: UpdateUserProfile")
}
func (m *MemStore) UpdateUserPassword(_ int64, _ string) error {
panic("memstore: not implemented: UpdateUserPassword")
}
func (m *MemStore) UpdateUserStatus(id int64, status string) error {
m.mu.Lock()
defer m.mu.Unlock()
u, ok := m.users[id]
if !ok {
return fmt.Errorf("user %d not found", id)
}
u.Status = status
return nil
}
func (m *MemStore) UpdateUserTOTPSecret(_ int64, _ *string) error {
panic("memstore: not implemented: UpdateUserTOTPSecret")
}
func (m *MemStore) UpdateUserRole(_ int64, _ int64) error {
panic("memstore: not implemented: UpdateUserRole")
}
func (m *MemStore) ResetAllUserStatuses() error {
panic("memstore: not implemented: ResetAllUserStatuses")
}
func (m *MemStore) DeleteAccount(_ context.Context, _ int64) error {
panic("memstore: not implemented: DeleteAccount")
}
func (m *MemStore) ListMembers() ([]db.MemberSummary, error) {
panic("memstore: not implemented: ListMembers")
}
// ---------- SessionStore ----------
func (m *MemStore) CreateSession(_ int64, _, _, _ string) (int64, error) {
panic("memstore: not implemented: CreateSession")
}
func (m *MemStore) GetSessionByTokenHash(_ string) (*db.Session, error) {
panic("memstore: not implemented: GetSessionByTokenHash")
}
func (m *MemStore) GetSessionWithBanStatus(_ string) (*db.SessionWithBanStatus, error) {
panic("memstore: not implemented: GetSessionWithBanStatus")
}
func (m *MemStore) DeleteSession(_ string) error {
panic("memstore: not implemented: DeleteSession")
}
func (m *MemStore) DeleteOtherSessions(_ int64, _ int64) (int64, error) {
panic("memstore: not implemented: DeleteOtherSessions")
}
func (m *MemStore) DeleteExpiredSessions() error {
panic("memstore: not implemented: DeleteExpiredSessions")
}
func (m *MemStore) DeleteSessionByID(_ int64, _ int64) error {
panic("memstore: not implemented: DeleteSessionByID")
}
func (m *MemStore) TouchSession(_ string) error {
panic("memstore: not implemented: TouchSession")
}
func (m *MemStore) ListUserSessions(_ int64) ([]db.Session, error) {
panic("memstore: not implemented: ListUserSessions")
}
func (m *MemStore) ForceLogoutUser(_ int64) error {
panic("memstore: not implemented: ForceLogoutUser")
}
func (m *MemStore) GetUserSessions(_ int64) ([]db.Session, error) {
panic("memstore: not implemented: GetUserSessions")
}
// ---------- RoleStore ----------
func (m *MemStore) GetRoleByID(id int64) (*db.Role, error) {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.roles[id]
if !ok {
return nil, fmt.Errorf("role %d not found", id)
}
cp := *r
return &cp, nil
}
func (m *MemStore) GetRoleForUser(userID int64) (*db.Role, error) {
m.mu.Lock()
defer m.mu.Unlock()
roleID, ok := m.userRoles[userID]
if !ok {
return nil, fmt.Errorf("no role for user %d", userID)
}
r, ok := m.roles[roleID]
if !ok {
return nil, fmt.Errorf("role %d not found", roleID)
}
cp := *r
return &cp, nil
}
func (m *MemStore) GetUserWithRole(_ int64) (*db.User, *db.Role, error) {
panic("memstore: not implemented: GetUserWithRole")
}
func (m *MemStore) ListRoles() ([]*db.Role, error) {
panic("memstore: not implemented: ListRoles")
}
// ---------- InviteStore ----------
func (m *MemStore) CreateInvite(_ int64, _ int, _ *time.Time) (string, error) {
panic("memstore: not implemented: CreateInvite")
}
func (m *MemStore) GetInvite(_ string) (*db.Invite, error) {
panic("memstore: not implemented: GetInvite")
}
func (m *MemStore) ListInvites() ([]*db.Invite, error) {
panic("memstore: not implemented: ListInvites")
}
func (m *MemStore) UseInviteAtomic(_ string) error {
panic("memstore: not implemented: UseInviteAtomic")
}
func (m *MemStore) RevokeInvite(_ string) error {
panic("memstore: not implemented: RevokeInvite")
}
// ---------- VoiceStore ----------
func (m *MemStore) JoinVoiceChannel(_ int64, _ int64) error {
panic("memstore: not implemented: JoinVoiceChannel")
}
func (m *MemStore) JoinVoiceChannelIfCapacity(_ int64, _ int64, _ int) error {
panic("memstore: not implemented: JoinVoiceChannelIfCapacity")
}
func (m *MemStore) LeaveVoiceChannel(_ int64) error {
panic("memstore: not implemented: LeaveVoiceChannel")
}
func (m *MemStore) LeaveVoiceChannelIfMatch(_ int64, _ int64, _ string) (bool, error) {
panic("memstore: not implemented: LeaveVoiceChannelIfMatch")
}
func (m *MemStore) GetVoiceState(_ int64) (*db.VoiceState, error) {
panic("memstore: not implemented: GetVoiceState")
}
func (m *MemStore) GetChannelVoiceStates(_ int64) ([]db.VoiceState, error) {
panic("memstore: not implemented: GetChannelVoiceStates")
}
func (m *MemStore) GetAllVoiceStates() ([]db.VoiceState, error) {
panic("memstore: not implemented: GetAllVoiceStates")
}
func (m *MemStore) UpdateVoiceMute(_ int64, _ bool) error {
panic("memstore: not implemented: UpdateVoiceMute")
}
func (m *MemStore) UpdateVoiceDeafen(_ int64, _ bool) error {
panic("memstore: not implemented: UpdateVoiceDeafen")
}
func (m *MemStore) ClearVoiceState(_ int64) error {
panic("memstore: not implemented: ClearVoiceState")
}
func (m *MemStore) ClearAllVoiceStates() error {
panic("memstore: not implemented: ClearAllVoiceStates")
}
func (m *MemStore) CountActiveCameras(_ int64) (int, error) {
panic("memstore: not implemented: CountActiveCameras")
}
func (m *MemStore) UpdateVoiceCamera(_ int64, _ bool) error {
panic("memstore: not implemented: UpdateVoiceCamera")
}
func (m *MemStore) EnableCameraIfUnderLimit(_ int64, _ int64, _ int) (bool, error) {
panic("memstore: not implemented: EnableCameraIfUnderLimit")
}
func (m *MemStore) UpdateVoiceScreenshare(_ int64, _ bool) error {
panic("memstore: not implemented: UpdateVoiceScreenshare")
}
func (m *MemStore) CountChannelVoiceUsers(_ int64) (int, error) {
panic("memstore: not implemented: CountChannelVoiceUsers")
}
// ---------- DMStore ----------
func (m *MemStore) GetOrCreateDMChannel(_ int64, _ int64) (*db.Channel, bool, error) {
panic("memstore: not implemented: GetOrCreateDMChannel")
}
func (m *MemStore) GetUserDMChannels(_ int64) ([]db.DMChannelInfo, error) {
return []db.DMChannelInfo{}, nil
}
func (m *MemStore) OpenDM(_, _ int64) error { return nil }
func (m *MemStore) CloseDM(_, _ int64) error { return nil }
func (m *MemStore) IsDMParticipant(userID, channelID int64) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
participants, ok := m.dmParticipants[channelID]
if !ok {
return false, nil
}
return participants[userID], nil
}
func (m *MemStore) GetDMParticipantIDs(channelID int64) ([]int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
participants, ok := m.dmParticipants[channelID]
if !ok {
return []int64{}, nil
}
ids := make([]int64, 0, len(participants))
for id := range participants {
ids = append(ids, id)
}
return ids, nil
}
func (m *MemStore) GetDMRecipient(_ int64, _ int64) (*db.User, error) {
return nil, nil
}
// ---------- BlockStore ----------
func (m *MemStore) BlockUser(_, _ int64) error { return nil }
func (m *MemStore) UnblockUser(_, _ int64) error { return nil }
func (m *MemStore) IsBlocked(blockerID, blockedID int64) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.blocks[blockerID] != nil && m.blocks[blockerID][blockedID] {
return true, nil
}
return false, nil
}
func (m *MemStore) IsEitherBlocked(userA, userB int64) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.blocks[userA] != nil && m.blocks[userA][userB] {
return true, nil
}
if m.blocks[userB] != nil && m.blocks[userB][userA] {
return true, nil
}
return false, nil
}
func (m *MemStore) ListBlockedUsers(_ int64) ([]int64, error) {
return []int64{}, nil
}
// ---------- AttachmentStore ----------
func (m *MemStore) CreateAttachment(_ string, _ int64, _, _, _ string, _ int64, _, _ *int) error {
panic("memstore: not implemented: CreateAttachment")
}
func (m *MemStore) GetAttachmentByID(_ string) (*db.Attachment, error) {
panic("memstore: not implemented: GetAttachmentByID")
}
func (m *MemStore) GetAttachmentWithChannel(_ string) (*db.AttachmentAccess, error) {
panic("memstore: not implemented: GetAttachmentWithChannel")
}
func (m *MemStore) DeleteOrphanedAttachments(_ string) ([]string, error) {
panic("memstore: not implemented: DeleteOrphanedAttachments")
}
// ---------- AdminStore ----------
func (m *MemStore) UserCount() (int64, error) {
panic("memstore: not implemented: UserCount")
}
func (m *MemStore) GetServerStats() (*db.ServerStats, error) {
panic("memstore: not implemented: GetServerStats")
}
func (m *MemStore) ListAllUsers(_ int, _ int) ([]db.UserWithRole, error) {
panic("memstore: not implemented: ListAllUsers")
}
func (m *MemStore) BanUser(_ int64, _ string, _ *time.Time) error {
panic("memstore: not implemented: BanUser")
}
func (m *MemStore) UnbanUser(_ int64) error {
panic("memstore: not implemented: UnbanUser")
}
func (m *MemStore) LogAudit(_ int64, _, _ string, _ int64, _ string) error {
return nil
}
func (m *MemStore) GetAuditLog(_ int, _ int) ([]db.AuditEntry, error) {
panic("memstore: not implemented: GetAuditLog")
}
func (m *MemStore) AdminCreateChannel(_, _, _, _ string, _ int) (int64, error) {
panic("memstore: not implemented: AdminCreateChannel")
}
func (m *MemStore) AdminUpdateChannel(_ int64, _, _ string, _, _ int, _ bool) error {
panic("memstore: not implemented: AdminUpdateChannel")
}
func (m *MemStore) AdminDeleteChannel(_ int64) error {
panic("memstore: not implemented: AdminDeleteChannel")
}
func (m *MemStore) BackupTo(_ string) error {
panic("memstore: not implemented: BackupTo")
}
func (m *MemStore) BackupToSafe(_, _ string) error {
panic("memstore: not implemented: BackupToSafe")
}
func (m *MemStore) CountUsersWithoutTOTP() (int, error) {
panic("memstore: not implemented: CountUsersWithoutTOTP")
}
// ---------- SettingsStore ----------
func (m *MemStore) GetSetting(_ string) (string, error) {
panic("memstore: not implemented: GetSetting")
}
func (m *MemStore) SetSetting(_, _ string) error {
panic("memstore: not implemented: SetSetting")
}
func (m *MemStore) GetAllSettings() (map[string]string, error) {
panic("memstore: not implemented: GetAllSettings")
}