refactor: remove low-value thin wrappers (#426)

## Summary

- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries

## Validation

- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
pull/427/head
zijiren 1 month ago committed by GitHub
parent b74e8c279c
commit 8711158d60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT pg_notify($1, '')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pg_notify",
"type_info": "Void",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "0194202f1e08d10cc50aaa92568bb9bcbb219b722e4570198fd9b75d3adc9a85"
}

@ -1,256 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT u.id AS \"id!: UserId\",\n u.username AS \"username!\",\n u.signup_method AS \"signup_method!: SignupMethod\",\n u.role AS \"role!: UserRole\",\n u.avatar_file_reference_id,\n u.status AS \"status!: UserStatus\",\n u.is_banned AS \"is_banned!\",\n u.banned_at,\n u.banned_by AS \"banned_by?: UserId\",\n u.banned_reason,\n u.created_at AS \"created_at!\",\n u.updated_at AS \"updated_at!\",\n u.version AS \"user_version!\",\n u.deleted_at,\n apc.changed_at,\n apc.version AS credential_version,\n apc.opaque_record,\n apc.opaque_credential_identifier,\n apc.opaque_ciphersuite,\n apc.opaque_server_setup_version\n FROM user_account_profiles u\n LEFT JOIN auth_password_credentials apc ON apc.user_id = u.id\n JOIN auth_email_identities aei ON aei.user_id = u.id\n WHERE LOWER(aei.email) = LOWER($1) AND u.deleted_at IS NULL AND aei.deleted_at IS NULL\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "id"
}
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "username"
}
}
},
{
"ordinal": 2,
"name": "signup_method!: SignupMethod",
"type_info": "Int2",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "signup_method"
}
}
},
{
"ordinal": 3,
"name": "role!: UserRole",
"type_info": "Int2",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "role"
}
}
},
{
"ordinal": 4,
"name": "avatar_file_reference_id",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "avatar_file_reference_id"
}
}
},
{
"ordinal": 5,
"name": "status!: UserStatus",
"type_info": "Int2",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "status"
}
}
},
{
"ordinal": 6,
"name": "is_banned!",
"type_info": "Bool",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "is_banned"
}
}
},
{
"ordinal": 7,
"name": "banned_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "banned_at"
}
}
},
{
"ordinal": 8,
"name": "banned_by?: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "banned_by"
}
}
},
{
"ordinal": 9,
"name": "banned_reason",
"type_info": "Text",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "banned_reason"
}
}
},
{
"ordinal": 10,
"name": "created_at!",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "created_at"
}
}
},
{
"ordinal": 11,
"name": "updated_at!",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "updated_at"
}
}
},
{
"ordinal": 12,
"name": "user_version!",
"type_info": "Int4",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "version"
}
}
},
{
"ordinal": 13,
"name": "deleted_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "deleted_at"
}
}
},
{
"ordinal": 14,
"name": "changed_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "changed_at"
}
}
},
{
"ordinal": 15,
"name": "credential_version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "version"
}
}
},
{
"ordinal": 16,
"name": "opaque_record",
"type_info": "Bytea",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_record"
}
}
},
{
"ordinal": 17,
"name": "opaque_credential_identifier",
"type_info": "Bytea",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_credential_identifier"
}
}
},
{
"ordinal": 18,
"name": "opaque_ciphersuite",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_ciphersuite"
}
}
},
{
"ordinal": 19,
"name": "opaque_server_setup_version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_server_setup_version"
}
}
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "01f9147da87665afe8c31aceec7c18b82f77a1d474ca1d043068383001181eb3"
}

@ -1,19 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE playback_source_metadata\n SET media_name = COALESCE($5, media_name),\n playlist_name = COALESCE($6, playlist_name),\n updated_at = CURRENT_TIMESTAMP,\n version = version + 1\n WHERE room_id = $1\n AND media_id IS NOT DISTINCT FROM $2\n AND playlist_id IS NOT DISTINCT FROM $3\n AND target_hash = $4\n AND (media_name IS DISTINCT FROM COALESCE($5, media_name)\n OR playlist_name IS DISTINCT FROM COALESCE($6, playlist_name))",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8",
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "0ad1b26b4dce7572be0a7b50500cecbd1a4f24adfa38ecb6df81e5b925a80e56"
}

@ -1,272 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT r.id AS \"id: RoomId\",\n r.name,\n r.description,\n r.cover_file_reference_id,\n rc.id AS \"category_id?: RoomCategoryId\",\n rc.key AS \"category_key?\",\n rc.name AS \"category_name?\",\n rc.description AS \"category_description?\",\n rc.sort_order AS \"category_sort_order?\",\n rc.is_enabled AS \"category_is_enabled?\",\n rc.created_at AS \"category_created_at?\",\n rc.updated_at AS \"category_updated_at?\",\n r.created_by AS \"created_by: UserId\",\n r.closed_at,\n r.created_at,\n r.updated_at,\n r.deleted_at,\n r.version,\n r.last_activity_at,\n r.is_public,\n EXISTS (\n SELECT 1 FROM room_bans rb\n WHERE rb.room_id = r.id\n AND rb.revoked_at IS NULL\n AND (rb.ends_at IS NULL OR rb.ends_at > CURRENT_TIMESTAMP)\n ) AS \"is_banned!\",\n COALESCE(COUNT(rm.user_id), 0)::int AS \"member_count!\"\n FROM rooms r\n LEFT JOIN room_categories rc ON rc.id = r.category_id\n LEFT JOIN room_members rm ON r.id = rm.room_id\n WHERE r.created_by = $1 AND r.deleted_at IS NULL\n GROUP BY r.id, r.name, r.description, r.cover_file_reference_id, r.category_id,\n rc.id, rc.key, rc.name, rc.description, rc.sort_order, rc.is_enabled,\n rc.created_at, rc.updated_at, r.created_by,\n r.closed_at, r.created_at, r.updated_at, r.deleted_at, r.version,\n r.last_activity_at, r.is_public\n ORDER BY r.created_at DESC\n LIMIT $2 OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: RoomId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "rooms",
"name": "id"
}
}
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text",
"origin": {
"Table": {
"table": "rooms",
"name": "name"
}
}
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text",
"origin": {
"Table": {
"table": "rooms",
"name": "description"
}
}
},
{
"ordinal": 3,
"name": "cover_file_reference_id",
"type_info": "Int8",
"origin": {
"Table": {
"table": "rooms",
"name": "cover_file_reference_id"
}
}
},
{
"ordinal": 4,
"name": "category_id?: RoomCategoryId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_categories",
"name": "id"
}
}
},
{
"ordinal": 5,
"name": "category_key?",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_categories",
"name": "key"
}
}
},
{
"ordinal": 6,
"name": "category_name?",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_categories",
"name": "name"
}
}
},
{
"ordinal": 7,
"name": "category_description?",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_categories",
"name": "description"
}
}
},
{
"ordinal": 8,
"name": "category_sort_order?",
"type_info": "Int4",
"origin": {
"Table": {
"table": "room_categories",
"name": "sort_order"
}
}
},
{
"ordinal": 9,
"name": "category_is_enabled?",
"type_info": "Bool",
"origin": {
"Table": {
"table": "room_categories",
"name": "is_enabled"
}
}
},
{
"ordinal": 10,
"name": "category_created_at?",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "room_categories",
"name": "created_at"
}
}
},
{
"ordinal": 11,
"name": "category_updated_at?",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "room_categories",
"name": "updated_at"
}
}
},
{
"ordinal": 12,
"name": "created_by: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "rooms",
"name": "created_by"
}
}
},
{
"ordinal": 13,
"name": "closed_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "rooms",
"name": "closed_at"
}
}
},
{
"ordinal": 14,
"name": "created_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "rooms",
"name": "created_at"
}
}
},
{
"ordinal": 15,
"name": "updated_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "rooms",
"name": "updated_at"
}
}
},
{
"ordinal": 16,
"name": "deleted_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "rooms",
"name": "deleted_at"
}
}
},
{
"ordinal": 17,
"name": "version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "rooms",
"name": "version"
}
}
},
{
"ordinal": 18,
"name": "last_activity_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "rooms",
"name": "last_activity_at"
}
}
},
{
"ordinal": 19,
"name": "is_public",
"type_info": "Bool",
"origin": {
"Table": {
"table": "rooms",
"name": "is_public"
}
}
},
{
"ordinal": 20,
"name": "is_banned!",
"type_info": "Bool",
"origin": "Expression"
},
{
"ordinal": 21,
"name": "member_count!",
"type_info": "Int4",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
false,
null,
null
]
},
"hash": "0c9323dd554bcd5334ced885030c67831780004bc9d60a9a1562a29dd059f2ba"
}

@ -1,90 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id AS \"id: RoomId\",\n requested_by AS \"requested_by: UserId\",\n name,\n description,\n is_public,\n requested_at\n FROM room_creation_requests\n WHERE reviewed_at IS NULL AND status = $1\n ORDER BY requested_at DESC, id DESC\n LIMIT $2 OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: RoomId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_creation_requests",
"name": "id"
}
}
},
{
"ordinal": 1,
"name": "requested_by: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_creation_requests",
"name": "requested_by"
}
}
},
{
"ordinal": 2,
"name": "name",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_creation_requests",
"name": "name"
}
}
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_creation_requests",
"name": "description"
}
}
},
{
"ordinal": 4,
"name": "is_public",
"type_info": "Bool",
"origin": {
"Table": {
"table": "room_creation_requests",
"name": "is_public"
}
}
},
{
"ordinal": 5,
"name": "requested_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "room_creation_requests",
"name": "requested_at"
}
}
}
],
"parameters": {
"Left": [
"Int2",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "0cada6649ced39553d548fd9dffe836e3dc9c7ce2e9ad76afd3c27637c4b840d"
}

@ -1,126 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT storage_backend, object_key, mime_type, size_bytes,\n content_manifest_sha256, metadata AS \"metadata!: FileMetadata\",\n created_at, validated_at, deleting_at\n FROM file_objects\n WHERE storage_backend = $1\n AND content_manifest_sha256 = $2\n AND size_bytes = $3\n AND deleting_at IS NULL\n ORDER BY validated_at DESC NULLS LAST, created_at ASC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "storage_backend",
"type_info": "Text",
"origin": {
"Table": {
"table": "file_objects",
"name": "storage_backend"
}
}
},
{
"ordinal": 1,
"name": "object_key",
"type_info": "Text",
"origin": {
"Table": {
"table": "file_objects",
"name": "object_key"
}
}
},
{
"ordinal": 2,
"name": "mime_type",
"type_info": "Text",
"origin": {
"Table": {
"table": "file_objects",
"name": "mime_type"
}
}
},
{
"ordinal": 3,
"name": "size_bytes",
"type_info": "Int8",
"origin": {
"Table": {
"table": "file_objects",
"name": "size_bytes"
}
}
},
{
"ordinal": 4,
"name": "content_manifest_sha256",
"type_info": "Text",
"origin": {
"Table": {
"table": "file_objects",
"name": "content_manifest_sha256"
}
}
},
{
"ordinal": 5,
"name": "metadata!: FileMetadata",
"type_info": "Jsonb",
"origin": {
"Table": {
"table": "file_objects",
"name": "metadata"
}
}
},
{
"ordinal": 6,
"name": "created_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "file_objects",
"name": "created_at"
}
}
},
{
"ordinal": 7,
"name": "validated_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "file_objects",
"name": "validated_at"
}
}
},
{
"ordinal": 8,
"name": "deleting_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "file_objects",
"name": "deleting_at"
}
}
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "2c795c623c32965a20e3e24e2735229964e00a93eb6765d9e177160a38a57700"
}

@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT m.playlist_id AS \"playlist_id!: PlaylistId\", COUNT(*) AS \"cnt!\"\n FROM media m\n LEFT JOIN users u\n ON m.creator_id = u.id\n AND u.deleted_at IS NULL\n WHERE m.playlist_id = ANY($1)\n AND m.deleted_at IS NULL\n AND (m.creator_id IS NULL OR (\n u.id IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM user_bans ub\n WHERE ub.user_id = u.id\n AND ub.revoked_at IS NULL\n AND (ub.ends_at IS NULL OR ub.ends_at > CURRENT_TIMESTAMP)\n )\n AND EXISTS (\n SELECT 1 FROM room_members rm\n WHERE rm.room_id = m.room_id AND rm.user_id = m.creator_id\n )\n AND NOT EXISTS (\n SELECT 1 FROM room_member_kick_cooldowns cooldown\n WHERE cooldown.room_id = m.room_id\n AND cooldown.user_id = m.creator_id\n AND cooldown.ends_at > CURRENT_TIMESTAMP\n )\n ))\n GROUP BY m.playlist_id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "playlist_id!: PlaylistId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "media",
"name": "playlist_id"
}
}
},
{
"ordinal": 1,
"name": "cnt!",
"type_info": "Int8",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
true,
null
]
},
"hash": "2e0be9d959744d54c074ea55d34740cadd1de09b4c6579eee13344bae2a7baa8"
}

@ -1,256 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT u.id AS \"id!: UserId\",\n u.username AS \"username!\",\n u.signup_method AS \"signup_method!: SignupMethod\",\n u.role AS \"role!: UserRole\",\n u.avatar_file_reference_id,\n u.status AS \"status!: UserStatus\",\n u.is_banned AS \"is_banned!\",\n u.banned_at,\n u.banned_by AS \"banned_by?: UserId\",\n u.banned_reason,\n u.created_at AS \"created_at!\",\n u.updated_at AS \"updated_at!\",\n u.version AS \"user_version!\",\n u.deleted_at,\n apc.changed_at,\n apc.version AS credential_version,\n apc.opaque_record,\n apc.opaque_credential_identifier,\n apc.opaque_ciphersuite,\n apc.opaque_server_setup_version\n FROM user_account_profiles u\n LEFT JOIN auth_password_credentials apc ON apc.user_id = u.id\n WHERE LOWER(u.username) = LOWER($1) AND u.deleted_at IS NULL\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "id"
}
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "username"
}
}
},
{
"ordinal": 2,
"name": "signup_method!: SignupMethod",
"type_info": "Int2",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "signup_method"
}
}
},
{
"ordinal": 3,
"name": "role!: UserRole",
"type_info": "Int2",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "role"
}
}
},
{
"ordinal": 4,
"name": "avatar_file_reference_id",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "avatar_file_reference_id"
}
}
},
{
"ordinal": 5,
"name": "status!: UserStatus",
"type_info": "Int2",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "status"
}
}
},
{
"ordinal": 6,
"name": "is_banned!",
"type_info": "Bool",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "is_banned"
}
}
},
{
"ordinal": 7,
"name": "banned_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "banned_at"
}
}
},
{
"ordinal": 8,
"name": "banned_by?: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "banned_by"
}
}
},
{
"ordinal": 9,
"name": "banned_reason",
"type_info": "Text",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "banned_reason"
}
}
},
{
"ordinal": 10,
"name": "created_at!",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "created_at"
}
}
},
{
"ordinal": 11,
"name": "updated_at!",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "updated_at"
}
}
},
{
"ordinal": 12,
"name": "user_version!",
"type_info": "Int4",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "version"
}
}
},
{
"ordinal": 13,
"name": "deleted_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_account_profiles",
"name": "deleted_at"
}
}
},
{
"ordinal": 14,
"name": "changed_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "changed_at"
}
}
},
{
"ordinal": 15,
"name": "credential_version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "version"
}
}
},
{
"ordinal": 16,
"name": "opaque_record",
"type_info": "Bytea",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_record"
}
}
},
{
"ordinal": 17,
"name": "opaque_credential_identifier",
"type_info": "Bytea",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_credential_identifier"
}
}
},
{
"ordinal": 18,
"name": "opaque_ciphersuite",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_ciphersuite"
}
}
},
{
"ordinal": 19,
"name": "opaque_server_setup_version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "auth_password_credentials",
"name": "opaque_server_setup_version"
}
}
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
true,
true
]
},
"hash": "343f8c888ca3221de7ad8d23b3788ce90d7c49a97370335d617e45c5f6b09c16"
}

@ -1,136 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id AS \"id: RoomLabelId\",\n key,\n name,\n description,\n color,\n category_id AS \"category_id: RoomCategoryId\",\n sort_order,\n is_enabled,\n created_at,\n updated_at\n FROM room_labels\n WHERE id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: RoomLabelId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_labels",
"name": "id"
}
}
},
{
"ordinal": 1,
"name": "key",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_labels",
"name": "key"
}
}
},
{
"ordinal": 2,
"name": "name",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_labels",
"name": "name"
}
}
},
{
"ordinal": 3,
"name": "description",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_labels",
"name": "description"
}
}
},
{
"ordinal": 4,
"name": "color",
"type_info": "Text",
"origin": {
"Table": {
"table": "room_labels",
"name": "color"
}
}
},
{
"ordinal": 5,
"name": "category_id: RoomCategoryId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_labels",
"name": "category_id"
}
}
},
{
"ordinal": 6,
"name": "sort_order",
"type_info": "Int4",
"origin": {
"Table": {
"table": "room_labels",
"name": "sort_order"
}
}
},
{
"ordinal": 7,
"name": "is_enabled",
"type_info": "Bool",
"origin": {
"Table": {
"table": "room_labels",
"name": "is_enabled"
}
}
},
{
"ordinal": 8,
"name": "created_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "room_labels",
"name": "created_at"
}
}
},
{
"ordinal": 9,
"name": "updated_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "room_labels",
"name": "updated_at"
}
}
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "35c1b59f2c0ce328e34191c214842a818135f51cd20a94959f85077b3b43793e"
}

@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM chat_messages\n WHERE created_at <= NOW() - INTERVAL '90 days'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "401132bddf01263a4f25442e6cc9c829bedfd65b7018d84d1d5921c553a062ed"
}

@ -1,122 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, user_id as \"user_id: UserId\", provider, server_id,\n provider_instance_name, credential_data as \"credential_data!: StoredProviderCredential\",\n expires_at, created_at, updated_at\n FROM user_media_provider_credentials\n WHERE expires_at IS NOT NULL AND expires_at <= NOW()\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "id"
}
}
},
{
"ordinal": 1,
"name": "user_id: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "user_id"
}
}
},
{
"ordinal": 2,
"name": "provider",
"type_info": "Int2",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "provider"
}
}
},
{
"ordinal": 3,
"name": "server_id",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "server_id"
}
}
},
{
"ordinal": 4,
"name": "provider_instance_name",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "provider_instance_name"
}
}
},
{
"ordinal": 5,
"name": "credential_data!: StoredProviderCredential",
"type_info": "Jsonb",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "credential_data"
}
}
},
{
"ordinal": 6,
"name": "expires_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "expires_at"
}
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "created_at"
}
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "user_media_provider_credentials",
"name": "updated_at"
}
}
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
true,
false,
true,
false,
false
]
},
"hash": "4259c0ae2ac799413e94e3de941de517edafa6f59ea6dfdbb88f9fc0a67810d6"
}

@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO room_member_versions (room_id, user_id, version, is_member, updated_at)\n VALUES ($1, $2, 1, FALSE, CURRENT_TIMESTAMP)\n ON CONFLICT (room_id, user_id) DO UPDATE\n SET version = room_member_versions.version + 1,\n is_member = FALSE,\n updated_at = CURRENT_TIMESTAMP\n RETURNING version",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_member_versions",
"name": "version"
}
}
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "435a2a76e85d82bcb900c5d4f8577d0b2751e042fcf3c7ad99bc7c3f2067cd27"
}

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*)\n FROM playlists p\n WHERE p.room_id = $1\n AND p.parent_id IS NULL\n AND p.deleted_at IS NULL\n AND EXISTS (SELECT 1 FROM rooms r WHERE r.id = p.room_id AND r.deleted_at IS NULL)\n AND (p.creator_id IS NULL OR EXISTS (\n SELECT 1 FROM users u WHERE u.id = p.creator_id AND u.deleted_at IS NULL\n ))\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "4c4bffb1f635495a48fb09c8b3a77344e6cffbfb03f308aff7e80fd6b30c5871"
}

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*) AS \"count!\"\n FROM media m\n LEFT JOIN users u\n ON m.creator_id = u.id\n AND u.deleted_at IS NULL\n WHERE m.playlist_id = $1\n AND m.deleted_at IS NULL\n AND (m.creator_id IS NULL OR (\n u.id IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM user_bans ub\n WHERE ub.user_id = u.id\n AND ub.revoked_at IS NULL\n AND (ub.ends_at IS NULL OR ub.ends_at > CURRENT_TIMESTAMP)\n )\n AND EXISTS (\n SELECT 1 FROM room_members rm\n WHERE rm.room_id = m.room_id AND rm.user_id = m.creator_id\n )\n AND NOT EXISTS (\n SELECT 1 FROM room_member_kick_cooldowns cooldown\n WHERE cooldown.room_id = m.room_id\n AND cooldown.user_id = m.creator_id\n AND cooldown.ends_at > CURRENT_TIMESTAMP\n )\n ))\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "592f2cbef4f1008c4ccacb87b858206df9fa7679d90a5a23d364d89ece148018"
}

@ -1,92 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO settings (key, group_name, value, version)\n VALUES ($1, $2, $3, $5)\n ON CONFLICT (key) DO UPDATE\n SET group_name = EXCLUDED.group_name,\n value = EXCLUDED.value,\n version = EXCLUDED.version,\n updated_at = NOW()\n WHERE settings.version = $4\n RETURNING key, group_name, value, version, created_at, updated_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "key",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "settings",
"name": "key"
}
}
},
{
"ordinal": 1,
"name": "group_name",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "settings",
"name": "group_name"
}
}
},
{
"ordinal": 2,
"name": "value",
"type_info": "Text",
"origin": {
"Table": {
"table": "settings",
"name": "value"
}
}
},
{
"ordinal": 3,
"name": "version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "settings",
"name": "version"
}
}
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "settings",
"name": "created_at"
}
}
},
{
"ordinal": 5,
"name": "updated_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "settings",
"name": "updated_at"
}
}
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Int4",
"Int4"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "69fad7386e59ebd38925b6d3b203beaf3f1051f76eea07e39838ebd9f45c0b54"
}

@ -1,193 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id as \"id: PlaylistId\",\n room_id as \"room_id: RoomId\",\n creator_id as \"creator_id: crate::models::UserId\",\n name,\n description,\n cover_file_reference_id,\n parent_id as \"parent_id: PlaylistId\",\n position,\n browse_access_mode,\n source_provider,\n source_config as \"source_config: crate::models::PlaylistSourceConfig\",\n NULLIF(provider_instance_name, '') AS \"provider_instance_name?\",\n created_at,\n updated_at,\n version\n FROM playlists p\n WHERE p.room_id = $1\n AND p.parent_id IS NULL\n AND p.deleted_at IS NULL\n AND EXISTS (SELECT 1 FROM rooms r WHERE r.id = p.room_id AND r.deleted_at IS NULL)\n AND (p.creator_id IS NULL OR EXISTS (\n SELECT 1 FROM users u WHERE u.id = p.creator_id AND u.deleted_at IS NULL\n ))\n ORDER BY position ASC\n LIMIT $2 OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: PlaylistId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "playlists",
"name": "id"
}
}
},
{
"ordinal": 1,
"name": "room_id: RoomId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "playlists",
"name": "room_id"
}
}
},
{
"ordinal": 2,
"name": "creator_id: crate::models::UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "playlists",
"name": "creator_id"
}
}
},
{
"ordinal": 3,
"name": "name",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "playlists",
"name": "name"
}
}
},
{
"ordinal": 4,
"name": "description",
"type_info": "Text",
"origin": {
"Table": {
"table": "playlists",
"name": "description"
}
}
},
{
"ordinal": 5,
"name": "cover_file_reference_id",
"type_info": "Int8",
"origin": {
"Table": {
"table": "playlists",
"name": "cover_file_reference_id"
}
}
},
{
"ordinal": 6,
"name": "parent_id: PlaylistId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "playlists",
"name": "parent_id"
}
}
},
{
"ordinal": 7,
"name": "position",
"type_info": "Float8",
"origin": {
"Table": {
"table": "playlists",
"name": "position"
}
}
},
{
"ordinal": 8,
"name": "browse_access_mode",
"type_info": "Int2",
"origin": {
"Table": {
"table": "playlists",
"name": "browse_access_mode"
}
}
},
{
"ordinal": 9,
"name": "source_provider",
"type_info": "Int2",
"origin": {
"Table": {
"table": "playlists",
"name": "source_provider"
}
}
},
{
"ordinal": 10,
"name": "source_config: crate::models::PlaylistSourceConfig",
"type_info": "Jsonb",
"origin": {
"Table": {
"table": "playlists",
"name": "source_config"
}
}
},
{
"ordinal": 11,
"name": "provider_instance_name?",
"type_info": "Text",
"origin": "Expression"
},
{
"ordinal": 12,
"name": "created_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "playlists",
"name": "created_at"
}
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "playlists",
"name": "updated_at"
}
}
},
{
"ordinal": 14,
"name": "version",
"type_info": "Int4",
"origin": {
"Table": {
"table": "playlists",
"name": "version"
}
}
}
],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
true,
false,
false,
true,
true,
false,
false,
true,
true,
null,
false,
false,
false
]
},
"hash": "751fc615b72a3955199302aa5b95fb595b4012ba03c6c73cf10b36f877ea319c"
}

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*) AS \"count!\"\n FROM room_creation_requests\n WHERE reviewed_at IS NULL AND status = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Int2"
]
},
"nullable": [
null
]
},
"hash": "7c0b1b7db07b030676a1511cd407f6ebbe7d09134b1da01d92248c9ec7add6df"
}

@ -1,150 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE room_members\n SET admin_added_permissions = admin_added_permissions | $3,\n version = version + 1\n WHERE room_id = $1 AND user_id = $2\n RETURNING room_id as \"room_id: RoomId\",\n user_id as \"user_id: UserId\",\n role as \"role: RoomRole\",\n added_permissions,\n removed_permissions,\n admin_added_permissions,\n admin_removed_permissions,\n remark_name,\n display_tag,\n joined_at,\n version",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "room_id: RoomId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "room_id"
}
}
},
{
"ordinal": 1,
"name": "user_id: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "user_id"
}
}
},
{
"ordinal": 2,
"name": "role: RoomRole",
"type_info": "Int2",
"origin": {
"Table": {
"table": "room_members",
"name": "role"
}
}
},
{
"ordinal": 3,
"name": "added_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "added_permissions"
}
}
},
{
"ordinal": 4,
"name": "removed_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "removed_permissions"
}
}
},
{
"ordinal": 5,
"name": "admin_added_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "admin_added_permissions"
}
}
},
{
"ordinal": 6,
"name": "admin_removed_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "admin_removed_permissions"
}
}
},
{
"ordinal": 7,
"name": "remark_name",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "room_members",
"name": "remark_name"
}
}
},
{
"ordinal": 8,
"name": "display_tag",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "room_members",
"name": "display_tag"
}
}
},
{
"ordinal": 9,
"name": "joined_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "room_members",
"name": "joined_at"
}
}
},
{
"ordinal": 10,
"name": "version",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "version"
}
}
}
],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "8817c0089bac303ddfd78872b339e0fc68e2de71ca96ecb273040095bb0bf533"
}

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n rm.room_id AS \"room_id: RoomId\",\n rm.user_id AS \"user_id: UserId\",\n rm.role AS \"role: RoomRole\",\n rm.added_permissions, rm.removed_permissions,\n rm.admin_added_permissions, rm.admin_removed_permissions,\n rm.joined_at,\n TRUE AS \"is_active!\",\n u.username,\n rm.remark_name,\n rm.display_tag\n FROM room_members rm\n JOIN users u ON rm.user_id = u.id\n WHERE rm.room_id = $1\n\t AND NOT EXISTS (\n\t SELECT 1 FROM room_member_kick_cooldowns rmkc\n\t WHERE rmkc.room_id = rm.room_id\n\t AND rmkc.user_id = rm.user_id\n\t AND rmkc.ends_at > CURRENT_TIMESTAMP\n\t )\n\t AND u.deleted_at IS NULL\n ORDER BY rm.joined_at ASC",
"query": "SELECT\n rm.room_id AS \"room_id: RoomId\",\n rm.user_id AS \"user_id: UserId\",\n rm.role AS \"role: RoomRole\",\n rm.added_permissions, rm.removed_permissions,\n rm.admin_added_permissions, rm.admin_removed_permissions,\n rm.joined_at,\n u.username,\n rm.remark_name,\n rm.display_tag\n FROM room_members rm\n JOIN users u ON rm.user_id = u.id\n WHERE rm.room_id = $1\n\t AND NOT EXISTS (\n\t SELECT 1 FROM room_member_kick_cooldowns rmkc\n\t WHERE rmkc.room_id = rm.room_id\n\t AND rmkc.user_id = rm.user_id\n\t AND rmkc.ends_at > CURRENT_TIMESTAMP\n\t )\n\t AND u.deleted_at IS NULL\n ORDER BY rm.joined_at ASC",
"describe": {
"columns": [
{
@ -93,12 +93,6 @@
},
{
"ordinal": 8,
"name": "is_active!",
"type_info": "Bool",
"origin": "Expression"
},
{
"ordinal": 9,
"name": "username",
"type_info": "Varchar",
"origin": {
@ -109,7 +103,7 @@
}
},
{
"ordinal": 10,
"ordinal": 9,
"name": "remark_name",
"type_info": "Varchar",
"origin": {
@ -120,7 +114,7 @@
}
},
{
"ordinal": 11,
"ordinal": 10,
"name": "display_tag",
"type_info": "Varchar",
"origin": {
@ -145,11 +139,10 @@
false,
false,
false,
null,
false,
false,
false
]
},
"hash": "92ed8a87ed143c1b97cd92833f15e74de76d4a427b9f705cd37163ac36e3c053"
"hash": "98eb7d9d0e0ddd562e9000c3bb2e49a78edd4b978369f63d8ddf73b9089d8cef"
}

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id AS \"id: RoomCategoryId\"\n FROM room_categories\n WHERE key = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: RoomCategoryId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_categories",
"name": "id"
}
}
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "9bd776773d8ee6c829213755809ce2426028cf5a9ba451dd9f4f7e2c84e99932"
}

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM chat_messages\n WHERE room_id = $1 AND id = $2 AND created_at = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Timestamptz"
]
},
"nullable": []
},
"hash": "b2099f83bae2b385165495f3e2db84b2b542e0410dc68d2dd46d59a5f640c9ed"
}

@ -1,150 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE room_members\n SET admin_removed_permissions = admin_removed_permissions | $3,\n version = version + 1\n WHERE room_id = $1 AND user_id = $2\n RETURNING room_id as \"room_id: RoomId\",\n user_id as \"user_id: UserId\",\n role as \"role: RoomRole\",\n added_permissions,\n removed_permissions,\n admin_added_permissions,\n admin_removed_permissions,\n remark_name,\n display_tag,\n joined_at,\n version",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "room_id: RoomId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "room_id"
}
}
},
{
"ordinal": 1,
"name": "user_id: UserId",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "user_id"
}
}
},
{
"ordinal": 2,
"name": "role: RoomRole",
"type_info": "Int2",
"origin": {
"Table": {
"table": "room_members",
"name": "role"
}
}
},
{
"ordinal": 3,
"name": "added_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "added_permissions"
}
}
},
{
"ordinal": 4,
"name": "removed_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "removed_permissions"
}
}
},
{
"ordinal": 5,
"name": "admin_added_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "admin_added_permissions"
}
}
},
{
"ordinal": 6,
"name": "admin_removed_permissions",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "admin_removed_permissions"
}
}
},
{
"ordinal": 7,
"name": "remark_name",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "room_members",
"name": "remark_name"
}
}
},
{
"ordinal": 8,
"name": "display_tag",
"type_info": "Varchar",
"origin": {
"Table": {
"table": "room_members",
"name": "display_tag"
}
}
},
{
"ordinal": 9,
"name": "joined_at",
"type_info": "Timestamptz",
"origin": {
"Table": {
"table": "room_members",
"name": "joined_at"
}
}
},
{
"ordinal": 10,
"name": "version",
"type_info": "Int8",
"origin": {
"Table": {
"table": "room_members",
"name": "version"
}
}
}
],
"parameters": {
"Left": [
"Int8",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "c939ee0cc41a3cc6606836e1e31c54756e8d8bfbca9cf4f15c07d9e9ea4dd3a7"
}

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n rm.room_id AS \"room_id: RoomId\",\n rm.user_id AS \"user_id: UserId\",\n rm.role AS \"role: RoomRole\",\n rm.added_permissions, rm.removed_permissions,\n rm.admin_added_permissions, rm.admin_removed_permissions,\n rm.remark_name,\n rm.display_tag,\n rm.joined_at,\n u.username,\n TRUE AS \"is_active!\"\n FROM room_members rm\n JOIN users u ON rm.user_id = u.id\n WHERE rm.room_id = $1 AND u.deleted_at IS NULL\n ORDER BY rm.joined_at ASC",
"query": "SELECT\n rm.room_id AS \"room_id: RoomId\",\n rm.user_id AS \"user_id: UserId\",\n rm.role AS \"role: RoomRole\",\n rm.added_permissions, rm.removed_permissions,\n rm.admin_added_permissions, rm.admin_removed_permissions,\n rm.remark_name,\n rm.display_tag,\n rm.joined_at,\n u.username\n FROM room_members rm\n JOIN users u ON rm.user_id = u.id\n WHERE rm.room_id = $1 AND u.deleted_at IS NULL\n ORDER BY rm.joined_at ASC",
"describe": {
"columns": [
{
@ -123,12 +123,6 @@
"name": "username"
}
}
},
{
"ordinal": 11,
"name": "is_active!",
"type_info": "Bool",
"origin": "Expression"
}
],
"parameters": {
@ -147,9 +141,8 @@
false,
false,
false,
false,
null
false
]
},
"hash": "494340f5b7481c98dd7259988cd8d137de881b21298af9c3d9cc4779de922a34"
"hash": "eb0c8329b5bae62f9a8aef48f3777fb0b3277d87e457049d6ccaf31edfa5413f"
}

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM auth_webauthn_credentials WHERE user_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8",
"origin": "Expression"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "ee462ce207bfb46003a330492bf9fa4bf0527c60f9d6a4ac376d78e06b83b0c7"
}

@ -3087,24 +3087,6 @@ dependencies = [
"url",
]
[[package]]
name = "http-acl-reqwest"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729f533eb3acdabd86497d444853185f4bdd320bae44ded1afca41010de55cdd"
dependencies = [
"anyhow",
"async-trait",
"bytes",
"http",
"http-acl",
"percent-encoding",
"reqwest",
"reqwest-middleware",
"thiserror 2.0.20",
"url",
]
[[package]]
name = "http-auth"
version = "0.1.10"
@ -6102,20 +6084,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "reqwest-middleware"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc3f1384cffa4f274dad2d4ddd73aed32fed8f786d96c6be8aa4e5fd3c3b58"
dependencies = [
"anyhow",
"async-trait",
"http",
"reqwest",
"thiserror 2.0.20",
"tower-service",
]
[[package]]
name = "resolv-conf"
version = "0.7.6"
@ -7630,7 +7598,6 @@ dependencies = [
"axum",
"http",
"serde",
"serde_json",
"sqids",
"synctv-common",
"synctv-core",
@ -7960,7 +7927,6 @@ dependencies = [
"chrono",
"chrono-tz",
"http-acl",
"http-acl-reqwest",
"iana-time-zone",
"ipnet",
"nanoid",
@ -7989,22 +7955,17 @@ dependencies = [
"dashmap 7.0.0-rc2",
"failsafe",
"futures",
"futures-util",
"governor",
"handlebars",
"hex",
"hkdf 0.13.0",
"hmac 0.13.0",
"hostname",
"humantime",
"image",
"indexmap 2.14.0",
"ipnet",
"jsonwebtoken",
"lettre",
"lru",
"lz4_flex",
"mockall",
"moka",
"nonzero_ext",
"oauth2",
@ -8012,7 +7973,6 @@ dependencies = [
"opendal",
"opendal-http-transport-reqwest",
"parking_lot",
"paste",
"percent-encoding",
"prometheus",
"rand 0.10.2",
@ -8039,7 +7999,6 @@ dependencies = [
"tokio-rustls",
"tokio-socks",
"tokio-util",
"tower",
"tracing",
"tracing-appender",
"tracing-subscriber",
@ -8253,7 +8212,6 @@ dependencies = [
"pbjson",
"pbjson-types",
"prost",
"prost-protovalidate",
"prost-reflect",
"prost-types",
"serde",
@ -8293,7 +8251,6 @@ dependencies = [
"pbjson",
"pbjson-types",
"prost",
"prost-protovalidate",
"prost-reflect",
"prost-types",
"serde",
@ -8322,7 +8279,6 @@ dependencies = [
"percent-encoding",
"prost",
"quick-xml",
"rand 0.10.2",
"rayon",
"reqwest",
"serde",
@ -8330,13 +8286,11 @@ dependencies = [
"subtle",
"synctv-common",
"tempfile",
"thiserror 2.0.20",
"tokio",
"tokio-util",
"tonic",
"tonic-prost",
"tonic-prost-build",
"tower",
"tracing",
"url",
"wiremock",
@ -8352,20 +8306,16 @@ dependencies = [
"criterion",
"dashmap 7.0.0-rc2",
"futures",
"mockall",
"moka",
"parking_lot",
"prost",
"rand 0.10.2",
"redis",
"serde",
"serde_json",
"subtle",
"synctv-cluster",
"synctv-common",
"synctv-core",
"synctv-core-testing",
"tempfile",
"thiserror 2.0.20",
"tokio",
"tokio-util",
@ -8381,10 +8331,8 @@ version = "1.0.2"
dependencies = [
"anyhow",
"async-trait",
"axum",
"backon",
"base64 0.23.1",
"bincode",
"byteorder",
"bytes",
"chrono",
@ -8415,7 +8363,6 @@ dependencies = [
"tempfile",
"thiserror 2.0.20",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"url",

@ -107,7 +107,7 @@ tonic-prost = "=0.14.6"
tonic-prost-build = "=0.14.6"
prost = "=0.14.4"
prost-types = "=0.14.4"
prost-reflect = "=0.16.5"
prost-reflect = { version = "=0.16.5", features = ["derive"] }
prost-reflect-build = "=0.16.1"
prost-protovalidate = "=0.6.0"
pbjson = "=0.9.0"

@ -25,13 +25,10 @@ Disabled or unused features may not emit their metrics. Do not expose the metric
| --- | --- | --- | --- |
| `http_requests_total` | counter | `method`, `path`, `status` | HTTP requests |
| `http_request_duration_seconds` | histogram | `method`, `path` | HTTP request duration |
| `http_error_rate_total` | counter | `method`, `path`, `error_type` | HTTP errors |
| `http_requests_in_flight` | gauge | none | In-flight HTTP requests |
| `websocket_connections_active` | gauge | none | Active WebSocket connections |
| `websocket_connections_total` | counter | `status` | WebSocket connection attempts |
| `websocket_messages_total` | counter | `direction`, `type` | WebSocket messages |
| `websocket_errors_total` | counter | `error_type` | WebSocket errors |
| `websocket_connection_duration_seconds` | histogram | none | WebSocket connection duration |
| `users_online` | gauge | none | Online users |
| `rooms_active` | gauge | none | Active rooms |
@ -39,19 +36,13 @@ Disabled or unused features may not emit their metrics. Do not expose the metric
| Metric | Type | Labels | Meaning |
| --- | --- | --- | --- |
| `db_query_duration_seconds` | histogram | `operation`, `table` | Database query duration |
| `db_operations_total` | counter | `operation`, `table`, `result` | Database operations |
| `db_connections_active` | gauge | none | Active DB connections |
| `db_connections_idle` | gauge | none | Idle DB connections |
| `db_connections_waiting` | gauge | none | Requests waiting for a connection |
| `db_pool_utilization_ratio` | gauge | `pool` | Pool utilization, from 0 to 1 |
| `db_connection_acquire_duration_seconds` | histogram | `pool` | Connection acquire duration |
| `db_query_errors_total` | counter | `operation`, `error_type` | DB query errors |
| `cache_hits_total` | counter | `cache_type`, `level` | Cache hits |
| `cache_misses_total` | counter | `cache_type`, `level` | Cache misses |
| `cache_evictions_total` | counter | `cache_type` | Cache evictions |
| `cache_errors_total` | counter | `cache_type`, `operation` | Cache operation errors |
| `cache_fill_duration_seconds` | histogram | `cache_type` | Cache fill duration |
## Business And Rate Limits
@ -62,7 +53,6 @@ Disabled or unused features may not emit their metrics. Do not expose the metric
| `synctv_file_object_delete_failures_total` | counter | `origin`, `backend` | File object delete failures |
| `synctv_file_cleanup_jobs_due` | gauge | none | Due file cleanup jobs waiting for retry |
| `synctv_file_cleanup_jobs_total` | counter | `action`, `origin`, `backend` | File cleanup retry job actions |
| `playlist_items_total` | counter | none | Added playlist items |
| `webrtc_peers_active` | gauge | none | Active WebRTC peers |
| `active_connections` | gauge | none | Active connections |
| `spawned_task_panics_total` | counter | `task_name` | Background task panics caught by `spawn_monitored` |
@ -70,8 +60,6 @@ Disabled or unused features may not emit their metrics. Do not expose the metric
| `email_delivery_in_flight` | gauge | none | Email jobs being delivered by this instance |
| `email_delivery_jobs_total` | counter | `kind`, `status` | Email job outcomes: `sent`, `retry`, `dead`, `superseded`, `fenced`, `persist_failed`, or `ack_failed` |
| `email_delivery_duration_seconds` | histogram | `kind`, `status` | Delivery duration by job kind and outcome |
| `rate_limit_checks_total` | counter | `backend`, `category` | Rate-limit checks |
| `rate_limit_rejections_total` | counter | `backend`, `category` | Rate-limit rejections |
| `rate_limit_redis_fallbacks_total` | counter | `category` | In-memory fallback after Redis rate-limit errors |
## Cluster
@ -84,14 +72,10 @@ Disabled or unused features may not emit their metrics. Do not expose the metric
| `synctv_realtime_events_dropped_total` | counter | `reason` | Dropped realtime events |
| `synctv_cluster_heartbeat_failures` | gauge | none | Consecutive heartbeat failures |
| `synctv_node_active_rooms` | gauge | none | Active rooms on this node |
| `synctv_cluster_node_health_status` | gauge | none | Node health status; 1 means healthy |
| `synctv_cluster_leader_election_state` | gauge | none | Leader election state; 1 means leader |
| `synctv_cluster_leader_election_epoch` | gauge | none | Current leader epoch |
| `synctv_cluster_leader_election_consecutive_failures` | gauge | none | Consecutive leader election failures |
| `synctv_cluster_epoch_mismatch_quarantine` | gauge | none | Epoch mismatch quarantine state |
| `synctv_cluster_redis_pubsub_health` | gauge | none | Redis pub/sub connection health |
| `synctv_cluster_member_count` | gauge | none | Cluster member count |
| `synctv_cluster_sync_errors_total` | counter | `error_type` | Cluster synchronization errors |
## Media And Livestream
@ -103,11 +87,5 @@ Disabled or unused features may not emit their metrics. Do not expose the metric
| `streamhub_restarts_total` | counter | `reason` | StreamHub event loop restarts |
| `livestream_active_publishers` | gauge | none | Active livestream publishers |
| `livestream_active_viewers` | gauge | none | Active livestream viewers |
| `livestream_bytes_total` | counter | `direction` | Livestream bytes transferred |
| `livestream_stream_duration_seconds` | histogram | `stream_type` | Livestream session duration |
| `livestream_pull_errors_total` | counter | `error_type` | Livestream pull errors |
| `livestream_relay_frame_drops_total` | counter | none | Relay frame drops caused by backpressure |
| `gop_cache_size` | gauge | none | Current GOP cache entries |
| `gop_cache_drops_total` | counter | none | GOP cache evictions |
| `gop_cache_memory_bytes` | gauge | none | GOP cache memory usage |
| `livestream_flv_slow_client_terminations_total` | counter | none | FLV slow-client terminations |

@ -25,13 +25,10 @@ curl -fsS \
| --- | --- | --- | --- |
| `http_requests_total` | counter | `method`, `path`, `status` | HTTP 请求数 |
| `http_request_duration_seconds` | histogram | `method`, `path` | HTTP 请求耗时 |
| `http_error_rate_total` | counter | `method`, `path`, `error_type` | HTTP 错误数 |
| `http_requests_in_flight` | gauge | none | 正在处理的 HTTP 请求 |
| `websocket_connections_active` | gauge | none | 当前 WebSocket 连接 |
| `websocket_connections_total` | counter | `status` | WebSocket 建连次数 |
| `websocket_messages_total` | counter | `direction`, `type` | WebSocket 消息数 |
| `websocket_errors_total` | counter | `error_type` | WebSocket 错误数 |
| `websocket_connection_duration_seconds` | histogram | none | WebSocket 连接持续时间 |
| `users_online` | gauge | none | 在线用户 |
| `rooms_active` | gauge | none | 活跃房间 |
@ -39,19 +36,13 @@ curl -fsS \
| 指标 | 类型 | Labels | 含义 |
| --- | --- | --- | --- |
| `db_query_duration_seconds` | histogram | `operation`, `table` | 数据库查询耗时 |
| `db_operations_total` | counter | `operation`, `table`, `result` | 数据库操作数 |
| `db_connections_active` | gauge | none | 活跃数据库连接 |
| `db_connections_idle` | gauge | none | 空闲数据库连接 |
| `db_connections_waiting` | gauge | none | 等待数据库连接的请求 |
| `db_pool_utilization_ratio` | gauge | `pool` | 连接池利用率,取值 0 到 1 |
| `db_connection_acquire_duration_seconds` | histogram | `pool` | 获取连接耗时 |
| `db_query_errors_total` | counter | `operation`, `error_type` | 数据库查询错误数 |
| `cache_hits_total` | counter | `cache_type`, `level` | 缓存命中数 |
| `cache_misses_total` | counter | `cache_type`, `level` | 缓存未命中数 |
| `cache_evictions_total` | counter | `cache_type` | 缓存淘汰数 |
| `cache_errors_total` | counter | `cache_type`, `operation` | 缓存操作错误数 |
| `cache_fill_duration_seconds` | histogram | `cache_type` | 缓存填充耗时 |
## 业务和限流
@ -62,7 +53,6 @@ curl -fsS \
| `synctv_file_object_delete_failures_total` | counter | `origin`, `backend` | 文件对象删除失败数 |
| `synctv_file_cleanup_jobs_due` | gauge | none | 到期等待重试的文件清理任务数 |
| `synctv_file_cleanup_jobs_total` | counter | `action`, `origin`, `backend` | 文件清理重试任务动作数 |
| `playlist_items_total` | counter | none | 播放列表项新增数 |
| `webrtc_peers_active` | gauge | none | 活跃 WebRTC peer |
| `active_connections` | gauge | none | 活跃连接 |
| `spawned_task_panics_total` | counter | `task_name` | `spawn_monitored` 捕获的后台任务 panic |
@ -70,8 +60,6 @@ curl -fsS \
| `email_delivery_in_flight` | gauge | none | 当前实例正在投递的邮件任务数 |
| `email_delivery_jobs_total` | counter | `kind`, `status` | 邮件任务结果数;状态包括 `sent`、`retry`、`dead`、`superseded`、`fenced`、`persist_failed` 和 `ack_failed` |
| `email_delivery_duration_seconds` | histogram | `kind`, `status` | 按任务类型和结果统计的投递耗时 |
| `rate_limit_checks_total` | counter | `backend`, `category` | 限流检查数 |
| `rate_limit_rejections_total` | counter | `backend`, `category` | 限流拒绝数 |
| `rate_limit_redis_fallbacks_total` | counter | `category` | Redis 限流失败后的内存降级次数 |
## 集群
@ -84,14 +72,10 @@ curl -fsS \
| `synctv_realtime_events_dropped_total` | counter | `reason` | 被丢弃的实时事件 |
| `synctv_cluster_heartbeat_failures` | gauge | none | 连续 heartbeat 失败数 |
| `synctv_node_active_rooms` | gauge | none | 当前节点活跃房间 |
| `synctv_cluster_node_health_status` | gauge | none | 节点健康状态1 表示 healthy |
| `synctv_cluster_leader_election_state` | gauge | none | leader election 状态1 表示 leader |
| `synctv_cluster_leader_election_epoch` | gauge | none | 当前 leader epoch |
| `synctv_cluster_leader_election_consecutive_failures` | gauge | none | 连续选主失败数 |
| `synctv_cluster_epoch_mismatch_quarantine` | gauge | none | epoch mismatch 隔离状态 |
| `synctv_cluster_redis_pubsub_health` | gauge | none | Redis pub/sub 连接健康状态 |
| `synctv_cluster_member_count` | gauge | none | 集群成员数 |
| `synctv_cluster_sync_errors_total` | counter | `error_type` | 集群同步错误数 |
## 媒体和直播
@ -103,11 +87,5 @@ curl -fsS \
| `streamhub_restarts_total` | counter | `reason` | StreamHub event loop 重启次数 |
| `livestream_active_publishers` | gauge | none | 活跃直播 publisher |
| `livestream_active_viewers` | gauge | none | 活跃直播 viewer |
| `livestream_bytes_total` | counter | `direction` | 直播传输字节数 |
| `livestream_stream_duration_seconds` | histogram | `stream_type` | 直播 session 持续时间 |
| `livestream_pull_errors_total` | counter | `error_type` | 直播拉流错误数 |
| `livestream_relay_frame_drops_total` | counter | none | backpressure 导致的 relay 丢帧 |
| `gop_cache_size` | gauge | none | 当前 GOP cache 条目数 |
| `gop_cache_drops_total` | counter | none | GOP cache 淘汰数 |
| `gop_cache_memory_bytes` | gauge | none | GOP cache 内存占用 |
| `livestream_flv_slow_client_terminations_total` | counter | none | FLV 慢客户端断开次数 |

@ -12,7 +12,6 @@ workspace = true
axum.workspace = true
http.workspace = true
serde.workspace = true
serde_json.workspace = true
synctv-common = { workspace = true, default-features = false }
synctv-core = { workspace = true, default-features = false }
synctv-proto = { workspace = true, default-features = false, features = ["main"] }

@ -10,8 +10,6 @@ pub mod error_codes {
pub const UNSPECIFIED: i32 = 0;
pub const UNAUTHENTICATED: i32 = 1000;
pub const TOKEN_EXPIRED: i32 = 1001;
pub const INVALID_CREDENTIALS: i32 = 1002;
pub const NOT_FOUND: i32 = 2000;
pub const ALREADY_EXISTS: i32 = 2001;
@ -19,17 +17,11 @@ pub mod error_codes {
pub const CONFLICT: i32 = 2003;
pub const INVALID_ARGUMENT: i32 = 3000;
pub const INVALID_FORMAT: i32 = 3001;
pub const VALUE_TOO_SHORT: i32 = 3002;
pub const VALUE_TOO_LONG: i32 = 3003;
pub const REQUIRED_FIELD_MISSING: i32 = 3004;
pub const PERMISSION_DENIED: i32 = 4000;
pub const FORBIDDEN: i32 = 4001;
pub const BANNED: i32 = 4002;
pub const INTERNAL_ERROR: i32 = 9000;
pub const DATABASE_ERROR: i32 = 9001;
pub const SERVICE_UNAVAILABLE: i32 = 9002;
pub const TIMEOUT: i32 = 9003;
}

@ -229,12 +229,6 @@ impl PublicIdCodec {
})
}
#[cfg(test)]
#[must_use]
pub fn default_for_tests() -> Self {
Self::plain()
}
pub fn encode<T>(&self, id: T) -> Result<String, String>
where
T: PublicIdType,
@ -423,7 +417,7 @@ mod tests {
#[test]
fn default_codec_uses_prefixed_decimal_ids() {
let codec = PublicIdCodec::default_for_tests();
let codec = PublicIdCodec::plain();
assert_eq!(
ok(
@ -499,7 +493,7 @@ mod tests {
#[test]
fn default_decode_requires_correct_prefix() {
let codec = PublicIdCodec::default_for_tests();
let codec = PublicIdCodec::plain();
assert_eq!(
ok(codec.decode_user_id("usr_1"), "user ID should decode"),
@ -515,7 +509,7 @@ mod tests {
#[test]
fn default_decode_rejects_invalid_payload() {
let codec = PublicIdCodec::default_for_tests();
let codec = PublicIdCodec::plain();
assert!(codec.decode_user_id("usr_").is_err());
assert!(codec.decode_user_id("usr_0").is_err());
@ -577,7 +571,7 @@ mod tests {
#[test]
fn generic_public_ids_are_domain_separated_by_prefix() {
let codec = PublicIdCodec::default_for_tests();
let codec = PublicIdCodec::plain();
let review = ok(
codec.encode_review_request_id(id::<ReviewRequestId>(1)),
"review request ID should encode",

@ -349,8 +349,8 @@ pub struct SharedApiRuntime {
pub proxy_signing_key: Arc<ProxySigningKey>,
pub media_swarm_signing_key: Arc<MediaSwarmSigningKey>,
pub webrtc_status: synctv_core::service::WebRtcRuntimeStatus,
pub server_state_runtime: Arc<crate::status::ServerStateRuntime>,
pub slice_cache_management_runtime: Arc<crate::status::SliceCacheManagementRuntime>,
pub server_state_runtime: Arc<crate::status::ServerStateService>,
pub slice_cache_management_runtime: Arc<crate::status::SliceCacheManagementService>,
}
#[cfg(test)]

@ -212,11 +212,6 @@ impl AppState {
}
}
/// Create shared `AppState` once so multiple transports can reuse the same impl instances.
pub fn create_app_state_from_options(options: RouterOptions) -> anyhow::Result<AppState> {
build_app_state(options)
}
/// Build `AppState` from `RouterOptions`, creating the shared API implementation layers.
pub fn build_app_state(options: RouterOptions) -> anyhow::Result<AppState> {
let shared_api_runtime = Arc::new(build_shared_api_runtime(&options)?);

@ -24,7 +24,13 @@ impl ChatEventDispatcher for RealtimeChatEventDispatcher {
fn dispatch(&self, event: &ChatMessageEvent) -> RealtimeDeliveryOutcome {
let outcome = self
.event_service
.broadcast_outcome(chat_message_event_to_realtime(event));
.broadcast_outcome(RealtimeEvent::ChatMessageEvent {
event_id: event.event_id.clone(),
room_id: event.room_id,
actor_user_id: event.actor_user_id,
event: event.clone(),
timestamp: event.occurred_at,
});
if outcome.distributed_delivery_missed() {
tracing::warn!(
room_id = %event.room_id,
@ -41,7 +47,13 @@ impl ChatEventDispatcher for RealtimeChatEventDispatcher {
fn dispatch_pin(&self, event: &ChatPinEvent) -> RealtimeDeliveryOutcome {
let outcome = self
.event_service
.broadcast_outcome(chat_pin_event_to_realtime(event));
.broadcast_outcome(RealtimeEvent::ChatPinEvent {
event_id: event.event_id.clone(),
room_id: event.room_id,
actor_user_id: event.actor_user_id,
event: event.clone(),
timestamp: event.occurred_at,
});
if outcome.distributed_delivery_missed() {
tracing::warn!(
room_id = %event.room_id,
@ -63,28 +75,6 @@ pub fn default_chat_event_dispatcher(
Arc::new(RealtimeChatEventDispatcher::new(event_service))
}
#[must_use]
pub fn chat_message_event_to_realtime(event: &ChatMessageEvent) -> RealtimeEvent {
RealtimeEvent::ChatMessageEvent {
event_id: event.event_id.clone(),
room_id: event.room_id,
actor_user_id: event.actor_user_id,
event: event.clone(),
timestamp: event.occurred_at,
}
}
#[must_use]
pub fn chat_pin_event_to_realtime(event: &ChatPinEvent) -> RealtimeEvent {
RealtimeEvent::ChatPinEvent {
event_id: event.event_id.clone(),
room_id: event.room_id,
actor_user_id: event.actor_user_id,
event: event.clone(),
timestamp: event.occurred_at,
}
}
#[cfg(test)]
mod tests {
use super::*;

@ -38,14 +38,6 @@ impl AppError {
Self::new(StatusCode::FORBIDDEN, message)
}
pub fn too_many_requests(message: impl Into<String>) -> Self {
Self::new(StatusCode::TOO_MANY_REQUESTS, message)
}
pub fn payload_too_large(message: impl Into<String>) -> Self {
Self::new(StatusCode::PAYLOAD_TOO_LARGE, message)
}
pub fn too_many_requests_with_retry(message: impl Into<String>, retry_after: u64) -> Self {
let api_error = crate::impls::ApiError::RateLimitedWithRetry {
message: message.into(),
@ -57,12 +49,6 @@ impl AppError {
}
}
#[must_use]
pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.extra_headers.push((name, value));
self
}
#[must_use]
pub fn status(&self) -> StatusCode {
crate::api_error_model::GoogleApiError::from_api_error(&self.api_error).http_status
@ -85,37 +71,11 @@ impl AppError {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
}
// Common user-facing error messages for consistency
#[must_use]
pub fn invalid_credentials() -> Self {
Self::unauthorized("Invalid username or password")
}
#[must_use]
pub fn session_expired() -> Self {
Self::unauthorized("Your session has expired. Please log in again.")
}
#[must_use]
pub fn token_invalid() -> Self {
Self::unauthorized(synctv_common::messages::INVALID_OR_EXPIRED_TOKEN)
}
#[must_use]
pub fn permission_denied() -> Self {
Self::forbidden("You do not have permission to perform this action")
}
#[must_use]
pub fn resource_not_found(resource: &str) -> Self {
Self::not_found(format!("{resource} not found"))
}
#[must_use]
pub fn validation_failed(field: &str, reason: &str) -> Self {
Self::bad_request(format!("Invalid {field}: {reason}"))
}
#[must_use]
pub fn rate_limited(retry_after: u64) -> Self {
Self::too_many_requests_with_retry(
@ -132,11 +92,6 @@ impl AppError {
)
}
#[must_use]
pub fn missing_authorization_header() -> Self {
Self::unauthorized(synctv_common::messages::MISSING_AUTHORIZATION_HEADER)
}
#[must_use]
pub fn invalid_authorization_header() -> Self {
Self::unauthorized(synctv_common::messages::INVALID_AUTHORIZATION_HEADER)
@ -270,7 +225,7 @@ impl From<crate::impls::ApiError> for AppError {
};
if let crate::impls::ApiError::RangeNotSatisfiable { total_size } = err {
if let Ok(value) = HeaderValue::from_str(&format!("bytes */{total_size}")) {
app_err = app_err.with_header(header::CONTENT_RANGE, value);
app_err.extra_headers.push((header::CONTENT_RANGE, value));
}
}
app_err
@ -469,16 +424,6 @@ mod tests {
assert_eq!(err.message(), "Internal error");
}
#[test]
fn test_missing_authorization_header_helper() {
let err = AppError::missing_authorization_header();
assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
err.message(),
synctv_common::messages::MISSING_AUTHORIZATION_HEADER
);
}
#[test]
fn test_invalid_authorization_header_helper() {
let err = AppError::invalid_authorization_header();

@ -219,12 +219,7 @@ impl AdminApiImpl {
admin_user_id: &UserId,
) -> Result<AuthorizedAdminActor, ApiError> {
let actor = self.require_admin_actor(admin_user_id).await?;
if *admin_user_id == LOCAL_MANAGEMENT_ACTOR_USER_ID {
AuthorizedAdminActor::new_management(*admin_user_id, actor.username, actor.role)
.map_err(ApiError::from)
} else {
AuthorizedAdminActor::new(*admin_user_id, actor.username, actor.role)
.map_err(ApiError::from)
}
AuthorizedAdminActor::new(*admin_user_id, actor.username, actor.role)
.map_err(ApiError::from)
}
}

@ -401,9 +401,7 @@ impl AdminApiImpl {
.await
.map_err(ApiError::from)?
{
if member.is_active {
candidate_ids.push(member.user_id);
}
candidate_ids.push(member.user_id);
}
let mut seen = std::collections::HashSet::new();

@ -44,14 +44,12 @@ fn room_member_with_user(
remark_name: member.remark_name.clone(),
display_tag: member.display_tag.clone(),
role: member.role,
status: member.status,
added_permissions: member.added_permissions,
removed_permissions: member.removed_permissions,
admin_added_permissions: member.admin_added_permissions,
admin_removed_permissions: member.admin_removed_permissions,
joined_at: member.joined_at,
is_online: presence.is_online,
is_active: member.status.is_active(),
}
}
@ -385,7 +383,7 @@ impl AdminApiImpl {
let admin_username = admin_actor.username;
let state = self
.room_service
.admin_set_room_password_as_internal(&room_id, new_password, Some(admin_user_id))
.admin_set_room_password(&room_id, new_password, Some(admin_user_id))
.await
.map_err(ApiError::from)?;
self.publish_room_cache_invalidation(&room_id);
@ -542,7 +540,7 @@ impl AdminApiImpl {
self.log_admin_action(
admin_user_id,
synctv_core::models::AuditAction::MemberStatusUpdated,
synctv_core::models::AuditAction::MembershipUpdated,
synctv_core::models::AuditTargetType::Member,
Some(target_uid.to_string()),
AuditDetails {
@ -604,7 +602,7 @@ impl AdminApiImpl {
self.log_admin_action(
admin_user_id,
synctv_core::models::AuditAction::MemberStatusUpdated,
synctv_core::models::AuditAction::MembershipUpdated,
synctv_core::models::AuditTargetType::Member,
Some(target_uid.to_string()),
AuditDetails {
@ -666,7 +664,7 @@ impl AdminApiImpl {
self.log_admin_action(
admin_user_id,
synctv_core::models::AuditAction::MemberStatusUpdated,
synctv_core::models::AuditAction::MembershipUpdated,
synctv_core::models::AuditTargetType::Member,
Some(target_uid.to_string()),
AuditDetails {

@ -1086,13 +1086,6 @@ impl AdminApiImpl {
}
}
pub async fn send_test_email(
&self,
req: synctv_proto::admin::SendTestEmailRequest,
) -> Result<synctv_proto::admin::SendTestEmailResponse, ApiError> {
self.send_test_email_with_control(req, None).await
}
pub async fn send_test_email_with_control(
&self,
req: synctv_proto::admin::SendTestEmailRequest,

@ -7,8 +7,8 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use synctv_core::models::{
DeletionSource, FromProviderParams, MemberStatus, PlaylistId, ReviewRequestId, RoomId,
RoomRole, RoomStatus, UserId, UserRole, UserStatus,
DeletionSource, FromProviderParams, PlaylistId, ReviewRequestId, RoomId, RoomRole, RoomStatus,
UserId, UserRole, UserStatus,
};
use synctv_core::service::ProvidersManager;
use synctv_core::{
@ -31,29 +31,6 @@ use synctv_realtime::sync::{
CacheTarget, ConnectionLimits, ConnectionManager, PublishRequest, RealtimeEvent,
};
fn direct_url_playback_info(url: &str, name: &str) -> synctv_core::models::PlaybackInfo {
synctv_core::models::PlaybackInfo {
thumbnail: None,
medias: vec![synctv_core::models::PlaybackMedia {
name: name.to_string(),
format: String::new(),
expire_at: None,
metadata: None,
p2p_swarm_id: None,
provider: synctv_core::models::PlaybackMediaProvider::DirectUrl(
synctv_core::models::PlaybackDirectUrlMedia::Direct {
url: url.to_string(),
headers: HashMap::new(),
},
),
}],
default_media_index: None,
subtitles: Vec::new(),
default_subtitle_index: None,
danmakus: Vec::new(),
default_danmaku_index: None,
}
}
use tokio::sync::mpsc;
type TestResult<T = ()> = anyhow::Result<T>;
@ -917,8 +894,7 @@ async fn create_room_with_member(
)
.await,
"room should be created",
)
.0;
);
fixture_value(
admin_api
@ -964,8 +940,7 @@ async fn test_admin_add_member_publishes_permission_changed_membership_event() -
None,
)
.await,
)?
.0;
)?;
let fanout = Arc::new(RecordingMembershipEventFanout::default());
admin_api.membership_event_fanout = fanout.clone();
@ -1198,17 +1173,20 @@ async fn create_room_media(
.await,
"compute next media position",
);
let media = fixture_value(
synctv_core::models::Media::from_direct_single_mode(
None,
let media = synctv_core::models::Media::from_provider_with_params(
synctv_core::models::FromProviderParams {
playlist_id: None,
room_id,
Some(creator_id),
name.to_string(),
"direct",
direct_url_playback_info("https://example.com/video.mp4", "default"),
creator_id: Some(creator_id),
name: name.to_string(),
description: String::new(),
source_config: synctv_core_testing::direct_url_media_source_config(
"https://example.com/video.mp4",
),
source_provider: synctv_core::models::SourceProvider::DirectUrl,
provider_instance_name: None,
position,
),
"direct media should build",
},
);
let created = fixture_value(
media_repo.create_with_executor(&media, &mut *tx).await,
@ -1612,8 +1590,7 @@ async fn test_update_room_taxonomy_handles_local_management_actor_labels() -> Te
None,
)
.await,
)?
.0;
)?;
let response = api_ok(
admin_api
@ -1933,14 +1910,12 @@ fn make_test_member(role: RoomRole) -> synctv_core::models::RoomMemberWithUser {
remark_name: "Test Member".to_string(),
display_tag: "VIP".to_string(),
role,
status: MemberStatus::Active,
added_permissions: 0,
removed_permissions: 0,
admin_added_permissions: 0,
admin_removed_permissions: 0,
joined_at: synctv_core::SystemClock.now(),
is_online: false,
is_active: true,
}
}
@ -2256,8 +2231,7 @@ async fn test_get_user_rooms_respects_related_room_query_semantics() -> TestResu
None,
)
.await,
)?
.0;
)?;
let joined_room = core_ok(
admin_api
.room_service
@ -2269,8 +2243,7 @@ async fn test_get_user_rooms_respects_related_room_query_semantics() -> TestResu
None,
)
.await,
)?
.0;
)?;
core_ok(
admin_api
.room_service
@ -3803,8 +3776,7 @@ async fn test_delete_user_deletes_owned_rooms_and_publishes_room_deleted() -> Te
None,
)
.await,
)?
.0;
)?;
api_ok(
admin_api
@ -3884,8 +3856,7 @@ async fn test_standalone_admin_delete_user_deletes_owned_room_without_outbox() -
None,
)
.await,
)?
.0;
)?;
api_ok(
admin_api
@ -4007,8 +3978,7 @@ async fn test_ban_user_resets_playback_for_media_created_by_target() -> TestResu
None,
)
.await,
)?
.0;
)?;
core_ok(
admin_api
@ -4352,8 +4322,7 @@ async fn test_ban_user_publishes_room_owner_inactive_event_for_owned_rooms() ->
None,
)
.await,
)?
.0;
)?;
api_ok(
admin_api
@ -4420,8 +4389,7 @@ async fn test_batch_ban_users_resets_playback_for_media_created_by_target() -> T
None,
)
.await,
)?
.0;
)?;
core_ok(
admin_api
@ -4506,8 +4474,7 @@ async fn test_batch_ban_users_publishes_room_owner_inactive_event_for_owned_room
None,
)
.await,
)?
.0;
)?;
let response = api_ok(
admin_api
@ -4695,8 +4662,7 @@ async fn test_get_stream_info_bypasses_room_membership_requirement_for_global_ad
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "stream-media").await;
let registry_room_id = room.id.to_string();
let registry_media_id = media.id.to_string();
@ -4755,8 +4721,7 @@ async fn test_kick_stream_reports_local_unpublish_enqueue_failure() -> TestResul
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "stream-media").await;
let registry_room_id = room.id.to_string();
let registry_media_id = media.id.to_string();
@ -4839,8 +4804,7 @@ async fn test_kick_stream_publishes_cluster_event_for_remote_publisher() -> Test
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "remote-stream-media").await;
let registry_room_id = room.id.to_string();
let registry_media_id = media.id.to_string();
@ -4922,8 +4886,7 @@ async fn test_kick_stream_reports_remote_fanout_failure() -> TestResult {
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "remote-stream-media").await;
let registry_room_id = room.id.to_string();
let registry_media_id = media.id.to_string();
@ -4994,8 +4957,7 @@ async fn test_list_room_streams_bypasses_room_membership_requirement_for_global_
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "stream-media").await;
let registry_room_id = room.id.to_string();
let registry_media_id = media.id.to_string();
@ -5097,8 +5059,7 @@ async fn test_create_publish_key_bypasses_room_membership_requirement_for_global
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "stream-media").await;
let public_room_id = public_room_id(&admin_api, room.id);
let public_media_id = public_media_id(&admin_api, media.id);
@ -5164,8 +5125,7 @@ async fn test_create_publish_key_rejects_media_from_banned_creator() -> TestResu
None,
)
.await,
)?
.0;
)?;
core_ok(
admin_api
.room_service
@ -5225,8 +5185,7 @@ async fn test_start_playback_bypasses_room_membership_requirement_for_global_adm
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "playback-media").await;
api_ok(
@ -5307,8 +5266,7 @@ async fn test_stop_playback_bypasses_room_membership_requirement_for_global_admi
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "playback-media").await;
core_ok(
@ -5358,8 +5316,7 @@ async fn test_get_playback_bypasses_room_membership_requirement_for_global_admin
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "playback-media").await;
core_ok(
@ -5417,8 +5374,7 @@ async fn test_get_playback_reloads_state_after_creator_ban_on_another_instance()
None,
)
.await,
)?
.0;
)?;
core_ok(
admin_api
.room_service
@ -5510,8 +5466,7 @@ async fn test_start_playback_returns_error_for_invalid_provider_config_for_globa
None,
)
.await,
)?
.0;
)?;
let media = synctv_core::models::Media::from_provider_with_params(FromProviderParams {
playlist_id: None,
@ -5584,8 +5539,7 @@ async fn test_get_playback_for_provider_media_signs_proxy_urls_for_global_admin(
None,
)
.await,
)?
.0;
)?;
core_ok(
admin_api
.room_service
@ -5778,8 +5732,7 @@ async fn test_get_playback_for_provider_media_signs_proxy_urls_for_local_managem
None,
)
.await,
)?
.0;
)?;
let media = synctv_core::models::Media::from_provider_with_params(FromProviderParams {
playlist_id: None,
@ -5867,8 +5820,7 @@ async fn test_list_playlists_bypasses_room_membership_requirement_for_global_adm
None,
)
.await,
)?
.0;
)?;
let playlist = core_ok(
admin_api
@ -5945,8 +5897,7 @@ async fn test_get_playlist_bypasses_room_membership_requirement_for_global_admin
None,
)
.await,
)?
.0;
)?;
let playlist = core_ok(
admin_api
@ -6012,8 +5963,7 @@ async fn test_update_playlist_bypasses_room_membership_requirement_for_global_ad
None,
)
.await,
)?
.0;
)?;
let playlist = core_ok(
admin_api
@ -6082,8 +6032,7 @@ async fn test_delete_playlist_bypasses_room_membership_requirement_for_global_ad
None,
)
.await,
)?
.0;
)?;
let playlist = core_ok(
admin_api
@ -6163,8 +6112,7 @@ async fn test_delete_playlist_publishes_cascaded_playlist_and_media_events_for_g
None,
)
.await,
)?
.0;
)?;
let parent_playlist = core_ok(
admin_api
@ -6313,8 +6261,7 @@ async fn test_list_media_bypasses_room_membership_requirement_for_global_admin()
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "media-a").await;
@ -6372,8 +6319,7 @@ async fn test_reset_room_settings_bypasses_room_membership_for_local_management_
None,
)
.await,
)?
.0;
)?;
let customized = synctv_core::models::RoomSettings {
chat_enabled: synctv_core::models::room_settings::ChatEnabled(false),
@ -6431,8 +6377,7 @@ async fn test_delete_room_bypasses_room_membership_for_local_management_actor()
None,
)
.await,
)?
.0;
)?;
let management_actor = LOCAL_MANAGEMENT_ACTOR_USER_ID;
let response = api_ok(
@ -6481,8 +6426,7 @@ async fn test_list_media_respects_search_filters_and_sort_for_static_root() -> T
None,
)
.await,
)?
.0;
)?;
core_ok(
admin_api
@ -6565,8 +6509,7 @@ async fn test_edit_media_bypasses_room_membership_requirement_for_global_admin()
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "media-edit").await;
@ -6640,8 +6583,7 @@ async fn test_local_management_actor_preserves_username_in_media_notifications()
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "management-media").await;
let management_actor = LOCAL_MANAGEMENT_ACTOR_USER_ID;
@ -6740,8 +6682,7 @@ async fn test_delete_media_bypasses_room_membership_requirement_for_global_admin
None,
)
.await,
)?
.0;
)?;
let media = create_room_media(&pool, room.id, owner.id, "media-delete").await;
@ -6791,8 +6732,7 @@ async fn test_move_media_bypasses_room_membership_requirement_for_global_admin()
None,
)
.await,
)?
.0;
)?;
let media_a = create_room_media(&pool, room.id, owner.id, "media-move-a").await;
let media_b = create_room_media(&pool, room.id, owner.id, "media-move-b").await;

@ -182,31 +182,6 @@ fn nonnegative_token_ttl_seconds(exp: i64, now: i64) -> Result<u64, ApiError> {
})
}
/// Outcome of a logout operation.
///
/// Logout always succeeds (the user's intent to log out is respected),
/// but `message` indicates if token invalidation may be delayed.
pub struct LogoutOutcome {
pub blacklist_ok: bool,
pub message: &'static str,
}
impl LogoutOutcome {
pub const fn success() -> Self {
Self {
blacklist_ok: true,
message: "",
}
}
pub const fn blacklist_failed() -> Self {
Self {
blacklist_ok: false,
message: "Logged out but token invalidation may be delayed",
}
}
}
impl ClientApiImpl {
pub async fn start_login_with_control(
&self,
@ -869,13 +844,6 @@ impl ClientApiImpl {
login_outcome_to_proto(outcome, &self.public_id_codec)
}
pub async fn refresh_token(
&self,
req: synctv_proto::client::RefreshTokenRequest,
) -> Result<synctv_proto::client::RefreshTokenResponse, ApiError> {
self.refresh_token_with_control(req, None).await
}
pub async fn refresh_token_with_control(
&self,
req: synctv_proto::client::RefreshTokenRequest,
@ -905,7 +873,7 @@ impl ClientApiImpl {
///
/// Returns an error when token revocation fails so callers never treat a
/// non-revoked token as successfully logged out.
pub async fn logout(&self, raw_token: &str) -> Result<LogoutOutcome, ApiError> {
pub async fn logout(&self, raw_token: &str) -> Result<(), ApiError> {
let now = self.clock.now().timestamp();
revoke_session_for_logout(
&self.jwt_service,
@ -929,7 +897,7 @@ impl ClientApiImpl {
},
)
.await?;
Ok(LogoutOutcome::success())
Ok(())
}
}

@ -2224,10 +2224,6 @@ pub fn user_status_to_proto(status: synctv_core::models::UserStatus) -> i32 {
i32::from(status)
}
pub fn member_status_to_proto(status: synctv_core::models::MemberStatus) -> i32 {
i32::from(status)
}
pub const fn resource_availability_to_proto(is_available: bool) -> i32 {
if is_available {
synctv_proto::client::ResourceAvailability::Available as i32
@ -2615,13 +2611,6 @@ pub fn try_room_to_proto_with_availability_presence_and_cover(
Ok(proto)
}
#[must_use]
pub fn normalize_created_room_settings(
settings: Option<&synctv_core::models::RoomSettings>,
) -> synctv_core::models::RoomSettings {
settings.cloned().unwrap_or_default()
}
pub fn try_media_to_proto_for_viewer_without_cover(
media: &synctv_core::models::Media,
is_available: bool,

@ -6,12 +6,6 @@ use crate::impls::ApiError;
// protobuf chunks. Core storage owns range resolution and byte streaming; HTTP
// uses the same FileObjectDownload as a normal binary response body.
pub fn metadata_content_range(
metadata: &synctv_core::models::FileObjectMetadata,
) -> Option<synctv_proto::client::FileByteRange> {
metadata.range.map(super::media::file_byte_range_to_proto)
}
/// Generic helper to convert FileObjectDownload into a proto chunk stream.
/// `build_proto` receives (mime_type, sha256, data, content_range, total_size).
fn generic_chunk_stream<T, F>(
@ -25,7 +19,7 @@ where
+ 'static,
{
let metadata = download.metadata;
let content_range = metadata_content_range(&metadata);
let content_range = metadata.range.map(super::media::file_byte_range_to_proto);
let mime_type = metadata.mime_type;
let content_manifest_sha256 = metadata.content_manifest_sha256;
let total_size_bytes = metadata.total_size_bytes;

@ -34,7 +34,7 @@ use super::convert::{
file_metadata_from_proto, media_source_config_to_proto, optional_proto_source_provider_to_core,
optional_provider_target_to_proto, playlist_source_config_to_proto, provider_target_from_proto,
};
use super::{ClientApiImpl, GuestRoomAccess, RoomActor};
use super::{ClientApiImpl, RoomActor};
use crate::media_fanout::{MediaFanoutService, PreparedMediaRemovedFanout};
use crate::playback_fanout::{PlaybackFanoutService, PreparedPlaybackStateFanout};
use crate::playlist_fanout::{PlaylistFanoutService, PreparedPlaylistDeletedFanout};
@ -503,20 +503,6 @@ pub fn file_upload_plan_to_proto(plan: FileUploadPlan) -> synctv_proto::client::
}
}
pub fn proto_complete_upload_parts(
parts: Vec<synctv_proto::client::CompleteFileUploadPart>,
) -> Vec<CompleteFileUploadPart> {
parts
.into_iter()
.map(|part| CompleteFileUploadPart {
part_number: part.part_number,
etag: part.etag,
size_bytes: part.size_bytes,
checksum_sha256: optional_trimmed_string(&part.checksum_sha256),
})
.collect()
}
pub fn complete_upload_session_request(
file_id: &str,
encoded_object_key: String,
@ -531,7 +517,15 @@ pub fn complete_upload_session_request(
upload_token: token,
upload_id,
ownership_proof: optional_trimmed_string(ownership_proof),
parts: proto_complete_upload_parts(parts),
parts: parts
.into_iter()
.map(|part| CompleteFileUploadPart {
part_number: part.part_number,
etag: part.etag,
size_bytes: part.size_bytes,
checksum_sha256: optional_trimmed_string(&part.checksum_sha256),
})
.collect(),
}
}
@ -650,21 +644,15 @@ pub fn file_upload_reference_to_proto(
Ok(synctv_proto::client::FileUploadReference { id: reference.id })
}
pub fn file_upload_reference_to_core(
reference: synctv_proto::client::FileUploadReference,
) -> synctv_core::models::SubmittedFileReference {
synctv_core::models::SubmittedFileReference {
id: reference.id,
kind: synctv_core::models::SubmittedFileReferenceKind::Upload,
}
}
pub fn required_file_upload_reference(
reference: Option<synctv_proto::client::FileUploadReference>,
field: &'static str,
) -> Result<synctv_core::models::SubmittedFileReference, ApiError> {
reference
.map(file_upload_reference_to_core)
.map(|reference| synctv_core::models::SubmittedFileReference {
id: reference.id,
kind: synctv_core::models::SubmittedFileReferenceKind::Upload,
})
.ok_or_else(|| ApiError::InvalidInput(format!("{field} is required")))
}
@ -2294,16 +2282,6 @@ impl ClientApiImpl {
self.list_playlist_items_for_actor(&actor, req).await
}
pub async fn list_playlist_items_as_guest(
&self,
access: &GuestRoomAccess,
req: synctv_proto::client::ListPlaylistItemsRequest,
) -> Result<synctv_proto::client::ListPlaylistItemsResponse, ApiError> {
crate::impls::validate_proto_request(&req)?;
self.list_playlist_items_for_actor(&RoomActor::Guest(access.clone()), req)
.await
}
pub async fn list_playlist_items_for_actor(
&self,
actor: &RoomActor,
@ -2859,15 +2837,6 @@ impl ClientApiImpl {
self.get_media_for_actor(&actor, media_id).await
}
pub async fn get_media_as_guest(
&self,
access: &GuestRoomAccess,
media_id: &str,
) -> Result<synctv_proto::client::Media, ApiError> {
self.get_media_for_actor(&RoomActor::Guest(access.clone()), media_id)
.await
}
pub async fn get_media_for_actor(
&self,
actor: &RoomActor,

@ -15,7 +15,7 @@ use super::convert::{
room_presence_stats_to_proto, try_members_to_proto, try_room_member_to_proto_with_permissions,
};
use super::media::{prepare_delete_entries_outbox_fanout, PrepareDeleteEntriesOutboxFanout};
use super::{ClientApiImpl, GuestRoomAccess, RoomActor};
use super::{ClientApiImpl, RoomActor};
pub fn compute_room_members_response_version(
response: &synctv_proto::client::GetRoomMembersResponse,
@ -246,15 +246,6 @@ impl ClientApiImpl {
self.get_room_members_for_actor(&actor, req).await
}
pub async fn get_room_members_as_guest(
&self,
access: &GuestRoomAccess,
req: synctv_proto::client::GetRoomMembersRequest,
) -> Result<synctv_proto::client::GetRoomMembersResponse, ApiError> {
self.get_room_members_for_actor(&RoomActor::Guest(access.clone()), req)
.await
}
pub async fn get_room_members_for_actor(
&self,
actor: &RoomActor,
@ -402,14 +393,12 @@ impl ClientApiImpl {
remark_name: member.remark_name.clone(),
display_tag: member.display_tag.clone(),
role: member.role,
status: member.status,
added_permissions: member.added_permissions,
removed_permissions: member.removed_permissions,
admin_added_permissions: member.admin_added_permissions,
admin_removed_permissions: member.admin_removed_permissions,
joined_at: member.joined_at,
is_online: target_presence.is_online,
is_active: member.status.is_active(),
};
let room_settings = self
.room_service
@ -479,14 +468,12 @@ impl ClientApiImpl {
remark_name: member.remark_name.clone(),
display_tag: member.display_tag.clone(),
role: member.role,
status: member.status,
added_permissions: member.added_permissions,
removed_permissions: member.removed_permissions,
admin_added_permissions: member.admin_added_permissions,
admin_removed_permissions: member.admin_removed_permissions,
joined_at: member.joined_at,
is_online: target_presence.is_online,
is_active: member.status.is_active(),
};
let room_settings = self
.room_service
@ -688,14 +675,12 @@ impl ClientApiImpl {
remark_name: member.remark_name.clone(),
display_tag: member.display_tag.clone(),
role: member.role,
status: member.status,
added_permissions: member.added_permissions,
removed_permissions: member.removed_permissions,
admin_added_permissions: member.admin_added_permissions,
admin_removed_permissions: member.admin_removed_permissions,
joined_at: member.joined_at,
is_online: target_presence.is_online,
is_active: true,
};
// Fetch room settings for proper three-layer permission calculation
@ -765,14 +750,12 @@ impl ClientApiImpl {
remark_name: member.remark_name.clone(),
display_tag: member.display_tag.clone(),
role: member.role,
status: member.status,
added_permissions: member.added_permissions,
removed_permissions: member.removed_permissions,
admin_added_permissions: member.admin_added_permissions,
admin_removed_permissions: member.admin_removed_permissions,
joined_at: member.joined_at,
is_online: target_presence.is_online,
is_active: true,
};
let room_settings = self
.room_service
@ -840,14 +823,12 @@ impl ClientApiImpl {
remark_name: member.remark_name.clone(),
display_tag: member.display_tag.clone(),
role: member.role,
status: member.status,
added_permissions: member.added_permissions,
removed_permissions: member.removed_permissions,
admin_added_permissions: member.admin_added_permissions,
admin_removed_permissions: member.admin_removed_permissions,
joined_at: member.joined_at,
is_online: target_presence.is_online,
is_active: true,
};
let room_settings = self
.room_service

@ -33,10 +33,7 @@ pub mod stream;
mod user;
mod webrtc;
pub use playback::{build_playback_state_update, build_start_playback_request};
pub use user::{
token_auth_context_from_claims, user_notification_preferences_to_proto,
user_preferences_update_from_proto,
};
pub use user::{user_notification_preferences_to_proto, user_preferences_update_from_proto};
// Proto conversion helpers used across impls modules within this crate.
pub mod convert;
@ -63,8 +60,7 @@ pub use convert::{
use crate::chat_event_dispatcher::{default_chat_event_dispatcher, ChatEventDispatcher};
use crate::fanout::{default_room_settings_fanout_service, RoomSettingsFanoutService};
use crate::impls::{
ApiError, ApiRequestContext, EndpointRateLimitCategory, EndpointRateLimitScope,
RequestExecutor, RequestMetadata,
ApiError, EndpointRateLimitCategory, EndpointRateLimitScope, RequestExecutor, RequestMetadata,
};
use crate::media_fanout::{default_media_fanout_service, MediaFanoutService};
use crate::membership_event_fanout::{
@ -1045,39 +1041,6 @@ impl ClientApiImpl {
synctv_core::service::JwtService::token_type_hint(token) == Some(TokenType::Guest)
}
async fn room_actor_for_bearer_token(
&self,
token: &str,
public_room_id: &str,
) -> Result<RoomActor, ApiError> {
if Self::is_guest_token(token) {
return self
.validate_guest_room_access(token, public_room_id)
.await
.map(RoomActor::Guest);
}
let claims = self.jwt_validator.validate_token(token).map_err(|_| {
ApiError::Authentication(synctv_common::messages::INVALID_OR_EXPIRED_TOKEN.to_string())
})?;
let authenticated = self
.request_executor()
.security_check_claims(&claims)
.await?;
self.room_actor_for_user(&authenticated.user_id(), public_room_id)
.await
}
pub async fn room_actor_for_authorization(
&self,
authorization: &str,
public_room_id: &str,
) -> Result<RoomActor, ApiError> {
let token = Self::bearer_token_from_authorization(authorization)?;
self.room_actor_for_bearer_token(&token, public_room_id)
.await
}
pub fn require_guest_permission(
access: &GuestRoomAccess,
permission: synctv_core::models::RoomPermission,
@ -1139,12 +1102,6 @@ impl ClientApiImpl {
}
}
pub(super) fn map_livestream_backend_error(
error: &(dyn std::error::Error + 'static),
) -> ApiError {
crate::impls::map_livestream_backend_error(error)
}
#[must_use]
pub fn new_with_runtime(options: ClientApiOptions, runtime: ClientApiRuntime) -> Self {
let read_pool = options
@ -1287,27 +1244,6 @@ impl ClientApiImpl {
})
}
pub fn execute_public_endpoint_with_context<'a, T, E, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
category: EndpointRateLimitCategory,
operation: F,
) -> BoxFuture<'a, Result<T, ApiError>>
where
T: Send + 'a,
E: Into<ApiError> + Send + 'a,
F: FnOnce(ApiRequestContext) -> Fut + Send + 'a,
Fut: std::future::Future<Output = Result<T, E>> + Send + 'a,
{
self.request_executor().execute_public_with_context(
metadata,
category,
move |request_context| async move {
operation(request_context).await.map_err(Into::into)
},
)
}
pub fn execute_public_endpoint_with_control<'a, T, E, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
@ -1329,26 +1265,6 @@ impl ClientApiImpl {
)
}
pub fn execute_scoped_public_endpoint_with_control<'a, T, E, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
category: EndpointRateLimitCategory,
scope: EndpointRateLimitScope,
operation: F,
) -> BoxFuture<'a, Result<T, ApiError>>
where
T: Send + 'a,
E: Into<ApiError> + Send + 'a,
F: FnOnce(synctv_core::provider::ExecutionControl) -> Fut + Send + 'a,
Fut: std::future::Future<Output = Result<T, E>> + Send + 'a,
{
let metadata = metadata.clone().with_endpoint_scope(Some(scope));
Box::pin(async move {
self.execute_public_endpoint_with_control(&metadata, category, operation)
.await
})
}
pub fn execute_user_endpoint<'a, T, E, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
@ -1387,29 +1303,6 @@ impl ClientApiImpl {
})
}
pub fn execute_user_endpoint_with_context<'a, T, E, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
category: EndpointRateLimitCategory,
operation: F,
) -> BoxFuture<'a, Result<T, ApiError>>
where
T: Send + 'a,
E: Into<ApiError> + Send + 'a,
F: FnOnce(ApiRequestContext, synctv_core::service::AuthenticatedToken) -> Fut + Send + 'a,
Fut: std::future::Future<Output = Result<T, E>> + Send + 'a,
{
self.request_executor().execute_user_with_context(
metadata,
category,
move |request_context, authenticated| async move {
operation(request_context, authenticated)
.await
.map_err(Into::into)
},
)
}
pub fn execute_user_endpoint_with_control<'a, T, E, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,

@ -722,7 +722,7 @@ impl ClientApiImpl {
Some(infrastructure) => infrastructure
.find_publisher(&room_id.to_string(), &media_id.to_string())
.await
.map_err(|error| Self::map_livestream_backend_error(error.as_ref()))?,
.map_err(|error| crate::impls::map_livestream_backend_error(error.as_ref()))?,
None => None,
};
apply_live_stream_generation(metadata, publisher.as_ref());
@ -1061,18 +1061,6 @@ impl ClientApiImpl {
self.get_playback_for_actor(&actor, req, None).await
}
pub async fn get_playback_with_context(
&self,
user_id: &UserId,
room_id: &str,
req: synctv_proto::client::GetPlaybackRequest,
request_control: &ExecutionControl,
) -> Result<synctv_proto::client::GetPlaybackResponse, ApiError> {
let actor = self.room_actor_for_user(user_id, room_id).await?;
self.get_playback_for_actor(&actor, req, Some(request_control))
.await
}
pub async fn get_playback_as_guest(
&self,
access: &GuestRoomAccess,

@ -15,7 +15,7 @@ use super::media::{
proto_file_range_request, proto_file_upload_range, proto_upload_manifest_parts,
required_file_upload_reference, uploaded_parts_response_fields,
};
use super::{ClientApiImpl, GuestRoomAccess, RoomActor};
use super::{ClientApiImpl, RoomActor};
use crate::impls::ApiError;
const DEFAULT_PLAYLIST_PAGE_SIZE: i32 = 50;
@ -615,15 +615,6 @@ impl ClientApiImpl {
self.get_playlist_for_actor(&actor, playlist_id).await
}
pub async fn get_playlist_as_guest(
&self,
access: &GuestRoomAccess,
playlist_id: &str,
) -> Result<synctv_proto::client::GetPlaylistResponse, ApiError> {
self.get_playlist_for_actor(&RoomActor::Guest(access.clone()), playlist_id)
.await
}
pub async fn get_playlist_for_actor(
&self,
actor: &RoomActor,
@ -690,15 +681,6 @@ impl ClientApiImpl {
self.list_playlists_for_actor(&actor, req).await
}
pub async fn list_playlists_as_guest(
&self,
access: &GuestRoomAccess,
req: synctv_proto::client::ListPlaylistsRequest,
) -> Result<synctv_proto::client::ListPlaylistsResponse, ApiError> {
self.list_playlists_for_actor(&RoomActor::Guest(access.clone()), req)
.await
}
pub async fn list_playlists_for_actor(
&self,
actor: &RoomActor,

@ -13,9 +13,9 @@ use synctv_core::service::ClientResourceAvailability;
use super::convert::{
apply_room_settings_patch_from_proto, chat_message_selection_from_proto_values,
chat_metadata_from_proto, file_metadata_from_proto, member_status_to_proto,
provider_target_from_proto, room_role_to_proto, room_settings_from_proto,
room_settings_to_proto, try_members_to_proto, try_playback_state_to_proto,
chat_metadata_from_proto, file_metadata_from_proto, provider_target_from_proto,
room_role_to_proto, room_settings_from_proto, room_settings_to_proto, try_members_to_proto,
try_playback_state_to_proto,
};
use super::media::{
complete_upload_response_fields, complete_upload_session_request,
@ -24,7 +24,7 @@ use super::media::{
room_cover_upload_create_result_to_proto, uploaded_parts_response_fields,
PrepareDeleteEntriesOutboxFanout,
};
use super::{ClientApiImpl, GuestRoomAccess, RoomActor};
use super::{ClientApiImpl, RoomActor};
mod support;
use support::*;
@ -761,9 +761,9 @@ impl ClientApiImpl {
// Batch-fetch room settings for full permission calculation.
let room_ids: Vec<synctv_core::models::RoomId> =
rooms.iter().map(|(room, _, _, _)| room.id).collect();
rooms.iter().map(|(room, _, _)| room.id).collect();
let room_models: Vec<synctv_core::models::Room> =
rooms.iter().map(|(room, _, _, _)| room.clone()).collect();
rooms.iter().map(|(room, _, _)| room.clone()).collect();
let (room_settings_map, presence_stats, availability_map, creator_views, favorite_room_ids) =
tokio::try_join!(
async {
@ -804,7 +804,7 @@ impl ClientApiImpl {
})
.collect::<Vec<_>>();
let room_list = stream::iter(rooms_with_creators)
.map(|((room, role, _status, member_count), creator)| {
.map(|((room, role, member_count), creator)| {
let room_settings_map = &room_settings_map;
let presence_by_room = &presence_by_room;
let availability_map = &availability_map;
@ -955,12 +955,11 @@ impl ClientApiImpl {
let category_id = parse_optional_room_category_id(&req.category_id, &self.public_id_codec)?;
let label_ids = parse_room_label_ids(&req.label_ids, &self.public_id_codec)?;
let response_settings =
crate::impls::client::convert::normalize_created_room_settings(settings.as_ref());
let response_settings = settings.clone().unwrap_or_default();
let prepared_outbox_fanout = self
.room_lifecycle_fanout
.prepare_room_created_outbox_fanout(uid);
let (room, _member) = self
let room = self
.room_service
.create_room_with_taxonomy_outbox(
synctv_core::service::CreateRoomWithTaxonomyRequest {
@ -1102,14 +1101,6 @@ impl ClientApiImpl {
})
}
pub async fn get_room_as_guest(
&self,
access: &GuestRoomAccess,
) -> Result<synctv_proto::client::GetRoomResponse, ApiError> {
self.get_room_for_actor(&RoomActor::Guest(access.clone()))
.await
}
async fn room_response_after_room_update(
&self,
room: &synctv_core::models::Room,
@ -1276,16 +1267,6 @@ impl ClientApiImpl {
self.room_response_after_room_update(&room, user_id).await
}
pub async fn join_room(
&self,
user_id: &UserId,
room_id: &str,
req: synctv_proto::client::JoinRoomRequest,
client_ip: Option<&str>,
) -> Result<synctv_proto::client::JoinRoomResponse, ApiError> {
Box::pin(self.join_room_with_control(user_id, room_id, req, client_ip, None)).await
}
pub async fn join_room_with_control(
&self,
user_id: &UserId,
@ -1352,7 +1333,7 @@ impl ClientApiImpl {
target_presence.is_online,
target_presence.connection_count,
);
let (_room, member, members) = self
let (_room, members) = self
.room_service
.join_room_with_outbox(
rid,
@ -1393,7 +1374,6 @@ impl ClientApiImpl {
),
members: proto_members,
playback_state: Some(playback_state),
membership_status: member_status_to_proto(member.status),
requires_approval,
})
}
@ -1459,7 +1439,7 @@ impl ClientApiImpl {
.as_ref()
.map_or(0, |presence| presence.connection_count),
);
let (room, member, members) = Box::pin(
let (room, members) = Box::pin(
self.room_service
.finish_room_opaque_password_login_with_outbox(
expected_room_id.as_ref(),
@ -1499,7 +1479,6 @@ impl ClientApiImpl {
),
members: proto_members,
playback_state: Some(playback_state),
membership_status: member_status_to_proto(member.status),
requires_approval,
})
}
@ -1919,14 +1898,6 @@ impl ClientApiImpl {
})
}
pub async fn get_room_settings_as_guest(
&self,
access: &GuestRoomAccess,
) -> Result<synctv_proto::client::GetRoomSettingsResponse, ApiError> {
self.get_room_settings_for_actor(&RoomActor::Guest(access.clone()))
.await
}
/// Reset room settings to defaults
pub async fn reset_room_settings(
&self,
@ -2730,15 +2701,6 @@ impl ClientApiImpl {
self.chat_event_dispatcher.dispatch_pin(event);
}
pub async fn get_chat_history_as_guest(
&self,
access: &GuestRoomAccess,
req: synctv_proto::client::GetChatHistoryRequest,
) -> Result<synctv_proto::client::GetChatHistoryResponse, ApiError> {
self.get_chat_history_for_actor(&RoomActor::Guest(access.clone()), req)
.await
}
pub async fn get_chat_history_for_actor(
&self,
actor: &RoomActor,
@ -3056,7 +3018,7 @@ mod tests {
let owner = create_user(&pool, "favorite_requires_owner").await?;
let outsider = create_user(&pool, "favorite_requires_outsider").await?;
let api = test_client_api(pool, user_service);
let (room, _) = api_ok(
let room = api_ok(
api.room_service
.create_room(
"Favorite membership room".to_string(),
@ -3093,7 +3055,7 @@ mod tests {
std::sync::Arc::new(synctv_core_testing::create_test_user_service(pool.clone()));
let owner = create_user(&pool, "favorite_update_owner").await?;
let api = test_client_api(pool, user_service);
let (room, _) = api_ok(
let room = api_ok(
api.room_service
.create_room(
"Favorite update response room".to_string(),
@ -3129,7 +3091,7 @@ mod tests {
let owner = create_user(&pool, "favorite_leave_owner").await?;
let member = create_user(&pool, "favorite_leave_member").await?;
let api = test_client_api(pool, user_service);
let (room, _) = api_ok(
let room = api_ok(
api.room_service
.create_room(
"Favorite leave room".to_string(),
@ -3206,7 +3168,7 @@ mod tests {
let member = create_user(&pool, "banned_room_visible_member").await?;
let outsider = create_user(&pool, "banned_room_visible_outsider").await?;
let api = test_client_api(pool, user_service);
let (room, _) = api_ok(
let room = api_ok(
api.room_service
.create_room(
"Banned room remains visible".to_string(),
@ -3317,7 +3279,7 @@ mod tests {
let owner = create_user(&pool, "banned_creator_visible_owner").await?;
let member = create_user(&pool, "banned_creator_visible_member").await?;
let api = test_client_api(pool, user_service);
let (room, _) = api_ok(
let room = api_ok(
api.room_service
.create_room(
"Banned creator room remains visible".to_string(),

@ -314,7 +314,7 @@ impl ClientApiImpl {
let media_ids = infrastructure
.list_streams_for_room(&rid.to_string())
.await
.map_err(|error| Self::map_livestream_backend_error(&*error))?;
.map_err(|error| crate::impls::map_livestream_backend_error(&*error))?;
let media_ids = media_ids
.into_iter()
@ -474,8 +474,7 @@ mod tests {
None,
None,
)
.await?
.0;
.await?;
room_service
.join_room(room.id, banned_creator.id, None)
.await?;

@ -8,9 +8,9 @@ use std::sync::{
Arc,
};
use synctv_core::models::{
MediaId, MemberStatus, Playlist, PlaylistBrowseAccessMode, PlaylistId, RoomGuestPermissionBits,
RoomId, RoomMemberPermissionBits, RoomPermission, RoomPermissionSet, RoomRole, RoomStatus,
UserId, UserRole, UserStatus,
MediaId, Playlist, PlaylistBrowseAccessMode, PlaylistId, RoomGuestPermissionBits, RoomId,
RoomMemberPermissionBits, RoomPermission, RoomPermissionSet, RoomRole, RoomStatus, UserId,
UserRole, UserStatus,
};
use synctv_core::provider::{ProviderStore, ProviderStoreResolver, StoreError, StoreLockGuard};
@ -54,7 +54,7 @@ impl synctv_core::provider::MediaProvider for ProviderEnrichmentProbe {
source_config: synctv_core::provider::SourceConfig<'_>,
) -> Result<Option<synctv_core::provider::SourceCover>, synctv_core::provider::ProviderError>
{
if source_config.is_media() {
if matches!(source_config, synctv_core::provider::SourceConfig::Media(_)) {
self.media_calls.fetch_add(1, Ordering::SeqCst);
} else {
self.playlist_calls.fetch_add(1, Ordering::SeqCst);
@ -122,30 +122,6 @@ fn test_public_id_codec() -> synctv_adapter::PublicIdCodec {
type TestResult<T = ()> = anyhow::Result<T>;
fn direct_url_playback_info(url: &str, name: &str) -> synctv_core::models::PlaybackInfo {
synctv_core::models::PlaybackInfo {
thumbnail: None,
medias: vec![synctv_core::models::PlaybackMedia {
name: name.to_string(),
format: String::new(),
expire_at: None,
metadata: None,
p2p_swarm_id: None,
provider: synctv_core::models::PlaybackMediaProvider::DirectUrl(
synctv_core::models::PlaybackDirectUrlMedia::Direct {
url: url.to_string(),
headers: std::collections::HashMap::new(),
},
),
}],
default_media_index: None,
subtitles: Vec::new(),
default_subtitle_index: None,
danmakus: Vec::new(),
default_danmaku_index: None,
}
}
fn test_error(message: impl Into<String>) -> anyhow::Error {
anyhow::anyhow!(message.into())
}
@ -595,7 +571,7 @@ fn test_room_list_backend_outage_maps_to_service_unavailable() {
fn test_livestream_backend_error_service_unavailable_stays_service_unavailable() {
let stream_error =
synctv_livestream::StreamError::RegistryError("redis temporarily unavailable".to_string());
let mapped = super::ClientApiImpl::map_livestream_backend_error(&stream_error);
let mapped = crate::impls::map_livestream_backend_error(&stream_error);
assert!(
matches!(mapped, ApiError::ServiceUnavailable(ref msg) if msg == "Live streaming service is temporarily unavailable. Please try again later."),
@ -610,7 +586,7 @@ fn test_livestream_backend_error_finds_nested_stream_error() {
))
.context("wrapped by anyhow");
let mapped = super::ClientApiImpl::map_livestream_backend_error(err.as_ref());
let mapped = crate::impls::map_livestream_backend_error(err.as_ref());
assert!(
matches!(mapped, ApiError::RateLimited(ref msg) if msg == "Live streaming capacity limit reached. Please try again later."),
@ -1439,16 +1415,21 @@ fn test_seafile_source_metadata_uses_native_path() -> TestResult {
#[test]
fn test_media_to_proto_direct_media_omits_default_instance_binding() -> TestResult {
let public_id_codec = test_public_id_codec();
let media = synctv_core::models::Media::from_direct_single_mode(
Some(PlaylistId::expect_positive(305)),
RoomId::expect_positive(301),
Some(UserId::expect_positive(304)),
"Direct Media".to_string(),
"direct",
direct_url_playback_info("https://example.com/video.mp4", "1080p"),
1.0,
)
.map_err(|error| test_error(error.to_string()))?;
let media = synctv_core::models::Media::from_provider_with_params(
synctv_core::models::FromProviderParams {
playlist_id: Some(PlaylistId::expect_positive(305)),
room_id: RoomId::expect_positive(301),
creator_id: Some(UserId::expect_positive(304)),
name: "Direct Media".to_string(),
description: String::new(),
source_config: synctv_core_testing::direct_url_media_source_config(
"https://example.com/video.mp4",
),
source_provider: synctv_core::models::SourceProvider::DirectUrl,
provider_instance_name: None,
position: 1.0,
},
);
let proto = api_ok(try_media_to_proto_for_viewer_without_cover(
&media,
true,
@ -1472,14 +1453,12 @@ fn make_test_member(role: RoomRole) -> synctv_core::models::RoomMemberWithUser {
remark_name: "Alice Remark".to_string(),
display_tag: "VIP".to_string(),
role,
status: MemberStatus::Active,
added_permissions: 0,
removed_permissions: 0,
admin_added_permissions: 0,
admin_removed_permissions: 0,
joined_at: synctv_core::SystemClock.now(),
is_online: true,
is_active: true,
}
}

@ -70,12 +70,6 @@ fn masked_email_for_sensitive_challenge(
Ok(String::new())
}
pub fn token_auth_context_from_claims(
claims: &synctv_core::service::Claims,
) -> Option<TokenAuthContext> {
claims.auth_context()
}
fn sensitive_challenge_to_proto(
challenge: SensitiveVerificationChallenge,
) -> Result<synctv_proto::client::SensitiveOperationVerificationChallenge, ApiError> {
@ -752,9 +746,7 @@ impl ClientApiImpl {
.await
.map_err(ApiError::from)?
.ok_or_else(|| ApiError::Authentication("Authentication failed".to_string()))?;
email_api
.check_email_address_rate_limit(&email, None)
.await?;
email_api.check_email_rate_limit(&email, None).await?;
email_api
.enqueue_tokenized_email_with_control(
&email,

@ -136,7 +136,7 @@ impl EmailApiImpl {
email.trim().to_ascii_lowercase()
}
async fn check_email_rate_limit(
pub(crate) async fn check_email_rate_limit(
&self,
email: &str,
control: Option<&ExecutionControl>,
@ -191,14 +191,6 @@ impl EmailApiImpl {
.map_err(ApiError::from)
}
pub async fn check_email_address_rate_limit(
&self,
email: &str,
control: Option<&ExecutionControl>,
) -> Result<(), ApiError> {
self.check_email_rate_limit(email, control).await
}
#[must_use]
pub fn new(
user_service: Arc<UserService>,
@ -258,7 +250,8 @@ impl EmailApiImpl {
/// Request a password reset email.
/// Returns generic message regardless of whether user exists (anti-enumeration).
pub async fn request_password_reset(
#[cfg(test)]
async fn request_password_reset(
&self,
email: &str,
) -> Result<RequestPasswordResetResult, ApiError> {
@ -317,7 +310,8 @@ impl EmailApiImpl {
}
/// Request an email login code.
pub async fn request_email_login(
#[cfg(test)]
async fn request_email_login(
&self,
user_id: &synctv_core::models::UserId,
email: &str,
@ -400,18 +394,6 @@ impl EmailApiImpl {
})
}
/// Start a password reset by consuming the email token and creating an
/// OPAQUE registration session for the replacement password.
pub async fn start_opaque_password_reset(
&self,
email: &str,
token: &str,
registration_request: bytes::Bytes,
) -> Result<StartOpaquePasswordResetResult, ApiError> {
self.start_opaque_password_reset_with_control(email, token, registration_request, None)
.await
}
pub async fn start_opaque_password_reset_with_control(
&self,
email: &str,
@ -486,15 +468,6 @@ impl EmailApiImpl {
})
}
pub async fn finish_opaque_password_reset(
&self,
session_id: &str,
registration_upload: bytes::Bytes,
) -> Result<ConfirmPasswordResetResult, ApiError> {
self.finish_opaque_password_reset_with_control(session_id, registration_upload, None)
.await
}
pub async fn finish_opaque_password_reset_with_control(
&self,
session_id: &str,
@ -537,7 +510,8 @@ impl EmailApiImpl {
}
/// Confirm an email login token and issue an authenticated session.
pub async fn confirm_email_login(
#[cfg(test)]
async fn confirm_email_login(
&self,
user_id: &synctv_core::models::UserId,
email: &str,

@ -570,16 +570,6 @@ impl StreamMessageHandler {
self.connection_id.as_str()
}
/// Invalidate the membership cache entry for a specific user in a room.
///
/// Called when a `KickUser` or `KickUserFromRoom` admin event is received,
/// ensuring that the heartbeat check will re-query the database on the next
/// tick instead of trusting the stale cached "member" status.
pub fn invalidate_membership_cache(&self, room_id: &RoomId, user_id: &UserId) {
let cache_key = (*room_id, *user_id);
self.membership_cache.invalidate(&cache_key);
}
fn public_room_id(&self) -> Result<String, String> {
self.public_id_codec
.encode_room_id(self.room_id)
@ -2822,7 +2812,8 @@ impl StreamMessageHandler {
impl StreamMessageHandler {
/// Handle incoming client message with all validations
pub async fn handle_client_message(&self, msg: &ClientMessage) -> Result<(), String> {
#[cfg(test)]
async fn handle_client_message(&self, msg: &ClientMessage) -> Result<(), String> {
self.handle_client_message_with_control(msg, None).await
}
@ -2977,18 +2968,6 @@ impl StreamMessageHandler {
self.sender.send(msg)
}
/// Get room ID
#[must_use]
pub const fn get_room_id(&self) -> &RoomId {
&self.room_id
}
/// Get the signed-in user ID for user principals.
#[must_use]
pub const fn get_user_id(&self) -> Option<UserId> {
self.principal.user_id()
}
}
mod event_messages;

@ -542,11 +542,6 @@ pub fn core_chat_attachment_to_proto(
pub struct ProtoCodec;
impl ProtoCodec {
/// Encode `ClientMessage` to binary
pub fn encode_client_message(msg: &ClientMessage) -> Result<Vec<u8>, String> {
Ok(msg.encode_to_vec())
}
/// Decode `ClientMessage` from binary
pub fn decode_client_message(data: &[u8]) -> Result<ClientMessage, String> {
ClientMessage::decode(data).map_err(|e| format!("Failed to decode message: {e}"))

@ -1,6 +1,6 @@
use rand::RngExt;
use std::time::Duration;
use synctv_core::models::{RealtimeActor, UserId};
use synctv_core::models::RealtimeActor;
/// Default TTL for membership cache entries (30 seconds).
///
@ -45,27 +45,12 @@ impl HeartbeatSchedule {
self.membership_cache_ttl
}
#[must_use]
pub const fn max_jitter_secs(self) -> u64 {
self.max_jitter_secs
}
#[must_use]
pub fn period_with_random_jitter(self) -> Duration {
self.base_interval
+ Duration::from_secs(rand::rng().random_range(0u64..=self.max_jitter_secs))
}
#[must_use]
pub fn period_for_user(self, user_id: &UserId) -> Duration {
let jitter_secs = if self.max_jitter_secs == 0 {
0
} else {
user_id.as_i64().unsigned_abs() % (self.max_jitter_secs + 1)
};
self.base_interval + Duration::from_secs(jitter_secs)
}
#[must_use]
pub fn period_for_actor(self, actor: &RealtimeActor) -> Duration {
use std::hash::{Hash, Hasher};

@ -2444,7 +2444,7 @@ where
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
format!("Fixture Room {node_id}"),
"test".to_string(),
@ -3035,7 +3035,7 @@ async fn test_observe_chat_events_replays_single_event_after_sequence() {
"chat-replay-owner@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Chat Replay Room".to_string(),
String::new(),
@ -3148,7 +3148,7 @@ async fn test_observe_chat_events_replays_events_after_sequence() {
"chat-replay-seq-owner@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Chat Replay Sequence Room".to_string(),
String::new(),
@ -4746,7 +4746,7 @@ async fn test_observed_playback_refreshes_when_current_playlist_is_updated() {
let updated_playlist = handler
.room_service
.playlist_service()
.set_playlist(
.set_playlist_with_outbox(
handler.room_id,
handler.test_user_id(),
synctv_core::service::SetPlaylistRequest {
@ -4757,6 +4757,7 @@ async fn test_observed_playback_refreshes_when_current_playlist_is_updated() {
provider_instance_name: None,
browse_access_mode: None,
},
None,
)
.await
.checked("editing current playback playlist should succeed");
@ -7930,13 +7931,12 @@ fn test_cached_membership_from_member_none() {
#[test]
fn test_cached_membership_from_member_active() {
use synctv_core::models::{MemberStatus, RoomMember, RoomRole};
use synctv_core::models::{RoomMember, RoomRole};
let member = RoomMember {
room_id: room_id(),
user_id: user_id(),
role: RoomRole::Member,
status: MemberStatus::Active,
added_permissions: 0,
removed_permissions: 0,
admin_added_permissions: 0,
@ -9533,7 +9533,7 @@ async fn test_pre_join_after_registration_rejects_closed_room_on_final_revalidat
let user_service = room_service.user_service().clone();
let owner = register_test_user(&user_service, "room-owner", "owner@test.invalid").await;
let member = register_test_user(&user_service, "room-member", "member@test.invalid").await;
let (room, _) = room_service
let room = room_service
.create_room(
"Realtime Room".to_string(),
"test".to_string(),
@ -9617,7 +9617,7 @@ async fn test_pre_join_after_registration_rejects_room_with_inactive_creator() {
"member-inactive-owner@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Realtime Room Inactive Owner".to_string(),
"test".to_string(),
@ -9700,7 +9700,7 @@ async fn test_pre_join_after_registration_rejects_banned_user_on_final_revalidat
"member-user-ban@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Realtime User Ban".to_string(),
"test".to_string(),
@ -9785,7 +9785,7 @@ async fn test_pre_join_after_registration_rolls_back_when_room_event_subscriptio
"member-sub-fail@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Realtime Room Subscription Fail".to_string(),
"test".to_string(),
@ -10128,7 +10128,7 @@ async fn test_resource_watch_prepare_enforces_room_connection_limit_and_releases
"watch-limit-owner@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Watch Limit Room".to_string(),
"watch-limit".to_string(),
@ -10230,7 +10230,7 @@ async fn test_resource_watch_prepare_rejects_missing_observe_resource_before_sub
"watch-missing-resource-owner@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Watch Missing Resource Room".to_string(),
"watch-missing-resource".to_string(),
@ -10307,7 +10307,7 @@ async fn test_resource_watch_chat_events_requires_view_chat_history_permission()
"watch-chat-perm-owner@test.invalid",
)
.await;
let (room, _) = room_service
let room = room_service
.create_room(
"Watch Chat Permission Room".to_string(),
"watch-chat-permission".to_string(),

@ -173,15 +173,6 @@ pub fn search_chat_messages_query_from_client_proto(
client::build_search_chat_messages_query(room_id, request, public_id_codec)
}
pub fn chat_message_receive_to_client_proto(
message: &synctv_core::models::ChatMessageWithAttachments,
public_id_codec: &synctv_adapter::PublicIdCodec,
username: Option<String>,
) -> Result<synctv_proto::client::ChatMessageReceive, ApiError> {
synctv_adapter::chat::chat_message_receive_to_proto(message, public_id_codec, username)
.map_err(|error| ApiError::Internal(error.to_string()))
}
pub fn chat_history_cursor_to_client_proto(
cursor: synctv_core::models::ChatHistoryCursor,
) -> String {
@ -374,13 +365,6 @@ pub fn proto_validated_optional_playlist_id(
proto_validated_optional_id(value, public_id_codec)
}
pub fn proto_validated_optional_room_id(
value: impl AsRef<str>,
public_id_codec: &synctv_adapter::PublicIdCodec,
) -> Result<Option<synctv_core::models::RoomId>, ApiError> {
proto_validated_optional_id(value, public_id_codec)
}
pub fn proto_validated_media_ids(
values: Vec<String>,
public_id_codec: &synctv_adapter::PublicIdCodec,
@ -401,22 +385,6 @@ pub fn proto_validated_playlist_ids(
.collect()
}
pub fn parse_optional_media_id_param(
value: &str,
field: &'static str,
public_id_codec: &synctv_adapter::PublicIdCodec,
) -> Result<Option<synctv_core::models::MediaId>, ApiError> {
parse_optional_id_param(value, field, public_id_codec)
}
pub fn parse_optional_playlist_id_param(
value: &str,
field: &'static str,
public_id_codec: &synctv_adapter::PublicIdCodec,
) -> Result<Option<synctv_core::models::PlaylistId>, ApiError> {
parse_optional_id_param(value, field, public_id_codec)
}
pub fn parse_optional_id_param<T>(
value: &str,
field: &'static str,

@ -380,34 +380,6 @@ impl OAuth2ApiImpl {
}
}
/// Get authorization URL for `OAuth2` login flow
///
/// Returns the URL to redirect the user to for authorization.
/// The frontend should redirect the browser to this URL.
pub async fn get_authorization_url(
&self,
provider: &str,
redirect_url: Option<String>,
) -> Result<(String, String), ApiError> {
self.get_authorization_url_with_control(provider, redirect_url, None)
.await
}
pub async fn get_authorization_url_with_control(
&self,
provider: &str,
redirect_url: Option<String>,
control: Option<&ExecutionControl>,
) -> Result<(String, String), ApiError> {
let (auth_url, state) = self
.oauth2_service
.get_authorization_url_with_control(provider, redirect_url, control)
.await
.map_err(ApiError::from)?;
Ok((auth_url, state))
}
pub async fn get_authorization_url_response_with_control(
&self,
req: GetAuthorizationUrlRequest,
@ -444,59 +416,6 @@ impl OAuth2ApiImpl {
})
}
/// Get authorization URL for binding `OAuth2` provider to existing user
///
/// Requires authentication. The `user_id` should come from the JWT token.
///
/// Security: Returns a generic "Authentication failed" error for both
/// non-existent users and disabled accounts to prevent user enumeration attacks.
pub async fn get_authorization_url_for_bind(
&self,
user_id: &UserId,
provider: &str,
redirect_url: Option<String>,
) -> Result<(String, String), ApiError> {
self.get_authorization_url_for_bind_with_control(user_id, provider, redirect_url, None)
.await
}
pub async fn get_authorization_url_for_bind_with_control(
&self,
user_id: &UserId,
provider: &str,
redirect_url: Option<String>,
control: Option<&ExecutionControl>,
) -> Result<(String, String), ApiError> {
// Verify user exists and is not banned/deleted
// Use generic error message to prevent user enumeration attacks
let user = self
.user_service
.get_user(user_id)
.await
.map_err(Self::map_bind_user_lookup_error)?;
if user.is_deleted() || user.status == UserStatus::Banned {
// Use the same generic error to prevent distinguishing between
// "user not found" and "user is disabled"
return Err(ApiError::Authentication(
"Authentication failed".to_string(),
));
}
let (auth_url, state) = self
.oauth2_service
.get_authorization_url_with_user_with_control(
provider,
redirect_url,
Some(*user_id),
control,
)
.await
.map_err(ApiError::from)?;
Ok((auth_url, state))
}
pub async fn get_authorization_url_for_bind_response_with_control(
&self,
user_id: &UserId,
@ -552,32 +471,6 @@ impl OAuth2ApiImpl {
})
}
/// Exchange authorization code for JWT token
///
/// Frontend calls this after receiving code and state from `OAuth2` provider redirect.
///
/// For login flow (no `target_user_id` in state):
/// - If user exists: log them in
/// - If user doesn't exist: create new user account
///
/// For bind flow (`target_user_id` present in state):
/// - Binds the `OAuth2` provider to the existing user account
/// - Returns empty tokens (user is already logged in)
///
/// The `current_user_id` parameter is required for the bind flow to verify that
/// only the intended user (the one who initiated the bind) can complete it.
/// Pass `None` for login-only flows (no authentication needed).
pub async fn exchange_authorization_code(
&self,
code: &str,
state: &str,
current_user_id: Option<&UserId>,
client_ip: Option<std::net::IpAddr>,
) -> Result<ExchangeCodeResult, ApiError> {
self.exchange_authorization_code_with_control(code, state, current_user_id, client_ip, None)
.await
}
pub async fn exchange_authorization_code_with_control(
&self,
code: &str,

@ -18,16 +18,6 @@ pub enum TransportProtocol {
Grpc,
}
impl TransportProtocol {
#[must_use]
pub const fn key_segment(self) -> &'static str {
match self {
Self::Http => "http",
Self::Grpc => "grpc",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndpointRateLimitCategory {
Auth,
@ -259,11 +249,6 @@ impl ApiRequestContext {
}
}
#[must_use]
pub fn from_metadata_ref(metadata: &RequestMetadata) -> Self {
Self::from_metadata(metadata.clone())
}
#[must_use]
pub const fn metadata(&self) -> &RequestMetadata {
&self.metadata
@ -324,11 +309,6 @@ impl ApiRequestContext {
pub fn child_execution_control(&self) -> ExecutionControl {
self.control().child()
}
#[must_use]
pub fn child_cancellation_control(&self) -> ExecutionControl {
self.control().child().without_deadline()
}
}
#[derive(Clone)]
@ -361,13 +341,6 @@ impl AuthenticationAttempt {
}
}
fn into_optional_if_valid(self) -> Option<AuthenticatedToken> {
match self {
Self::Authenticated(authenticated) => Some(*authenticated),
Self::Missing | Self::Failed(_) => None,
}
}
fn into_required(self) -> Result<AuthenticatedToken, ApiError> {
match self {
Self::Missing => Err(ApiError::Authentication(
@ -400,38 +373,6 @@ impl RequestExecutor {
&self.jwt_validator
}
pub async fn authenticate_required(
&self,
metadata: &RequestMetadata,
) -> Result<AuthenticatedToken, ApiError> {
let authorization = metadata.authorization.as_deref().ok_or_else(|| {
ApiError::Authentication(synctv_common::messages::AUTHENTICATION_REQUIRED.to_string())
})?;
self.authenticate_authorization(authorization).await
}
pub async fn authenticate_optional(
&self,
metadata: &RequestMetadata,
) -> Result<Option<AuthenticatedToken>, ApiError> {
let Some(authorization) = metadata.authorization.as_deref() else {
return Ok(None);
};
self.authenticate_authorization(authorization)
.await
.map(Some)
}
pub async fn authenticate_optional_if_valid(
&self,
metadata: &RequestMetadata,
) -> Result<Option<AuthenticatedToken>, ApiError> {
Ok(self
.authenticate_for_request(metadata)
.await?
.into_optional_if_valid())
}
async fn authenticate_for_request(
&self,
metadata: &RequestMetadata,
@ -487,7 +428,7 @@ impl RequestExecutor {
#[must_use]
pub fn prepare_context(&self, metadata: &RequestMetadata) -> ApiRequestContext {
ApiRequestContext::from_metadata_ref(metadata)
ApiRequestContext::from_metadata(metadata.clone())
}
pub fn execute_public<'a, T, F, Fut>(
@ -553,22 +494,6 @@ impl RequestExecutor {
})
}
pub fn execute_optional_user<'a, T, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
category: EndpointRateLimitCategory,
operation: F,
) -> BoxFuture<'a, Result<T, ApiError>>
where
T: Send + 'a,
F: FnOnce(Option<AuthenticatedToken>) -> Fut + Send + 'a,
Fut: Future<Output = Result<T, ApiError>> + Send + 'a,
{
self.execute_optional_user_with_context(metadata, category, move |_, authenticated| {
operation(authenticated)
})
}
pub fn execute_optional_user_with_context<'a, T, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
@ -624,79 +549,6 @@ impl RequestExecutor {
)
}
pub fn execute_optional_user_if_valid<'a, T, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
category: EndpointRateLimitCategory,
operation: F,
) -> BoxFuture<'a, Result<T, ApiError>>
where
T: Send + 'a,
F: FnOnce(Option<AuthenticatedToken>) -> Fut + Send + 'a,
Fut: Future<Output = Result<T, ApiError>> + Send + 'a,
{
self.execute_optional_user_if_valid_with_context(
metadata,
category,
move |_, authenticated| operation(authenticated),
)
}
pub fn execute_optional_user_if_valid_with_context<'a, T, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
category: EndpointRateLimitCategory,
operation: F,
) -> BoxFuture<'a, Result<T, ApiError>>
where
T: Send + 'a,
F: FnOnce(ApiRequestContext, Option<AuthenticatedToken>) -> Fut + Send + 'a,
Fut: Future<Output = Result<T, ApiError>> + Send + 'a,
{
let request_context = self.prepare_context(metadata);
let request_control = request_context.child_execution_control();
async move {
request_context.check_active()?;
request_control
.run(async move {
let authentication = self.authenticate_for_request(metadata).await?;
self.enforce_rate_limit(
metadata,
category,
authentication.rate_limit_identity(),
Some(request_context.control()),
)
.await?;
let authenticated = authentication.into_optional_if_valid();
request_context.check_active()?;
operation(request_context, authenticated).await
})
.await
.map_err(|err| ApiError::Timeout(err.to_string()))?
}
.boxed()
}
pub fn execute_optional_user_if_valid_with_control<'a, T, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,
category: EndpointRateLimitCategory,
operation: F,
) -> BoxFuture<'a, Result<T, ApiError>>
where
T: Send + 'a,
F: FnOnce(ExecutionControl, Option<AuthenticatedToken>) -> Fut + Send + 'a,
Fut: Future<Output = Result<T, ApiError>> + Send + 'a,
{
self.execute_optional_user_if_valid_with_context(
metadata,
category,
move |request_context, authenticated| {
operation(request_context.child_execution_control(), authenticated)
},
)
}
pub fn execute_user<'a, T, F, Fut>(
&'a self,
metadata: &'a RequestMetadata,

@ -193,12 +193,6 @@ pub fn validate_playback_speed(speed: f64) -> InputValidationResult<f64> {
Ok(speed)
}
pub fn validate_websocket_connect_request(
request: &synctv_proto::client::WebSocketConnectRequest,
) -> Result<(), crate::impls::ApiError> {
crate::impls::validate_proto_request(request)
}
#[cfg(test)]
mod tests {
use super::*;

@ -42,9 +42,7 @@ pub mod test_support;
pub mod webrtc_status;
pub use api_runtime::*;
pub use app_state::{
create_app_state_from_options, AppState, ProxyCacheLifecycleRuntime, RouterOptions,
};
pub use app_state::{build_app_state, AppState, ProxyCacheLifecycleRuntime, RouterOptions};
pub use http_error::{AppError, AppResult};
pub use impls::*;
pub use metrics_auth::{MetricsAccessController, MetricsAccessError};

@ -1347,7 +1347,7 @@ fn normalize_hls_segment_name(
.filter(|name| !name.is_empty())
.ok_or_else(|| ApiError::InvalidInput("Invalid segment name".to_string()))?;
if let Err(error) = synctv_livestream::HlsStreamingApi::validate_segment_name(normalized) {
if let Err(error) = synctv_livestream::validate_hls_segment_name(normalized) {
tracing::warn!(
segment = %normalized,
error = %error,

@ -15,30 +15,24 @@ pub use server_state_cluster::ServerStateGrpcService;
pub use slice_cache::slice_cache_management_runtime_from_router_options;
pub use synctv_core::service::{
livestream_snapshot_from_publishers, response_for_server_state_nodes as response_for_nodes,
validate_server_state_selection, ServerStateCluster as ClusterStatus,
ServerStateClusterNode as ClusterNodeStatus, ServerStateClusterRuntime,
ServerStateClusterStatus, ServerStateClusterTarget, ServerStateCpu as CpuStatus,
ServerStateCpuStatus, ServerStateDatabase as DatabaseStatus,
ServerStateDatabasePool as DatabasePoolStatus, ServerStateDatabaseStatus,
ServerStateEmail as EmailStatus, ServerStateEmailStatus, ServerStateError, ServerStateFailure,
ServerStateLivestream as LivestreamStatus, ServerStateLivestreamRuntime,
ServerStateLivestreamSnapshot, ServerStateLivestreamStatus, ServerStateMemory as MemoryStatus,
ServerStateMemoryStatus, ServerStateNode, ServerStateNodeStatus,
ServerStateRealtime as RealtimeStatus, ServerStateRealtimeMetrics, ServerStateRealtimeRuntime,
ServerStateRedis as RedisStatus, ServerStateRedisStatus, ServerStateRemoteClient,
ServerStateResponse, ServerStateResult, ServerStateScope, ServerStateSelection,
ServerStateService as ServerStateRuntime, ServerStateServiceDependencies,
ServerStateSliceCache as SliceCacheStatus, ServerStateSliceCacheRuntime,
ServerStateSliceCacheStatus, ServerStateSummary, ServerStateWebRtc as WebRtcStatus,
ServerStateWebRtcStatus, ServerStateWsTicket as WsTicketStatus, ServerStateWsTicketStatus,
SliceCacheConfigInfo, SliceCacheEvictExpiredNodeResult, SliceCacheEvictExpiredResponse,
SliceCacheManagementClusterRuntime, SliceCacheManagementError,
livestream_snapshot_from_publishers, validate_server_state_selection, ServerStateCluster,
ServerStateClusterNode, ServerStateClusterRuntime, ServerStateClusterStatus,
ServerStateClusterTarget, ServerStateCpu, ServerStateCpuStatus, ServerStateDatabase,
ServerStateDatabasePool, ServerStateDatabaseStatus, ServerStateEmail, ServerStateEmailStatus,
ServerStateError, ServerStateFailure, ServerStateLivestream, ServerStateLivestreamRuntime,
ServerStateLivestreamSnapshot, ServerStateLivestreamStatus, ServerStateMemory,
ServerStateMemoryStatus, ServerStateNode, ServerStateNodeStatus, ServerStateRealtime,
ServerStateRealtimeMetrics, ServerStateRealtimeRuntime, ServerStateRedis,
ServerStateRedisStatus, ServerStateRemoteClient, ServerStateResponse, ServerStateResult,
ServerStateScope, ServerStateSelection, ServerStateService, ServerStateServiceDependencies,
ServerStateSliceCache, ServerStateSliceCacheRuntime, ServerStateSliceCacheStatus,
ServerStateSummary, ServerStateWebRtc, ServerStateWebRtcStatus, ServerStateWsTicket,
ServerStateWsTicketStatus, SliceCacheConfigInfo, SliceCacheEvictExpiredNodeResult,
SliceCacheEvictExpiredResponse, SliceCacheManagementClusterRuntime, SliceCacheManagementError,
SliceCacheManagementLocalRuntime, SliceCacheManagementRemoteClient, SliceCacheManagementResult,
SliceCacheManagementService as SliceCacheManagementRuntime,
SliceCacheManagementServiceDependencies, SliceCacheNodeFailure, SliceCachePurgeNodeResult,
SliceCachePurgeResponse, SliceCachePurgeResult, SliceCacheSelection,
SliceCacheStats as SliceCacheManagementStats, SliceCacheStatsNode, SliceCacheStatsResponse,
SliceCacheManagementService, SliceCacheManagementServiceDependencies, SliceCacheNodeFailure,
SliceCachePurgeNodeResult, SliceCachePurgeResponse, SliceCachePurgeResult, SliceCacheSelection,
SliceCacheStats, SliceCacheStatsNode, SliceCacheStatsResponse,
};
use synctv_realtime::sync::ConnectionRuntime;
@ -68,7 +62,7 @@ impl From<ServerStateError> for AppError {
#[must_use]
pub fn server_state_runtime_from_router_options(
config: &crate::app_state::RouterOptions,
) -> ServerStateRuntime {
) -> ServerStateService {
let cluster_runtime = config.cluster_client.as_ref().map(|client| {
Arc::new(ApiServerStateClusterRuntime {
client: client.clone(),
@ -80,7 +74,7 @@ pub fn server_state_runtime_from_router_options(
}) as Arc<dyn ServerStateRemoteClient>
});
ServerStateRuntime::new(ServerStateServiceDependencies {
ServerStateService::new(ServerStateServiceDependencies {
runtime_params: Arc::new(config.runtime_settings.server_state.clone()),
user_service: config.user_service.clone(),
realtime_runtime: Arc::new(ApiServerStateRealtimeRuntime {
@ -109,17 +103,6 @@ pub fn server_state_runtime_from_router_options(
})
}
pub async fn collect_server_state(
runtime: &ServerStateRuntime,
selection: ServerStateSelection,
) -> ServerStateResult<ServerStateResponse> {
runtime.collect_server_state(selection).await
}
pub async fn collect_local_server_state(runtime: &ServerStateRuntime) -> ServerStateNode {
runtime.collect_local_server_state().await
}
struct ApiServerStateRealtimeRuntime {
event_service: Arc<dyn RealtimeEventService>,
connection_manager: Arc<dyn ConnectionRuntime>,
@ -224,9 +207,9 @@ struct ApiServerStateSliceCacheRuntime {
}
impl ServerStateSliceCacheRuntime for ApiServerStateSliceCacheRuntime {
fn snapshot(&self) -> SliceCacheStatus {
fn snapshot(&self) -> ServerStateSliceCache {
let stats = self.cache.stats();
SliceCacheStatus {
ServerStateSliceCache {
status: if stats.engine_enabled {
ServerStateSliceCacheStatus::Healthy
} else {

@ -6,13 +6,13 @@ use super::*;
#[derive(Clone)]
pub struct ServerStateGrpcService {
runtime: Arc<ServerStateRuntime>,
runtime: Arc<ServerStateService>,
auth: synctv_cluster::grpc::ClusterAuthInterceptor,
}
impl ServerStateGrpcService {
#[must_use]
pub fn new(runtime: Arc<ServerStateRuntime>, cluster_secret: String) -> Self {
pub fn new(runtime: Arc<ServerStateService>, cluster_secret: String) -> Self {
Self {
runtime,
auth: synctv_cluster::grpc::ClusterAuthInterceptor::new(cluster_secret),
@ -209,11 +209,11 @@ pub(super) fn cluster_proto_to_server_state_node(
updated_at: node.updated_at,
version: node.version,
api_address: node.api_address,
realtime: RealtimeStatus {
realtime: ServerStateRealtime {
distributed_enabled: realtime.distributed_enabled,
connection_count: realtime.connection_count,
},
database: DatabaseStatus {
database: ServerStateDatabase {
status: cluster_database_status_to_core(database.status),
host: database.host,
port: database.port,
@ -230,7 +230,7 @@ pub(super) fn cluster_proto_to_server_state_node(
read_pool: cluster_database_pool_to_core(database.read_pool.unwrap_or_default()),
message: non_empty_string(database.message),
},
redis: RedisStatus {
redis: ServerStateRedis {
status: cluster_redis_status_to_core(redis.status),
configured: redis.configured,
deployment_mode: redis.deployment_mode,
@ -244,7 +244,7 @@ pub(super) fn cluster_proto_to_server_state_node(
ping_latency_ms: redis.ping_latency_ms,
message: non_empty_string(redis.message),
},
cluster: ClusterStatus {
cluster: ServerStateCluster {
status: cluster_resource_status_to_core(cluster.status),
enabled: cluster.enabled,
discovery_mode: cluster.discovery_mode,
@ -258,16 +258,16 @@ pub(super) fn cluster_proto_to_server_state_node(
.collect(),
message: non_empty_string(cluster.message),
},
ws_ticket: WsTicketStatus {
ws_ticket: ServerStateWsTicket {
status: cluster_ws_ticket_status_to_core(ws_ticket.status),
cross_node_capable: ws_ticket.cross_node_capable,
message: non_empty_string(ws_ticket.message),
},
email: EmailStatus {
email: ServerStateEmail {
status: cluster_email_status_to_core(email.status),
configured: email.configured,
},
livestream: LivestreamStatus {
livestream: ServerStateLivestream {
status: cluster_livestream_status_to_core(livestream.status),
configured: livestream.configured,
active_publisher_count: livestream.active_publisher_count,
@ -281,14 +281,14 @@ pub(super) fn cluster_proto_to_server_state_node(
hls_storage_path: livestream.hls_storage_path,
hls_memory_max_mb: livestream.hls_memory_max_mb,
},
memory: MemoryStatus {
memory: ServerStateMemory {
status: cluster_memory_status_to_core(memory.status),
used_bytes: memory.used_bytes,
total_bytes: memory.total_bytes,
available_bytes: memory.available_bytes,
usage_percent: memory.usage_percent,
},
webrtc: WebRtcStatus {
webrtc: ServerStateWebRtc {
status: cluster_webrtc_status_to_core(webrtc.status),
mode: webrtc.mode,
builtin_stun_configured: webrtc.builtin_stun_configured,
@ -298,7 +298,7 @@ pub(super) fn cluster_proto_to_server_state_node(
external_addr: webrtc.external_addr,
message: non_empty_string(webrtc.message),
},
cpu: CpuStatus {
cpu: ServerStateCpu {
status: cluster_cpu_status_to_core(cpu.status),
available_parallelism: cpu.available_parallelism,
current_load_1m: cpu.current_load_1m,
@ -307,7 +307,7 @@ pub(super) fn cluster_proto_to_server_state_node(
load_average_5m: cpu.load_average_5m,
load_average_15m: cpu.load_average_15m,
},
slice_cache: SliceCacheStatus {
slice_cache: ServerStateSliceCache {
status: cluster_slice_cache_status_to_core(slice_cache.status),
engine_enabled: slice_cache.engine_enabled,
backend: slice_cache.backend,
@ -330,7 +330,7 @@ pub(super) fn cluster_proto_to_server_state_node(
}
fn database_pool_to_cluster_proto(
pool: &DatabasePoolStatus,
pool: &ServerStateDatabasePool,
) -> synctv_cluster::grpc::synctv::cluster::ServerStateDatabasePool {
synctv_cluster::grpc::synctv::cluster::ServerStateDatabasePool {
size: pool.size,
@ -341,8 +341,8 @@ fn database_pool_to_cluster_proto(
fn cluster_database_pool_to_core(
pool: synctv_cluster::grpc::synctv::cluster::ServerStateDatabasePool,
) -> DatabasePoolStatus {
DatabasePoolStatus {
) -> ServerStateDatabasePool {
ServerStateDatabasePool {
size: pool.size,
idle_connections: pool.idle_connections,
active_connections: pool.active_connections,
@ -350,7 +350,7 @@ fn cluster_database_pool_to_core(
}
fn cluster_node_to_cluster_proto(
node: ClusterNodeStatus,
node: ServerStateClusterNode,
) -> synctv_cluster::grpc::synctv::cluster::ServerStateClusterNode {
synctv_cluster::grpc::synctv::cluster::ServerStateClusterNode {
node_id: node.node_id,
@ -362,8 +362,8 @@ fn cluster_node_to_cluster_proto(
fn cluster_proto_node_to_core(
node: synctv_cluster::grpc::synctv::cluster::ServerStateClusterNode,
) -> ClusterNodeStatus {
ClusterNodeStatus {
) -> ServerStateClusterNode {
ServerStateClusterNode {
node_id: node.node_id,
cluster_address: node.cluster_address,
last_heartbeat: node.last_heartbeat,

@ -13,9 +13,8 @@ use synctv_core::service::{
ServerStateClusterTarget, SliceCacheConfigInfo, SliceCacheEvictExpiredNodeResult,
SliceCacheManagementClusterRuntime, SliceCacheManagementError,
SliceCacheManagementLocalRuntime, SliceCacheManagementRemoteClient, SliceCacheManagementResult,
SliceCacheManagementService as SliceCacheManagementRuntime,
SliceCacheManagementServiceDependencies, SliceCachePurgeNodeResult, SliceCachePurgeResult,
SliceCacheStats as SliceCacheManagementStats, SliceCacheStatsNode,
SliceCacheManagementService, SliceCacheManagementServiceDependencies,
SliceCachePurgeNodeResult, SliceCachePurgeResult, SliceCacheStats, SliceCacheStatsNode,
};
const SLICE_CACHE_REMOTE_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
@ -43,7 +42,7 @@ impl From<SliceCacheManagementError> for AppError {
#[must_use]
pub fn slice_cache_management_runtime_from_router_options(
config: &crate::app_state::RouterOptions,
) -> SliceCacheManagementRuntime {
) -> SliceCacheManagementService {
let cluster_runtime = config.cluster_client.as_ref().map(|client| {
Arc::new(ApiServerStateClusterRuntime {
client: client.clone(),
@ -55,7 +54,7 @@ pub fn slice_cache_management_runtime_from_router_options(
}) as Arc<dyn SliceCacheManagementRemoteClient>
});
SliceCacheManagementRuntime::new(SliceCacheManagementServiceDependencies {
SliceCacheManagementService::new(SliceCacheManagementServiceDependencies {
node_id: config.event_service.node_id().to_string(),
local_runtime: Arc::new(ApiSliceCacheManagementLocalRuntime {
cache: config.proxy_slice_cache.clone(),
@ -97,7 +96,7 @@ struct ApiSliceCacheManagementLocalRuntime {
#[async_trait]
impl SliceCacheManagementLocalRuntime for ApiSliceCacheManagementLocalRuntime {
fn stats(&self) -> SliceCacheManagementStats {
fn stats(&self) -> SliceCacheStats {
slice_cache_stats_from_proxy(self.cache.stats())
}
@ -252,8 +251,8 @@ fn proxy_evict_expired_to_api(
fn slice_cache_stats_from_proxy(
stats: synctv_proxy::slice_cache::SliceCacheStats,
) -> SliceCacheManagementStats {
SliceCacheManagementStats {
) -> SliceCacheStats {
SliceCacheStats {
config: SliceCacheConfigInfo {
engine_enabled: stats.engine_enabled,
backend: stats.backend,

@ -15,7 +15,7 @@ use synctv_realtime::sync::{
};
use tokio::sync::{broadcast, mpsc};
use crate::impls::{AdminApiRuntime, AdminReadServices, ClientApiRuntime, RequestExecutor};
use crate::impls::{AdminReadServices, ClientApiRuntime, RequestExecutor};
use crate::realtime_fanout::ChannelRealtimeFanoutService;
use synctv_realtime::fanout::{RealtimeEventService, RealtimeFanoutService, RealtimeMetrics};
@ -100,18 +100,6 @@ pub fn client_api_runtime() -> ClientApiRuntime {
)
}
#[allow(dead_code)]
pub fn admin_api_runtime() -> AdminApiRuntime {
AdminApiRuntime::local_disabled(
Arc::new(local_request_executor()),
proxy_signing_key(b"test-admin-api-runtime-signing-key-32-bytes!"),
media_swarm_signing_key(b"test-admin-api-media-swarm-signing-key-32-bytes!"),
Arc::new(synctv_core::provider::ProviderStoreRegistry::local_only(
"test:admin:",
)),
)
}
#[derive(Default)]
pub struct RecordingRealtimeEventService {
pub broadcast_calls: AtomicUsize,

@ -88,7 +88,7 @@ fn map_slice_cache_error(
pub struct AdminServiceImpl {
admin_api: Arc<AdminApiImpl>,
runtime_settings: Arc<synctv_api_common::ApiRuntimeSettings>,
slice_cache_runtime: Arc<synctv_api_common::status::SliceCacheManagementRuntime>,
slice_cache_runtime: Arc<synctv_api_common::status::SliceCacheManagementService>,
}
impl AdminServiceImpl {
@ -96,7 +96,7 @@ impl AdminServiceImpl {
pub const fn new(
admin_api: Arc<AdminApiImpl>,
runtime_settings: Arc<synctv_api_common::ApiRuntimeSettings>,
slice_cache_runtime: Arc<synctv_api_common::status::SliceCacheManagementRuntime>,
slice_cache_runtime: Arc<synctv_api_common::status::SliceCacheManagementService>,
) -> Self {
Self {
admin_api,

@ -318,9 +318,7 @@ impl UserService for ClientServiceImpl {
client_api
.start_sensitive_operation_verification(
&authenticated.user_id(),
synctv_api_common::impls::client::token_auth_context_from_claims(
&authenticated.claims,
),
authenticated.claims.auth_context(),
req,
)
.await

@ -964,7 +964,7 @@ fn cluster_node_id(event_service: &Arc<dyn RealtimeEventService>) -> String {
fn build_fallback_http_app_state(
deps: FallbackHttpAppStateDeps,
) -> anyhow::Result<Arc<synctv_api_common::AppState>> {
Ok(Arc::new(synctv_api_common::create_app_state_from_options(
Ok(Arc::new(synctv_api_common::build_app_state(
synctv_api_common::RouterOptions {
runtime_settings: deps.runtime_settings,
user_service: deps.user_service,
@ -3428,7 +3428,7 @@ mod tests {
.jwt_service
.sign_access_token(&owner.id, 0)
.map_err(|error| test_error(error.to_string()))?;
let (room, _) = room_service
let room = room_service
.create_room(
"gRPC Chat Watch Room".to_string(),
String::new(),

@ -1089,7 +1089,7 @@ pub async fn logout(
let authorization = request_meta.authorization.clone();
let executor = state.shared_api_runtime.client_api.clone();
let client_api = state.shared_api_runtime.client_api.clone();
let outcome = executor
executor
.execute_user_endpoint(
&request_meta,
EndpointRateLimitCategory::Write,
@ -1113,7 +1113,7 @@ pub async fn logout(
Ok(Json(LogoutResponse {
success: true,
message: outcome.message.to_string(),
message: String::new(),
}))
}

@ -21,6 +21,7 @@ use tracing::warn;
use crate::http::AppState;
use synctv_api_common::observability::metrics;
use synctv_api_common::webrtc_status;
use synctv_core::service::{email_health, ws_ticket_backend_is_safe_for_mode, ws_ticket_health};
use synctv_proto::client::{HealthDetails, HealthResponse, MemoryHealth};
/// Timeout for individual health check probes (DB, Redis).
@ -139,7 +140,7 @@ pub async fn readiness_check(State(state): State<AppState>) -> impl IntoResponse
let ws_ticket_status = {
let svc = &state.ws_ticket_service;
let is_cluster_mode = state.runtime_settings.cluster_runtime_enabled();
let health = check_ws_ticket_health(svc.as_ref());
let health = ws_ticket_health(svc.as_ref());
if ws_ticket_backend_is_safe_for_mode(svc.as_ref(), is_cluster_mode) {
Some(health)
} else {
@ -158,10 +159,7 @@ pub async fn readiness_check(State(state): State<AppState>) -> impl IntoResponse
};
// Check email service (if configured) - validates SMTP config is present
let email_status = state
.email_service
.as_ref()
.map(|svc| check_email_health(svc));
let email_status = state.email_service.as_ref().map(|svc| email_health(svc));
// Check livestream infrastructure (if configured)
let livestream_status = state
@ -321,31 +319,6 @@ pub(crate) async fn check_redis_health_from_conn(
}
}
/// Check WebSocket ticket service health
///
/// Reports whether the service supports cross-node ticket validation.
pub(crate) fn check_ws_ticket_health(
svc: &dyn synctv_core::service::WebSocketTicketService,
) -> String {
synctv_core::service::ws_ticket_health(svc)
}
pub(crate) fn ws_ticket_backend_is_safe_for_mode(
svc: &dyn synctv_core::service::WebSocketTicketService,
cluster_mode: bool,
) -> bool {
synctv_core::service::ws_ticket_backend_is_safe_for_mode(svc, cluster_mode)
}
/// Check email service health
///
/// Validates that the email service has SMTP configuration. The service is
/// considered healthy if it is configured with valid SMTP settings; if no
/// config is provided the service is present but unconfigured (informational).
pub(crate) fn check_email_health(svc: &synctv_core::service::EmailService) -> String {
synctv_core::service::email_health(svc)
}
/// Memory usage threshold percentage for marking the node as unhealthy.
/// When memory usage exceeds this threshold, the node should not receive
/// additional traffic to prevent OOM or performance degradation.
@ -885,11 +858,11 @@ mod tests {
"distributed mode should accept any store that advertises cluster capability"
);
assert!(
check_ws_ticket_health(&shared_tickets).contains("cross-node capable"),
ws_ticket_health(&shared_tickets).contains("cross-node capable"),
"health helper should expose capability rather than backend implementation"
);
assert!(
check_ws_ticket_health(&memory_tickets).contains("single-node"),
ws_ticket_health(&memory_tickets).contains("single-node"),
"health helper should expose capability rather than backend implementation"
);
}
@ -911,7 +884,7 @@ mod tests {
fn test_email_health_reports_configuration_only() -> TestResult {
let unconfigured =
synctv_core::service::EmailService::new(Arc::new(TestEmailConfigProvider(None)))?;
assert_eq!(check_email_health(&unconfigured), "not configured");
assert_eq!(email_health(&unconfigured), "not configured");
let configured = synctv_core::service::EmailService::new(Arc::new(
TestEmailConfigProvider(Some(synctv_core::service::EmailConfig {
@ -927,7 +900,7 @@ mod tests {
use_tls: true,
})),
))?;
assert_eq!(check_email_health(&configured), "configured");
assert_eq!(email_health(&configured), "configured");
Ok(())
}

@ -261,12 +261,11 @@ static X_FORWARDED_PROTO: LazyLock<HeaderName> =
LazyLock::new(|| HeaderName::from_static("x-forwarded-proto"));
pub use synctv_api_common::app_state::{
build_app_state, create_app_state_from_options, AppState, ProxyCacheLifecycleRuntime,
RouterOptions,
build_app_state, AppState, ProxyCacheLifecycleRuntime, RouterOptions,
};
pub fn create_router_from_options(options: RouterOptions) -> anyhow::Result<axum::Router> {
let state = create_app_state_from_options(options)?;
let state = build_app_state(options)?;
create_router_from_shared_state(&state)
}
@ -279,7 +278,7 @@ pub fn create_router_from_shared_state(state: &AppState) -> anyhow::Result<axum:
pub fn create_router_with_state_from_options(
options: RouterOptions,
) -> anyhow::Result<(axum::Router, AppState)> {
let state = create_app_state_from_options(options)?;
let state = build_app_state(options)?;
let router = create_router_from_shared_state(&state)?;
Ok((router, state))
}

@ -1912,7 +1912,7 @@ async fn test_chat_events_sse_receives_live_send_event() -> TestResult {
.await,
)?;
let access_token = core_ok(state.jwt_service.sign_access_token(&owner.id, 0))?;
let (room, _) = core_ok(
let room = core_ok(
state
.room_service
.create_room(
@ -2030,7 +2030,7 @@ async fn test_chat_events_sse_replays_after_last_event_id_header() -> TestResult
.await,
)?;
let access_token = core_ok(state.jwt_service.sign_access_token(&owner.id, 0))?;
let (room, _) = core_ok(
let room = core_ok(
state
.room_service
.create_room(
@ -2177,7 +2177,7 @@ async fn test_chat_events_sse_unknown_last_event_id_returns_bad_request() -> Tes
.await,
)?;
let access_token = core_ok(state.jwt_service.sign_access_token(&owner.id, 0))?;
let (room, _) = core_ok(
let room = core_ok(
state
.room_service
.create_room(

@ -611,9 +611,7 @@ pub async fn start_sensitive_operation_verification(
client_api
.start_sensitive_operation_verification(
&auth.user_id(),
synctv_api_common::impls::client::token_auth_context_from_claims(
&auth.claims,
),
auth.claims.auth_context(),
req,
)
.await

@ -1215,10 +1215,8 @@ async fn prepare_websocket_upgrade(
request_meta: &ApiRequestMetadata,
handshake_control: &ExecutionControl,
) -> Result<PreparedWebSocketUpgrade, AppError> {
synctv_api_common::impls::validation::validate_websocket_connect_request(
&websocket_connect_request(query),
)
.map_err(crate::http::error::map_api_error)?;
synctv_api_common::impls::validate_proto_request(&websocket_connect_request(query))
.map_err(crate::http::error::map_api_error)?;
validate_websocket_origin(
headers,

@ -868,8 +868,7 @@ async fn test_validate_websocket_room_membership_rejects_room_with_inactive_crea
None,
)
.await
.map_err(|error| test_error(error.to_string()))?
.0;
.map_err(|error| test_error(error.to_string()))?;
room_service
.join_room(room.id, member.id, None)
.await

@ -6,10 +6,9 @@ pub mod openapi;
pub(crate) mod providers;
pub use http::{
create_app_state_from_options, create_health_router, create_metrics_router,
create_router_from_options, create_router_from_shared_state,
create_router_with_state_from_options, extract_client_ip as extract_http_client_ip,
hsts_header, liveness_check, map_api_error as map_http_api_error, security_headers_middleware,
build_app_state, create_health_router, create_metrics_router, create_router_from_options,
create_router_from_shared_state, create_router_with_state_from_options, extract_client_ip,
hsts_header, liveness_check, map_api_error, security_headers_middleware,
start_proxy_cache_lifecycle, websocket_handler, AppError, AppResult, AppState, AuthMethod,
ProxyCacheLifecycleRuntime, RouterOptions,
};

@ -87,7 +87,7 @@ fn playback_provider_options_preflight_for_server(
None => None,
};
let cors_config = synctv_proxy::CorsConfig::new(server.cors_allowed_origins.clone());
synctv_proxy::proxy_options_preflight_with_cors(origin, &cors_config)
synctv_proxy::handle_cors_preflight(origin, &cors_config)
}
pub(crate) fn app_error_from_control(err: &synctv_common::ExecutionControlError) -> AppError {

@ -7,7 +7,7 @@ pub use synctv_api_grpc::{
};
pub use synctv_api_http::{
create_health_router, create_metrics_router, create_router_from_options,
create_router_from_shared_state, create_router_with_state_from_options, extract_http_client_ip,
hsts_header, liveness_check, map_http_api_error, security_headers_middleware,
create_router_from_shared_state, create_router_with_state_from_options, extract_client_ip,
hsts_header, liveness_check, map_api_error, security_headers_middleware,
start_proxy_cache_lifecycle, websocket_handler, AuthMethod,
};

@ -193,7 +193,7 @@ async fn test_chat_write_endpoints_require_signed_in_user_and_chat_permission()
.create(&make_user("chat_write_member"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Chat Write Permission Room".to_string(),
String::new(),
@ -328,7 +328,7 @@ async fn test_chat_read_endpoints_require_view_chat_history_permission() {
.create(&make_user("chat_read_member"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Chat Read Permission Room".to_string(),
String::new(),
@ -441,7 +441,7 @@ async fn test_chat_delete_endpoint_allows_sender_and_delete_chat_messages_permis
.create(&make_user("chat_delete_admin"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Chat Delete Permission Room".to_string(),
String::new(),
@ -579,7 +579,7 @@ async fn test_chat_admin_delete_endpoint_writes_audit_log() {
.create(&make_user("chat_audit_admin"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Chat Audit Room".to_string(),
String::new(),

@ -5,7 +5,7 @@
#![allow(clippy::unwrap_used)]
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use synctv_api::extract_http_client_ip as extract_client_ip;
use synctv_api::extract_client_ip;
use synctv_api::ApiRuntimeSettings as Config;
/// Create a config with the given trusted proxies.

@ -191,7 +191,7 @@ async fn create_client_api_fixture() -> ClientApiFixture {
.create(&make_user("availability_creator"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Availability Room".to_string(),
String::new(),
@ -272,7 +272,7 @@ async fn single_room_discovery_uses_primary_while_feed_uses_read_pool() {
.create(&make_user("primary_discovery_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Primary Discovery Room".to_string(),
String::new(),

@ -514,7 +514,7 @@ async fn test_get_playback_returns_dynamic_playlist_item_playback_info() {
.create(&make_user("api_dynamic_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Dynamic Playback".to_string(),
String::new(),
@ -653,7 +653,7 @@ async fn test_list_playlist_items_returns_current_path_for_dynamic_playlist() {
.create(&make_user("api_dynamic_path_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Dynamic Path".to_string(),
String::new(),
@ -761,7 +761,7 @@ async fn test_dynamic_playlist_get_playback_uses_bound_provider_instance() {
.create(&make_user("api_dynamic_bound_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Dynamic Bound Playback".to_string(),
String::new(),
@ -863,7 +863,7 @@ async fn test_static_provider_playback_with_signing_key_uses_provider_store_regi
.create(&make_user("api_playback_provider_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Signed Provider Playback".to_string(),
String::new(),
@ -986,7 +986,7 @@ async fn test_get_playback_without_active_media_returns_idle_playback_info() {
.create(&make_user("api_idle_playback_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Idle Playback".to_string(),
String::new(),
@ -1078,7 +1078,7 @@ async fn test_get_playback_returns_state_when_playback_info_generation_fails() {
.create(&make_user("api_playback_state_only"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Playback State Only".to_string(),
String::new(),
@ -1187,7 +1187,7 @@ async fn test_start_playback_returns_error_for_invalid_live_proxy_source_config(
.create(&make_user("api_playback_invalid_live_proxy"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Invalid Live Proxy Playback".to_string(),
String::new(),
@ -1285,7 +1285,7 @@ async fn test_dynamic_playlist_list_items_uses_bound_provider_instance() {
.create(&make_user("api_dynamic_bound_list_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Dynamic Bound Browse".to_string(),
String::new(),
@ -1380,7 +1380,7 @@ async fn test_list_playlist_items_allows_room_root_with_empty_playlist_id() {
.create(&make_user("api_root_items_owner"))
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"API Root Items".to_string(),
String::new(),
@ -1391,36 +1391,21 @@ async fn test_list_playlist_items_allows_room_root_with_empty_playlist_id() {
.await
.unwrap();
let root_media = synctv_core::models::Media::from_direct_single_mode(
None,
room.id,
Some(owner.id),
"Root Media".to_string(),
"direct",
synctv_core::models::PlaybackInfo {
thumbnail: None,
medias: vec![synctv_core::models::PlaybackMedia {
name: String::new(),
format: "mp4".to_string(),
expire_at: None,
metadata: None,
p2p_swarm_id: None,
provider: synctv_core::models::PlaybackMediaProvider::DirectUrl(
synctv_core::models::PlaybackDirectUrlMedia::Direct {
url: "https://example.com/root.mp4".to_string(),
headers: std::collections::HashMap::new(),
},
),
}],
default_media_index: None,
subtitles: Vec::new(),
default_subtitle_index: None,
danmakus: Vec::new(),
default_danmaku_index: None,
let root_media = synctv_core::models::Media::from_provider_with_params(
synctv_core::models::FromProviderParams {
playlist_id: None,
room_id: room.id,
creator_id: Some(owner.id),
name: "Root Media".to_string(),
description: String::new(),
source_config: synctv_core_testing::direct_url_media_source_config(
"https://example.com/root.mp4",
),
source_provider: synctv_core::models::SourceProvider::DirectUrl,
provider_instance_name: None,
position: 0.0,
},
0.0,
)
.expect("direct media should build");
);
synctv_core::repository::MediaRepository::new(pool.clone())
.create(&root_media)
.await

@ -142,36 +142,11 @@ mod error_responses {
#[tokio::test]
async fn test_app_error_helpers_return_public_messages() {
let cases = [
(
AppError::invalid_credentials(),
StatusCode::UNAUTHORIZED,
vec!["Invalid username or password"],
),
(
AppError::session_expired(),
StatusCode::UNAUTHORIZED,
vec!["expired"],
),
(
AppError::token_invalid(),
StatusCode::UNAUTHORIZED,
Vec::new(),
),
(
AppError::permission_denied(),
StatusCode::FORBIDDEN,
Vec::new(),
),
(
AppError::resource_not_found("Room"),
StatusCode::NOT_FOUND,
vec!["Room"],
),
(
AppError::validation_failed("email", "must be valid"),
StatusCode::BAD_REQUEST,
vec!["email", "must be valid"],
),
(
AppError::bad_request("Invalid email format"),
StatusCode::BAD_REQUEST,
@ -311,8 +286,7 @@ mod error_responses {
mod error_classification {
use axum::http::StatusCode;
use synctv_api::map_http_api_error as map_api_error;
use synctv_api::ApiError;
use synctv_api::{map_api_error, ApiError};
use synctv_core::Error as CoreError;
#[test]

@ -102,7 +102,7 @@ async fn test_list_my_rooms_relation_filter_and_response_relation_are_consistent
.await
.unwrap();
let (created_room, _) = room_service
let created_room = room_service
.create_room(
"Actor Created Room".to_string(),
String::new(),
@ -113,7 +113,7 @@ async fn test_list_my_rooms_relation_filter_and_response_relation_are_consistent
.await
.unwrap();
let (participating_room, _) = room_service
let participating_room = room_service
.create_room(
"External Created Room".to_string(),
String::new(),

@ -254,7 +254,7 @@ async fn test_client_member_approval_api_contracts() {
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Client Approval API Room".to_string(),
String::new(),
@ -389,7 +389,7 @@ async fn test_admin_member_approval_api_contracts() {
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Admin Approval API Room".to_string(),
String::new(),
@ -503,7 +503,7 @@ async fn test_client_room_join_review_uses_request_id_not_user_id() {
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Stale Request-ID Review Room".to_string(),
String::new(),
@ -587,7 +587,7 @@ async fn test_admin_room_join_review_uses_request_id_not_user_id() {
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Admin Stale Request-ID Review Room".to_string(),
String::new(),
@ -667,7 +667,7 @@ async fn test_room_join_review_approval_rejects_globally_banned_target() {
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Banned Target Review Room".to_string(),
String::new(),
@ -739,7 +739,7 @@ async fn test_add_member_resolves_existing_room_join_review() {
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Add Resolves Review Room".to_string(),
String::new(),

@ -18,7 +18,6 @@ use synctv_core::{
BruteForceProtection, InMemoryTokenBlacklistStore, JwtService, RoomService, UserService,
},
};
use synctv_proto::common::MemberStatus;
use synctv_realtime::sync::{ConnectionLimits, ConnectionManager};
fn make_user(username: &str) -> User {
@ -111,7 +110,7 @@ async fn test_join_room_response_exposes_pending_membership_contract() {
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Approval Contract Room".to_string(),
String::new(),
@ -125,7 +124,7 @@ async fn test_join_room_response_exposes_pending_membership_contract() {
let public_id_codec = synctv_api::PublicIdCodec::plain();
let room_id = public_id_codec.encode_room_id(room.id).unwrap();
let response = client_api
.join_room(
.join_room_with_control(
&joiner.id,
&room_id,
synctv_proto::client::JoinRoomRequest {
@ -135,15 +134,11 @@ async fn test_join_room_response_exposes_pending_membership_contract() {
display_tag: String::new(),
},
None,
None,
)
.await
.unwrap();
assert_eq!(
response.membership_status,
MemberStatus::Active as i32,
"join response returns the synthetic requester member separately from review status"
);
assert!(
response.requires_approval,
"join response must explicitly tell the client that approval is required"

@ -140,7 +140,7 @@ async fn test_get_room_members_requires_view_members_permission() {
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Member Visibility Room".to_string(),
String::new(),
@ -222,7 +222,7 @@ async fn test_get_room_members_hides_pending_members_from_non_moderators() {
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Pending Visibility Room".to_string(),
String::new(),
@ -341,7 +341,7 @@ async fn test_get_room_members_returns_stable_version_until_membership_changes()
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Member Version Room".to_string(),
String::new(),
@ -420,7 +420,7 @@ async fn test_get_room_members_marks_realtime_connections_online() {
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Member Online Room".to_string(),
String::new(),

@ -282,7 +282,7 @@ async fn test_finish_room_password_registration_rejects_session_for_different_ro
.create(&make_user("password_registration_owner"))
.await
.unwrap();
let (room_a, _) = room_service
let room_a = room_service
.create_room(
"Password Registration A".to_string(),
String::new(),
@ -292,7 +292,7 @@ async fn test_finish_room_password_registration_rejects_session_for_different_ro
)
.await
.unwrap();
let (room_b, _) = room_service
let room_b = room_service
.create_room(
"Password Registration B".to_string(),
String::new(),
@ -360,7 +360,7 @@ async fn test_finish_room_password_login_rejects_session_for_different_room_befo
.create(&make_user("password_login_member"))
.await
.unwrap();
let (room_a, _) = room_service
let room_a = room_service
.create_room(
"Password Login A".to_string(),
String::new(),
@ -370,7 +370,7 @@ async fn test_finish_room_password_login_rejects_session_for_different_room_befo
)
.await
.unwrap();
let (room_b, _) = room_service
let room_b = room_service
.create_room(
"Password Login B".to_string(),
String::new(),
@ -470,7 +470,7 @@ async fn test_client_api_room_password_success_resets_bruteforce_counter() {
let owner = user_repo.create(&make_user("room_owner")).await.unwrap();
let member = user_repo.create(&make_user("room_member")).await.unwrap();
let (room, _member) = room_service
let room = room_service
.create_room(
"Protected Room".to_string(),
"Room with password".to_string(),
@ -561,7 +561,7 @@ async fn test_preissued_room_password_opaque_sessions_cannot_bypass_finish_locko
.create(&make_user("preissued_room_member"))
.await
.unwrap();
let (room, _member) = room_service
let room = room_service
.create_room(
"Preissued Protected Room".to_string(),
"Room with password".to_string(),

@ -13,9 +13,8 @@ use synctv_api::{
use synctv_core::{
cache::{KeyBuilder, UsernameCache},
models::{
room_settings::RequireApproval, MemberStatus, ReviewRequestId, ReviewStatus,
RoomAdminPermissionBits, RoomRole, RoomSettings, RoomStatus, SignupMethod, User, UserId,
UserRole, UserStatus,
room_settings::RequireApproval, ReviewRequestId, ReviewStatus, RoomAdminPermissionBits,
RoomRole, RoomSettings, RoomStatus, SignupMethod, User, UserId, UserRole, UserStatus,
},
repository::{
ProviderInstanceRepository, RoomMemberRepository, RoomRepository, SettingsRepository,
@ -204,7 +203,7 @@ async fn test_add_member_rejects_banned_user_status() {
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"User Status Matrix Room".to_string(),
String::new(),
@ -297,7 +296,7 @@ async fn test_member_permission_matrix_controls_moderation_apis() {
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Moderation Permission Matrix Room".to_string(),
String::new(),
@ -509,7 +508,7 @@ async fn test_update_member_permissions_requires_admin_override_fields_for_admin
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Admin Override Matrix Room".to_string(),
String::new(),
@ -621,7 +620,7 @@ async fn test_transfer_room_ownership_requires_creator_and_active_member_target(
require_approval: RequireApproval(true),
..Default::default()
};
let (room, _) = room_service
let room = room_service
.create_room(
"Ownership Matrix Room".to_string(),
String::new(),
@ -769,7 +768,7 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
.await
.unwrap();
let (_public_room, _) = room_service
let _public_room = room_service
.create_room(
"Matrix Public Room".to_string(),
"public room".to_string(),
@ -779,7 +778,7 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
)
.await
.unwrap();
let (pending_room, _) = room_service
let pending_room = room_service
.create_room(
"Matrix Pending Room".to_string(),
"pending room".to_string(),
@ -789,7 +788,7 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
)
.await
.unwrap();
let (rejected_room, _) = room_service
let rejected_room = room_service
.create_room(
"Matrix Rejected Room".to_string(),
"rejected room".to_string(),
@ -799,7 +798,7 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
)
.await
.unwrap();
let (closed_room, _) = room_service
let closed_room = room_service
.create_room(
"Matrix Closed Room".to_string(),
"closed room".to_string(),
@ -809,7 +808,7 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
)
.await
.unwrap();
let (banned_room, _) = room_service
let banned_room = room_service
.create_room(
"Matrix Banned Room".to_string(),
"banned room".to_string(),
@ -823,7 +822,7 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
require_approval: RequireApproval(true),
..Default::default()
};
let (joined_room, _) = room_service
let joined_room = room_service
.create_room(
"Matrix Joined Count Room".to_string(),
"joined room".to_string(),
@ -870,10 +869,11 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
.await
.unwrap();
room_service
.approve_join_request(
.approve_join_request_with_outbox(
joined_room.id,
external_owner.id,
ReviewRequestId::expect_positive(actor_join_request_id),
None,
)
.await
.unwrap();
@ -1084,15 +1084,6 @@ async fn test_room_state_filters_and_member_count_ignore_pending_and_banned_memb
3,
"fixture should contain only active membership rows"
);
assert_eq!(
joined_members
.iter()
.filter(|member| member.status == MemberStatus::Active)
.count(),
3,
"fixture sanity check: exactly 3 memberships should be active"
);
let _ = root_admin;
}
@ -1268,7 +1259,7 @@ async fn test_join_room_rejects_banned_rooms() {
.await
.unwrap();
let (room, _) = room_service
let room = room_service
.create_room(
"Join Banned Room".to_string(),
String::new(),
@ -1281,7 +1272,7 @@ async fn test_join_room_rejects_banned_rooms() {
room_repo.update_ban_status(&room.id, true).await.unwrap();
let error = client_api
.join_room(
.join_room_with_control(
&joiner.id,
&public_id_codec().encode_room_id(room.id).unwrap(),
synctv_proto::client::JoinRoomRequest {
@ -1291,6 +1282,7 @@ async fn test_join_room_rejects_banned_rooms() {
display_tag: String::new(),
},
None,
None,
)
.await
.expect_err("banned room must reject join_room");

@ -161,7 +161,9 @@ mod permissions {
use super::*;
use std::sync::Arc;
use synctv_api::{ApiError, ClientApiImpl};
use synctv_api::{
ApiError, ClientApiImpl, EndpointRateLimitCategory, RequestMetadata, TransportProtocol,
};
use synctv_core::cache::{l2_backend::RedisCacheL2, KeyBuilder, UsernameCache};
use synctv_core::models::room_settings::{AllowGuestJoin, GuestAddedPermissions};
use synctv_core::repository::SettingsRepository;
@ -338,7 +340,7 @@ mod permissions {
let creator = register_fixture_user(&fixture, "webrtc_turn_creator").await;
let member = register_fixture_user(&fixture, "webrtc_turn_member").await;
let (room, _) = fixture
let room = fixture
.room_service
.create_room(
"WebRTC Custom ICE Room".to_string(),
@ -406,7 +408,7 @@ mod permissions {
let creator = register_fixture_user(&fixture, "webrtc_guest_creator").await;
let (room, _) = fixture
let room = fixture
.room_service
.create_room(
"WebRTC Guest ICE Room".to_string(),
@ -447,17 +449,17 @@ mod permissions {
.public_id_codec
.encode_room_id(room.id)
.expect("public room id");
let actor = fixture
.client_api
.room_actor_for_authorization(&format!("Bearer {token}"), &public_room_id)
.await
.expect("guest actor");
let response = fixture
.client_api
.get_ice_servers_for_actor(&actor)
.await
.expect("guest ICE bootstrap");
let metadata = RequestMetadata::new(TransportProtocol::Http)
.with_authorization(Some(format!("Bearer {token}")));
let response = ClientApiImpl::execute_room_actor_endpoint(
Arc::new(fixture.client_api.clone()),
&metadata,
public_room_id,
EndpointRateLimitCategory::Read,
|client_api, actor| async move { client_api.get_ice_servers_for_actor(&actor).await },
)
.await
.expect("guest ICE bootstrap");
assert_eq!(
response.servers[0].urls,
@ -485,7 +487,7 @@ mod permissions {
let creator = register_fixture_user(&fixture, "webrtc_creator").await;
let member = register_fixture_user(&fixture, "webrtc_member").await;
let (room, _) = fixture
let room = fixture
.room_service
.create_room(
"WebRTC Permission Room".to_string(),
@ -557,7 +559,7 @@ mod permissions {
let creator = register_fixture_user(&fixture, "webrtc_banned_creator").await;
let member = register_fixture_user(&fixture, "webrtc_banned_member").await;
let (room, _) = fixture
let room = fixture
.room_service
.create_room(
"WebRTC Banned Room".to_string(),
@ -602,7 +604,7 @@ mod permissions {
let creator = register_fixture_user(&fixture, "webrtc_closed_creator").await;
let member = register_fixture_user(&fixture, "webrtc_closed_member").await;
let (mut room, _) = fixture
let mut room = fixture
.room_service
.create_room(
"WebRTC Closed Room".to_string(),

@ -225,18 +225,6 @@ mod jwt_auth {
assert!(svc.verify_token("").is_err());
}
#[test]
fn test_validator_extract_user_id_from_bearer() {
let svc = test_jwt_service();
let validator = test_validator();
let user_id = UserId::expect_positive(10_000_018);
let token = svc.sign_access_token(&user_id, 0).unwrap();
let _bearer = format!("Bearer {token}");
let extracted = validator.validate_and_extract_user_id(&token).unwrap();
assert_eq!(extracted, user_id);
}
#[test]
fn test_validator_http_bearer_header() {
let svc = test_jwt_service();
@ -1261,8 +1249,8 @@ mod websocket_e2e {
playback_duration_probe: None,
};
let state = synctv_api::create_app_state_from_options(router_options)
.expect("test HTTP app state should build");
let state =
synctv_api::build_app_state(router_options).expect("test HTTP app state should build");
let app = axum::Router::new()
.route("/ws/rooms/{roomId}", axum::routing::get(websocket_handler))
@ -1325,7 +1313,7 @@ mod websocket_e2e {
user_id: &UserId,
room_name: &str,
) -> String {
let (room, _member) = room_service
let room = room_service
.create_room(room_name.to_string(), String::new(), *user_id, None, None)
.await
.expect("create room");

@ -178,33 +178,6 @@ impl HealthMonitor {
}
}
/// Create a new health monitor with custom probe configuration.
///
/// Creates its own independent `CancellationToken`. For integration with an
/// application-wide shutdown hierarchy, combine with
/// [`with_cancellation_token`](Self::with_cancellation_token) or call
/// [`set_cancellation_token`](Self::set_cancellation_token) after construction.
#[must_use]
pub fn with_probe_config<N>(
node_registry: Arc<N>,
check_interval_secs: u64,
probe_config: HealthProbeConfig,
) -> Self
where
N: ClusterNodeDirectory + 'static,
{
Self {
node_registry,
check_interval_secs,
health_status: Arc::new(RwLock::new(std::collections::HashMap::new())),
cancel_token: CancellationToken::new(),
last_successful_refresh_at: Arc::new(std::sync::atomic::AtomicU64::new(0)),
probe_config,
probe_states: Arc::new(RwLock::new(std::collections::HashMap::new())),
join_handle: Mutex::new(None),
}
}
/// Create a new health monitor that participates in an external shutdown hierarchy
/// and uses a caller-supplied probe configuration.
#[must_use]
@ -647,7 +620,12 @@ mod tests {
probe_interval_secs: 30,
cluster_secret: String::new(),
};
let monitor = HealthMonitor::with_probe_config(registry, 10, config);
let monitor = HealthMonitor::with_cancellation_token_and_probe_config(
registry,
10,
&CancellationToken::new(),
config,
);
assert_eq!(monitor.probe_config.failure_threshold, 5);
assert_eq!(monitor.probe_config.success_threshold, 3);
Ok(())

@ -19,7 +19,7 @@ use tokio::sync::RwLock;
use tokio::time::{interval, Duration};
use tokio_util::sync::CancellationToken;
use super::node_registry::{NodeDiscoverySource, NodeInfo};
use super::node_registry::NodeInfo;
use super::probe_node_identity;
use super::runtime::ClusterNodeDirectory;
use crate::error::{Error, Result};
@ -122,16 +122,8 @@ impl K8sDnsDiscovery {
}
}
/// Attach a `NodeRegistry` so that readiness-verified DNS peers are merged
/// into the local transient node view used by health monitoring and routing.
#[must_use]
pub fn with_node_registry<N>(self, registry: Arc<N>) -> Self
where
N: ClusterNodeDirectory + 'static,
{
self.with_node_directory(registry)
}
/// Attach a node directory so readiness-verified DNS peers are merged into
/// the local transient node view used by health monitoring and routing.
#[must_use]
pub fn with_node_directory(mut self, registry: Arc<dyn ClusterNodeDirectory>) -> Self {
self.node_registry = Some(registry);
@ -161,19 +153,14 @@ impl K8sDnsDiscovery {
.collect();
for (_, info) in verified_peers {
registry
.upsert_discovered_local_node(info, NodeDiscoverySource::K8sDns)
.await;
registry.upsert_discovered_local_node(info).await;
}
for node_id in old_mapping
.values()
.filter(|node_id| !new_node_ids.contains(*node_id))
{
if !registry
.remove_discovered_local_node(node_id, NodeDiscoverySource::K8sDns)
.await
{
if !registry.remove_discovered_local_node(node_id).await {
tracing::debug!(
node_id,
"stale K8s DNS discovered node was already absent from local registry"
@ -226,7 +213,7 @@ impl K8sDnsDiscovery {
/// Resolve peers and update the internal cache.
///
/// When a `NodeRegistry` is attached (via [`with_node_registry`]), this also:
/// When a node directory is attached via [`with_node_directory`](Self::with_node_directory), this also:
/// - Probes newly discovered peers to confirm gRPC readiness and real node identity
/// - Merges verified peers into the registry's transient local node view
/// - Removes disappeared transient DNS-only entries without touching Redis membership
@ -252,10 +239,9 @@ impl K8sDnsDiscovery {
let mut verified_peers = Vec::new();
for (peer, identity) in probe_results {
if let Some(identity) = identity {
let mut info =
let info =
NodeInfo::new(identity.node_id, peer.cluster_address.clone())
.with_epoch(identity.epoch);
info.set_discovery_source(NodeDiscoverySource::K8sDns);
verified_peers.push((peer.ip.clone(), info));
} else {
tracing::debug!(
@ -444,21 +430,20 @@ mod tests {
8080,
"10.0.0.1".to_string(),
)
.with_node_registry(registry.clone());
.with_node_directory(registry.clone());
let mut node_info =
let node_info =
NodeInfo::new("peer-node-1".to_string(), "10.0.0.2:50051".to_string()).with_epoch(7);
node_info.set_discovery_source(NodeDiscoverySource::K8sDns);
disc.sync_verified_peers_to_registry(vec![("10.0.0.2".to_string(), node_info)])
.await;
assert!(
registry.test_get_local("peer-node-1").await.is_some(),
registry.get_node_local("peer-node-1").await.is_some(),
"verified DNS peers must be keyed by probed node_id"
);
assert!(
registry.test_get_local("10.0.0.2").await.is_none(),
registry.get_node_local("10.0.0.2").await.is_none(),
"DNS IP must not be used as authoritative node_id"
);
Ok(())
@ -472,18 +457,17 @@ mod tests {
8080,
"10.0.0.1".to_string(),
)
.with_node_registry(registry.clone());
.with_node_directory(registry.clone());
let mut node_info = NodeInfo::new("peer-node-1".to_string(), "10.0.0.2:50051".to_string());
node_info.set_discovery_source(NodeDiscoverySource::K8sDns);
let node_info = NodeInfo::new("peer-node-1".to_string(), "10.0.0.2:50051".to_string());
disc.sync_verified_peers_to_registry(vec![("10.0.0.2".to_string(), node_info)])
.await;
assert!(registry.test_get_local("peer-node-1").await.is_some());
assert!(registry.get_node_local("peer-node-1").await.is_some());
disc.sync_verified_peers_to_registry(Vec::new()).await;
assert!(
registry.test_get_local("peer-node-1").await.is_none(),
registry.get_node_local("peer-node-1").await.is_none(),
"vanished DNS peers should be removed from transient local cache"
);
Ok(())
@ -498,10 +482,9 @@ mod tests {
8080,
"10.0.0.1".to_string(),
)
.with_node_registry(registry.clone());
.with_node_directory(registry.clone());
let mut dns_node = NodeInfo::new("peer-node-1".to_string(), "10.0.0.2:50051".to_string());
dns_node.set_discovery_source(NodeDiscoverySource::K8sDns);
let dns_node = NodeInfo::new("peer-node-1".to_string(), "10.0.0.2:50051".to_string());
disc.sync_verified_peers_to_registry(vec![("10.0.0.2".to_string(), dns_node)])
.await;
@ -515,7 +498,7 @@ mod tests {
disc.sync_verified_peers_to_registry(Vec::new()).await;
assert!(
registry.test_get_local("peer-node-1").await.is_some(),
registry.get_node_local("peer-node-1").await.is_some(),
"DNS disappearance must not evict a real node entry that replaced the transient DNS one"
);
Ok(())
@ -530,24 +513,22 @@ mod tests {
8080,
"10.0.0.1".to_string(),
)
.with_node_registry(registry.clone());
.with_node_directory(registry.clone());
let mut original =
let original =
NodeInfo::new("peer-node-1".to_string(), "10.0.0.2:50051".to_string()).with_epoch(7);
original.set_discovery_source(NodeDiscoverySource::K8sDns);
disc.sync_verified_peers_to_registry(vec![("10.0.0.2".to_string(), original)])
.await;
let mut restarted =
let restarted =
NodeInfo::new("peer-node-1".to_string(), "10.0.0.9:50051".to_string()).with_epoch(8);
restarted.set_discovery_source(NodeDiscoverySource::K8sDns);
disc.sync_verified_peers_to_registry(vec![("10.0.0.9".to_string(), restarted)])
.await;
let node = registry
.test_get_local("peer-node-1")
.get_node_local("peer-node-1")
.await
.ok_or_else(|| {
crate::Error::NotFound("transient DNS peer should still exist".to_string())

@ -122,15 +122,7 @@ impl LoadBalancer {
}
}
/// Attach a health monitor to filter out unhealthy nodes
#[must_use]
pub fn with_health_monitor<H>(self, monitor: Arc<H>) -> Self
where
H: ClusterHealthRuntime + 'static,
{
self.with_health_runtime(monitor)
}
/// Attach a health runtime to filter out unhealthy nodes.
#[must_use]
pub fn with_health_runtime(mut self, monitor: Arc<dyn ClusterHealthRuntime>) -> Self {
self.health_monitor = Some(monitor);
@ -222,12 +214,6 @@ impl LoadBalancer {
let nodes = self.get_healthy_nodes().await?;
Ok(nodes.into_iter().map(|n| n.node_id).collect())
}
/// Get count of available healthy nodes
pub async fn available_count(&self) -> Result<usize> {
let nodes = self.get_healthy_nodes().await?;
Ok(nodes.len())
}
}
#[cfg(test)]
@ -298,7 +284,7 @@ mod tests {
let registry = make_registry()?;
let monitor = Arc::new(HealthMonitor::new(Arc::clone(&registry), 60));
let _lb =
LoadBalancer::new(registry, LoadBalancingStrategy::Random).with_health_monitor(monitor);
LoadBalancer::new(registry, LoadBalancingStrategy::Random).with_health_runtime(monitor);
Ok(())
}
@ -479,7 +465,7 @@ mod tests {
}
let lb = LoadBalancer::new(Arc::clone(&registry), LoadBalancingStrategy::Random)
.with_health_monitor(monitor);
.with_health_runtime(monitor);
let err = lb
.select_node_by_id("node-1")
@ -522,17 +508,6 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn test_available_count() -> Result<()> {
let registry = make_registry()?;
register_nodes(&registry, 4).await;
let lb = LoadBalancer::new(Arc::clone(&registry), LoadBalancingStrategy::Random);
let count = lb.available_count().await?;
assert_eq!(count, 4);
Ok(())
}
#[tokio::test]
async fn test_select_node_uses_routable_nodes_in_degraded_mode_when_fresh_enough() -> Result<()>
{
@ -583,7 +558,7 @@ mod tests {
}
let lb = LoadBalancer::new(Arc::clone(&registry), LoadBalancingStrategy::RoundRobin)
.with_health_monitor(Arc::clone(&monitor));
.with_health_runtime(monitor.clone());
// Should never select node-1
let mut selected: HashSet<String> = HashSet::new();
@ -614,7 +589,7 @@ mod tests {
}
let lb = LoadBalancer::new(Arc::clone(&registry), LoadBalancingStrategy::Random)
.with_health_monitor(monitor);
.with_health_runtime(monitor);
let node = lb.select_node().await;
assert!(
@ -638,7 +613,7 @@ mod tests {
monitor.test_set_last_successful_refresh_at(unix_now_secs_for_test()?.saturating_sub(2));
let lb = LoadBalancer::new(Arc::clone(&registry), LoadBalancingStrategy::Random)
.with_health_monitor(monitor);
.with_health_runtime(monitor);
let err = lb
.select_node()
@ -658,7 +633,7 @@ mod tests {
let monitor = Arc::new(HealthMonitor::new(Arc::clone(&registry), 60));
let lb = LoadBalancer::new(Arc::clone(&registry), LoadBalancingStrategy::Random)
.with_health_monitor(monitor);
.with_health_runtime(monitor);
let selected = lb.select_node().await?;
assert!(
@ -682,7 +657,7 @@ mod tests {
}
let lb = LoadBalancer::new(Arc::clone(&registry), LoadBalancingStrategy::RoundRobin)
.with_health_monitor(monitor);
.with_health_runtime(monitor);
let mut selected: HashSet<String> = HashSet::new();
for _ in 0..20 {

@ -16,8 +16,8 @@ pub use health_monitor::{HealthMonitor, NodeHealth};
pub use k8s_dns::{K8sDnsDiscovery, K8sDnsDiscoveryOptions};
pub use load_balancer::{LoadBalancer, LoadBalancingStrategy};
pub use node_registry::{
ClusterMode, HeartbeatResult, LocalClusterNodeDirectoryFactory, NodeInfo, NodeRegistry,
NodeViewMode, RedisClusterNodeDirectoryFactory,
ClusterMode, HeartbeatResult, NodeInfo, NodeRegistry, NodeViewMode,
RedisClusterNodeDirectoryFactory,
};
pub use runtime::{ClusterHealthRuntime, ClusterNodeDirectory, ClusterNodeDirectoryFactory};
pub use static_discovery::{
@ -37,11 +37,6 @@ pub fn build_cluster_node_directory_factory(
Arc::new(RedisClusterNodeDirectoryFactory::new(runtime))
}
#[must_use]
pub fn build_local_cluster_node_directory_factory() -> Arc<dyn ClusterNodeDirectoryFactory> {
Arc::new(LocalClusterNodeDirectoryFactory)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProbedNodeIdentity {
pub node_id: String,

@ -210,20 +210,6 @@ fn duration_millis_u64(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeDiscoverySource {
K8sDns,
}
impl NodeDiscoverySource {
#[must_use]
const fn metadata_value(self) -> &'static str {
match self {
Self::K8sDns => NODE_DISCOVERY_SOURCE_K8S_DNS,
}
}
}
/// Node information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfo {
@ -256,24 +242,18 @@ impl NodeInfo {
self
}
#[must_use]
pub fn with_discovery_source(mut self, source: NodeDiscoverySource) -> Self {
self.set_discovery_source(source);
self
}
pub fn set_discovery_source(&mut self, source: NodeDiscoverySource) {
pub fn mark_k8s_dns_discovered(&mut self) {
self.metadata.insert(
NODE_METADATA_DISCOVERY_KEY.to_string(),
source.metadata_value().to_string(),
NODE_DISCOVERY_SOURCE_K8S_DNS.to_string(),
);
}
#[must_use]
pub fn has_discovery_source(&self, source: NodeDiscoverySource) -> bool {
pub fn is_k8s_dns_discovered(&self) -> bool {
self.metadata
.get(NODE_METADATA_DISCOVERY_KEY)
.is_some_and(|value| value == source.metadata_value())
.is_some_and(|value| value == NODE_DISCOVERY_SOURCE_K8S_DNS)
}
/// Check if node is stale (no recent heartbeat)
@ -451,24 +431,6 @@ impl ClusterNodeDirectoryFactory for RedisClusterNodeDirectoryFactory {
}
}
#[derive(Clone, Default)]
pub struct LocalClusterNodeDirectoryFactory;
impl ClusterNodeDirectoryFactory for LocalClusterNodeDirectoryFactory {
fn build(
&self,
node_id: String,
heartbeat_timeout_secs: i64,
key_prefix: &str,
) -> Result<Arc<dyn ClusterNodeDirectory>> {
Ok(Arc::new(NodeRegistry::new_local_only(
node_id,
heartbeat_timeout_secs,
key_prefix,
)?))
}
}
impl NodeRegistry {
async fn invalidate_node_view_cache(&self) {
self.nodes_cache.invalidate(&()).await;
@ -493,7 +455,7 @@ impl NodeRegistry {
nodes
.into_iter()
.filter(|node| !node.has_discovery_source(NodeDiscoverySource::K8sDns))
.filter(|node| !node.is_k8s_dns_discovered())
.collect()
}
@ -510,7 +472,7 @@ impl NodeRegistry {
for node in local_nodes.values() {
if !node.is_stale(self.heartbeat_timeout_secs)
&& node.has_discovery_source(NodeDiscoverySource::K8sDns)
&& node.is_k8s_dns_discovered()
&& !redis_node_ids.contains(&node.node_id)
{
merged_nodes.push(node.clone());
@ -1597,18 +1559,13 @@ impl NodeRegistry {
/// Remove a transient discovery-only node from the local cache.
///
/// This only removes the entry when it is still marked with the expected
/// discovery source, preventing DNS disappearance from evicting a real node
/// record that has since been refreshed from Redis.
pub async fn remove_discovered_local_node(
&self,
node_id: &str,
discovery_source: NodeDiscoverySource,
) -> bool {
/// This only removes entries still marked as K8s DNS discoveries, preventing
/// DNS disappearance from evicting a node refreshed from Redis.
pub async fn remove_discovered_local_node(&self, node_id: &str) -> bool {
let mut nodes = self.local_nodes.write().await;
let should_remove = nodes
.get(node_id)
.is_some_and(|node| node.has_discovery_source(discovery_source));
.is_some_and(NodeInfo::is_k8s_dns_discovered);
if should_remove {
nodes.remove(node_id);
@ -1620,18 +1577,13 @@ impl NodeRegistry {
/// Upsert a transient discovery-only node into the local cache.
///
/// Entries from the same discovery source are replaced so refreshed peer
/// metadata becomes visible immediately. Entries owned by another source are
/// left untouched to avoid transient discovery data clobbering Redis-backed
/// state.
pub async fn upsert_discovered_local_node(
&self,
node_info: NodeInfo,
discovery_source: NodeDiscoverySource,
) {
/// Existing K8s DNS entries are replaced so refreshed peer metadata becomes
/// visible immediately. Redis-backed entries remain untouched.
pub async fn upsert_discovered_local_node(&self, mut node_info: NodeInfo) {
node_info.mark_k8s_dns_discovered();
let mut nodes = self.local_nodes.write().await;
match nodes.get_mut(&node_info.node_id) {
Some(existing) if existing.has_discovery_source(discovery_source) => {
Some(existing) if existing.is_k8s_dns_discovered() => {
*existing = node_info;
}
Some(_) => {}
@ -1762,18 +1714,6 @@ impl NodeRegistry {
nodes.remove(node_id);
}
/// Alias for [`get_all_nodes_local`] in test context.
#[doc(hidden)]
pub async fn test_get_all_local(&self) -> Vec<NodeInfo> {
self.get_all_nodes_local().await
}
/// Alias for [`get_node_local`] in test context.
#[doc(hidden)]
pub async fn test_get_local(&self, node_id: &str) -> Option<NodeInfo> {
self.get_node_local(node_id).await
}
/// Test-only hook to override the cluster mode.
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
@ -1863,13 +1803,6 @@ impl NodeRegistry {
std::time::Duration::from_millis(self.reregister_backoff_ms.load(Ordering::Relaxed))
}
/// Get the timestamp of the last re-registration attempt (for tests).
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
pub fn last_reregister_attempt(&self) -> u64 {
self.last_reregister_attempt.load(Ordering::Relaxed)
}
/// Set a specific backoff duration for testing purposes.
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
@ -1917,20 +1850,12 @@ impl ClusterNodeDirectory for NodeRegistry {
Self::update_local_metadata(self, key, value).await;
}
async fn upsert_discovered_local_node(
&self,
node_info: NodeInfo,
discovery_source: NodeDiscoverySource,
) {
Self::upsert_discovered_local_node(self, node_info, discovery_source).await;
async fn upsert_discovered_local_node(&self, node_info: NodeInfo) {
Self::upsert_discovered_local_node(self, node_info).await;
}
async fn remove_discovered_local_node(
&self,
node_id: &str,
discovery_source: NodeDiscoverySource,
) -> bool {
Self::remove_discovered_local_node(self, node_id, discovery_source).await
async fn remove_discovered_local_node(&self, node_id: &str) -> bool {
Self::remove_discovered_local_node(self, node_id).await
}
fn heartbeat_timeout_secs(&self) -> i64 {
@ -2127,7 +2052,7 @@ mod tests {
let mut local_nodes = HashMap::new();
let mut dns_peer = NodeInfo::new("dns-peer-1".to_string(), "10.0.0.2:50051".to_string());
dns_peer.set_discovery_source(NodeDiscoverySource::K8sDns);
dns_peer.mark_k8s_dns_discovered();
local_nodes.insert(dns_peer.node_id.clone(), dns_peer);
local_nodes.insert(
@ -2157,7 +2082,7 @@ mod tests {
let redis_peer = NodeInfo::new("redis-peer-1".to_string(), "10.0.0.10:50051".to_string());
let mut dns_peer = NodeInfo::new("dns-peer-1".to_string(), "10.0.0.2:50051".to_string());
dns_peer.set_discovery_source(NodeDiscoverySource::K8sDns);
dns_peer.mark_k8s_dns_discovered();
registry
.nodes_cache

@ -4,9 +4,7 @@ use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use super::health_monitor::NodeHealth;
use super::node_registry::{
ClusterMode, HeartbeatResult, NodeDiscoverySource, NodeInfo, NodeViewMode,
};
use super::node_registry::{ClusterMode, HeartbeatResult, NodeInfo, NodeViewMode};
use crate::error::Result;
#[async_trait]
@ -19,16 +17,8 @@ pub trait ClusterNodeDirectory: Send + Sync {
async fn get_all_nodes(&self) -> Result<Vec<NodeInfo>>;
async fn get_routable_nodes(&self) -> Result<(Vec<NodeInfo>, NodeViewMode)>;
async fn update_local_metadata(&self, key: &str, value: String);
async fn upsert_discovered_local_node(
&self,
node_info: NodeInfo,
discovery_source: NodeDiscoverySource,
);
async fn remove_discovered_local_node(
&self,
node_id: &str,
discovery_source: NodeDiscoverySource,
) -> bool;
async fn upsert_discovered_local_node(&self, node_info: NodeInfo);
async fn remove_discovered_local_node(&self, node_id: &str) -> bool;
fn heartbeat_timeout_secs(&self) -> i64;
fn cluster_mode(&self) -> ClusterMode;
fn cancel_token(&self) -> CancellationToken;

@ -127,18 +127,6 @@ pub struct StaticDiscovery {
}
impl StaticDiscovery {
/// Create a new StaticDiscovery
pub fn new<N>(
config: StaticDiscoveryConfig,
node_registry: Arc<N>,
cancel_token: CancellationToken,
) -> Self
where
N: ClusterNodeDirectory + 'static,
{
Self::from_runtime(config, node_registry, cancel_token)
}
pub fn from_runtime(
config: StaticDiscoveryConfig,
node_registry: Arc<dyn ClusterNodeDirectory>,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save