mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2384 lines
96 KiB
Rust
2384 lines
96 KiB
Rust
use axum::Router;
|
|
use utoipa::OpenApi;
|
|
use utoipa_swagger_ui::SwaggerUi;
|
|
|
|
use crate::http::{
|
|
admin, auth, email, notifications, oauth2, public, room, room_extra, ticket, user, webrtc,
|
|
websocket, AppState,
|
|
};
|
|
use crate::providers::{
|
|
acfun, alist, bilibili, cctv, cloudreve, common, douyin, douyu, emby, fnos, huya, nextcloud,
|
|
playback_provider::{
|
|
acfun as playback_provider_acfun, alist as playback_provider_alist,
|
|
bilibili as playback_provider_bilibili, cctv as playback_provider_cctv,
|
|
cloudreve as playback_provider_cloudreve, direct_url as playback_provider_direct_url,
|
|
douyin as playback_provider_douyin, douyu as playback_provider_douyu,
|
|
emby as playback_provider_emby, fnos as playback_provider_fnos,
|
|
huya as playback_provider_huya, live_proxy as playback_provider_live_proxy,
|
|
nextcloud as playback_provider_nextcloud, qnap as playback_provider_qnap,
|
|
rtmp as playback_provider_rtmp, seafile as playback_provider_seafile,
|
|
synology as playback_provider_synology, tiktok as playback_provider_tiktok,
|
|
truenas as playback_provider_truenas, twitch as playback_provider_twitch,
|
|
youtube as playback_provider_youtube,
|
|
},
|
|
qnap, seafile, synology, tiktok, truenas, twitch, youtube,
|
|
};
|
|
use synctv_proto::client;
|
|
|
|
#[derive(utoipa::ToSchema)]
|
|
#[schema(rename_all = "camelCase")]
|
|
#[allow(dead_code)]
|
|
pub struct GoogleRpcStatusSchema {
|
|
pub code: i32,
|
|
pub message: String,
|
|
pub details: Vec<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(OpenApi)]
|
|
#[openapi(
|
|
paths(
|
|
public::get_public_settings,
|
|
public::get_server_info,
|
|
public::get_server_time,
|
|
auth::confirm_email_login,
|
|
auth::create_guest_token,
|
|
auth::register_with_direct_password,
|
|
auth::login_with_direct_password,
|
|
auth::start_login,
|
|
auth::request_email_registration,
|
|
auth::confirm_email_registration,
|
|
auth::start_opaque_registration,
|
|
auth::finish_opaque_registration,
|
|
auth::start_opaque_login,
|
|
auth::finish_opaque_login,
|
|
auth::start_passkey_registration,
|
|
auth::finish_passkey_registration,
|
|
auth::start_passkey_login,
|
|
auth::finish_passkey_login,
|
|
auth::request_email_login,
|
|
auth::request_mfa_email_code,
|
|
auth::verify_mfa_email_code,
|
|
auth::start_mfa_passkey,
|
|
auth::finish_mfa_passkey,
|
|
auth::verify_mfa_totp,
|
|
auth::verify_mfa_recovery_code,
|
|
auth::refresh_token,
|
|
auth::logout,
|
|
ticket::create_ticket,
|
|
webrtc::get_ice_servers,
|
|
user::get_me,
|
|
user::update_user,
|
|
user::get_user_preferences,
|
|
user::update_user_preferences,
|
|
user::set_two_factor_enabled,
|
|
user::start_sensitive_operation_verification,
|
|
user::start_sensitive_operation_passkey,
|
|
user::request_sensitive_operation_email_code,
|
|
user::finish_sensitive_operation_verification,
|
|
user::start_opaque_password_update,
|
|
user::finish_opaque_password_update,
|
|
user::start_passkey_bind,
|
|
user::finish_passkey_bind,
|
|
user::list_passkeys,
|
|
user::delete_passkey,
|
|
user::start_totp_setup,
|
|
user::finish_totp_setup,
|
|
user::regenerate_totp_recovery_codes,
|
|
user::delete_totp,
|
|
user::discover_rooms,
|
|
user::get_room_discovery,
|
|
user::list_my_rooms,
|
|
user::close_account,
|
|
email::request_password_reset,
|
|
email::start_opaque_password_reset,
|
|
email::finish_opaque_password_reset,
|
|
notifications::list_notifications,
|
|
notifications::get_notification,
|
|
notifications::mark_as_read,
|
|
notifications::mark_all_as_read,
|
|
notifications::delete_notification,
|
|
notifications::delete_all_read,
|
|
oauth2::get_authorize_url,
|
|
oauth2::exchange_authorization_code,
|
|
oauth2::get_bind_authorize_url,
|
|
oauth2::unlink_provider,
|
|
oauth2::list_available_providers,
|
|
oauth2::get_linked_providers,
|
|
common::list_instances,
|
|
common::list_provider_instances,
|
|
common::add_provider_instance,
|
|
common::update_provider_instance,
|
|
common::delete_provider_instance,
|
|
common::reconnect_provider_instance,
|
|
common::enable_provider_instance,
|
|
common::disable_provider_instance,
|
|
common::list_backends,
|
|
alist::login,
|
|
alist::list,
|
|
alist::search,
|
|
alist::me,
|
|
alist::logout,
|
|
alist::binds,
|
|
cloudreve::login,
|
|
cloudreve::list,
|
|
cloudreve::search,
|
|
cloudreve::me,
|
|
cloudreve::logout,
|
|
cloudreve::binds,
|
|
twitch::bind,
|
|
twitch::binds,
|
|
twitch::unbind,
|
|
twitch::resolve,
|
|
twitch::list_channel_items,
|
|
twitch::list_followed_live,
|
|
twitch::list_category_streams,
|
|
twitch::list_top_categories,
|
|
twitch::search_live_channels,
|
|
twitch::list_schedule,
|
|
huya::resolve,
|
|
douyu::resolve,
|
|
acfun::resolve,
|
|
cctv::resolve,
|
|
youtube::bind,
|
|
youtube::binds,
|
|
youtube::unbind,
|
|
youtube::resolve,
|
|
youtube::list,
|
|
douyin::bind,
|
|
douyin::binds,
|
|
douyin::unbind,
|
|
douyin::resolve,
|
|
douyin::list_user_posts,
|
|
tiktok::bind,
|
|
tiktok::binds,
|
|
tiktok::unbind,
|
|
tiktok::resolve,
|
|
tiktok::get_user,
|
|
tiktok::list_user_posts,
|
|
fnos::login,
|
|
fnos::list,
|
|
fnos::media_libraries,
|
|
fnos::media_items,
|
|
fnos::set_favorite,
|
|
fnos::set_watched,
|
|
fnos::server_info,
|
|
fnos::logout,
|
|
fnos::binds,
|
|
fnos::thumbnail,
|
|
qnap::login,
|
|
qnap::list,
|
|
qnap::capabilities,
|
|
qnap::logout,
|
|
qnap::binds,
|
|
qnap::thumbnail,
|
|
synology::login,
|
|
synology::list_files,
|
|
synology::list_libraries,
|
|
synology::list_movies,
|
|
synology::list_tv_shows,
|
|
synology::list_episodes,
|
|
synology::list_home_videos,
|
|
synology::list_tv_recordings,
|
|
synology::logout,
|
|
synology::binds,
|
|
synology::image,
|
|
nextcloud::login,
|
|
nextcloud::start_login_flow,
|
|
nextcloud::poll_login_flow,
|
|
nextcloud::list,
|
|
nextcloud::list_favorites,
|
|
nextcloud::logout,
|
|
nextcloud::binds,
|
|
nextcloud::preview,
|
|
seafile::login,
|
|
seafile::unlock_library,
|
|
seafile::list_repositories,
|
|
seafile::list,
|
|
seafile::list_starred,
|
|
seafile::logout,
|
|
seafile::binds,
|
|
seafile::thumbnail,
|
|
truenas::login,
|
|
truenas::list,
|
|
truenas::logout,
|
|
truenas::binds,
|
|
emby::login,
|
|
emby::list,
|
|
emby::me,
|
|
emby::logout,
|
|
emby::binds,
|
|
bilibili::parse,
|
|
bilibili::list_playlist,
|
|
bilibili::list_live_areas,
|
|
bilibili::list_favorite_folders,
|
|
bilibili::list_followed_pgc,
|
|
bilibili::list_history,
|
|
bilibili::list_pgc_timeline,
|
|
bilibili::list_pgc_seasons,
|
|
bilibili::login_qr,
|
|
bilibili::qr_check,
|
|
bilibili::sms_start,
|
|
bilibili::sms_send,
|
|
bilibili::sms_login,
|
|
bilibili::user_info,
|
|
bilibili::binds,
|
|
bilibili::logout,
|
|
emby::thumbnail,
|
|
playback_provider_direct_url::get_direct_url_stream,
|
|
playback_provider_direct_url::head_direct_url_stream,
|
|
playback_provider_direct_url::get_direct_url_hls_manifest,
|
|
playback_provider_direct_url::get_direct_url_hls_resource,
|
|
playback_provider_direct_url::head_direct_url_hls_resource,
|
|
playback_provider_direct_url::get_direct_url_dash_manifest,
|
|
playback_provider_direct_url::get_direct_url_dash_resource,
|
|
playback_provider_direct_url::head_direct_url_dash_resource,
|
|
playback_provider_direct_url::get_direct_url_subtitle,
|
|
playback_provider_twitch::get_twitch_resource,
|
|
playback_provider_twitch::get_twitch_segment,
|
|
playback_provider_twitch::watch_twitch_chat,
|
|
playback_provider_youtube::get_youtube_resource,
|
|
playback_provider_youtube::get_youtube_segment,
|
|
playback_provider_youtube::get_youtube_subtitle,
|
|
playback_provider_huya::get_huya_resource,
|
|
playback_provider_huya::get_huya_segment,
|
|
playback_provider_huya::watch_huya_danmaku,
|
|
playback_provider_douyu::get_douyu_resource,
|
|
playback_provider_douyu::get_douyu_segment,
|
|
playback_provider_douyu::watch_douyu_danmaku,
|
|
playback_provider_douyin::get_resource,
|
|
playback_provider_douyin::get_hls_resource,
|
|
playback_provider_douyin::watch_danmaku,
|
|
playback_provider_tiktok::get_tiktok_resource,
|
|
playback_provider_tiktok::get_tiktok_hls_resource,
|
|
playback_provider_tiktok::get_tiktok_subtitle,
|
|
playback_provider_acfun::get_acfun_resource,
|
|
playback_provider_acfun::get_acfun_hls_resource,
|
|
playback_provider_acfun::get_acfun_danmaku_file,
|
|
playback_provider_acfun::watch_acfun_danmaku,
|
|
playback_provider_cctv::get_cctv_resource,
|
|
playback_provider_cctv::get_cctv_segment,
|
|
playback_provider_fnos::get_fnos_resource,
|
|
playback_provider_fnos::get_fnos_segment,
|
|
playback_provider_fnos::get_fnos_subtitle,
|
|
playback_provider_fnos::get_fnos_thumbnail,
|
|
playback_provider_qnap::get_qnap_resource,
|
|
playback_provider_qnap::get_qnap_subtitle,
|
|
playback_provider_qnap::get_qnap_thumbnail,
|
|
playback_provider_synology::get_synology_resource,
|
|
playback_provider_synology::get_synology_segment,
|
|
playback_provider_synology::get_synology_subtitle,
|
|
playback_provider_cloudreve::get_cloudreve_resource,
|
|
playback_provider_cloudreve::get_cloudreve_hls_manifest,
|
|
playback_provider_cloudreve::get_cloudreve_hls_resource,
|
|
playback_provider_cloudreve::get_cloudreve_subtitle,
|
|
playback_provider_nextcloud::get_nextcloud_resource,
|
|
playback_provider_nextcloud::get_nextcloud_subtitle,
|
|
playback_provider_seafile::get_seafile_resource,
|
|
playback_provider_seafile::get_seafile_subtitle,
|
|
playback_provider_truenas::get_truenas_resource,
|
|
playback_provider_truenas::get_truenas_subtitle,
|
|
playback_provider_alist::get_alist_file_stream,
|
|
playback_provider_alist::head_alist_file_stream,
|
|
playback_provider_alist::get_alist_transcoded_hls_manifest,
|
|
playback_provider_alist::get_alist_transcoded_hls_resource,
|
|
playback_provider_alist::head_alist_transcoded_hls_resource,
|
|
playback_provider_alist::get_alist_subtitle,
|
|
playback_provider_alist::get_alist_thumbnail,
|
|
playback_provider_emby::get_emby_media_stream,
|
|
playback_provider_emby::head_emby_media_stream,
|
|
playback_provider_emby::get_emby_hls_manifest,
|
|
playback_provider_emby::get_emby_hls_resource,
|
|
playback_provider_emby::head_emby_hls_resource,
|
|
playback_provider_emby::get_emby_subtitle,
|
|
playback_provider_bilibili::get_bilibili_media_stream,
|
|
playback_provider_bilibili::head_bilibili_media_stream,
|
|
playback_provider_bilibili::get_bilibili_hls_manifest,
|
|
playback_provider_bilibili::get_bilibili_hls_resource,
|
|
playback_provider_bilibili::head_bilibili_hls_resource,
|
|
playback_provider_bilibili::get_bilibili_dash_manifest,
|
|
playback_provider_bilibili::get_bilibili_dash_resource,
|
|
playback_provider_bilibili::head_bilibili_dash_resource,
|
|
playback_provider_bilibili::get_bilibili_subtitle,
|
|
playback_provider_bilibili::get_bilibili_danmaku_file,
|
|
playback_provider_rtmp::get_rtmp_flv_stream,
|
|
playback_provider_rtmp::head_rtmp_flv_stream,
|
|
playback_provider_rtmp::get_rtmp_hls_master,
|
|
playback_provider_rtmp::get_rtmp_hls_playlist,
|
|
playback_provider_rtmp::get_rtmp_hls_segment,
|
|
playback_provider_rtmp::head_rtmp_hls_segment,
|
|
playback_provider_live_proxy::get_live_proxy_flv_stream,
|
|
playback_provider_live_proxy::head_live_proxy_flv_stream,
|
|
playback_provider_live_proxy::get_live_proxy_hls_master,
|
|
playback_provider_live_proxy::get_live_proxy_hls_playlist,
|
|
playback_provider_live_proxy::get_live_proxy_hls_segment,
|
|
playback_provider_live_proxy::head_live_proxy_hls_segment,
|
|
websocket::websocket_room_connect_doc,
|
|
room::create_room,
|
|
room::discover_rooms,
|
|
room::get_room_discovery,
|
|
room::get_room,
|
|
room::join_room,
|
|
room::leave_room,
|
|
room::get_room_members,
|
|
room::list_room_streams,
|
|
room::create_room_publish_key,
|
|
room::get_room_stream_info,
|
|
room::kick_room_stream,
|
|
room::get_playback,
|
|
room::get_room_settings,
|
|
room::update_room_settings,
|
|
room::update_room_visibility,
|
|
room::transfer_room_ownership,
|
|
room::list_playlists,
|
|
room::create_playlist,
|
|
room::get_playlist,
|
|
room::get_media,
|
|
room::add_media,
|
|
room::clear_playlist,
|
|
room::delete_media,
|
|
room::edit_media,
|
|
room::delete_entries,
|
|
room::push_media_batch,
|
|
room::move_media,
|
|
room::start_playback,
|
|
room::play_next,
|
|
room::play_previous,
|
|
room::list_playback_history,
|
|
room::play_history_entry,
|
|
room::stop_playback,
|
|
room::update_playback_state,
|
|
room::start_room_password_login,
|
|
room::finish_room_password_login,
|
|
room::start_room_password_registration,
|
|
room::finish_room_password_registration,
|
|
room::clear_room_password,
|
|
room::watch_chat_events,
|
|
room::watch_chat_pin_events,
|
|
room::get_chat_history,
|
|
room::search_chat_messages,
|
|
room::get_chat_message,
|
|
room::get_chat_message_context,
|
|
room::get_chat_message_read_receipts,
|
|
room::send_chat_message,
|
|
room::create_chat_attachment_upload_session,
|
|
room::edit_chat_message,
|
|
room::delete_chat_message,
|
|
room::list_pinned_chat_messages,
|
|
room::pin_chat_message,
|
|
room::unpin_chat_message,
|
|
room::set_chat_reaction,
|
|
room::clear_chat_reaction,
|
|
room::list_chat_reaction_users,
|
|
room::mark_chat_read,
|
|
room::get_chat_read_state,
|
|
room::report_content,
|
|
room::list_room_content_reports,
|
|
room::get_room_content_report,
|
|
room::update_room_content_report_status,
|
|
room::list_playlist_items,
|
|
room::delete_playlist,
|
|
room::update_playlist,
|
|
room::move_playlist,
|
|
room::reset_room_settings,
|
|
room_extra::kick_member,
|
|
room_extra::set_member_permissions,
|
|
room_extra::list_room_join_reviews,
|
|
room_extra::approve_room_join_review,
|
|
room_extra::reject_room_join_review,
|
|
admin::get_service_state,
|
|
admin::list_user_registration_reviews,
|
|
admin::approve_user_registration_review,
|
|
admin::reject_user_registration_review,
|
|
admin::list_room_creation_reviews,
|
|
admin::approve_room_creation_review,
|
|
admin::reject_room_creation_review,
|
|
admin::list_room_join_reviews,
|
|
admin::approve_room_join_review,
|
|
admin::reject_room_join_review,
|
|
admin::list_ban_records,
|
|
admin::list_content_reports,
|
|
admin::get_content_report,
|
|
admin::update_content_report_status,
|
|
admin::get_settings,
|
|
admin::set_settings,
|
|
admin::export_settings,
|
|
admin::import_settings,
|
|
admin::send_test_email,
|
|
admin::list_users,
|
|
admin::create_user,
|
|
admin::get_user,
|
|
admin::delete_user,
|
|
admin::restore_user,
|
|
admin::get_user_preferences,
|
|
admin::update_user_preferences,
|
|
admin::set_user_role,
|
|
admin::set_user_password,
|
|
admin::set_user_username,
|
|
admin::ban_user,
|
|
admin::unban_user,
|
|
admin::get_user_rooms,
|
|
admin::batch_ban_users,
|
|
admin::batch_delete_users,
|
|
admin::list_rooms,
|
|
admin::list_room_categories,
|
|
admin::upsert_room_category,
|
|
admin::delete_room_category,
|
|
admin::list_room_labels,
|
|
admin::upsert_room_label,
|
|
admin::delete_room_label,
|
|
admin::get_room,
|
|
admin::update_room_taxonomy,
|
|
admin::delete_room,
|
|
admin::set_room_password,
|
|
admin::get_room_members,
|
|
admin::add_member,
|
|
admin::update_member_permissions,
|
|
admin::kick_member,
|
|
admin::ban_room,
|
|
admin::unban_room,
|
|
admin::get_room_settings,
|
|
admin::set_room_settings,
|
|
admin::reset_room_settings,
|
|
admin::batch_ban_rooms,
|
|
admin::batch_delete_rooms,
|
|
admin::list_streams,
|
|
admin::kick_stream,
|
|
admin::list_admins,
|
|
admin::add_admin,
|
|
admin::remove_admin
|
|
),
|
|
components(
|
|
schemas(
|
|
GoogleRpcStatusSchema,
|
|
client::HealthResponse,
|
|
client::HealthDetails,
|
|
client::MemoryHealth,
|
|
client::CctvPlaybackMetadata,
|
|
client::CctvChapterMetadata,
|
|
client::CreateWebSocketTicketRequest,
|
|
client::CreateWebSocketTicketResponse,
|
|
client::SetUsernameRequest,
|
|
client::User,
|
|
client::RegisterResponse,
|
|
client::RegisterWithDirectPasswordRequest,
|
|
client::LoginWithDirectPasswordRequest,
|
|
client::LoginMethod,
|
|
client::StartLoginRequest,
|
|
client::StartLoginResponse,
|
|
client::ConfirmEmailLoginRequest,
|
|
client::LoginResponse,
|
|
client::CreateGuestTokenRequest,
|
|
client::CreateGuestTokenResponse,
|
|
client::StartOpaqueRegistrationRequest,
|
|
client::StartOpaqueRegistrationResponse,
|
|
client::FinishOpaqueRegistrationRequest,
|
|
client::StartOpaqueLoginRequest,
|
|
client::StartOpaqueLoginResponse,
|
|
client::FinishOpaqueLoginRequest,
|
|
client::StartPasskeyRegistrationRequest,
|
|
client::StartPasskeyRegistrationResponse,
|
|
client::FinishPasskeyRegistrationRequest,
|
|
client::StartPasskeyLoginRequest,
|
|
client::StartPasskeyLoginResponse,
|
|
client::FinishPasskeyLoginRequest,
|
|
client::RequestEmailLoginRequest,
|
|
client::RequestEmailLoginResponse,
|
|
client::RequestEmailRegistrationRequest,
|
|
client::RequestEmailRegistrationResponse,
|
|
client::ConfirmEmailRegistrationRequest,
|
|
client::RequestMfaEmailCodeRequest,
|
|
client::RequestMfaEmailCodeResponse,
|
|
client::VerifyMfaEmailCodeRequest,
|
|
client::StartMfaPasskeyRequest,
|
|
client::StartMfaPasskeyResponse,
|
|
client::FinishMfaPasskeyRequest,
|
|
client::VerifyMfaTotpRequest,
|
|
client::VerifyMfaRecoveryCodeRequest,
|
|
client::RefreshTokenRequest,
|
|
client::RefreshTokenResponse,
|
|
client::LogoutResponse,
|
|
client::CreateRoomPublishKeyRequest,
|
|
client::CreateRoomPublishKeyResponse,
|
|
client::RequestPasswordResetRequest,
|
|
client::RequestPasswordResetResponse,
|
|
client::StartOpaquePasswordResetRequest,
|
|
client::StartOpaquePasswordResetResponse,
|
|
client::FinishOpaquePasswordResetRequest,
|
|
client::ConfirmPasswordResetResponse,
|
|
client::GetPublicSettingsResponse,
|
|
client::GetServerInfoResponse,
|
|
client::GetServerTimeRequest,
|
|
client::GetServerTimeResponse,
|
|
client::User,
|
|
client::SensitiveOperationVerificationChallenge,
|
|
client::StartSensitiveOperationVerificationRequest,
|
|
client::SensitiveOperationVerificationOutcome,
|
|
client::StartSensitiveOperationPasskeyRequest,
|
|
client::StartSensitiveOperationPasskeyResponse,
|
|
client::RequestSensitiveOperationEmailCodeRequest,
|
|
client::RequestSensitiveOperationEmailCodeResponse,
|
|
client::FinishSensitiveOperationVerificationRequest,
|
|
client::StartOpaquePasswordUpdateRequest,
|
|
client::StartOpaquePasswordUpdateResponse,
|
|
client::FinishOpaquePasswordUpdateRequest,
|
|
client::User,
|
|
client::StartPasskeyBindRequest,
|
|
client::StartPasskeyBindResponse,
|
|
client::FinishPasskeyBindRequest,
|
|
client::PasskeyCredential,
|
|
client::ListPasskeysResponse,
|
|
client::DeletePasskeyRequest,
|
|
client::DeletePasskeyResponse,
|
|
client::StartTotpSetupRequest,
|
|
client::StartTotpSetupResponse,
|
|
client::FinishTotpSetupRequest,
|
|
client::TotpRecoveryCodesResponse,
|
|
client::RegenerateTotpRecoveryCodesRequest,
|
|
client::DeleteTotpRequest,
|
|
client::DeleteTotpResponse,
|
|
client::CloseAccountRequest,
|
|
client::CloseAccountResponse,
|
|
client::GetUserPreferencesResponse,
|
|
client::UpdateUserPreferencesRequest,
|
|
client::UpdateUserPreferencesResponse,
|
|
client::SetTwoFactorEnabledRequest,
|
|
client::GetIceServersResponse,
|
|
client::ListMyRoomsResponse,
|
|
client::DeleteRoomResponse,
|
|
client::CreateRoomRequest,
|
|
client::Room,
|
|
client::DiscoverRoomsResponse,
|
|
client::RoomDiscoveryItem,
|
|
client::RoomCategory,
|
|
client::RoomLabel,
|
|
client::ListRoomCategoriesResponse,
|
|
client::ListRoomLabelsResponse,
|
|
client::GetRoomResponse,
|
|
client::JoinRoomRequest,
|
|
client::JoinRoomResponse,
|
|
client::LeaveRoomResponse,
|
|
client::GetRoomMembersResponse,
|
|
client::ListRoomStreamsResponse,
|
|
client::StreamEntry,
|
|
client::GetPlaybackResponse,
|
|
client::GetRoomSettingsResponse,
|
|
client::UpdateRoomSettingsRequest,
|
|
client::UpdateRoomVisibilityRequest,
|
|
client::Room,
|
|
client::ListPlaylistsResponse,
|
|
client::CreatePlaylistRequest,
|
|
client::Playlist,
|
|
client::GetPlaylistResponse,
|
|
client::Playlist,
|
|
client::Media,
|
|
client::AddMediaRequest,
|
|
client::Media,
|
|
client::ClearPlaylistResponse,
|
|
client::DeleteMediaResponse,
|
|
client::EditMediaRequest,
|
|
client::Media,
|
|
client::DeleteEntriesRequest,
|
|
client::DeleteEntriesResponse,
|
|
client::AddMediaBatchRequest,
|
|
client::AddMediaBatchResponse,
|
|
client::MoveMediaRequest,
|
|
client::MoveMediaResponse,
|
|
client::StartPlaybackRequest,
|
|
client::StopPlaybackRequest,
|
|
client::UpdatePlaybackStateRequest,
|
|
client::StartRoomPasswordLoginRequest,
|
|
client::StartRoomPasswordLoginResponse,
|
|
client::FinishRoomPasswordLoginRequest,
|
|
client::StartRoomPasswordRegistrationRequest,
|
|
client::StartRoomPasswordRegistrationResponse,
|
|
client::FinishRoomPasswordRegistrationRequest,
|
|
client::ClearRoomPasswordRequest,
|
|
client::SetRoomPasswordResponse,
|
|
client::GetChatHistoryResponse,
|
|
client::ChatReactionSummary,
|
|
client::ChatReactionUser,
|
|
client::ListChatReactionUsersRequest,
|
|
client::ListChatReactionUsersResponse,
|
|
client::SetChatReactionRequest,
|
|
client::ChatMessageEvent,
|
|
client::ReportContentRequest,
|
|
client::ReportContentResponse,
|
|
client::ReportRoomTarget,
|
|
client::ReportUserTarget,
|
|
client::ReportRoomMemberTarget,
|
|
client::ReportChatMessageTarget,
|
|
client::ListPlaylistItemsRequest,
|
|
client::ListPlaylistItemsResponse,
|
|
client::PagePagination,
|
|
client::CursorPagination,
|
|
client::DeletePlaylistResponse,
|
|
client::UpdatePlaylistRequest,
|
|
client::Playlist,
|
|
client::MovePlaylistRequest,
|
|
client::Playlist,
|
|
client::RoomSettings,
|
|
client::KickMemberResponse,
|
|
client::UpdateMemberPermissionsRequest,
|
|
client::RoomJoinReview,
|
|
client::ListRoomJoinReviewsRequest,
|
|
client::ListRoomJoinReviewsResponse,
|
|
client::ApproveRoomJoinReviewRequest,
|
|
client::ApproveRoomJoinReviewResponse,
|
|
client::RejectRoomJoinReviewRequest,
|
|
client::NotificationProto,
|
|
client::ListNotificationsResponse,
|
|
client::NotificationProto,
|
|
client::MarkAsReadRequest,
|
|
client::MarkAllAsReadRequest,
|
|
client::GetAuthorizationUrlResponse,
|
|
client::GetAuthorizationUrlForBindResponse,
|
|
client::ExchangeAuthorizationCodeRequest,
|
|
client::ExchangeAuthorizationCodeResponse,
|
|
client::ListAvailableProvidersResponse,
|
|
client::OAuth2ProviderInstance,
|
|
client::UnlinkProviderResponse,
|
|
client::GetLinkedProvidersResponse,
|
|
client::LinkedProvider,
|
|
synctv_proto::providers::common::ProviderInstancesResponse,
|
|
synctv_proto::providers::common::ProviderBackendsResponse,
|
|
synctv_proto::providers::alist::LoginRequest,
|
|
synctv_proto::providers::alist::LoginResponse,
|
|
synctv_proto::providers::alist::GetBindsResponse,
|
|
synctv_proto::providers::alist::ListRequest,
|
|
synctv_proto::providers::alist::ListResponse,
|
|
synctv_proto::providers::alist::FileItem,
|
|
synctv_proto::providers::alist::SearchRequest,
|
|
synctv_proto::providers::alist::SearchResponse,
|
|
synctv_proto::providers::alist::SearchItem,
|
|
synctv_proto::providers::alist::GetMeRequest,
|
|
synctv_proto::providers::alist::GetMeResponse,
|
|
synctv_proto::providers::alist::LogoutRequest,
|
|
synctv_proto::providers::alist::LogoutResponse,
|
|
synctv_proto::providers::cloudreve::LoginRequest,
|
|
synctv_proto::providers::cloudreve::LoginResponse,
|
|
synctv_proto::providers::cloudreve::ListRequest,
|
|
synctv_proto::providers::cloudreve::ListResponse,
|
|
synctv_proto::providers::cloudreve::PagePagination,
|
|
synctv_proto::providers::cloudreve::CursorPagination,
|
|
synctv_proto::providers::cloudreve::SearchRequest,
|
|
synctv_proto::providers::cloudreve::SearchResponse,
|
|
synctv_proto::providers::cloudreve::FileItem,
|
|
synctv_proto::providers::cloudreve::GetMeRequest,
|
|
synctv_proto::providers::cloudreve::GetMeResponse,
|
|
synctv_proto::providers::cloudreve::LogoutRequest,
|
|
synctv_proto::providers::cloudreve::LogoutResponse,
|
|
synctv_proto::providers::cloudreve::GetBindsResponse,
|
|
synctv_proto::providers::cloudreve::BindInfo,
|
|
synctv_proto::providers::twitch::BindRequest,
|
|
synctv_proto::providers::twitch::BindResponse,
|
|
synctv_proto::providers::twitch::GetBindsResponse,
|
|
synctv_proto::providers::twitch::BindInfo,
|
|
synctv_proto::providers::twitch::UnbindRequest,
|
|
synctv_proto::providers::twitch::UnbindResponse,
|
|
synctv_proto::providers::twitch::ResolveRequest,
|
|
synctv_proto::providers::twitch::ResolveResponse,
|
|
synctv_proto::providers::twitch::Metadata,
|
|
synctv_proto::providers::twitch::Quality,
|
|
synctv_proto::providers::twitch::Chapter,
|
|
synctv_proto::providers::twitch::ListChannelItemsRequest,
|
|
synctv_proto::providers::twitch::ListChannelItemsResponse,
|
|
synctv_proto::providers::twitch::ListItem,
|
|
synctv_proto::providers::twitch::ListFollowedLiveRequest,
|
|
synctv_proto::providers::twitch::ListFollowedLiveResponse,
|
|
synctv_proto::providers::twitch::ListCategoryStreamsRequest,
|
|
synctv_proto::providers::twitch::ListCategoryStreamsResponse,
|
|
synctv_proto::providers::twitch::StreamItem,
|
|
synctv_proto::providers::twitch::ListTopCategoriesRequest,
|
|
synctv_proto::providers::twitch::ListTopCategoriesResponse,
|
|
synctv_proto::providers::twitch::CategoryItem,
|
|
synctv_proto::providers::twitch::SearchLiveChannelsRequest,
|
|
synctv_proto::providers::twitch::SearchLiveChannelsResponse,
|
|
synctv_proto::providers::twitch::SearchChannelItem,
|
|
synctv_proto::providers::twitch::ListScheduleRequest,
|
|
synctv_proto::providers::twitch::ListScheduleResponse,
|
|
synctv_proto::providers::twitch::ScheduleSegment,
|
|
synctv_proto::providers::huya::ResolveRequest,
|
|
synctv_proto::providers::huya::ResolveResponse,
|
|
synctv_proto::providers::huya::Metadata,
|
|
synctv_proto::providers::huya::Quality,
|
|
synctv_proto::providers::douyu::ResolveRequest,
|
|
synctv_proto::providers::douyu::ResolveResponse,
|
|
synctv_proto::providers::douyu::Metadata,
|
|
synctv_proto::providers::douyu::Quality,
|
|
synctv_proto::providers::acfun::ResolveRequest,
|
|
synctv_proto::providers::acfun::ResolveResponse,
|
|
synctv_proto::providers::acfun::Metadata,
|
|
synctv_proto::providers::acfun::Quality,
|
|
synctv_proto::providers::cctv::ResolveRequest,
|
|
synctv_proto::providers::cctv::ResolveResponse,
|
|
synctv_proto::providers::cctv::Metadata,
|
|
synctv_proto::providers::cctv::Chapter,
|
|
synctv_proto::providers::cctv::Stream,
|
|
synctv_proto::providers::youtube::BindRequest,
|
|
synctv_proto::providers::youtube::BindResponse,
|
|
synctv_proto::providers::youtube::GetBindsResponse,
|
|
synctv_proto::providers::youtube::BindInfo,
|
|
synctv_proto::providers::youtube::UnbindRequest,
|
|
synctv_proto::providers::youtube::UnbindResponse,
|
|
synctv_proto::providers::youtube::ResolveRequest,
|
|
synctv_proto::providers::youtube::ResolveResponse,
|
|
synctv_proto::providers::youtube::Metadata,
|
|
synctv_proto::providers::youtube::Format,
|
|
synctv_proto::providers::douyin::BindRequest,
|
|
synctv_proto::providers::douyin::BindResponse,
|
|
synctv_proto::providers::douyin::GetBindsResponse,
|
|
synctv_proto::providers::douyin::BindInfo,
|
|
synctv_proto::providers::douyin::UnbindRequest,
|
|
synctv_proto::providers::douyin::UnbindResponse,
|
|
synctv_proto::providers::douyin::ResolveRequest,
|
|
synctv_proto::providers::douyin::ResolveResponse,
|
|
synctv_proto::providers::douyin::ListUserPostsRequest,
|
|
synctv_proto::providers::douyin::ListUserPostsResponse,
|
|
synctv_proto::providers::tiktok::BindRequest,
|
|
synctv_proto::providers::tiktok::BindResponse,
|
|
synctv_proto::providers::tiktok::GetBindsResponse,
|
|
synctv_proto::providers::tiktok::BindInfo,
|
|
synctv_proto::providers::tiktok::UnbindRequest,
|
|
synctv_proto::providers::tiktok::UnbindResponse,
|
|
synctv_proto::providers::tiktok::ResolveRequest,
|
|
synctv_proto::providers::tiktok::ResolveResponse,
|
|
synctv_proto::providers::tiktok::GetUserRequest,
|
|
synctv_proto::providers::tiktok::GetUserResponse,
|
|
synctv_proto::providers::tiktok::ListUserPostsRequest,
|
|
synctv_proto::providers::tiktok::ListUserPostsResponse,
|
|
synctv_proto::providers::fnos::LoginRequest,
|
|
synctv_proto::providers::fnos::LoginResponse,
|
|
synctv_proto::providers::fnos::Authenticated,
|
|
synctv_proto::providers::fnos::TwoFactorRequired,
|
|
synctv_proto::providers::fnos::ListRequest,
|
|
synctv_proto::providers::fnos::ListResponse,
|
|
synctv_proto::providers::fnos::FileItem,
|
|
synctv_proto::providers::fnos::ListMediaLibrariesRequest,
|
|
synctv_proto::providers::fnos::ListMediaLibrariesResponse,
|
|
synctv_proto::providers::fnos::MediaLibrary,
|
|
synctv_proto::providers::fnos::ListMediaItemsRequest,
|
|
synctv_proto::providers::fnos::ListMediaItemsResponse,
|
|
synctv_proto::providers::fnos::MediaItem,
|
|
synctv_proto::providers::fnos::SetFavoriteRequest,
|
|
synctv_proto::providers::fnos::SetFavoriteResponse,
|
|
synctv_proto::providers::fnos::SetWatchedRequest,
|
|
synctv_proto::providers::fnos::SetWatchedResponse,
|
|
synctv_proto::providers::fnos::GetServerInfoRequest,
|
|
synctv_proto::providers::fnos::GetServerInfoResponse,
|
|
synctv_proto::providers::fnos::LogoutRequest,
|
|
synctv_proto::providers::fnos::LogoutResponse,
|
|
synctv_proto::providers::fnos::GetBindsResponse,
|
|
synctv_proto::providers::fnos::BindInfo,
|
|
synctv_proto::providers::qnap::LoginRequest,
|
|
synctv_proto::providers::qnap::LoginResponse,
|
|
synctv_proto::providers::qnap::ListRequest,
|
|
synctv_proto::providers::qnap::ListResponse,
|
|
synctv_proto::providers::qnap::FileItem,
|
|
synctv_proto::providers::qnap::GetCapabilitiesRequest,
|
|
synctv_proto::providers::qnap::GetCapabilitiesResponse,
|
|
synctv_proto::providers::qnap::LogoutRequest,
|
|
synctv_proto::providers::qnap::LogoutResponse,
|
|
synctv_proto::providers::qnap::GetBindsResponse,
|
|
synctv_proto::providers::qnap::BindInfo,
|
|
synctv_proto::providers::synology::LoginRequest,
|
|
synctv_proto::providers::synology::LoginResponse,
|
|
synctv_proto::providers::synology::ListFilesRequest,
|
|
synctv_proto::providers::synology::ListFilesResponse,
|
|
synctv_proto::providers::synology::FileItem,
|
|
synctv_proto::providers::synology::ListLibrariesRequest,
|
|
synctv_proto::providers::synology::ListLibrariesResponse,
|
|
synctv_proto::providers::synology::VideoLibrary,
|
|
synctv_proto::providers::synology::ListMoviesRequest,
|
|
synctv_proto::providers::synology::ListTvShowsRequest,
|
|
synctv_proto::providers::synology::ListEpisodesRequest,
|
|
synctv_proto::providers::synology::ListHomeVideosRequest,
|
|
synctv_proto::providers::synology::ListTvRecordingsRequest,
|
|
synctv_proto::providers::synology::ListVideoItemsResponse,
|
|
synctv_proto::providers::synology::VideoItem,
|
|
synctv_proto::providers::synology::VideoFile,
|
|
synctv_proto::providers::synology::SynologyVideoEntryKind,
|
|
synctv_proto::providers::synology::LogoutRequest,
|
|
synctv_proto::providers::synology::LogoutResponse,
|
|
synctv_proto::providers::synology::GetBindsResponse,
|
|
synctv_proto::providers::synology::BindInfo,
|
|
synctv_proto::providers::nextcloud::LoginRequest,
|
|
synctv_proto::providers::nextcloud::LoginResponse,
|
|
synctv_proto::providers::nextcloud::StartLoginFlowRequest,
|
|
synctv_proto::providers::nextcloud::StartLoginFlowResponse,
|
|
synctv_proto::providers::nextcloud::PollLoginFlowRequest,
|
|
synctv_proto::providers::nextcloud::ListRequest,
|
|
synctv_proto::providers::nextcloud::ListFavoritesRequest,
|
|
synctv_proto::providers::nextcloud::ListResponse,
|
|
synctv_proto::providers::nextcloud::FileItem,
|
|
synctv_proto::providers::nextcloud::LogoutRequest,
|
|
synctv_proto::providers::nextcloud::LogoutResponse,
|
|
synctv_proto::providers::nextcloud::GetBindsResponse,
|
|
synctv_proto::providers::nextcloud::BindInfo,
|
|
synctv_proto::providers::seafile::LoginRequest,
|
|
synctv_proto::providers::seafile::LoginResponse,
|
|
synctv_proto::providers::seafile::UnlockLibraryRequest,
|
|
synctv_proto::providers::seafile::UnlockLibraryResponse,
|
|
synctv_proto::providers::seafile::ListRepositoriesRequest,
|
|
synctv_proto::providers::seafile::ListRequest,
|
|
synctv_proto::providers::seafile::ListStarredRequest,
|
|
synctv_proto::providers::seafile::ListResponse,
|
|
synctv_proto::providers::seafile::FileItem,
|
|
synctv_proto::providers::seafile::LogoutRequest,
|
|
synctv_proto::providers::seafile::LogoutResponse,
|
|
synctv_proto::providers::seafile::GetBindsResponse,
|
|
synctv_proto::providers::seafile::BindInfo,
|
|
synctv_proto::providers::truenas::LoginRequest,
|
|
synctv_proto::providers::truenas::LoginResponse,
|
|
synctv_proto::providers::truenas::ListRequest,
|
|
synctv_proto::providers::truenas::ListResponse,
|
|
synctv_proto::providers::truenas::FileItem,
|
|
synctv_proto::providers::truenas::LogoutRequest,
|
|
synctv_proto::providers::truenas::LogoutResponse,
|
|
synctv_proto::providers::truenas::GetBindsResponse,
|
|
synctv_proto::providers::truenas::BindInfo,
|
|
synctv_proto::providers::emby::LoginRequest,
|
|
synctv_proto::providers::emby::LoginResponse,
|
|
synctv_proto::providers::emby::GetBindsResponse,
|
|
synctv_proto::providers::emby::ListRequest,
|
|
synctv_proto::providers::emby::ListResponse,
|
|
synctv_proto::providers::emby::MediaItem,
|
|
synctv_proto::providers::emby::GetMeRequest,
|
|
synctv_proto::providers::emby::GetMeResponse,
|
|
synctv_proto::providers::emby::LogoutRequest,
|
|
synctv_proto::providers::emby::LogoutResponse,
|
|
synctv_proto::providers::bilibili::ParseRequest,
|
|
synctv_proto::providers::bilibili::ParseResponse,
|
|
synctv_proto::providers::bilibili::ParseCandidate,
|
|
synctv_proto::providers::bilibili::ListLiveAreasRequest,
|
|
synctv_proto::providers::bilibili::ListLiveAreasResponse,
|
|
synctv_proto::providers::bilibili::LiveArea,
|
|
synctv_proto::providers::bilibili::ListFavoriteFoldersRequest,
|
|
synctv_proto::providers::bilibili::ListFavoriteFoldersResponse,
|
|
synctv_proto::providers::bilibili::FavoriteFolder,
|
|
synctv_proto::providers::bilibili::ListFollowedPgcRequest,
|
|
synctv_proto::providers::bilibili::ListFollowedPgcResponse,
|
|
synctv_proto::providers::bilibili::FollowedPgcSeason,
|
|
synctv_proto::providers::bilibili::PgcFollowType,
|
|
synctv_proto::providers::bilibili::ListHistoryRequest,
|
|
synctv_proto::providers::bilibili::ListHistoryResponse,
|
|
synctv_proto::providers::bilibili::HistoryItem,
|
|
synctv_proto::providers::bilibili::ListPgcTimelineRequest,
|
|
synctv_proto::providers::bilibili::ListPgcTimelineResponse,
|
|
synctv_proto::providers::bilibili::PgcTimelineItem,
|
|
synctv_proto::providers::bilibili::ListPgcSeasonsRequest,
|
|
synctv_proto::providers::bilibili::ListPgcSeasonsResponse,
|
|
synctv_proto::providers::bilibili::PgcSeason,
|
|
synctv_proto::providers::bilibili::PgcSeasonType,
|
|
synctv_proto::providers::bilibili::PgcSeasonOrder,
|
|
synctv_proto::providers::bilibili::QrCodeResponse,
|
|
synctv_proto::providers::bilibili::CheckQrRequest,
|
|
synctv_proto::providers::bilibili::QrStatusResponse,
|
|
synctv_proto::providers::bilibili::StartSmsLoginRequest,
|
|
synctv_proto::providers::bilibili::StartSmsLoginResponse,
|
|
synctv_proto::providers::bilibili::SendSmsRequest,
|
|
synctv_proto::providers::bilibili::SendSmsResponse,
|
|
synctv_proto::providers::bilibili::LoginSmsRequest,
|
|
synctv_proto::providers::bilibili::LoginSmsResponse,
|
|
synctv_proto::providers::bilibili::UserInfoRequest,
|
|
synctv_proto::providers::bilibili::UserInfoResponse,
|
|
synctv_proto::providers::bilibili::GetBindsResponse,
|
|
synctv_proto::providers::bilibili::LogoutRequest,
|
|
synctv_proto::providers::bilibili::LogoutResponse,
|
|
synctv_proto::providers::bilibili::QrLoginStatus,
|
|
synctv_proto::client::GetRoomStreamInfoResponse,
|
|
synctv_proto::client::RoomStreamPublisherInfo,
|
|
synctv_proto::client::KickRoomStreamRequest,
|
|
synctv_proto::client::KickRoomStreamResponse,
|
|
synctv_proto::admin::GetServiceStateResponse,
|
|
synctv_proto::admin::RuntimeSettings,
|
|
synctv_proto::admin::RuntimeSettingsSnapshot,
|
|
synctv_proto::admin::UpdateSettingsRequest,
|
|
synctv_proto::admin::ImportSettingsRequest,
|
|
synctv_proto::admin::ImportSettingsResponse,
|
|
synctv_proto::admin::SendTestEmailRequest,
|
|
synctv_proto::admin::SendTestEmailResponse,
|
|
synctv_proto::admin::UserRegistrationReview,
|
|
synctv_proto::admin::ListUserRegistrationReviewsRequest,
|
|
synctv_proto::admin::ListUserRegistrationReviewsResponse,
|
|
synctv_proto::admin::ApproveUserRegistrationReviewRequest,
|
|
synctv_proto::admin::ApproveUserRegistrationReviewResponse,
|
|
synctv_proto::admin::RejectUserRegistrationReviewRequest,
|
|
synctv_proto::admin::RoomCreationReview,
|
|
synctv_proto::admin::ListRoomCreationReviewsRequest,
|
|
synctv_proto::admin::ListRoomCreationReviewsResponse,
|
|
synctv_proto::admin::ApproveRoomCreationReviewRequest,
|
|
synctv_proto::admin::ApproveRoomCreationReviewResponse,
|
|
synctv_proto::admin::RejectRoomCreationReviewRequest,
|
|
synctv_proto::admin::RoomJoinReview,
|
|
synctv_proto::admin::ListRoomJoinReviewsRequest,
|
|
synctv_proto::admin::ListRoomJoinReviewsResponse,
|
|
synctv_proto::admin::ApproveRoomJoinReviewRequest,
|
|
synctv_proto::admin::ApproveRoomJoinReviewResponse,
|
|
synctv_proto::admin::RejectRoomJoinReviewRequest,
|
|
synctv_proto::admin::BanRecord,
|
|
synctv_proto::admin::ListBanRecordsRequest,
|
|
synctv_proto::admin::ListBanRecordsResponse,
|
|
synctv_proto::admin::ContentReport,
|
|
synctv_proto::admin::ListContentReportsRequest,
|
|
synctv_proto::admin::ListContentReportsResponse,
|
|
synctv_proto::admin::ContentReport,
|
|
synctv_proto::admin::UpdateContentReportStatusRequest,
|
|
synctv_proto::admin::UpdateContentReportStatusResponse,
|
|
synctv_proto::admin::ListUsersResponse,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::GetUserPreferencesResponse,
|
|
synctv_proto::admin::UpdateUserPreferencesRequest,
|
|
synctv_proto::admin::UpdateUserPreferencesResponse,
|
|
synctv_proto::admin::CreateUserRequest,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::DeleteUserResponse,
|
|
synctv_proto::admin::UpdateUserRoleRequest,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::SetUserPasswordRequest,
|
|
synctv_proto::admin::SetUserPasswordResponse,
|
|
synctv_proto::admin::UpdateUserUsernameRequest,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::BanUserRequest,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::GetUserRoomsResponse,
|
|
synctv_proto::admin::BatchBanUsersRequest,
|
|
synctv_proto::admin::BatchBanUsersResponse,
|
|
synctv_proto::admin::BatchDeleteUsersRequest,
|
|
synctv_proto::admin::BatchDeleteUsersResponse,
|
|
synctv_proto::admin::ListRoomsResponse,
|
|
synctv_proto::admin::ListRoomCategoriesRequest,
|
|
synctv_proto::admin::ListRoomCategoriesResponse,
|
|
synctv_proto::admin::UpsertRoomCategoryRequest,
|
|
synctv_proto::client::RoomCategory,
|
|
synctv_proto::admin::DeleteRoomCategoryResponse,
|
|
synctv_proto::admin::ListRoomLabelsRequest,
|
|
synctv_proto::admin::ListRoomLabelsResponse,
|
|
synctv_proto::admin::UpsertRoomLabelRequest,
|
|
synctv_proto::client::RoomLabel,
|
|
synctv_proto::admin::DeleteRoomLabelResponse,
|
|
synctv_proto::admin::UpdateRoomTaxonomyRequest,
|
|
synctv_proto::admin::Room,
|
|
synctv_proto::admin::Room,
|
|
synctv_proto::admin::DeleteRoomResponse,
|
|
synctv_proto::admin::UpdateRoomPasswordRequest,
|
|
synctv_proto::admin::UpdateRoomPasswordResponse,
|
|
synctv_proto::admin::GetRoomMembersResponse,
|
|
synctv_proto::admin::BanRoomRequest,
|
|
synctv_proto::admin::Room,
|
|
synctv_proto::admin::Room,
|
|
synctv_proto::admin::GetRoomSettingsResponse,
|
|
synctv_proto::admin::UpdateRoomSettingsRequest,
|
|
synctv_proto::admin::Room,
|
|
synctv_proto::admin::BatchBanRoomsRequest,
|
|
synctv_proto::admin::BatchBanRoomsResponse,
|
|
synctv_proto::admin::BatchDeleteRoomsRequest,
|
|
synctv_proto::admin::BatchDeleteRoomsResponse,
|
|
synctv_proto::providers::common::ListProviderInstancesRequest,
|
|
synctv_proto::providers::common::ListProviderInstancesResponse,
|
|
synctv_proto::providers::common::AddProviderInstanceRequest,
|
|
synctv_proto::providers::common::AddProviderInstanceResponse,
|
|
synctv_proto::providers::common::UpdateProviderInstanceRequest,
|
|
synctv_proto::providers::common::UpdateProviderInstanceResponse,
|
|
synctv_proto::providers::common::DeleteProviderInstanceResponse,
|
|
synctv_proto::providers::common::ReconnectProviderInstanceResponse,
|
|
synctv_proto::providers::common::EnableProviderInstanceResponse,
|
|
synctv_proto::providers::common::DisableProviderInstanceResponse,
|
|
synctv_proto::admin::ListActiveStreamsResponse,
|
|
synctv_proto::admin::KickStreamRequest,
|
|
synctv_proto::admin::KickStreamResponse,
|
|
synctv_proto::admin::ListAdminsResponse,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::RemoveAdminResponse,
|
|
synctv_proto::admin::AdminUser,
|
|
synctv_proto::admin::Room,
|
|
synctv_proto::providers::common::ProviderInstance
|
|
)
|
|
),
|
|
modifiers(&SecurityAddon),
|
|
tags(
|
|
(name = "Health", description = "Health and readiness endpoints"),
|
|
(name = "Public", description = "Unauthenticated public endpoints"),
|
|
(name = "Auth", description = "Authentication and session lifecycle"),
|
|
(name = "WebSocket", description = "WebSocket handshake and ticket bootstrap endpoints"),
|
|
(name = "WebRTC", description = "WebRTC transport bootstrap endpoints"),
|
|
(name = "Streaming", description = "Publish-key and live-stream bootstrap endpoints"),
|
|
(name = "Email", description = "Email bind, login, and password reset endpoints"),
|
|
(name = "Notification", description = "Authenticated user notification endpoints"),
|
|
(name = "OAuth2", description = "OAuth2 login and account-link endpoints"),
|
|
(name = "Provider", description = "Provider discovery and backend selection endpoints"),
|
|
(name = "DirectUrl Playback Provider", description = "DirectUrl playback transport endpoints"),
|
|
(name = "Alist Playback Provider", description = "Alist playback transport endpoints"),
|
|
(name = "Emby Playback Provider", description = "Emby playback transport endpoints"),
|
|
(name = "Bilibili Playback Provider", description = "Bilibili playback and danmaku transport endpoints"),
|
|
(name = "RTMP Playback Provider", description = "RTMP live playback transport endpoints"),
|
|
(name = "LiveProxy Playback Provider", description = "LiveProxy playback transport endpoints"),
|
|
(name = "User", description = "Current-user profile and ownership endpoints"),
|
|
(name = "Room", description = "Core room lifecycle, membership, media, playback and playlist endpoints"),
|
|
(name = "Room Member", description = "Room-scoped member moderation and permission endpoints"),
|
|
(name = "Admin", description = "Administrative and moderation HTTP endpoints")
|
|
)
|
|
)]
|
|
pub struct ApiDoc;
|
|
|
|
struct SecurityAddon;
|
|
|
|
impl utoipa::Modify for SecurityAddon {
|
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
|
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
|
|
|
let components = openapi.components.get_or_insert_with(Default::default);
|
|
components.add_security_scheme(
|
|
"bearer_auth",
|
|
SecurityScheme::Http(
|
|
HttpBuilder::new()
|
|
.scheme(HttpAuthScheme::Bearer)
|
|
.bearer_format("JWT")
|
|
.build(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
Router::new()
|
|
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::Value;
|
|
use utoipa::OpenApi;
|
|
|
|
type TestResult<T = ()> = anyhow::Result<T>;
|
|
|
|
fn test_error(message: impl Into<String>) -> anyhow::Error {
|
|
anyhow::anyhow!(message.into())
|
|
}
|
|
|
|
fn openapi_json() -> TestResult<Value> {
|
|
Ok(serde_json::to_value(super::ApiDoc::openapi())?)
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_preserves_authored_path_order() {
|
|
let doc = super::ApiDoc::openapi();
|
|
let paths = doc
|
|
.paths
|
|
.paths
|
|
.keys()
|
|
.take(5)
|
|
.map(String::as_str)
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(
|
|
paths,
|
|
[
|
|
"/api/public/settings",
|
|
"/api/public/server-info",
|
|
"/api/public/time",
|
|
"/api/auth/email/confirm",
|
|
"/api/auth/guest-token",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_preserves_component_field_order() -> TestResult {
|
|
let doc = super::ApiDoc::openapi();
|
|
let schema = doc
|
|
.components
|
|
.as_ref()
|
|
.and_then(|components| components.schemas.get("GoogleRpcStatusSchema"))
|
|
.ok_or_else(|| test_error("GoogleRpcStatusSchema should be registered"))?;
|
|
let schema_json = serde_json::to_string(schema)?;
|
|
let code = schema_json
|
|
.find("\"code\"")
|
|
.ok_or_else(|| test_error("code property should be documented"))?;
|
|
let message = schema_json
|
|
.find("\"message\"")
|
|
.ok_or_else(|| test_error("message property should be documented"))?;
|
|
let details = schema_json
|
|
.find("\"details\"")
|
|
.ok_or_else(|| test_error("details property should be documented"))?;
|
|
|
|
assert!(code < message && message < details);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_keeps_bearer_auth_off_public_routes() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
assert!(
|
|
doc.get("security").is_none(),
|
|
"document-level security should not force bearer auth onto public routes"
|
|
);
|
|
|
|
for (path, method) in [
|
|
("/api/public/settings", "get"),
|
|
("/api/auth/email/confirm", "post"),
|
|
("/api/auth/opaque/registration/start", "post"),
|
|
("/api/auth/opaque/registration/finish", "post"),
|
|
("/api/rooms/discover", "get"),
|
|
("/api/rooms/{roomId}/discovery", "get"),
|
|
] {
|
|
assert!(
|
|
doc["paths"][path][method].get("security").is_none(),
|
|
"{method} {path} should be documented as public"
|
|
);
|
|
}
|
|
|
|
let notifications_security = doc["paths"]["/api/notifications"]["get"]["security"]
|
|
.as_array()
|
|
.ok_or_else(|| test_error("authenticated endpoints should declare security"))?;
|
|
assert!(
|
|
!notifications_security.is_empty(),
|
|
"authenticated endpoints should keep bearer auth requirements"
|
|
);
|
|
for path in [
|
|
"/api/user/rooms/discover",
|
|
"/api/user/rooms/{roomId}/discovery",
|
|
] {
|
|
let security = doc["paths"][path]["get"]["security"]
|
|
.as_array()
|
|
.ok_or_else(|| test_error("user discovery should declare security"))?;
|
|
assert!(
|
|
!security.is_empty(),
|
|
"GET {path} should require bearer auth"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_cloudreve_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for (path, method) in [
|
|
("/api/providers/cloudreve/login", "post"),
|
|
("/api/providers/cloudreve/list", "post"),
|
|
("/api/providers/cloudreve/search", "post"),
|
|
("/api/providers/cloudreve/me", "post"),
|
|
("/api/providers/cloudreve/logout", "post"),
|
|
("/api/providers/cloudreve/binds", "get"),
|
|
] {
|
|
let operation = &doc["paths"][path][method];
|
|
assert!(
|
|
operation.is_object(),
|
|
"{method} {path} should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"{method} {path} should document its success response"
|
|
);
|
|
assert!(
|
|
operation["security"].is_array(),
|
|
"{method} {path} should require bearer authentication"
|
|
);
|
|
}
|
|
|
|
for path in [
|
|
"/api/providers/cloudreve/login",
|
|
"/api/providers/cloudreve/list",
|
|
"/api/providers/cloudreve/search",
|
|
"/api/providers/cloudreve/me",
|
|
"/api/providers/cloudreve/logout",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"POST {path} should document its protobuf request body"
|
|
);
|
|
}
|
|
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("cloudreve")),
|
|
"Cloudreve protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_youtube_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for (path, method) in [
|
|
("/api/providers/youtube/bind", "post"),
|
|
("/api/providers/youtube/binds", "get"),
|
|
("/api/providers/youtube/unbind", "post"),
|
|
(
|
|
"/api/playback-providers/{roomId}/youtube/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
("/api/playback-providers/{roomId}/youtube/{version}/segments", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/youtube/{version}/subtitles/{modeName}/{subtitleIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
let operation = &doc["paths"][path][method];
|
|
assert!(
|
|
operation.is_object(),
|
|
"{method} {path} should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"{method} {path} should document its success response"
|
|
);
|
|
}
|
|
|
|
for path in [
|
|
"/api/providers/youtube/bind",
|
|
"/api/providers/youtube/unbind",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"POST {path} should document its protobuf request body"
|
|
);
|
|
}
|
|
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("youtube")),
|
|
"YouTube protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_generation_scoped_live_hls_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for provider in ["rtmp", "live-proxy"] {
|
|
for path in [
|
|
format!("/api/playback-providers/{{roomId}}/{provider}/{{version}}/hls-master"),
|
|
format!(
|
|
"/api/playback-providers/{{roomId}}/{provider}/{{version}}/hls/{{generationId}}/index.m3u8"
|
|
),
|
|
format!(
|
|
"/api/playback-providers/{{roomId}}/{provider}/{{version}}/hls/{{generationId}}/{{segmentName}}"
|
|
),
|
|
] {
|
|
let operation = &doc["paths"][&path]["get"];
|
|
assert!(operation.is_object(), "GET {path} should be documented");
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"GET {path} should document its success response"
|
|
);
|
|
}
|
|
for removed_path in [
|
|
format!("/api/playback-providers/{{roomId}}/{provider}/{{version}}/hls-playlist"),
|
|
format!(
|
|
"/api/playback-providers/{{roomId}}/{provider}/{{version}}/hls-segments/{{segmentName}}"
|
|
),
|
|
] {
|
|
assert!(
|
|
doc["paths"].get(&removed_path).is_none(),
|
|
"removed live HLS path should be absent: {removed_path}"
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_douyin_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for (path, method) in [
|
|
("/api/providers/douyin/bind", "post"),
|
|
("/api/providers/douyin/binds", "get"),
|
|
("/api/providers/douyin/unbind", "post"),
|
|
("/api/providers/douyin/resolve", "post"),
|
|
("/api/providers/douyin/user-posts", "post"),
|
|
(
|
|
"/api/playback-providers/{roomId}/douyin/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/douyin/{version}/hls-resources/{modeName}/{mediaIndex}/{resourceKind}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/douyin/{version}/danmakus/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
let operation = &doc["paths"][path][method];
|
|
assert!(
|
|
operation.is_object(),
|
|
"{method} {path} should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"{method} {path} should document its success response"
|
|
);
|
|
}
|
|
|
|
for path in [
|
|
"/api/providers/douyin/bind",
|
|
"/api/providers/douyin/unbind",
|
|
"/api/providers/douyin/resolve",
|
|
"/api/providers/douyin/user-posts",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"POST {path} should document its protobuf request body"
|
|
);
|
|
}
|
|
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("douyin")),
|
|
"Douyin protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_tiktok_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for (path, method) in [
|
|
("/api/providers/tiktok/bind", "post"),
|
|
("/api/providers/tiktok/binds", "get"),
|
|
("/api/providers/tiktok/unbind", "post"),
|
|
("/api/providers/tiktok/resolve", "post"),
|
|
("/api/providers/tiktok/user", "post"),
|
|
("/api/providers/tiktok/user-posts", "post"),
|
|
(
|
|
"/api/playback-providers/{roomId}/tiktok/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/tiktok/{version}/hls-resources/{modeName}/{mediaIndex}/{resourceKind}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/tiktok/{version}/subtitles/{modeName}/{subtitleIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
let operation = &doc["paths"][path][method];
|
|
assert!(
|
|
operation.is_object(),
|
|
"{method} {path} should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"{method} {path} should document its success response"
|
|
);
|
|
}
|
|
|
|
for path in [
|
|
"/api/providers/tiktok/bind",
|
|
"/api/providers/tiktok/unbind",
|
|
"/api/providers/tiktok/resolve",
|
|
"/api/providers/tiktok/user",
|
|
"/api/providers/tiktok/user-posts",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"POST {path} should document its protobuf request body"
|
|
);
|
|
}
|
|
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("tiktok")),
|
|
"TikTok protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_twitch_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for (path, method) in [
|
|
("/api/providers/twitch/bind", "post"),
|
|
("/api/providers/twitch/binds", "get"),
|
|
("/api/providers/twitch/unbind", "post"),
|
|
("/api/providers/twitch/resolve", "post"),
|
|
("/api/providers/twitch/channel-items", "post"),
|
|
(
|
|
"/api/playback-providers/{roomId}/twitch/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
("/api/playback-providers/{roomId}/twitch/{version}/segments", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/twitch/{version}/chats/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
let operation = &doc["paths"][path][method];
|
|
assert!(
|
|
operation.is_object(),
|
|
"{method} {path} should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"{method} {path} should document its success response"
|
|
);
|
|
}
|
|
|
|
for path in [
|
|
"/api/providers/twitch/bind",
|
|
"/api/providers/twitch/unbind",
|
|
"/api/providers/twitch/resolve",
|
|
"/api/providers/twitch/channel-items",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"POST {path} should document its protobuf request body"
|
|
);
|
|
}
|
|
|
|
let chat_response = &doc["paths"]
|
|
["/api/playback-providers/{roomId}/twitch/{version}/chats/{modeName}/{mediaIndex}"]
|
|
["get"]["responses"]["200"]["content"]["text/event-stream"];
|
|
assert!(
|
|
chat_response["schema"].is_object(),
|
|
"Twitch chat SSE should document its event schema"
|
|
);
|
|
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("twitch")),
|
|
"Twitch protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_huya_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
let operation = &doc["paths"]["/api/providers/huya/resolve"]["post"];
|
|
assert!(operation.is_object(), "Huya resolve should be documented");
|
|
assert!(
|
|
operation["requestBody"].is_object(),
|
|
"Huya resolve should document its protobuf request body"
|
|
);
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"Huya resolve should document its response"
|
|
);
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("huya")),
|
|
"Huya provider schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_douyu_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
let operation = &doc["paths"]["/api/providers/douyu/resolve"]["post"];
|
|
assert!(operation.is_object(), "Douyu resolve should be documented");
|
|
assert!(operation["requestBody"].is_object());
|
|
assert!(operation["responses"]["200"].is_object());
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(schemas.keys().any(|name| name.contains("douyu")));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_acfun_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
let operation = &doc["paths"]["/api/providers/acfun/resolve"]["post"];
|
|
assert!(operation.is_object(), "AcFun resolve should be documented");
|
|
assert!(operation["requestBody"].is_object());
|
|
assert!(operation["responses"]["200"].is_object());
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(schemas.keys().any(|name| name.contains("acfun")));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_cctv_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
let operation = &doc["paths"]["/api/providers/cctv/resolve"]["post"];
|
|
assert!(operation.is_object(), "CCTV resolve should be documented");
|
|
assert!(operation["requestBody"].is_object());
|
|
assert!(operation["responses"]["200"].is_object());
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(schemas.keys().any(|name| name.contains("cctv")));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_fnos_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for (path, method) in [
|
|
("/api/providers/fnos/login", "post"),
|
|
("/api/providers/fnos/list", "post"),
|
|
("/api/providers/fnos/server-info", "post"),
|
|
("/api/providers/fnos/logout", "post"),
|
|
("/api/providers/fnos/binds", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/fnos/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/fnos/{version}/segments",
|
|
"get",
|
|
),
|
|
] {
|
|
assert!(
|
|
doc["paths"][path][method].is_object(),
|
|
"{method} {path} should be documented"
|
|
);
|
|
}
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("fnos")),
|
|
"FNOS protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_qnap_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for (path, method) in [
|
|
("/api/providers/qnap/login", "post"),
|
|
("/api/providers/qnap/list", "post"),
|
|
("/api/providers/qnap/capabilities", "post"),
|
|
("/api/providers/qnap/logout", "post"),
|
|
("/api/providers/qnap/binds", "get"),
|
|
("/api/providers/qnap/thumbnail", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/qnap/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/qnap/{version}/subtitles/{modeName}/{subtitleIndex}",
|
|
"get",
|
|
),
|
|
("/api/playback-providers/{roomId}/qnap/{version}/thumbnail", "get"),
|
|
] {
|
|
let operation = &doc["paths"][path][method];
|
|
assert!(
|
|
operation.is_object(),
|
|
"{method} {path} should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["200"].is_object(),
|
|
"{method} {path} should document its success response"
|
|
);
|
|
}
|
|
|
|
for path in [
|
|
"/api/providers/qnap/login",
|
|
"/api/providers/qnap/list",
|
|
"/api/providers/qnap/capabilities",
|
|
"/api/providers/qnap/logout",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"POST {path} should document its protobuf request body"
|
|
);
|
|
}
|
|
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("qnap")),
|
|
"QNAP protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_synology_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for (path, method) in [
|
|
("/api/providers/synology/login", "post"),
|
|
("/api/providers/synology/files", "post"),
|
|
("/api/providers/synology/libraries", "post"),
|
|
("/api/providers/synology/movies", "post"),
|
|
("/api/providers/synology/tv-shows", "post"),
|
|
("/api/providers/synology/episodes", "post"),
|
|
("/api/providers/synology/home-videos", "post"),
|
|
("/api/providers/synology/tv-recordings", "post"),
|
|
("/api/providers/synology/logout", "post"),
|
|
("/api/providers/synology/binds", "get"),
|
|
("/api/providers/synology/image", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/synology/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
("/api/playback-providers/{roomId}/synology/{version}/segments", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/synology/{version}/subtitles/{modeName}/{subtitleIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
assert!(
|
|
doc["paths"][path][method].is_object(),
|
|
"missing Synology OpenAPI operation {method} {path}"
|
|
);
|
|
}
|
|
|
|
for path in [
|
|
"/api/providers/synology/login",
|
|
"/api/providers/synology/files",
|
|
"/api/providers/synology/libraries",
|
|
"/api/providers/synology/movies",
|
|
"/api/providers/synology/tv-shows",
|
|
"/api/providers/synology/episodes",
|
|
"/api/providers/synology/home-videos",
|
|
"/api/providers/synology/tv-recordings",
|
|
"/api/providers/synology/logout",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"missing Synology request body for {path}"
|
|
);
|
|
}
|
|
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("synology")),
|
|
"Synology protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_nextcloud_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for (path, method) in [
|
|
("/api/providers/nextcloud/login", "post"),
|
|
("/api/providers/nextcloud/login-flow/start", "post"),
|
|
("/api/providers/nextcloud/login-flow/poll", "post"),
|
|
("/api/providers/nextcloud/list", "post"),
|
|
("/api/providers/nextcloud/favorites", "post"),
|
|
("/api/providers/nextcloud/logout", "post"),
|
|
("/api/providers/nextcloud/binds", "get"),
|
|
("/api/providers/nextcloud/preview", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/nextcloud/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/nextcloud/{version}/subtitles/{modeName}/{subtitleIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
assert!(
|
|
doc["paths"][path][method].is_object(),
|
|
"missing Nextcloud OpenAPI operation {method} {path}"
|
|
);
|
|
}
|
|
for path in [
|
|
"/api/providers/nextcloud/login",
|
|
"/api/providers/nextcloud/login-flow/start",
|
|
"/api/providers/nextcloud/login-flow/poll",
|
|
"/api/providers/nextcloud/list",
|
|
"/api/providers/nextcloud/favorites",
|
|
"/api/providers/nextcloud/logout",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"missing Nextcloud request body for {path}"
|
|
);
|
|
}
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("nextcloud")),
|
|
"Nextcloud protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_seafile_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for (path, method) in [
|
|
("/api/providers/seafile/login", "post"),
|
|
("/api/providers/seafile/unlock-library", "post"),
|
|
("/api/providers/seafile/repositories", "post"),
|
|
("/api/providers/seafile/list", "post"),
|
|
("/api/providers/seafile/starred", "post"),
|
|
("/api/providers/seafile/logout", "post"),
|
|
("/api/providers/seafile/binds", "get"),
|
|
("/api/providers/seafile/thumbnail", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/seafile/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/seafile/{version}/subtitles/{modeName}/{subtitleIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
assert!(
|
|
doc["paths"][path][method].is_object(),
|
|
"missing Seafile OpenAPI operation {method} {path}"
|
|
);
|
|
}
|
|
for path in [
|
|
"/api/providers/seafile/login",
|
|
"/api/providers/seafile/unlock-library",
|
|
"/api/providers/seafile/repositories",
|
|
"/api/providers/seafile/list",
|
|
"/api/providers/seafile/starred",
|
|
"/api/providers/seafile/logout",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"missing Seafile request body for {path}"
|
|
);
|
|
}
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("seafile")),
|
|
"Seafile protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_truenas_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for (path, method) in [
|
|
("/api/providers/truenas/login", "post"),
|
|
("/api/providers/truenas/list", "post"),
|
|
("/api/providers/truenas/logout", "post"),
|
|
("/api/providers/truenas/binds", "get"),
|
|
(
|
|
"/api/playback-providers/{roomId}/truenas/{version}/resources/{modeName}/{mediaIndex}",
|
|
"get",
|
|
),
|
|
(
|
|
"/api/playback-providers/{roomId}/truenas/{version}/subtitles/{modeName}/{subtitleIndex}",
|
|
"get",
|
|
),
|
|
] {
|
|
assert!(
|
|
doc["paths"][path][method].is_object(),
|
|
"missing TrueNAS OpenAPI operation {method} {path}"
|
|
);
|
|
}
|
|
for path in [
|
|
"/api/providers/truenas/login",
|
|
"/api/providers/truenas/list",
|
|
"/api/providers/truenas/logout",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["post"]["requestBody"].is_object(),
|
|
"missing TrueNAS request body for {path}"
|
|
);
|
|
}
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("truenas")),
|
|
"TrueNAS protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_huya_playback_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for path in [
|
|
"/api/playback-providers/{roomId}/huya/{version}/resources/{modeName}/{mediaIndex}",
|
|
"/api/playback-providers/{roomId}/huya/{version}/segments",
|
|
"/api/playback-providers/{roomId}/huya/{version}/danmakus/{modeName}/{mediaIndex}",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["get"].is_object(),
|
|
"GET {path} should be documented"
|
|
);
|
|
}
|
|
let danmaku = &doc["paths"]
|
|
["/api/playback-providers/{roomId}/huya/{version}/danmakus/{modeName}/{mediaIndex}"]
|
|
["get"]["responses"]["200"]["content"]["text/event-stream"];
|
|
assert!(
|
|
danmaku["schema"].is_object(),
|
|
"Huya danmaku should document its event schema"
|
|
);
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("huya")),
|
|
"Huya protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_douyu_playback_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for path in [
|
|
"/api/playback-providers/{roomId}/douyu/{version}/resources/{modeName}/{mediaIndex}",
|
|
"/api/playback-providers/{roomId}/douyu/{version}/segments",
|
|
"/api/playback-providers/{roomId}/douyu/{version}/danmakus/{modeName}/{mediaIndex}",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["get"].is_object(),
|
|
"GET {path} should be documented"
|
|
);
|
|
}
|
|
let danmaku = &doc["paths"]
|
|
["/api/playback-providers/{roomId}/douyu/{version}/danmakus/{modeName}/{mediaIndex}"]
|
|
["get"]["responses"]["200"]["content"]["text/event-stream"];
|
|
assert!(
|
|
danmaku["schema"].is_object(),
|
|
"Douyu danmaku should document its event schema"
|
|
);
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("douyu")),
|
|
"Douyu protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_acfun_playback_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for path in [
|
|
"/api/playback-providers/{roomId}/acfun/{version}/resources/{modeName}/{mediaIndex}",
|
|
"/api/playback-providers/{roomId}/acfun/{version}/hls-resources/{modeName}/{mediaIndex}/{resourceKind}",
|
|
"/api/playback-providers/{roomId}/acfun/{version}/danmaku-files/{modeName}/{mediaIndex}",
|
|
"/api/playback-providers/{roomId}/acfun/{version}/danmakus/{modeName}/{mediaIndex}",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["get"].is_object(),
|
|
"GET {path} should be documented"
|
|
);
|
|
}
|
|
let danmaku = &doc["paths"]
|
|
["/api/playback-providers/{roomId}/acfun/{version}/danmakus/{modeName}/{mediaIndex}"]
|
|
["get"]["responses"]["200"]["content"]["text/event-stream"];
|
|
assert!(
|
|
danmaku["schema"].is_object(),
|
|
"AcFun live danmaku should document its event schema"
|
|
);
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas.keys().any(|name| name.contains("acfun")),
|
|
"AcFun protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_cctv_playback_provider_contract() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
for path in [
|
|
"/api/playback-providers/{roomId}/cctv/{version}/resources/{modeName}/{mediaIndex}",
|
|
"/api/playback-providers/{roomId}/cctv/{version}/segments",
|
|
] {
|
|
assert!(
|
|
doc["paths"][path]["get"].is_object(),
|
|
"missing CCTV playback provider path {path}"
|
|
);
|
|
}
|
|
let schemas = doc["components"]["schemas"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error("OpenAPI components.schemas should be an object"))?;
|
|
assert!(
|
|
schemas
|
|
.keys()
|
|
.any(|name| name.to_ascii_lowercase().contains("cctv")),
|
|
"CCTV protobuf schemas should be registered"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_matches_public_room_error_contracts() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
assert!(
|
|
doc["paths"]["/api/rooms/discover"]["get"]["responses"]["400"].is_object(),
|
|
"room discovery should document validation errors"
|
|
);
|
|
assert!(
|
|
doc["paths"]["/api/rooms/{roomId}/discovery"]["get"]["responses"]["400"].is_object(),
|
|
"room discovery item should document invalid room IDs"
|
|
);
|
|
assert!(
|
|
doc["paths"]["/api/rooms/{roomId}/discovery"]["get"]["responses"]["404"].is_object(),
|
|
"room discovery item should document missing rooms"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_oauth2_unconfigured_as_service_unavailable() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
let responses = &doc["paths"]["/api/oauth2/providers"]["get"]["responses"];
|
|
|
|
assert!(
|
|
responses["503"].is_object(),
|
|
"OAuth2 provider listing should document the unconfigured case as 503"
|
|
);
|
|
assert!(
|
|
responses.get("400").is_none(),
|
|
"OAuth2 provider listing should not document missing service as a bad request"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_marks_notifications_read_all_body_optional() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
let request_body = &doc["paths"]["/api/notifications/read-all"]["post"]["requestBody"];
|
|
assert!(
|
|
request_body.is_object(),
|
|
"mark-all-as-read should still document its request body schema"
|
|
);
|
|
|
|
let required = request_body["required"].as_bool();
|
|
|
|
assert_ne!(
|
|
required,
|
|
Some(true),
|
|
"mark-all-as-read should not document its request body as required"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_marks_publish_key_body_optional() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
let request_body = &doc["paths"]
|
|
["/api/playback-providers/{roomId}/rtmp/{mediaId}/publish-key"]["post"]["requestBody"];
|
|
assert!(
|
|
request_body.is_object(),
|
|
"publish-key creation should document its request body schema"
|
|
);
|
|
assert_ne!(
|
|
request_body["required"].as_bool(),
|
|
Some(true),
|
|
"legacy publish-key creation may omit the request body"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_admin_user_preferences_routes() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
let path = &doc["paths"]["/api/admin/users/{userId}/preferences"];
|
|
assert!(
|
|
path["get"].is_object(),
|
|
"admin get-user-preferences route should be documented"
|
|
);
|
|
assert!(
|
|
path["patch"].is_object(),
|
|
"admin update-user-preferences route should be documented"
|
|
);
|
|
|
|
let request_body = &path["patch"]["requestBody"];
|
|
assert!(
|
|
request_body.is_object(),
|
|
"admin update-user-preferences should document its request body"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_chat_routes() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for (path, method, responses) in [
|
|
(
|
|
"/api/rooms/{roomId}/chat/messages",
|
|
"post",
|
|
&["200", "400", "401", "403", "429"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/chat/attachments/upload-session",
|
|
"post",
|
|
&["200", "400", "401", "403", "429"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/chat/messages/{messageId}",
|
|
"get",
|
|
&["200", "400", "401", "403", "404"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/chat/messages/{messageId}",
|
|
"patch",
|
|
&["200", "400", "401", "403", "404", "409"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/chat/messages/{messageId}",
|
|
"delete",
|
|
&["200", "400", "401", "403", "404", "409"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/chat/messages/{messageId}/context",
|
|
"get",
|
|
&["200", "400", "401", "403", "404"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/chat/read-state",
|
|
"post",
|
|
&["200", "400", "401", "403", "404"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/chat/read-state",
|
|
"get",
|
|
&["200", "401", "403"][..],
|
|
),
|
|
(
|
|
"/api/rooms/{roomId}/watch/chat-events",
|
|
"get",
|
|
&["200", "400", "401", "403", "503"][..],
|
|
),
|
|
] {
|
|
assert_response_codes(&doc, path, method, responses)?;
|
|
}
|
|
|
|
assert_parameter_location(
|
|
&doc,
|
|
"/api/rooms/{roomId}/chat/messages/{messageId}",
|
|
"get",
|
|
"includeDeleted",
|
|
"query",
|
|
)?;
|
|
assert_parameter_location(
|
|
&doc,
|
|
"/api/rooms/{roomId}/watch/chat-events",
|
|
"get",
|
|
"afterEventSequence",
|
|
"query",
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn assert_response_codes(
|
|
doc: &Value,
|
|
path: &str,
|
|
method: &str,
|
|
expected: &[&str],
|
|
) -> TestResult {
|
|
let responses = doc["paths"][path][method]["responses"]
|
|
.as_object()
|
|
.ok_or_else(|| test_error(format!("{method} {path} should document responses")))?;
|
|
|
|
for code in expected {
|
|
assert!(
|
|
responses.get(*code).is_some_and(Value::is_object),
|
|
"{method} {path} should document HTTP {code}; responses were {:?}",
|
|
responses.keys().collect::<Vec<_>>()
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn assert_parameter_location(
|
|
doc: &Value,
|
|
path: &str,
|
|
method: &str,
|
|
name: &str,
|
|
expected_location: &str,
|
|
) -> TestResult {
|
|
let params = doc["paths"][path][method]["parameters"]
|
|
.as_array()
|
|
.ok_or_else(|| test_error(format!("{method} {path} should document parameters")))?;
|
|
let locations = params
|
|
.iter()
|
|
.filter(|param| param["name"] == name)
|
|
.map(|param| param["in"].as_str().unwrap_or("<missing>"))
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(
|
|
locations,
|
|
vec![expected_location],
|
|
"{method} {path} should document {name} only as {expected_location}; got {locations:?}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn assert_parameter_absent(doc: &Value, path: &str, method: &str, name: &str) -> TestResult {
|
|
let params = doc["paths"][path][method]["parameters"]
|
|
.as_array()
|
|
.map_or(&[][..], Vec::as_slice);
|
|
let locations = params
|
|
.iter()
|
|
.filter(|param| param["name"] == name)
|
|
.map(|param| param["in"].as_str().unwrap_or("<missing>"))
|
|
.collect::<Vec<_>>();
|
|
|
|
assert!(
|
|
locations.is_empty(),
|
|
"{method} {path} should document {name} in the request body; got parameter locations {locations:?}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn assert_request_body_schema_ref(
|
|
doc: &Value,
|
|
path: &str,
|
|
method: &str,
|
|
expected_schema: &str,
|
|
) -> TestResult {
|
|
let schema_ref = doc["paths"][path][method]["requestBody"]["content"]["application/json"]
|
|
["schema"]["$ref"]
|
|
.as_str()
|
|
.ok_or_else(|| {
|
|
test_error(format!(
|
|
"{method} {path} should document a JSON request body schema ref"
|
|
))
|
|
})?;
|
|
|
|
assert!(
|
|
schema_ref.ends_with(expected_schema),
|
|
"{method} {path} should use {expected_schema} request body schema; got {schema_ref}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_query_struct_params_as_query() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
for name in [
|
|
"page",
|
|
"pageSize",
|
|
"search",
|
|
"role",
|
|
"sortBy",
|
|
"sortDirection",
|
|
] {
|
|
assert_parameter_location(&doc, "/api/rooms/{roomId}/members", "get", name, "query")?;
|
|
}
|
|
assert_parameter_location(&doc, "/api/rooms/{roomId}/members", "get", "roomId", "path")?;
|
|
|
|
for name in [
|
|
"page",
|
|
"pageSize",
|
|
"status",
|
|
"search",
|
|
"isBanned",
|
|
"sortBy",
|
|
"sortDirection",
|
|
] {
|
|
assert_parameter_location(
|
|
&doc,
|
|
"/api/admin/users/{userId}/rooms",
|
|
"get",
|
|
name,
|
|
"query",
|
|
)?;
|
|
}
|
|
assert_parameter_location(
|
|
&doc,
|
|
"/api/admin/users/{userId}/rooms",
|
|
"get",
|
|
"userId",
|
|
"path",
|
|
)?;
|
|
|
|
for name in [
|
|
"page",
|
|
"pageSize",
|
|
"search",
|
|
"role",
|
|
"sortBy",
|
|
"sortDirection",
|
|
] {
|
|
assert_parameter_location(
|
|
&doc,
|
|
"/api/admin/rooms/{roomId}/members",
|
|
"get",
|
|
name,
|
|
"query",
|
|
)?;
|
|
}
|
|
assert_parameter_location(
|
|
&doc,
|
|
"/api/admin/rooms/{roomId}/members",
|
|
"get",
|
|
"roomId",
|
|
"path",
|
|
)?;
|
|
|
|
assert_parameter_absent(&doc, "/api/providers/alist/list", "post", "instanceName")?;
|
|
assert_request_body_schema_ref(
|
|
&doc,
|
|
"/api/providers/alist/list",
|
|
"post",
|
|
"synctv_provider_alist_ListRequest",
|
|
)?;
|
|
assert_parameter_location(
|
|
&doc,
|
|
"/api/rooms/{roomId}/playback",
|
|
"get",
|
|
"streamPreference",
|
|
"query",
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_provider_endpoint_error_contracts() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
|
|
let provider_operation_errors = ["400", "401", "403", "404", "408", "409", "429", "503"];
|
|
for (path, method) in [
|
|
("/api/providers/alist/login", "post"),
|
|
("/api/providers/alist/list", "post"),
|
|
("/api/providers/bilibili/parse", "post"),
|
|
("/api/providers/bilibili/login/qr/generate", "post"),
|
|
("/api/providers/emby/login", "post"),
|
|
("/api/providers/emby/list", "post"),
|
|
] {
|
|
assert_response_codes(&doc, path, method, &provider_operation_errors)?;
|
|
}
|
|
|
|
let provider_binds_errors = ["400", "401", "403", "408", "429", "503"];
|
|
for path in [
|
|
"/api/providers/alist/binds",
|
|
"/api/providers/bilibili/binds",
|
|
"/api/providers/emby/binds",
|
|
] {
|
|
assert_response_codes(&doc, path, "get", &provider_binds_errors)?;
|
|
}
|
|
|
|
let admin_provider_errors = ["400", "401", "403", "408", "429", "503"];
|
|
for (path, method) in [
|
|
("/api/providers/instances", "get"),
|
|
("/api/providers/instances", "post"),
|
|
] {
|
|
assert_response_codes(&doc, path, method, &admin_provider_errors)?;
|
|
}
|
|
|
|
let admin_provider_mutation_errors =
|
|
["400", "401", "403", "404", "408", "409", "429", "503"];
|
|
for (path, method) in [
|
|
("/api/providers/instances/{name}", "put"),
|
|
("/api/providers/instances/{name}", "delete"),
|
|
("/api/providers/instances/{name}/reconnect", "post"),
|
|
("/api/providers/instances/{name}/enable", "post"),
|
|
("/api/providers/instances/{name}/disable", "post"),
|
|
] {
|
|
assert_response_codes(&doc, path, method, &admin_provider_mutation_errors)?;
|
|
}
|
|
|
|
assert_response_codes(
|
|
&doc,
|
|
"/api/providers/emby/thumbnail/{itemId}",
|
|
"get",
|
|
&["400", "401", "403", "404", "408", "429", "503"],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_playlist_source_fields() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
let playlist = doc
|
|
.pointer("/components/schemas/synctv_client_Playlist/properties")
|
|
.ok_or_else(|| test_error("Playlist schema properties should exist"))?;
|
|
|
|
for field in ["sourceConfig", "sourceProvider", "providerInstanceName"] {
|
|
assert!(
|
|
playlist.get(field).is_some(),
|
|
"Playlist schema should document {field}: {playlist:?}"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn openapi_documents_websocket_handshake_endpoint() -> TestResult {
|
|
let doc = openapi_json()?;
|
|
let operation = &doc["paths"]["/ws/rooms/{roomId}"]["get"];
|
|
|
|
assert!(
|
|
operation.is_object(),
|
|
"WebSocket route should be present in OpenAPI"
|
|
);
|
|
assert_eq!(operation["operationId"], "connectRoomWebSocket");
|
|
|
|
let params = operation["parameters"]
|
|
.as_array()
|
|
.ok_or_else(|| test_error("WebSocket route should describe handshake parameters"))?;
|
|
assert!(
|
|
params.iter().any(|param| {
|
|
param["name"] == "roomId" && param["in"] == "path" && param["required"] == true
|
|
}),
|
|
"room_id path parameter should be documented"
|
|
);
|
|
assert!(
|
|
params
|
|
.iter()
|
|
.any(|param| param["name"] == "ticket" && param["in"] == "query"),
|
|
"ticket query parameter should be documented"
|
|
);
|
|
assert!(
|
|
params
|
|
.iter()
|
|
.any(|param| param["name"] == "Authorization" && param["in"] == "header"),
|
|
"Authorization header should be documented"
|
|
);
|
|
assert!(
|
|
params
|
|
.iter()
|
|
.any(|param| param["name"] == "Origin" && param["in"] == "header"),
|
|
"Origin header should be documented"
|
|
);
|
|
|
|
assert!(
|
|
operation["responses"]["101"].is_object(),
|
|
"successful WebSocket upgrade should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["401"].is_object(),
|
|
"authentication failures should be documented"
|
|
);
|
|
assert!(
|
|
operation["responses"]["503"].is_object(),
|
|
"runtime dependency failures should be documented"
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|