diff --git a/.sqlx/query-0194202f1e08d10cc50aaa92568bb9bcbb219b722e4570198fd9b75d3adc9a85.json b/.sqlx/query-0194202f1e08d10cc50aaa92568bb9bcbb219b722e4570198fd9b75d3adc9a85.json deleted file mode 100644 index f7b1385f..00000000 --- a/.sqlx/query-0194202f1e08d10cc50aaa92568bb9bcbb219b722e4570198fd9b75d3adc9a85.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-01f9147da87665afe8c31aceec7c18b82f77a1d474ca1d043068383001181eb3.json b/.sqlx/query-01f9147da87665afe8c31aceec7c18b82f77a1d474ca1d043068383001181eb3.json deleted file mode 100644 index 6ccb0129..00000000 --- a/.sqlx/query-01f9147da87665afe8c31aceec7c18b82f77a1d474ca1d043068383001181eb3.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-0ad1b26b4dce7572be0a7b50500cecbd1a4f24adfa38ecb6df81e5b925a80e56.json b/.sqlx/query-0ad1b26b4dce7572be0a7b50500cecbd1a4f24adfa38ecb6df81e5b925a80e56.json deleted file mode 100644 index b1094391..00000000 --- a/.sqlx/query-0ad1b26b4dce7572be0a7b50500cecbd1a4f24adfa38ecb6df81e5b925a80e56.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-0c9323dd554bcd5334ced885030c67831780004bc9d60a9a1562a29dd059f2ba.json b/.sqlx/query-0c9323dd554bcd5334ced885030c67831780004bc9d60a9a1562a29dd059f2ba.json deleted file mode 100644 index f7b9c4bc..00000000 --- a/.sqlx/query-0c9323dd554bcd5334ced885030c67831780004bc9d60a9a1562a29dd059f2ba.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-0cada6649ced39553d548fd9dffe836e3dc9c7ce2e9ad76afd3c27637c4b840d.json b/.sqlx/query-0cada6649ced39553d548fd9dffe836e3dc9c7ce2e9ad76afd3c27637c4b840d.json deleted file mode 100644 index 47d968d5..00000000 --- a/.sqlx/query-0cada6649ced39553d548fd9dffe836e3dc9c7ce2e9ad76afd3c27637c4b840d.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-2c795c623c32965a20e3e24e2735229964e00a93eb6765d9e177160a38a57700.json b/.sqlx/query-2c795c623c32965a20e3e24e2735229964e00a93eb6765d9e177160a38a57700.json deleted file mode 100644 index 844dedfb..00000000 --- a/.sqlx/query-2c795c623c32965a20e3e24e2735229964e00a93eb6765d9e177160a38a57700.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-2e0be9d959744d54c074ea55d34740cadd1de09b4c6579eee13344bae2a7baa8.json b/.sqlx/query-2e0be9d959744d54c074ea55d34740cadd1de09b4c6579eee13344bae2a7baa8.json deleted file mode 100644 index b25bfeac..00000000 --- a/.sqlx/query-2e0be9d959744d54c074ea55d34740cadd1de09b4c6579eee13344bae2a7baa8.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-343f8c888ca3221de7ad8d23b3788ce90d7c49a97370335d617e45c5f6b09c16.json b/.sqlx/query-343f8c888ca3221de7ad8d23b3788ce90d7c49a97370335d617e45c5f6b09c16.json deleted file mode 100644 index c4264958..00000000 --- a/.sqlx/query-343f8c888ca3221de7ad8d23b3788ce90d7c49a97370335d617e45c5f6b09c16.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-35c1b59f2c0ce328e34191c214842a818135f51cd20a94959f85077b3b43793e.json b/.sqlx/query-35c1b59f2c0ce328e34191c214842a818135f51cd20a94959f85077b3b43793e.json deleted file mode 100644 index f0625bfd..00000000 --- a/.sqlx/query-35c1b59f2c0ce328e34191c214842a818135f51cd20a94959f85077b3b43793e.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-401132bddf01263a4f25442e6cc9c829bedfd65b7018d84d1d5921c553a062ed.json b/.sqlx/query-401132bddf01263a4f25442e6cc9c829bedfd65b7018d84d1d5921c553a062ed.json deleted file mode 100644 index 82cd69be..00000000 --- a/.sqlx/query-401132bddf01263a4f25442e6cc9c829bedfd65b7018d84d1d5921c553a062ed.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-4259c0ae2ac799413e94e3de941de517edafa6f59ea6dfdbb88f9fc0a67810d6.json b/.sqlx/query-4259c0ae2ac799413e94e3de941de517edafa6f59ea6dfdbb88f9fc0a67810d6.json deleted file mode 100644 index 149bfe7f..00000000 --- a/.sqlx/query-4259c0ae2ac799413e94e3de941de517edafa6f59ea6dfdbb88f9fc0a67810d6.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-435a2a76e85d82bcb900c5d4f8577d0b2751e042fcf3c7ad99bc7c3f2067cd27.json b/.sqlx/query-435a2a76e85d82bcb900c5d4f8577d0b2751e042fcf3c7ad99bc7c3f2067cd27.json deleted file mode 100644 index 77b03d1a..00000000 --- a/.sqlx/query-435a2a76e85d82bcb900c5d4f8577d0b2751e042fcf3c7ad99bc7c3f2067cd27.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-4c4bffb1f635495a48fb09c8b3a77344e6cffbfb03f308aff7e80fd6b30c5871.json b/.sqlx/query-4c4bffb1f635495a48fb09c8b3a77344e6cffbfb03f308aff7e80fd6b30c5871.json deleted file mode 100644 index dd52b682..00000000 --- a/.sqlx/query-4c4bffb1f635495a48fb09c8b3a77344e6cffbfb03f308aff7e80fd6b30c5871.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-592f2cbef4f1008c4ccacb87b858206df9fa7679d90a5a23d364d89ece148018.json b/.sqlx/query-592f2cbef4f1008c4ccacb87b858206df9fa7679d90a5a23d364d89ece148018.json deleted file mode 100644 index 4402ee9b..00000000 --- a/.sqlx/query-592f2cbef4f1008c4ccacb87b858206df9fa7679d90a5a23d364d89ece148018.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-69fad7386e59ebd38925b6d3b203beaf3f1051f76eea07e39838ebd9f45c0b54.json b/.sqlx/query-69fad7386e59ebd38925b6d3b203beaf3f1051f76eea07e39838ebd9f45c0b54.json deleted file mode 100644 index 492ca804..00000000 --- a/.sqlx/query-69fad7386e59ebd38925b6d3b203beaf3f1051f76eea07e39838ebd9f45c0b54.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-751fc615b72a3955199302aa5b95fb595b4012ba03c6c73cf10b36f877ea319c.json b/.sqlx/query-751fc615b72a3955199302aa5b95fb595b4012ba03c6c73cf10b36f877ea319c.json deleted file mode 100644 index 93b2b441..00000000 --- a/.sqlx/query-751fc615b72a3955199302aa5b95fb595b4012ba03c6c73cf10b36f877ea319c.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-7c0b1b7db07b030676a1511cd407f6ebbe7d09134b1da01d92248c9ec7add6df.json b/.sqlx/query-7c0b1b7db07b030676a1511cd407f6ebbe7d09134b1da01d92248c9ec7add6df.json deleted file mode 100644 index e596479e..00000000 --- a/.sqlx/query-7c0b1b7db07b030676a1511cd407f6ebbe7d09134b1da01d92248c9ec7add6df.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-8817c0089bac303ddfd78872b339e0fc68e2de71ca96ecb273040095bb0bf533.json b/.sqlx/query-8817c0089bac303ddfd78872b339e0fc68e2de71ca96ecb273040095bb0bf533.json deleted file mode 100644 index 54d514cc..00000000 --- a/.sqlx/query-8817c0089bac303ddfd78872b339e0fc68e2de71ca96ecb273040095bb0bf533.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-92ed8a87ed143c1b97cd92833f15e74de76d4a427b9f705cd37163ac36e3c053.json b/.sqlx/query-98eb7d9d0e0ddd562e9000c3bb2e49a78edd4b978369f63d8ddf73b9089d8cef.json similarity index 78% rename from .sqlx/query-92ed8a87ed143c1b97cd92833f15e74de76d4a427b9f705cd37163ac36e3c053.json rename to .sqlx/query-98eb7d9d0e0ddd562e9000c3bb2e49a78edd4b978369f63d8ddf73b9089d8cef.json index 31d701cf..ac7b96cc 100644 --- a/.sqlx/query-92ed8a87ed143c1b97cd92833f15e74de76d4a427b9f705cd37163ac36e3c053.json +++ b/.sqlx/query-98eb7d9d0e0ddd562e9000c3bb2e49a78edd4b978369f63d8ddf73b9089d8cef.json @@ -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" } diff --git a/.sqlx/query-9bd776773d8ee6c829213755809ce2426028cf5a9ba451dd9f4f7e2c84e99932.json b/.sqlx/query-9bd776773d8ee6c829213755809ce2426028cf5a9ba451dd9f4f7e2c84e99932.json deleted file mode 100644 index 398fe769..00000000 --- a/.sqlx/query-9bd776773d8ee6c829213755809ce2426028cf5a9ba451dd9f4f7e2c84e99932.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-b2099f83bae2b385165495f3e2db84b2b542e0410dc68d2dd46d59a5f640c9ed.json b/.sqlx/query-b2099f83bae2b385165495f3e2db84b2b542e0410dc68d2dd46d59a5f640c9ed.json deleted file mode 100644 index f2c439d0..00000000 --- a/.sqlx/query-b2099f83bae2b385165495f3e2db84b2b542e0410dc68d2dd46d59a5f640c9ed.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-c939ee0cc41a3cc6606836e1e31c54756e8d8bfbca9cf4f15c07d9e9ea4dd3a7.json b/.sqlx/query-c939ee0cc41a3cc6606836e1e31c54756e8d8bfbca9cf4f15c07d9e9ea4dd3a7.json deleted file mode 100644 index 094903e3..00000000 --- a/.sqlx/query-c939ee0cc41a3cc6606836e1e31c54756e8d8bfbca9cf4f15c07d9e9ea4dd3a7.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-494340f5b7481c98dd7259988cd8d137de881b21298af9c3d9cc4779de922a34.json b/.sqlx/query-eb0c8329b5bae62f9a8aef48f3777fb0b3277d87e457049d6ccaf31edfa5413f.json similarity index 86% rename from .sqlx/query-494340f5b7481c98dd7259988cd8d137de881b21298af9c3d9cc4779de922a34.json rename to .sqlx/query-eb0c8329b5bae62f9a8aef48f3777fb0b3277d87e457049d6ccaf31edfa5413f.json index ea173bf0..465de4e6 100644 --- a/.sqlx/query-494340f5b7481c98dd7259988cd8d137de881b21298af9c3d9cc4779de922a34.json +++ b/.sqlx/query-eb0c8329b5bae62f9a8aef48f3777fb0b3277d87e457049d6ccaf31edfa5413f.json @@ -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" } diff --git a/.sqlx/query-ee462ce207bfb46003a330492bf9fa4bf0527c60f9d6a4ac376d78e06b83b0c7.json b/.sqlx/query-ee462ce207bfb46003a330492bf9fa4bf0527c60f9d6a4ac376d78e06b83b0c7.json deleted file mode 100644 index a956f312..00000000 --- a/.sqlx/query-ee462ce207bfb46003a330492bf9fa4bf0527c60f9d6a4ac376d78e06b83b0c7.json +++ /dev/null @@ -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" -} diff --git a/Cargo.lock b/Cargo.lock index 0d7b174a..0e2c37d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 638868c3..09c26bac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/docs/src/content/docs/en/reference/metrics-catalog.mdx b/docs/src/content/docs/en/reference/metrics-catalog.mdx index 9a424075..c05b7cb9 100644 --- a/docs/src/content/docs/en/reference/metrics-catalog.mdx +++ b/docs/src/content/docs/en/reference/metrics-catalog.mdx @@ -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 | diff --git a/docs/src/content/docs/reference/metrics-catalog.mdx b/docs/src/content/docs/reference/metrics-catalog.mdx index 77ddc21e..f96fc9cd 100644 --- a/docs/src/content/docs/reference/metrics-catalog.mdx +++ b/docs/src/content/docs/reference/metrics-catalog.mdx @@ -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 慢客户端断开次数 | diff --git a/synctv-adapter/Cargo.toml b/synctv-adapter/Cargo.toml index 27599cf1..ad29c67e 100644 --- a/synctv-adapter/Cargo.toml +++ b/synctv-adapter/Cargo.toml @@ -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"] } diff --git a/synctv-adapter/src/error.rs b/synctv-adapter/src/error.rs index e2d54cda..b9bd07a8 100644 --- a/synctv-adapter/src/error.rs +++ b/synctv-adapter/src/error.rs @@ -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; } diff --git a/synctv-adapter/src/public_id.rs b/synctv-adapter/src/public_id.rs index 98df30c5..50f334af 100644 --- a/synctv-adapter/src/public_id.rs +++ b/synctv-adapter/src/public_id.rs @@ -229,12 +229,6 @@ impl PublicIdCodec { }) } - #[cfg(test)] - #[must_use] - pub fn default_for_tests() -> Self { - Self::plain() - } - pub fn encode(&self, id: T) -> Result 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::(1)), "review request ID should encode", diff --git a/synctv-api-common/src/api_runtime.rs b/synctv-api-common/src/api_runtime.rs index 0bb1843f..b4edfa33 100644 --- a/synctv-api-common/src/api_runtime.rs +++ b/synctv-api-common/src/api_runtime.rs @@ -349,8 +349,8 @@ pub struct SharedApiRuntime { pub proxy_signing_key: Arc, pub media_swarm_signing_key: Arc, pub webrtc_status: synctv_core::service::WebRtcRuntimeStatus, - pub server_state_runtime: Arc, - pub slice_cache_management_runtime: Arc, + pub server_state_runtime: Arc, + pub slice_cache_management_runtime: Arc, } #[cfg(test)] diff --git a/synctv-api-common/src/app_state.rs b/synctv-api-common/src/app_state.rs index c35129f0..26e8b538 100644 --- a/synctv-api-common/src/app_state.rs +++ b/synctv-api-common/src/app_state.rs @@ -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 { - build_app_state(options) -} - /// Build `AppState` from `RouterOptions`, creating the shared API implementation layers. pub fn build_app_state(options: RouterOptions) -> anyhow::Result { let shared_api_runtime = Arc::new(build_shared_api_runtime(&options)?); diff --git a/synctv-api-common/src/chat_event_dispatcher.rs b/synctv-api-common/src/chat_event_dispatcher.rs index 0d6b4147..c204a939 100644 --- a/synctv-api-common/src/chat_event_dispatcher.rs +++ b/synctv-api-common/src/chat_event_dispatcher.rs @@ -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::*; diff --git a/synctv-api-common/src/http_error.rs b/synctv-api-common/src/http_error.rs index c37b1e97..c94cc118 100644 --- a/synctv-api-common/src/http_error.rs +++ b/synctv-api-common/src/http_error.rs @@ -38,14 +38,6 @@ impl AppError { Self::new(StatusCode::FORBIDDEN, message) } - pub fn too_many_requests(message: impl Into) -> Self { - Self::new(StatusCode::TOO_MANY_REQUESTS, message) - } - - pub fn payload_too_large(message: impl Into) -> Self { - Self::new(StatusCode::PAYLOAD_TOO_LARGE, message) - } - pub fn too_many_requests_with_retry(message: impl Into, 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 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(); diff --git a/synctv-api-common/src/impls/admin/auth.rs b/synctv-api-common/src/impls/admin/auth.rs index 430166e2..b50dbe72 100644 --- a/synctv-api-common/src/impls/admin/auth.rs +++ b/synctv-api-common/src/impls/admin/auth.rs @@ -219,12 +219,7 @@ impl AdminApiImpl { admin_user_id: &UserId, ) -> Result { 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) } } diff --git a/synctv-api-common/src/impls/admin/playback.rs b/synctv-api-common/src/impls/admin/playback.rs index bc88850e..5c2bd681 100644 --- a/synctv-api-common/src/impls/admin/playback.rs +++ b/synctv-api-common/src/impls/admin/playback.rs @@ -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(); diff --git a/synctv-api-common/src/impls/admin/rooms.rs b/synctv-api-common/src/impls/admin/rooms.rs index 9c838813..723c762b 100644 --- a/synctv-api-common/src/impls/admin/rooms.rs +++ b/synctv-api-common/src/impls/admin/rooms.rs @@ -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 { diff --git a/synctv-api-common/src/impls/admin/settings.rs b/synctv-api-common/src/impls/admin/settings.rs index 8876507f..c6e71f6e 100644 --- a/synctv-api-common/src/impls/admin/settings.rs +++ b/synctv-api-common/src/impls/admin/settings.rs @@ -1086,13 +1086,6 @@ impl AdminApiImpl { } } - pub async fn send_test_email( - &self, - req: synctv_proto::admin::SendTestEmailRequest, - ) -> Result { - self.send_test_email_with_control(req, None).await - } - pub async fn send_test_email_with_control( &self, req: synctv_proto::admin::SendTestEmailRequest, diff --git a/synctv-api-common/src/impls/admin/tests.rs b/synctv-api-common/src/impls/admin/tests.rs index 0be816ae..65e18974 100644 --- a/synctv-api-common/src/impls/admin/tests.rs +++ b/synctv-api-common/src/impls/admin/tests.rs @@ -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 = anyhow::Result; @@ -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; diff --git a/synctv-api-common/src/impls/client/auth.rs b/synctv-api-common/src/impls/client/auth.rs index 833762fb..051951b2 100644 --- a/synctv-api-common/src/impls/client/auth.rs +++ b/synctv-api-common/src/impls/client/auth.rs @@ -182,31 +182,6 @@ fn nonnegative_token_ttl_seconds(exp: i64, now: i64) -> Result { }) } -/// 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 { - 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 { + 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(()) } } diff --git a/synctv-api-common/src/impls/client/convert.rs b/synctv-api-common/src/impls/client/convert.rs index dc0d1386..f71fcdf3 100644 --- a/synctv-api-common/src/impls/client/convert.rs +++ b/synctv-api-common/src/impls/client/convert.rs @@ -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, diff --git a/synctv-api-common/src/impls/client/file_download.rs b/synctv-api-common/src/impls/client/file_download.rs index 7df1f318..8e95d80b 100644 --- a/synctv-api-common/src/impls/client/file_download.rs +++ b/synctv-api-common/src/impls/client/file_download.rs @@ -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 { - 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( @@ -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; diff --git a/synctv-api-common/src/impls/client/media.rs b/synctv-api-common/src/impls/client/media.rs index 7c2525dc..c73588dc 100644 --- a/synctv-api-common/src/impls/client/media.rs +++ b/synctv-api-common/src/impls/client/media.rs @@ -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, -) -> Vec { - 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, field: &'static str, ) -> Result { 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 { - 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 { - self.get_media_for_actor(&RoomActor::Guest(access.clone()), media_id) - .await - } - pub async fn get_media_for_actor( &self, actor: &RoomActor, diff --git a/synctv-api-common/src/impls/client/member.rs b/synctv-api-common/src/impls/client/member.rs index f67adb55..96f1d75a 100644 --- a/synctv-api-common/src/impls/client/member.rs +++ b/synctv-api-common/src/impls/client/member.rs @@ -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 { - 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 diff --git a/synctv-api-common/src/impls/client/mod.rs b/synctv-api-common/src/impls/client/mod.rs index e4f4ee8a..ce7381d4 100644 --- a/synctv-api-common/src/impls/client/mod.rs +++ b/synctv-api-common/src/impls/client/mod.rs @@ -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 { - 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 { - 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> - where - T: Send + 'a, - E: Into + Send + 'a, - F: FnOnce(ApiRequestContext) -> Fut + Send + 'a, - Fut: std::future::Future> + 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> - where - T: Send + 'a, - E: Into + Send + 'a, - F: FnOnce(synctv_core::provider::ExecutionControl) -> Fut + Send + 'a, - Fut: std::future::Future> + 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> - where - T: Send + 'a, - E: Into + Send + 'a, - F: FnOnce(ApiRequestContext, synctv_core::service::AuthenticatedToken) -> Fut + Send + 'a, - Fut: std::future::Future> + 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, diff --git a/synctv-api-common/src/impls/client/playback.rs b/synctv-api-common/src/impls/client/playback.rs index 9b1fb008..84004143 100644 --- a/synctv-api-common/src/impls/client/playback.rs +++ b/synctv-api-common/src/impls/client/playback.rs @@ -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 { - 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, diff --git a/synctv-api-common/src/impls/client/playlist.rs b/synctv-api-common/src/impls/client/playlist.rs index fac983df..971ef195 100644 --- a/synctv-api-common/src/impls/client/playlist.rs +++ b/synctv-api-common/src/impls/client/playlist.rs @@ -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 { - 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 { - self.list_playlists_for_actor(&RoomActor::Guest(access.clone()), req) - .await - } - pub async fn list_playlists_for_actor( &self, actor: &RoomActor, diff --git a/synctv-api-common/src/impls/client/room.rs b/synctv-api-common/src/impls/client/room.rs index c4c885b5..8734434e 100644 --- a/synctv-api-common/src/impls/client/room.rs +++ b/synctv-api-common/src/impls/client/room.rs @@ -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 = - rooms.iter().map(|(room, _, _, _)| room.id).collect(); + rooms.iter().map(|(room, _, _)| room.id).collect(); let room_models: Vec = - 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::>(); 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 { - 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 { - 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 { - 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 { - 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(), diff --git a/synctv-api-common/src/impls/client/stream.rs b/synctv-api-common/src/impls/client/stream.rs index c2eb412a..5e0d1819 100644 --- a/synctv-api-common/src/impls/client/stream.rs +++ b/synctv-api-common/src/impls/client/stream.rs @@ -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?; diff --git a/synctv-api-common/src/impls/client/tests.rs b/synctv-api-common/src/impls/client/tests.rs index 3fcc5ed5..2787f040 100644 --- a/synctv-api-common/src/impls/client/tests.rs +++ b/synctv-api-common/src/impls/client/tests.rs @@ -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, 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 = anyhow::Result; -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) -> 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, } } diff --git a/synctv-api-common/src/impls/client/user.rs b/synctv-api-common/src/impls/client/user.rs index cb3b605d..7daab5f8 100644 --- a/synctv-api-common/src/impls/client/user.rs +++ b/synctv-api-common/src/impls/client/user.rs @@ -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 { - claims.auth_context() -} - fn sensitive_challenge_to_proto( challenge: SensitiveVerificationChallenge, ) -> Result { @@ -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, diff --git a/synctv-api-common/src/impls/email.rs b/synctv-api-common/src/impls/email.rs index 1a576f8f..b4fa2e10 100644 --- a/synctv-api-common/src/impls/email.rs +++ b/synctv-api-common/src/impls/email.rs @@ -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, @@ -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 { @@ -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 { - 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 { - 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, diff --git a/synctv-api-common/src/impls/messaging.rs b/synctv-api-common/src/impls/messaging.rs index a58d9320..e07d3937 100644 --- a/synctv-api-common/src/impls/messaging.rs +++ b/synctv-api-common/src/impls/messaging.rs @@ -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 { 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 { - self.principal.user_id() - } } mod event_messages; diff --git a/synctv-api-common/src/impls/messaging/codec.rs b/synctv-api-common/src/impls/messaging/codec.rs index 80b2398d..c405e23d 100644 --- a/synctv-api-common/src/impls/messaging/codec.rs +++ b/synctv-api-common/src/impls/messaging/codec.rs @@ -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, String> { - Ok(msg.encode_to_vec()) - } - /// Decode `ClientMessage` from binary pub fn decode_client_message(data: &[u8]) -> Result { ClientMessage::decode(data).map_err(|e| format!("Failed to decode message: {e}")) diff --git a/synctv-api-common/src/impls/messaging/heartbeat.rs b/synctv-api-common/src/impls/messaging/heartbeat.rs index 591c69c2..a6dbff5a 100644 --- a/synctv-api-common/src/impls/messaging/heartbeat.rs +++ b/synctv-api-common/src/impls/messaging/heartbeat.rs @@ -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}; diff --git a/synctv-api-common/src/impls/messaging/tests.rs b/synctv-api-common/src/impls/messaging/tests.rs index 0392263d..c21feed0 100644 --- a/synctv-api-common/src/impls/messaging/tests.rs +++ b/synctv-api-common/src/impls/messaging/tests.rs @@ -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(), diff --git a/synctv-api-common/src/impls/mod.rs b/synctv-api-common/src/impls/mod.rs index 7b79f010..230765e0 100644 --- a/synctv-api-common/src/impls/mod.rs +++ b/synctv-api-common/src/impls/mod.rs @@ -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, -) -> Result { - 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, - public_id_codec: &synctv_adapter::PublicIdCodec, -) -> Result, ApiError> { - proto_validated_optional_id(value, public_id_codec) -} - pub fn proto_validated_media_ids( values: Vec, 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, 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, ApiError> { - parse_optional_id_param(value, field, public_id_codec) -} - pub fn parse_optional_id_param( value: &str, field: &'static str, diff --git a/synctv-api-common/src/impls/oauth2.rs b/synctv-api-common/src/impls/oauth2.rs index c015514e..80b634e7 100644 --- a/synctv-api-common/src/impls/oauth2.rs +++ b/synctv-api-common/src/impls/oauth2.rs @@ -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, - ) -> 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, - 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, - ) -> 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, - 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, - ) -> Result { - 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, diff --git a/synctv-api-common/src/impls/request_context.rs b/synctv-api-common/src/impls/request_context.rs index f9ff8ed6..8f602575 100644 --- a/synctv-api-common/src/impls/request_context.rs +++ b/synctv-api-common/src/impls/request_context.rs @@ -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 { - match self { - Self::Authenticated(authenticated) => Some(*authenticated), - Self::Missing | Self::Failed(_) => None, - } - } - fn into_required(self) -> Result { 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 { - 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, 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, 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> - where - T: Send + 'a, - F: FnOnce(Option) -> Fut + Send + 'a, - Fut: Future> + 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> - where - T: Send + 'a, - F: FnOnce(Option) -> Fut + Send + 'a, - Fut: Future> + 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> - where - T: Send + 'a, - F: FnOnce(ApiRequestContext, Option) -> Fut + Send + 'a, - Fut: Future> + 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> - where - T: Send + 'a, - F: FnOnce(ExecutionControl, Option) -> Fut + Send + 'a, - Fut: Future> + 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, diff --git a/synctv-api-common/src/impls/validation.rs b/synctv-api-common/src/impls/validation.rs index 8f9887e0..64489707 100644 --- a/synctv-api-common/src/impls/validation.rs +++ b/synctv-api-common/src/impls/validation.rs @@ -193,12 +193,6 @@ pub fn validate_playback_speed(speed: f64) -> InputValidationResult { 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::*; diff --git a/synctv-api-common/src/lib.rs b/synctv-api-common/src/lib.rs index 5dcfb5a5..8ea30820 100644 --- a/synctv-api-common/src/lib.rs +++ b/synctv-api-common/src/lib.rs @@ -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}; diff --git a/synctv-api-common/src/playback_provider/common.rs b/synctv-api-common/src/playback_provider/common.rs index 1486baad..4eaadb90 100644 --- a/synctv-api-common/src/playback_provider/common.rs +++ b/synctv-api-common/src/playback_provider/common.rs @@ -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, diff --git a/synctv-api-common/src/status.rs b/synctv-api-common/src/status.rs index 974f0a05..c66eef1f 100644 --- a/synctv-api-common/src/status.rs +++ b/synctv-api-common/src/status.rs @@ -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 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 }); - 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 { - 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, connection_manager: Arc, @@ -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 { diff --git a/synctv-api-common/src/status/server_state_cluster.rs b/synctv-api-common/src/status/server_state_cluster.rs index d598db49..2da0f3d7 100644 --- a/synctv-api-common/src/status/server_state_cluster.rs +++ b/synctv-api-common/src/status/server_state_cluster.rs @@ -6,13 +6,13 @@ use super::*; #[derive(Clone)] pub struct ServerStateGrpcService { - runtime: Arc, + runtime: Arc, auth: synctv_cluster::grpc::ClusterAuthInterceptor, } impl ServerStateGrpcService { #[must_use] - pub fn new(runtime: Arc, cluster_secret: String) -> Self { + pub fn new(runtime: Arc, 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, diff --git a/synctv-api-common/src/status/slice_cache.rs b/synctv-api-common/src/status/slice_cache.rs index b4ef3cb6..5e574199 100644 --- a/synctv-api-common/src/status/slice_cache.rs +++ b/synctv-api-common/src/status/slice_cache.rs @@ -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 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 }); - 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, diff --git a/synctv-api-common/src/test_support.rs b/synctv-api-common/src/test_support.rs index f532e613..0212b604 100644 --- a/synctv-api-common/src/test_support.rs +++ b/synctv-api-common/src/test_support.rs @@ -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, diff --git a/synctv-api-grpc/src/grpc/admin_service.rs b/synctv-api-grpc/src/grpc/admin_service.rs index 15373080..b7faf72a 100644 --- a/synctv-api-grpc/src/grpc/admin_service.rs +++ b/synctv-api-grpc/src/grpc/admin_service.rs @@ -88,7 +88,7 @@ fn map_slice_cache_error( pub struct AdminServiceImpl { admin_api: Arc, runtime_settings: Arc, - slice_cache_runtime: Arc, + slice_cache_runtime: Arc, } impl AdminServiceImpl { @@ -96,7 +96,7 @@ impl AdminServiceImpl { pub const fn new( admin_api: Arc, runtime_settings: Arc, - slice_cache_runtime: Arc, + slice_cache_runtime: Arc, ) -> Self { Self { admin_api, diff --git a/synctv-api-grpc/src/grpc/client_service/user.rs b/synctv-api-grpc/src/grpc/client_service/user.rs index 02d37dda..5facc836 100644 --- a/synctv-api-grpc/src/grpc/client_service/user.rs +++ b/synctv-api-grpc/src/grpc/client_service/user.rs @@ -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 diff --git a/synctv-api-grpc/src/grpc/mod.rs b/synctv-api-grpc/src/grpc/mod.rs index e94c7456..bac7078e 100644 --- a/synctv-api-grpc/src/grpc/mod.rs +++ b/synctv-api-grpc/src/grpc/mod.rs @@ -964,7 +964,7 @@ fn cluster_node_id(event_service: &Arc) -> String { fn build_fallback_http_app_state( deps: FallbackHttpAppStateDeps, ) -> anyhow::Result> { - 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(), diff --git a/synctv-api-http/src/http/auth.rs b/synctv-api-http/src/http/auth.rs index 6b641208..3e45106b 100644 --- a/synctv-api-http/src/http/auth.rs +++ b/synctv-api-http/src/http/auth.rs @@ -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(), })) } diff --git a/synctv-api-http/src/http/health.rs b/synctv-api-http/src/http/health.rs index 01d4c93a..67043948 100644 --- a/synctv-api-http/src/http/health.rs +++ b/synctv-api-http/src/http/health.rs @@ -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) -> 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) -> 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(()) } diff --git a/synctv-api-http/src/http/mod.rs b/synctv-api-http/src/http/mod.rs index 2a4d3060..9aaab3da 100644 --- a/synctv-api-http/src/http/mod.rs +++ b/synctv-api-http/src/http/mod.rs @@ -261,12 +261,11 @@ static X_FORWARDED_PROTO: LazyLock = 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 { - 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 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)) } diff --git a/synctv-api-http/src/http/tests.rs b/synctv-api-http/src/http/tests.rs index 3558981f..ccf86261 100644 --- a/synctv-api-http/src/http/tests.rs +++ b/synctv-api-http/src/http/tests.rs @@ -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( diff --git a/synctv-api-http/src/http/user.rs b/synctv-api-http/src/http/user.rs index 9e684ce5..dd65761a 100644 --- a/synctv-api-http/src/http/user.rs +++ b/synctv-api-http/src/http/user.rs @@ -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 diff --git a/synctv-api-http/src/http/websocket.rs b/synctv-api-http/src/http/websocket.rs index dff406cb..47cca557 100644 --- a/synctv-api-http/src/http/websocket.rs +++ b/synctv-api-http/src/http/websocket.rs @@ -1215,10 +1215,8 @@ async fn prepare_websocket_upgrade( request_meta: &ApiRequestMetadata, handshake_control: &ExecutionControl, ) -> Result { - 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, diff --git a/synctv-api-http/src/http/websocket/tests.rs b/synctv-api-http/src/http/websocket/tests.rs index 71dad16d..b9b79eaf 100644 --- a/synctv-api-http/src/http/websocket/tests.rs +++ b/synctv-api-http/src/http/websocket/tests.rs @@ -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 diff --git a/synctv-api-http/src/lib.rs b/synctv-api-http/src/lib.rs index 274be105..e6b43738 100644 --- a/synctv-api-http/src/lib.rs +++ b/synctv-api-http/src/lib.rs @@ -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, }; diff --git a/synctv-api-http/src/providers/mod.rs b/synctv-api-http/src/providers/mod.rs index a07a24a3..d3a13b9d 100644 --- a/synctv-api-http/src/providers/mod.rs +++ b/synctv-api-http/src/providers/mod.rs @@ -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 { diff --git a/synctv-api/src/lib.rs b/synctv-api/src/lib.rs index 8236db0e..dccb55f1 100644 --- a/synctv-api/src/lib.rs +++ b/synctv-api/src/lib.rs @@ -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, }; diff --git a/synctv-api/tests/chat_permission_integration_tests.rs b/synctv-api/tests/chat_permission_integration_tests.rs index c619864e..0c8e3067 100644 --- a/synctv-api/tests/chat_permission_integration_tests.rs +++ b/synctv-api/tests/chat_permission_integration_tests.rs @@ -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(), diff --git a/synctv-api/tests/client_ip_tests.rs b/synctv-api/tests/client_ip_tests.rs index a81cfccc..3ae730c7 100644 --- a/synctv-api/tests/client_ip_tests.rs +++ b/synctv-api/tests/client_ip_tests.rs @@ -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. diff --git a/synctv-api/tests/client_resource_availability_tests.rs b/synctv-api/tests/client_resource_availability_tests.rs index 4747b156..1bfd914d 100644 --- a/synctv-api/tests/client_resource_availability_tests.rs +++ b/synctv-api/tests/client_resource_availability_tests.rs @@ -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(), diff --git a/synctv-api/tests/dynamic_playback_api_tests.rs b/synctv-api/tests/dynamic_playback_api_tests.rs index fb97b194..60798253 100644 --- a/synctv-api/tests/dynamic_playback_api_tests.rs +++ b/synctv-api/tests/dynamic_playback_api_tests.rs @@ -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 diff --git a/synctv-api/tests/http_integration_test.rs b/synctv-api/tests/http_integration_test.rs index 62995234..b55c92ec 100644 --- a/synctv-api/tests/http_integration_test.rs +++ b/synctv-api/tests/http_integration_test.rs @@ -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] diff --git a/synctv-api/tests/list_my_rooms_integration_tests.rs b/synctv-api/tests/list_my_rooms_integration_tests.rs index 62cae104..52de78a0 100644 --- a/synctv-api/tests/list_my_rooms_integration_tests.rs +++ b/synctv-api/tests/list_my_rooms_integration_tests.rs @@ -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(), diff --git a/synctv-api/tests/member_approval_api_integration_tests.rs b/synctv-api/tests/member_approval_api_integration_tests.rs index 08d56d66..0d91d6d1 100644 --- a/synctv-api/tests/member_approval_api_integration_tests.rs +++ b/synctv-api/tests/member_approval_api_integration_tests.rs @@ -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(), diff --git a/synctv-api/tests/room_join_approval_integration_tests.rs b/synctv-api/tests/room_join_approval_integration_tests.rs index 82cddd4c..234b6b41 100644 --- a/synctv-api/tests/room_join_approval_integration_tests.rs +++ b/synctv-api/tests/room_join_approval_integration_tests.rs @@ -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" diff --git a/synctv-api/tests/room_member_visibility_integration_tests.rs b/synctv-api/tests/room_member_visibility_integration_tests.rs index 5ab72f96..0ee8536a 100644 --- a/synctv-api/tests/room_member_visibility_integration_tests.rs +++ b/synctv-api/tests/room_member_visibility_integration_tests.rs @@ -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(), diff --git a/synctv-api/tests/room_password_rate_limit_integration_tests.rs b/synctv-api/tests/room_password_rate_limit_integration_tests.rs index aed3380a..5200a3b9 100644 --- a/synctv-api/tests/room_password_rate_limit_integration_tests.rs +++ b/synctv-api/tests/room_password_rate_limit_integration_tests.rs @@ -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(), diff --git a/synctv-api/tests/user_room_member_matrix_integration_tests.rs b/synctv-api/tests/user_room_member_matrix_integration_tests.rs index 09d0975f..3077cbdf 100644 --- a/synctv-api/tests/user_room_member_matrix_integration_tests.rs +++ b/synctv-api/tests/user_room_member_matrix_integration_tests.rs @@ -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"); diff --git a/synctv-api/tests/webrtc_integration_test.rs b/synctv-api/tests/webrtc_integration_test.rs index 12c78b8b..fb8e4df5 100644 --- a/synctv-api/tests/webrtc_integration_test.rs +++ b/synctv-api/tests/webrtc_integration_test.rs @@ -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(), diff --git a/synctv-api/tests/websocket_test.rs b/synctv-api/tests/websocket_test.rs index 8834c093..55e3cf30 100644 --- a/synctv-api/tests/websocket_test.rs +++ b/synctv-api/tests/websocket_test.rs @@ -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"); diff --git a/synctv-cluster/src/discovery/health_monitor.rs b/synctv-cluster/src/discovery/health_monitor.rs index 49b58800..5589d7e9 100644 --- a/synctv-cluster/src/discovery/health_monitor.rs +++ b/synctv-cluster/src/discovery/health_monitor.rs @@ -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( - node_registry: Arc, - 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(()) diff --git a/synctv-cluster/src/discovery/k8s_dns.rs b/synctv-cluster/src/discovery/k8s_dns.rs index 4ff3d592..a61f6b78 100644 --- a/synctv-cluster/src/discovery/k8s_dns.rs +++ b/synctv-cluster/src/discovery/k8s_dns.rs @@ -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(self, registry: Arc) -> 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) -> 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()) diff --git a/synctv-cluster/src/discovery/load_balancer.rs b/synctv-cluster/src/discovery/load_balancer.rs index 14eeb9f1..6c85ae66 100644 --- a/synctv-cluster/src/discovery/load_balancer.rs +++ b/synctv-cluster/src/discovery/load_balancer.rs @@ -122,15 +122,7 @@ impl LoadBalancer { } } - /// Attach a health monitor to filter out unhealthy nodes - #[must_use] - pub fn with_health_monitor(self, monitor: Arc) -> 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) -> 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 { - 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(®istry), 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(®istry), 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(®istry, 4).await; - let lb = LoadBalancer::new(Arc::clone(®istry), 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(®istry), LoadBalancingStrategy::RoundRobin) - .with_health_monitor(Arc::clone(&monitor)); + .with_health_runtime(monitor.clone()); // Should never select node-1 let mut selected: HashSet = HashSet::new(); @@ -614,7 +589,7 @@ mod tests { } let lb = LoadBalancer::new(Arc::clone(®istry), 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(®istry), 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(®istry), 60)); let lb = LoadBalancer::new(Arc::clone(®istry), 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(®istry), LoadBalancingStrategy::RoundRobin) - .with_health_monitor(monitor); + .with_health_runtime(monitor); let mut selected: HashSet = HashSet::new(); for _ in 0..20 { diff --git a/synctv-cluster/src/discovery/mod.rs b/synctv-cluster/src/discovery/mod.rs index 83e0d010..f501576c 100644 --- a/synctv-cluster/src/discovery/mod.rs +++ b/synctv-cluster/src/discovery/mod.rs @@ -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 { - Arc::new(LocalClusterNodeDirectoryFactory) -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProbedNodeIdentity { pub node_id: String, diff --git a/synctv-cluster/src/discovery/node_registry.rs b/synctv-cluster/src/discovery/node_registry.rs index ebfccac3..b48e0fe8 100644 --- a/synctv-cluster/src/discovery/node_registry.rs +++ b/synctv-cluster/src/discovery/node_registry.rs @@ -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> { - 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 { - 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 { - 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 diff --git a/synctv-cluster/src/discovery/runtime.rs b/synctv-cluster/src/discovery/runtime.rs index af2cb389..ec8d1aba 100644 --- a/synctv-cluster/src/discovery/runtime.rs +++ b/synctv-cluster/src/discovery/runtime.rs @@ -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>; async fn get_routable_nodes(&self) -> Result<(Vec, 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; diff --git a/synctv-cluster/src/discovery/static_discovery.rs b/synctv-cluster/src/discovery/static_discovery.rs index 258b8859..26b5bc47 100644 --- a/synctv-cluster/src/discovery/static_discovery.rs +++ b/synctv-cluster/src/discovery/static_discovery.rs @@ -127,18 +127,6 @@ pub struct StaticDiscovery { } impl StaticDiscovery { - /// Create a new StaticDiscovery - pub fn new( - config: StaticDiscoveryConfig, - node_registry: Arc, - 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, diff --git a/synctv-cluster/src/grpc/client.rs b/synctv-cluster/src/grpc/client.rs index 7da0f4b9..86ee1fef 100644 --- a/synctv-cluster/src/grpc/client.rs +++ b/synctv-cluster/src/grpc/client.rs @@ -20,13 +20,6 @@ pub struct ClusterClient { } impl ClusterClient { - pub fn new(node_registry: Arc, config: ClusterClientConfig) -> Self - where - N: ClusterNodeDirectory + 'static, - { - Self::from_runtime(node_registry, config) - } - pub const fn from_runtime( node_registry: Arc, config: ClusterClientConfig, @@ -37,20 +30,16 @@ impl ClusterClient { } } - async fn find_routable_node(&self, target_node_id: &str) -> Result { - let (nodes, _view_mode) = self.node_registry.get_routable_nodes().await?; - nodes - .into_iter() - .find(|node| node.node_id == target_node_id) - .ok_or_else(|| Error::Rpc(format!("cluster node '{target_node_id}' is not routable"))) - } - /// Resolve a routable cluster node by ID. /// /// This keeps cluster's responsibility limited to topology discovery. Callers /// use the returned `cluster_address` to invoke their own internal gRPC services. pub async fn resolve_routable_node(&self, target_node_id: &str) -> Result { - self.find_routable_node(target_node_id).await + let (nodes, _view_mode) = self.node_registry.get_routable_nodes().await?; + nodes + .into_iter() + .find(|node| node.node_id == target_node_id) + .ok_or_else(|| Error::Rpc(format!("cluster node '{target_node_id}' is not routable"))) } /// Return all routable remote nodes, excluding this client node. diff --git a/synctv-cluster/src/grpc/server.rs b/synctv-cluster/src/grpc/server.rs index 50de4772..4739d83c 100644 --- a/synctv-cluster/src/grpc/server.rs +++ b/synctv-cluster/src/grpc/server.rs @@ -19,14 +19,6 @@ pub struct ClusterServer { } impl ClusterServer { - #[must_use] - pub fn new(node_registry: Arc) -> Self - where - N: ClusterNodeDirectory + 'static, - { - Self::from_runtime(node_registry) - } - #[must_use] pub fn from_runtime(node_registry: Arc) -> Self { Self { diff --git a/synctv-cluster/src/leader/k8s_lease.rs b/synctv-cluster/src/leader/k8s_lease.rs index 55c735dc..40edfca5 100644 --- a/synctv-cluster/src/leader/k8s_lease.rs +++ b/synctv-cluster/src/leader/k8s_lease.rs @@ -865,12 +865,6 @@ impl K8sLeaderElector { } } - /// Returns the number of consecutive leadership losses. - /// Useful for monitoring and health checks. - pub fn consecutive_losses(&self) -> u64 { - self.consecutive_losses.load(Ordering::Relaxed) - } - /// Returns the number of consecutive election failures. /// Useful for health check endpoints. pub fn consecutive_failures(&self) -> u64 { diff --git a/synctv-cluster/src/leader/mod.rs b/synctv-cluster/src/leader/mod.rs index 118ee773..b6d50d19 100644 --- a/synctv-cluster/src/leader/mod.rs +++ b/synctv-cluster/src/leader/mod.rs @@ -583,28 +583,6 @@ impl Default for LeaderElectorConfig { } impl LeaderElector { - /// Create a new leader elector with default configuration. - /// - /// The `key_prefix` is prepended to the leader election lock key in Redis - /// (e.g. `"synctv:"` produces the lock key `synctv:leader_election`). - /// Pass an empty string to use the unprefixed default key. - /// - /// # Safety Warning: Sentinel Mode - /// - /// When `is_sentinel` is true, a startup warning is logged about the - /// distributed lock vulnerability during Sentinel failover. For production - /// deployments with Redis Sentinel, consider using K8s Lease-based leader - /// election instead (requires `k8s` feature). - pub fn new( - redis_conn: redis::aio::ConnectionManager, - identity: String, - key_prefix: &str, - is_sentinel: bool, - ) -> Self { - let config = LeaderElectorConfig::default(); - Self::new_with_config(redis_conn, identity, &config, key_prefix, is_sentinel) - } - /// Create a new leader elector with custom configuration. /// /// The `key_prefix` is prepended to the leader election lock key in Redis. @@ -636,17 +614,6 @@ impl LeaderElector { Self::new_with_runtime_config(redis_runtime, identity, &config, key_prefix, is_sentinel) } - pub fn new_with_shared_runtime( - redis_conn: Arc>, - identity: String, - config: &LeaderElectorConfig, - key_prefix: &str, - is_sentinel: bool, - ) -> Self { - let redis_runtime = synctv_core::shared_runtime(redis_conn); - Self::new_with_runtime_config(redis_runtime, identity, config, key_prefix, is_sentinel) - } - pub fn new_with_runtime_config( redis_runtime: Arc, identity: String, diff --git a/synctv-cluster/src/lib.rs b/synctv-cluster/src/lib.rs index b62d8a1d..d0a675bc 100644 --- a/synctv-cluster/src/lib.rs +++ b/synctv-cluster/src/lib.rs @@ -9,12 +9,10 @@ pub mod leader; #[cfg(feature = "k8s")] pub use discovery::K8sDnsDiscovery; pub use discovery::{ - build_cluster_node_directory_factory, build_local_cluster_node_directory_factory, - normalize_static_peer_address, ClusterHealthRuntime, ClusterMode, ClusterNodeDirectory, - ClusterNodeDirectoryFactory, HealthMonitor, HeartbeatResult, LoadBalancer, - LoadBalancingStrategy, LocalClusterNodeDirectoryFactory, NodeHealth, NodeInfo, NodeRegistry, - NodeViewMode, RedisClusterNodeDirectoryFactory, StaticDiscovery, StaticDiscoveryConfig, - StaticPeerConfig, + build_cluster_node_directory_factory, normalize_static_peer_address, ClusterHealthRuntime, + ClusterMode, ClusterNodeDirectory, ClusterNodeDirectoryFactory, HealthMonitor, HeartbeatResult, + LoadBalancer, LoadBalancingStrategy, NodeHealth, NodeInfo, NodeRegistry, NodeViewMode, + RedisClusterNodeDirectoryFactory, StaticDiscovery, StaticDiscoveryConfig, StaticPeerConfig, }; pub use error::{Error, Result}; pub use grpc::{ diff --git a/synctv-cluster/tests/cluster_coordination_tests.rs b/synctv-cluster/tests/cluster_coordination_tests.rs index 77421d22..005e05fa 100644 --- a/synctv-cluster/tests/cluster_coordination_tests.rs +++ b/synctv-cluster/tests/cluster_coordination_tests.rs @@ -49,7 +49,7 @@ async fn test_node_registry_register_and_get() { )) .await; - let node = registry.test_get_local("self").await; + let node = registry.get_node_local("self").await; assert!(node.is_some()); let node = node.unwrap(); assert_eq!(node.node_id, "self"); @@ -79,7 +79,7 @@ async fn test_node_registry_concurrent_registration() { futures::future::join_all(handles).await; - let all_nodes = registry.test_get_all_local().await; + let all_nodes = registry.get_all_nodes_local().await; assert_eq!(all_nodes.len(), 10); } @@ -96,11 +96,11 @@ async fn test_node_registry_unregister() { let remote = NodeInfo::new("remote-1".to_string(), "localhost:8081".to_string()); registry.test_insert_local(remote).await; - assert_eq!(registry.test_get_all_local().await.len(), 2); + assert_eq!(registry.get_all_nodes_local().await.len(), 2); // Unregister the remote node registry.test_remove_local("remote-1").await; - assert_eq!(registry.test_get_all_local().await.len(), 1); + assert_eq!(registry.get_all_nodes_local().await.len(), 1); } #[tokio::test] @@ -113,7 +113,7 @@ async fn test_node_registry_get_nonexistent() { )) .await; - let node = registry.test_get_local("nonexistent").await; + let node = registry.get_node_local("nonexistent").await; assert!(node.is_none()); } @@ -236,7 +236,7 @@ async fn test_load_balancer_with_health_monitor_no_status() { // Monitor has no statuses yet -- all nodes should be available let monitor = Arc::new(HealthMonitor::new(Arc::clone(®istry), 60)); let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::RoundRobin) - .with_health_monitor(monitor); + .with_health_runtime(monitor); let available = lb.get_available_nodes().await.unwrap(); assert_eq!( @@ -246,25 +246,6 @@ async fn test_load_balancer_with_health_monitor_no_status() { ); } -#[tokio::test] -async fn test_load_balancer_available_count() { - let registry = make_registry("self"); - registry - .test_insert_local(NodeInfo::new( - "self".to_string(), - "localhost:50051".to_string(), - )) - .await; - - for i in 1..4 { - let node = NodeInfo::new(format!("node-{i}"), format!("localhost:{}", 8080 + i)); - registry.test_insert_local(node).await; - } - - let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::Random); - assert_eq!(lb.available_count().await.unwrap(), 4); -} - #[tokio::test] async fn test_load_balancer_select_by_id() { let registry = make_registry("self"); diff --git a/synctv-cluster/tests/cluster_server_tests.rs b/synctv-cluster/tests/cluster_server_tests.rs index 0aa56a24..6209cc7c 100644 --- a/synctv-cluster/tests/cluster_server_tests.rs +++ b/synctv-cluster/tests/cluster_server_tests.rs @@ -15,7 +15,7 @@ fn make_server() -> (Arc, ClusterServer) { NodeRegistry::new_local_only("test-node".to_string(), 30, "cluster-server-test:") .expect("local registry should initialize"), ); - let server = ClusterServer::new(registry.clone()); + let server = ClusterServer::from_runtime(registry.clone()); (registry, server) } diff --git a/synctv-cluster/tests/load_balancer_unhealthy_exclusion_tests.rs b/synctv-cluster/tests/load_balancer_unhealthy_exclusion_tests.rs index f0d660d4..b29d7b6a 100644 --- a/synctv-cluster/tests/load_balancer_unhealthy_exclusion_tests.rs +++ b/synctv-cluster/tests/load_balancer_unhealthy_exclusion_tests.rs @@ -41,7 +41,7 @@ async fn test_only_healthy_node_selected() { } let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::Random) - .with_health_monitor(Arc::clone(&monitor)); + .with_health_runtime(monitor.clone()); // Select 10 times - should always get node-c for i in 0..10 { @@ -68,7 +68,7 @@ async fn test_all_unhealthy_returns_error() { } let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::Random) - .with_health_monitor(monitor); + .with_health_runtime(monitor); let selected = lb.select_node().await; assert!( @@ -92,7 +92,7 @@ async fn test_degraded_nodes_selectable() { } let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::RoundRobin) - .with_health_monitor(monitor); + .with_health_runtime(monitor); let mut selected_ids = HashSet::new(); for _ in 0..20 { @@ -122,7 +122,7 @@ async fn test_round_robin_cycles_through_healthy() { } let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::RoundRobin) - .with_health_monitor(monitor); + .with_health_runtime(monitor); let mut seen_b = false; let mut seen_c = false; @@ -169,7 +169,7 @@ async fn test_unknown_health_treated_as_healthy() { } let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::Random) - .with_health_monitor(monitor); + .with_health_runtime(monitor); let mut selected_ids = HashSet::new(); for _ in 0..30 { @@ -200,7 +200,7 @@ async fn test_select_node_by_id_respects_health_filtering() { } let lb = LoadBalancer::new(Arc::clone(®istry), LoadBalancingStrategy::Random) - .with_health_monitor(Arc::clone(&monitor)); + .with_health_runtime(monitor.clone()); let healthy = lb .select_node_by_id("node-a") diff --git a/synctv-common/Cargo.toml b/synctv-common/Cargo.toml index 9e4cae10..42e51d3d 100644 --- a/synctv-common/Cargo.toml +++ b/synctv-common/Cargo.toml @@ -14,7 +14,6 @@ tls-native-roots = [] [dependencies] http-acl.workspace = true -http-acl-reqwest.workspace = true ipnet.workspace = true reqwest.workspace = true rustls = { workspace = true, optional = true } diff --git a/synctv-common/src/execution.rs b/synctv-common/src/execution.rs index e27b507c..2b6388cf 100644 --- a/synctv-common/src/execution.rs +++ b/synctv-common/src/execution.rs @@ -75,12 +75,6 @@ impl ExecutionControl { } } - #[must_use] - pub fn without_deadline(mut self) -> Self { - self.deadline = None; - self - } - pub fn cancel(&self) { self.cancellation.cancel(); } diff --git a/synctv-common/src/http.rs b/synctv-common/src/http.rs index 7be7b15b..99ce59d5 100644 --- a/synctv-common/src/http.rs +++ b/synctv-common/src/http.rs @@ -59,13 +59,6 @@ impl SsrfSafeClientBuilder { self } - /// Disable SSRF DNS enforcement. - #[must_use] - pub fn disable_ssrf_guard(mut self) -> Self { - self.ssrf_guard = None; - self - } - /// Override the overall request timeout. #[must_use] pub const fn request_timeout(mut self, timeout: Duration) -> Self { @@ -192,12 +185,6 @@ mod tests { assert_eq!(b.resolves, vec![("example.com".to_string(), addr)]); } - #[test] - fn test_disable_ssrf_guard_is_explicit() { - let builder = SsrfSafeClientBuilder::new().disable_ssrf_guard(); - assert!(builder.ssrf_guard.is_none()); - } - #[test] fn test_ssrf_guard_is_opt_in() { let builder = SsrfSafeClientBuilder::new().ssrf_guard(SsrfGuard::strict_policy()); diff --git a/synctv-common/src/id.rs b/synctv-common/src/id.rs index 8e456872..94996bb3 100644 --- a/synctv-common/src/id.rs +++ b/synctv-common/src/id.rs @@ -13,15 +13,10 @@ pub fn generate(size: usize) -> String { nanoid!(size, &BASE62_ALPHABET) } -#[must_use] -pub fn generate_default() -> String { - generate(DEFAULT_LENGTH) -} - #[macro_export] macro_rules! snanoid { () => { - $crate::id::generate_default() + $crate::id::generate($crate::id::DEFAULT_LENGTH) }; ($size:expr) => { $crate::id::generate($size) diff --git a/synctv-common/src/messages.rs b/synctv-common/src/messages.rs index 811ea338..3bb22922 100644 --- a/synctv-common/src/messages.rs +++ b/synctv-common/src/messages.rs @@ -1,6 +1,5 @@ pub const AUTHENTICATION_REQUIRED: &str = "Authentication required"; pub const INVALID_OR_EXPIRED_TOKEN: &str = "Invalid or expired token"; -pub const MISSING_AUTHORIZATION_HEADER: &str = "Missing Authorization header"; pub const INVALID_AUTHORIZATION_HEADER: &str = "Invalid Authorization header"; pub const INVALID_AUTHORIZATION_HEADER_NON_UTF8: &str = "Invalid Authorization header: non-UTF-8 value"; @@ -8,7 +7,6 @@ pub const PERMISSION_DENIED: &str = "Permission denied"; pub const USERNAME_OR_EMAIL_ALREADY_TAKEN: &str = "Username or email already taken"; pub const EMAIL_SERVICE_UNAVAILABLE: &str = "Email service is not available on this server."; pub const INVALID_PROXY_SIGNATURE: &str = "Invalid proxy signature"; -pub const UNKNOWN_PROVIDER: &str = "Unknown provider"; pub const PROVIDER_RESOURCE_NOT_FOUND: &str = "Provider resource not found"; pub const PROVIDER_INSTANCE_NOT_FOUND: &str = "Provider instance not found"; pub const RESOURCE_NOT_FOUND: &str = "Resource not found"; diff --git a/synctv-common/src/reserved.rs b/synctv-common/src/reserved.rs index 73077592..5d313962 100644 --- a/synctv-common/src/reserved.rs +++ b/synctv-common/src/reserved.rs @@ -1,3 +1,2 @@ pub const REGISTRATION_BRUTE_FORCE_SCOPE: &str = "__registration__"; -pub const SYSTEM_USER_ID: &str = "__system__"; pub const SYSTEM_USERNAME: &str = "__system__"; diff --git a/synctv-common/src/ssrf.rs b/synctv-common/src/ssrf.rs index aa15b07e..cd5439e3 100644 --- a/synctv-common/src/ssrf.rs +++ b/synctv-common/src/ssrf.rs @@ -190,7 +190,6 @@ struct SsrfPolicy { allow_private_network_targets: bool, default_denied_ip_ranges: Vec, host_allowlist_denied_ip_ranges: Vec, - extra_denied_ip_ranges: Vec, extra_allowed_ip_ranges: Vec, denied_hosts: HashSet, allowed_hosts: HashSet, @@ -254,10 +253,6 @@ impl SsrfPolicy { return true; } - if contains_ip(&self.extra_denied_ip_ranges, ip) { - return false; - } - if !self.allow_private_network_targets && contains_ip(&self.default_denied_ip_ranges, ip) { return false; } @@ -270,10 +265,6 @@ impl SsrfPolicy { return true; } - if contains_ip(&self.extra_denied_ip_ranges, ip) { - return false; - } - self.allowed_hosts.contains(&normalize_host(host)) && !contains_ip(&self.host_allowlist_denied_ip_ranges, ip) } @@ -492,42 +483,20 @@ impl SsrfGuard { /// Starts with strict SSRF defaults and allows adding extra denied/allowed /// ranges and hosts. pub struct SsrfGuardBuilder { - extra_denied_ip_ranges: Vec, - extra_denied_hosts: Vec, extra_allowed_ip_ranges: Vec, extra_allowed_hosts: Vec, allow_private_network_targets: bool, - allow_http: bool, - allow_https: bool, } impl SsrfGuardBuilder { const fn new() -> Self { Self { - extra_denied_ip_ranges: Vec::new(), - extra_denied_hosts: Vec::new(), extra_allowed_ip_ranges: Vec::new(), extra_allowed_hosts: Vec::new(), allow_private_network_targets: false, - allow_http: true, - allow_https: true, } } - /// Add an extra IP range to deny (beyond defaults). - #[must_use] - pub fn extra_denied_ip_range(mut self, range: IpNet) -> Self { - self.extra_denied_ip_ranges.push(range); - self - } - - /// Add an extra hostname to deny (beyond defaults). - #[must_use] - pub fn extra_denied_host(mut self, host: String) -> Self { - self.extra_denied_hosts.push(host); - self - } - /// Add an IP range to explicitly allow (overrides deny rules). #[must_use] pub fn extra_allowed_ip_range(mut self, range: IpNet) -> Self { @@ -550,20 +519,6 @@ impl SsrfGuardBuilder { self } - /// Set whether HTTP is allowed (default: true). - #[must_use] - pub const fn allow_http(mut self, allow: bool) -> Self { - self.allow_http = allow; - self - } - - /// Set whether HTTPS is allowed (default: true). - #[must_use] - pub const fn allow_https(mut self, allow: bool) -> Self { - self.allow_https = allow; - self - } - /// Build the [`SsrfGuard`] with configured policy. #[must_use] pub fn build(self) -> SsrfGuard { @@ -584,21 +539,16 @@ impl SsrfGuardBuilder { self.allow_private_network_targets || !self.extra_allowed_ip_ranges.is_empty(); let mut builder = HttpAcl::builder(); let extra_allowed_ip_ranges = self.extra_allowed_ip_ranges; - let extra_denied_ip_ranges = self.extra_denied_ip_ranges; let allowed_hosts = self .extra_allowed_hosts .into_iter() .map(|host| normalize_host(&host)) .collect::>(); - let mut denied_hosts = DEFAULT_DENIED_HOSTS + let denied_hosts = DEFAULT_DENIED_HOSTS .iter() .map(|host| normalize_host(host)) .collect::>(); - for host in self.extra_denied_hosts { - denied_hosts.insert(normalize_host(&host)); - } - // Apply default denied ranges for range_str in DEFAULT_EXTRA_DENIED_RANGES { let range = parse_builtin_cidr(range_str)?; @@ -610,13 +560,6 @@ impl SsrfGuardBuilder { builder = valid_acl_config(builder.add_denied_host(host.clone()), "valid hostname")?; } - // Apply extra denied ranges to the underlying ACL where they cannot - // conflict with explicit allow ranges. The guard's own policy below is - // authoritative for IP decisions. - for range in &extra_denied_ip_ranges { - builder = valid_acl_config(builder.add_denied_ip_range(*range), "valid IP range")?; - } - // Apply extra allowed hosts for host in &allowed_hosts { builder = valid_acl_config(builder.add_allowed_host(host.clone()), "valid hostname")?; @@ -626,8 +569,8 @@ impl SsrfGuardBuilder { .non_global_ip_ranges(allow_non_global_ip_ranges) .ip_acl_default(true) .host_acl_default(true) - .http(self.allow_http) - .https(self.allow_https) + .http(true) + .https(true) .try_build(); let acl = valid_acl_config(acl, "SSRF ACL configuration is valid")?; @@ -639,7 +582,6 @@ impl SsrfGuardBuilder { allow_private_network_targets: self.allow_private_network_targets, default_denied_ip_ranges, host_allowlist_denied_ip_ranges: parse_ranges(HOST_ALLOWLIST_DENIED_RANGES)?, - extra_denied_ip_ranges, extra_allowed_ip_ranges, denied_hosts, allowed_hosts, @@ -683,7 +625,6 @@ impl SsrfGuard { allow_private_network_targets: false, default_denied_ip_ranges: Vec::new(), host_allowlist_denied_ip_ranges: Vec::new(), - extra_denied_ip_ranges: Vec::new(), extra_allowed_ip_ranges: Vec::new(), denied_hosts: HashSet::new(), allowed_hosts: HashSet::new(), @@ -1094,56 +1035,4 @@ mod tests { let result = resolver.resolve(parse_test_dns_name("example.com")?).await; assert_resolution_blocked(result, "example.com") } - - // Builder tests - - #[test] - fn test_builder_extra_denied_ip_range() -> TestResult { - // Use a global IP range (Cloudflare's 104.16.0.0/12) to test custom deny rules. - // Non-global ranges like 203.0.113.0/24 are already blocked by default. - let guard = SsrfGuard::builder() - .extra_denied_ip_range(parse_test_cidr("104.16.0.0/12")?) - .build(); - // Custom denied range is blocked - assert!(guard.is_ip_blocked(&IpAddr::V4(Ipv4Addr::new(104, 16, 0, 1)))); - // Default blocks still apply - assert!(guard.is_ip_blocked(&IpAddr::V4(Ipv4Addr::LOCALHOST))); - // Public IPs outside the denied range still allowed - assert!(!guard.is_ip_blocked(&IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)))); - Ok(()) - } - - #[test] - fn test_builder_extra_denied_host() { - let guard = SsrfGuard::builder() - .extra_denied_host("evil.internal".to_string()) - .build(); - assert!(guard.is_host_blocked("evil.internal")); - // Default blocks still apply - assert!(guard.is_host_blocked("localhost")); - // Public hosts still allowed - assert!(!guard.is_host_blocked("example.com")); - } - - #[test] - fn test_builder_disallow_http() -> TestResult { - let guard = SsrfGuard::builder().allow_http(false).build(); - let acl = acl_for_test(&guard)?; - // HTTP should be disallowed - assert!(acl.is_scheme_allowed("http").is_denied()); - // HTTPS should still be allowed - assert!(acl.is_scheme_allowed("https").is_allowed()); - Ok(()) - } - - #[test] - fn test_builder_disallow_https() -> TestResult { - let guard = SsrfGuard::builder().allow_https(false).build(); - let acl = acl_for_test(&guard)?; - // HTTPS should be disallowed - assert!(acl.is_scheme_allowed("https").is_denied()); - // HTTP should still be allowed - assert!(acl.is_scheme_allowed("http").is_allowed()); - Ok(()) - } } diff --git a/synctv-core/Cargo.toml b/synctv-core/Cargo.toml index b40c2b3a..02e8b371 100644 --- a/synctv-core/Cargo.toml +++ b/synctv-core/Cargo.toml @@ -38,7 +38,6 @@ serde_json.workspace = true # Caching moka.workspace = true -lru.workspace = true dashmap.workspace = true async_singleflight.workspace = true @@ -65,11 +64,9 @@ opendal-http-transport-reqwest.workspace = true thiserror.workspace = true bytes.workspace = true parking_lot.workspace = true -paste.workspace = true url.workspace = true percent-encoding.workspace = true regex.workspace = true -ipnet.workspace = true # HTTP Client reqwest.workspace = true @@ -80,12 +77,10 @@ webpki-roots.workspace = true rustls.workspace = true # Resilience -tower.workspace = true governor.workspace = true nonzero_ext.workspace = true backon.workspace = true failsafe.workspace = true -futures-util.workspace = true # Time chrono.workspace = true @@ -104,7 +99,6 @@ handlebars.workspace = true # Misc base64.workspace = true -hostname.workspace = true lz4_flex.workspace = true zstd.workspace = true image.workspace = true @@ -151,7 +145,6 @@ tls-native-roots = [ workspace = true [dev-dependencies] -mockall.workspace = true tokio = { workspace = true, features = ["test-util"] } criterion.workspace = true rand.workspace = true diff --git a/synctv-core/src/cache/consistency.rs b/synctv-core/src/cache/consistency.rs index 414665e5..755360e5 100644 --- a/synctv-core/src/cache/consistency.rs +++ b/synctv-core/src/cache/consistency.rs @@ -28,13 +28,6 @@ use crate::{ SharedStateProfile, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConsistencyPolicy { - EventuallyConsistent, - ReadYourWrites, - Strong, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum CacheDomain { Permission { @@ -147,13 +140,6 @@ fn cache_domain_metric_label(domain: &CacheDomain) -> &'static str { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VersionedCacheValue { - pub version: i64, - pub value: T, -} - #[async_trait] pub trait VersionFenceStore: Send + Sync { async fn current_version(&self, domain: &CacheDomain) -> Result>; @@ -1175,22 +1161,6 @@ impl ConsistencyCoordinator { } } - pub async fn commit_observed_write(&self, domain: &CacheDomain, version: i64) -> Result { - if !self.is_authoritative() { - Self::record_success(domain, "commit_bypass"); - return Ok(version); - } - - let reservation = VersionFenceReservation { - version, - token: String::new(), - started_at_ms: None, - }; - let result = self.fence_store.commit_write(domain, &reservation).await; - Self::record_result(domain, "commit_write", &result); - result - } - pub async fn commit_reserved_write( &self, domain: &CacheDomain, diff --git a/synctv-core/src/cache/invalidation.rs b/synctv-core/src/cache/invalidation.rs index b04c7cfe..7bb80ddb 100644 --- a/synctv-core/src/cache/invalidation.rs +++ b/synctv-core/src/cache/invalidation.rs @@ -1583,81 +1583,6 @@ impl CacheInvalidationService { } self.broadcast_remote(msg).await } - - /// Minimum interval between lag-triggered flushes. - /// - /// Matches the rate-limiting in `CacheManager::start_invalidation_listener` - /// to prevent sustained lag storms from cascading into continuous DB stampedes. - const LAG_FLUSH_MIN_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); - - /// Spawn a named invalidation listener that subscribes to this service's - /// broadcast channel and dispatches messages to a user-provided handler. - /// - /// This extracts the common pattern shared by `CacheManager`, `PermissionService`, - /// `PlaybackService`, `RoomSettingsService`, etc.: - /// - /// 1. Subscribe to the broadcast channel - /// 2. Spawn a monitored task that loops on `recv()` - /// 3. On `Ok(msg)` -> call `handler(msg)` - /// 4. On `Lagged(n)` -> call `on_lagged()` to flush caches (rate-limited) - /// 5. On `Closed` -> break - /// - /// The `on_lagged` handler is rate-limited to at most once every 5 seconds - /// to prevent continuous cache flush cascades under sustained lag. - /// - /// # Arguments - /// * `name` - Task name for monitoring (e.g., "`room_settings_invalidation_listener`") - /// * `handler` - Async closure called for each received message - /// * `on_lagged` - Async closure called when the receiver falls behind (should flush caches) - pub fn spawn_listener(&self, name: &'static str, handler: H, on_lagged: L) - where - H: Fn(InvalidationMessage) -> Hf + Send + 'static, - Hf: std::future::Future + Send, - L: Fn(u64) -> Lf + Send + 'static, - Lf: std::future::Future + Send, - { - let mut receiver = self.subscribe(); - - crate::spawn::spawn_monitored(name, async move { - // Rate-limit lag-triggered flushes to at most once per interval. - let mut last_lag_flush = std::time::Instant::now() - .checked_sub(Self::LAG_FLUSH_MIN_INTERVAL) - .unwrap_or_else(std::time::Instant::now); - - loop { - match receiver.recv().await { - Ok(msg) => { - handler(msg).await; - } - Err(broadcast::error::RecvError::Closed) => { - debug!("{name}: invalidation channel closed, stopping listener"); - break; - } - Err(broadcast::error::RecvError::Lagged(n)) => { - let now = std::time::Instant::now(); - let elapsed = now.duration_since(last_lag_flush); - if elapsed >= Self::LAG_FLUSH_MIN_INTERVAL { - warn!( - lagged_messages = n, - "{name}: invalidation listener lagged, triggering flush (rate-limited to once per {}s)", - Self::LAG_FLUSH_MIN_INTERVAL.as_secs() - ); - on_lagged(n).await; - crate::metrics::cache::CACHE_LAG_FLUSH_TOTAL - .with_label_values(&[name]) - .inc(); - last_lag_flush = now; - } else { - warn!( - lagged_messages = n, - "{name}: invalidation listener lagged, skipping flush (rate-limited)" - ); - } - } - } - } - }); - } } #[async_trait] diff --git a/synctv-core/src/cache/key_builder.rs b/synctv-core/src/cache/key_builder.rs index 210ae206..83ae14ab 100644 --- a/synctv-core/src/cache/key_builder.rs +++ b/synctv-core/src/cache/key_builder.rs @@ -81,43 +81,6 @@ impl KeyBuilder { self.prefixed_key(&format!("cluster:nodes:{node_id}")) } - /// Active nodes list (Sorted Set) - /// - /// Type: Sorted Set - /// Member: `node_id` - /// Score: timestamp (for cleanup) - #[must_use] - pub fn cluster_nodes_active(&self) -> String { - self.prefixed_key("cluster:nodes:active") - } - - /// Stream publisher information - /// - /// Type: Hash + TTL (300s) - /// Fields: `node_id`, `started_at`, status, `viewer_count` - #[must_use] - pub fn stream_info(&self, stream_key: &str) -> String { - self.prefixed_key(&format!("stream:info:{stream_key}")) - } - - /// Stream pull subscribers - /// - /// Type: Set + TTL (300s) - /// Members: `node_id` (nodes that are pulling this stream) - #[must_use] - pub fn stream_subscribers(&self, stream_key: &str) -> String { - self.prefixed_key(&format!("stream:subscribers:{stream_key}")) - } - - /// Stream statistics - /// - /// Type: Hash + TTL (600s) - /// Fields: viewers, bitrate, packets, bytes - #[must_use] - pub fn stream_stats(&self, stream_key: &str) -> String { - self.prefixed_key(&format!("stream:stats:{stream_key}")) - } - /// Room current state /// /// Type: Hash @@ -136,43 +99,6 @@ impl KeyBuilder { self.prefixed_key(&format!("room:{room_id}:members")) } - /// Room online users - /// - /// Type: Sorted Set - /// Members: `user_id` - /// Score: `last_activity_timestamp` - #[must_use] - pub fn room_online_users(&self, room_id: &str) -> String { - self.prefixed_key(&format!("room:{room_id}:online")) - } - - /// Room viewer count - /// - /// Type: String + TTL (60s) - /// Value: number (count) - #[must_use] - pub fn room_viewers(&self, room_id: &str) -> String { - self.prefixed_key(&format!("room:{room_id}:viewers")) - } - - /// Playback information cache - /// - /// Type: String + TTL (dynamic) - /// Value: JSON with playback state - #[must_use] - pub fn playback_cache(&self, cache_key: &str) -> String { - self.prefixed_key(&format!("playback:{cache_key}")) - } - - /// User session - /// - /// Type: String + TTL (dynamic) - /// Value: JSON with session data - #[must_use] - pub fn user_session(&self, session_id: &str) -> String { - self.prefixed_key(&format!("session:{session_id}")) - } - /// Short-lived auth/session workflow key. /// /// Namespaces are fixed by the owning service (for example OPAQUE, MFA, or @@ -329,22 +255,6 @@ impl KeyBuilder { pub fn cache_invalidation_stream(&self) -> String { self.prefixed_key("cache:invalidate:stream") } - - /// Cluster events pub/sub channel - /// - /// Used for cross-cluster message broadcasting - #[must_use] - pub fn realtime_events_channel(&self) -> String { - self.prefixed_key("cluster:events") - } - - /// Room-specific messages channel - /// - /// Used for room message broadcasting. - #[must_use] - pub fn room_messages_channel(&self, room_id: &str) -> String { - self.prefixed_key(&format!("room:{room_id}:messages")) - } } #[cfg(test)] @@ -356,8 +266,6 @@ mod tests { let builder = KeyBuilder::new("prod"); assert_eq!(builder.cluster_node("node-1"), "prod:cluster:nodes:node-1"); - - assert_eq!(builder.stream_info("room_123"), "prod:stream:info:room_123"); } #[test] diff --git a/synctv-core/src/cache/manager.rs b/synctv-core/src/cache/manager.rs index e3fc9155..540c2aaf 100644 --- a/synctv-core/src/cache/manager.rs +++ b/synctv-core/src/cache/manager.rs @@ -48,7 +48,7 @@ impl CacheManager { /// messages to the appropriate cache: /// - `InvalidationMessage::User { user_id }` -> `user_cache.invalidate_by_id()` /// - `InvalidationMessage::Room { room_id }` -> `room_cache.invalidate_by_id()` - /// - `InvalidationMessage::All` -> `clear_all_l1()` + /// - `InvalidationMessage::All` -> clear every L1 cache /// /// Permission-related messages are ignored here (handled by `PermissionService`). pub fn start_invalidation_listener( @@ -187,19 +187,6 @@ impl CacheManager { } }) } - - /// Clear all L1 caches (memory only) - /// - /// Useful for testing or manual cache clearing. - /// Note: L2 (Redis) caches are not cleared. - pub fn clear_all_l1(&self) { - self.user_cache.clear_l1(); - self.room_cache.clear_l1(); - if let Some(ref uc) = self.username_cache { - uc.clear_memory(); - } - tracing::debug!("All L1 caches cleared"); - } } impl std::fmt::Debug for CacheManager { diff --git a/synctv-core/src/cache/member_permission_cache.rs b/synctv-core/src/cache/member_permission_cache.rs index 8762d262..037ac4ff 100644 --- a/synctv-core/src/cache/member_permission_cache.rs +++ b/synctv-core/src/cache/member_permission_cache.rs @@ -95,7 +95,6 @@ impl CachedMemberPermissionSource { room_id: self.room_id, user_id: self.user_id, role: self.role, - status: crate::models::MemberStatus::Active, added_permissions: self.added_permissions, removed_permissions: self.removed_permissions, admin_added_permissions: self.admin_added_permissions, diff --git a/synctv-core/src/cache/mod.rs b/synctv-core/src/cache/mod.rs index 40072ce6..0a419035 100644 --- a/synctv-core/src/cache/mod.rs +++ b/synctv-core/src/cache/mod.rs @@ -15,8 +15,8 @@ pub mod username_cache; pub use consistency::version_fence_store_from_shared_state_profile; pub use consistency::{ - CacheDomain, ConsistencyCoordinator, ConsistencyPolicy, LocalVersionFenceStore, - RedisVersionFenceStore, VersionFenceReservation, VersionFenceStore, VersionedCacheValue, + CacheDomain, ConsistencyCoordinator, LocalVersionFenceStore, RedisVersionFenceStore, + VersionFenceReservation, VersionFenceStore, }; pub use invalidation::{CacheInvalidationRuntime, CacheInvalidationService, InvalidationMessage}; pub use key_builder::KeyBuilder; diff --git a/synctv-core/src/cache/room_cache.rs b/synctv-core/src/cache/room_cache.rs index 692b1117..ada4c311 100644 --- a/synctv-core/src/cache/room_cache.rs +++ b/synctv-core/src/cache/room_cache.rs @@ -55,22 +55,11 @@ pub struct CachedRoom { updated_at: chrono::DateTime, } -pub struct CachedRoomSnapshot { - pub id: String, - pub name: String, - pub owner_id: String, - pub is_public: bool, - pub status: RoomStatus, - pub is_banned: bool, - pub deleted_at: Option>, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - impl CachedRoom { /// Create a new `CachedRoom` with default status (Active), not banned, not deleted. /// /// This is a convenience constructor for active, visible room snapshots. + #[cfg(test)] #[must_use] pub fn new( id: String, @@ -92,21 +81,6 @@ impl CachedRoom { } } - #[must_use] - pub fn from_snapshot(snapshot: CachedRoomSnapshot) -> Self { - Self { - id: snapshot.id, - name: snapshot.name, - owner_id: snapshot.owner_id, - is_public: snapshot.is_public, - status: snapshot.status, - is_banned: snapshot.is_banned, - deleted_at: snapshot.deleted_at, - created_at: snapshot.created_at, - updated_at: snapshot.updated_at, - } - } - /// Get the room ID #[must_use] pub fn id(&self) -> &str { @@ -119,12 +93,6 @@ impl CachedRoom { &self.name } - /// Get the owner (creator) user ID - #[must_use] - pub fn owner_id(&self) -> &str { - &self.owner_id - } - /// Whether the room is publicly listed #[must_use] pub const fn is_public(&self) -> bool { @@ -143,12 +111,6 @@ impl CachedRoom { self.is_banned } - /// Get the soft-delete timestamp (None if not deleted) - #[must_use] - pub const fn deleted_at(&self) -> Option> { - self.deleted_at - } - /// Get the `created_at` timestamp #[must_use] pub const fn created_at(&self) -> chrono::DateTime { diff --git a/synctv-core/src/credential_encryption.rs b/synctv-core/src/credential_encryption.rs index 1d518e3e..ca948f16 100644 --- a/synctv-core/src/credential_encryption.rs +++ b/synctv-core/src/credential_encryption.rs @@ -186,10 +186,6 @@ impl CredentialEncryption { .internal_with_err("Decrypted credential is not valid JSON") } - pub fn decrypt_value(&self, value: &serde_json::Value) -> Result { - self.decrypt_value_with_context(value, b"") - } - pub fn decrypt_value_with_context( &self, value: &serde_json::Value, @@ -203,11 +199,6 @@ impl CredentialEncryption { } } - /// Encrypt a JSON Value and return as a string Value for DB storage - pub fn encrypt_to_value(&self, plaintext: &serde_json::Value) -> Result { - self.encrypt_to_value_with_context(plaintext, b"") - } - pub fn encrypt_to_value_with_context( &self, plaintext: &serde_json::Value, @@ -271,7 +262,7 @@ mod tests { } #[test] - fn test_decrypt_value_encrypted() { + fn test_decrypt_value_with_context_encrypted() { let enc = ok( CredentialEncryption::new(&test_key()), "encryption should build", @@ -279,7 +270,7 @@ mod tests { let original = json!({"apiKey": "secret123"}); let encrypted_value = ok( - enc.encrypt_to_value(&original), + enc.encrypt_to_value_with_context(&original, b"test-context"), "credential value should encrypt", ); assert!(encrypted_value @@ -287,21 +278,21 @@ mod tests { .is_some_and(|s| s.starts_with("enc:"))); let decrypted = ok( - enc.decrypt_value(&encrypted_value), + enc.decrypt_value_with_context(&encrypted_value, b"test-context"), "credential value should decrypt", ); assert_eq!(original, decrypted); } #[test] - fn test_decrypt_value_plaintext_returns_error() { + fn test_decrypt_value_with_context_plaintext_returns_error() { let enc = ok( CredentialEncryption::new(&test_key()), "encryption should build", ); let plaintext = json!({"cookies": {"SESSDATA": "test"}}); - let result = enc.decrypt_value(&plaintext); + let result = enc.decrypt_value_with_context(&plaintext, b"test-context"); assert!(result.is_err(), "Plaintext JSON values should be rejected"); } diff --git a/synctv-core/src/error.rs b/synctv-core/src/error.rs index 99072616..f07341fb 100644 --- a/synctv-core/src/error.rs +++ b/synctv-core/src/error.rs @@ -73,11 +73,6 @@ impl Error { pub const fn kick_cooldown_denied_message() -> &'static str { KICK_COOLDOWN_DENIED_MESSAGE } - - #[must_use] - pub fn is_kick_cooldown_denied(&self) -> bool { - matches!(self, Self::KickCooldownDenied) - } } impl From for Error { @@ -293,7 +288,6 @@ mod tests { let error = Error::kick_cooldown_denied(); assert!(matches!(error, Error::KickCooldownDenied)); - assert!(error.is_kick_cooldown_denied()); assert_eq!( Error::kick_cooldown_denied_message(), "User was recently kicked from this room and cannot access it yet" diff --git a/synctv-core/src/lib.rs b/synctv-core/src/lib.rs index eb92c79b..3e047fdf 100644 --- a/synctv-core/src/lib.rs +++ b/synctv-core/src/lib.rs @@ -89,7 +89,6 @@ pub mod resilience; pub mod service; pub mod shared_state; pub mod spawn; -pub mod transaction; pub mod validation; #[cfg(test)] @@ -102,13 +101,11 @@ pub use clock::{ }; pub use error::{Error, InternalExt, Result}; pub use redis_runtime::{ - coordination_runtime_from_client, coordination_runtime_from_client_with_connection_options, + coordination_runtime_from_client, coordination_runtime_from_client_with_connection_options_and_operation_timeout, direct_runtime, - direct_runtime_from_conn, direct_runtime_with_operation_timeout, redis_connection_manager_options, redis_runtime_snapshot, shared_runtime, - shared_runtime_from_conn, shared_runtime_with_operation_timeout, DirectRedisConnectionRuntime, - ManagedRedisRuntime, OnDemandRedisRuntime, RedisConnectionRuntime, RedisCoordinationRuntime, - RedisDeploymentMode, SharedRedisConnectionRuntime, + shared_runtime_from_conn, DirectRedisConnectionRuntime, ManagedRedisRuntime, + OnDemandRedisRuntime, RedisConnectionRuntime, RedisCoordinationRuntime, RedisDeploymentMode, + SharedRedisConnectionRuntime, }; pub use shared_state::{SharedStateMode, SharedStateProfile}; -pub use transaction::with_transaction; diff --git a/synctv-core/src/logging.rs b/synctv-core/src/logging.rs index b22e048f..331db900 100644 --- a/synctv-core/src/logging.rs +++ b/synctv-core/src/logging.rs @@ -453,15 +453,6 @@ pub fn effective_log_level(config: &LoggingOptions) -> anyhow::Result { parse_log_level(&config.global.level) } -/// Generate a trace ID for request tracing. -#[must_use] -pub fn generate_trace_id() -> String { - use rand::RngExt; - let mut rng = rand::rng(); - let trace_id: u128 = rng.random(); - format!("{trace_id:032x}") -} - #[cfg(test)] mod tests { use super::*; diff --git a/synctv-core/src/metrics.rs b/synctv-core/src/metrics.rs index c8977a3c..166643d1 100644 --- a/synctv-core/src/metrics.rs +++ b/synctv-core/src/metrics.rs @@ -4,8 +4,8 @@ //! All metrics are automatically exposed via the /metrics endpoint for Prometheus scraping. use prometheus::{ - core::Collector, CounterVec, Encoder, Gauge, GaugeVec, HistogramOpts, HistogramVec, IntCounter, - IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, TextEncoder, + core::Collector, CounterVec, Encoder, GaugeVec, HistogramOpts, HistogramVec, IntCounter, + IntCounterVec, IntGauge, Opts, Registry, TextEncoder, }; /// Global metrics registry. @@ -47,11 +47,6 @@ fn int_gauge(name: &str, help: &str) -> IntGauge { register_metric(metric, name) } -fn gauge(name: &str, help: &str) -> Gauge { - let metric = Gauge::new(name, help).unwrap_or_else(|error| abort_invalid_metric(name, &error)); - register_metric(metric, name) -} - fn counter_vec(name: &str, help: &str, labels: &[&str]) -> CounterVec { let metric = CounterVec::new(Opts::new(name, help), labels) .unwrap_or_else(|error| abort_invalid_metric(name, &error)); @@ -70,12 +65,6 @@ fn gauge_vec(name: &str, help: &str, labels: &[&str]) -> GaugeVec { register_metric(metric, name) } -fn int_gauge_vec(name: &str, help: &str, labels: &[&str]) -> IntGaugeVec { - let metric = IntGaugeVec::new(Opts::new(name, help), labels) - .unwrap_or_else(|error| abort_invalid_metric(name, &error)); - register_metric(metric, name) -} - fn histogram_vec(opts: HistogramOpts, labels: &[&str]) -> HistogramVec { let metric_name = opts.common_opts.fq_name(); let metric = HistogramVec::new(opts, labels) @@ -113,16 +102,6 @@ pub mod http { ) }); - /// HTTP error rate counter, labeled by method, path, and error type. - pub static HTTP_ERROR_RATE: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_counter_vec( - "http_error_rate_total", - "Total HTTP errors by type", - &["method", "path", "error_type"], - ) - }); - /// Number of in-flight HTTP requests. pub static HTTP_REQUESTS_IN_FLIGHT: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -151,16 +130,6 @@ pub mod http { ) }); - /// Total WebSocket messages processed, labeled by direction (inbound/outbound) and type (text/binary/ping/pong). - pub static WEBSOCKET_MESSAGES_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_counter_vec( - "websocket_messages_total", - "Total number of WebSocket messages processed", - &["direction", "type"], - ) - }); - /// Total WebSocket errors, labeled by error type. pub static WEBSOCKET_ERRORS_TOTAL: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -170,21 +139,6 @@ pub mod http { &["error_type"], ) }); - - /// WebSocket connection duration in seconds (how long each connection was alive). - pub static WEBSOCKET_CONNECTION_DURATION_SECONDS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "websocket_connection_duration_seconds", - "WebSocket connection duration in seconds", - ) - .buckets(vec![ - 1.0, 5.0, 15.0, 30.0, 60.0, 300.0, 900.0, 1800.0, 3600.0, 7200.0, - ]), - &[], - ) - }); } /// Application runtime metrics shared by core services and transport adapters. @@ -212,16 +166,6 @@ pub mod application { ) }); - /// Total playlist items added. - pub static PLAYLIST_ITEMS_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_counter_vec( - "playlist_items_total", - "Total number of playlist items added", - &[], - ) - }); - /// Total chat messages sent. pub static CHAT_MESSAGES_TOTAL: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -324,40 +268,6 @@ pub mod cache { ) }); - /// Cache fill duration histogram (time taken to load from DB and populate cache) - pub static CACHE_FILL_DURATION: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "cache_fill_duration_seconds", - "Time taken to fill cache from database", - ), - &["cache_type"], - ) - }); - - /// `SingleFlight` merge counter (how many concurrent requests were deduplicated) - pub static SINGLEFLIGHT_MERGES: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - counter_vec( - "cache_singleflight_merges_total", - "Total number of requests merged by SingleFlight", - &["cache_type"], - ) - }); - - /// Cross-replica cache invalidation duration histogram - pub static INVALIDATION_LATENCY: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "cache_invalidation_latency_seconds", - "Time taken for cross-replica cache invalidation", - ), - &["cache_type"], - ) - }); - /// Total cache invalidations, labeled by cache type. pub static CACHE_INVALIDATIONS: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -458,31 +368,6 @@ pub mod cache { pub mod database { use super::*; - /// Query duration histogram with optimized buckets for P50/P95/P99. - pub static DB_QUERY_DURATION: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "db_query_duration_seconds", - "Database query duration in seconds (P50/P95/P99)", - ) - .buckets(vec![ - 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, - ]), - &["operation", "table"], - ) - }); - - /// Total database operations, labeled by operation, table, and result. - pub static DB_OPERATIONS_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_counter_vec( - "db_operations_total", - "Total database operations", - &["operation", "table", "result"], - ) - }); - /// Active connections gauge pub static DB_CONNECTIONS_ACTIVE: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -492,15 +377,6 @@ pub mod database { ) }); - /// Query error counter - pub static DB_QUERY_ERRORS: std::sync::LazyLock = std::sync::LazyLock::new(|| { - counter_vec( - "db_query_errors_total", - "Total number of database query errors", - &["operation", "error_type"], - ) - }); - /// Pool utilization percentage (0.0 to 1.0) pub static DB_POOL_UTILIZATION: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -511,37 +387,6 @@ pub mod database { ) }); - /// Connections waiting for a connection from the pool - pub static DB_CONNECTIONS_WAITING: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_gauge( - "db_connections_waiting", - "Number of connections waiting for a connection from the pool", - ) - }); - - /// Connection acquire duration histogram - pub static DB_CONNECTION_ACQUIRE_DURATION: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "db_connection_acquire_duration_seconds", - "Time taken to acquire a connection from the pool", - ), - &["pool"], - ) - }); - - /// Transaction rollback counter - pub static DB_TRANSACTION_ROLLBACKS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - counter_vec( - "db_transaction_rollbacks_total", - "Total number of database transaction rollbacks", - &["reason"], - ) - }); - /// Total connections in the pool (max pool size) pub static DB_POOL_SIZE_MAX: std::sync::LazyLock = std::sync::LazyLock::new(|| { int_gauge( @@ -585,60 +430,6 @@ pub mod remote_transport { &["service", "method", "status"], ) }); - - /// Active remote transport streams gauge. - pub static REMOTE_TRANSPORT_ACTIVE_STREAMS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_gauge( - "grpc_active_streams", - "Current number of active remote transport streams", - ) - }); -} - -/// Redis operations -pub mod redis { - use super::*; - - /// Total Redis operation errors, labeled by operation type. - pub static REDIS_ERRORS: std::sync::LazyLock = std::sync::LazyLock::new(|| { - int_counter_vec( - "redis_errors_total", - "Total Redis operation errors", - &["operation"], - ) - }); - - /// Redis operation duration in seconds, labeled by operation type. - /// Buckets optimized for P50/P95/P99 calculation. - pub static REDIS_OPERATION_DURATION: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "redis_operation_duration_seconds", - "Redis operation duration in seconds (P50/P95/P99)", - ) - .buckets(vec![ - 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, - ]), - &["operation"], - ) - }); - - /// Total Redis operations, labeled by operation and result (success/error). - pub static REDIS_OPERATIONS_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_counter_vec( - "redis_operations_total", - "Total Redis operations", - &["operation", "result"], - ) - }); - - /// Redis connection pool size. - pub static REDIS_POOL_SIZE: std::sync::LazyLock = std::sync::LazyLock::new(|| { - int_gauge_vec("redis_pool_size", "Redis connection pool size", &["pool"]) - }); } /// Cluster operations @@ -702,14 +493,6 @@ pub mod cluster { ) }); - /// Node health status (1 = healthy, 0 = unhealthy). - pub static NODE_HEALTH_STATUS: std::sync::LazyLock = std::sync::LazyLock::new(|| { - int_gauge( - "synctv_cluster_node_health_status", - "Node health status (1 = healthy, 0 = unhealthy)", - ) - }); - /// Leader election state (1 = leader, 0 = follower). pub static LEADER_ELECTION_STATE: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -765,82 +548,6 @@ pub mod cluster { ) }); - /// Redis pub/sub connection health (1 = connected, 0 = disconnected). - pub static REDIS_PUBSUB_HEALTH: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_gauge( - "synctv_cluster_redis_pubsub_health", - "Redis pub/sub connection health (1 = connected, 0 = disconnected)", - ) - }); - - /// Total number of cluster members. - pub static CLUSTER_MEMBER_COUNT: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_gauge( - "synctv_cluster_member_count", - "Total number of cluster members", - ) - }); - - /// Leader election duration in seconds. - pub static LEADER_ELECTION_DURATION: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "synctv_cluster_leader_election_duration_seconds", - "Leader election duration in seconds", - ), - &["result"], - ) - }); - - /// Redis pub/sub message publish latency in seconds. - pub static REDIS_PUBSUB_PUBLISH_LATENCY: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "synctv_cluster_redis_pubsub_publish_latency_seconds", - "Redis pub/sub message publish latency in seconds", - ) - .buckets(vec![0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1]), - &["channel"], - ) - }); - - /// Node-to-node message latency in seconds (end-to-end). - pub static NODE_MESSAGE_LATENCY: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "synctv_cluster_node_message_latency_seconds", - "Node-to-node message latency in seconds", - ) - .buckets(vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5]), - &["event_type"], - ) - }); - - /// Total cluster synchronization errors, labeled by error type. - pub static CLUSTER_SYNC_ERRORS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_counter_vec( - "synctv_cluster_sync_errors_total", - "Total cluster synchronization errors", - &["error_type"], - ) - }); - - /// This node's last successful heartbeat timestamp (Unix timestamp). - /// Reports only this node's own heartbeat (no per-node_id label to avoid unbounded cardinality). - pub static NODE_LAST_HEARTBEAT: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - gauge( - "synctv_cluster_node_last_heartbeat_timestamp", - "This node's last successful heartbeat timestamp (Unix)", - ) - }); - /// Total distributed counter TTL refresh operations, labeled by result. /// /// Labels: "success", "failure". @@ -974,26 +681,6 @@ pub mod logging { pub mod rate_limit { use super::*; - /// Total rate limit checks, labeled by backend ("redis" or "memory") and category. - pub static RATE_LIMIT_CHECKS_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - counter_vec( - "rate_limit_checks_total", - "Total number of rate limit checks", - &["backend", "category"], - ) - }); - - /// Total rate limit rejections (429s), labeled by backend and category. - pub static RATE_LIMIT_REJECTIONS_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - counter_vec( - "rate_limit_rejections_total", - "Total number of rate limit rejections (429)", - &["backend", "category"], - ) - }); - /// Redis errors that triggered fallback to in-memory rate limiting. pub static RATE_LIMIT_REDIS_FALLBACKS_TOTAL: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -1087,38 +774,6 @@ pub mod livestream { ) }); - /// Total bytes transferred for livestream, labeled by direction (in/out). - pub static LIVESTREAM_BYTES_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - counter_vec( - "livestream_bytes_total", - "Total bytes transferred for livestream", - &["direction"], - ) - }); - - /// Livestream duration in seconds (how long each stream session lasted). - pub static LIVESTREAM_STREAM_DURATION_SECONDS: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "livestream_stream_duration_seconds", - "Livestream session duration in seconds", - ), - &["stream_type"], - ) - }); - - /// Total stream pull errors, labeled by error type. - pub static LIVESTREAM_PULL_ERRORS_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - counter_vec( - "livestream_pull_errors_total", - "Total number of livestream pull errors", - &["error_type"], - ) - }); - /// Total relay frames dropped due to backpressure. pub static LIVESTREAM_RELAY_FRAME_DROPS: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -1128,32 +783,6 @@ pub mod livestream { ) }); - /// Number of cached GOPs across all active streams. - pub static GOP_CACHE_SIZE: std::sync::LazyLock = std::sync::LazyLock::new(|| { - int_gauge( - "gop_cache_size", - "Number of cached GOPs across all active streams", - ) - }); - - /// Total number of GOPs evicted due to memory limits since process start. - pub static GOP_CACHE_DROPS_TOTAL: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_counter( - "gop_cache_drops_total", - "Total number of GOPs evicted due to memory limits", - ) - }); - - /// Current memory usage in bytes of the GOP cache across all active streams. - pub static GOP_CACHE_MEMORY_BYTES: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - int_gauge( - "gop_cache_memory_bytes", - "Current memory usage in bytes of the GOP cache across all active streams", - ) - }); - /// Total FLV stream terminations due to slow client (exceeded consecutive frame drops). pub static LIVESTREAM_FLV_SLOW_CLIENT_TERMINATIONS_TOTAL: std::sync::LazyLock = std::sync::LazyLock::new(|| { @@ -1164,116 +793,6 @@ pub mod livestream { }); } -/// Helper macro to record HTTP request metrics -#[macro_export] -macro_rules! record_http_request { - ($method:expr, $path:expr, $status:expr, $duration:expr) => { - let status_str = $status.to_string(); - let method_str = $method.to_string(); - - $crate::metrics::http::HTTP_REQUEST_DURATION_SECONDS - .with_label_values(&[&method_str, $path]) - .observe($duration.as_secs_f64()); - - $crate::metrics::http::HTTP_REQUESTS_TOTAL - .with_label_values(&[&method_str, $path, &status_str]) - .inc(); - }; -} - -/// Helper macro to record cache metrics -#[macro_export] -macro_rules! record_cache_hit { - ($cache_type:expr, $level:expr) => { - $crate::metrics::cache::CACHE_HITS - .with_label_values(&[$cache_type, $level]) - .inc(); - }; -} - -#[macro_export] -macro_rules! record_cache_miss { - ($cache_type:expr, $level:expr) => { - $crate::metrics::cache::CACHE_MISSES - .with_label_values(&[$cache_type, $level]) - .inc(); - }; -} - -/// Helper macro to record database query metrics -#[macro_export] -macro_rules! record_db_query { - ($operation:expr, $table:expr, $duration:expr, $error:expr) => { - $crate::metrics::database::DB_QUERY_DURATION - .with_label_values(&[$operation, $table]) - .observe($duration.as_secs_f64()); - - if let Err(e) = $error { - let error_type = if e.to_string().contains("timeout") { - "timeout" - } else if e.to_string().contains("connection") { - "connection" - } else { - "other" - }; - $crate::metrics::database::DB_QUERY_ERRORS - .with_label_values(&[$operation, error_type]) - .inc(); - } - }; -} - -/// Hot path metrics -pub mod hot_paths { - use super::*; - - /// API endpoint latency for hot paths, optimized for P50/P95/P99. - pub static API_HOT_PATH_LATENCY: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "api_hot_path_latency_seconds", - "API hot path latency in seconds (P50/P95/P99)", - ) - .buckets(vec![ - 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, - ]), - &["endpoint", "method"], - ) - }); - - /// Database query latency for hot paths. - pub static DB_HOT_PATH_LATENCY: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "db_hot_path_latency_seconds", - "Database hot path query latency in seconds (P50/P95/P99)", - ) - .buckets(vec![ - 0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, - ]), - &["query_name", "table"], - ) - }); - - /// Redis operation latency for hot paths. - pub static REDIS_HOT_PATH_LATENCY: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - histogram_vec( - HistogramOpts::new( - "redis_hot_path_latency_seconds", - "Redis hot path operation latency in seconds (P50/P95/P99)", - ) - .buckets(vec![0.0005, 0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1]), - &["operation", "key_pattern"], - ) - }); -} - -/// Tracing and observability. -pub mod tracing_spans {} - /// Expose metrics in Prometheus format pub fn gather_metrics() -> String { logging::sync_dropped_lines(&crate::logging::dropped_lines_by_component()); @@ -1307,22 +826,13 @@ mod tests { http::WEBSOCKET_CONNECTIONS_TOTAL .with_label_values(&["success"]) .inc(); - http::WEBSOCKET_MESSAGES_TOTAL - .with_label_values(&["inbound", "binary"]) - .inc(); http::WEBSOCKET_ERRORS_TOTAL .with_label_values(&["_test"]) .inc(); - http::WEBSOCKET_CONNECTION_DURATION_SECONDS - .with_label_values(&[] as &[&str]) - .observe(1.0); application::ROOMS_ACTIVE.set(0); application::USERS_ONLINE.set(0); application::STREAMS_ACTIVE.set(0); application::WEBRTC_PEERS_ACTIVE.set(0); - application::PLAYLIST_ITEMS_TOTAL - .with_label_values(&[] as &[&str]) - .inc(); application::CHAT_MESSAGES_TOTAL .with_label_values(&[] as &[&str]) .inc(); @@ -1351,18 +861,6 @@ mod tests { .observe(0.0); livestream::LIVESTREAM_ACTIVE_PUBLISHERS.set(0); livestream::LIVESTREAM_ACTIVE_VIEWERS.set(0); - livestream::LIVESTREAM_BYTES_TOTAL - .with_label_values(&["in"]) - .inc_by(0.0); - livestream::LIVESTREAM_STREAM_DURATION_SECONDS - .with_label_values(&["_test"]) - .observe(0.0); - livestream::LIVESTREAM_PULL_ERRORS_TOTAL - .with_label_values(&["_test"]) - .inc(); - livestream::GOP_CACHE_SIZE.set(0); - livestream::GOP_CACHE_DROPS_TOTAL.inc(); - livestream::GOP_CACHE_MEMORY_BYTES.set(0); logging::sync_dropped_lines(&[("_test".to_string(), 0)]); let output = gather_metrics(); @@ -1376,18 +874,10 @@ mod tests { output.contains("websocket_connections_total"), "Missing websocket_connections_total" ); - assert!( - output.contains("websocket_messages_total"), - "Missing websocket_messages_total" - ); assert!( output.contains("websocket_errors_total"), "Missing websocket_errors_total" ); - assert!( - output.contains("websocket_connection_duration_seconds"), - "Missing websocket_connection_duration_seconds" - ); assert!( output.contains("logging_dropped_lines_total"), "Missing logging_dropped_lines_total" @@ -1399,10 +889,6 @@ mod tests { output.contains("webrtc_peers_active"), "Missing webrtc_peers_active" ); - assert!( - output.contains("playlist_items_total"), - "Missing playlist_items_total" - ); assert!( output.contains("chat_messages_total"), "Missing chat_messages_total" @@ -1443,28 +929,6 @@ mod tests { output.contains("livestream_active_viewers"), "Missing livestream_active_viewers" ); - assert!( - output.contains("livestream_bytes_total"), - "Missing livestream_bytes_total" - ); - assert!( - output.contains("livestream_stream_duration_seconds"), - "Missing livestream_stream_duration_seconds" - ); - assert!( - output.contains("livestream_pull_errors_total"), - "Missing livestream_pull_errors_total" - ); - assert!(output.contains("gop_cache_size"), "Missing gop_cache_size"); - assert!( - output.contains("gop_cache_drops_total"), - "Missing gop_cache_drops_total" - ); - assert!( - output.contains("gop_cache_memory_bytes"), - "Missing gop_cache_memory_bytes" - ); - // Database metrics assert!( output.contains("db_connections_active"), diff --git a/synctv-core/src/models/audit.rs b/synctv-core/src/models/audit.rs index 2b04652a..747ecb5f 100644 --- a/synctv-core/src/models/audit.rs +++ b/synctv-core/src/models/audit.rs @@ -206,7 +206,7 @@ pub enum AuditAction { MemberKicked, MemberRoleUpdated, MemberPermissionUpdated, - MemberStatusUpdated, + MembershipUpdated, RoomSettingsUpdated, UserApproved, RoomApproved, @@ -253,7 +253,7 @@ impl AuditAction { Self::MemberKicked => "member_kicked", Self::MemberRoleUpdated => "member_role_updated", Self::MemberPermissionUpdated => "member_permission_updated", - Self::MemberStatusUpdated => "member_status_updated", + Self::MembershipUpdated => "membership_updated", Self::RoomSettingsUpdated => "room_settings_updated", Self::UserApproved => "user_approved", Self::RoomApproved => "room_approved", @@ -301,7 +301,7 @@ impl AuditAction { Self::MemberKicked => 22, Self::MemberRoleUpdated => 23, Self::MemberPermissionUpdated => 24, - Self::MemberStatusUpdated => 25, + Self::MembershipUpdated => 25, Self::RoomSettingsUpdated => 26, Self::UserApproved => 27, Self::RoomApproved => 28, @@ -356,7 +356,7 @@ impl TryFrom for AuditAction { 22 => Ok(Self::MemberKicked), 23 => Ok(Self::MemberRoleUpdated), 24 => Ok(Self::MemberPermissionUpdated), - 25 => Ok(Self::MemberStatusUpdated), + 25 => Ok(Self::MembershipUpdated), 26 => Ok(Self::RoomSettingsUpdated), 27 => Ok(Self::UserApproved), 28 => Ok(Self::RoomApproved), @@ -406,7 +406,7 @@ impl FromStr for AuditAction { "member_kicked" => Ok(Self::MemberKicked), "member_role_updated" => Ok(Self::MemberRoleUpdated), "member_permission_updated" => Ok(Self::MemberPermissionUpdated), - "member_status_updated" => Ok(Self::MemberStatusUpdated), + "membership_updated" => Ok(Self::MembershipUpdated), "room_settings_updated" => Ok(Self::RoomSettingsUpdated), "user_approved" => Ok(Self::UserApproved), "room_approved" => Ok(Self::RoomApproved), diff --git a/synctv-core/src/models/chat.rs b/synctv-core/src/models/chat.rs index 92692e45..bf35e5a4 100644 --- a/synctv-core/src/models/chat.rs +++ b/synctv-core/src/models/chat.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; use std::str::FromStr; use super::{ @@ -265,14 +264,6 @@ impl ChatMetadata { Self::MemberJoined(_) | Self::PlaybackChanged(_) => None, } } - - #[must_use] - pub fn user_mut(&mut self) -> Option<&mut ChatUserMetadata> { - match self { - Self::User(metadata) => Some(metadata), - Self::MemberJoined(_) | Self::PlaybackChanged(_) => None, - } - } } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -432,12 +423,6 @@ impl ChatMessageSelection { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SendChatRequest { - pub room_id: RoomId, - pub content: String, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatAttachment { @@ -498,20 +483,6 @@ pub struct CreateChatAttachmentUploadSession { pub metadata: super::file_storage::FileMetadata, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatAttachmentUploadSession { - pub attachment: super::file_storage::NewStoredFile, - pub upload_required: bool, - pub ownership_proof_required: bool, - pub ownership_proof_nonce: Option, - pub ownership_proof_ranges: Vec, - pub upload_url: Option, - pub upload_method: Option, - pub upload_headers: BTreeMap, - pub expires_at: Option>, - pub max_size_bytes: i64, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatMessageWithAttachments { @@ -619,17 +590,6 @@ pub struct DeleteChatMessage { pub expected_version: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatReaction { - pub room_id: RoomId, - pub message_id: i64, - pub message_created_at: DateTime, - pub user_id: UserId, - pub reaction_key: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChatReactionSummary { diff --git a/synctv-core/src/models/file_storage.rs b/synctv-core/src/models/file_storage.rs index 6238459c..2f49a8b3 100644 --- a/synctv-core/src/models/file_storage.rs +++ b/synctv-core/src/models/file_storage.rs @@ -633,14 +633,6 @@ pub enum FileUploadSessionCreateResult { } impl FileUploadSessionCreateResult { - #[must_use] - pub const fn as_session(&self) -> Option<&FileUploadSession> { - match self { - Self::Session(session) => Some(session), - Self::Plan(_) => None, - } - } - #[must_use] pub fn into_session(self) -> Option { match self { @@ -648,22 +640,6 @@ impl FileUploadSessionCreateResult { Self::Plan(_) => None, } } - - #[must_use] - pub const fn as_plan(&self) -> Option<&FileUploadPlan> { - match self { - Self::Plan(plan) => Some(plan), - Self::Session(_) => None, - } - } - - #[must_use] - pub fn into_plan(self) -> Option { - match self { - Self::Plan(plan) => Some(plan), - Self::Session(_) => None, - } - } } #[cfg(test)] @@ -671,8 +647,12 @@ impl std::ops::Deref for FileUploadSessionCreateResult { type Target = FileUploadSession; fn deref(&self) -> &Self::Target { - self.as_session() - .expect("upload session create result should contain a session") + match self { + FileUploadSessionCreateResult::Session(session) => session, + FileUploadSessionCreateResult::Plan(_) => { + panic!("upload session create result should contain a session") + } + } } } @@ -753,17 +733,6 @@ pub struct GetFileObject { pub range: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileObjectData { - pub storage_backend: String, - pub object_key: String, - pub mime_type: String, - pub size_bytes: i64, - pub content_manifest_sha256: String, - pub data: Vec, - pub range: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompleteFileUploadSessionResult { pub object: Option, diff --git a/synctv-core/src/models/media.rs b/synctv-core/src/models/media.rs index 6c6ac464..820383af 100644 --- a/synctv-core/src/models/media.rs +++ b/synctv-core/src/models/media.rs @@ -83,8 +83,6 @@ pub enum SourceProvider { TikTok, } -pub type ProviderType = SourceProvider; - impl FromStr for SourceProvider { type Err = String; @@ -241,15 +239,6 @@ pub fn provider_type_name_from_code(code: i16) -> Result { SourceProvider::try_from(code).map(|provider| provider.to_string()) } -pub fn provider_type_codes_from_names<'a>( - names: impl IntoIterator, -) -> Result, String> { - names - .into_iter() - .map(|name| provider_type_code_from_name(name)) - .collect() -} - impl std::fmt::Display for SourceProvider { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) @@ -296,17 +285,6 @@ pub struct FromProviderParams { pub position: f64, } -#[derive(Debug, Clone)] -pub struct DirectMultimodeParams { - pub playlist_id: Option, - pub room_id: RoomId, - pub creator_id: Option, - pub name: String, - pub playback_infos: HashMap, - pub default_mode: String, - pub position: f64, -} - impl Media { #[must_use] pub fn from_provider_with_params(params: FromProviderParams) -> Self { @@ -332,74 +310,6 @@ impl Media { } } - pub fn from_direct_multimode(params: DirectMultimodeParams) -> crate::Result { - let default_info = params - .playback_infos - .get(¶ms.default_mode) - .or_else(|| params.playback_infos.values().next()); - let default_media = default_info.and_then(|info| { - info.default_media_index - .and_then(|index| info.medias.get(index)) - .or_else(|| info.medias.first()) - }); - let default_url = default_media - .and_then(PlaybackMedia::direct_url) - .ok_or_else(|| { - crate::Error::InvalidInput("direct media requires a playback URL".to_string()) - })?; - let default_headers = default_media.map_or_else( - std::collections::HashMap::new, - PlaybackMedia::upstream_headers, - ); - - let source_config = super::MediaSourceConfig::DirectUrl( - super::DirectUrlMediaSourceConfig::single(default_url.to_string(), default_headers), - ); - - let now = crate::SystemClock.now(); - Ok(Self { - id: MediaId::new(), - playlist_id: params.playlist_id, - room_id: params.room_id, - creator_id: params.creator_id, - name: params.name, - description: String::new(), - position: params.position, - source_provider: SourceProvider::DirectUrl, - source_config, - provider_instance_name: None, - cover_file_reference_id: None, - thumbnail_file_reference_id: None, - added_at: now, - updated_at: now, - version: 0, - }) - } - - /// Create a direct URL media with single playback info (convenience method) - pub fn from_direct_single_mode( - playlist_id: Option, - room_id: RoomId, - creator_id: Option, - name: String, - mode_name: &str, - playback_info: PlaybackInfo, - position: f64, - ) -> crate::Result { - let mut playback_infos = HashMap::new(); - playback_infos.insert(mode_name.to_string(), playback_info); - - Self::from_direct_multimode(DirectMultimodeParams { - playlist_id, - room_id, - creator_id, - name, - playback_infos, - default_mode: mode_name.to_string(), - position, - }) - } - #[must_use] pub fn cover_file_reference_target( &self, @@ -2048,14 +1958,6 @@ impl PlaybackMetadata { _ => None, } } - - #[must_use] - pub const fn as_live(&self) -> Option<&LivePlaybackMetadata> { - match self { - Self::Live(metadata) => Some(metadata), - _ => None, - } - } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -2721,33 +2623,6 @@ pub struct SynologySubtitleMetadata { // Helper implementations impl PlaybackResult { - /// Create a `PlaybackResult` from Media and single mode `PlaybackInfo` - #[must_use] - pub fn from_media_single_mode( - media: &Media, - mode_name: &str, - playback_info: PlaybackInfo, - ) -> Self { - let mut playback_infos = std::collections::HashMap::new(); - playback_infos.insert(mode_name.to_string(), playback_info); - - Self { - id: Some(media.id), - playlist_id: media.playlist_id, - room_id: media.room_id, - name: media.name.clone(), - provider: media.source_provider, - provider_instance_name: media.provider_instance_name.clone(), - position: media.position, - playback_infos, - default_mode: mode_name.to_string(), - duration_seconds: None, - playback_kind: PlaybackKind::Regular, - target: None, - metadata: None, - } - } - /// Create a new builder #[must_use] pub fn builder( @@ -2772,19 +2647,6 @@ impl PlaybackResult { metadata: None, } } - - /// Replace metadata. - #[must_use] - pub fn with_metadata(mut self, metadata: PlaybackMetadata) -> Self { - self.metadata = Some(metadata); - self - } - - /// Get the default playback info - #[must_use] - pub fn get_default_playback_info(&self) -> Option<&PlaybackInfo> { - self.playback_infos.get(&self.default_mode) - } } /// Builder for `PlaybackResult` @@ -2988,20 +2850,6 @@ impl PlaybackInfoBuilder { } impl PlaybackMedia { - #[must_use] - pub fn direct_url(&self) -> Option<&str> { - match &self.provider { - PlaybackMediaProvider::Cloudreve(PlaybackCloudreveMedia::Direct { url, .. }) - | PlaybackMediaProvider::DirectUrl(PlaybackDirectUrlMedia::Direct { url, .. }) => { - Some(url) - } - PlaybackMediaProvider::Alist(PlaybackAlistMedia::Direct { url, .. }) - | PlaybackMediaProvider::Bilibili(PlaybackBilibiliMedia::Direct { url, .. }) - | PlaybackMediaProvider::Emby(PlaybackEmbyMedia::Direct { url, .. }) => Some(url), - _ => None, - } - } - #[must_use] pub fn upstream_url(&self) -> Option<&str> { match &self.provider { diff --git a/synctv-core/src/models/mod.rs b/synctv-core/src/models/mod.rs index 8d31abf9..c306bcd0 100644 --- a/synctv-core/src/models/mod.rs +++ b/synctv-core/src/models/mod.rs @@ -116,22 +116,21 @@ pub mod validation; pub use audit::{AuditAction, AuditDetails, AuditTargetType, AuditUpdatedFields}; pub use chat::{ - ChatAttachment, ChatAttachmentKind, ChatAttachmentUploadSession, ChatEventKind, - ChatHistoryCursor, ChatHistoryPage, ChatMemberJoinedMetadata, ChatMention, ChatMentionInput, - ChatMessage, ChatMessageContext, ChatMessageEvent, ChatMessageEventLog, - ChatMessageOperationKind, ChatMessagePin, ChatMessageReadReceiptMember, - ChatMessageReadReceiptUser, ChatMessageReadReceiptsPage, ChatMessageSelection, - ChatMessageStatus, ChatMessageType, ChatMessageWithAttachments, ChatMetadata, ChatPinEvent, - ChatPinEventKind, ChatPinEventLog, ChatPinnedMessage, ChatPlaybackChangedMetadata, - ChatPlaybackMessagesQuery, ChatPlaybackMetadata, ChatPresentationMetadata, ChatReaction, - ChatReactionSummary, ChatReactionUser, ChatReactionUsersCursor, ChatReactionUsersPage, - ChatReadState, ChatReadStateWithUnread, ChatSearchMessagesPage, ChatSearchMessagesQuery, - ChatUserMetadata, CreateChatAttachmentUploadSession, DeleteChatMessage, EditChatMessage, - EventCursor, MarkChatRead, PinChatMessage, PlaybackChangeReason, SendChatMessage, - SendChatRequest, SetChatReaction, UnpinChatMessage, CHAT_ATTACHMENT_FILENAME_MAX_CHARS, - CHAT_ATTACHMENT_ID_MAX_CHARS, CHAT_CLIENT_MESSAGE_ID_MAX_CHARS, - CHAT_CLIENT_OPERATION_ID_MAX_CHARS, CHAT_EVENT_ID_MAX_CHARS, CHAT_EVENT_TYPE_MAX_CHARS, - CHAT_PIN_NOTE_MAX_CHARS, CHAT_REACTION_KEY_MAX_CHARS, + ChatAttachment, ChatAttachmentKind, ChatEventKind, ChatHistoryCursor, ChatHistoryPage, + ChatMemberJoinedMetadata, ChatMention, ChatMentionInput, ChatMessage, ChatMessageContext, + ChatMessageEvent, ChatMessageEventLog, ChatMessageOperationKind, ChatMessagePin, + ChatMessageReadReceiptMember, ChatMessageReadReceiptUser, ChatMessageReadReceiptsPage, + ChatMessageSelection, ChatMessageStatus, ChatMessageType, ChatMessageWithAttachments, + ChatMetadata, ChatPinEvent, ChatPinEventKind, ChatPinEventLog, ChatPinnedMessage, + ChatPlaybackChangedMetadata, ChatPlaybackMessagesQuery, ChatPlaybackMetadata, + ChatPresentationMetadata, ChatReactionSummary, ChatReactionUser, ChatReactionUsersCursor, + ChatReactionUsersPage, ChatReadState, ChatReadStateWithUnread, ChatSearchMessagesPage, + ChatSearchMessagesQuery, ChatUserMetadata, CreateChatAttachmentUploadSession, + DeleteChatMessage, EditChatMessage, EventCursor, MarkChatRead, PinChatMessage, + PlaybackChangeReason, SendChatMessage, SetChatReaction, UnpinChatMessage, + CHAT_ATTACHMENT_FILENAME_MAX_CHARS, CHAT_ATTACHMENT_ID_MAX_CHARS, + CHAT_CLIENT_MESSAGE_ID_MAX_CHARS, CHAT_CLIENT_OPERATION_ID_MAX_CHARS, CHAT_EVENT_ID_MAX_CHARS, + CHAT_EVENT_TYPE_MAX_CHARS, CHAT_PIN_NOTE_MAX_CHARS, CHAT_REACTION_KEY_MAX_CHARS, }; pub use content_report::{ ContentReport, ContentReportAdminRow, ContentReportMetadata, ContentReportStatus, @@ -142,13 +141,13 @@ pub use file_storage::{ CompleteFileUploadPart, CompleteFileUploadSession, CompleteFileUploadSessionResult, CreateFileUploadSession, FileAudioMetadata, FileBlob, FileBlobCompression, FileBlobPart, FileByteRange, FileCleanupJob, FileCleanupMetadata, FileMetadata, FileObject, FileObjectAccess, - FileObjectData, FileObjectDownload, FileObjectGroup, FileObjectKind, FileObjectMetadata, - FileObjectVariant, FileOwnershipProofRange, FileRangeRequest, FileReferenceMetadata, - FileReferenceTarget, FileUploadManifestPart, FileUploadOwnershipProofMetadata, - FileUploadPartUrl, FileUploadPlan, FileUploadPlanPart, FileUploadPolicy, FileUploadRange, - FileUploadSession, FileUploadSessionCreateResult, FileUploadSessionKind, - FileUploadSessionMetadata, FileUploadSessionPart, FileUploadSessionRecord, FileVariantMetadata, - GetFileObject, NewStoredFile, StoreFileUpload, StoreFileUploadResult, StoredFileReference, + FileObjectDownload, FileObjectGroup, FileObjectKind, FileObjectMetadata, FileObjectVariant, + FileOwnershipProofRange, FileRangeRequest, FileReferenceMetadata, FileReferenceTarget, + FileUploadManifestPart, FileUploadOwnershipProofMetadata, FileUploadPartUrl, FileUploadPlan, + FileUploadPlanPart, FileUploadPolicy, FileUploadRange, FileUploadSession, + FileUploadSessionCreateResult, FileUploadSessionKind, FileUploadSessionMetadata, + FileUploadSessionPart, FileUploadSessionRecord, FileVariantMetadata, GetFileObject, + NewStoredFile, StoreFileUpload, StoreFileUploadResult, StoredFileReference, SubmittedFileReference, SubmittedFileReferenceKind, FILE_CLEANUP_ORIGIN_MAX_CHARS, FILE_ID_MAX_CHARS, FILE_OBJECT_KEY_MAX_CHARS, FILE_REFERENCE_ID_MAX_CHARS, FILE_REFERENCE_KIND_MAX_CHARS, FILE_SHA256_HEX_CHARS, FILE_STORAGE_BACKEND_MAX_CHARS, @@ -160,36 +159,35 @@ pub use id::{ }; pub use lifecycle::DeletionSource; pub use media::{ - provider_type_code_from_name, provider_type_codes_from_names, provider_type_name_from_code, - AcFunPlaybackFormat, AcFunPlaybackMetadata, AcFunPlaybackResourceKind, AlistPlaybackMetadata, + provider_type_code_from_name, provider_type_name_from_code, AcFunPlaybackFormat, + AcFunPlaybackMetadata, AcFunPlaybackResourceKind, AlistPlaybackMetadata, AlistTranscodingTaskMetadata, AlistVideoPreviewMetadata, BilibiliDashAudioStream, BilibiliDashManifest, BilibiliDashManifestSlot, BilibiliDashManifests, BilibiliDashSegmentBase, BilibiliDashVideoStream, BilibiliPlaybackKind, BilibiliPlaybackMetadata, CctvChapterMetadata, - CctvPlaybackMetadata, CctvPlaybackStreamKind, DirectMultimodeParams, DirectUrlPlaybackMetadata, - DouyinPlaybackKind, DouyinPlaybackMetadata, DouyinPlaybackResource, DouyuPlaybackCodec, - DouyuPlaybackFormat, DouyuPlaybackMetadata, EmbyPlaybackKind, EmbyPlaybackMetadata, - FnosAudioTrackMetadata, FnosFilePlaybackMetadata, FnosMediaPlaybackMetadata, - FnosPlaybackMetadata, FnosProxyResource, FnosSubtitleTrackMetadata, FnosTranscodeResource, - FromProviderParams, HuyaPlaybackFormat, HuyaPlaybackMetadata, HuyaPlaybackResourceKind, - LivePlaybackMetadata, LiveProxyPlaybackMetadata, Media, MediaListQuery, MediaListSortBy, - NextcloudPlaybackMetadata, PlaybackAcFunDanmaku, PlaybackAcFunMedia, PlaybackAlistMedia, - PlaybackAlistSubtitle, PlaybackBilibiliDanmaku, PlaybackBilibiliMedia, - PlaybackBilibiliSubtitle, PlaybackCctvMedia, PlaybackCloudreveMedia, PlaybackCloudreveSubtitle, - PlaybackDanmaku, PlaybackDanmakuProvider, PlaybackDirectUrlDanmaku, PlaybackDirectUrlMedia, - PlaybackDirectUrlSubtitle, PlaybackDouyinDanmaku, PlaybackDouyinMedia, PlaybackDouyuDanmaku, - PlaybackDouyuMedia, PlaybackEmbyMedia, PlaybackEmbySubtitle, PlaybackFnosMedia, - PlaybackFnosSubtitle, PlaybackHuyaDanmaku, PlaybackHuyaMedia, PlaybackInfo, - PlaybackLiveProxyMedia, PlaybackMedia, PlaybackMediaMetadata, PlaybackMediaProvider, - PlaybackMetadata, PlaybackNextcloudMedia, PlaybackNextcloudSubtitle, PlaybackQnapMedia, - PlaybackQnapSubtitle, PlaybackResult, PlaybackRtmpMedia, PlaybackSeafileMedia, - PlaybackSeafileSubtitle, PlaybackSubtitle, PlaybackSubtitleProvider, PlaybackSynologyMedia, - PlaybackSynologySubtitle, PlaybackTikTokMedia, PlaybackTikTokSubtitle, PlaybackTrueNasMedia, - PlaybackTrueNasSubtitle, PlaybackTwitchDanmaku, PlaybackTwitchMedia, PlaybackYoutubeMedia, - PlaybackYoutubeSubtitle, ProviderType, QnapPlaybackMetadata, QnapPlaybackMode, - QnapPlaybackResource, SeafilePlaybackMetadata, SourceProvider, SynologyAudioTrackMetadata, - SynologyPlaybackMetadata, SynologyPlaybackProfile, SynologyPlaybackResource, - SynologySubtitleMetadata, TikTokPlaybackKind, TikTokPlaybackMetadata, TikTokPlaybackResource, - TrueNasPlaybackMetadata, TwitchChapterMetadata, TwitchPlaybackMetadata, + CctvPlaybackMetadata, CctvPlaybackStreamKind, DirectUrlPlaybackMetadata, DouyinPlaybackKind, + DouyinPlaybackMetadata, DouyinPlaybackResource, DouyuPlaybackCodec, DouyuPlaybackFormat, + DouyuPlaybackMetadata, EmbyPlaybackKind, EmbyPlaybackMetadata, FnosAudioTrackMetadata, + FnosFilePlaybackMetadata, FnosMediaPlaybackMetadata, FnosPlaybackMetadata, FnosProxyResource, + FnosSubtitleTrackMetadata, FnosTranscodeResource, FromProviderParams, HuyaPlaybackFormat, + HuyaPlaybackMetadata, HuyaPlaybackResourceKind, LivePlaybackMetadata, + LiveProxyPlaybackMetadata, Media, MediaListQuery, MediaListSortBy, NextcloudPlaybackMetadata, + PlaybackAcFunDanmaku, PlaybackAcFunMedia, PlaybackAlistMedia, PlaybackAlistSubtitle, + PlaybackBilibiliDanmaku, PlaybackBilibiliMedia, PlaybackBilibiliSubtitle, PlaybackCctvMedia, + PlaybackCloudreveMedia, PlaybackCloudreveSubtitle, PlaybackDanmaku, PlaybackDanmakuProvider, + PlaybackDirectUrlDanmaku, PlaybackDirectUrlMedia, PlaybackDirectUrlSubtitle, + PlaybackDouyinDanmaku, PlaybackDouyinMedia, PlaybackDouyuDanmaku, PlaybackDouyuMedia, + PlaybackEmbyMedia, PlaybackEmbySubtitle, PlaybackFnosMedia, PlaybackFnosSubtitle, + PlaybackHuyaDanmaku, PlaybackHuyaMedia, PlaybackInfo, PlaybackLiveProxyMedia, PlaybackMedia, + PlaybackMediaMetadata, PlaybackMediaProvider, PlaybackMetadata, PlaybackNextcloudMedia, + PlaybackNextcloudSubtitle, PlaybackQnapMedia, PlaybackQnapSubtitle, PlaybackResult, + PlaybackRtmpMedia, PlaybackSeafileMedia, PlaybackSeafileSubtitle, PlaybackSubtitle, + PlaybackSubtitleProvider, PlaybackSynologyMedia, PlaybackSynologySubtitle, PlaybackTikTokMedia, + PlaybackTikTokSubtitle, PlaybackTrueNasMedia, PlaybackTrueNasSubtitle, PlaybackTwitchDanmaku, + PlaybackTwitchMedia, PlaybackYoutubeMedia, PlaybackYoutubeSubtitle, QnapPlaybackMetadata, + QnapPlaybackMode, QnapPlaybackResource, SeafilePlaybackMetadata, SourceProvider, + SynologyAudioTrackMetadata, SynologyPlaybackMetadata, SynologyPlaybackProfile, + SynologyPlaybackResource, SynologySubtitleMetadata, TikTokPlaybackKind, TikTokPlaybackMetadata, + TikTokPlaybackResource, TrueNasPlaybackMetadata, TwitchChapterMetadata, TwitchPlaybackMetadata, TwitchPlaybackResourceKind, YoutubePlaybackMetadata, YoutubePlaybackResource, }; pub use notification::{ @@ -197,8 +195,7 @@ pub use notification::{ NotificationData, NotificationListQuery, NotificationListSortBy, NotificationType, }; pub use oauth2_client::{ - oauth2_provider_type_code_from_name, oauth2_provider_type_name_from_code, - OAuth2AuthUrlResponse, OAuth2CallbackRequest, OAuth2CallbackResponse, OAuth2Provider, + oauth2_provider_type_code_from_name, oauth2_provider_type_name_from_code, OAuth2Provider, OAuth2UserInfo, UserOAuthProviderMapping, }; pub use opaque_password::{ @@ -246,12 +243,11 @@ pub use realtime_event::{ }; pub use review::ReviewStatus; pub use room::{ - AutoPlaySettings, CreateRoomRequest, PlayMode, Room, RoomCategory, RoomLabel, RoomListQuery, - RoomListSortBy, RoomStatus, RoomWithCount, UpdateRoomRequest, UpsertRoomCategory, - UpsertRoomLabel, + AutoPlaySettings, PlayMode, Room, RoomCategory, RoomLabel, RoomListQuery, RoomListSortBy, + RoomStatus, RoomWithCount, UpsertRoomCategory, UpsertRoomLabel, }; pub use room_member::{ - AddMemberOptions, MemberStatus, MyRoomListQuery, MyRoomListSortBy, MyRoomRelation, RoomMember, + AddMemberOptions, MyRoomListQuery, MyRoomListSortBy, MyRoomRelation, RoomMember, RoomMemberListQuery, RoomMemberListSortBy, RoomMemberWithUser, }; pub use room_settings::RoomSettings; @@ -279,8 +275,7 @@ pub use source_config::{ YoutubePlaylistSourceConfig, }; pub use user::{ - CreateUserRequest, SignupMethod, UpdateUserRequest, User, UserLifecycleMetadata, UserListQuery, - UserListSortBy, UserRole, UserStatus, + SignupMethod, User, UserLifecycleMetadata, UserListQuery, UserListSortBy, UserRole, UserStatus, }; pub use user_preferences::{ UserAuthFactors, UserNotificationPreferences, UserPreferences, UserPreferencesUpdate, diff --git a/synctv-core/src/models/oauth2_client.rs b/synctv-core/src/models/oauth2_client.rs index aa8ba788..ffc49017 100644 --- a/synctv-core/src/models/oauth2_client.rs +++ b/synctv-core/src/models/oauth2_client.rs @@ -196,27 +196,6 @@ pub struct OAuth2UserInfo { pub avatar: Option, } -/// `OAuth2` authorization URL response -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OAuth2AuthUrlResponse { - pub url: String, - pub state: String, -} - -/// `OAuth2` callback request -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OAuth2CallbackRequest { - pub code: String, - pub state: String, -} - -/// `OAuth2` callback response -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OAuth2CallbackResponse { - pub token: Option, // JWT token if login - pub redirect: Option, // Redirect URL -} - #[cfg(test)] mod tests { use super::*; diff --git a/synctv-core/src/models/permission.rs b/synctv-core/src/models/permission.rs index 9aaa9399..857560d4 100644 --- a/synctv-core/src/models/permission.rs +++ b/synctv-core/src/models/permission.rs @@ -716,15 +716,6 @@ impl Role { Self::Guest => RoomGuestPermissionBits::from_permissions(permissions), } } - - #[must_use] - pub const fn permissions_from_override_bits(&self, bits: u64) -> u64 { - match self { - Self::Creator | Self::Admin => RoomAdminPermissionBits::to_permissions(bits), - Self::Member => RoomMemberPermissionBits::to_permissions(bits), - Self::Guest => RoomGuestPermissionBits::to_permissions(bits), - } - } } impl FromStr for Role { diff --git a/synctv-core/src/models/playback.rs b/synctv-core/src/models/playback.rs index b10f3292..d955c883 100644 --- a/synctv-core/src/models/playback.rs +++ b/synctv-core/src/models/playback.rs @@ -24,12 +24,6 @@ i16_enum!(PlaybackKind, "invalid playback kind", { }); impl PlaybackKind { - /// Whether a newly selected source should enter at its live edge. - #[must_use] - pub const fn starts_at_live_edge(self) -> bool { - matches!(self, Self::Live) - } - /// Whether the source exposes a seekable presentation timeline. #[must_use] pub const fn is_seekable(self) -> bool { @@ -326,7 +320,6 @@ mod tests { assert!(!PlaybackKind::Live.accepts_position_updates()); assert!(PlaybackKind::Regular.supports_duration()); assert!(PlaybackKind::Regular.allows_auto_advance()); - assert!(PlaybackKind::Live.starts_at_live_edge()); assert!(!PlaybackKind::Live.is_seekable()); assert!(PlaybackKind::Regular.is_seekable()); } diff --git a/synctv-core/src/models/playlist.rs b/synctv-core/src/models/playlist.rs index 8ef2c153..3f74df3d 100644 --- a/synctv-core/src/models/playlist.rs +++ b/synctv-core/src/models/playlist.rs @@ -115,12 +115,6 @@ impl Playlist { self.source_provider.is_some() } - /// Check if this is a static playlist. - #[must_use] - pub const fn is_static(&self) -> bool { - self.source_provider.is_none() - } - #[must_use] pub const fn effective_browse_access_mode(&self) -> PlaylistBrowseAccessMode { match self.browse_access_mode { diff --git a/synctv-core/src/models/room.rs b/synctv-core/src/models/room.rs index 564187b9..32596b00 100644 --- a/synctv-core/src/models/room.rs +++ b/synctv-core/src/models/room.rs @@ -5,7 +5,6 @@ use std::str::FromStr; use super::id::{RoomCategoryId, RoomId, RoomLabelId, UserId}; use super::query::SortDirection; -use super::RoomSettings; use crate::Error; fn default_last_activity_at() -> DateTime { @@ -335,33 +334,6 @@ impl Room { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateRoomRequest { - pub name: String, - /// Room description (max 500 characters) - #[serde(default)] - pub description: String, - pub password: Option, - pub settings: Option, - pub category_id: Option, - #[serde(default)] - pub label_ids: Vec, - #[serde(default = "default_enabled")] - pub is_public: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateRoomRequest { - pub name: Option, - /// Room description (max 500 characters) - pub description: Option, - pub closed: Option, - pub settings: Option, - pub category_id: Option, - pub label_ids: Option>, - pub is_public: Option, -} - sort_field_enum! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] diff --git a/synctv-core/src/models/room_member.rs b/synctv-core/src/models/room_member.rs index a97cfca7..d61e3c60 100644 --- a/synctv-core/src/models/room_member.rs +++ b/synctv-core/src/models/room_member.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use std::str::FromStr; use super::id::{RoomId, UserId}; use super::permission::{ @@ -10,65 +9,8 @@ use super::permission::{ use super::query::SortDirection; use super::room::RoomStatus; -/// Current room membership state. -/// -/// `room_members` is active-only: if a row exists, the user is currently a -/// member. Leave, kick, and user purge physically delete rows. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "lowercase")] -pub enum MemberStatus { - #[default] - Active, -} - -impl MemberStatus { - #[must_use] - pub const fn as_str(&self) -> &'static str { - "active" - } - - #[must_use] - pub const fn is_active(&self) -> bool { - true - } -} - -impl FromStr for MemberStatus { - type Err = String; - - fn from_str(raw: &str) -> Result { - match raw.trim().to_ascii_lowercase().as_str() { - "active" => Ok(Self::Active), - other => Err(format!("Unknown member status: {other}")), - } - } -} - -impl std::fmt::Display for MemberStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -impl From for i32 { - fn from(_: MemberStatus) -> Self { - 1 - } -} - -impl TryFrom for MemberStatus { - type Error = String; - - fn try_from(value: i32) -> Result { - match value { - 1 => Ok(Self::Active), - _ => Err(format!("Unknown member status: {value}")), - } - } -} - /// Repository-level options for admitting a user into a room membership row. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct AddMemberOptions { /// Check if room is active pub check_room_active: bool, @@ -83,18 +25,6 @@ pub struct AddMemberOptions { } impl AddMemberOptions { - /// Create default options (all checks enabled, no max limit) - #[must_use] - pub const fn new() -> Self { - Self { - check_room_active: true, - check_duplicate: true, - check_max_members: false, - max_members: 0, - invalidate_cache: true, - } - } - /// Set max members limit (enables the check) #[must_use] pub const fn with_max_members(mut self, max: u64) -> Self { @@ -102,33 +32,17 @@ impl AddMemberOptions { self.check_max_members = true; self } +} - /// Skip max members check - #[must_use] - pub const fn skip_max_members_check(mut self) -> Self { - self.check_max_members = false; - self - } - - /// Skip room active check - #[must_use] - pub const fn skip_active_check(mut self) -> Self { - self.check_room_active = false; - self - } - - /// Skip duplicate membership check - #[must_use] - pub const fn skip_duplicate_check(mut self) -> Self { - self.check_duplicate = false; - self - } - - /// Skip cache invalidation - #[must_use] - pub const fn skip_cache_invalidation(mut self) -> Self { - self.invalidate_cache = false; - self +impl Default for AddMemberOptions { + fn default() -> Self { + Self { + check_room_active: true, + check_duplicate: true, + check_max_members: false, + max_members: 0, + invalidate_cache: true, + } } } @@ -234,8 +148,6 @@ pub struct RoomMember { pub room_id: RoomId, pub user_id: UserId, pub role: RoomRole, - #[serde(skip)] - pub status: MemberStatus, pub added_permissions: u64, pub removed_permissions: u64, pub admin_added_permissions: u64, @@ -253,7 +165,6 @@ impl RoomMember { room_id, user_id, role, - status: MemberStatus::Active, added_permissions: 0, removed_permissions: 0, admin_added_permissions: 0, @@ -265,16 +176,6 @@ impl RoomMember { } } - #[must_use] - pub const fn is_active(&self) -> bool { - true - } - - #[must_use] - pub const fn status(&self) -> MemberStatus { - MemberStatus::Active - } - #[must_use] pub const fn effective_permissions( &self, @@ -311,21 +212,6 @@ impl RoomMember { ) -> bool { self.effective_permissions(role_default).has(permission) } - - pub const fn add_permissions(&mut self, permissions: u64) { - self.added_permissions |= permissions; - } - - pub const fn remove_permissions(&mut self, permissions: u64) { - self.removed_permissions |= permissions; - } - - pub const fn reset_to_role_default(&mut self) { - self.added_permissions = 0; - self.removed_permissions = 0; - self.admin_added_permissions = 0; - self.admin_removed_permissions = 0; - } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -334,7 +220,6 @@ pub struct RoomMemberWithUser { pub user_id: UserId, pub username: String, pub role: RoomRole, - pub status: MemberStatus, pub added_permissions: u64, pub removed_permissions: u64, pub admin_added_permissions: u64, @@ -343,7 +228,6 @@ pub struct RoomMemberWithUser { pub display_tag: String, pub joined_at: DateTime, pub is_online: bool, - pub is_active: bool, } impl RoomMemberWithUser { @@ -353,7 +237,6 @@ impl RoomMemberWithUser { room_id: self.room_id, user_id: self.user_id, role: self.role, - status: self.status, added_permissions: self.added_permissions, removed_permissions: self.removed_permissions, admin_added_permissions: self.admin_added_permissions, @@ -433,19 +316,6 @@ mod tests { assert!(result.has(crate::models::RoomPermission::USE_VOICE_CHAT)); } - #[test] - fn member_status_i32_conversions_reject_unknown_input() { - assert_eq!(i32::from(MemberStatus::Active), 1); - assert!(MemberStatus::try_from(0).is_err()); - assert!(MemberStatus::try_from(2).is_err()); - } - - #[test] - fn test_member_is_active() { - let member = test_member(RoomRole::Member); - assert!(member.is_active()); - } - #[test] fn test_room_member_list_sort_by_as_sql() { assert_eq!(RoomMemberListSortBy::JoinedAt.as_sql(), "rm.joined_at"); diff --git a/synctv-core/src/models/source_config.rs b/synctv-core/src/models/source_config.rs index 93c7378c..1dbae2b8 100644 --- a/synctv-core/src/models/source_config.rs +++ b/synctv-core/src/models/source_config.rs @@ -16,8 +16,7 @@ pub enum PlaybackProxyMode { } impl PlaybackProxyMode { - #[must_use] - pub const fn is_auto(&self) -> bool { + const fn is_auto(&self) -> bool { matches!(self, Self::Auto) } diff --git a/synctv-core/src/models/user.rs b/synctv-core/src/models/user.rs index 26ff9321..f0453adc 100644 --- a/synctv-core/src/models/user.rs +++ b/synctv-core/src/models/user.rs @@ -409,20 +409,6 @@ impl User { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateUserRequest { - pub username: String, - pub email: Option, // Optional email - pub password: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateUserRequest { - pub username: Option, - pub email: Option>, // Option>: Some(None) means set to NULL, None means don't update - pub password: Option, -} - sort_field_enum! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] diff --git a/synctv-core/src/oauth2/mod.rs b/synctv-core/src/oauth2/mod.rs index 7453a111..f08029bd 100644 --- a/synctv-core/src/oauth2/mod.rs +++ b/synctv-core/src/oauth2/mod.rs @@ -51,12 +51,6 @@ impl OAuth2Authorization { pub fn without_pkce(auth_url: String) -> Self { Self::new(auth_url, String::new()) } - - #[must_use] - pub fn with_nonce(mut self, nonce: String) -> Self { - self.nonce = Some(nonce); - self - } } /// `OAuth2` provider trait @@ -163,13 +157,6 @@ impl ProviderRegistry { registry.insert(provider_type.to_string(), factory); } - /// Get a registered factory function by type. - #[must_use] - pub fn get_factory(&self, provider_type: &str) -> Option { - let registry = self.factories.read(); - registry.get(provider_type).cloned() - } - /// Create a provider instance with configuration. /// /// Looks up the factory function in the registry and calls it. @@ -178,9 +165,14 @@ impl ProviderRegistry { provider_type: &str, config: &OAuth2ProviderPrivateConfig, ) -> Result, Error> { - let factory = self.get_factory(provider_type).ok_or_else(|| { - Error::InvalidInput(format!("Unknown provider type: {provider_type}")) - })?; + let factory = self + .factories + .read() + .get(provider_type) + .cloned() + .ok_or_else(|| { + Error::InvalidInput(format!("Unknown provider type: {provider_type}")) + })?; factory(config) } } diff --git a/synctv-core/src/oauth2/providers/mod.rs b/synctv-core/src/oauth2/providers/mod.rs index 6c6dd775..63cf2512 100644 --- a/synctv-core/src/oauth2/providers/mod.rs +++ b/synctv-core/src/oauth2/providers/mod.rs @@ -203,16 +203,6 @@ pub(super) fn build_provider_http_client( .internal_with_err("Failed to build HTTP client") } -#[cfg(test)] -pub(super) fn build_oauth2_http_client_with_timeout( - timeout: Duration, - ssrf_guard: &synctv_common::ssrf::SsrfGuard, -) -> Result { - build_ssrf_safe_provider_client(timeout, ssrf_guard) - .map(OAuth2HttpClient::new) - .internal_with_err("Failed to build OAuth2 HTTP client") -} - pub(super) fn build_oauth2_http_client( ssrf_guard: &synctv_common::ssrf::SsrfGuard, ) -> Result, Error> { @@ -375,7 +365,9 @@ mod tests { #[tokio::test] async fn token_exchange_client_allows_localhost_but_request_still_fails_without_server() { let guard = synctv_common::ssrf::SsrfGuard::disabled(); - let http_client = build_oauth2_http_client_with_timeout(Duration::from_millis(50), &guard) + let http_client = build_ssrf_safe_provider_client(Duration::from_millis(50), &guard) + .map(OAuth2HttpClient::new) + .internal_with_err("Failed to build OAuth2 HTTP client") .checked("operation should succeed"); let client = BasicClient::new(ClientId::new("client_id".to_string())) diff --git a/synctv-core/src/oauth2/providers/oidc.rs b/synctv-core/src/oauth2/providers/oidc.rs index 092f5207..aef082a3 100644 --- a/synctv-core/src/oauth2/providers/oidc.rs +++ b/synctv-core/src/oauth2/providers/oidc.rs @@ -712,11 +712,12 @@ impl OidcProvider { )); } let (auth_url, _csrf_token) = request.url(); - let authorization = match pkce_verifier { + let mut authorization = match pkce_verifier { Some(pkce_verifier) => OAuth2Authorization::new(auth_url.to_string(), pkce_verifier), None => OAuth2Authorization::without_pkce(auth_url.to_string()), }; - Ok(authorization.with_nonce(nonce)) + authorization.nonce = Some(nonce); + Ok(authorization) } async fn fetch_jwks(&self, jwks_uri: &str) -> Result { diff --git a/synctv-core/src/provider/alist.rs b/synctv-core/src/provider/alist.rs index 4923cc1d..ba808d84 100644 --- a/synctv-core/src/provider/alist.rs +++ b/synctv-core/src/provider/alist.rs @@ -1295,7 +1295,10 @@ impl AlistProvider { pub fn otp_code_from_secret(otp_secret: Option<&str>) -> Result { otp_secret.map_or_else( || Ok(String::new()), - |secret| Self::current_otp_code(secret).map_err(ProviderError::InvalidConfig), + |secret| { + Self::otp_code_at_timestamp(secret, crate::SystemClock.now().timestamp()) + .map_err(ProviderError::InvalidConfig) + }, ) } @@ -1322,10 +1325,6 @@ impl AlistProvider { }) } - pub fn current_otp_code(otp_secret: &str) -> Result { - Self::otp_code_at_timestamp(otp_secret, crate::SystemClock.now().timestamp()) - } - pub fn otp_code_at_timestamp(otp_secret: &str, timestamp: i64) -> Result { let secret = Self::normalize_otp_secret(Some(otp_secret.to_string())) .ok_or_else(|| "Alist OTP secret must not be empty".to_string())?; @@ -3482,7 +3481,7 @@ mod tests { fn test_provider(client: Arc) -> TestResult { let defaults = ok( - ProviderClientManager::new_for_tests(), + ProviderClientManager::new(), "default provider clients should build", )?; let manager = ProviderClientManager::with_custom_clients( diff --git a/synctv-core/src/provider/bilibili.rs b/synctv-core/src/provider/bilibili.rs index d8a06fc0..e36c7cd7 100644 --- a/synctv-core/src/provider/bilibili.rs +++ b/synctv-core/src/provider/bilibili.rs @@ -47,7 +47,6 @@ use crate::service::RemoteProviderManager; use super::upstream_transport::bilibili as bilibili_upstream; -pub const DASH_MANIFEST_METADATA_KEY: &str = "bilibili_dash_manifests"; pub const LIVE_DANMAKU_FORMAT: &str = "synctv-bilibili-live"; pub const LIVE_DANMAKU_TRACK_NAME: &str = "Bilibili Live Danmaku"; const SMS_LOGIN_SESSION_TTL_SECONDS: i64 = 10 * 60; @@ -464,26 +463,6 @@ pub struct BilibiliUserInfoResponse { pub is_vip: bool, } -#[derive(Debug, Clone)] -pub struct BilibiliLiveDanmuInfoRequest { - pub room_id: u64, - pub cookies: HashMap, -} - -#[derive(Debug, Clone)] -pub struct BilibiliLiveDanmuHost { - pub host: String, - pub port: u32, - pub wss_port: u32, - pub ws_port: u32, -} - -#[derive(Debug, Clone)] -pub struct BilibiliLiveDanmuInfoResponse { - pub token: String, - pub host_list: Vec, -} - fn bilibili_match_request(req: BilibiliMatchRequest) -> bilibili_upstream::MatchReq { bilibili_upstream::MatchReq { url: req.url } } @@ -549,15 +528,6 @@ fn bilibili_user_info_request(req: BilibiliUserInfoRequest) -> bilibili_upstream } } -fn bilibili_live_danmu_info_request( - req: BilibiliLiveDanmuInfoRequest, -) -> bilibili_upstream::GetLiveDanmuInfoReq { - bilibili_upstream::GetLiveDanmuInfoReq { - cookies: req.cookies, - room_id: req.room_id, - } -} - impl BilibiliProvider { /// Provider type name constant. pub const NAME: &'static str = "bilibili"; @@ -1482,35 +1452,6 @@ impl BilibiliProvider { }) } - /// Get live danmaku server info for the WebSocket connection - pub async fn get_live_danmu_info_with_context( - &self, - req: BilibiliLiveDanmuInfoRequest, - instance_name: Option<&str>, - request_context: Option<&super::ExecutionControl>, - ) -> Result { - let client = self - .get_client_with_context(instance_name, request_context) - .await?; - let resp = client - .get_live_danmu_info(bilibili_live_danmu_info_request(req)) - .await - .map_err(ProviderError::from)?; - Ok(BilibiliLiveDanmuInfoResponse { - token: resp.token, - host_list: resp - .host_list - .into_iter() - .map(|host| BilibiliLiveDanmuHost { - host: host.host, - port: host.port, - wss_port: host.wss_port, - ws_port: host.ws_port, - }) - .collect(), - }) - } - fn page_info_from_provider(page_info: bilibili_upstream::VideoPageInfo) -> BilibiliPageInfo { let actors = if page_info.actors.is_empty() { Vec::new() diff --git a/synctv-core/src/provider/mod.rs b/synctv-core/src/provider/mod.rs index 64cdc7d6..84ab1f31 100644 --- a/synctv-core/src/provider/mod.rs +++ b/synctv-core/src/provider/mod.rs @@ -288,15 +288,14 @@ pub use bilibili::{ BilibiliCaptchaResponse, BilibiliDashManifestMode, BilibiliDashResourceRequest, BilibiliFavoriteFolder, BilibiliFollowedPgcPage, BilibiliFollowedPgcSeason, BilibiliHistoryItem, BilibiliHistoryPage, BilibiliHlsResourceRequest, BilibiliLiveArea, - BilibiliLiveDanmuHost, BilibiliLiveDanmuInfoRequest, BilibiliLiveDanmuInfoResponse, BilibiliMatchRequest, BilibiliMatchResponse, BilibiliMatchedResource, BilibiliPageInfo, BilibiliParseLivePageRequest, BilibiliParsePgcPageRequest, BilibiliParseVideoPageRequest, BilibiliPersistedQrLoginResponse, BilibiliPgcSeasonIndexItem, BilibiliPgcSeasonIndexPage, BilibiliPgcTimelineItem, BilibiliProvider, BilibiliQrCodeResponse, BilibiliQrLoginRequest, BilibiliQrLoginResponse, BilibiliQrLoginStatus, BilibiliSmsLoginRequest, BilibiliSmsLoginResponse, BilibiliSmsLoginTokenCodec, BilibiliSmsRequest, BilibiliSmsResponse, - BilibiliUserInfoRequest, BilibiliUserInfoResponse, BilibiliVideoInfo, - DASH_MANIFEST_METADATA_KEY, LIVE_DANMAKU_FORMAT, LIVE_DANMAKU_TRACK_NAME, + BilibiliUserInfoRequest, BilibiliUserInfoResponse, BilibiliVideoInfo, LIVE_DANMAKU_FORMAT, + LIVE_DANMAKU_TRACK_NAME, }; pub use cctv::CctvProvider; pub use cloudreve::{ diff --git a/synctv-core/src/provider/playback_transport.rs b/synctv-core/src/provider/playback_transport.rs index c76dac5b..e5c8c7a5 100644 --- a/synctv-core/src/provider/playback_transport.rs +++ b/synctv-core/src/provider/playback_transport.rs @@ -124,19 +124,6 @@ pub enum PlaybackTransportAction { }, } -impl PlaybackTransportAction { - #[must_use] - pub const fn bypasses_unary_timeout(&self) -> bool { - matches!( - self, - Self::LiveFlv { .. } - | Self::LiveHlsMaster { .. } - | Self::LiveHlsPlaylist { .. } - | Self::LiveHlsSegment { .. } - ) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LiveFlvAccess { pub user_id: UserId, diff --git a/synctv-core/src/provider/provider_client.rs b/synctv-core/src/provider/provider_client.rs index f130a13b..e00224d8 100644 --- a/synctv-core/src/provider/provider_client.rs +++ b/synctv-core/src/provider/provider_client.rs @@ -71,11 +71,6 @@ impl ProviderClientManager { Ok(Self::new_with_provider_http_client(provider_client)) } - #[cfg(test)] - pub(crate) fn new_for_tests() -> Result { - Self::new() - } - /// Create a new `ProviderClientManager` with a shared local provider HTTP client. #[must_use] pub fn new_with_provider_http_client(client: reqwest::Client) -> Self { diff --git a/synctv-core/src/provider/provider_client_tests.rs b/synctv-core/src/provider/provider_client_tests.rs index d16fe3e9..e8100286 100644 --- a/synctv-core/src/provider/provider_client_tests.rs +++ b/synctv-core/src/provider/provider_client_tests.rs @@ -4,8 +4,7 @@ use crate::test_helpers::TestResultExt; /// Test that `resolve_alist_client` returns local client when no remote connection is provided #[test] fn test_resolve_alist_client_returns_local_without_remote_connection() { - let manager = - ProviderClientManager::new_for_tests().checked("default provider HTTP client should build"); + let manager = ProviderClientManager::new().checked("default provider HTTP client should build"); let local_client = manager.local_alist_client(); let resolved_client = manager.resolve_alist_client(None); diff --git a/synctv-core/src/provider/store.rs b/synctv-core/src/provider/store.rs index 97485e5a..575f5f28 100644 --- a/synctv-core/src/provider/store.rs +++ b/synctv-core/src/provider/store.rs @@ -63,10 +63,6 @@ impl StoreLockGuard { release: Some(Box::new(release)), } } - - pub fn noop() -> Self { - Self { release: None } - } } impl Drop for StoreLockGuard { diff --git a/synctv-core/src/provider/traits.rs b/synctv-core/src/provider/traits.rs index be50786c..9723abe5 100644 --- a/synctv-core/src/provider/traits.rs +++ b/synctv-core/src/provider/traits.rs @@ -279,16 +279,6 @@ impl<'a> SourceConfig<'a> { Self::DynamicPlaylist(_) => SourceConfigKind::DynamicPlaylist, } } - - #[must_use] - pub const fn is_media(self) -> bool { - matches!(self, Self::Media(_)) - } - - #[must_use] - pub const fn is_dynamic_playlist(self) -> bool { - matches!(self, Self::DynamicPlaylist(_)) - } } impl From> for PreparedSourceConfig { diff --git a/synctv-core/src/redis_runtime.rs b/synctv-core/src/redis_runtime.rs index 9e48cd13..a0c43521 100644 --- a/synctv-core/src/redis_runtime.rs +++ b/synctv-core/src/redis_runtime.rs @@ -123,17 +123,9 @@ pub struct OnDemandRedisRuntime { impl OnDemandRedisRuntime { #[must_use] pub fn new(client: redis::Client) -> Self { - Self::new_with_connection_options(client, redis::aio::ConnectionManagerConfig::new()) - } - - #[must_use] - pub fn new_with_connection_options( - client: redis::Client, - manager_options: redis::aio::ConnectionManagerConfig, - ) -> Self { Self::new_with_connection_options_and_operation_timeout( client, - manager_options, + redis::aio::ConnectionManagerConfig::new(), crate::resilience::timeout::REDIS_OPERATION_TIMEOUT, ) } @@ -212,41 +204,6 @@ impl ManagedRedisRuntime { operation_timeout, } } - - pub async fn from_client(client: redis::Client) -> redis::RedisResult { - Self::from_client_with_config(client, redis::aio::ConnectionManagerConfig::new()).await - } - - pub async fn from_client_with_config( - client: redis::Client, - manager_options: redis::aio::ConnectionManagerConfig, - ) -> redis::RedisResult { - Self::from_client_with_config_and_operation_timeout( - client, - manager_options, - crate::resilience::timeout::REDIS_OPERATION_TIMEOUT, - ) - .await - } - - pub async fn from_client_with_config_and_operation_timeout( - client: redis::Client, - manager_options: redis::aio::ConnectionManagerConfig, - operation_timeout: Duration, - ) -> redis::RedisResult { - let conn = - redis::aio::ConnectionManager::new_with_config(client.clone(), manager_options).await?; - Ok(Self::new_with_operation_timeout( - client, - Arc::new(tokio::sync::RwLock::new(conn)), - operation_timeout, - )) - } - - #[must_use] - pub fn shared_conn(&self) -> Arc> { - self.conn.clone() - } } #[async_trait] @@ -278,24 +235,6 @@ pub fn direct_runtime(conn: redis::aio::ConnectionManager) -> Arc Arc { - Arc::new(DirectRedisConnectionRuntime::new_with_operation_timeout( - conn, - operation_timeout, - )) -} - -#[must_use] -pub fn direct_runtime_from_conn( - conn: Option, -) -> Option> { - conn.map(direct_runtime) -} - #[must_use] pub fn shared_runtime( conn: Arc>, @@ -303,17 +242,6 @@ pub fn shared_runtime( Arc::new(SharedRedisConnectionRuntime::new(conn)) } -#[must_use] -pub fn shared_runtime_with_operation_timeout( - conn: Arc>, - operation_timeout: Duration, -) -> Arc { - Arc::new(SharedRedisConnectionRuntime::new_with_operation_timeout( - conn, - operation_timeout, - )) -} - #[must_use] pub fn shared_runtime_from_conn( conn: Option>>, @@ -328,17 +256,6 @@ pub fn coordination_runtime_from_client( Arc::new(OnDemandRedisRuntime::new(client)) } -#[must_use] -pub fn coordination_runtime_from_client_with_connection_options( - client: redis::Client, - manager_options: redis::aio::ConnectionManagerConfig, -) -> Arc { - Arc::new(OnDemandRedisRuntime::new_with_connection_options( - client, - manager_options, - )) -} - #[must_use] pub fn coordination_runtime_from_client_with_connection_options_and_operation_timeout( client: redis::Client, diff --git a/synctv-core/src/repository/chat.rs b/synctv-core/src/repository/chat.rs index 37ad154d..8ba82d89 100644 --- a/synctv-core/src/repository/chat.rs +++ b/synctv-core/src/repository/chat.rs @@ -436,13 +436,6 @@ impl ChatRepository { }) } - pub async fn insert_event(&self, event: &ChatMessageEvent) -> Result { - let mut tx = self.pool().begin().await?; - let logged = self.insert_event_in_tx(&mut tx, event).await?; - tx.commit().await?; - Ok(logged) - } - pub async fn insert_message_event( &self, message: &ChatMessage, @@ -1211,21 +1204,6 @@ impl ChatRepository { Ok(()) } - pub async fn list_events_after( - &self, - room_id: &RoomId, - after_event_id: Option<&str>, - limit: i32, - ) -> Result> { - self.list_events_after_with_selection( - room_id, - after_event_id, - limit, - &ChatMessageSelection::user_default(), - ) - .await - } - pub async fn list_events_after_with_selection( &self, room_id: &RoomId, @@ -1315,21 +1293,6 @@ impl ChatRepository { rows.into_iter().map(ChatEventRow::try_into_log).collect() } - pub async fn list_events_after_sequence( - &self, - room_id: &RoomId, - after_sequence: i64, - limit: i32, - ) -> Result> { - self.list_events_after_sequence_with_selection( - room_id, - after_sequence, - limit, - &ChatMessageSelection::user_default(), - ) - .await - } - pub async fn list_events_after_sequence_with_selection( &self, room_id: &RoomId, @@ -2139,25 +2102,6 @@ impl ChatRepository { Ok((messages, next_cursor)) } - pub async fn list_history_page_for_viewer( - &self, - room_id: &RoomId, - cursor: Option, - limit: i32, - include_deleted: bool, - viewer_user_id: Option<&UserId>, - ) -> Result { - self.list_history_page_for_viewer_with_selection( - room_id, - cursor, - limit, - include_deleted, - viewer_user_id, - &ChatMessageSelection::user_default(), - ) - .await - } - pub async fn list_history_page_for_viewer_with_selection( &self, room_id: &RoomId, @@ -2322,13 +2266,6 @@ impl ChatRepository { }) } - pub async fn list_playback_messages( - &self, - query: &ChatPlaybackMessagesQuery, - ) -> Result> { - self.list_playback_messages_for_viewer(query, None).await - } - pub async fn list_playback_messages_for_viewer( &self, query: &ChatPlaybackMessagesQuery, @@ -2421,25 +2358,6 @@ impl ChatRepository { .await } - pub async fn list_context_around_message( - &self, - room_id: &RoomId, - message_id: i64, - before_limit: i32, - after_limit: i32, - include_deleted: bool, - ) -> Result> { - self.list_context_around_message_for_viewer( - room_id, - message_id, - before_limit, - after_limit, - include_deleted, - None, - ) - .await - } - pub async fn list_context_around_message_for_viewer( &self, room_id: &RoomId, @@ -2646,15 +2564,6 @@ impl ChatRepository { optional_chat_message_from_row(msg) } - pub async fn get_with_attachments_by_room_and_id( - &self, - room_id: &RoomId, - message_id: i64, - ) -> Result> { - self.get_with_attachments_by_room_and_id_for_viewer(room_id, message_id, None) - .await - } - pub async fn get_with_attachments_by_room_and_id_for_viewer( &self, room_id: &RoomId, @@ -2725,70 +2634,6 @@ impl ChatRepository { })) } - pub async fn edit( - &self, - room_id: &RoomId, - message_id: i64, - content: &str, - metadata: &Option, - expected_version: Option, - ) -> Result> { - let metadata = ChatMetadata::normalized_for_optional_storage(metadata.as_ref())?; - let mut builder = sqlx::QueryBuilder::::new( - r" - UPDATE chat_messages - SET content = ", - ); - builder.push_bind(content); - builder.push(", metadata = "); - builder.push_bind(&metadata); - builder.push(", status = "); - builder.push_bind(i16::from(ChatMessageStatus::Edited)); - builder.push( - ", version = version + 1, edited_at = NOW() - WHERE room_id = ", - ); - builder.push_bind(room_id.as_i64()); - builder.push(" AND id = "); - builder.push_bind(message_id); - builder.push(" AND status <> "); - builder.push_bind(i16::from(ChatMessageStatus::Deleted)); - if let Some(version) = expected_version { - builder.push(" AND version = "); - builder.push_bind(version); - } - builder.push( - r" - RETURNING id, room_id, user_id, client_message_id, content, message_type, - status, version, reply_to_message_id, reply_to_message_created_at, metadata, edited_at, - deleted_at, deleted_by, delete_reason, created_at - ", - ); - - let message = builder - .build_query_as::() - .fetch_optional(self.pool()) - .await?; - let Some(message) = message else { - return Ok(None); - }; - let attachments = self - .attachments_for_message(message.id, message.created_at) - .await?; - let mentions = self - .mentions_for_messages(std::slice::from_ref(&message)) - .await? - .remove(&chat_message_key(&message)) - .unwrap_or_default(); - Ok(Some(ChatMessageWithAttachments { - message, - attachments, - reactions: Vec::new(), - mentions, - pin: None, - })) - } - pub async fn edit_with_event( &self, request: EditChatMessageEventRequest<'_>, @@ -2920,67 +2765,6 @@ impl ChatRepository { })) } - pub async fn soft_delete( - &self, - room_id: &RoomId, - message_id: i64, - deleted_by: &UserId, - reason: Option<&str>, - expected_version: Option, - ) -> Result> { - let mut builder = sqlx::QueryBuilder::::new( - r" - UPDATE chat_messages - SET content = '', status = ", - ); - builder.push_bind(i16::from(ChatMessageStatus::Deleted)); - builder.push(", version = version + 1, deleted_at = NOW(), deletion_source = "); - builder.push_bind(DeletionSource::User); - builder.push(", deleted_by = "); - builder.push_bind(deleted_by.as_i64()); - builder.push(", delete_reason = "); - builder.push_bind(reason); - builder.push(" WHERE room_id = "); - builder.push_bind(room_id.as_i64()); - builder.push(" AND id = "); - builder.push_bind(message_id); - builder.push(" AND status <> "); - builder.push_bind(i16::from(ChatMessageStatus::Deleted)); - if let Some(version) = expected_version { - builder.push(" AND version = "); - builder.push_bind(version); - } - builder.push( - r" - RETURNING id, room_id, user_id, client_message_id, content, message_type, - status, version, reply_to_message_id, reply_to_message_created_at, metadata, edited_at, - deleted_at, deleted_by, delete_reason, created_at - ", - ); - - let message = builder - .build_query_as::() - .fetch_optional(self.pool()) - .await?; - let Some(message) = message else { - return Ok(None); - }; - self.delete_pin_for_message(message.room_id, message.id, message.created_at) - .await?; - let mentions = self - .mentions_for_messages(std::slice::from_ref(&message)) - .await? - .remove(&chat_message_key(&message)) - .unwrap_or_default(); - Ok(Some(ChatMessageWithAttachments { - message, - attachments: Vec::new(), - reactions: Vec::new(), - mentions, - pin: None, - })) - } - pub async fn soft_delete_with_event( &self, request: DeleteChatMessageEventRequest<'_>, @@ -3129,27 +2913,6 @@ impl ChatRepository { Ok(result.rows_affected() > 0) } - pub async fn delete_in_room( - &self, - room_id: &RoomId, - message_id: i64, - created_at: DateTime, - ) -> Result { - let result = sqlx::query!( - r" - DELETE FROM chat_messages - WHERE room_id = $1 AND id = $2 AND created_at = $3 - ", - room_id.as_i64(), - message_id, - created_at - ) - .execute(self.pool()) - .await?; - - Ok(result.rows_affected() > 0) - } - pub async fn count_by_room(&self, room_id: &RoomId) -> Result { let count = sqlx::query_scalar!( r#" @@ -3196,19 +2959,6 @@ impl ChatRepository { Ok(result.rows_affected()) } - pub async fn delete_messages_older_than_retention(&self) -> Result { - let result = sqlx::query!( - r" - DELETE FROM chat_messages - WHERE created_at <= NOW() - INTERVAL '90 days' - " - ) - .execute(self.pool()) - .await?; - - Ok(result.rows_affected()) - } - pub async fn cleanup_all_rooms( &self, keep_count: i32, @@ -4071,19 +3821,6 @@ impl ChatRepository { row.map(ChatPinEventRow::try_into_log).transpose() } - async fn attachments_for_message( - &self, - message_id: i64, - message_created_at: DateTime, - ) -> Result> { - self.attachments_for_message_from_pool( - self.eventually_consistent_pool(), - message_id, - message_created_at, - ) - .await - } - async fn attachments_for_message_from_pool( &self, pool: &PgPool, @@ -4463,34 +4200,6 @@ impl ChatRepository { Ok(()) } - async fn delete_pin_for_message( - &self, - room_id: RoomId, - message_id: i64, - message_created_at: DateTime, - ) -> Result<()> { - sqlx::query!( - r" - DELETE FROM chat_message_pins - WHERE room_id = $1 AND message_id = $2 AND message_created_at = $3 - ", - room_id.as_i64(), - message_id, - message_created_at - ) - .execute(self.pool()) - .await?; - Ok(()) - } - - async fn mentions_for_messages( - &self, - messages: &[ChatMessage], - ) -> Result>> { - self.mentions_for_messages_from_pool(self.eventually_consistent_pool(), messages) - .await - } - async fn mentions_for_messages_from_pool( &self, pool: &PgPool, diff --git a/synctv-core/src/repository/file_storage.rs b/synctv-core/src/repository/file_storage.rs index 868b76c4..3e02aba5 100644 --- a/synctv-core/src/repository/file_storage.rs +++ b/synctv-core/src/repository/file_storage.rs @@ -573,36 +573,6 @@ impl FileStorageRepository { Ok(object) } - pub async fn get_any_object_by_manifest( - &self, - storage_backend: &str, - content_manifest_sha256: &str, - size_bytes: i64, - ) -> Result> { - validate_required_sha256(content_manifest_sha256, "content_manifest_sha256")?; - let object = sqlx::query_as!( - FileObject, - r#" - SELECT storage_backend, object_key, mime_type, size_bytes, - content_manifest_sha256, metadata AS "metadata!: FileMetadata", - created_at, validated_at, deleting_at - FROM file_objects - WHERE storage_backend = $1 - AND content_manifest_sha256 = $2 - AND size_bytes = $3 - AND deleting_at IS NULL - ORDER BY validated_at DESC NULLS LAST, created_at ASC - LIMIT 1 - "#, - storage_backend, - content_manifest_sha256.trim().to_ascii_lowercase(), - size_bytes, - ) - .fetch_optional(&self.pool) - .await?; - Ok(object) - } - pub async fn update_object_metadata( &self, storage_backend: &str, @@ -1260,42 +1230,6 @@ impl FileStorageRepository { Ok(session) } - pub async fn get_active_reference_by_target( - &self, - reference_kind: &str, - reference_id: &str, - ) -> Result> { - let reference = sqlx::query_as!( - StoredFileReferenceRow, - r#" - SELECT r.id AS file_reference_id, - o.storage_backend, - o.object_key, - o.mime_type, - o.size_bytes, - o.content_manifest_sha256, - r.metadata AS "metadata!: FileReferenceMetadata", - o.created_at, - o.validated_at - FROM file_references r - JOIN file_objects o - ON o.storage_backend = r.storage_backend - AND o.object_key = r.object_key - WHERE r.reference_kind = $1 - AND r.reference_id = $2 - AND r.released_at IS NULL - AND o.deleting_at IS NULL - ORDER BY r.updated_at DESC, r.id DESC - LIMIT 1 - "#, - reference_kind, - reference_id, - ) - .fetch_optional(&self.pool) - .await?; - Ok(reference.map(StoredFileReference::from)) - } - pub async fn get_active_reference_metadata_by_target( &self, reference_kind: &str, @@ -1408,40 +1342,6 @@ impl FileStorageRepository { scalar_value(exists, "file blob part EXISTS") } - pub async fn get_blob( - &self, - storage_backend: &str, - object_key: &str, - ) -> Result> { - let Some(object) = self.get_object(storage_backend, object_key).await? else { - return Ok(None); - }; - let parts = self.list_blob_parts(storage_backend, object_key).await?; - if parts.is_empty() { - return Ok(None); - } - let compression = parts - .first() - .map_or(FileBlobCompression::None, |part| part.compression); - let mut data = Vec::new(); - for part in parts { - data.extend_from_slice(&part.data); - } - Ok(Some(FileBlob { - storage_backend: object.storage_backend, - object_key: object.object_key, - mime_type: object.mime_type, - size_bytes: object.size_bytes, - total_size_bytes: object.size_bytes, - content_manifest_sha256: object.content_manifest_sha256, - compression, - range: None, - data: data.into(), - metadata: object.metadata, - created_at: object.created_at, - })) - } - pub async fn delete_blob(&self, storage_backend: &str, object_key: &str) -> Result { Ok(self.delete_blob_parts(storage_backend, object_key).await? > 0) } @@ -2025,59 +1925,6 @@ impl FileStorageRepository { .collect()) } - pub async fn enqueue_cleanup_jobs( - &self, - origin: &str, - files: &[FileReferenceTarget], - metadata: &FileCleanupMetadata, - error: &str, - ) -> Result<()> { - if files.is_empty() { - return Ok(()); - } - validate_required_text(origin, "file cleanup origin", FILE_CLEANUP_ORIGIN_MAX_CHARS)?; - - let mut tx = self.pool.begin().await?; - for file in files { - validate_file_reference_fields( - &file.storage_backend, - &file.object_key, - &file.reference_kind, - &file.reference_id, - )?; - sqlx::query!( - r#" - INSERT INTO file_cleanup_jobs ( - origin, storage_backend, object_key, reference_kind, - reference_id, metadata, last_error - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (reference_kind, reference_id, storage_backend, object_key) - DO UPDATE SET - origin = EXCLUDED.origin, - metadata = EXCLUDED.metadata, - last_error = EXCLUDED.last_error, - next_attempt_at = CURRENT_TIMESTAMP, - locked_at = NULL, - locked_by = NULL, - completed_at = NULL, - updated_at = CURRENT_TIMESTAMP - "#, - origin, - &file.storage_backend, - &file.object_key, - &file.reference_kind, - &file.reference_id, - metadata as &FileCleanupMetadata, - error, - ) - .execute(&mut *tx) - .await?; - } - tx.commit().await?; - Ok(()) - } - pub async fn release_references_and_enqueue_cleanup_jobs( &self, origin: &str, diff --git a/synctv-core/src/repository/media.rs b/synctv-core/src/repository/media.rs index a4afd23f..5452431d 100644 --- a/synctv-core/src/repository/media.rs +++ b/synctv-core/src/repository/media.rs @@ -1411,54 +1411,6 @@ impl MediaRepository { Ok(rows.into_iter().map(Into::into).collect()) } - /// Get room-root media items with limit and offset (no count query). - pub async fn get_room_root_limit_offset( - &self, - room_id: &RoomId, - limit: i64, - offset: i64, - ) -> Result> { - let rows = sqlx::query_as!( - MediaRow, - r#" - SELECT id as "id: MediaId", - playlist_id as "playlist_id: PlaylistId", - room_id as "room_id: RoomId", - creator_id as "creator_id: UserId", - name, - description, - position, - source_provider as "source_provider: ProviderTypeName", - source_config as "source_config: crate::models::MediaSourceConfig", - NULLIF(provider_instance_name, '') AS "provider_instance_name?", - cover_file_reference_id, - thumbnail_file_reference_id, - added_at, updated_at, version - FROM media - WHERE media.room_id = $1 - AND media.deleted_at IS NULL - AND EXISTS ( - SELECT 1 FROM rooms r - WHERE r.id = media.room_id AND r.deleted_at IS NULL - ) - AND (media.creator_id IS NULL OR EXISTS ( - SELECT 1 FROM users u - WHERE u.id = media.creator_id AND u.deleted_at IS NULL - )) - AND media.playlist_id IS NULL - ORDER BY position ASC - LIMIT $2 OFFSET $3 - "#, - room_id as &RoomId, - limit, - offset, - ) - .fetch_all(&self.pool) - .await?; - - Ok(rows.into_iter().map(Into::into).collect()) - } - /// Delete media from playlist pub async fn delete(&self, media_id: &MediaId) -> Result { let result = sqlx::query!( @@ -2352,44 +2304,6 @@ impl MediaRepository { Ok(count) } - /// Count only media whose creator is still active (or media without a creator). - pub async fn count_by_playlist_accessible(&self, playlist_id: &PlaylistId) -> Result { - let count = sqlx::query_scalar!( - r#" - SELECT COUNT(*) AS "count!" - FROM media m - LEFT JOIN users u - ON m.creator_id = u.id - AND u.deleted_at IS NULL - WHERE m.playlist_id = $1 - AND m.deleted_at IS NULL - AND (m.creator_id IS NULL OR ( - u.id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM user_bans ub - WHERE ub.user_id = u.id - AND ub.revoked_at IS NULL - AND (ub.ends_at IS NULL OR ub.ends_at > CURRENT_TIMESTAMP) - ) - AND EXISTS ( - SELECT 1 FROM room_members rm - WHERE rm.room_id = m.room_id AND rm.user_id = m.creator_id - ) - AND NOT EXISTS ( - SELECT 1 FROM room_member_kick_cooldowns cooldown - WHERE cooldown.room_id = m.room_id - AND cooldown.user_id = m.creator_id - AND cooldown.ends_at > CURRENT_TIMESTAMP - ) - )) - "#, - playlist_id.as_i64(), - ) - .fetch_one(&self.pool) - .await?; - - Ok(count) - } - /// Count media items directly under the room root. pub async fn count_room_root(&self, room_id: &RoomId) -> Result { let count = sqlx::query_scalar!( @@ -2450,57 +2364,6 @@ impl MediaRepository { } Ok(result) } - - /// Batch count only media whose creator is still active (or media without a creator). - pub async fn count_by_playlists_batch_accessible( - &self, - playlist_ids: &[PlaylistId], - ) -> Result> { - if playlist_ids.is_empty() { - return Ok(std::collections::HashMap::new()); - } - let ids: Vec = playlist_ids.iter().map(PlaylistId::as_i64).collect(); - - let rows = sqlx::query!( - r#" - SELECT m.playlist_id AS "playlist_id!: PlaylistId", COUNT(*) AS "cnt!" - FROM media m - LEFT JOIN users u - ON m.creator_id = u.id - AND u.deleted_at IS NULL - WHERE m.playlist_id = ANY($1) - AND m.deleted_at IS NULL - AND (m.creator_id IS NULL OR ( - u.id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM user_bans ub - WHERE ub.user_id = u.id - AND ub.revoked_at IS NULL - AND (ub.ends_at IS NULL OR ub.ends_at > CURRENT_TIMESTAMP) - ) - AND EXISTS ( - SELECT 1 FROM room_members rm - WHERE rm.room_id = m.room_id AND rm.user_id = m.creator_id - ) - AND NOT EXISTS ( - SELECT 1 FROM room_member_kick_cooldowns cooldown - WHERE cooldown.room_id = m.room_id - AND cooldown.user_id = m.creator_id - AND cooldown.ends_at > CURRENT_TIMESTAMP - ) - )) - GROUP BY m.playlist_id - "#, - &ids, - ) - .fetch_all(&self.pool) - .await?; - - let mut result = std::collections::HashMap::new(); - for row in rows { - result.insert(row.playlist_id, row.cnt); - } - Ok(result) - } } #[cfg(test)] diff --git a/synctv-core/src/repository/media_tests.rs b/synctv-core/src/repository/media_tests.rs index 815c1c93..6660d783 100644 --- a/synctv-core/src/repository/media_tests.rs +++ b/synctv-core/src/repository/media_tests.rs @@ -29,40 +29,6 @@ fn direct_url_media_source_config(url: impl Into) -> MediaSourceConfig { }) } -fn direct_url_playback_info(url: &str, name: &str) -> crate::models::PlaybackInfo { - crate::models::PlaybackInfo { - thumbnail: None, - medias: vec![crate::models::PlaybackMedia { - name: name.to_string(), - format: String::new(), - expire_at: None, - metadata: None, - p2p_swarm_id: None, - provider: crate::models::PlaybackMediaProvider::DirectUrl( - crate::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 direct_url_first_media_url(source_config: &MediaSourceConfig) -> &str { - let MediaSourceConfig::DirectUrl(config) = source_config else { - panic!("expected DirectUrl source_config"); - }; - config.medias.first().map_or_else( - || panic!("expected DirectUrl media resource"), - |media| media.url.as_str(), - ) -} - async fn insert_test_provider_instance(pool: &PgPool, name: &str, provider: &str) { let now = crate::SystemClock.now(); let instance = ProviderInstance { @@ -165,71 +131,6 @@ fn test_push_media_list_order_by_uses_static_sort_branches() { ); } -/// Unit test: `Media::from_direct_single_mode` -#[test] -fn test_media_from_direct_single_mode() { - let playlist_id = PlaylistId::new(); - let room_id = RoomId::new(); - let creator_id = UserId::new(); - - let playback_info = direct_url_playback_info("https://example.com/video.mp4", "1080P"); - - let media = Media::from_direct_single_mode( - Some(playlist_id), - room_id, - Some(creator_id), - "Single Mode Video".to_string(), - "direct", - playback_info, - 5.0, - ) - .checked("direct media should build"); - - assert_eq!(media.name, "Single Mode Video"); - assert!((media.position - 5.0).abs() < f64::EPSILON); - assert!(media.provider_instance_name.is_none()); - assert_eq!( - direct_url_first_media_url(&media.source_config), - "https://example.com/video.mp4" - ); -} - -/// Unit test: `Media::from_direct_multimode` -#[test] -fn test_media_from_direct_multimode() { - let playlist_id = PlaylistId::new(); - let room_id = RoomId::new(); - - let mut playback_infos = std::collections::HashMap::new(); - playback_infos.insert( - "direct".to_string(), - direct_url_playback_info("https://example.com/video.mp4", "1080P"), - ); - playback_infos.insert( - "proxied".to_string(), - direct_url_playback_info("https://proxy.example.com/video.mp4", "720P"), - ); - - let media = Media::from_direct_multimode(crate::models::DirectMultimodeParams { - playlist_id: Some(playlist_id), - room_id, - creator_id: None, - name: "Multimode Video".to_string(), - playback_infos, - default_mode: "direct".to_string(), - position: 10.0, - }) - .checked("direct multimode media should build"); - - assert_eq!(media.name, "Multimode Video"); - assert!((media.position - 10.0).abs() < f64::EPSILON); - assert!(media.provider_instance_name.is_none()); - assert_eq!( - direct_url_first_media_url(&media.source_config), - "https://example.com/video.mp4" - ); -} - /// Integration test: Create and get media #[tokio::test] #[ignore = "Requires Docker"] diff --git a/synctv-core/src/repository/mod.rs b/synctv-core/src/repository/mod.rs index 208c72c4..243f4318 100644 --- a/synctv-core/src/repository/mod.rs +++ b/synctv-core/src/repository/mod.rs @@ -87,7 +87,7 @@ pub use room_resource_event::{ RoomResourceEventSummary, RoomResourceEventSummaryDetails, RoomResourceKind, }; pub use room_settings::RoomSettingsRepository; -pub use room_taxonomy::{RoomTaxonomyAssignment, RoomTaxonomyRepository}; +pub use room_taxonomy::RoomTaxonomyRepository; pub use settings::SettingsRepository; pub use system_stats::{SystemStats, SystemStatsRepository}; pub use totp_credential::{TotpCredential, TotpCredentialRepository}; diff --git a/synctv-core/src/repository/playback.rs b/synctv-core/src/repository/playback.rs index 8b6f4e08..fa8a33dc 100644 --- a/synctv-core/src/repository/playback.rs +++ b/synctv-core/src/repository/playback.rs @@ -515,38 +515,6 @@ impl RoomPlaybackStateRepository { self.update_in_tx(state, state.version + 1, None).await } - /// Update playback state with optimistic locking and an externally allocated version. - /// - /// Strong-cache write paths reserve the next version from Redis first, then - /// store that exact version in Postgres so Redis cannot lag behind the DB. - pub async fn update_with_exact_version( - &self, - state: &RoomPlaybackState, - new_version: i64, - ) -> Result { - self.update_in_tx(state, new_version, None).await - } - - pub async fn update_with_exact_version_and_previous_progress( - &self, - state: &RoomPlaybackState, - new_version: i64, - previous_progress_position: Option, - ) -> Result { - self.update_in_tx(state, new_version, previous_progress_position) - .await - } - - pub async fn update_with_exact_version_executor( - &self, - state: &RoomPlaybackState, - new_version: i64, - conn: &mut PgConnection, - ) -> Result { - self.update_with_exact_version_on_conn(state, new_version, None, conn) - .await - } - pub async fn update_with_exact_version_executor_and_previous_progress( &self, state: &RoomPlaybackState, @@ -558,15 +526,6 @@ impl RoomPlaybackStateRepository { .await } - /// List playback states impacted by media/playlists owned by a creator. - pub async fn find_playback_for_creator( - &self, - creator_id: &UserId, - ) -> Result> { - self.find_playback_for_creator_with_executor(creator_id, &self.pool) - .await - } - pub async fn find_playback_for_creator_with_executor<'e, E>( &self, creator_id: &UserId, diff --git a/synctv-core/src/repository/playback_source_metadata.rs b/synctv-core/src/repository/playback_source_metadata.rs index a40e7f35..40452629 100644 --- a/synctv-core/src/repository/playback_source_metadata.rs +++ b/synctv-core/src/repository/playback_source_metadata.rs @@ -299,36 +299,6 @@ impl PlaybackSourceMetadataRepository { Ok(metadata) } - pub async fn update_names_if_present( - &self, - identity: &PlaybackSourceIdentity, - media_name: Option<&str>, - playlist_name: Option<&str>, - ) -> Result<()> { - sqlx::query!( - r#"UPDATE playback_source_metadata - SET media_name = COALESCE($5, media_name), - playlist_name = COALESCE($6, playlist_name), - updated_at = CURRENT_TIMESTAMP, - version = version + 1 - WHERE room_id = $1 - AND media_id IS NOT DISTINCT FROM $2 - AND playlist_id IS NOT DISTINCT FROM $3 - AND target_hash = $4 - AND (media_name IS DISTINCT FROM COALESCE($5, media_name) - OR playlist_name IS DISTINCT FROM COALESCE($6, playlist_name))"#, - identity.room_id.as_i64(), - identity.media_id.map(i64::from), - identity.playlist_id.map(i64::from), - &identity.target_hash, - media_name, - playlist_name, - ) - .execute(&self.pool) - .await?; - Ok(()) - } - pub async fn list_active_finite_sources_for_rooms( &self, room_ids: &[RoomId], diff --git a/synctv-core/src/repository/playlist.rs b/synctv-core/src/repository/playlist.rs index 9bea34e9..a4f21f0e 100644 --- a/synctv-core/src/repository/playlist.rs +++ b/synctv-core/src/repository/playlist.rs @@ -673,74 +673,6 @@ impl PlaylistRepository { rows.into_iter().map(TryInto::try_into).collect() } - /// Count top-level playlists in a room. - pub async fn count_top_level(&self, room_id: &RoomId) -> Result { - let count = sqlx::query_scalar!( - r" - SELECT COUNT(*) - FROM playlists p - WHERE p.room_id = $1 - AND p.parent_id IS NULL - AND p.deleted_at IS NULL - AND EXISTS (SELECT 1 FROM rooms r WHERE r.id = p.room_id AND r.deleted_at IS NULL) - AND (p.creator_id IS NULL OR EXISTS ( - SELECT 1 FROM users u WHERE u.id = p.creator_id AND u.deleted_at IS NULL - )) - ", - room_id as &RoomId, - ) - .fetch_one(&self.pool) - .await?; - - required_count(count, "top-level playlist") - } - - /// Get paginated top-level playlists in a room. - pub async fn get_top_level_paginated( - &self, - room_id: &RoomId, - limit: i64, - offset: i64, - ) -> Result> { - let rows = sqlx::query_as!( - PlaylistRow, - r#" - SELECT id as "id: PlaylistId", - room_id as "room_id: RoomId", - creator_id as "creator_id: crate::models::UserId", - name, - description, - cover_file_reference_id, - parent_id as "parent_id: PlaylistId", - position, - browse_access_mode, - source_provider, - source_config as "source_config: crate::models::PlaylistSourceConfig", - NULLIF(provider_instance_name, '') AS "provider_instance_name?", - created_at, - updated_at, - version - FROM playlists p - WHERE p.room_id = $1 - AND p.parent_id IS NULL - AND p.deleted_at IS NULL - AND EXISTS (SELECT 1 FROM rooms r WHERE r.id = p.room_id AND r.deleted_at IS NULL) - AND (p.creator_id IS NULL OR EXISTS ( - SELECT 1 FROM users u WHERE u.id = p.creator_id AND u.deleted_at IS NULL - )) - ORDER BY position ASC - LIMIT $2 OFFSET $3 - "#, - room_id as &RoomId, - limit, - offset, - ) - .fetch_all(&self.pool) - .await?; - - rows.into_iter().map(TryInto::try_into).collect() - } - /// Get children playlists of a parent pub async fn get_children(&self, parent_id: &PlaylistId) -> Result> { let rows = sqlx::query_as!( diff --git a/synctv-core/src/repository/provider_instance.rs b/synctv-core/src/repository/provider_instance.rs index 217fa0dd..da669b68 100644 --- a/synctv-core/src/repository/provider_instance.rs +++ b/synctv-core/src/repository/provider_instance.rs @@ -1023,16 +1023,11 @@ impl ProviderInstanceRepository { self.decrypt_instance_rows(rows) } - /// Get all enabled provider instances (sensitive fields decrypted) - pub async fn get_all_enabled(&self) -> Result> { - self.get_all_enabled_from_primary().await - } - /// Get all enabled provider instances from the primary database. /// /// These rows contain connection-building inputs and secrets. Reading them /// from a lagging replica can cache stale enabled credentials. - pub async fn get_all_enabled_from_primary(&self) -> Result> { + pub async fn get_all_enabled(&self) -> Result> { let rows = sqlx::query_as!( ProviderInstanceRow, r" @@ -1107,16 +1102,8 @@ impl ProviderInstanceRepository { Ok((self.decrypt_instance_rows(rows)?, total)) } - /// Get instances that support a specific provider type (sensitive fields decrypted) - pub async fn find_by_provider(&self, provider: &str) -> Result> { - self.find_by_provider_from_primary(provider).await - } - /// Get enabled instances that support a provider type from the primary database. - pub async fn find_by_provider_from_primary( - &self, - provider: &str, - ) -> Result> { + pub async fn find_by_provider(&self, provider: &str) -> Result> { let rows = sqlx::query_as!( ProviderInstanceRow, r" @@ -1701,24 +1688,6 @@ impl UserProviderCredentialRepository { Ok(()) } - /// Get all expired credentials (for cleanup jobs, decrypted) - pub async fn get_expired(&self) -> Result> { - let rows = sqlx::query_as!( - UserProviderCredentialRow, - r#" - SELECT id, user_id as "user_id: UserId", provider, server_id, - provider_instance_name, credential_data as "credential_data!: StoredProviderCredential", - expires_at, created_at, updated_at - FROM user_media_provider_credentials - WHERE expires_at IS NOT NULL AND expires_at <= NOW() - "#, - ) - .fetch_all(&self.pool) - .await?; - - self.decrypt_credential_rows(rows) - } - /// Delete all expired credentials pub async fn delete_expired(&self) -> Result { let result = sqlx::query!( diff --git a/synctv-core/src/repository/realtime_outbox.rs b/synctv-core/src/repository/realtime_outbox.rs index 4fc4035b..3099cd6c 100644 --- a/synctv-core/src/repository/realtime_outbox.rs +++ b/synctv-core/src/repository/realtime_outbox.rs @@ -545,24 +545,6 @@ impl RealtimeOutboxRepository { .await?; Ok(result.rows_affected()) } - - pub async fn notify_dispatchers(&self) -> Result<()> { - sqlx::query!("SELECT pg_notify($1, '')", REALTIME_OUTBOX_CHANNEL) - .execute(&self.pool) - .await?; - Ok(()) - } - - #[cfg(test)] - pub async fn notify_dispatchers_with_executor<'e, E>(&self, executor: E) -> Result<()> - where - E: sqlx::PgExecutor<'e>, - { - sqlx::query!("SELECT pg_notify($1, '')", REALTIME_OUTBOX_CHANNEL) - .execute(executor) - .await?; - Ok(()) - } } fn ensure_outbox_row_updated(rows_affected: u64, id: &str, operation: &str) -> Result<()> { diff --git a/synctv-core/src/repository/review.rs b/synctv-core/src/repository/review.rs index 6bbbb3db..ef539ffb 100644 --- a/synctv-core/src/repository/review.rs +++ b/synctv-core/src/repository/review.rs @@ -424,21 +424,6 @@ impl ReviewRepository { Ok(ReviewPage { rows, total }) } - pub async fn reject_user_registration( - &self, - request_id: UserId, - reviewed_by: Option, - reason: &str, - ) -> Result { - Self::reject_user_registration_with_executor( - self.pools.primary(), - request_id, - reviewed_by, - reason, - ) - .await - } - pub async fn approve_user_registration_with_executor<'e, E>( executor: E, request_id: UserId, diff --git a/synctv-core/src/repository/room.rs b/synctv-core/src/repository/room.rs index 679ddfe0..baaa5ba1 100644 --- a/synctv-core/src/repository/room.rs +++ b/synctv-core/src/repository/room.rs @@ -309,15 +309,6 @@ impl RoomRepository { Ok(created.into()) } - pub async fn active_name_exists_for_creator( - &self, - creator_id: &UserId, - name: &str, - ) -> Result { - Self::active_name_exists_for_creator_with_executor(creator_id, name, self.pools.primary()) - .await - } - pub async fn active_name_exists_for_creator_with_executor<'e, E>( creator_id: &UserId, name: &str, @@ -1628,109 +1619,6 @@ impl RoomRepository { Ok((rooms, count)) } - /// Get rooms created by a specific user with member count (optimized) - pub async fn list_by_creator_with_count( - &self, - creator_id: &UserId, - pagination: PageParams, - ) -> Result<(Vec, i64)> { - let limit = pagination.limit_i64()?; - let offset = pagination.offset_i64()?; - - let count = required_count( - sqlx::query_scalar!( - "SELECT COUNT(*) as count - FROM rooms - WHERE created_by = $1 AND deleted_at IS NULL", - creator_id as &UserId, - ) - .fetch_one(self.pools.primary()) - .await?, - "rooms by creator with count", - )?; - - let rows = sqlx::query!( - r#" - SELECT r.id AS "id: RoomId", - r.name, - r.description, - r.cover_file_reference_id, - rc.id AS "category_id?: RoomCategoryId", - rc.key AS "category_key?", - rc.name AS "category_name?", - rc.description AS "category_description?", - rc.sort_order AS "category_sort_order?", - rc.is_enabled AS "category_is_enabled?", - rc.created_at AS "category_created_at?", - rc.updated_at AS "category_updated_at?", - r.created_by AS "created_by: UserId", - r.closed_at, - r.created_at, - r.updated_at, - r.deleted_at, - r.version, - r.last_activity_at, - r.is_public, - EXISTS ( - SELECT 1 FROM room_bans rb - WHERE rb.room_id = r.id - AND rb.revoked_at IS NULL - AND (rb.ends_at IS NULL OR rb.ends_at > CURRENT_TIMESTAMP) - ) AS "is_banned!", - COALESCE(COUNT(rm.user_id), 0)::int AS "member_count!" - FROM rooms r - LEFT JOIN room_categories rc ON rc.id = r.category_id - LEFT JOIN room_members rm ON r.id = rm.room_id - WHERE r.created_by = $1 AND r.deleted_at IS NULL - GROUP BY r.id, r.name, r.description, r.cover_file_reference_id, r.category_id, - rc.id, rc.key, rc.name, rc.description, rc.sort_order, rc.is_enabled, - rc.created_at, rc.updated_at, r.created_by, - r.closed_at, r.created_at, r.updated_at, r.deleted_at, r.version, - r.last_activity_at, r.is_public - ORDER BY r.created_at DESC - LIMIT $2 OFFSET $3 - "#, - creator_id as &UserId, - limit, - offset - ) - .fetch_all(self.pools.primary()) - .await?; - - let rooms_with_count = rows - .into_iter() - .map(|row| crate::models::RoomWithCount { - room: RoomRow { - id: row.id, - name: row.name, - description: row.description, - cover_file_reference_id: row.cover_file_reference_id, - category_id: row.category_id, - category_key: row.category_key, - category_name: row.category_name, - category_description: row.category_description, - category_sort_order: row.category_sort_order, - category_is_enabled: row.category_is_enabled, - category_created_at: row.category_created_at, - category_updated_at: row.category_updated_at, - created_by: row.created_by, - closed_at: row.closed_at, - is_banned: row.is_banned, - is_public: row.is_public, - created_at: row.created_at, - updated_at: row.updated_at, - deleted_at: row.deleted_at, - version: row.version, - last_activity_at: row.last_activity_at, - } - .into(), - member_count: row.member_count, - }) - .collect(); - - Ok((rooms_with_count, count)) - } - /// Update derived room status by closing or reopening the room. /// /// This intentionally does NOT use optimistic locking (CAS) because status diff --git a/synctv-core/src/repository/room_member.rs b/synctv-core/src/repository/room_member.rs index c400a5b7..6f8a3741 100644 --- a/synctv-core/src/repository/room_member.rs +++ b/synctv-core/src/repository/room_member.rs @@ -2,8 +2,8 @@ use sqlx::PgPool; use crate::{ models::{ - AddMemberOptions, MemberStatus, MyRoomListQuery, MyRoomListSortBy, MyRoomRelation, - PageParams, RoomCategoryId, RoomId, RoomMember, RoomMemberListQuery, RoomMemberListSortBy, + AddMemberOptions, MyRoomListQuery, MyRoomListSortBy, MyRoomRelation, PageParams, + RoomCategoryId, RoomId, RoomMember, RoomMemberListQuery, RoomMemberListSortBy, RoomMemberWithUser, RoomRole, RoomStatus, UserId, }, repository::query_builder::trusted_dynamic_sql, @@ -45,7 +45,6 @@ struct RoomMemberWithUserRow { admin_added_permissions: i64, admin_removed_permissions: i64, joined_at: chrono::DateTime, - is_active: bool, } #[derive(Debug, Clone)] @@ -229,31 +228,6 @@ impl RoomMemberRepository { Ok(version) } - pub async fn next_lifecycle_version_with_executor<'e, E>( - &self, - room_id: &RoomId, - user_id: &UserId, - executor: E, - ) -> Result - where - E: sqlx::PgExecutor<'e>, - { - let version = sqlx::query_scalar!( - "INSERT INTO room_member_versions (room_id, user_id, version, is_member, updated_at) - VALUES ($1, $2, 1, FALSE, CURRENT_TIMESTAMP) - ON CONFLICT (room_id, user_id) DO UPDATE - SET version = room_member_versions.version + 1, - is_member = FALSE, - updated_at = CURRENT_TIMESTAMP - RETURNING version", - room_id as &RoomId, - user_id as &UserId, - ) - .fetch_one(executor) - .await?; - Ok(version) - } - pub async fn mark_active_lifecycle_with_executor<'e, E>( &self, room_id: &RoomId, @@ -398,9 +372,7 @@ impl RoomMemberRepository { } } - fn my_room_result_from_row( - row: MyRoomListRow, - ) -> (crate::models::Room, RoomRole, MemberStatus, i32) { + fn my_room_result_from_row(row: MyRoomListRow) -> (crate::models::Room, RoomRole, i32) { let status = if row.closed_at.is_some() { crate::models::RoomStatus::Closed } else { @@ -434,7 +406,7 @@ impl RoomMemberRepository { version: row.version, }; - (room, row.user_role, MemberStatus::Active, row.member_count) + (room, row.user_role, row.member_count) } /// Add user to room with role. @@ -983,7 +955,6 @@ impl RoomMemberRepository { rm.added_permissions, rm.removed_permissions, rm.admin_added_permissions, rm.admin_removed_permissions, rm.joined_at, - TRUE AS "is_active!", u.username, rm.remark_name, rm.display_tag @@ -1008,34 +979,6 @@ impl RoomMemberRepository { .collect() } - /// List active members in a room with database-level pagination - /// - /// Uses a separate count query so out-of-range pages still return the - /// number of matching members. - /// - /// Returns a tuple of (members, total_count) where: - /// - `members`: Vec of `RoomMemberWithUser` for the current page - /// - `total_count`: Total number of active members in the room (i64) - /// - /// # Exclusions - /// - /// This method excludes members blocked by active kick cooldown rules and - /// soft-deleted users. - pub async fn list_by_room_paginated( - &self, - room_id: &RoomId, - pagination: PageParams, - ) -> Result<(Vec, i64)> { - self.list_by_room_query( - room_id, - &RoomMemberListQuery { - pagination, - ..Default::default() - }, - ) - .await - } - pub async fn list_by_room_query( &self, room_id: &RoomId, @@ -1078,7 +1021,6 @@ impl RoomMemberRepository { rm.added_permissions, rm.removed_permissions, rm.admin_added_permissions, rm.admin_removed_permissions, rm.joined_at, - TRUE AS is_active, u.username, rm.remark_name, rm.display_tag @@ -1123,55 +1065,6 @@ impl RoomMemberRepository { Ok((members?, total_count)) } - /// List all active members in a room with online status - pub async fn list_by_room_with_online( - &self, - room_id: &RoomId, - online_user_ids: &[UserId], - ) -> Result> { - let rows = sqlx::query_as!( - RoomMemberWithUserRow, - r#"SELECT - rm.room_id AS "room_id: RoomId", - rm.user_id AS "user_id: UserId", - rm.role AS "role: RoomRole", - rm.added_permissions, rm.removed_permissions, - rm.admin_added_permissions, rm.admin_removed_permissions, - rm.joined_at, - TRUE AS "is_active!", - u.username, - rm.remark_name, - rm.display_tag - FROM room_members rm - JOIN users u ON rm.user_id = u.id - WHERE rm.room_id = $1 - AND NOT EXISTS ( - SELECT 1 FROM room_member_kick_cooldowns rmkc - WHERE rmkc.room_id = rm.room_id - AND rmkc.user_id = rm.user_id - AND rmkc.ends_at > CURRENT_TIMESTAMP - ) - AND u.deleted_at IS NULL - ORDER BY rm.joined_at ASC"#, - room_id.as_i64() - ) - .fetch_all(self.pools.primary()) - .await?; - - let online_set: std::collections::HashSet<_> = online_user_ids - .iter() - .map(super::super::models::id::UserId::as_i64) - .collect(); - - rows.into_iter() - .map(|row| { - let mut member = Self::typed_row_to_member_with_user(row)?; - member.is_online = online_set.contains(&member.user_id.as_i64()); - Ok(member) - }) - .collect() - } - /// Update member role with optimistic locking. pub async fn update_role( &self, @@ -1274,23 +1167,6 @@ impl RoomMemberRepository { member.ok_or_else(|| Error::NotFound("User is not a member of this room".to_string())) } - pub async fn update_display_info( - &self, - room_id: &RoomId, - user_id: &UserId, - remark_name: Option<&str>, - display_tag: Option<&str>, - ) -> Result { - self.update_display_info_with_executor( - room_id, - user_id, - remark_name, - display_tag, - self.pools.primary(), - ) - .await - } - /// Update member role inside an existing transaction with optimistic locking. pub async fn update_role_with_version_executor( &self, @@ -1875,47 +1751,6 @@ impl RoomMemberRepository { } } - /// Atomically grant admin-specific permission bits. - /// - /// Only applies to current members. Returns `NotFound` if the membership no longer exists. - pub async fn grant_admin_permission_atomic( - &self, - room_id: &RoomId, - user_id: &UserId, - permission: u64, - ) -> Result { - let member = sqlx::query_as!( - RoomMemberRow, - r#"UPDATE room_members - SET admin_added_permissions = admin_added_permissions | $3, - version = version + 1 - WHERE room_id = $1 AND user_id = $2 - RETURNING room_id as "room_id: RoomId", - user_id as "user_id: UserId", - role as "role: RoomRole", - added_permissions, - removed_permissions, - admin_added_permissions, - admin_removed_permissions, - remark_name, - display_tag, - joined_at, - version"#, - room_id as &RoomId, - user_id as &UserId, - permission_bits_to_i64(permission)?, - ) - .fetch_optional(self.pools.primary()) - .await? - .map(Self::typed_row_to_member) - .transpose()?; - - match member { - Some(m) => Ok(m), - None => Err(Error::NotFound("Active room member not found".to_string())), - } - } - /// Atomically grant admin-specific permission bits for an active admin that still matches the expected role. pub async fn grant_admin_permission_atomic_for_role( &self, @@ -2139,47 +1974,6 @@ impl RoomMemberRepository { } } - /// Atomically revoke admin-specific permission bits. - /// - /// Only applies to current members. Returns `NotFound` if the membership no longer exists. - pub async fn revoke_admin_permission_atomic( - &self, - room_id: &RoomId, - user_id: &UserId, - permission: u64, - ) -> Result { - let member = sqlx::query_as!( - RoomMemberRow, - r#"UPDATE room_members - SET admin_removed_permissions = admin_removed_permissions | $3, - version = version + 1 - WHERE room_id = $1 AND user_id = $2 - RETURNING room_id as "room_id: RoomId", - user_id as "user_id: UserId", - role as "role: RoomRole", - added_permissions, - removed_permissions, - admin_added_permissions, - admin_removed_permissions, - remark_name, - display_tag, - joined_at, - version"#, - room_id as &RoomId, - user_id as &UserId, - permission_bits_to_i64(permission)?, - ) - .fetch_optional(self.pools.primary()) - .await? - .map(Self::typed_row_to_member) - .transpose()?; - - match member { - Some(m) => Ok(m), - None => Err(Error::NotFound("Active room member not found".to_string())), - } - } - /// Atomically revoke admin-specific permission bits for an active admin that still matches the expected role. pub async fn revoke_admin_permission_atomic_for_role( &self, @@ -2408,34 +2202,6 @@ impl RoomMemberRepository { Ok(exists) } - pub async fn is_in_kick_cooldown_with_executor<'e, E>( - &self, - room_id: &RoomId, - user_id: &UserId, - executor: E, - ) -> Result - where - E: sqlx::PgExecutor<'e>, - { - let exists = sqlx::query_scalar!( - r#" - SELECT EXISTS( - SELECT 1 - FROM room_member_kick_cooldowns - WHERE room_id = $1 - AND user_id = $2 - AND ends_at > CURRENT_TIMESTAMP - ) as "exists!" - "#, - room_id as &RoomId, - user_id as &UserId, - ) - .fetch_one(executor) - .await?; - - Ok(exists) - } - pub async fn add_kick_cooldown_with_executor<'e, E>( &self, insert: KickCooldownInsert<'_>, @@ -2588,113 +2354,6 @@ impl RoomMemberRepository { Ok((room_ids, total_count)) } - /// Get rooms where a user is a member with full room details and member count (optimized) - /// Returns (room, role, status, `member_count`) tuples - /// - /// Uses a separate count query so out-of-range pages still return the - /// number of matching rooms. - pub async fn list_by_user_with_details( - &self, - user_id: &UserId, - pagination: PageParams, - ) -> Result<(Vec<(crate::models::Room, RoomRole, MemberStatus, i32)>, i64)> { - self.list_by_user_with_query( - user_id, - &MyRoomListQuery { - pagination, - ..Default::default() - }, - ) - .await - } - - /// List rooms where a user is a member with full room details and query semantics. - pub async fn list_by_user_with_query( - &self, - user_id: &UserId, - query: &MyRoomListQuery, - ) -> Result<(Vec<(crate::models::Room, RoomRole, MemberStatus, i32)>, i64)> { - let limit = query.pagination.limit_i64()?; - let offset = query.pagination.offset_i64()?; - let search_pattern = query.search.as_deref().and_then(ilike_contains_pattern); - let wb = Self::build_my_room_list_conditions(query); - let (count_where_sql, _) = wb.build(2)?; - let (where_sql, _) = wb.build(4)?; - let order_by_sql = Self::my_room_order_by_sql(query); - let count_sql = format!( - r" - SELECT COUNT(*) - FROM room_members rm - JOIN rooms r ON rm.room_id = r.id - WHERE rm.user_id = $1 AND {count_where_sql} - " - ); - let total_count = Self::bind_my_room_count_filters( - sqlx::query_scalar::<_, i64>(trusted_dynamic_sql(count_sql)).bind(user_id), - search_pattern.as_deref(), - ) - .fetch_one(self.pools.primary()) - .await?; - let sql = format!( - r" - SELECT - r.id, r.name, r.description, - r.cover_file_reference_id, - rc.id AS category_id, - rc.key AS category_key, - rc.name AS category_name, - rc.description AS category_description, - rc.sort_order AS category_sort_order, - rc.is_enabled AS category_is_enabled, - rc.created_at AS category_created_at, - rc.updated_at AS category_updated_at, - r.created_by, r.closed_at, - r.created_at, r.updated_at, r.deleted_at, r.version, r.last_activity_at, - r.is_public, - {ACTIVE_ROOM_BAN_EXISTS_SQL} AS is_banned, - rm.role as user_role, - COUNT(rm2.user_id)::int as member_count - FROM room_members rm - JOIN rooms r ON rm.room_id = r.id - LEFT JOIN room_categories rc ON rc.id = r.category_id - LEFT JOIN room_members rm2 - ON r.id = rm2.room_id - AND NOT EXISTS ( - SELECT 1 FROM room_member_kick_cooldowns rmkc2 - WHERE rmkc2.room_id = rm2.room_id - AND rmkc2.user_id = rm2.user_id - AND rmkc2.ends_at > CURRENT_TIMESTAMP - ) - WHERE rm.user_id = $1 AND {where_sql} - GROUP BY r.id, r.name, r.description, r.cover_file_reference_id, r.category_id, - r.created_by, r.closed_at, - r.created_at, r.updated_at, r.deleted_at, r.version, r.last_activity_at, - rc.id, rc.key, rc.name, rc.description, rc.sort_order, rc.is_enabled, - rc.created_at, rc.updated_at, - rm.role, rm.joined_at, rm.last_visited_at, rm.visit_count - ORDER BY {order_by_sql} - LIMIT $2 OFFSET $3 - " - ); - - let rows = Self::bind_my_room_filters( - sqlx::query_as::<_, MyRoomListRow>(trusted_dynamic_sql(sql)) - .bind(user_id) - .bind(limit) - .bind(offset), - search_pattern.as_deref(), - ) - .fetch_all(self.pools.primary()) - .await?; - - let results = rows - .into_iter() - .map(Self::my_room_result_from_row) - .collect(); - - Ok((results, total_count)) - } - /// List joined rooms whose creator account still exists. /// /// Moderation and account-status restrictions are represented through the @@ -2704,7 +2363,7 @@ impl RoomMemberRepository { &self, user_id: &UserId, query: &MyRoomListQuery, - ) -> Result<(Vec<(crate::models::Room, RoomRole, MemberStatus, i32)>, i64)> { + ) -> Result<(Vec<(crate::models::Room, RoomRole, i32)>, i64)> { self.list_accessible_by_user_with_query_from_pool(user_id, query, self.pools.read()) .await } @@ -2714,7 +2373,7 @@ impl RoomMemberRepository { user_id: &UserId, query: &MyRoomListQuery, pool: &PgPool, - ) -> Result<(Vec<(crate::models::Room, RoomRole, MemberStatus, i32)>, i64)> { + ) -> Result<(Vec<(crate::models::Room, RoomRole, i32)>, i64)> { let limit = query.pagination.limit_i64()?; let offset = query.pagination.offset_i64()?; let search_pattern = query.search.as_deref().and_then(ilike_contains_pattern); @@ -2809,8 +2468,7 @@ impl RoomMemberRepository { rm.remark_name, rm.display_tag, rm.joined_at, - u.username, - TRUE AS "is_active!" + u.username FROM room_members rm JOIN users u ON rm.user_id = u.id WHERE rm.room_id = $1 AND u.deleted_at IS NULL @@ -2943,7 +2601,6 @@ impl RoomMemberRepository { user_id: row.user_id, username: row.username, role: row.role, - status: MemberStatus::Active, added_permissions: db_permission_i64_to_u64( row.added_permissions, "added_permissions", @@ -2964,7 +2621,6 @@ impl RoomMemberRepository { display_tag: row.display_tag, joined_at: row.joined_at, is_online: false, - is_active: row.is_active, }) } @@ -2973,7 +2629,6 @@ impl RoomMemberRepository { room_id: row.room_id, user_id: row.user_id, role: row.role, - status: MemberStatus::Active, added_permissions: db_permission_i64_to_u64( row.added_permissions, "added_permissions", diff --git a/synctv-core/src/repository/room_settings.rs b/synctv-core/src/repository/room_settings.rs index 4f17013f..35e62752 100644 --- a/synctv-core/src/repository/room_settings.rs +++ b/synctv-core/src/repository/room_settings.rs @@ -300,15 +300,4 @@ impl RoomSettingsRepository { row.ok_or(Error::OptimisticLockConflict) } } - - pub async fn delete_all(&self, room_id: &RoomId) -> Result<()> { - sqlx::query!( - "DELETE FROM room_settings WHERE room_id = $1", - room_id as &RoomId, - ) - .execute(self.pools.primary()) - .await?; - - Ok(()) - } } diff --git a/synctv-core/src/repository/room_taxonomy.rs b/synctv-core/src/repository/room_taxonomy.rs index 5fe4e070..3054959a 100644 --- a/synctv-core/src/repository/room_taxonomy.rs +++ b/synctv-core/src/repository/room_taxonomy.rs @@ -132,12 +132,6 @@ impl From for RoomLabel { } } -#[derive(Debug, Clone)] -pub struct RoomTaxonomyAssignment { - pub category_id: Option, - pub label_ids: Vec, -} - #[derive(Clone)] pub struct RoomTaxonomyRepository { pools: RepoPools, @@ -168,20 +162,6 @@ impl RoomTaxonomyRepository { self.pools.read() } - pub async fn category_id_by_key(&self, key: &str) -> Result> { - let id = sqlx::query_scalar!( - r#" - SELECT id AS "id: RoomCategoryId" - FROM room_categories - WHERE key = $1 - "#, - key - ) - .fetch_optional(self.pools.primary()) - .await?; - Ok(id) - } - pub async fn list_categories(&self, enabled_only: bool) -> Result> { self.list_categories_from_pool(enabled_only, self.pools.primary()) .await @@ -294,30 +274,6 @@ impl RoomTaxonomyRepository { Ok(row.map(Into::into)) } - pub async fn get_label(&self, id: RoomLabelId) -> Result> { - let row = sqlx::query_as!( - RoomLabelRow, - r#" - SELECT id AS "id: RoomLabelId", - key, - name, - description, - color, - category_id AS "category_id: RoomCategoryId", - sort_order, - is_enabled, - created_at, - updated_at - FROM room_labels - WHERE id = $1 - "#, - id.as_i64() - ) - .fetch_optional(self.pools.primary()) - .await?; - Ok(row.map(Into::into)) - } - pub async fn categories_by_ids( &self, category_ids: &[RoomCategoryId], @@ -683,22 +639,4 @@ impl RoomTaxonomyRepository { } Ok(()) } - - pub async fn labels_for_room_creation_request( - &self, - request_id: RoomId, - ) -> Result> { - let labels = sqlx::query_scalar!( - r#" - SELECT label_id AS "label_id: RoomLabelId" - FROM room_creation_request_labels - WHERE request_id = $1 - ORDER BY label_id ASC - "#, - request_id as RoomId - ) - .fetch_all(self.pools.primary()) - .await?; - Ok(labels) - } } diff --git a/synctv-core/src/repository/settings.rs b/synctv-core/src/repository/settings.rs index d302a177..c11beae4 100644 --- a/synctv-core/src/repository/settings.rs +++ b/synctv-core/src/repository/settings.rs @@ -73,59 +73,6 @@ impl SettingsRepository { Ok(version) } - /// Insert or update a setting using the exact version reserved in the - /// runtime-setting version fence. - pub async fn upsert_with_exact_version( - &self, - key: &str, - group_name: &str, - value: &str, - expected_version: i32, - new_version: i32, - ) -> Result { - if new_version <= expected_version { - return Err(Error::InvalidInput(format!( - "new setting version {new_version} must be greater than expected version {expected_version}" - ))); - } - - let row = sqlx::query_as!( - RuntimeSetting, - r" - INSERT INTO settings (key, group_name, value, version) - VALUES ($1, $2, $3, $5) - ON CONFLICT (key) DO UPDATE - SET group_name = EXCLUDED.group_name, - value = EXCLUDED.value, - version = EXCLUDED.version, - updated_at = NOW() - WHERE settings.version = $4 - RETURNING key, group_name, value, version, created_at, updated_at - ", - key, - group_name, - value, - expected_version, - new_version, - ) - .fetch_optional(&self.pool) - .await?; - - if let Some(row) = row { - debug!( - "Upserted setting '{}' with exact version ({} -> {})", - key, expected_version, row.version - ); - Ok(row) - } else { - debug!( - "Optimistic lock conflict for exact-version setting upsert '{}' (expected {})", - key, expected_version - ); - Err(Error::OptimisticLockConflict) - } - } - /// Update a setting value by key /// /// A database trigger on the `settings` table automatically sends a diff --git a/synctv-core/src/repository/user_email.rs b/synctv-core/src/repository/user_email.rs index 06c69929..24b742a3 100644 --- a/synctv-core/src/repository/user_email.rs +++ b/synctv-core/src/repository/user_email.rs @@ -367,43 +367,6 @@ impl UserEmailRepository { Ok(row.map(Into::into)) } - pub async fn get_by_email_with_executor<'e, E>( - &self, - email: &str, - executor: E, - ) -> Result> - where - E: sqlx::PgExecutor<'e>, - { - let row = sqlx::query_as!( - UserWithEmailRow, - r#" - SELECT u.id AS "id!: UserId", - u.username AS "username!", - u.signup_method AS "signup_method!: SignupMethod", - u.role AS "role!: UserRole", - u.avatar_file_reference_id, - u.status AS "status!: UserStatus", - u.is_banned AS "is_banned!", - u.banned_at, - u.banned_by AS "banned_by?: UserId", - u.banned_reason, - u.created_at AS "created_at!", - u.updated_at AS "updated_at!", - u.version AS "version!", - u.deleted_at, - aei.email - FROM user_account_profiles u - JOIN auth_email_identities aei ON aei.user_id = u.id - WHERE LOWER(aei.email) = LOWER($1) AND u.deleted_at IS NULL AND aei.deleted_at IS NULL - "#, - email - ) - .fetch_optional(executor) - .await?; - Ok(row.map(Into::into)) - } - pub async fn email_exists(&self, email: &str) -> Result { self.email_exists_with_executor(email, &self.pool).await } diff --git a/synctv-core/src/repository/user_oauth_provider.rs b/synctv-core/src/repository/user_oauth_provider.rs index d96b5d47..4fda8764 100644 --- a/synctv-core/src/repository/user_oauth_provider.rs +++ b/synctv-core/src/repository/user_oauth_provider.rs @@ -231,14 +231,6 @@ impl UserOAuthProviderRepository { Ok(result.rows_affected() > 0) } - /// Delete all `OAuth2` provider mappings for a user (all providers). - /// - /// Used during user deletion to clean up all OAuth bindings. - pub async fn delete_all_for_user(&self, user_id: &UserId) -> Result { - self.delete_all_for_user_with_executor(user_id, &self.pool) - .await - } - /// Delete all `OAuth2` provider mappings for a user using a provided executor (pool or transaction). /// /// Used during user deletion to atomically clean up OAuth bindings within the same diff --git a/synctv-core/src/repository/user_password.rs b/synctv-core/src/repository/user_password.rs index fbd22c8a..0218479e 100644 --- a/synctv-core/src/repository/user_password.rs +++ b/synctv-core/src/repository/user_password.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use sqlx::PgPool; use crate::{ - models::{OpaquePasswordRecord, SignupMethod, User, UserId, UserRole, UserStatus}, + models::{OpaquePasswordRecord, User, UserId}, Error, Result, }; @@ -48,114 +48,6 @@ pub struct PasswordCredentialState { pub version: i32, } -#[derive(Debug, Clone)] -pub struct UserWithPasswordCredential { - pub user: User, - pub credential_state: PasswordCredentialState, - pub opaque: Option, -} - -#[derive(sqlx::FromRow)] -struct UserWithPasswordCredentialRow { - id: UserId, - username: String, - signup_method: SignupMethod, - role: UserRole, - avatar_file_reference_id: Option, - status: UserStatus, - is_banned: bool, - banned_at: Option>, - banned_by: Option, - banned_reason: Option, - created_at: DateTime, - updated_at: DateTime, - user_version: i32, - deleted_at: Option>, - changed_at: Option>, - credential_version: Option, - opaque_record: Option>, - opaque_credential_identifier: Option>, - opaque_ciphersuite: Option, - opaque_server_setup_version: Option, -} - -impl UserWithPasswordCredentialRow { - fn to_user(&self) -> User { - User { - id: self.id, - username: self.username.clone(), - role: self.role, - avatar_file_reference_id: self.avatar_file_reference_id, - status: self.status, - is_banned: self.is_banned, - banned_at: self.banned_at, - banned_by: self.banned_by, - banned_reason: self.banned_reason.clone(), - signup_method: self.signup_method, - created_at: self.created_at, - updated_at: self.updated_at, - version: self.user_version, - deleted_at: self.deleted_at, - } - } -} - -impl TryFrom for UserWithPasswordCredential { - type Error = Error; - - fn try_from(row: UserWithPasswordCredentialRow) -> Result { - let user = row.to_user(); - let user_id = row.id; - let user_created_at = row.created_at; - let credential_state = match (row.changed_at, row.credential_version) { - (Some(changed_at), Some(version)) => PasswordCredentialState { - user_id, - changed_at, - version, - }, - (None, None) => PasswordCredentialState { - user_id, - changed_at: user_created_at, - version: 0, - }, - _ => { - return Err(Error::Internal( - "Incomplete password credential state".to_string(), - )); - } - }; - let opaque = match ( - row.opaque_record, - row.opaque_credential_identifier, - row.opaque_ciphersuite, - row.opaque_server_setup_version, - ) { - (Some(record), Some(credential_identifier), Some(ciphersuite), Some(version)) => { - Some(StoredOpaquePasswordCredential { - record: OpaquePasswordRecord { - record, - credential_identifier, - ciphersuite, - server_setup_version: version, - }, - }) - } - (None, None, None, None) => None, - _ => { - return Err(Error::Internal( - "Incomplete OPAQUE password credential material".to_string(), - )); - } - }; - - Ok(Self { - user, - credential_state, - opaque, - }) - } -} - impl<'a> PasswordCredentialMaterial<'a> { #[must_use] pub const fn opaque_only(opaque_record: &'a OpaquePasswordRecord) -> Self { @@ -239,85 +131,6 @@ impl UserPasswordRepository { Ok(row.map(Into::into)) } - pub async fn get_by_username_with_credential( - &self, - username: &str, - ) -> Result> { - let row = sqlx::query_as!( - UserWithPasswordCredentialRow, - r#" - SELECT u.id AS "id!: UserId", - u.username AS "username!", - u.signup_method AS "signup_method!: SignupMethod", - u.role AS "role!: UserRole", - u.avatar_file_reference_id, - u.status AS "status!: UserStatus", - u.is_banned AS "is_banned!", - u.banned_at, - u.banned_by AS "banned_by?: UserId", - u.banned_reason, - u.created_at AS "created_at!", - u.updated_at AS "updated_at!", - u.version AS "user_version!", - u.deleted_at, - apc.changed_at, - apc.version AS credential_version, - apc.opaque_record, - apc.opaque_credential_identifier, - apc.opaque_ciphersuite, - apc.opaque_server_setup_version - FROM user_account_profiles u - LEFT JOIN auth_password_credentials apc ON apc.user_id = u.id - WHERE LOWER(u.username) = LOWER($1) AND u.deleted_at IS NULL - "#, - username - ) - .fetch_optional(&self.pool) - .await?; - - row.map(TryInto::try_into).transpose() - } - - pub async fn get_by_email_with_credential( - &self, - email: &str, - ) -> Result> { - let row = sqlx::query_as!( - UserWithPasswordCredentialRow, - r#" - SELECT u.id AS "id!: UserId", - u.username AS "username!", - u.signup_method AS "signup_method!: SignupMethod", - u.role AS "role!: UserRole", - u.avatar_file_reference_id, - u.status AS "status!: UserStatus", - u.is_banned AS "is_banned!", - u.banned_at, - u.banned_by AS "banned_by?: UserId", - u.banned_reason, - u.created_at AS "created_at!", - u.updated_at AS "updated_at!", - u.version AS "user_version!", - u.deleted_at, - apc.changed_at, - apc.version AS credential_version, - apc.opaque_record, - apc.opaque_credential_identifier, - apc.opaque_ciphersuite, - apc.opaque_server_setup_version - FROM user_account_profiles u - LEFT JOIN auth_password_credentials apc ON apc.user_id = u.id - JOIN auth_email_identities aei ON aei.user_id = u.id - WHERE LOWER(aei.email) = LOWER($1) AND u.deleted_at IS NULL AND aei.deleted_at IS NULL - "#, - email - ) - .fetch_optional(&self.pool) - .await?; - - row.map(TryInto::try_into).transpose() - } - pub async fn get_opaque_credential( &self, user_id: &UserId, @@ -374,10 +187,6 @@ impl UserPasswordRepository { .map_err(Error::Database) } - pub async fn has_credential(&self, user_id: &UserId) -> Result { - self.has_opaque_credential(user_id).await - } - pub async fn get_state(&self, user_id: &UserId) -> Result { let row = sqlx::query_as!( PasswordCredentialStateRow, diff --git a/synctv-core/src/repository/user_preferences.rs b/synctv-core/src/repository/user_preferences.rs index a00d9f90..4a56e7ec 100644 --- a/synctv-core/src/repository/user_preferences.rs +++ b/synctv-core/src/repository/user_preferences.rs @@ -74,22 +74,6 @@ impl UserPreferencesRepository { )) } - pub async fn set_two_factor_enabled_with_executor<'e, E>( - &self, - user_id: &UserId, - enabled: bool, - executor: E, - ) -> Result - where - E: sqlx::PgExecutor<'e>, - { - let update = UserPreferencesUpdate { - two_factor_enabled: Some(enabled), - ..UserPreferencesUpdate::default() - }; - self.update_with_executor(user_id, &update, executor).await - } - pub async fn update( &self, user_id: &UserId, @@ -288,14 +272,4 @@ impl UserPreferencesRepository { Ok(enabled) } - - pub async fn assert_can_enable_two_factor(&self, user_id: &UserId) -> Result { - let factors = self.auth_factors(user_id).await?; - if !factors.supports_two_factor() { - return Err(Error::InvalidInput( - "Two-factor authentication requires at least two usable verification methods: password, passkey, authenticator app, or verified email".to_string(), - )); - } - Ok(factors) - } } diff --git a/synctv-core/src/repository/webauthn_credential.rs b/synctv-core/src/repository/webauthn_credential.rs index c63587df..7b180afe 100644 --- a/synctv-core/src/repository/webauthn_credential.rs +++ b/synctv-core/src/repository/webauthn_credential.rs @@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize}; use sqlx::PgPool; use webauthn_rs::prelude::Passkey; -use crate::repository::required_count; use crate::{models::UserId, Error, Result}; #[derive(Debug, Clone)] @@ -178,11 +177,6 @@ impl WebAuthnCredentialRepository { Ok(()) } - pub async fn delete_for_user(&self, user_id: &UserId, credential_id: &[u8]) -> Result { - self.delete_for_user_with_executor(user_id, credential_id, &self.pool) - .await - } - pub async fn delete_for_user_with_executor<'e, E>( &self, user_id: &UserId, @@ -202,23 +196,6 @@ impl WebAuthnCredentialRepository { Ok(result.rows_affected() > 0) } - pub async fn count_by_user_with_executor<'e, E>( - &self, - user_id: &UserId, - executor: E, - ) -> Result - where - E: sqlx::PgExecutor<'e>, - { - let count = sqlx::query_scalar!( - "SELECT COUNT(*) FROM auth_webauthn_credentials WHERE user_id = $1", - user_id as &UserId, - ) - .fetch_one(executor) - .await?; - required_count(count, "WebAuthn credential") - } - pub async fn exists_for_user_with_executor<'e, E>( &self, user_id: &UserId, diff --git a/synctv-core/src/resilience.rs b/synctv-core/src/resilience.rs index 93ab4092..8324ebe5 100644 --- a/synctv-core/src/resilience.rs +++ b/synctv-core/src/resilience.rs @@ -71,20 +71,6 @@ pub mod timeout { self.redis = timeout; self } - - /// Set HTTP request timeout - #[must_use] - pub const fn with_http_timeout(mut self, timeout: Duration) -> Self { - self.http = timeout; - self - } - - /// Set the remote transport call timeout. - #[must_use] - pub const fn with_remote_transport_timeout(mut self, timeout: Duration) -> Self { - self.remote_transport = timeout; - self - } } } @@ -164,15 +150,6 @@ pub mod circuit_breaker { let policy = failsafe::failure_policy::consecutive_failures(failure_threshold, backoff); failsafe::Config::new().failure_policy(policy).build() } - - /// Create a circuit breaker with default settings (5 failures, 10-60s backoff) - #[must_use] - pub fn create_default() -> failsafe::StateMachine< - failsafe::failure_policy::ConsecutiveFailures, - (), - > { - create(5, Duration::from_secs(10), Duration::from_mins(1)) - } } #[cfg(test)] diff --git a/synctv-core/src/service/audit.rs b/synctv-core/src/service/audit.rs index 53237f8d..d7246ad8 100644 --- a/synctv-core/src/service/audit.rs +++ b/synctv-core/src/service/audit.rs @@ -48,7 +48,6 @@ //! buffer capacity via configuration use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; @@ -67,21 +66,6 @@ const FLUSH_BATCH_SIZE: usize = 100; /// Flush interval in seconds const FLUSH_INTERVAL_SECS: u64 = 5; -/// Audit log entry -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AuditLog { - pub id: String, - pub actor_id: String, - pub actor_username: String, - pub action: AuditAction, - pub target_type: AuditTargetType, - pub target_id: Option, - pub details: AuditDetails, - pub ip_address: Option, - pub user_agent: Option, - pub created_at: DateTime, -} - /// Parameters for logging an audit event #[derive(Debug, Clone)] pub struct AuditEventParams { @@ -318,66 +302,6 @@ impl AuditService { Ok(()) } - /// Log user creation - pub async fn log_user_created( - &self, - actor_id: String, - actor_username: String, - target_user_id: String, - ) -> Result<()> { - self.log(AuditEventParams { - actor_id, - actor_username, - action: AuditAction::UserCreated, - target_type: AuditTargetType::User, - target_id: Some(target_user_id), - details: AuditDetails::reason("User created via admin panel"), - ip_address: None, - user_agent: None, - }) - .await - } - - /// Log user ban - pub async fn log_user_banned( - &self, - actor_id: String, - actor_username: String, - target_user_id: String, - ) -> Result<()> { - self.log(AuditEventParams { - actor_id, - actor_username, - action: AuditAction::UserBanned, - target_type: AuditTargetType::User, - target_id: Some(target_user_id), - details: AuditDetails::reason("User banned by admin"), - ip_address: None, - user_agent: None, - }) - .await - } - - /// Log room deletion - pub async fn log_room_deleted( - &self, - actor_id: String, - actor_username: String, - room_id: String, - ) -> Result<()> { - self.log(AuditEventParams { - actor_id, - actor_username, - action: AuditAction::RoomDeleted, - target_type: AuditTargetType::Room, - target_id: Some(room_id), - details: AuditDetails::reason("Room deleted by admin"), - ip_address: None, - user_agent: None, - }) - .await - } - pub async fn log_stream_kicked(&self, request: StreamKickAuditRequest) -> Result<()> { let target_id = format!("{}:{}", request.room_id, request.media_id); self.log(AuditEventParams { @@ -427,24 +351,9 @@ impl AuditService { }) .await } - - /// Log a user logout event. - /// - /// Records when a user explicitly logs out (access token blacklisted). - pub async fn log_user_logout( - &self, - user_id: String, - username: String, - ip_address: Option, - user_agent: Option, - ) -> Result<()> { - self.log(user_logout_event_params( - user_id, username, ip_address, user_agent, - )) - .await - } } +#[cfg(test)] fn user_logout_event_params( user_id: String, username: String, diff --git a/synctv-core/src/service/audit_partition_manager.rs b/synctv-core/src/service/audit_partition_manager.rs index 1e9e32d1..f3eb42bd 100644 --- a/synctv-core/src/service/audit_partition_manager.rs +++ b/synctv-core/src/service/audit_partition_manager.rs @@ -139,24 +139,6 @@ impl AuditPartitionManager { Ok(result) } - /// Create a partition for a specific date - pub async fn create_partition(&self, date: chrono::NaiveDate) -> Result { - info!("Creating audit log partition for date: {}", date); - - let mut conn = acquire_unbounded_ddl_connection(&self.pool) - .await - .internal_with_err("Failed to acquire DDL connection for single partition creation")?; - let partition_name = Self::create_partition_detail_with_connection(&mut conn, date) - .await? - .partition_name; - - Ok(PartitionInfo { - partition: partition_name, - row_count: 0, - size_centi_mib: 0, - }) - } - async fn create_partition_detail_with_connection( conn: &mut PoolConnection, date: chrono::NaiveDate, diff --git a/synctv-core/src/service/auth/brute_force.rs b/synctv-core/src/service/auth/brute_force.rs index 3bac1552..b870ef17 100644 --- a/synctv-core/src/service/auth/brute_force.rs +++ b/synctv-core/src/service/auth/brute_force.rs @@ -479,13 +479,6 @@ impl BruteForceProtection { Arc::clone(&self.username_tracker) } - /// Test-only accessor for the IP tracker. - #[cfg(test)] - #[must_use] - pub fn ip_tracker(&self) -> Arc { - Arc::clone(&self.ip_tracker) - } - /// Check if a login attempt is allowed for the given username and optional IP. /// /// Returns `Ok(())` if the attempt is allowed, or an error: diff --git a/synctv-core/src/service/auth/brute_force/tracker.rs b/synctv-core/src/service/auth/brute_force/tracker.rs index 5e651fce..1980ac9d 100644 --- a/synctv-core/src/service/auth/brute_force/tracker.rs +++ b/synctv-core/src/service/auth/brute_force/tracker.rs @@ -164,15 +164,6 @@ impl RedisAttemptTracker { } } - #[must_use] - pub fn new_fail_closed( - conn: Arc>, - max_capacity: u64, - ttl_secs: u64, - ) -> Self { - Self::from_runtime_fail_closed(crate::shared_runtime(conn), max_capacity, ttl_secs) - } - #[must_use] pub fn from_runtime_fail_closed( conn: Arc, diff --git a/synctv-core/src/service/auth/token_blacklist/stores.rs b/synctv-core/src/service/auth/token_blacklist/stores.rs index ffeef000..b6fea9fd 100644 --- a/synctv-core/src/service/auth/token_blacklist/stores.rs +++ b/synctv-core/src/service/auth/token_blacklist/stores.rs @@ -420,18 +420,9 @@ impl TieredTokenBlacklistStore { durable: impl TokenBlacklistStore + 'static, redis_runtime: Option>, key_prefix: String, - ) -> Self { - Self::from_durable_runtime(Arc::new(durable), redis_runtime, key_prefix) - } - - #[must_use] - pub fn from_durable_runtime( - durable: Arc, - redis_runtime: Option>, - key_prefix: String, ) -> Self { Self { - durable, + durable: Arc::new(durable), redis_runtime, // L1 blacklist: max 100k entries, background eviction at 120s l1_blacklist: moka::future::Cache::builder() diff --git a/synctv-core/src/service/auth/validator.rs b/synctv-core/src/service/auth/validator.rs index a4b74f13..9a5336b2 100644 --- a/synctv-core/src/service/auth/validator.rs +++ b/synctv-core/src/service/auth/validator.rs @@ -4,7 +4,7 @@ //! request entrypoints. use super::{jwt::JwtService, Claims}; -use crate::{models::UserId, Error, Result}; +use crate::{Error, Result}; use std::sync::Arc; /// JWT validator for authentication credentials. @@ -56,20 +56,6 @@ impl JwtValidator { pub fn validate_token(&self, token: &str) -> Result { self.jwt_service.verify_access_token(token) } - - /// Validate JWT token and return user ID - /// - /// Convenience method that extracts just the `user_id` from the token. - /// - /// # Arguments - /// * `token` - JWT token string - /// - /// # Returns - /// User ID extracted from the token - pub fn validate_and_extract_user_id(&self, token: &str) -> Result { - let claims = self.validate_token(token)?; - Ok(claims.user_id()) - } } impl std::fmt::Debug for JwtValidator { @@ -96,28 +82,12 @@ impl JwtValidator { let token = Self::extract_bearer_token(auth_header)?; self.validate_token(&token) } - - /// Validate JWT from an Authorization header value and extract user ID. - /// - /// Convenience method for call sites that only need the `user_id`. - /// - /// # Arguments - /// * `auth_header` - Authorization header value (e.g., "Bearer ") - /// - /// # Returns - /// User ID extracted from the token - pub fn validate_authorization_header_extract_user_id( - &self, - auth_header: &str, - ) -> Result { - let claims = self.validate_authorization_header(auth_header)?; - Ok(claims.user_id()) - } } #[cfg(test)] mod tests { use super::*; + use crate::models::UserId; fn ok(result: std::result::Result, context: &str) -> T { match result { @@ -195,20 +165,4 @@ mod tests { let result = validator.validate_authorization_header("Basic invalid"); assert!(matches!(result, Err(Error::Authentication(_)))); } - - #[test] - fn test_validate_authorization_header_extract_user_id() { - let jwt_service = create_test_jwt_service(); - let validator = JwtValidator::new(jwt_service.clone()); - - let token = create_test_token(&jwt_service, 98_003); - - let user_id = - validator.validate_authorization_header_extract_user_id(&format!("Bearer {token}")); - let user_id = ok( - user_id, - "authorization header bearer token user ID should extract", - ); - assert_eq!(user_id.to_string(), "98003"); - } } diff --git a/synctv-core/src/service/chat.rs b/synctv-core/src/service/chat.rs index 5f0e1150..9ed51e5b 100644 --- a/synctv-core/src/service/chat.rs +++ b/synctv-core/src/service/chat.rs @@ -20,10 +20,10 @@ use crate::{ ChatPinEvent, ChatPinnedMessage, ChatPlaybackMessagesQuery, ChatReactionUsersCursor, ChatReactionUsersPage, ChatReadStateWithUnread, ChatSearchMessagesPage, ChatSearchMessagesQuery, CompleteFileUploadSession, CompleteFileUploadSessionResult, - CreateChatAttachmentUploadSession, DeleteChatMessage, EditChatMessage, FileBlob, - FileObjectDownload, FileRangeRequest, FileUploadRange, FileUploadSessionCreateResult, - GetFileObject, MarkChatRead, PinChatMessage, RoomId, SendChatMessage, SetChatReaction, - StoreFileUpload, StoreFileUploadResult, SubmittedFileReference, SubmittedFileReferenceKind, + CreateChatAttachmentUploadSession, DeleteChatMessage, EditChatMessage, FileObjectDownload, + FileRangeRequest, FileUploadRange, FileUploadSessionCreateResult, GetFileObject, + MarkChatRead, PinChatMessage, RoomId, SendChatMessage, SetChatReaction, StoreFileUpload, + StoreFileUploadResult, SubmittedFileReference, SubmittedFileReferenceKind, UnpinChatMessage, UserId, CHAT_PIN_NOTE_MAX_CHARS, }, repository::{ @@ -107,20 +107,6 @@ impl ChatMessageCleanupService { } } - async fn cleanup_room_cap(&self, room_id: RoomId, keep_count: i64) -> Result { - super::cleanup_ops::cleanup_chat_messages_with_files( - &self.pool, - Some(&self.file_storage_service), - super::cleanup_ops::ChatMessageCleanupScope::RoomCap { - room_id, - keep_count, - }, - FileStorageCleanupOrigin::ReferenceCapExceeded, - "room cap purge", - ) - .await - } - async fn cleanup_active_rooms_cap( &self, keep_count: i64, @@ -271,30 +257,6 @@ impl ChatService { .await } - pub async fn get_attachment_object( - &self, - encoded_object_key: &str, - read_token: &str, - ) -> Result { - self.get_attachment_object_range(encoded_object_key, read_token, None) - .await - } - - pub async fn get_attachment_object_range( - &self, - encoded_object_key: &str, - read_token: &str, - range: Option, - ) -> Result { - self.file_storage_service - .get_object(GetFileObject { - encoded_object_key: encoded_object_key.to_string(), - read_token: read_token.to_string(), - range, - }) - .await - } - pub async fn get_attachment_object_stream( &self, encoded_object_key: &str, @@ -331,52 +293,6 @@ impl ChatService { .map_err(|_| Error::Internal("max pinned chat message limit exceeds i64".to_string())) } - /// Send a chat message - /// - /// # Arguments - /// * `room_id` - Room ID - /// * `user_id` - User ID sending the message - /// * `content` - Message content - /// - /// # Returns - /// The created chat message - pub async fn send_message( - &self, - room_id: RoomId, - user_id: UserId, - content: String, - ) -> Result { - self.send_message_with_control(room_id, user_id, content, None) - .await - } - - /// Send a chat message with cooperative execution control. - pub async fn send_message_with_control( - &self, - room_id: RoomId, - user_id: UserId, - content: String, - control: Option<&ExecutionControl>, - ) -> Result { - let event = self - .send_message_event_with_control( - SendChatMessage { - room_id, - user_id, - client_message_id: None, - content, - message_type: ChatMessageType::User, - reply_to_message_id: None, - metadata: None, - attachments: Vec::new(), - mentions: Vec::new(), - }, - control, - ) - .await?; - Ok(event.message.message) - } - pub async fn send_message_event(&self, request: SendChatMessage) -> Result { Ok(self.send_message_event_outcome(request).await?.event) } @@ -389,17 +305,6 @@ impl ChatService { .await } - pub async fn send_message_event_with_control( - &self, - request: SendChatMessage, - control: Option<&ExecutionControl>, - ) -> Result { - Ok(self - .send_message_event_with_control_outcome(request, control) - .await? - .event) - } - pub async fn send_message_event_with_control_outcome( &self, mut request: SendChatMessage, @@ -620,17 +525,6 @@ impl ChatService { )) } - pub async fn get_history_with_attachments( - &self, - room_id: &RoomId, - cursor: Option, - limit: i32, - include_deleted: bool, - ) -> Result<(Vec, Option)> { - self.get_history_with_attachments_for_viewer(room_id, cursor, limit, include_deleted, None) - .await - } - pub async fn get_history_with_attachments_for_viewer( &self, room_id: &RoomId, @@ -673,14 +567,6 @@ impl ChatService { Ok(page) } - pub async fn get_playback_messages_with_attachments( - &self, - query: ChatPlaybackMessagesQuery, - ) -> Result> { - self.get_playback_messages_with_attachments_for_viewer(query, None) - .await - } - pub async fn get_playback_messages_with_attachments_for_viewer( &self, query: ChatPlaybackMessagesQuery, @@ -711,16 +597,6 @@ impl ChatService { Ok(page) } - pub async fn get_message_with_attachments( - &self, - room_id: &RoomId, - message_id: i64, - include_deleted: bool, - ) -> Result { - self.get_message_with_attachments_for_viewer(room_id, message_id, include_deleted, None) - .await - } - pub async fn get_message_with_attachments_for_viewer( &self, room_id: &RoomId, @@ -741,25 +617,6 @@ impl ChatService { Ok(message) } - pub async fn get_message_context( - &self, - room_id: &RoomId, - message_id: i64, - before_limit: i32, - after_limit: i32, - include_deleted: bool, - ) -> Result { - self.get_message_context_for_viewer( - room_id, - message_id, - before_limit, - after_limit, - include_deleted, - None, - ) - .await - } - pub async fn get_message_context_for_viewer( &self, room_id: &RoomId, @@ -1282,33 +1139,6 @@ impl ChatService { }) } - /// Delete a chat message - /// - /// # Arguments - /// * `room_id` - Room that owns the message - /// * `message_id` - Message ID to delete - /// * `user_id` - User ID requesting deletion (must be sender or have `DELETE_CHAT_MESSAGES` permission) - /// - /// # Returns - /// Result indicating success or failure - pub async fn delete_message( - &self, - room_id: &RoomId, - message_id: i64, - user_id: &UserId, - ) -> Result { - self.delete_message_event(DeleteChatMessage { - room_id: *room_id, - message_id, - user_id: *user_id, - client_operation_id: None, - reason: None, - expected_version: None, - }) - .await - .map(|_| true) - } - pub async fn list_pinned_messages( &self, room_id: &RoomId, @@ -1580,36 +1410,6 @@ impl ChatService { self.reaction_detail_cache.run_pending_tasks().await; } - /// Cleanup old chat messages for a specific room based on global settings - /// - /// # Arguments - /// * `room_id` - Room ID to cleanup - /// * `max_messages` - Maximum messages to keep (0 = unlimited) - /// - /// # Returns - /// Number of messages deleted - pub async fn cleanup_room_messages(&self, room_id: &RoomId, max_messages: u64) -> Result { - if max_messages == 0 { - return Ok(0); - } - - let deleted = self - .message_cleanup_service - .cleanup_room_cap(*room_id, max_messages_to_keep_count(max_messages)?) - .await?; - - if deleted > 0 { - debug!( - room_id = %room_id, - deleted = deleted, - max_messages = max_messages, - "Cleaned up old chat messages" - ); - } - - Ok(deleted) - } - /// Cleanup old chat messages for all rooms using global settings /// /// This method uses a single optimized SQL query with window functions to delete diff --git a/synctv-core/src/service/chat_tests.rs b/synctv-core/src/service/chat_tests.rs index f728c2f8..8b378b54 100644 --- a/synctv-core/src/service/chat_tests.rs +++ b/synctv-core/src/service/chat_tests.rs @@ -528,10 +528,22 @@ async fn send_test_chat_text( ) -> ChatMessage { ok( service - .send_message(room_id, user_id, content.to_string()) + .send_message_event(SendChatMessage { + room_id, + user_id, + client_message_id: None, + content: content.to_string(), + message_type: ChatMessageType::User, + reply_to_message_id: None, + metadata: None, + attachments: Vec::new(), + mentions: Vec::new(), + }) .await, "chat message should send", ) + .message + .message } fn test_chat_search_query( @@ -584,7 +596,7 @@ async fn chat_search_filters_by_user_and_paginates_matches() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Chat Search Room".to_string(), @@ -1057,7 +1069,7 @@ async fn database_file_storage_roundtrips_uploaded_object() { ), "room service should build", ); - let (_room, _) = ok( + let _room = ok( room_service .create_room( "Database Image Room".to_string(), @@ -1255,7 +1267,7 @@ async fn database_file_storage_rejects_checksum_mismatch() { ), "room service should build", ); - let (_room, _) = ok( + let _room = ok( room_service .create_room( "Database Image Checksum Room".to_string(), @@ -1782,7 +1794,7 @@ async fn metadata_only_attachment_token_is_stripped_before_persistence() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Attachment Token Strip Room".to_string(), @@ -1907,7 +1919,7 @@ async fn attachment_message_does_not_require_inline_content_reference() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Attachment Inline Free Room".to_string(), @@ -1991,7 +2003,7 @@ async fn visible_chat_attachment_can_be_reused_without_uploading_bytes() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Attachment Reuse Room".to_string(), @@ -2142,7 +2154,7 @@ async fn chat_attachment_reuse_token_requires_source_room_visibility() { ), "room service should build", ); - let (source_room, _) = ok( + let source_room = ok( room_service .create_room( "Attachment Reuse Source".to_string(), @@ -2160,7 +2172,7 @@ async fn chat_attachment_reuse_token_requires_source_room_visibility() { .await, "member should join source room", ); - let (target_room, _) = ok( + let target_room = ok( room_service .create_room( "Attachment Reuse Target".to_string(), @@ -2306,7 +2318,7 @@ async fn chat_mentions_must_point_to_inline_at_token() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Mention Inline Required Room".to_string(), @@ -2407,7 +2419,7 @@ async fn custom_file_storage_can_normalize_attachment_metadata() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Attachment Storage Room".to_string(), @@ -2499,7 +2511,7 @@ async fn deleting_attachment_message_releases_attachment_objects() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Delete Attachment Room".to_string(), @@ -2612,7 +2624,7 @@ async fn cleanup_all_rooms_releases_attachment_objects() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Cleanup Attachment Room".to_string(), @@ -2710,7 +2722,7 @@ async fn concurrent_idempotent_send_returns_existing_created_event() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Idempotent Send Room".to_string(), @@ -2835,7 +2847,7 @@ async fn concurrent_same_edit_returns_existing_edit_event() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Concurrent Edit Room".to_string(), @@ -2960,7 +2972,7 @@ async fn concurrent_same_delete_returns_existing_delete_event() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Concurrent Delete Room".to_string(), @@ -3096,7 +3108,7 @@ async fn chat_reactions_update_history_and_emit_reaction_events() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Reaction Room".to_string(), @@ -3331,7 +3343,7 @@ async fn pinned_chat_messages_list_and_emit_state_events() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Pinned Chat Room".to_string(), @@ -3594,7 +3606,7 @@ async fn pinning_chat_message_respects_runtime_room_pin_limit() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Pinned Chat Limit Room".to_string(), @@ -3727,7 +3739,7 @@ async fn read_state_tracks_unread_count_and_stays_monotonic() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Read State Room".to_string(), @@ -3917,7 +3929,7 @@ async fn message_context_returns_messages_around_anchor_in_chronological_order() ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Context Room".to_string(), @@ -3953,7 +3965,7 @@ async fn message_context_returns_messages_around_anchor_in_chronological_order() let context = ok( service - .get_message_context(&room.id, messages[2].id, 2, 2, false) + .get_message_context_for_viewer(&room.id, messages[2].id, 2, 2, false, None) .await, "context should load", ); @@ -4009,7 +4021,7 @@ async fn chat_text_validation_rejects_whitespace_send_and_edit() { ), "room service should build", ); - let (room, _) = ok( + let room = ok( room_service .create_room( "Text Validation Room".to_string(), diff --git a/synctv-core/src/service/cleanup.rs b/synctv-core/src/service/cleanup.rs index 2e7f0f69..bd8d1828 100644 --- a/synctv-core/src/service/cleanup.rs +++ b/synctv-core/src/service/cleanup.rs @@ -353,7 +353,12 @@ impl CleanupService { // 2b. Purge only independently deleted resources. Account- and // room-propagated rows remain governed by their aggregate recovery window. if self.config.resource_soft_delete_retention_days > 0 { - match self.purge_soft_deleted_resources().await { + match cleanup_ops::cleanup_soft_deleted_media_and_playlists( + &self.pool, + self.config.resource_soft_delete_retention_days, + ) + .await + { Ok(cleanup) => { result.media_purged = cleanup.media_purged; result.playlists_purged = cleanup.playlists_purged; @@ -417,7 +422,12 @@ impl CleanupService { // 5. Delete old read notifications if self.config.notification_retention_days > 0 { - match self.delete_old_notifications().await { + match cleanup_ops::delete_old_read_notifications( + &self.pool, + self.config.notification_retention_days, + ) + .await + { Ok(count) => { result.notifications_deleted += count; if count > 0 { @@ -467,9 +477,11 @@ impl CleanupService { } }; if chat_message_event_retention_seconds > 0 { - match self - .cleanup_chat_message_events(chat_message_event_retention_seconds) - .await + match cleanup_ops::delete_old_chat_message_events( + &self.pool, + chat_message_event_retention_seconds, + ) + .await { Ok(count) => { result.chat_message_events_deleted = count; @@ -482,7 +494,12 @@ impl CleanupService { } if self.config.room_resource_event_retention_seconds > 0 { - match self.cleanup_room_resource_events().await { + match cleanup_ops::delete_old_room_resource_events( + &self.pool, + self.config.room_resource_event_retention_seconds, + ) + .await + { Ok(count) => { result.room_resource_events_deleted = count; if count > 0 { @@ -495,7 +512,12 @@ impl CleanupService { // 8. Cleanup stale playback progress rows. if self.config.playback_progress_retention_days > 0 { - match self.cleanup_stale_playback_progress().await { + match cleanup_ops::delete_stale_playback_progress( + &self.pool, + self.config.playback_progress_retention_days, + ) + .await + { Ok(count) => { result.playback_progress_deleted = count; if count > 0 { @@ -559,7 +581,13 @@ impl CleanupService { } } - match self.cleanup_realtime_outbox().await { + match cleanup_ops::delete_delivered_realtime_outbox( + &self.pool, + self.config.realtime_outbox_sent_retention_days, + self.config.realtime_outbox_dead_retention_days, + ) + .await + { Ok(count) => { result.realtime_outbox_deleted = count; if count > 0 { @@ -1238,16 +1266,6 @@ impl CleanupService { Ok(purged) } - async fn purge_soft_deleted_resources( - &self, - ) -> Result { - cleanup_ops::cleanup_soft_deleted_media_and_playlists( - &self.pool, - self.config.resource_soft_delete_retention_days, - ) - .await - } - async fn purge_soft_deleted_chat_messages(&self) -> Result { let days = i64::from(self.config.resource_soft_delete_retention_days); self.resource_tasks @@ -1295,15 +1313,6 @@ impl CleanupService { .await } - /// Delete read notifications older than the retention period - async fn delete_old_notifications(&self) -> Result { - cleanup_ops::delete_old_read_notifications( - &self.pool, - self.config.notification_retention_days, - ) - .await - } - /// Delete all notifications (including unread) older than the max retention period /// /// This prevents unbounded growth from unread notifications that are never @@ -1336,35 +1345,6 @@ impl CleanupService { Ok(deleted_count.max(0).cast_unsigned()) } - async fn cleanup_room_resource_events(&self) -> Result { - cleanup_ops::delete_old_room_resource_events( - &self.pool, - self.config.room_resource_event_retention_seconds, - ) - .await - } - - async fn cleanup_chat_message_events(&self, retention_seconds: u64) -> Result { - cleanup_ops::delete_old_chat_message_events(&self.pool, retention_seconds).await - } - - async fn cleanup_realtime_outbox(&self) -> Result { - cleanup_ops::delete_delivered_realtime_outbox( - &self.pool, - self.config.realtime_outbox_sent_retention_days, - self.config.realtime_outbox_dead_retention_days, - ) - .await - } - - async fn cleanup_stale_playback_progress(&self) -> Result { - cleanup_ops::delete_stale_playback_progress( - &self.pool, - self.config.playback_progress_retention_days, - ) - .await - } - async fn cleanup_playback_history(&self) -> Result { let (retention_days, max_entries_per_room) = self.playback_history_retention()?; crate::repository::PlaybackHistoryRepository::new(self.pool.clone()) diff --git a/synctv-core/src/service/cleanup_ops.rs b/synctv-core/src/service/cleanup_ops.rs index badbc8d6..faa13d75 100644 --- a/synctv-core/src/service/cleanup_ops.rs +++ b/synctv-core/src/service/cleanup_ops.rs @@ -27,10 +27,6 @@ pub(super) const CHAT_MESSAGE_COUNT_PRUNING_DAYS: i32 = 90; #[derive(Clone, Copy)] pub(super) enum ChatMessageCleanupScope { - RoomCap { - room_id: RoomId, - keep_count: i64, - }, ActiveRoomsCap { keep_count: i64, activity_window_minutes: i32, @@ -470,27 +466,6 @@ pub(super) async fn cleanup_chat_messages_with_files( ) .await } - ChatMessageCleanupScope::RoomCap { - room_id, - keep_count, - } => { - let mut tx = pool.begin().await?; - let batch = cleanup_room_chat_messages_batch(&mut tx, room_id, keep_count).await?; - tx.commit() - .await - .internal_with_err("Failed to commit chat message cleanup batch")?; - if let Some(storage) = storage { - schedule_chat_attachment_cleanup( - storage, - origin, - &batch.attachments, - batch.deleted, - log_context, - ) - .await; - } - Ok(batch.deleted) - } ChatMessageCleanupScope::Retention { retention_days } => { let mut tx = pool.begin().await?; let batch = cleanup_retained_chat_messages_batch(&mut tx, retention_days).await?; diff --git a/synctv-core/src/service/distributed_lock.rs b/synctv-core/src/service/distributed_lock.rs index e2fd4fd2..cc455ed2 100644 --- a/synctv-core/src/service/distributed_lock.rs +++ b/synctv-core/src/service/distributed_lock.rs @@ -10,12 +10,9 @@ use redis::Script; use std::future::Future; use std::sync::{Arc, LazyLock}; -mod guard; #[cfg(test)] mod tests; -pub use guard::LockGuard; - static GENERATE_FENCING_TOKEN_SCRIPT: LazyLock