mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* docs(plans): phased remediation plan for the 2026-08-19 audit Executes the audit's §8 MUST-fix verdict and §9.1 fix order: one phase per finding group, statuses updated in place as phases land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * test(client): give the renderWindow-breaker test its own timeout (audit F-5) 30 synchronous 100-row jsdom rebuilds can exceed vitest's default 5s on a loaded runner; the test timed out once under CI-like load and passes in isolation, so it now carries an explicit 20s budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * docs: fix the ten wrong reference-doc statements from audit 2026-08-19 (B-01..B-10) schema.md: migrations 030/031 documented, attachments ON DELETE SET NULL (matching 030's rebuild), index inventory rewritten from cumulative migration state, writer/reader pool split described, default-roles table made a consistent post-migration snapshot, dbgen preamble updated. protocol.md: DM chat events documented as sequenced/ring-buffered/replayable (they are), plugin_broadcast seq flipped to Yes, retry_after claim removed (no WS error carries it), the five enforced-but-documented-as-None rate limits added (channel_focus, mark_read, call_decline, chat_command, ping), E2EE announce/offer budgets corrected incl. the per-target inner cap, BAD_PAYLOAD and NOT_KEY_HOLDER added to the error table, ready voice_states/ roles field lists completed, member_join top-level status documented. api.md: diagnostics endpoint is ADMINISTRATOR-only (H-8) with a per-IP limiter and host:port livekit_url, error-code table now matches emitted codes (INTERNAL_ERROR, STORAGE_ERROR 507; oversize upload is 400), body-cap exemptions listed, identity_public_key documented on PATCH /users/me, plugin endpoints' plain-text errors + X-Plugin-Runtime header documented, /health 503 degraded state documented, metrics/LiveKit CIDR keys named, updates/apply restart-conflict 409s added. Also folds in the audit's D-04/D-05 comment and plan-header staleness fixes (buildReady comment, e2e spec-count comments, logctx stray word, three plan status headers). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * fix(server): log the five silently-discarded persistence errors (audit F-3/F-4/D-16) Lockout Upsert/Delete/Cleanup failures (auth/ratelimit.go), the H-6 session-cap eviction failure in CreateSession (db/auth_queries.go), and the channel_focus read-state write failure (service/channel.go) all discarded their errors with no trace — a brute-force lockout could silently fail to survive a restart. In-memory behavior is unchanged (warn-and-continue); the lockout write paths are pinned by tests mirroring OC-0061's load-path test. The session-cap and read-state sites are log-only additions on seams the existing suites already exercise on the success path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * fix(dm): blocking a user evicts them from the pair's live 1:1 DM voice call (audit F-1) The block gate ran only at voice_join and voluntary voice_token_refresh, so a blocked user already in the shared 1:1 DM call kept their session indefinitely — the same guard-asymmetry family as A-2026-08-03. handleBlockUser now severs the call through the dmVoiceEvictor capability handleCloseDM already exercises, using a new find-only FindDMChannelIDBetween lookup (sqlc-generated; mirrors GetOrCreateDMChannel's is_group=0 clause so group DM calls stay exempt, matching requireDMNotBlocked). Pinned by three handler tests: shared-DM eviction, no-DM no-op, group-only no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * fix(ws): close the role-reassignment/handshake race (audit F-2) A role reassignment landing mid-handshake was invisible for the socket's whole life: both handshake paths resolved permissions from the auth-time c.user snapshot, revokeUnreadableChannels early-returns for a user not yet in h.clients, and its Unsubscribe no-ops on the pubsub identity guard once a reconnect replaced the client. Three coordinated fixes: (1) refreshUserSnapshot re-reads the user row (and role name) in reconnectPrecheck and handleFreshConnect, fail-closed; (2) the resume-fallback path re-reads the role once more after registerNow and runs the revocation pass when it moved, so the reassignment-vs-registration orderings meet in the middle; (3) revokeUnreadableChannels re-resolves the live client immediately before acting, mirroring RefreshChannelVisibility. Pinned by four tests driving real WS handshakes through the existing race hooks plus a new pre-register/pre-act hook pair; ws suite green under the default and deadlock builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * refactor(client): remove the inert replay-dedup machinery (audit F-6) The server writes auth_ok before the replay burst, so replayDedup — created on socket-open and cleared when auth_ok is processed — could never be active for a real replayed frame, and the dispatcher's isReplaying() unread gates never fired. Their no-op behavior is the correct behavior (a buffer/db resume has no ready payload, so replayed frames must count as unread), so the machinery, the gates, and the misleading comments are removed rather than repaired. The pinning tests injected replay frames in an order a spec-compliant server never produces; they are replaced by a test pinning the real contract (frames after auth_ok are dispatched verbatim; duplicate handling belongs to the stores). Client suite green: 5036/5036. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * docs(plans): mark remediation phases 1-6 done Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ * fix(ws): nolint the context-less revoke call golangci-lint flags revokeUnreadableChannels takes no context by design (admin HubBroadcaster interface); annotate the one call site inside a ctx-taking function, matching the RefreshChannelVisibility precedent. golangci-lint v2.11.3: 0 issues. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HtkxwdqE4pUv82GQPRsTeQ --------- Co-authored-by: Claude <noreply@anthropic.com>
361 lines
10 KiB
Go
361 lines
10 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: dm.sql
|
|
|
|
package dbgen
|
|
|
|
import (
|
|
"context"
|
|
)
|
|
|
|
const closeDM = `-- name: CloseDM :exec
|
|
DELETE FROM dm_open_state WHERE user_id = ? AND channel_id = ?
|
|
`
|
|
|
|
type CloseDMParams struct {
|
|
UserID int64 `json:"userId"`
|
|
ChannelID int64 `json:"channelId"`
|
|
}
|
|
|
|
func (q *Queries) CloseDM(ctx context.Context, arg CloseDMParams) error {
|
|
_, err := q.db.ExecContext(ctx, closeDM, arg.UserID, arg.ChannelID)
|
|
return err
|
|
}
|
|
|
|
const countDMParticipants = `-- name: CountDMParticipants :one
|
|
SELECT COUNT(*) FROM dm_participants WHERE channel_id = ?
|
|
`
|
|
|
|
func (q *Queries) CountDMParticipants(ctx context.Context, channelID int64) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, countDMParticipants, channelID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const findDMChannelIDBetween = `-- name: FindDMChannelIDBetween :one
|
|
SELECT dp1.channel_id FROM dm_participants dp1
|
|
JOIN dm_participants dp2 ON dp1.channel_id = dp2.channel_id
|
|
JOIN channels c ON c.id = dp1.channel_id
|
|
WHERE dp1.user_id = ? AND dp2.user_id = ? AND c.type = 'dm' AND c.is_group = 0
|
|
ORDER BY dp1.channel_id ASC
|
|
`
|
|
|
|
type FindDMChannelIDBetweenParams struct {
|
|
UserID int64 `json:"userId"`
|
|
UserID_2 int64 `json:"userId2"`
|
|
}
|
|
|
|
// The 1:1 DM channel between two users, if one exists. Mirrors the lookup
|
|
// inside GetOrCreateDMChannel (raw, transactional) without creating anything:
|
|
// the is_group clause keeps group DMs out, matching the block-enforcement
|
|
// boundary (blocks never gate group DMs). ORDER BY makes the row choice
|
|
// deterministic should duplicates ever exist.
|
|
func (q *Queries) FindDMChannelIDBetween(ctx context.Context, arg FindDMChannelIDBetweenParams) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, findDMChannelIDBetween, arg.UserID, arg.UserID_2)
|
|
var channel_id int64
|
|
err := row.Scan(&channel_id)
|
|
return channel_id, err
|
|
}
|
|
|
|
const getDMParticipantIDs = `-- name: GetDMParticipantIDs :many
|
|
SELECT user_id FROM dm_participants WHERE channel_id = ?
|
|
`
|
|
|
|
func (q *Queries) GetDMParticipantIDs(ctx context.Context, channelID int64) ([]int64, error) {
|
|
rows, err := q.db.QueryContext(ctx, getDMParticipantIDs, channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []int64{}
|
|
for rows.Next() {
|
|
var user_id int64
|
|
if err := rows.Scan(&user_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, user_id)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getDMParticipants = `-- name: GetDMParticipants :many
|
|
SELECT
|
|
u.id AS id,
|
|
u.username AS username,
|
|
COALESCE(u.display_name, '') AS display_name,
|
|
COALESCE(u.avatar, '') AS avatar,
|
|
u.status AS status
|
|
FROM dm_participants dp
|
|
JOIN users u ON u.id = dp.user_id
|
|
WHERE dp.channel_id = ?
|
|
ORDER BY u.id ASC
|
|
`
|
|
|
|
type GetDMParticipantsRow struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
DisplayName string `json:"displayName"`
|
|
Avatar string `json:"avatar"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (q *Queries) GetDMParticipants(ctx context.Context, channelID int64) ([]GetDMParticipantsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getDMParticipants, channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []GetDMParticipantsRow{}
|
|
for rows.Next() {
|
|
var i GetDMParticipantsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.DisplayName,
|
|
&i.Avatar,
|
|
&i.Status,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getDMParticipantsForUser = `-- name: GetDMParticipantsForUser :many
|
|
SELECT
|
|
dp.channel_id AS channel_id,
|
|
u.id AS id,
|
|
u.username AS username,
|
|
COALESCE(u.display_name, '') AS display_name,
|
|
COALESCE(u.avatar, '') AS avatar,
|
|
u.status AS status
|
|
FROM dm_open_state dos
|
|
JOIN dm_participants dp ON dp.channel_id = dos.channel_id
|
|
JOIN users u ON u.id = dp.user_id
|
|
WHERE dos.user_id = ?
|
|
ORDER BY dp.channel_id ASC, u.id ASC
|
|
`
|
|
|
|
type GetDMParticipantsForUserRow struct {
|
|
ChannelID int64 `json:"channelId"`
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
DisplayName string `json:"displayName"`
|
|
Avatar string `json:"avatar"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// Every participant of every DM the user has open, in one pass. Includes the
|
|
// user themselves so a caller can tell "group of three" from "group of three
|
|
// others"; the Go layer filters when it needs the others.
|
|
func (q *Queries) GetDMParticipantsForUser(ctx context.Context, userID int64) ([]GetDMParticipantsForUserRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getDMParticipantsForUser, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []GetDMParticipantsForUserRow{}
|
|
for rows.Next() {
|
|
var i GetDMParticipantsForUserRow
|
|
if err := rows.Scan(
|
|
&i.ChannelID,
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.DisplayName,
|
|
&i.Avatar,
|
|
&i.Status,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getUserDMChannelIDs = `-- name: GetUserDMChannelIDs :many
|
|
SELECT channel_id FROM dm_open_state WHERE user_id = ?
|
|
`
|
|
|
|
func (q *Queries) GetUserDMChannelIDs(ctx context.Context, userID int64) ([]int64, error) {
|
|
rows, err := q.db.QueryContext(ctx, getUserDMChannelIDs, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []int64{}
|
|
for rows.Next() {
|
|
var channel_id int64
|
|
if err := rows.Scan(&channel_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, channel_id)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getUserDMChannels = `-- name: GetUserDMChannels :many
|
|
SELECT
|
|
c.id AS channel_id,
|
|
c.name AS name,
|
|
c.is_group AS is_group,
|
|
lm.id AS last_message_id,
|
|
COALESCE(lm.content, '') AS last_message,
|
|
COALESCE(lm.timestamp, '') AS last_message_at,
|
|
(SELECT COUNT(*) FROM messages mu
|
|
WHERE mu.channel_id = c.id AND mu.deleted = 0
|
|
AND mu.id > COALESCE((SELECT rs.last_message_id FROM read_states rs
|
|
WHERE rs.channel_id = c.id AND rs.user_id = dos.user_id), 0)
|
|
) AS unread_count
|
|
FROM dm_open_state dos
|
|
JOIN channels c ON c.id = dos.channel_id AND c.type = 'dm'
|
|
LEFT JOIN messages lm ON lm.id = (
|
|
SELECT MAX(id) FROM messages WHERE channel_id = c.id AND deleted = 0
|
|
)
|
|
WHERE dos.user_id = ?
|
|
ORDER BY COALESCE(lm.timestamp, dos.opened_at) DESC
|
|
`
|
|
|
|
type GetUserDMChannelsRow struct {
|
|
ChannelID int64 `json:"channelId"`
|
|
Name string `json:"name"`
|
|
IsGroup int64 `json:"isGroup"`
|
|
LastMessageID *int64 `json:"lastMessageId"`
|
|
LastMessage string `json:"lastMessage"`
|
|
LastMessageAt string `json:"lastMessageAt"`
|
|
UnreadCount int64 `json:"unreadCount"`
|
|
}
|
|
|
|
// A DM row carries no recipient any more: dm_participants holds N users, so
|
|
// "the other one" is only well defined for a two-person DM. The participant
|
|
// set comes from GetDMParticipantsForUser below, one extra query for the whole
|
|
// list rather than one per channel, and the Go layer stitches them together.
|
|
func (q *Queries) GetUserDMChannels(ctx context.Context, userID int64) ([]GetUserDMChannelsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, getUserDMChannels, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []GetUserDMChannelsRow{}
|
|
for rows.Next() {
|
|
var i GetUserDMChannelsRow
|
|
if err := rows.Scan(
|
|
&i.ChannelID,
|
|
&i.Name,
|
|
&i.IsGroup,
|
|
&i.LastMessageID,
|
|
&i.LastMessage,
|
|
&i.LastMessageAt,
|
|
&i.UnreadCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const isDMParticipant = `-- name: IsDMParticipant :one
|
|
SELECT user_id FROM dm_participants WHERE user_id = ? AND channel_id = ?
|
|
`
|
|
|
|
type IsDMParticipantParams struct {
|
|
UserID int64 `json:"userId"`
|
|
ChannelID int64 `json:"channelId"`
|
|
}
|
|
|
|
func (q *Queries) IsDMParticipant(ctx context.Context, arg IsDMParticipantParams) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, isDMParticipant, arg.UserID, arg.ChannelID)
|
|
var user_id int64
|
|
err := row.Scan(&user_id)
|
|
return user_id, err
|
|
}
|
|
|
|
const isGroupDM = `-- name: IsGroupDM :one
|
|
SELECT is_group FROM channels WHERE id = ? AND type = 'dm'
|
|
`
|
|
|
|
func (q *Queries) IsGroupDM(ctx context.Context, id int64) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, isGroupDM, id)
|
|
var is_group int64
|
|
err := row.Scan(&is_group)
|
|
return is_group, err
|
|
}
|
|
|
|
const openDM = `-- name: OpenDM :execrows
|
|
INSERT OR IGNORE INTO dm_open_state (user_id, channel_id) VALUES (?, ?)
|
|
`
|
|
|
|
type OpenDMParams struct {
|
|
UserID int64 `json:"userId"`
|
|
ChannelID int64 `json:"channelId"`
|
|
}
|
|
|
|
func (q *Queries) OpenDM(ctx context.Context, arg OpenDMParams) (int64, error) {
|
|
result, err := q.db.ExecContext(ctx, openDM, arg.UserID, arg.ChannelID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected()
|
|
}
|
|
|
|
const removeDMParticipant = `-- name: RemoveDMParticipant :exec
|
|
DELETE FROM dm_participants WHERE channel_id = ? AND user_id = ?
|
|
`
|
|
|
|
type RemoveDMParticipantParams struct {
|
|
ChannelID int64 `json:"channelId"`
|
|
UserID int64 `json:"userId"`
|
|
}
|
|
|
|
func (q *Queries) RemoveDMParticipant(ctx context.Context, arg RemoveDMParticipantParams) error {
|
|
_, err := q.db.ExecContext(ctx, removeDMParticipant, arg.ChannelID, arg.UserID)
|
|
return err
|
|
}
|
|
|
|
const setDMChannelName = `-- name: SetDMChannelName :exec
|
|
UPDATE channels SET name = ? WHERE id = ? AND type = 'dm'
|
|
`
|
|
|
|
type SetDMChannelNameParams struct {
|
|
Name string `json:"name"`
|
|
ID int64 `json:"id"`
|
|
}
|
|
|
|
func (q *Queries) SetDMChannelName(ctx context.Context, arg SetDMChannelNameParams) error {
|
|
_, err := q.db.ExecContext(ctx, setDMChannelName, arg.Name, arg.ID)
|
|
return err
|
|
}
|