/* * Copyright (c) 2026, Tobias Müller git@tsmr.eu * */ use crate::api::ApiRuntime; pub use crate::api::PqcPreKeyInput; use crate::api::Server; use crate::context::Context; use crate::error::{Result, TwonlyError}; use crate::frb_generated::StreamSink; use crate::services::avatars; use crate::services::contacts::ContactService; use crate::services::media_upload::{MediaSizeReport, MediaUploadService}; use crate::services::messages::MessageService; use crate::services::outbox_dispatch::OutboxDispatchService; use crate::services::stories::StoryAudience; use crate::user_config::UserConfig; use flutter_rust_bridge::frb; use std::collections::HashMap; #[frb(ignore)] pub enum ServerResult { Ok(T), ErrorCode(i32), } fn api_result(result: ServerResult) -> Result { match result { ServerResult::Ok(value) => Ok(value), ServerResult::ErrorCode(code) => Err(TwonlyError::Api(code)), } } fn empty_api_response(bytes: Vec) -> Result<()> { crate::api::runtime::helpers::decode_empty_ok(bytes).and_then(api_result) } #[derive(Clone, Debug)] pub struct ApiConfig { /// Full WebSocket URL, for example `wss://api.twonly.eu/api/client`. pub websocket_url: String, /// Authentication identity. `None` keeps the transport usable for /// unauthenticated registration/recovery requests. /// The persisted integer schema/app version used by the legacy-token /// migration guard in the Dart implementation. pub legacy_user_app_version: i64, pub in_background: bool, pub can_use_login_token_for_auth: bool, } impl ApiConfig { pub(crate) async fn from_rust_state(context: &Context) -> Result { let user = UserConfig::load_from(context)?; Ok(Self { websocket_url: format!("{}client", RustApi::api_base_url("wss".to_owned())), legacy_user_app_version: user.as_ref().map_or(0, |value| value.app_version), in_background: context.is_notification_runtime(), can_use_login_token_for_auth: user .as_ref() .is_some_and(|value| value.can_use_login_token_for_auth), }) } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ApiConnectionState { Stopped, Connecting, Connected, Authenticating, Authenticated, Reconnecting, Suspended, PermanentlyRejected, } #[derive(Clone, Debug)] pub struct ApiEvent { pub kind: ApiEventKind, pub state: Option, pub message: Option, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ApiEventKind { ConnectionStateChanged, TransportError, Authenticated, PlanUpdated, AppOutdated, NewDeviceRegistered, LoginTokenMigrated, /// An image shared into a home-screen widget finished downloading. The /// manifest is already rewritten; only the native widgets still have to be /// told to redraw, which is something Flutter has to ask for. WidgetMediaReceived, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbPasswordlessNotificationMessage { pub id: i64, pub encrypted_message: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbUserData { pub user_id: i64, pub username: Vec, pub public_identity_key: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbProofOfWork { pub prefix: String, pub difficulty: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbMemoriesUsage { pub current_bytes: i64, pub count: i64, pub max_bytes: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbAdditionalAccount { pub user_id: i64, pub plan_id: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbPlanBalance { pub used_daily_media_upload_limit: i64, pub used_upload_media_size_limit: i64, pub payment_period_days: Option, pub last_payment_done_unix_timestamp: Option, pub additional_accounts: Vec, pub auto_renewal: Option, pub additional_account_owner_id: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbPresignedPost { pub url: String, pub fields: Vec<(String, String)>, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrbMemoriesUploadUrls { pub media_id: String, pub thumbnail_upload: Option, pub full_upload: Option, } /// Flutter-facing facade for the Rust-owned API runtime. pub struct RustApi {} impl RustApi { #[frb(sync)] pub fn decode_avatar_svg(avatar_svg_compressed: Vec) -> String { avatars::decode_avatar_svg(avatar_svg_compressed) } #[frb(sync)] pub fn avatar_png_path(contact_id: i64, profile_counter: i64) -> Result { let ctx = Context::get_static()?; Ok( avatars::contact_avatar_path(ctx, contact_id, profile_counter) .display() .to_string(), ) } /// Path to a contact's avatar PNG, rendered from the stored SVG if it is /// missing. Returns `None` when the contact has no avatar at all. pub async fn ensure_avatar_png(contact_id: i64) -> Result> { let ctx = Context::get_static()?; let path = avatars::ensure_contact_avatar_png(ctx, contact_id).await?; Ok(path.map(|path| path.display().to_string())) } pub async fn current_user_avatar_path() -> Result> { let ctx = Context::get_static()?.clone(); let path = tokio::task::spawn_blocking(move || avatars::current_user_avatar_path(&ctx)) .await .map_err(|error| { TwonlyError::Generic(format!("avatar render task failed: {error}")) })??; Ok(path.map(|path| path.display().to_string())) } /// Settles every background transfer this device believes is still in /// flight and resumes any upload a terminated process left behind. pub async fn finish_started_media_uploads() -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx).finish_started_uploads().await } /// Creates the media row and its content-encryption material and returns /// the media id the UI addresses every later step by. pub async fn initialize_media_upload( media_type: String, display_limit_in_milliseconds: Option, is_draft_media: bool, ) -> Result { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .initialize(media_type, display_limit_in_milliseconds, is_draft_media) .await } /// Starts the optional location window for a Memory row created by the /// local save button rather than the outgoing-media initializer. pub async fn initialize_memory_location(media_id: String) -> Result<()> { let ctx = Context::get_static()?; crate::services::location_metadata::initialize_for_media(ctx, &media_id).await } /// Begins the optional precise-location lookup when the capture camera is /// opened, so a later photo usually has a fix immediately available. pub async fn prewarm_memory_location() -> Result<()> { let ctx = Context::get_static()?; crate::services::location_metadata::prewarm(ctx) } /// Creates one outgoing message per selected group, plus the story rows /// for `story_audience`, and starts the upload they all share. pub async fn send_media_to_groups( media_id: String, group_ids: Vec, additional_message_data: Option>, widget_only: bool, story_audience: Option, ) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .insert_into_messages( media_id, group_ids, additional_message_data, widget_only, story_audience, ) .await } pub async fn register_home_widget(widget_id: String, platform: String) -> Result<()> { let ctx = Context::get_static()?; crate::services::home_widget::register_widget(ctx, &widget_id, &platform).await } pub async fn unregister_home_widget(widget_id: String) -> Result<()> { let ctx = Context::get_static()?; crate::services::home_widget::unregister_widget(ctx, &widget_id).await } pub async fn set_home_widget_groups( widget_id: String, platform: String, contact_group_ids: Vec, ) -> Result<()> { let ctx = Context::get_static()?; crate::services::home_widget::set_widget_groups( ctx, &widget_id, &platform, &contact_group_ids, ) .await } pub async fn sync_widget_permissions() -> Result<()> { let ctx = Context::get_static()?; crate::services::home_widget::sync_permissions(ctx).await } pub async fn purge_widget_media() -> Result<()> { let ctx = Context::get_static()?; crate::services::home_widget::purge_widget_media(ctx).await } /// Republishes the widget manifest. /// /// The contact groups a widget offers in its configuration UI are read from /// that file, so it has to be rewritten whenever the groups change — not /// only when images arrive. Cheaper than a full permission sync, which this /// deliberately does not do: editing a group does not change which widgets /// are placed. pub async fn refresh_widget_manifest() -> Result<()> { let ctx = Context::get_static()?; crate::services::home_widget::refresh_manifest(ctx).await } /// Drops a single image a widget is showing, at the user's request. pub async fn delete_widget_media(media_id: String) -> Result<()> { let ctx = Context::get_static()?; crate::services::home_widget::delete_media(ctx, &media_id).await } /// Retries the media sends whose receipts are still marked for retry. pub async fn reupload_pending_media() -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx).reupload_pending().await } pub async fn store_media(media_id: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx).store(&media_id).await } /// Reports that a Flutter plugin step finished so Rust can record the /// derived state (thumbnail present, crop analyzed, new size and hash). pub async fn media_step_finished(media_id: String, kind: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .media_step_finished(&media_id, &kind) .await } /// Exports a stored media file to the user's photo library. pub async fn save_media_to_gallery(media_id: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .save_to_gallery(&media_id) .await } /// Trims fully transparent borders an editor left around a stored image and /// refreshes the preview and content hash derived from it. pub async fn crop_media_transparent_borders(media_id: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .crop_transparent_borders(&media_id) .await } /// Deletes every file of a media item while keeping its row. pub async fn remove_media_files(media_id: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx).remove_files(&media_id).await } /// Gives up on a media file whose source could not be produced, marking /// its messages as deleted by the sender instead of retrying forever. pub async fn abandon_media(media_id: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx).abandon(&media_id).await } /// Explains a send that stopped because the media was too large: the size /// it reached and the largest single object the plan accepts. pub async fn media_size_limit_report(media_id: String) -> Result { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .size_limit_report(&media_id) .await } /// Deletes temporary media whose messages are finished with it. pub async fn purge_media_temp_folder() -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx).purge_temp_folder().await } pub async fn set_media_display_limit( media_id: String, display_limit_in_milliseconds: Option, ) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .set_display_limit(&media_id, display_limit_in_milliseconds) .await } pub async fn set_media_requires_authentication( media_id: String, requires_authentication: bool, ) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .set_requires_authentication(&media_id, requires_authentication) .await } /// Stores the cut the editor's video trimmer asked for. Both bounds are /// milliseconds into the recording, `None` keeps that end of the clip. pub async fn set_media_trim( media_id: String, trim_start_ms: Option, trim_end_ms: Option, ) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .set_trim(&media_id, trim_start_ms, trim_end_ms) .await } pub async fn toggle_media_remove_audio(media_id: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx) .toggle_remove_audio(&media_id) .await } pub async fn request_contact_by_username(username: String) -> Result<()> { let ctx = Context::get_static()?; ContactService::new(ctx) .request_by_username(username, false) .await } pub async fn try_request_contact_by_id(contact_id: i64, expected_public_key: Vec) -> bool { let result = match Context::get_static() { Ok(ctx) => { ContactService::new(ctx) .request_by_id(contact_id, expected_public_key, false) .await } Err(error) => Err(error), }; if let Err(error) = result { tracing::error!(contact_id, %error, "failed to establish session and send contact request"); return false; } true } pub async fn authentication_headers() -> Result> { use base64::Engine as _; let ctx = Context::get_static()?; let user = UserConfig::load_required_from(ctx)?; let mut headers = HashMap::new(); if user.can_use_login_token_for_auth { let login_token = ctx.key_manager.lock().await.main_key.get_login_token(); headers.insert("x-twonly-user-id".into(), format!("{:016X}", user.user_id)); headers.insert("x-twonly-login-token".into(), hex::encode(login_token)); } else { let encoded = ctx .secure_storage .read("api_auth_token")? .ok_or_else(|| TwonlyError::Generic("API auth token is not defined".into()))?; let auth_token = base64::engine::general_purpose::STANDARD .decode(encoded) .map_err(|error| { TwonlyError::Generic(format!("invalid stored API auth token: {error}")) })?; headers.insert("x-twonly-auth-token".into(), hex::encode(auth_token)); } Ok(headers) } #[frb(sync)] pub fn api_base_url(protocol: String) -> String { // Every non-production build talks to the development server. A Flutter // profile build compiles Rust with `--release`, so `debug_assertions` // is off and cannot tell it apart from a store build; cargokit enables // the `dev-api` feature for that configuration instead (see // `rust/cargokit.yaml`). if cfg!(debug_assertions) || cfg!(feature = "dev-api") { format!("{}://dev-api.twonly.eu/api/", protocol) } else { format!("{}://api.twonly.eu/api/", protocol) } } pub async fn connect() -> Result<()> { let ctx = Context::get_static()?; ApiRuntime::connect(ctx).await } /// Reloads identity and authentication settings from Rust-owned storage. /// No configuration values are accepted from the caller. pub async fn reload_configuration() -> Result<()> { let ctx = Context::get_static()?; ApiRuntime::reload_configuration(ctx).await } pub async fn close() -> Result<()> { let ctx = Context::get_static()?; ApiRuntime::close(ctx).await } pub async fn connection_state() -> Result { let ctx = Context::get_static()?; ApiRuntime::connection_state(ctx).await } pub async fn set_background(in_background: bool) -> Result<()> { let ctx = Context::get_static()?; if in_background { // The socket is about to stop being an option, so anything still // waiting on it is handed to the OS, which delivers it whether or // not this process survives. let ctx = ctx.clone(); tokio::spawn(async move { if let Err(error) = OutboxDispatchService::new(&ctx).hand_pending_to_os().await { tracing::warn!(%error, "could not hand queued envelopes to the OS"); } }); } else { // Coming back to the foreground is the first chance to notice that // the OS finished a media transfer while this process was idle. crate::services::direct_media_upload::watch_pending_uploads(ctx); let ctx = ctx.clone(); tokio::spawn(async move { // Envelopes the OS has been carrying are cleaned up here; the // socket takes over for anything still unacknowledged. if let Err(error) = OutboxDispatchService::new(&ctx).purge_expired().await { tracing::warn!(%error, "could not purge finished outbox dispatches"); } }); } ApiRuntime::set_background(ctx, in_background).await } /// Transcodes a captured video while the user is still editing it, so the /// send itself only has to encrypt and hand over. pub async fn prerender_media(media_id: String) -> Result<()> { let ctx = Context::get_static()?; MediaUploadService::new(ctx).prerender(&media_id).await } /// Hands every queued envelope to an OS-owned transfer. Used when the app /// is being torn down while messages are still unsent. pub async fn hand_outbox_to_os() -> Result<()> { let ctx = Context::get_static()?; OutboxDispatchService::new(ctx).hand_pending_to_os().await?; Ok(()) } pub async fn set_network_available(available: bool) -> Result<()> { let ctx = Context::get_static()?; ApiRuntime::set_network_available(ctx, available).await } pub async fn send_binary(bytes: Vec) -> Result<()> { let ctx = Context::get_static()?; ApiRuntime::send_binary(ctx, bytes).await } /// Sends a protobuf ClientToServer envelope. Rust assigns the sequence ID /// and resolves this future with the matching ServerToClient response. pub async fn request_binary(bytes: Vec) -> Result> { let ctx = Context::get_static()?; ApiRuntime::request_binary(ctx, bytes).await } pub async fn allocate_sequence() -> Result { let ctx = Context::get_static()?; ApiRuntime::allocate_sequence(ctx).await } pub async fn events(sink: StreamSink) -> Result<()> { let ctx = Context::get_static()?; ApiRuntime::events(ctx, sink).await } pub async fn register( username: String, proof_of_work: i64, lang_code: String, is_ios: bool, ) -> Result { let ctx = Context::get_static()?; Server::register(ctx, username, proof_of_work, lang_code, is_ios) .await .and_then(api_result) } pub async fn get_user_by_id(user_id: i64) -> Result { let ctx = Context::get_static()?; let res = Server::get_user_by_id(ctx, user_id) .await .and_then(api_result)?; Ok(FrbUserData { user_id: res.user_id, username: res.username.unwrap_or_default(), public_identity_key: res.public_identity_key.unwrap_or_default(), }) } pub async fn check_for_deleted_usernames() -> Result<()> { let ctx = Context::get_static()?; Server::check_for_deleted_usernames(ctx).await } pub async fn get_user_id_from_username(username: String) -> Result { let ctx = Context::get_static()?; Server::get_user_id_from_username(ctx, username) .await .and_then(api_result) } pub async fn get_user_data(username: String) -> Result { let ctx = Context::get_static()?; let res = Server::get_user_by_username(ctx, username) .await .and_then(api_result)?; Ok(FrbUserData { user_id: res.user_id, username: res.username.unwrap_or_default(), public_identity_key: res.public_identity_key.unwrap_or_default(), }) } pub async fn get_proof_of_work() -> Result { let ctx = Context::get_static()?; let res = Server::get_proof_of_work(ctx).await.and_then(api_result)?; Ok(FrbProofOfWork { prefix: res.prefix, difficulty: res.difficulty, }) } pub async fn download_done(token: Vec) -> Result<()> { let ctx = Context::get_static()?; Server::download_done(ctx, token).await.and_then(api_result) } pub async fn download_media(media_id: String) -> Result<()> { let ctx = Context::get_static()?; crate::services::mediafiles::MediaFileService::new(ctx) .download_when_available(&media_id) .await } pub async fn download_pending_media() -> Result<()> { let ctx = Context::get_static()?; crate::services::mediafiles::MediaFileService::new(ctx) .download_pending() .await } pub async fn request_media_reupload(media_id: String) -> Result<()> { let ctx = Context::get_static()?; crate::services::mediafiles::MediaFileService::new(ctx) .request_reupload(&media_id) .await } pub async fn retry_pending_media_reuploads() -> Result<()> { let ctx = Context::get_static()?; crate::services::mediafiles::MediaFileService::new(ctx) .retry_pending_reuploads() .await } pub async fn set_login_token(token: Vec) -> Result<()> { let ctx = Context::get_static()?; Server::set_login_token(ctx, token) .await .and_then(api_result) } pub async fn request_memories_upload( size: i64, original_date: i64, media_id: String, ) -> Result { let ctx = Context::get_static()?; let res = Server::request_memories_upload(ctx, size, original_date, media_id) .await .and_then(api_result)?; Ok(FrbMemoriesUploadUrls { media_id: res.media_id, thumbnail_upload: res.thumbnail_upload.map(|p| FrbPresignedPost { url: p.url, fields: p.fields.into_iter().collect(), }), full_upload: res.full_upload.map(|p| FrbPresignedPost { url: p.url, fields: p.fields.into_iter().collect(), }), }) } pub async fn get_memories_usage() -> Result { let ctx = Context::get_static()?; let res = Server::get_memories_usage(ctx).await.and_then(api_result)?; Ok(FrbMemoriesUsage { current_bytes: res.current_bytes, count: res.count, max_bytes: res.max_bytes, }) } pub async fn get_memories_url(media_id: String, thumbnail: bool) -> Result { let ctx = Context::get_static()?; let res = Server::get_memories_url(ctx, media_id, thumbnail) .await .and_then(api_result)?; Ok(res.full_download_url) } pub async fn confirm_memories_upload(media_id: String) -> Result<()> { let ctx = Context::get_static()?; Server::confirm_memories_upload(ctx, media_id) .await .and_then(api_result) } pub async fn delete_memory(media_id: String) -> Result<()> { let ctx = Context::get_static()?; Server::delete_memory(ctx, media_id) .await .and_then(api_result) } pub async fn disable_memories_backup() -> Result<()> { let ctx = Context::get_static()?; Server::disable_memories_backup(ctx) .await .and_then(api_result) } pub async fn get_plan_balance() -> Result { let ctx = Context::get_static()?; let res = Server::get_plan_balance_model(ctx) .await .and_then(api_result)?; Ok(FrbPlanBalance { used_daily_media_upload_limit: res.used_daily_media_upload_limit, used_upload_media_size_limit: res.used_upload_media_size_limit, payment_period_days: res.payment_period_days, last_payment_done_unix_timestamp: res.last_payment_done_unix_timestamp, additional_accounts: res .additional_accounts .into_iter() .map(|a| FrbAdditionalAccount { user_id: a.user_id, plan_id: a.plan_id, }) .collect(), auto_renewal: res.auto_renewal, additional_account_owner_id: res.additional_account_owner_id, }) } pub async fn load_plan_balance() -> Result { Self::get_plan_balance().await } pub async fn remove_additional_user(user_id: i64) -> Result<()> { let ctx = Context::get_static()?; Server::remove_additional_user(ctx, user_id) .await .and_then(api_result) } pub async fn add_additional_user(user_id: i64) -> Result<()> { let ctx = Context::get_static()?; Server::add_additional_user(ctx, user_id) .await .and_then(api_result) } pub async fn register_passwordless_recovery( encrypted_server_key: Vec, pin_unlock_token: Option>, ) -> Result<()> { let ctx = Context::get_static()?; Server::register_passwordless_recovery(ctx, encrypted_server_key, pin_unlock_token) .await .and_then(api_result) } pub async fn perform_passwordless_recovery_heartbeat() -> Result<()> { let ctx = Context::get_static()?; crate::api::messages::incoming::recovery::perform_heartbeat(ctx).await } pub async fn get_server_key_for_passwordless_recovery( user_id: i64, server_key_protection: Vec, pin_unlock_token: Option>, pin_protection_key: Option>, email: Option, ) -> Result> { let ctx = Context::get_static()?; Server::get_server_key_for_passwordless_recovery( ctx, user_id, server_key_protection, pin_unlock_token, pin_protection_key, email, ) .await .and_then(api_result) } pub async fn submit_recovery_share( notification_id: String, encrypted_message: Vec, ) -> Result<()> { let ctx = Context::get_static()?; Server::submit_recovery_share(ctx, notification_id, encrypted_message) .await .and_then(api_result) } pub async fn register_passwordless_notification( notification_id: String, download_auth_token: Vec, lang_code: String, google_fcm: Option, ) -> Result<()> { let ctx = Context::get_static()?; Server::register_passwordless_notification( ctx, notification_id, download_auth_token, lang_code, google_fcm, ) .await .and_then(api_result) } pub async fn check_for_passwordless_notification( notification_id: String, download_auth_token: Vec, already_received_message_ids: Vec, ) -> Result> { let ctx = Context::get_static()?; let res = Server::check_for_passwordless_notification( ctx, notification_id, download_auth_token, already_received_message_ids, ) .await .and_then(api_result)?; Ok(res .messages .into_iter() .map(|msg| FrbPasswordlessNotificationMessage { id: msg.id, encrypted_message: msg.encrypted_message, }) .collect()) } pub async fn report_user(user_id: i64, reason: String) -> Result<()> { let ctx = Context::get_static()?; Server::report_user(ctx, user_id, reason) .await .and_then(api_result) } pub async fn delete_account() -> Result<()> { let ctx = Context::get_static()?; Server::delete_account(ctx).await.and_then(api_result) } pub async fn update_fcm_token(token: String) -> Result<()> { let ctx = Context::get_static()?; Server::update_fcm_token(ctx, token) .await .and_then(api_result) } pub async fn ipa_purchase( product_id: String, source: String, verification_data: String, ) -> Result<()> { let ctx = Context::get_static()?; empty_api_response(Server::ipa_purchase(ctx, product_id, source, verification_data).await?) } pub async fn change_username(username: String) -> Result<()> { let ctx = Context::get_static()?; Server::change_username(ctx, username) .await .and_then(api_result) } pub async fn force_ipa_check() -> Result<()> { let ctx = Context::get_static()?; Server::force_ipa_check(ctx).await.and_then(api_result) } pub async fn update_signed_pre_key(id: i64, key: Vec, signature: Vec) -> Result<()> { let ctx = Context::get_static()?; empty_api_response(Server::update_signed_pre_key(ctx, id, key, signature).await?) } #[allow(clippy::too_many_arguments)] pub async fn upload_pqc_pre_keys( public_identity_key: Vec, registration_id: i64, ecc_signed_prekey_id: i64, ecc_signed_prekey: Vec, ecc_signed_prekey_signature: Vec, kyber_signed_prekey_id: i64, kyber_signed_prekey: Vec, kyber_signed_prekey_signature: Vec, prekeys: Vec, ) -> Result<()> { let ctx = Context::get_static()?; Server::upload_pqc_pre_keys( ctx, public_identity_key, registration_id, ecc_signed_prekey_id, ecc_signed_prekey, ecc_signed_prekey_signature, kyber_signed_prekey_id, kyber_signed_prekey, kyber_signed_prekey_signature, prekeys, ) .await .and_then(empty_api_response) } pub async fn send_text_message(user_id: i64, body: Vec) -> Result<()> { let ctx = Context::get_static()?; Server::send_text_message(ctx, user_id, body, false) .await .and_then(api_result) } pub async fn send_encrypted_content( contact_id: i64, content: Vec, message_id: Option, only_send_if_no_receipts_are_open: Option, only_return_encrypted_data: Option, blocking: Option, ) -> Result>> { let ctx = Context::get_static()?; crate::api::messages::outgoing::send_c2c_message_to_contact() .ctx(ctx) .contact_id(contact_id) .encrypted_content(content) .maybe_message_id(message_id) .only_send_if_no_receipts_are_open(only_send_if_no_receipts_are_open.unwrap_or(false)) .only_return_encrypted_data(only_return_encrypted_data.unwrap_or(false)) .blocking(blocking.unwrap_or(false)) .call() .await .map_err(Into::into) } pub async fn send_encrypted_content_to_group( group_id: String, content: Vec, message_id: Option, only_send_if_no_receipts_are_open: Option, ) -> Result<()> { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .send_to_group( group_id, content, message_id, only_send_if_no_receipts_are_open.unwrap_or(false), ) .await } pub async fn insert_and_send_text( group_id: String, text: String, quote_message_id: Option, additional_message_data: Option>, ) -> Result { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .insert_and_send_text(group_id, text, quote_message_id, additional_message_data) .await } pub async fn insert_and_send_additional_data( group_id: String, message_type: String, additional_data: Vec, hidden: bool, ) -> Result { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .insert_and_send_additional_data(group_id, message_type, additional_data, hidden) .await } pub async fn insert_and_send_contact_share( group_id: String, contact_ids: Vec, ) -> Result { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .insert_and_send_contact_share(group_id, contact_ids) .await } pub async fn insert_and_send_ask_about_user( contact_id: i64, ask_about_user_id: i64, ) -> Result { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .insert_and_send_ask_about_user(contact_id, ask_about_user_id) .await } pub async fn send_typing(group_id: String, is_typing: bool) -> Result<()> { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .send_typing(group_id, is_typing) .await } pub async fn retransmit_all_messages() -> Result<()> { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .retransmit_all() .await } pub async fn send_queued_message(receipt_id: String) -> Result<()> { let ctx = Context::get_static()?; crate::services::messages::MessageService::new(ctx) .send_receipt(receipt_id) .await } pub async fn notify_messages_opened(contact_id: i64, message_ids: Vec) -> Result<()> { let ctx = Context::get_static()?; MessageService::new(ctx) .notify_opened(contact_id, message_ids) .await } /// The number of pending notification events. iOS cannot derive its app /// icon badge from the delivered alerts, so the running app pushes this /// into `UNUserNotificationCenter` whenever the outbox changes. pub async fn notification_badge_count() -> Result { crate::services::notifications::badge_count(Context::get_static()?).await } /// Acknowledges every pending notification of a conversation the user just /// opened and returns the native notification IDs to withdraw. pub async fn clear_conversation_notifications(conversation_id: String) -> Result> { crate::services::notifications::clear_conversation(Context::get_static()?, &conversation_id) .await } /// Acknowledges the pending contact-request notifications and returns the /// native notification IDs to withdraw. pub async fn clear_contact_request_notifications() -> Result> { crate::services::notifications::clear_contact_requests(Context::get_static()?).await } pub async fn send_contact_profile(contact_id: i64) -> Result<()> { let ctx = Context::get_static()?; ContactService::new(ctx).send_profile(contact_id).await } pub async fn establish_signal_session( contact_id: i64, expected_public_key: Option>, ) -> Result<()> { let ctx = Context::get_static()?; ContactService::new(ctx) .establish_signal_session(contact_id, expected_public_key) .await } }