1774 lines
65 KiB
Rust
1774 lines
65 KiB
Rust
use crate::message_common::*;
|
|
use iota_storage::util::chat_files::{self, MessageState};
|
|
use iota_storage::util::chats_util::{self, get_user, has_user, mod_user};
|
|
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 mtp::codec::{
|
|
CommunicationType, CommunicationValue, DataType, DataValue, TypeMap, VerifiedRelayContent,
|
|
};
|
|
|
|
use crate::relay::VerifiedRelayContext;
|
|
use iota_storage::storage_error::StorageError;
|
|
|
|
#[derive(Debug)]
|
|
pub struct MessageMutation {
|
|
pub sender_id: i64,
|
|
pub partner_id: i64,
|
|
pub send_time: i64,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SettingMutation {
|
|
pub response: CommunicationValue,
|
|
pub changed: Option<CommunicationValue>,
|
|
}
|
|
|
|
struct SettingLocator {
|
|
scope: SettingScope,
|
|
scope_key: String,
|
|
name: String,
|
|
}
|
|
|
|
fn required_sender_id(cv: &CommunicationValue) -> Result<i64, CommunicationValue> {
|
|
let sender = cv
|
|
.require_sender()
|
|
.map_err(|_| error_response(cv, CommunicationType::ErrorInvalidData))?;
|
|
i64::try_from(sender).map_err(|_| error_response(cv, CommunicationType::ErrorInvalidData))
|
|
}
|
|
|
|
fn sender_wire_id(sender_id: i64) -> u64 {
|
|
u64::try_from(sender_id).expect("validated authenticated sender is non-negative")
|
|
}
|
|
|
|
pub fn message_mutation(cv: &CommunicationValue) -> Result<MessageMutation, CommunicationValue> {
|
|
let sender_id = required_sender_id(cv)?;
|
|
let partner_id = data_i64(cv, DataType::ChatPartnerId)
|
|
.filter(|id| *id > 0)
|
|
.ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?;
|
|
let send_time = data_i64(cv, DataType::SendTime)
|
|
.filter(|time| *time > 0)
|
|
.ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?;
|
|
|
|
Ok(MessageMutation {
|
|
sender_id,
|
|
partner_id,
|
|
send_time,
|
|
})
|
|
}
|
|
|
|
pub fn success_response(cv: &CommunicationValue) -> CommunicationValue {
|
|
error_response(cv, CommunicationType::Success)
|
|
}
|
|
|
|
fn add_conversation_for_user(
|
|
user_id: i64,
|
|
other_id: i64,
|
|
name: Option<&str>,
|
|
) -> Result<(), StorageError> {
|
|
let mut contact = get_user(user_id, other_id)?
|
|
.unwrap_or_else(|| iota_storage::users::contact::Contact::new(other_id));
|
|
if let Some(name) = name {
|
|
contact.user_name = Some(name.to_string());
|
|
}
|
|
contact.set_last_message_at(now_millis_i64());
|
|
mod_user(user_id, &contact)
|
|
}
|
|
|
|
fn relay_field<'a>(
|
|
payload: &'a DataValue,
|
|
data_type: DataType,
|
|
type_map: &TypeMap,
|
|
) -> Option<&'a DataValue> {
|
|
payload.get_field(data_type.try_to_id(type_map)?)
|
|
}
|
|
|
|
fn relay_string<'a>(
|
|
payload: &'a DataValue,
|
|
data_type: DataType,
|
|
type_map: &TypeMap,
|
|
) -> Option<&'a str> {
|
|
relay_field(payload, data_type, type_map)?.as_str()
|
|
}
|
|
|
|
fn relay_number(payload: &DataValue, data_type: DataType, type_map: &TypeMap) -> Option<i128> {
|
|
relay_field(payload, data_type, type_map)?.as_number()
|
|
}
|
|
|
|
fn relay_identity(
|
|
payload: &DataValue,
|
|
data_type: DataType,
|
|
type_map: &TypeMap,
|
|
) -> Result<Option<u64>, String> {
|
|
let Some(value) = relay_field(payload, data_type, type_map) else {
|
|
return Ok(None);
|
|
};
|
|
if let Some(number) = value.as_number() {
|
|
return u64::try_from(number)
|
|
.map(Some)
|
|
.map_err(|_| format!("Relay {data_type:?} is outside the user ID range"));
|
|
}
|
|
if let Some(text) = value.as_str() {
|
|
return text
|
|
.parse::<u64>()
|
|
.map(Some)
|
|
.map_err(|_| format!("Relay {data_type:?} is not a user ID"));
|
|
}
|
|
Err(format!("Relay {data_type:?} has an invalid user ID value"))
|
|
}
|
|
|
|
fn validate_relay_identity(
|
|
context: &VerifiedRelayContext,
|
|
payload: &DataValue,
|
|
) -> Result<(), String> {
|
|
if relay_identity(payload, DataType::SenderId, &context.type_map)?
|
|
.is_some_and(|sender_id| sender_id != context.signer_id)
|
|
{
|
|
return Err("Relay SenderId does not match the authenticated signer".into());
|
|
}
|
|
if relay_identity(payload, DataType::ReceiverId, &context.type_map)?
|
|
.is_some_and(|receiver_id| receiver_id != context.final_recipient_id)
|
|
{
|
|
return Err("Relay ReceiverId does not match the authenticated recipient".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/*
|
|
* Apply only operations whose actor and recipient can be taken from verified
|
|
* Relay metadata. The raw Relay frame never enters these handlers, so outer
|
|
* routing fields cannot become application identity.
|
|
*/
|
|
pub fn apply_verified_relay_content(
|
|
context: &VerifiedRelayContext,
|
|
content: &VerifiedRelayContent,
|
|
accepted_at: i64,
|
|
storage_owner: i64,
|
|
sent_by_self: bool,
|
|
) -> Result<(), String> {
|
|
validate_relay_identity(context, &content.content)?;
|
|
let sender_id = i64::try_from(context.signer_id)
|
|
.map_err(|_| "Relay signer ID exceeds the local storage range".to_string())?;
|
|
let recipient_id = i64::try_from(context.final_recipient_id)
|
|
.map_err(|_| "Relay recipient ID exceeds the local storage range".to_string())?;
|
|
let created_at = i64::try_from(context.created_at)
|
|
.map_err(|_| "Relay creation time exceeds the local storage range".to_string())?;
|
|
|
|
match content.message_type.as_str() {
|
|
"MessageState" => {
|
|
let partner_id =
|
|
relay_number(&content.content, DataType::ChatPartnerId, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.filter(|id| *id == recipient_id)
|
|
.ok_or_else(|| "Relay MessageState has an invalid ChatPartnerId".to_string())?;
|
|
let relay_message_id = relay_string(
|
|
&content.content,
|
|
DataType::RelayMessageId,
|
|
&context.type_map,
|
|
)
|
|
.ok_or_else(|| "Relay MessageState is missing RelayMessageId".to_string())?;
|
|
let event_at = relay_number(&content.content, DataType::EventAt, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.ok_or_else(|| "Relay MessageState is missing EventAt".to_string())?;
|
|
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(|| "Relay MessageState has an invalid state".to_string())?;
|
|
chat_files::record_message_receipt(
|
|
storage_owner,
|
|
recipient_id,
|
|
relay_message_id,
|
|
sender_id,
|
|
&context.message_id,
|
|
state,
|
|
event_at,
|
|
now_millis_i64(),
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
let _ = partner_id;
|
|
Ok(())
|
|
}
|
|
"MessageSend" => {
|
|
let message = relay_string(&content.content, DataType::Content, &context.type_map)
|
|
.ok_or_else(|| "Relay MessageSend is missing Content".to_string())?;
|
|
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.unwrap_or(created_at);
|
|
let height = relay_number(&content.content, DataType::Height, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.unwrap_or_default();
|
|
let reply_to = relay_number(&content.content, DataType::ReplyId, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok());
|
|
if relay_string(
|
|
&content.content,
|
|
DataType::RelayMessageId,
|
|
&context.type_map,
|
|
)
|
|
.is_some_and(|relay_message_id| relay_message_id != context.message_id)
|
|
{
|
|
return Err(
|
|
"Relay MessageSend identity does not match its protected message ID".into(),
|
|
);
|
|
}
|
|
chat_files::add_message(chat_files::NewMessage {
|
|
relay_signer_id: sender_id,
|
|
relay_message_id: &context.message_id,
|
|
authored_at: created_at,
|
|
send_time,
|
|
storage_owner,
|
|
external_user: if sent_by_self {
|
|
recipient_id
|
|
} else {
|
|
sender_id
|
|
},
|
|
sent_by_self,
|
|
content: message,
|
|
height,
|
|
reply_to,
|
|
origin_iota_received_at: sent_by_self.then_some(accepted_at),
|
|
destination_iota_received_at: (!sent_by_self).then_some(accepted_at),
|
|
initial_state: MessageState::Sent,
|
|
})
|
|
.map_err(|error| error.to_string())?;
|
|
Ok(())
|
|
}
|
|
"MessageEdit" => {
|
|
let message = relay_string(&content.content, DataType::Content, &context.type_map)
|
|
.ok_or_else(|| "Relay MessageEdit is missing Content".to_string())?;
|
|
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.ok_or_else(|| "Relay MessageEdit is missing SendTime".to_string())?;
|
|
chat_files::apply_remote_edit(recipient_id, sender_id, send_time, sender_id, message)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
"MessageReactionAdd" | "MessageReactionRemove" => {
|
|
let reaction = relay_string(&content.content, DataType::Reaction, &context.type_map)
|
|
.filter(|value| !value.is_empty() && value.len() <= 64)
|
|
.ok_or_else(|| "Relay reaction is invalid".to_string())?;
|
|
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.ok_or_else(|| "Relay reaction is missing SendTime".to_string())?;
|
|
let result = if content.message_type == "MessageReactionAdd" {
|
|
chat_files::add_reaction(recipient_id, sender_id, send_time, sender_id, reaction)
|
|
} else {
|
|
chat_files::remove_reaction(recipient_id, sender_id, send_time, sender_id, reaction)
|
|
};
|
|
result.map_err(|error| error.to_string())
|
|
}
|
|
"MessageDeleteLive" => {
|
|
let send_time = relay_number(&content.content, DataType::SendTime, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.ok_or_else(|| "Relay MessageDeleteLive is missing SendTime".to_string())?;
|
|
chat_files::apply_remote_delete(recipient_id, sender_id, send_time, sender_id)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
"SetChatSecret" => {
|
|
let frame = CommunicationValue::new(CommunicationType::SetChatSecret)
|
|
.with_payload(content.content.clone());
|
|
let recipients = chat_secret_recipients(&frame)
|
|
.ok_or_else(|| "Relay SetChatSecret has no recipients".to_string())?;
|
|
let recipient = recipients
|
|
.into_iter()
|
|
.find(|value| value.user_id == storage_owner.to_string())
|
|
.ok_or_else(|| "Relay SetChatSecret recipient mismatch".to_string())?;
|
|
let chat_id = data_string(&frame, DataType::ChatId)
|
|
.ok_or_else(|| "Relay SetChatSecret is missing ChatId".to_string())?;
|
|
let secret_id = data_string(&frame, DataType::SecretId)
|
|
.ok_or_else(|| "Relay SetChatSecret is missing SecretId".to_string())?;
|
|
let version = data_i64(&frame, DataType::VersionNumber)
|
|
.ok_or_else(|| "Relay SetChatSecret is missing VersionNumber".to_string())?;
|
|
let wrapping_scheme = data_string(&frame, DataType::WrappingScheme)
|
|
.ok_or_else(|| "Relay SetChatSecret is missing WrappingScheme".to_string())?;
|
|
e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret {
|
|
user_id: storage_owner.to_string(),
|
|
chat_id,
|
|
secret_id,
|
|
version,
|
|
encrypted_secret: recipient.encrypted_secret,
|
|
kem_ciphertext: recipient.kem_ciphertext,
|
|
wrapping_scheme,
|
|
created_at,
|
|
updated_at: now_millis_i64(),
|
|
})
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
"AddConversation" => {
|
|
let other_id =
|
|
relay_number(&content.content, DataType::ChatPartnerId, &context.type_map)
|
|
.and_then(|value| i64::try_from(value).ok())
|
|
.filter(|id| *id > 0)
|
|
.ok_or_else(|| {
|
|
"Relay AddConversation has an invalid ChatPartnerId".to_string()
|
|
})?;
|
|
let user_id = storage_owner;
|
|
if user_id <= 0 {
|
|
return Err("Relay AddConversation has an invalid storage owner".into());
|
|
}
|
|
add_conversation_for_user(
|
|
user_id,
|
|
other_id,
|
|
relay_string(
|
|
&content.content,
|
|
DataType::ChatPartnerName,
|
|
&context.type_map,
|
|
),
|
|
)
|
|
.map_err(|error| format!("AddConversation persistence failed: {error}"))
|
|
}
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue {
|
|
let mutation = match message_mutation(cv) {
|
|
Ok(mutation) => mutation,
|
|
Err(response) => return response,
|
|
};
|
|
let Some(content) = cv.get_data(DataType::Content).as_str() else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
|
|
match chat_files::edit_message(
|
|
mutation.sender_id,
|
|
mutation.partner_id,
|
|
mutation.send_time,
|
|
mutation.sender_id,
|
|
content,
|
|
) {
|
|
Ok(()) => success_response(cv),
|
|
Err(_) => error_response(cv, CommunicationType::ErrorNotFound),
|
|
}
|
|
}
|
|
|
|
pub fn handle_message_reaction(cv: &CommunicationValue, add: bool) -> CommunicationValue {
|
|
let mutation = match message_mutation(cv) {
|
|
Ok(mutation) => mutation,
|
|
Err(response) => return response,
|
|
};
|
|
let Some(reaction) = cv.get_data(DataType::Reaction).as_str() else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
if reaction.is_empty() || reaction.len() > 64 {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
}
|
|
|
|
let result = if add {
|
|
chat_files::add_reaction(
|
|
mutation.sender_id,
|
|
mutation.partner_id,
|
|
mutation.send_time,
|
|
mutation.sender_id,
|
|
reaction,
|
|
)
|
|
} else {
|
|
chat_files::remove_reaction(
|
|
mutation.sender_id,
|
|
mutation.partner_id,
|
|
mutation.send_time,
|
|
mutation.sender_id,
|
|
reaction,
|
|
)
|
|
};
|
|
|
|
match result {
|
|
Ok(()) => success_response(cv),
|
|
Err(iota_storage::storage_error::StorageError::ReactionLimitReached) => {
|
|
error_response(cv, CommunicationType::ErrorInvalidData)
|
|
}
|
|
Err(_) => error_response(cv, CommunicationType::ErrorNotFound),
|
|
}
|
|
}
|
|
|
|
pub fn handle_message_delete(cv: &CommunicationValue) -> CommunicationValue {
|
|
let mutation = match message_mutation(cv) {
|
|
Ok(mutation) => mutation,
|
|
Err(response) => return response,
|
|
};
|
|
|
|
match chat_files::delete_message(mutation.sender_id, mutation.partner_id, mutation.send_time) {
|
|
Ok(()) => success_response(cv),
|
|
Err(_) => error_response(cv, CommunicationType::ErrorNotFound),
|
|
}
|
|
}
|
|
|
|
fn stored_message_fields(
|
|
message: &chat_files::StoredMessage,
|
|
storage_owner: i64,
|
|
partner_id: i64,
|
|
) -> Vec<(DataType, DataValue)> {
|
|
let mut fields = vec![
|
|
(
|
|
DataType::MessageId,
|
|
DataValue::SignedNumber(message.id as i128),
|
|
),
|
|
(
|
|
DataType::SendTime,
|
|
DataValue::SignedNumber(message.message_time as i128),
|
|
),
|
|
(DataType::Content, DataValue::Str(message.content.clone())),
|
|
(
|
|
DataType::MessageState,
|
|
DataValue::Str(message.message_state.clone()),
|
|
),
|
|
(
|
|
DataType::Height,
|
|
DataValue::SignedNumber(message.height as i128),
|
|
),
|
|
];
|
|
let sender_id = if message.sent_by_self {
|
|
storage_owner
|
|
} else {
|
|
partner_id
|
|
};
|
|
if let Ok(sender_id) = u128::try_from(sender_id) {
|
|
fields.push((DataType::SenderId, DataValue::UnsignedNumber(sender_id)));
|
|
}
|
|
if let Some(relay_message_id) = &message.relay_message_id {
|
|
fields.push((
|
|
DataType::RelayMessageId,
|
|
DataValue::Str(relay_message_id.clone()),
|
|
));
|
|
}
|
|
for (data_type, timestamp) in [
|
|
(DataType::AuthoredAt, message.authored_at),
|
|
(
|
|
DataType::OriginIotaReceivedAt,
|
|
message.origin_iota_received_at,
|
|
),
|
|
(
|
|
DataType::DestinationIotaReceivedAt,
|
|
message.destination_iota_received_at,
|
|
),
|
|
(DataType::ClientReceivedAt, message.client_received_at),
|
|
(
|
|
DataType::ClientReceivedRecordedAt,
|
|
message.client_received_recorded_at,
|
|
),
|
|
(DataType::ReadAt, message.read_at),
|
|
(DataType::ReadRecordedAt, message.read_recorded_at),
|
|
] {
|
|
if let Some(timestamp) = timestamp {
|
|
fields.push((data_type, DataValue::SignedNumber(timestamp.into())));
|
|
}
|
|
}
|
|
if message.edited {
|
|
fields.push((DataType::Edited, DataValue::Bool(true)));
|
|
}
|
|
if let Some(reply_to) = message.reply_to.and_then(|id| u64::try_from(id).ok()) {
|
|
fields.push((
|
|
DataType::ReplyId,
|
|
DataValue::UnsignedNumber(u128::from(reply_to)),
|
|
));
|
|
}
|
|
if !message.reactions.is_empty() {
|
|
let reactions = message
|
|
.reactions
|
|
.iter()
|
|
.map(|reaction| {
|
|
typed_container(vec![
|
|
(
|
|
DataType::Reaction,
|
|
DataValue::Str(reaction.reaction.clone()),
|
|
),
|
|
(
|
|
DataType::SenderId,
|
|
DataValue::SignedNumber(reaction.user_id as i128),
|
|
),
|
|
])
|
|
})
|
|
.collect();
|
|
fields.push((DataType::Reactions, DataValue::Array(reactions)));
|
|
}
|
|
fields
|
|
}
|
|
|
|
fn stored_message_value(
|
|
message: &chat_files::StoredMessage,
|
|
storage_owner: i64,
|
|
partner_id: i64,
|
|
) -> DataValue {
|
|
typed_container(stored_message_fields(message, storage_owner, partner_id))
|
|
}
|
|
|
|
fn synced_setting_value(setting: &SyncedSetting) -> DataValue {
|
|
typed_container(vec![
|
|
(
|
|
DataType::SettingId,
|
|
DataValue::SignedNumber(setting.id.into()),
|
|
),
|
|
(
|
|
DataType::SettingScope,
|
|
DataValue::Str(setting.scope.as_str().to_string()),
|
|
),
|
|
(
|
|
DataType::SettingTarget,
|
|
DataValue::Str(setting.scope_key.clone()),
|
|
),
|
|
(DataType::SettingsName, DataValue::Str(setting.name.clone())),
|
|
(DataType::Payload, DataValue::Str(setting.payload.clone())),
|
|
(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(setting.revision.into()),
|
|
),
|
|
])
|
|
}
|
|
|
|
pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue {
|
|
let Some(user_id) = data_string(cv, DataType::UserId) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let sender_id = match required_sender_id(cv) {
|
|
Ok(sender_id) => sender_id,
|
|
Err(response) => return response,
|
|
};
|
|
if user_id != sender_id.to_string() {
|
|
return error_response(cv, CommunicationType::ErrorNotFound);
|
|
}
|
|
let Some(chat_id) = data_string(cv, DataType::ChatId) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
|
|
match e2ee_storage::get_chat_secret(ChatSecretQuery {
|
|
user_id,
|
|
chat_id,
|
|
secret_id: data_string(cv, DataType::SecretId),
|
|
}) {
|
|
Ok(Some(record)) => CommunicationValue::new(CommunicationType::ChatSecretResponse)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(sender_id))
|
|
.add_typed_default(DataType::UserId, DataValue::Str(record.user_id))
|
|
.add_typed_default(DataType::ChatId, DataValue::Str(record.chat_id))
|
|
.add_typed_default(DataType::SecretId, DataValue::Str(record.secret_id))
|
|
.add_typed_default(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(record.version as i128),
|
|
)
|
|
.add_typed_default(
|
|
DataType::EncryptedSecret,
|
|
DataValue::Bytes(record.encrypted_secret),
|
|
)
|
|
.add_typed_default(
|
|
DataType::KemCiphertext,
|
|
DataValue::Bytes(record.kem_ciphertext),
|
|
)
|
|
.add_typed_default(
|
|
DataType::WrappingScheme,
|
|
DataValue::Str(record.wrapping_scheme),
|
|
)
|
|
.add_typed_default(
|
|
DataType::CreatedAt,
|
|
DataValue::SignedNumber(record.created_at as i128),
|
|
)
|
|
.add_typed_default(
|
|
DataType::UpdatedAt,
|
|
DataValue::SignedNumber(record.updated_at as i128),
|
|
),
|
|
Ok(None) => error_response(cv, CommunicationType::ErrorNotSet),
|
|
Err(_) => error_response(cv, CommunicationType::ErrorInvalidData),
|
|
}
|
|
}
|
|
|
|
pub fn handle_create_app(cv: &CommunicationValue) -> CommunicationValue {
|
|
let sender_id = match required_sender_id(cv) {
|
|
Ok(sender_id) => sender_id,
|
|
Err(response) => return response,
|
|
};
|
|
let app_identifier = cv
|
|
.get_data(DataType::AppIdentifier)
|
|
.as_str()
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let app_public_key = cv
|
|
.get_data(DataType::AppPublicKey)
|
|
.as_str()
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
if !app_identifier.is_empty() && !app_public_key.is_empty() {
|
|
let user = match iota_storage::users::user_manager::get_user(sender_id) {
|
|
Ok(user) => user,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
|
};
|
|
if let Some(mut user) = user {
|
|
if !user.trusted_apps.contains_key(&app_identifier) {
|
|
user.trusted_apps.insert(app_identifier, app_public_key);
|
|
iota_storage::users::user_manager::update_user(user);
|
|
}
|
|
}
|
|
}
|
|
|
|
CommunicationValue::new(CommunicationType::CreateApp)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(sender_id))
|
|
}
|
|
|
|
pub fn handle_delete_app(cv: &CommunicationValue) -> CommunicationValue {
|
|
let sender_id = match required_sender_id(cv) {
|
|
Ok(sender_id) => sender_id,
|
|
Err(response) => return response,
|
|
};
|
|
let app_identifier = cv
|
|
.get_data(DataType::AppIdentifier)
|
|
.as_str()
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
if !app_identifier.is_empty() {
|
|
let user = match iota_storage::users::user_manager::get_user(sender_id) {
|
|
Ok(user) => user,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
|
};
|
|
if let Some(mut user) = user {
|
|
if user.trusted_apps.contains_key(&app_identifier) {
|
|
user.trusted_apps.remove(&app_identifier);
|
|
iota_storage::users::user_manager::update_user(user);
|
|
}
|
|
}
|
|
}
|
|
|
|
CommunicationValue::new(CommunicationType::DeleteApp)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(sender_id))
|
|
}
|
|
|
|
fn contact_value(
|
|
contact: &iota_storage::users::contact::Contact,
|
|
messages: &[chat_files::StoredMessage],
|
|
storage_owner: i64,
|
|
) -> DataValue {
|
|
let mut fields = vec![(
|
|
DataType::UserId,
|
|
DataValue::SignedNumber(contact.user_id as i128),
|
|
)];
|
|
if let Some(name) = &contact.user_name {
|
|
fields.push((DataType::Username, DataValue::Str(name.clone())));
|
|
}
|
|
if let Some(last_message_at) = contact.last_message_at {
|
|
fields.push((
|
|
DataType::LastMessageAt,
|
|
DataValue::SignedNumber(last_message_at as i128),
|
|
));
|
|
}
|
|
fields.push((
|
|
DataType::Messages,
|
|
DataValue::Array(
|
|
messages
|
|
.iter()
|
|
.filter(|message| message.external_user == contact.user_id)
|
|
.map(|message| stored_message_value(message, storage_owner, contact.user_id))
|
|
.collect(),
|
|
),
|
|
));
|
|
typed_container(fields)
|
|
}
|
|
|
|
fn current_contact_ids(user_id: i64) -> Result<DataValue, StorageError> {
|
|
Ok(contact_ids_value(
|
|
chats_util::get_users(user_id)?
|
|
.into_iter()
|
|
.map(|contact| contact.user_id),
|
|
))
|
|
}
|
|
|
|
fn contact_ids_value(ids: impl IntoIterator<Item = i64>) -> DataValue {
|
|
let mut contact_ids = ids.into_iter().collect::<Vec<_>>();
|
|
contact_ids.sort_unstable();
|
|
contact_ids.dedup();
|
|
|
|
DataValue::Array(
|
|
contact_ids
|
|
.into_iter()
|
|
.map(|user_id| DataValue::SignedNumber(user_id as i128))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod presence_tests {
|
|
use super::{contact_ids_value, handle_get_chats, message_mutation};
|
|
use mtp::codec::{CommunicationType, CommunicationValue, DataValue};
|
|
|
|
#[test]
|
|
fn contact_snapshot_is_sorted_and_deduplicated() {
|
|
assert_eq!(
|
|
contact_ids_value([9, 3, 9, 4, 3]),
|
|
DataValue::Array(vec![
|
|
DataValue::SignedNumber(3),
|
|
DataValue::SignedNumber(4),
|
|
DataValue::SignedNumber(9),
|
|
])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn message_mutation_rejects_a_missing_authenticated_sender() {
|
|
let request = CommunicationValue::new(CommunicationType::MessageEdit).with_id(11);
|
|
let response = message_mutation(&request).expect_err("missing sender must be rejected");
|
|
|
|
assert!(response.is_type(CommunicationType::ErrorInvalidData));
|
|
assert_eq!(response.id(), Some(11));
|
|
assert_eq!(response.receiver(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn read_handler_rejects_a_missing_authenticated_sender() {
|
|
let request = CommunicationValue::new(CommunicationType::GetChats).with_id(12);
|
|
let response = handle_get_chats(&request);
|
|
|
|
assert!(response.is_type(CommunicationType::ErrorInvalidData));
|
|
assert_eq!(response.id(), Some(12));
|
|
assert_eq!(response.receiver(), None);
|
|
}
|
|
}
|
|
|
|
fn sync_error(cv: &CommunicationValue) -> CommunicationValue {
|
|
error_response(cv, CommunicationType::ErrorInvalidData).add_typed_default(
|
|
DataType::SessionId,
|
|
cv.get_data(DataType::SessionId)
|
|
.cloned()
|
|
.unwrap_or(DataValue::Null),
|
|
)
|
|
}
|
|
|
|
/// The sender is authenticated by MTP; a UserId embedded by a client is never trusted here.
|
|
pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue {
|
|
use iota_storage::util::sync::{self, CACHE_SCHEMA_VERSION};
|
|
let user_id = match required_sender_id(cv) {
|
|
Ok(id) if id > 0 => id,
|
|
_ => return sync_error(cv),
|
|
};
|
|
let session_id = match data_i64(cv, DataType::SessionId) {
|
|
Some(id) if id > 0 => id,
|
|
_ => return sync_error(cv),
|
|
};
|
|
let reported_version = match data_i64(cv, DataType::VersionNumber) {
|
|
Some(version) if version >= 0 => version,
|
|
_ => return sync_error(cv),
|
|
};
|
|
let cache_valid = cv.get_data(DataType::CacheValid).as_bool().unwrap_or(false);
|
|
let schema = data_i64(cv, DataType::CacheSchemaVersion).unwrap_or(0);
|
|
let head = match sync::head(user_id) {
|
|
Ok(version) => version,
|
|
Err(_) => return sync_error(cv),
|
|
};
|
|
let known_session = sync::has_session(user_id, session_id).unwrap_or(false);
|
|
let full = !cache_valid
|
|
|| reported_version == 0
|
|
|| !known_session
|
|
|| reported_version > head
|
|
|| schema != CACHE_SCHEMA_VERSION;
|
|
let (contacts, messages, settings, deleted_messages, deleted_contacts, deleted_settings, mode) =
|
|
if full {
|
|
let settings = match synced_settings::list(user_id) {
|
|
Ok(settings) => settings,
|
|
Err(_) => return sync_error(cv),
|
|
};
|
|
(
|
|
match chats_util::get_users(user_id) {
|
|
Ok(contacts) => contacts,
|
|
Err(_) => return sync_error(cv),
|
|
},
|
|
chat_files::get_all_messages(user_id),
|
|
settings,
|
|
Vec::new(),
|
|
Vec::new(),
|
|
Vec::new(),
|
|
"full",
|
|
)
|
|
} else {
|
|
match sync::delta(user_id, reported_version, head) {
|
|
Ok(delta) => {
|
|
let settings =
|
|
match synced_settings::list_by_ids(user_id, &delta.setting_upserts) {
|
|
Ok(settings) => settings,
|
|
Err(_) => return sync_error(cv),
|
|
};
|
|
(
|
|
match chats_util::get_users_by_ids(user_id, &delta.contact_upserts) {
|
|
Ok(contacts) => contacts,
|
|
Err(_) => return sync_error(cv),
|
|
},
|
|
chat_files::get_messages_by_ids(user_id, &delta.message_upserts),
|
|
settings,
|
|
delta.deleted_message_ids,
|
|
delta.deleted_contact_ids,
|
|
delta.deleted_setting_ids,
|
|
"delta",
|
|
)
|
|
}
|
|
Err(_) => {
|
|
let settings = match synced_settings::list(user_id) {
|
|
Ok(settings) => settings,
|
|
Err(_) => return sync_error(cv),
|
|
};
|
|
(
|
|
match chats_util::get_users(user_id) {
|
|
Ok(contacts) => contacts,
|
|
Err(_) => return sync_error(cv),
|
|
},
|
|
chat_files::get_all_messages(user_id),
|
|
settings,
|
|
Vec::new(),
|
|
Vec::new(),
|
|
Vec::new(),
|
|
"full",
|
|
)
|
|
}
|
|
}
|
|
};
|
|
let message_values = messages
|
|
.iter()
|
|
.map(|message| stored_message_value(message, user_id, message.external_user))
|
|
.collect();
|
|
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)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(user_id))
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
)
|
|
.add_typed_default(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(head as i128),
|
|
)
|
|
.add_typed_default(
|
|
DataType::CacheSchemaVersion,
|
|
DataValue::SignedNumber(CACHE_SCHEMA_VERSION as i128),
|
|
)
|
|
.add_typed_default(DataType::SyncMode, DataValue::Str(mode.into()))
|
|
.add_typed_default(
|
|
DataType::Contacts,
|
|
DataValue::Array(
|
|
contacts
|
|
.iter()
|
|
.map(|contact| contact_value(contact, &messages, user_id))
|
|
.collect(),
|
|
),
|
|
)
|
|
.add_typed_default(DataType::Messages, DataValue::Array(message_values))
|
|
.add_typed_default(
|
|
DataType::Settings,
|
|
DataValue::Array(settings.iter().map(synced_setting_value).collect()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::Communities,
|
|
DataValue::Array(community_values(user_id)),
|
|
)
|
|
.add_typed_default(
|
|
DataType::DeletedMessageIds,
|
|
DataValue::Array(
|
|
deleted_messages
|
|
.into_iter()
|
|
.map(|id| DataValue::SignedNumber(id as i128))
|
|
.collect(),
|
|
),
|
|
)
|
|
.add_typed_default(
|
|
DataType::DeletedContactIds,
|
|
DataValue::Array(
|
|
deleted_contacts
|
|
.into_iter()
|
|
.map(|id| DataValue::SignedNumber(id as i128))
|
|
.collect(),
|
|
),
|
|
)
|
|
.add_typed_default(
|
|
DataType::DeletedSettingIds,
|
|
DataValue::Array(
|
|
deleted_settings
|
|
.into_iter()
|
|
.map(|id| DataValue::SignedNumber(id as i128))
|
|
.collect(),
|
|
),
|
|
)
|
|
.add_typed_default(DataType::UserIds, contact_ids)
|
|
.add_typed_default(DataType::Calls, DataValue::Array(Vec::new()))
|
|
}
|
|
|
|
pub fn handle_client_state_ack(cv: &CommunicationValue) -> CommunicationValue {
|
|
use iota_storage::util::sync::{self, CACHE_SCHEMA_VERSION};
|
|
let user_id = match required_sender_id(cv) {
|
|
Ok(id) if id > 0 => id,
|
|
_ => return sync_error(cv),
|
|
};
|
|
let session_id = match data_i64(cv, DataType::SessionId) {
|
|
Some(id) if id > 0 => id,
|
|
_ => return sync_error(cv),
|
|
};
|
|
let version = match data_i64(cv, DataType::VersionNumber) {
|
|
Some(version) if version >= 0 => version,
|
|
_ => return sync_error(cv),
|
|
};
|
|
if sync::acknowledge(user_id, session_id, version, CACHE_SCHEMA_VERSION).is_err() {
|
|
return sync_error(cv);
|
|
}
|
|
success_response(cv)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
)
|
|
.add_typed_default(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(version as i128),
|
|
)
|
|
}
|
|
|
|
pub fn handle_message_state(cv: &CommunicationValue) {
|
|
let sender_id = match required_sender_id(cv) {
|
|
Ok(sender_id) => sender_id,
|
|
Err(_) => return,
|
|
};
|
|
let receiver_id = match data_i64(cv, DataType::ChatPartnerId) {
|
|
Some(id) if id > 0 => id,
|
|
_ => return,
|
|
};
|
|
|
|
let timestamp_i64 = data_i64(cv, DataType::SendTime).unwrap_or_else(now_millis_i64);
|
|
|
|
let _ = chat_files::change_message_state(
|
|
timestamp_i64,
|
|
receiver_id,
|
|
sender_id,
|
|
MessageState::from_str(cv.get_data(DataType::MessageState).as_str().unwrap_or("")),
|
|
);
|
|
}
|
|
|
|
pub fn handle_messages_get(cv: &CommunicationValue) -> CommunicationValue {
|
|
let my_id = match cv.require_sender() {
|
|
Ok(my_id) => my_id,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let Ok(my_id_i64) = i64::try_from(my_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(partner_id) = data_i64(cv, DataType::UserId).filter(|id| *id > 0) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(offset) = data_i64(cv, DataType::Offset).filter(|offset| *offset >= 0) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(amount) = data_i64(cv, DataType::Amount).filter(|amount| *amount > 0) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let messages = chat_files::get_messages(my_id_i64, partner_id, offset, amount);
|
|
let mut msg_array: Vec<DataValue> = Vec::new();
|
|
for m in &messages {
|
|
msg_array.push(stored_message_value(m, my_id_i64, partner_id));
|
|
}
|
|
|
|
CommunicationValue::new(CommunicationType::MessagesGet)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(DataType::Messages, DataValue::Array(msg_array))
|
|
}
|
|
|
|
pub fn handle_message_get(cv: &CommunicationValue) -> CommunicationValue {
|
|
let Some(send_time) = data_i64(cv, DataType::SendTime) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let partner_id = data_i64(cv, DataType::ChatPartnerId);
|
|
let owner = match required_sender_id(cv) {
|
|
Ok(owner) => owner,
|
|
Err(response) => return response,
|
|
};
|
|
|
|
let (message, offset) = match partner_id {
|
|
Some(partner_id) => match chat_files::get_message_with_offset(owner, partner_id, send_time)
|
|
{
|
|
Ok(Some((message, offset))) => (message, Some(offset)),
|
|
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
},
|
|
None => match chat_files::get_message(owner, send_time, None) {
|
|
Ok(Some(message)) => (message, None),
|
|
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
},
|
|
};
|
|
|
|
let mut response = CommunicationValue::new(CommunicationType::MessageGet)
|
|
.with_request_id(cv)
|
|
.with_receiver(u64::try_from(owner).expect("authenticated sender is non-negative"));
|
|
for (data_type, value) in stored_message_fields(&message, owner, message.external_user) {
|
|
response = response.add_typed_default(data_type, value);
|
|
}
|
|
if let Some(offset) = offset {
|
|
response =
|
|
response.add_typed_default(DataType::Offset, DataValue::SignedNumber(offset as i128));
|
|
}
|
|
response
|
|
}
|
|
|
|
pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue {
|
|
let user_id = match cv.require_sender() {
|
|
Ok(user_id) => user_id,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let Ok(user_id_i64) = i64::try_from(user_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let users = match chats_util::get_users(user_id_i64) {
|
|
Ok(users) => users,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
|
};
|
|
let mut user_array = Vec::new();
|
|
for user in users {
|
|
let mut container = Vec::new();
|
|
container.push((
|
|
DataType::UserId,
|
|
DataValue::SignedNumber(user.user_id as i128),
|
|
));
|
|
if let Some(name) = user.user_name {
|
|
container.push((DataType::Username, DataValue::Str(name)));
|
|
}
|
|
if let Some(ts) = user.last_message_at {
|
|
container.push((DataType::LastMessageAt, DataValue::SignedNumber(ts as i128)));
|
|
}
|
|
user_array.push(typed_container(container));
|
|
}
|
|
CommunicationValue::new(CommunicationType::GetChats)
|
|
.with_request_id(cv)
|
|
.with_receiver(user_id)
|
|
.add_typed_default(DataType::UserIds, DataValue::Array(user_array))
|
|
}
|
|
|
|
pub fn handle_add_community(cv: &CommunicationValue) -> CommunicationValue {
|
|
let sender_id = match required_sender_id(cv) {
|
|
Ok(sender_id) => sender_id,
|
|
Err(response) => return response,
|
|
};
|
|
let Some(address) = cv.get_data(DataType::CommunityAddress).as_str() else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(title) = cv.get_data(DataType::CommunityTitle).as_str() else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(position) = cv.get_data(DataType::Position).as_str() else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
CommunitiesUtil::add_community(
|
|
sender_id,
|
|
address.to_string(),
|
|
title.to_string(),
|
|
position.to_string(),
|
|
);
|
|
CommunicationValue::new(CommunicationType::AddCommunity)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(sender_id))
|
|
}
|
|
|
|
pub fn handle_get_communities(cv: &CommunicationValue) -> CommunicationValue {
|
|
let sender_id = match required_sender_id(cv) {
|
|
Ok(sender_id) => sender_id,
|
|
Err(response) => return response,
|
|
};
|
|
CommunicationValue::new(CommunicationType::GetCommunities)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(sender_id))
|
|
.add_typed_default(
|
|
DataType::Communities,
|
|
DataValue::Array(community_values(sender_id)),
|
|
)
|
|
}
|
|
|
|
fn community_values(storage_owner: i64) -> Vec<DataValue> {
|
|
CommunitiesUtil::get_communities(storage_owner)
|
|
.into_iter()
|
|
.map(|community| {
|
|
typed_container(vec![
|
|
(
|
|
DataType::CommunityAddress,
|
|
DataValue::Str(community.address),
|
|
),
|
|
(DataType::CommunityTitle, DataValue::Str(community.title)),
|
|
(DataType::Position, DataValue::Str(community.position)),
|
|
])
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn handle_remove_community(cv: &CommunicationValue) -> CommunicationValue {
|
|
let sender_id = match required_sender_id(cv) {
|
|
Ok(sender_id) => sender_id,
|
|
Err(response) => return response,
|
|
};
|
|
let Some(address) = cv.get_data(DataType::CommunityAddress).as_str() else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
if CommunitiesUtil::remove_community(sender_id, address.to_string()).is_err() {
|
|
return error_response(cv, CommunicationType::ErrorInternal);
|
|
}
|
|
CommunicationValue::new(CommunicationType::RemoveCommunity)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(sender_id))
|
|
}
|
|
|
|
pub fn handle_global_settings_save(cv: &CommunicationValue) -> CommunicationValue {
|
|
let my_id = match cv.require_sender() {
|
|
Ok(my_id) => my_id,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let Ok(my_id_i64) = i64::try_from(my_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Missing settings payload".to_string()),
|
|
);
|
|
};
|
|
|
|
if settings::save_global(my_id_i64, settings_value).is_err() {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
}
|
|
|
|
let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsSave)
|
|
.with_receiver(my_id)
|
|
.with_request_id(cv);
|
|
|
|
if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() {
|
|
response = response.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
}
|
|
|
|
response
|
|
}
|
|
|
|
pub fn handle_global_settings_load(cv: &CommunicationValue) -> CommunicationValue {
|
|
let my_id = match cv.require_sender() {
|
|
Ok(my_id) => my_id,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let Ok(my_id_i64) = i64::try_from(my_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Ok(settings_value) = settings::load_global(my_id_i64) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(settings_value_str) = settings_value else {
|
|
let mut response = CommunicationValue::new(CommunicationType::ErrorNotFound)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Path,
|
|
DataValue::Str("global.settings".to_string()),
|
|
);
|
|
|
|
if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() {
|
|
response = response.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
}
|
|
|
|
return response;
|
|
};
|
|
|
|
let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsLoad)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(DataType::Payload, DataValue::Str(settings_value_str));
|
|
|
|
if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() {
|
|
response = response.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
}
|
|
|
|
response
|
|
}
|
|
|
|
pub fn handle_settings_save(
|
|
cv: &CommunicationValue,
|
|
_expected_session_id: i128,
|
|
) -> CommunicationValue {
|
|
let my_id = match cv.require_sender() {
|
|
Ok(my_id) => my_id,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let Ok(my_id_i64) = i64::try_from(my_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Missing session_id".to_string()),
|
|
);
|
|
};
|
|
if session_id == 0 || session_id > 1_000_000 {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Invalid session_id".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
};
|
|
let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Missing settings_name".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
};
|
|
let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Missing settings payload".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
};
|
|
|
|
if !settings_name
|
|
.chars()
|
|
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
|
|
|| settings_name.contains("..")
|
|
{
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Invalid settings_name".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SettingsName,
|
|
DataValue::Str(settings_name.to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
}
|
|
let Ok(session_id_i64) = i64::try_from(session_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
|
|
if settings::save(my_id_i64, session_id_i64, settings_name, settings_value).is_err() {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
}
|
|
|
|
CommunicationValue::new(CommunicationType::SettingsSave)
|
|
.with_receiver(my_id)
|
|
.with_request_id(cv)
|
|
.add_typed_default(
|
|
DataType::SettingsName,
|
|
DataValue::Str(settings_name.to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
)
|
|
}
|
|
|
|
pub fn handle_settings_load(
|
|
cv: &CommunicationValue,
|
|
_expected_session_id: i128,
|
|
) -> CommunicationValue {
|
|
let my_id = match cv.require_sender() {
|
|
Ok(my_id) => my_id,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let Ok(my_id_i64) = i64::try_from(my_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Missing session_id".to_string()),
|
|
);
|
|
};
|
|
if session_id == 0 || session_id > 1_000_000 {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Invalid session_id".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
}
|
|
let Ok(session_id_i64) = i64::try_from(session_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Missing settings_name".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
};
|
|
|
|
if !settings_name
|
|
.chars()
|
|
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
|
|
|| settings_name.contains("..")
|
|
{
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Invalid settings_name".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SettingsName,
|
|
DataValue::Str(settings_name.to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
}
|
|
|
|
let Ok(settings_value) = settings::load(my_id_i64, session_id_i64, settings_name) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(settings_value_str) = settings_value else {
|
|
return CommunicationValue::new(CommunicationType::ErrorNotFound)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::SettingsName,
|
|
DataValue::Str(settings_name.to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
};
|
|
|
|
CommunicationValue::new(CommunicationType::SettingsLoad)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(DataType::Payload, DataValue::Str(settings_value_str))
|
|
.add_typed_default(
|
|
DataType::SettingsName,
|
|
DataValue::Str(settings_name.to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
)
|
|
}
|
|
|
|
pub fn handle_settings_list(
|
|
cv: &CommunicationValue,
|
|
_expected_session_id: i128,
|
|
) -> CommunicationValue {
|
|
let my_id = match cv.require_sender() {
|
|
Ok(my_id) => my_id,
|
|
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let Ok(my_id_i64) = i64::try_from(my_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Missing session_id".to_string()),
|
|
);
|
|
};
|
|
if session_id == 0 || session_id > 1_000_000 {
|
|
return CommunicationValue::new(CommunicationType::ErrorInvalidData)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(
|
|
DataType::Message,
|
|
DataValue::Str("Invalid session_id".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
);
|
|
}
|
|
let Ok(session_id_i64) = i64::try_from(session_id) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
|
|
let Ok(settings) = settings::list(my_id_i64, session_id_i64) else {
|
|
return error_response(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
let settings_json = settings.into_iter().map(DataValue::Str).collect();
|
|
CommunicationValue::new(CommunicationType::SettingsList)
|
|
.with_request_id(cv)
|
|
.with_receiver(my_id)
|
|
.add_typed_default(DataType::Settings, DataValue::Array(settings_json))
|
|
.add_typed_default(
|
|
DataType::SessionId,
|
|
DataValue::SignedNumber(session_id as i128),
|
|
)
|
|
}
|
|
|
|
fn setting_response(
|
|
cv: &CommunicationValue,
|
|
response_type: CommunicationType,
|
|
setting: &SyncedSetting,
|
|
) -> CommunicationValue {
|
|
CommunicationValue::new(response_type)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(setting.user_id))
|
|
.add_typed_default(
|
|
DataType::SettingId,
|
|
DataValue::SignedNumber(setting.id.into()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SettingScope,
|
|
DataValue::Str(setting.scope.as_str().to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SettingTarget,
|
|
DataValue::Str(setting.scope_key.clone()),
|
|
)
|
|
.add_typed_default(DataType::SettingsName, DataValue::Str(setting.name.clone()))
|
|
.add_typed_default(DataType::Payload, DataValue::Str(setting.payload.clone()))
|
|
.add_typed_default(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(setting.revision.into()),
|
|
)
|
|
}
|
|
|
|
fn setting_changed(setting: &SyncedSetting) -> CommunicationValue {
|
|
CommunicationValue::new(CommunicationType::SyncedSettingChanged)
|
|
.with_receiver(sender_wire_id(setting.user_id))
|
|
.add_typed_default(
|
|
DataType::SettingId,
|
|
DataValue::SignedNumber(setting.id.into()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SettingScope,
|
|
DataValue::Str(setting.scope.as_str().to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SettingTarget,
|
|
DataValue::Str(setting.scope_key.clone()),
|
|
)
|
|
.add_typed_default(DataType::SettingsName, DataValue::Str(setting.name.clone()))
|
|
.add_typed_default(DataType::Payload, DataValue::Str(setting.payload.clone()))
|
|
.add_typed_default(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(setting.revision.into()),
|
|
)
|
|
}
|
|
|
|
fn setting_deleted(user_id: i64, deleted: &synced_settings::DeletedSetting) -> CommunicationValue {
|
|
CommunicationValue::new(CommunicationType::SyncedSettingChanged)
|
|
.with_receiver(sender_wire_id(user_id))
|
|
.add_typed_default(
|
|
DataType::DeletedSettingIds,
|
|
DataValue::Array(vec![DataValue::SignedNumber(deleted.id.into())]),
|
|
)
|
|
.add_typed_default(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(deleted.revision.into()),
|
|
)
|
|
}
|
|
|
|
fn parse_setting_locator(cv: &CommunicationValue) -> Result<SettingLocator, CommunicationValue> {
|
|
let Some(scope_name) = cv.get_data(DataType::SettingScope).as_str() else {
|
|
return Err(error_response(cv, CommunicationType::ErrorInvalidData));
|
|
};
|
|
let Some(scope) = SettingScope::parse(scope_name) else {
|
|
return Err(error_response(cv, CommunicationType::ErrorInvalidData));
|
|
};
|
|
let Some(scope_key) = cv.get_data(DataType::SettingTarget).as_str() else {
|
|
return Err(error_response(cv, CommunicationType::ErrorInvalidData));
|
|
};
|
|
let Some(name) = cv.get_data(DataType::SettingsName).as_str() else {
|
|
return Err(error_response(cv, CommunicationType::ErrorInvalidData));
|
|
};
|
|
if !synced_settings::is_valid_name(name) {
|
|
return Err(error_response(cv, CommunicationType::ErrorInvalidData));
|
|
}
|
|
match scope {
|
|
SettingScope::User if !scope_key.is_empty() => {
|
|
Err(error_response(cv, CommunicationType::ErrorInvalidData))
|
|
}
|
|
SettingScope::Contact if !scope_key.parse::<i64>().is_ok_and(|id| id > 0) => {
|
|
Err(error_response(cv, CommunicationType::ErrorInvalidData))
|
|
}
|
|
SettingScope::Community if scope_key.is_empty() => {
|
|
Err(error_response(cv, CommunicationType::ErrorInvalidData))
|
|
}
|
|
_ => Ok(SettingLocator {
|
|
scope,
|
|
scope_key: scope_key.to_string(),
|
|
name: name.to_string(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_setting_target(
|
|
user_id: i64,
|
|
locator: &SettingLocator,
|
|
) -> Result<(), CommunicationType> {
|
|
match locator.scope {
|
|
SettingScope::User => Ok(()),
|
|
SettingScope::Contact => {
|
|
let contact_id = locator
|
|
.scope_key
|
|
.parse::<i64>()
|
|
.map_err(|_| CommunicationType::ErrorInvalidData)?;
|
|
match has_user(user_id, contact_id) {
|
|
Ok(true) => Ok(()),
|
|
Ok(false) => Err(CommunicationType::ErrorInvalidData),
|
|
Err(_) => Err(CommunicationType::ErrorInternal),
|
|
}
|
|
}
|
|
SettingScope::Community => {
|
|
match CommunitiesUtil::has_community(user_id, &locator.scope_key) {
|
|
Ok(true) => Ok(()),
|
|
Ok(false) => Err(CommunicationType::ErrorInvalidData),
|
|
Err(_) => Err(CommunicationType::ErrorInternal),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setting_mutation_error(
|
|
cv: &CommunicationValue,
|
|
error_type: CommunicationType,
|
|
) -> SettingMutation {
|
|
SettingMutation {
|
|
response: error_response(cv, error_type),
|
|
changed: None,
|
|
}
|
|
}
|
|
|
|
pub fn handle_synced_setting_set(cv: &CommunicationValue) -> SettingMutation {
|
|
let user_id = match required_sender_id(cv) {
|
|
Ok(user_id) if user_id > 0 => user_id,
|
|
_ => return setting_mutation_error(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let locator = match parse_setting_locator(cv) {
|
|
Ok(locator) => locator,
|
|
Err(response) => {
|
|
return SettingMutation {
|
|
response,
|
|
changed: None,
|
|
};
|
|
}
|
|
};
|
|
if let Err(error_type) = validate_setting_target(user_id, &locator) {
|
|
return setting_mutation_error(cv, error_type);
|
|
}
|
|
let Some(payload) = cv.get_data(DataType::Payload).as_str() else {
|
|
return setting_mutation_error(cv, CommunicationType::ErrorInvalidData);
|
|
};
|
|
match synced_settings::set(
|
|
user_id,
|
|
locator.scope,
|
|
&locator.scope_key,
|
|
&locator.name,
|
|
payload,
|
|
) {
|
|
Ok(setting) => SettingMutation {
|
|
response: setting_response(cv, CommunicationType::SyncedSettingSet, &setting),
|
|
changed: Some(setting_changed(&setting)),
|
|
},
|
|
Err(_) => setting_mutation_error(cv, CommunicationType::ErrorInternal),
|
|
}
|
|
}
|
|
|
|
pub fn handle_synced_setting_get(cv: &CommunicationValue) -> CommunicationValue {
|
|
let user_id = match required_sender_id(cv) {
|
|
Ok(user_id) if user_id > 0 => user_id,
|
|
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let locator = match parse_setting_locator(cv) {
|
|
Ok(locator) => locator,
|
|
Err(response) => return response,
|
|
};
|
|
if let Err(error_type) = validate_setting_target(user_id, &locator) {
|
|
return error_response(cv, error_type);
|
|
}
|
|
match synced_settings::get(user_id, locator.scope, &locator.scope_key, &locator.name) {
|
|
Ok(Some(setting)) => setting_response(cv, CommunicationType::SyncedSettingGet, &setting),
|
|
Ok(None) => error_response(cv, CommunicationType::ErrorNotFound),
|
|
Err(_) => error_response(cv, CommunicationType::ErrorInternal),
|
|
}
|
|
}
|
|
|
|
pub fn handle_synced_setting_delete(cv: &CommunicationValue) -> SettingMutation {
|
|
let user_id = match required_sender_id(cv) {
|
|
Ok(user_id) if user_id > 0 => user_id,
|
|
_ => return setting_mutation_error(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
let locator = match parse_setting_locator(cv) {
|
|
Ok(locator) => locator,
|
|
Err(response) => {
|
|
return SettingMutation {
|
|
response,
|
|
changed: None,
|
|
};
|
|
}
|
|
};
|
|
if let Err(error_type) = validate_setting_target(user_id, &locator) {
|
|
return setting_mutation_error(cv, error_type);
|
|
}
|
|
match synced_settings::delete(user_id, locator.scope, &locator.scope_key, &locator.name) {
|
|
Ok(Some(deleted)) => SettingMutation {
|
|
response: CommunicationValue::new(CommunicationType::SyncedSettingDelete)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(user_id))
|
|
.add_typed_default(
|
|
DataType::SettingId,
|
|
DataValue::SignedNumber(deleted.id.into()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::VersionNumber,
|
|
DataValue::SignedNumber(deleted.revision.into()),
|
|
),
|
|
changed: deleted.changed.then(|| setting_deleted(user_id, &deleted)),
|
|
},
|
|
Ok(None) => SettingMutation {
|
|
response: CommunicationValue::new(CommunicationType::SyncedSettingDelete)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(user_id)),
|
|
changed: None,
|
|
},
|
|
Err(_) => setting_mutation_error(cv, CommunicationType::ErrorInternal),
|
|
}
|
|
}
|
|
|
|
pub fn handle_synced_settings_list(cv: &CommunicationValue) -> CommunicationValue {
|
|
let user_id = match required_sender_id(cv) {
|
|
Ok(user_id) if user_id > 0 => user_id,
|
|
_ => return error_response(cv, CommunicationType::ErrorInvalidData),
|
|
};
|
|
match synced_settings::list(user_id) {
|
|
Ok(settings) => CommunicationValue::new(CommunicationType::SyncedSettingsList)
|
|
.with_request_id(cv)
|
|
.with_receiver(sender_wire_id(user_id))
|
|
.add_typed_default(
|
|
DataType::Settings,
|
|
DataValue::Array(settings.iter().map(synced_setting_value).collect()),
|
|
),
|
|
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};
|
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
|
|
|
fn request() -> CommunicationValue {
|
|
CommunicationValue::new(CommunicationType::SyncedSettingSet)
|
|
.with_id(1)
|
|
.with_sender(7)
|
|
.add_typed_default(DataType::SettingScope, DataValue::Str("user".to_string()))
|
|
.add_typed_default(DataType::SettingTarget, DataValue::Str(String::new()))
|
|
.add_typed_default(
|
|
DataType::SettingsName,
|
|
DataValue::Str("notifications.enabled".to_string()),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn missing_sender_is_rejected_for_synced_settings() {
|
|
let response = handle_synced_setting_get(&request().without_sender());
|
|
|
|
assert!(response.is_type(CommunicationType::ErrorInvalidData));
|
|
assert_eq!(response.id(), Some(1));
|
|
assert_eq!(response.receiver(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn user_scope_rejects_a_non_empty_target() {
|
|
let request =
|
|
request().add_typed_default(DataType::SettingTarget, DataValue::Str("123".to_string()));
|
|
|
|
let response = handle_synced_setting_set(&request).response;
|
|
|
|
assert!(response.is_type(CommunicationType::ErrorInvalidData));
|
|
}
|
|
|
|
#[test]
|
|
fn contact_scope_rejects_a_malformed_target() {
|
|
let request = request()
|
|
.add_typed_default(
|
|
DataType::SettingScope,
|
|
DataValue::Str("contact".to_string()),
|
|
)
|
|
.add_typed_default(
|
|
DataType::SettingTarget,
|
|
DataValue::Str("not-a-user".to_string()),
|
|
);
|
|
|
|
let response = handle_synced_setting_set(&request).response;
|
|
|
|
assert!(response.is_type(CommunicationType::ErrorInvalidData));
|
|
}
|
|
|
|
#[test]
|
|
fn community_scope_requires_an_address() {
|
|
let request = request()
|
|
.add_typed_default(
|
|
DataType::SettingScope,
|
|
DataValue::Str("community".to_string()),
|
|
)
|
|
.add_typed_default(DataType::SettingTarget, DataValue::Str(String::new()));
|
|
|
|
let response = handle_synced_setting_set(&request).response;
|
|
|
|
assert!(response.is_type(CommunicationType::ErrorInvalidData));
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_setting_name_is_rejected() {
|
|
let request = request().add_typed_default(
|
|
DataType::SettingsName,
|
|
DataValue::Str("notifications..enabled".to_string()),
|
|
);
|
|
|
|
let response = parse_setting_locator(&request);
|
|
|
|
assert!(response.is_err());
|
|
}
|
|
}
|