[Add] Client settings

This commit is contained in:
Alex Emmet 2026-09-01 22:17:57 +02:00
commit 430c12e139
No known key found for this signature in database
22 changed files with 2779 additions and 127 deletions

View file

@ -5,12 +5,21 @@ use iota_storage::util::communities_util::CommunitiesUtil;
use iota_storage::util::e2ee_storage::{self, ChatSecretQuery};
use iota_storage::util::settings;
use iota_storage::util::synced_settings::{self, SettingScope, SyncedSetting};
use iota_storage::util::user_blobs::{self, UserBlob};
use iota_storage::util::{blocked_users, message_storage_policy, receipt_policy};
use mtp::codec::{
CommunicationType, CommunicationValue, DataType, DataValue, TypeMap, VerifiedRelayContent,
};
use crate::relay::VerifiedRelayContext;
use iota_storage::storage_error::StorageError;
use std::sync::atomic::{AtomicU32, Ordering};
static NEXT_NOTIFICATION_ID: AtomicU32 = AtomicU32::new(1);
fn next_notification_id() -> u32 {
NEXT_NOTIFICATION_ID.fetch_add(1, Ordering::Relaxed).max(1)
}
#[derive(Debug)]
pub struct MessageMutation {
@ -25,6 +34,18 @@ pub struct SettingMutation {
pub changed: Option<CommunicationValue>,
}
#[derive(Debug)]
pub struct BlobMutation {
pub response: CommunicationValue,
pub changed: Option<CommunicationValue>,
}
#[derive(Debug)]
pub struct PolicyMutation {
pub response: CommunicationValue,
pub changed: Option<CommunicationValue>,
}
struct SettingLocator {
scope: SettingScope,
scope_key: String,
@ -174,6 +195,15 @@ pub fn apply_verified_relay_content(
.map(MessageState::from_str)
.filter(|state| matches!(state, MessageState::Received | MessageState::Read))
.ok_or_else(|| "Relay MessageState has an invalid state".to_string())?;
if sent_by_self {
return chat_files::change_message_state_by_relay_id(
storage_owner,
recipient_id,
relay_message_id,
state,
)
.map_err(|error| error.to_string());
}
chat_files::record_message_receipt(
storage_owner,
recipient_id,
@ -323,6 +353,32 @@ pub fn apply_verified_relay_content(
}
}
/* Local receipt disclosure is checked before the relay enters durable state. */
pub fn validate_outgoing_receipt_policy(
sender_id: i64,
context: &VerifiedRelayContext,
content: &VerifiedRelayContent,
) -> Result<(), String> {
if content.message_type != "MessageState" {
return Ok(());
}
let state = relay_string(&content.content, DataType::MessageState, &context.type_map)
.map(MessageState::from_str)
.filter(|state| matches!(state, MessageState::Received | MessageState::Read))
.ok_or_else(|| "local MessageState has an invalid state".to_string())?;
let policy = receipt_policy::get(sender_id).map_err(|error| error.to_string())?;
let allowed = match state {
MessageState::Read => policy.send_read_receipts,
MessageState::Received => policy.send_received_receipts,
MessageState::Sent | MessageState::Sending => false,
};
if allowed {
Ok(())
} else {
Err("local receipt disclosure is disabled by policy".into())
}
}
pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue {
let mutation = match message_mutation(cv) {
Ok(mutation) => mutation,
@ -777,9 +833,11 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
Err(_) => return sync_error(cv),
};
let known_session = sync::has_session(user_id, session_id).unwrap_or(false);
let acknowledged_version = sync::acknowledged_version(user_id, session_id).unwrap_or(None);
let full = !cache_valid
|| reported_version == 0
|| !known_session
|| acknowledged_version.is_some_and(|version| reported_version < version)
|| reported_version > head
|| schema != CACHE_SCHEMA_VERSION;
let (contacts, messages, settings, deleted_messages, deleted_contacts, deleted_settings, mode) =
@ -845,11 +903,59 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
.iter()
.map(|message| stored_message_value(message, user_id, message.external_user))
.collect();
if iota_storage::util::client_message_delivery::record_sync_delivery(
user_id,
session_id,
head,
messages.iter().map(|message| message.id),
)
.is_err()
{
return sync_error(cv);
}
let (blobs, deleted_blobs) = if mode == "delta" {
match sync::delta(user_id, reported_version, head) {
Ok(delta) => {
let blobs = match user_blobs::list_by_ids(user_id, &delta.blob_upserts) {
Ok(blobs) => blobs,
Err(_) => return sync_error(cv),
};
let deleted =
match user_blobs::list_deleted_by_ids(user_id, &delta.deleted_blob_ids) {
Ok(blobs) => blobs,
Err(_) => return sync_error(cv),
};
(blobs, deleted)
}
Err(_) => return sync_error(cv),
}
} else {
match user_blobs::list(user_id) {
Ok(blobs) => (blobs, Vec::new()),
Err(_) => return sync_error(cv),
}
};
let blocked_users = match blocked_users::list(user_id) {
Ok(users) => users,
Err(_) => return sync_error(cv),
};
let receipt_policy = match receipt_policy::get(user_id) {
Ok(policy) => policy,
Err(_) => return sync_error(cv),
};
let message_storage_policy = match message_storage_policy::get(user_id) {
Ok(policy) => policy,
Err(_) => return sync_error(cv),
};
let contact_ids = match current_contact_ids(user_id) {
Ok(contact_ids) => contact_ids,
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
};
CommunicationValue::new(CommunicationType::ClientStateSync)
let retention_duration = match message_storage_policy.retention {
message_storage_policy::MessageRetention::Forever => None,
message_storage_policy::MessageRetention::Duration { duration_ms } => Some(duration_ms),
};
let mut response = CommunicationValue::new(CommunicationType::ClientStateSync)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
@ -879,6 +985,45 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
DataType::Settings,
DataValue::Array(settings.iter().map(synced_setting_value).collect()),
)
.add_typed_default(
DataType::Blobs,
DataValue::Array(blobs.iter().map(blob_value).collect()),
)
.add_typed_default(
DataType::DeletedBlobIds,
DataValue::Array(
deleted_blobs
.iter()
.map(|blob| DataValue::Str(blob.blob_id.clone()))
.collect(),
),
)
.add_typed_default(
DataType::BlockedUserIds,
DataValue::Array(
blocked_users
.into_iter()
.map(|id| DataValue::SignedNumber(id.into()))
.collect(),
),
)
.add_typed_default(
DataType::SendReadReceipts,
DataValue::Bool(receipt_policy.send_read_receipts),
)
.add_typed_default(
DataType::SendReceivedReceipts,
DataValue::Bool(receipt_policy.send_received_receipts),
)
.add_typed_default(
DataType::MessageHistoryMode,
DataValue::Str(match message_storage_policy.history_mode {
message_storage_policy::MessageHistoryMode::Retain => "retain".into(),
message_storage_policy::MessageHistoryMode::DeleteAfterClientDelivery => {
"delete_after_client_delivery".into()
}
}),
)
.add_typed_default(
DataType::Communities,
DataValue::Array(community_values(user_id)),
@ -911,11 +1056,18 @@ pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
),
)
.add_typed_default(DataType::UserIds, contact_ids)
.add_typed_default(DataType::Calls, DataValue::Array(Vec::new()))
.add_typed_default(DataType::Calls, DataValue::Array(Vec::new()));
if let Some(duration_ms) = retention_duration {
response = response.add_typed_default(
DataType::MessageRetentionDuration,
DataValue::SignedNumber(duration_ms.into()),
);
}
response
}
pub fn handle_client_state_ack(cv: &CommunicationValue) -> CommunicationValue {
use iota_storage::util::sync::{self, CACHE_SCHEMA_VERSION};
use iota_storage::util::sync::CACHE_SCHEMA_VERSION;
let user_id = match required_sender_id(cv) {
Ok(id) if id > 0 => id,
_ => return sync_error(cv),
@ -928,7 +1080,14 @@ pub fn handle_client_state_ack(cv: &CommunicationValue) -> CommunicationValue {
Some(version) if version >= 0 => version,
_ => return sync_error(cv),
};
if sync::acknowledge(user_id, session_id, version, CACHE_SCHEMA_VERSION).is_err() {
if iota_storage::util::client_message_delivery::acknowledge_client_state(
user_id,
session_id,
version,
CACHE_SCHEMA_VERSION,
)
.is_err()
{
return sync_error(cv);
}
success_response(cv)
@ -1483,6 +1642,66 @@ fn setting_response(
)
}
fn blob_value(blob: &UserBlob) -> DataValue {
typed_container(vec![
(DataType::BlobId, DataValue::Str(blob.blob_id.clone())),
(DataType::Blob, DataValue::Bytes(blob.blob.clone())),
(
DataType::VersionNumber,
DataValue::SignedNumber(blob.revision.into()),
),
(
DataType::UpdatedAt,
DataValue::SignedNumber(blob.updated_at.into()),
),
])
}
fn blob_response(
cv: &CommunicationValue,
ty: CommunicationType,
blob: &UserBlob,
) -> CommunicationValue {
CommunicationValue::new(ty)
.with_request_id(cv)
.with_receiver(sender_wire_id(blob.user_id))
.add_typed_default(DataType::BlobId, DataValue::Str(blob.blob_id.clone()))
.add_typed_default(DataType::Blob, DataValue::Bytes(blob.blob.clone()))
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(blob.revision.into()),
)
.add_typed_default(
DataType::UpdatedAt,
DataValue::SignedNumber(blob.updated_at.into()),
)
}
fn blob_changed(user_id: i64, blob_id: String, revision: i64, deleted: bool) -> CommunicationValue {
CommunicationValue::new(CommunicationType::UserBlobChanged)
.with_id(next_notification_id())
.with_receiver(sender_wire_id(user_id))
.add_typed_default(DataType::BlobId, DataValue::Str(blob_id))
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(revision.into()),
)
.add_typed_default(DataType::Deleted, DataValue::Bool(deleted))
}
fn blob_request(cv: &CommunicationValue) -> Result<(i64, String), CommunicationValue> {
let user_id = required_sender_id(cv)?;
if user_id <= 0 {
return Err(error_response(cv, CommunicationType::ErrorInvalidData));
}
let id = cv
.get_data(DataType::BlobId)
.and_then(DataValue::as_str)
.filter(|id| !id.is_empty())
.ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?;
Ok((user_id, id.to_owned()))
}
fn setting_changed(setting: &SyncedSetting) -> CommunicationValue {
CommunicationValue::new(CommunicationType::SyncedSettingChanged)
.with_receiver(sender_wire_id(setting.user_id))
@ -1703,6 +1922,410 @@ pub fn handle_synced_settings_list(cv: &CommunicationValue) -> CommunicationValu
}
}
pub fn handle_user_blob_put(cv: &CommunicationValue) -> BlobMutation {
let Ok((user_id, blob_id)) = blob_request(cv) else {
return BlobMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
let Some(blob) = cv.get_data(DataType::Blob).and_then(DataValue::as_bytes) else {
return BlobMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
let expected_revision = data_i64(cv, DataType::ExpectedRevision);
match user_blobs::put(user_id, &blob_id, &blob, expected_revision) {
Ok(stored) => BlobMutation {
response: blob_response(cv, CommunicationType::UserBlobPut, &stored),
changed: Some(blob_changed(
user_id,
stored.blob_id.clone(),
stored.revision,
false,
)),
},
Err(iota_storage::storage_error::StorageError::RevisionConflict) => BlobMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
},
Err(_) => BlobMutation {
response: error_response(cv, CommunicationType::ErrorInternal),
changed: None,
},
}
}
pub fn handle_user_blob_get(cv: &CommunicationValue) -> CommunicationValue {
let Ok((user_id, blob_id)) = blob_request(cv) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match user_blobs::get(user_id, &blob_id) {
Ok(Some(blob)) => blob_response(cv, CommunicationType::UserBlobGet, &blob),
Ok(None) => error_response(cv, CommunicationType::ErrorNotFound),
Err(_) => error_response(cv, CommunicationType::ErrorInternal),
}
}
pub fn handle_user_blob_delete(cv: &CommunicationValue) -> BlobMutation {
let Ok((user_id, blob_id)) = blob_request(cv) else {
return BlobMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
match user_blobs::delete(user_id, &blob_id, data_i64(cv, DataType::ExpectedRevision)) {
Ok(Some(deleted)) => BlobMutation {
response: CommunicationValue::new(CommunicationType::UserBlobDelete)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(DataType::BlobId, DataValue::Str(blob_id.clone()))
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(deleted.revision.into()),
),
changed: deleted
.changed
.then(|| blob_changed(user_id, blob_id, deleted.revision, true)),
},
Ok(None) => BlobMutation {
response: CommunicationValue::new(CommunicationType::UserBlobDelete)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id)),
changed: None,
},
Err(iota_storage::storage_error::StorageError::RevisionConflict) => BlobMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
},
Err(_) => BlobMutation {
response: error_response(cv, CommunicationType::ErrorInternal),
changed: None,
},
}
}
pub fn handle_user_blob_list(cv: &CommunicationValue) -> CommunicationValue {
let Ok(user_id) = required_sender_id(cv) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
if user_id <= 0 {
return error_response(cv, CommunicationType::ErrorInvalidData);
}
match user_blobs::list(user_id) {
Ok(blobs) => CommunicationValue::new(CommunicationType::UserBlobList)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::Blobs,
DataValue::Array(blobs.iter().map(blob_value).collect()),
),
Err(_) => error_response(cv, CommunicationType::ErrorInternal),
}
}
fn authenticated_user(cv: &CommunicationValue) -> Result<i64, CommunicationValue> {
let user_id = required_sender_id(cv)?;
if user_id <= 0 {
return Err(error_response(cv, CommunicationType::ErrorInvalidData));
}
Ok(user_id)
}
pub fn handle_user_block(cv: &CommunicationValue) -> PolicyMutation {
let Ok(user_id) = authenticated_user(cv) else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
let Some(blocked_user_id) = data_i64(cv, DataType::BlockedUserId).filter(|id| *id > 0) else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
match blocked_users::block(user_id, blocked_user_id) {
Ok(record) => PolicyMutation {
response: CommunicationValue::new(CommunicationType::UserBlock)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::BlockedUserId,
DataValue::SignedNumber(blocked_user_id.into()),
)
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(record.revision.into()),
),
changed: Some(
CommunicationValue::new(CommunicationType::BlockedUsersChanged)
.with_id(next_notification_id())
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::BlockedUserId,
DataValue::SignedNumber(blocked_user_id.into()),
)
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(record.revision.into()),
)
.add_typed_default(DataType::Deleted, DataValue::Bool(record.deleted)),
),
},
Err(_) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInternal),
changed: None,
},
}
}
pub fn handle_user_unblock(cv: &CommunicationValue) -> PolicyMutation {
let Ok(user_id) = authenticated_user(cv) else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
let Some(blocked_user_id) = data_i64(cv, DataType::BlockedUserId).filter(|id| *id > 0) else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
match blocked_users::unblock(user_id, blocked_user_id) {
Ok(mutation) => PolicyMutation {
response: CommunicationValue::new(CommunicationType::UserUnblock)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::BlockedUserId,
DataValue::SignedNumber(blocked_user_id.into()),
),
changed: mutation.map(|mutation| {
CommunicationValue::new(CommunicationType::BlockedUsersChanged)
.with_id(next_notification_id())
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::BlockedUserId,
DataValue::SignedNumber(blocked_user_id.into()),
)
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(mutation.revision.into()),
)
.add_typed_default(DataType::Deleted, DataValue::Bool(mutation.deleted))
}),
},
Err(_) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInternal),
changed: None,
},
}
}
pub fn handle_blocked_users_get(cv: &CommunicationValue) -> CommunicationValue {
let Ok(user_id) = authenticated_user(cv) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match blocked_users::list(user_id) {
Ok(users) => CommunicationValue::new(CommunicationType::BlockedUsersGet)
.with_request_id(cv)
.with_receiver(sender_wire_id(user_id))
.add_typed_default(
DataType::BlockedUserIds,
DataValue::Array(
users
.into_iter()
.map(|id| DataValue::SignedNumber(id.into()))
.collect(),
),
),
Err(_) => error_response(cv, CommunicationType::ErrorInternal),
}
}
fn receipt_response(
cv: &CommunicationValue,
ty: CommunicationType,
policy: receipt_policy::ReceiptPolicy,
) -> CommunicationValue {
CommunicationValue::new(ty)
.with_request_id(cv)
.with_receiver(sender_wire_id(policy.user_id))
.add_typed_default(
DataType::SendReadReceipts,
DataValue::Bool(policy.send_read_receipts),
)
.add_typed_default(
DataType::SendReceivedReceipts,
DataValue::Bool(policy.send_received_receipts),
)
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(policy.revision.into()),
)
}
pub fn handle_receipt_policy_get(cv: &CommunicationValue) -> CommunicationValue {
let Ok(user_id) = authenticated_user(cv) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match receipt_policy::get(user_id) {
Ok(policy) => receipt_response(cv, CommunicationType::ReceiptPolicyGet, policy),
Err(_) => error_response(cv, CommunicationType::ErrorInternal),
}
}
pub fn handle_receipt_policy_set(cv: &CommunicationValue) -> PolicyMutation {
let Ok(user_id) = authenticated_user(cv) else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
let (Some(read), Some(received)) = (
cv.get_data(DataType::SendReadReceipts)
.and_then(DataValue::as_bool),
cv.get_data(DataType::SendReceivedReceipts)
.and_then(DataValue::as_bool),
) else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
match receipt_policy::set(user_id, read, received) {
Ok(policy) => {
let response = receipt_response(cv, CommunicationType::ReceiptPolicySet, policy);
let changed = receipt_response(cv, CommunicationType::ReceiptPolicyChanged, policy)
.with_id(next_notification_id());
PolicyMutation {
response,
changed: Some(changed),
}
}
Err(_) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInternal),
changed: None,
},
}
}
fn storage_policy_response(
cv: &CommunicationValue,
ty: CommunicationType,
policy: message_storage_policy::MessageStoragePolicy,
) -> CommunicationValue {
let (history_mode, duration) = match policy.history_mode {
message_storage_policy::MessageHistoryMode::Retain => ("retain", None),
message_storage_policy::MessageHistoryMode::DeleteAfterClientDelivery => {
("delete_after_client_delivery", None)
}
};
let retention_duration = match policy.retention {
message_storage_policy::MessageRetention::Forever => None,
message_storage_policy::MessageRetention::Duration { duration_ms } => Some(duration_ms),
};
let mut response = CommunicationValue::new(ty)
.with_request_id(cv)
.with_receiver(sender_wire_id(policy.user_id))
.add_typed_default(
DataType::MessageHistoryMode,
DataValue::Str(history_mode.into()),
)
.add_typed_default(
DataType::VersionNumber,
DataValue::SignedNumber(policy.revision.into()),
);
if let Some(duration_ms) = retention_duration.or(duration) {
response = response.add_typed_default(
DataType::MessageRetentionDuration,
DataValue::SignedNumber(duration_ms.into()),
);
}
response
}
pub fn handle_message_storage_policy_get(cv: &CommunicationValue) -> CommunicationValue {
let Ok(user_id) = authenticated_user(cv) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match message_storage_policy::get(user_id) {
Ok(policy) => {
storage_policy_response(cv, CommunicationType::MessageStoragePolicyGet, policy)
}
Err(_) => error_response(cv, CommunicationType::ErrorInternal),
}
}
pub fn handle_message_storage_policy_set(cv: &CommunicationValue) -> PolicyMutation {
let Ok(user_id) = authenticated_user(cv) else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
let Some(history_mode) = cv
.get_data(DataType::MessageHistoryMode)
.and_then(DataValue::as_str)
else {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
};
let history_mode = match history_mode {
"retain" => message_storage_policy::MessageHistoryMode::Retain,
"delete_after_client_delivery" => {
message_storage_policy::MessageHistoryMode::DeleteAfterClientDelivery
}
_ => {
return PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
};
}
};
let retention = match data_i64(cv, DataType::MessageRetentionDuration) {
Some(duration_ms) => message_storage_policy::MessageRetention::Duration { duration_ms },
None => message_storage_policy::MessageRetention::Forever,
};
match message_storage_policy::set(user_id, history_mode, retention) {
Ok(policy) => {
let response =
storage_policy_response(cv, CommunicationType::MessageStoragePolicySet, policy);
let changed =
storage_policy_response(cv, CommunicationType::MessageStoragePolicyChanged, policy)
.with_id(next_notification_id());
PolicyMutation {
response,
changed: Some(changed),
}
}
Err(_) => PolicyMutation {
response: error_response(cv, CommunicationType::ErrorInvalidData),
changed: None,
},
}
}
/* This request is issued over Omikron's trusted Iota connection. Clients have
* no route to it, preventing arbitrary block-relationship disclosure. */
pub fn handle_user_block_check(cv: &CommunicationValue) -> CommunicationValue {
let Some(sender_id) = data_i64(cv, DataType::SenderId).filter(|id| *id > 0) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
let Some(receiver_id) = data_i64(cv, DataType::ReceiverId).filter(|id| *id > 0) else {
return error_response(cv, CommunicationType::ErrorInvalidData);
};
match blocked_users::is_blocked(receiver_id, sender_id) {
Ok(blocked) => CommunicationValue::new(CommunicationType::UserBlockCheck)
.with_request_id(cv)
.add_typed_default(DataType::IsBlocked, DataValue::Bool(blocked)),
Err(_) => error_response(cv, CommunicationType::ErrorInternal),
}
}
#[cfg(test)]
mod synced_settings_tests {
use super::{handle_synced_setting_get, handle_synced_setting_set, parse_setting_locator};