mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* chore(workflows): raise subagent effort tiers (sonnet/haiku to xhigh, prove opus to high) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(voice): 6 defect(s) (OC-0098, OC-0004, OC-0005, OC-0006, OC-0007, OC-0020) * fix(db): 1 defect(s) (OC-0096) * fix(admin): 1 defect(s) (OC-0097) * fix(auth): 2 defect(s) (OC-0099, OC-0021) * fix(voice): 1 defect(s) (OC-0018) * fix(admin): 1 defect(s) (OC-0045) * fix(api): 1 defect(s) (OC-0103) * fix(client): 1 defect(s) (OC-0105) * fix(client): 1 defect(s) (OC-0107) * fix(api): 1 defect(s) (OC-0109) * fix(api): 1 defect(s) (OC-0112) * test(admin): compare restore bytes with bytes.Equal Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(voice): 2 defect(s) (OC-0095, OC-0014) OC-0095: createRoom never called setE2EEEnabled(true), so the full ECDH/HKDF/AES-GCM key exchange completed but frames still reached the SFU in plaintext. OC-0014: token refresh timer was 23h while the server mints LiveKit tokens with a 5-minute TTL, so any reconnect after minute 5 presented an expired token. * fix(profile): 2 defect(s) (OC-0100, OC-0102) * fix(service): 1 defect(s) (OC-0022) Archived channels were only read-only for SendMessage/DeleteMessage. Edit, reaction, pin and purge sinks bypassed the check. Route every write sink through a shared requireChannelWritable gate. * fix(api): 1 defect(s) (OC-0048) * chore(workflows): correct stale model labels in bughunt-fix phase details Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(client): 1 defect(s) (OC-0015) * fix(voice): 1 defect(s) (OC-0002) * test: fix two CI-only failures in the batch-4 test suite The delete-account broadcast test now observes member_ban on a second client's socket: the hub broadcasts and then force-disconnects the target, so on a slow runner the close could beat the target's own copy of the frame. The observer is also the party the event exists for. The voice e2e mock now echoes the real joined channel id on voice_leave (it hardcoded channel_id 0, which the dispatcher's channel-matched self-leave teardown correctly ignores), and the rejoin test waits for the mock's delayed echoes to settle before clicking the row again — clicking inside the echo window toggled a leave instead of a join. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
83 lines
3.1 KiB
Go
83 lines
3.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/owncord/server/auth"
|
|
"github.com/owncord/server/db"
|
|
"github.com/owncord/server/permissions"
|
|
)
|
|
|
|
// maxPurgeLimit bounds one purge request. Matches the message page size, so a
|
|
// moderator can clear exactly what a client shows in one screenful and no
|
|
// single call can fan out an unbounded id list to every channel subscriber.
|
|
const maxPurgeLimit = 100
|
|
|
|
// PurgeMessages soft-deletes the newest limit non-deleted messages in a
|
|
// channel, optionally restricted to messages older than before.
|
|
//
|
|
// The actor needs READ_MESSAGES|MANAGE_MESSAGES on the channel (per-channel
|
|
// overrides apply), the same pair the single-message moderator delete and the
|
|
// pin toggle require — MANAGE_MESSAGES alone would let a role the admin panel's
|
|
// "Can access" toggle locked out of a private channel wipe it.
|
|
//
|
|
// DMs are rejected outright: a DM has no MANAGE_MESSAGES gate to check, so
|
|
// there is no participant-scoped authority a bulk delete could answer to.
|
|
func (s *MessageService) PurgeMessages(ctx context.Context, userID, channelID int64, limit int, before int64) (*PurgeMessagesResult, error) {
|
|
ratKey := auth.Key("chat_purge", userID)
|
|
if s.limiter != nil && !s.limiter.Allow(ratKey, 5, time.Second) {
|
|
return nil, ErrRateLimited
|
|
}
|
|
|
|
if channelID <= 0 {
|
|
return nil, fmt.Errorf("%w: channel_id must be a positive integer", ErrBadRequest)
|
|
}
|
|
if limit < 1 {
|
|
return nil, fmt.Errorf("%w: limit must be between 1 and %d", ErrBadRequest, maxPurgeLimit)
|
|
}
|
|
if before < 0 {
|
|
return nil, fmt.Errorf("%w: before must be a non-negative integer", ErrBadRequest)
|
|
}
|
|
if limit > maxPurgeLimit {
|
|
limit = maxPurgeLimit
|
|
}
|
|
|
|
ch, err := s.st.GetChannel(ctx, channelID)
|
|
if err != nil || ch == nil {
|
|
return nil, fmt.Errorf("%w: channel not found", ErrNotFound)
|
|
}
|
|
if ch.Type == "dm" {
|
|
return nil, fmt.Errorf("%w: bulk delete is not available in direct messages", ErrForbidden)
|
|
}
|
|
// Archived channels are read-only. PurgeMessages bypasses
|
|
// checkSendPermission (it runs its own MANAGE_MESSAGES check below), so it
|
|
// needs the shared gate directly — see requireChannelWritable in
|
|
// message_perms.go.
|
|
if err := requireChannelWritable(ch); err != nil {
|
|
return nil, err
|
|
}
|
|
if !s.perms.HasChannelPerm(ctx, userID, channelID, permissions.ReadMessages|permissions.ManageMessages) {
|
|
return nil, fmt.Errorf("%w: missing MANAGE_MESSAGES permission", ErrForbidden)
|
|
}
|
|
|
|
ids, err := s.st.PurgeChannelMessages(ctx, channelID, before, limit)
|
|
if err != nil {
|
|
slog.Error("MessageService.PurgeMessages", "err", err, "channel_id", channelID)
|
|
return nil, fmt.Errorf("%w: failed to purge messages", ErrInternal)
|
|
}
|
|
if ids == nil {
|
|
ids = []int64{}
|
|
}
|
|
|
|
slog.Info("messages purged", "user_id", userID, "channel_id", channelID, "count", len(ids))
|
|
// One audit row per purge, not per message. Audit rows must survive a
|
|
// request canceled after the delete committed.
|
|
db.WriteAudit(context.WithoutCancel(ctx), s.st, userID, "message_purge", "channel", channelID,
|
|
fmt.Sprintf("purged %d messages, limit=%d, before=%d", len(ids), limit, before))
|
|
|
|
return &PurgeMessagesResult{ChannelID: channelID, MessageIDs: ids}, nil
|
|
}
|