twonly-app/rust/src/bridge/api.rs
otsmr 351d82c6d7
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
allow to store the location in memories
2026-09-11 18:02:09 +02:00

1035 lines
37 KiB
Rust

/*
* 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::user_config::UserConfig;
use flutter_rust_bridge::frb;
use std::collections::HashMap;
#[frb(ignore)]
pub enum ServerResult<T> {
Ok(T),
ErrorCode(i32),
}
fn api_result<T>(result: ServerResult<T>) -> Result<T> {
match result {
ServerResult::Ok(value) => Ok(value),
ServerResult::ErrorCode(code) => Err(TwonlyError::Api(code)),
}
}
fn empty_api_response(bytes: Vec<u8>) -> 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<Self> {
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<ApiConnectionState>,
pub message: Option<String>,
}
#[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<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FrbUserData {
pub user_id: i64,
pub username: Vec<u8>,
pub public_identity_key: Vec<u8>,
}
#[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<i64>,
pub last_payment_done_unix_timestamp: Option<i64>,
pub additional_accounts: Vec<FrbAdditionalAccount>,
pub auto_renewal: Option<bool>,
pub additional_account_owner_id: Option<i64>,
}
#[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<FrbPresignedPost>,
pub full_upload: Option<FrbPresignedPost>,
}
/// 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<u8>) -> String {
avatars::decode_avatar_svg(avatar_svg_compressed)
}
#[frb(sync)]
pub fn avatar_png_path(contact_id: i64, profile_counter: i64) -> Result<String> {
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<Option<String>> {
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<Option<String>> {
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<i64>,
is_draft_media: bool,
) -> Result<String> {
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 and starts the upload.
pub async fn send_media_to_groups(
media_id: String,
group_ids: Vec<String>,
additional_message_data: Option<Vec<u8>>,
widget_only: bool,
) -> Result<()> {
let ctx = Context::get_static()?;
MediaUploadService::new(ctx)
.insert_into_messages(media_id, group_ids, additional_message_data, widget_only)
.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<i64>,
) -> 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<MediaSizeReport> {
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<i64>,
) -> 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<i64>,
trim_end_ms: Option<i64>,
) -> 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<u8>) -> 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<HashMap<String, String>> {
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<ApiConnectionState> {
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<u8>) -> 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<u8>) -> Result<Vec<u8>> {
let ctx = Context::get_static()?;
ApiRuntime::request_binary(ctx, bytes).await
}
pub async fn allocate_sequence() -> Result<u64> {
let ctx = Context::get_static()?;
ApiRuntime::allocate_sequence(ctx).await
}
pub async fn events(sink: StreamSink<ApiEvent>) -> 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<i64> {
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<FrbUserData> {
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<i64> {
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<FrbUserData> {
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<FrbProofOfWork> {
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<u8>) -> 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<u8>) -> 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<FrbMemoriesUploadUrls> {
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<FrbMemoriesUsage> {
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<String> {
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<FrbPlanBalance> {
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<FrbPlanBalance> {
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<u8>,
pin_unlock_token: Option<Vec<u8>>,
) -> 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<u8>,
pin_unlock_token: Option<Vec<u8>>,
pin_protection_key: Option<Vec<u8>>,
email: Option<String>,
) -> Result<Vec<u8>> {
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<u8>,
) -> 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<u8>,
lang_code: String,
google_fcm: Option<String>,
) -> 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<u8>,
already_received_message_ids: Vec<i64>,
) -> Result<Vec<FrbPasswordlessNotificationMessage>> {
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<u8>, signature: Vec<u8>) -> 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<u8>,
registration_id: i64,
ecc_signed_prekey_id: i64,
ecc_signed_prekey: Vec<u8>,
ecc_signed_prekey_signature: Vec<u8>,
kyber_signed_prekey_id: i64,
kyber_signed_prekey: Vec<u8>,
kyber_signed_prekey_signature: Vec<u8>,
prekeys: Vec<PqcPreKeyInput>,
) -> 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<u8>) -> 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<u8>,
message_id: Option<String>,
only_send_if_no_receipts_are_open: Option<bool>,
only_return_encrypted_data: Option<bool>,
blocking: Option<bool>,
) -> Result<Option<Vec<u8>>> {
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<u8>,
message_id: Option<String>,
only_send_if_no_receipts_are_open: Option<bool>,
) -> 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<String>,
additional_message_data: Option<Vec<u8>>,
) -> Result<String> {
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<u8>,
hidden: bool,
) -> Result<String> {
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<i64>,
) -> Result<String> {
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<String> {
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<String>) -> 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<i64> {
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<Vec<String>> {
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<Vec<String>> {
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<Vec<u8>>,
) -> Result<()> {
let ctx = Context::get_static()?;
ContactService::new(ctx)
.establish_signal_session(contact_id, expected_public_key)
.await
}
}