diff --git a/backend/canisters/community/api/src/updates/c2c_tip_message.rs b/backend/canisters/community/api/src/updates/c2c_tip_message.rs index cc1a226284..1ebe072355 100644 --- a/backend/canisters/community/api/src/updates/c2c_tip_message.rs +++ b/backend/canisters/community/api/src/updates/c2c_tip_message.rs @@ -1,6 +1,6 @@ use candid::CandidType; use serde::{Deserialize, Serialize}; -use types::{ChannelId, CompletedCryptoTransaction, MessageId, MessageIndex, UserId}; +use types::{CanisterId, ChannelId, Cryptocurrency, MessageId, MessageIndex, UserId}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { @@ -8,7 +8,9 @@ pub struct Args { pub channel_id: ChannelId, pub thread_root_message_index: Option, pub message_id: MessageId, - pub transfer: CompletedCryptoTransaction, + pub ledger: CanisterId, + pub token: Cryptocurrency, + pub amount: u128, pub username: String, pub display_name: Option, } diff --git a/backend/canisters/community/impl/src/updates/c2c_tip_message.rs b/backend/canisters/community/impl/src/updates/c2c_tip_message.rs index 6b50789fc0..71d616e66c 100644 --- a/backend/canisters/community/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/community/impl/src/updates/c2c_tip_message.rs @@ -2,7 +2,7 @@ use crate::activity_notifications::handle_activity_notification; use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; -use chat_events::Reader; +use chat_events::{Reader, TipMessageArgs}; use community_canister::c2c_tip_message::{Response::*, *}; use group_chat_core::TipMessageResult; use ledger_utils::format_crypto_amount_with_symbol; @@ -29,17 +29,19 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { if let Some(channel) = state.data.channels.get_mut(&args.channel_id) { let now = state.env.now(); - let token = args.transfer.token(); - let amount = args.transfer.units(); - match channel.chat.tip_message( + let tip_message_args = TipMessageArgs { user_id, - args.recipient, - args.thread_root_message_index, - args.message_id, - args.transfer, + recipient: args.recipient, + thread_root_message_index: args.thread_root_message_index, + message_id: args.message_id, + ledger: args.ledger, + token: args.token.clone(), + amount: args.amount, now, - ) { + }; + + match channel.chat.tip_message(tip_message_args) { TipMessageResult::Success => { if let Some((message_index, message_event_index)) = channel .chat @@ -61,7 +63,11 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { tipped_by: user_id, tipped_by_name: args.username, tipped_by_display_name: args.display_name, - tip: format_crypto_amount_with_symbol(amount, token.decimals().unwrap_or(8), token.token_symbol()), + tip: format_crypto_amount_with_symbol( + args.amount, + args.token.decimals().unwrap_or(8), + args.token.token_symbol(), + ), community_avatar_id: state.data.avatar.as_ref().map(|a| a.id), channel_avatar_id: channel.chat.avatar.as_ref().map(|a| a.id), }); diff --git a/backend/canisters/group/api/src/updates/c2c_tip_message.rs b/backend/canisters/group/api/src/updates/c2c_tip_message.rs index 382357905b..b904ba339f 100644 --- a/backend/canisters/group/api/src/updates/c2c_tip_message.rs +++ b/backend/canisters/group/api/src/updates/c2c_tip_message.rs @@ -1,13 +1,15 @@ use candid::CandidType; use serde::{Deserialize, Serialize}; -use types::{CompletedCryptoTransaction, MessageId, MessageIndex, UserId}; +use types::{CanisterId, Cryptocurrency, MessageId, MessageIndex, UserId}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub recipient: UserId, pub thread_root_message_index: Option, pub message_id: MessageId, - pub transfer: CompletedCryptoTransaction, + pub ledger: CanisterId, + pub token: Cryptocurrency, + pub amount: u128, pub username: String, pub display_name: Option, } diff --git a/backend/canisters/group/impl/src/updates/c2c_tip_message.rs b/backend/canisters/group/impl/src/updates/c2c_tip_message.rs index a84f732a6c..129cc4b337 100644 --- a/backend/canisters/group/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/group/impl/src/updates/c2c_tip_message.rs @@ -2,7 +2,7 @@ use crate::activity_notifications::handle_activity_notification; use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; -use chat_events::Reader; +use chat_events::{Reader, TipMessageArgs}; use group_canister::c2c_tip_message::{Response::*, *}; use group_chat_core::TipMessageResult; use ledger_utils::format_crypto_amount_with_symbol; @@ -23,17 +23,19 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { let user_id = state.env.caller().into(); let now = state.env.now(); - let token = args.transfer.token(); - let amount = args.transfer.units(); - match state.data.chat.tip_message( + let tip_message_args = TipMessageArgs { user_id, - args.recipient, - args.thread_root_message_index, - args.message_id, - args.transfer, + recipient: args.recipient, + thread_root_message_index: args.thread_root_message_index, + message_id: args.message_id, + ledger: args.ledger, + token: args.token.clone(), + amount: args.amount, now, - ) { + }; + + match state.data.chat.tip_message(tip_message_args) { TipMessageResult::Success => { if let Some((message_index, message_event_index)) = state .data @@ -56,7 +58,11 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { tipped_by: user_id, tipped_by_name: args.username, tipped_by_display_name: args.display_name, - tip: format_crypto_amount_with_symbol(amount, token.decimals().unwrap_or(8), token.token_symbol()), + tip: format_crypto_amount_with_symbol( + args.amount, + args.token.decimals().unwrap_or(8), + args.token.token_symbol(), + ), group_avatar_id: state.data.chat.avatar.as_ref().map(|a| a.id), }), ); diff --git a/backend/canisters/user/api/can.did b/backend/canisters/user/api/can.did index 278c72418f..f13538a7e7 100644 --- a/backend/canisters/user/api/can.did +++ b/backend/canisters/user/api/can.did @@ -119,9 +119,13 @@ type RemoveReactionResponse = variant { type TipMessageArgs = record { chat : Chat; + recipient : UserId; thread_root_message_index : opt MessageIndex; message_id : MessageId; - transfer : PendingCryptoTransaction; + ledger : CanisterId; + token : Cryptocurrency; + amount : nat; + fee : nat; }; type TipMessageResponse = variant { diff --git a/backend/canisters/user/api/src/updates/c2c_tip_message.rs b/backend/canisters/user/api/src/updates/c2c_tip_message.rs index b09d2ad6a3..1ca681692f 100644 --- a/backend/canisters/user/api/src/updates/c2c_tip_message.rs +++ b/backend/canisters/user/api/src/updates/c2c_tip_message.rs @@ -1,12 +1,14 @@ use candid::CandidType; use serde::{Deserialize, Serialize}; -use types::{CompletedCryptoTransaction, MessageId, MessageIndex}; +use types::{CanisterId, Cryptocurrency, MessageId, MessageIndex}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub thread_root_message_index: Option, pub message_id: MessageId, - pub transfer: CompletedCryptoTransaction, + pub ledger: CanisterId, + pub token: Cryptocurrency, + pub amount: u128, pub username: String, pub display_name: Option, pub user_avatar_id: Option, diff --git a/backend/canisters/user/api/src/updates/tip_message.rs b/backend/canisters/user/api/src/updates/tip_message.rs index 2207395968..2986a00018 100644 --- a/backend/canisters/user/api/src/updates/tip_message.rs +++ b/backend/canisters/user/api/src/updates/tip_message.rs @@ -1,13 +1,17 @@ use candid::CandidType; use serde::{Deserialize, Serialize}; -use types::{Chat, CompletedCryptoTransaction, MessageId, MessageIndex, PendingCryptoTransaction}; +use types::{CanisterId, Chat, CompletedCryptoTransaction, Cryptocurrency, MessageId, MessageIndex, UserId}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub chat: Chat, + pub recipient: UserId, pub thread_root_message_index: Option, pub message_id: MessageId, - pub transfer: PendingCryptoTransaction, + pub ledger: CanisterId, + pub token: Cryptocurrency, + pub amount: u128, + pub fee: u128, } #[derive(CandidType, Serialize, Deserialize, Debug)] diff --git a/backend/canisters/user/impl/src/updates/c2c_tip_message.rs b/backend/canisters/user/impl/src/updates/c2c_tip_message.rs index 44545beba9..4a514e7954 100644 --- a/backend/canisters/user/impl/src/updates/c2c_tip_message.rs +++ b/backend/canisters/user/impl/src/updates/c2c_tip_message.rs @@ -1,7 +1,7 @@ use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; -use chat_events::{Reader, TipMessageResult}; +use chat_events::{Reader, TipMessageArgs, TipMessageResult}; use ledger_utils::format_crypto_amount_with_symbol; use types::{DirectMessageTipped, EventIndex, Notification, UserId}; use user_canister::c2c_tip_message::{Response::*, *}; @@ -19,19 +19,20 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { if let Some(chat) = state.data.direct_chats.get_mut(&user_id.into()) { let now = state.env.now(); let my_user_id = state.env.canister_id().into(); - let token = args.transfer.token(); - let amount = args.transfer.units(); + + let tip_message_args = TipMessageArgs { + user_id, + recipient: my_user_id, + thread_root_message_index: args.thread_root_message_index, + message_id: args.message_id, + ledger: args.ledger, + token: args.token.clone(), + amount: args.amount, + now, + }; if matches!( - chat.events.tip_message( - user_id, - my_user_id, - EventIndex::default(), - args.thread_root_message_index, - args.message_id, - args.transfer, - now, - ), + chat.events.tip_message(tip_message_args, EventIndex::default(),), TipMessageResult::Success ) { if let Some(event) = chat @@ -46,7 +47,11 @@ fn c2c_tip_message_impl(args: Args, state: &mut RuntimeState) -> Response { message_event_index: event.index, username: args.username, display_name: args.display_name, - tip: format_crypto_amount_with_symbol(amount, token.decimals().unwrap_or(8), token.token_symbol()), + tip: format_crypto_amount_with_symbol( + args.amount, + args.token.decimals().unwrap_or(8), + args.token.token_symbol(), + ), user_avatar_id: args.user_avatar_id, }); state.push_notification(my_user_id, notification); diff --git a/backend/canisters/user/impl/src/updates/tip_message.rs b/backend/canisters/user/impl/src/updates/tip_message.rs index 30abc83adf..54bd94f9a7 100644 --- a/backend/canisters/user/impl/src/updates/tip_message.rs +++ b/backend/canisters/user/impl/src/updates/tip_message.rs @@ -1,10 +1,11 @@ use crate::crypto::process_transaction; use crate::guards::caller_is_owner; use crate::{mutate_state, read_state, run_regular_jobs, RuntimeState}; +use candid::Principal; use canister_tracing_macros::trace; -use chat_events::TipMessageResult; +use chat_events::{TipMessageArgs, TipMessageResult}; use ic_cdk_macros::update; -use types::{Chat, ChatId, CompletedCryptoTransaction, EventIndex, MessageId, MessageIndex, UserId}; +use types::{icrc1, Chat, ChatId, CommunityId, EventIndex, PendingCryptoTransaction, TimestampNanos, UserId}; use user_canister::tip_message::{Response::*, *}; #[update(guard = "caller_is_owner")] @@ -12,39 +13,31 @@ use user_canister::tip_message::{Response::*, *}; async fn tip_message(args: Args) -> Response { run_regular_jobs(); - let prepare_result = match read_state(|state| prepare(&args, state)) { + let (prepare_result, now_nanos) = match read_state(|state| prepare(&args, state)) { Ok(ok) => ok, Err(response) => return *response, }; + let pending_transfer = PendingCryptoTransaction::ICRC1(icrc1::PendingCryptoTransaction { + ledger: args.ledger, + token: args.token.clone(), + amount: args.amount, + to: Principal::from(args.recipient).into(), + fee: args.fee, + memo: None, + created: now_nanos, + }); // Make the crypto transfer - let transfer = match process_transaction(args.transfer).await { - Ok(completed) => completed, + let completed_transfer = match process_transaction(pending_transfer).await { + Ok(transfer) => transfer, Err(failed) => return TransferFailed(failed.error_message().to_string()), }; - match args.chat { - Chat::Direct(chat_id) => mutate_state(|state| { - tip_direct_chat_message( - prepare_result, - chat_id, - args.thread_root_message_index, - args.message_id, - transfer, - state, - ) - }), - Chat::Group(chat_id) => { + match prepare_result { + PrepareResult::Direct(tip_message_args) => mutate_state(|state| tip_direct_chat_message(tip_message_args, state)), + PrepareResult::Group(group_id, c2c_args) => { use group_canister::c2c_tip_message::Response; - let args = group_canister::c2c_tip_message::Args { - recipient: prepare_result.recipient, - thread_root_message_index: args.thread_root_message_index, - message_id: args.message_id, - transfer, - username: prepare_result.username, - display_name: prepare_result.display_name, - }; - match group_canister_c2c_client::c2c_tip_message(chat_id.into(), &args).await { + match group_canister_c2c_client::c2c_tip_message(group_id.into(), &c2c_args).await { Ok(Response::Success) => Success, Ok(Response::MessageNotFound) => MessageNotFound, Ok(Response::CannotTipSelf) => CannotTipSelf, @@ -53,21 +46,12 @@ async fn tip_message(args: Args) -> Response { Ok(Response::GroupFrozen) => ChatFrozen, Ok(Response::UserNotInGroup) => ChatNotFound, Ok(Response::UserSuspended) => UserSuspended, - Err(error) => InternalError(format!("{error:?}"), Box::new(args.transfer)), + Err(error) => InternalError(format!("{error:?}"), Box::new(completed_transfer)), } } - Chat::Channel(community_id, channel_id) => { + PrepareResult::Channel(community_id, c2c_args) => { use community_canister::c2c_tip_message::Response; - let args = community_canister::c2c_tip_message::Args { - recipient: prepare_result.recipient, - channel_id, - thread_root_message_index: args.thread_root_message_index, - message_id: args.message_id, - transfer, - username: prepare_result.username, - display_name: prepare_result.display_name, - }; - match community_canister_c2c_client::c2c_tip_message(community_id.into(), &args).await { + match community_canister_c2c_client::c2c_tip_message(community_id.into(), &c2c_args).await { Ok(Response::Success) => Success, Ok(Response::MessageNotFound) => MessageNotFound, Ok(Response::CannotTipSelf) => CannotTipSelf, @@ -76,86 +60,96 @@ async fn tip_message(args: Args) -> Response { Ok(Response::CommunityFrozen) => ChatFrozen, Ok(Response::UserSuspended) => UserSuspended, Ok(Response::UserNotInCommunity | Response::ChannelNotFound) => ChatNotFound, - Err(error) => InternalError(format!("{error:?}"), Box::new(args.transfer)), + Err(error) => InternalError(format!("{error:?}"), Box::new(completed_transfer)), } } } } -struct PrepareResult { - my_user_id: UserId, - recipient: UserId, - username: String, - display_name: Option, +enum PrepareResult { + Direct(TipMessageArgs), + Group(ChatId, group_canister::c2c_tip_message::Args), + Channel(CommunityId, community_canister::c2c_tip_message::Args), } -fn prepare(args: &Args, state: &RuntimeState) -> Result> { - if args.transfer.is_zero() { - return Err(Box::new(TransferCannotBeZero)); - } - - let recipient = match args.transfer.user_id() { - Some(u) => u, - None => return Err(Box::new(TransferNotToMessageSender)), - }; - +fn prepare(args: &Args, state: &RuntimeState) -> Result<(PrepareResult, TimestampNanos), Box> { let my_user_id: UserId = state.env.canister_id().into(); - if my_user_id == recipient { - return Err(Box::new(CannotTipSelf)); - } - - if !match &args.chat { - Chat::Direct(d) => state.data.direct_chats.has(d), - Chat::Group(g) => state.data.group_chats.has(g), - Chat::Channel(c, _) => state.data.communities.has(c), - } { - return Err(Box::new(ChatNotFound)); - } - if state.data.suspended.value { Err(Box::new(UserSuspended)) - } else if args.transfer.is_zero() { + } else if args.amount == 0 { Err(Box::new(TransferCannotBeZero)) + } else if my_user_id == args.recipient { + Err(Box::new(CannotTipSelf)) } else { - Ok(PrepareResult { - my_user_id, - recipient, - username: state.data.username.value.clone(), - display_name: state.data.display_name.value.clone(), - }) + let now_nanos = state.env.now_nanos(); + match args.chat { + Chat::Direct(chat_id) if state.data.direct_chats.has(&chat_id) => Ok(( + PrepareResult::Direct(TipMessageArgs { + user_id: my_user_id, + recipient: args.recipient, + thread_root_message_index: args.thread_root_message_index, + message_id: args.message_id, + ledger: args.ledger, + token: args.token.clone(), + amount: args.amount, + now: state.env.now(), + }), + now_nanos, + )), + Chat::Group(group_id) if state.data.group_chats.has(&group_id) => Ok(( + PrepareResult::Group( + group_id, + group_canister::c2c_tip_message::Args { + recipient: args.recipient, + thread_root_message_index: args.thread_root_message_index, + message_id: args.message_id, + ledger: args.ledger, + token: args.token.clone(), + amount: args.amount, + username: state.data.username.value.clone(), + display_name: state.data.display_name.value.clone(), + }, + ), + now_nanos, + )), + Chat::Channel(community_id, channel_id) if state.data.communities.has(&community_id) => Ok(( + PrepareResult::Channel( + community_id, + community_canister::c2c_tip_message::Args { + recipient: args.recipient, + channel_id, + thread_root_message_index: args.thread_root_message_index, + message_id: args.message_id, + ledger: args.ledger, + token: args.token.clone(), + amount: args.amount, + username: state.data.username.value.clone(), + display_name: state.data.display_name.value.clone(), + }, + ), + now_nanos, + )), + _ => Err(Box::new(ChatNotFound)), + } } } -fn tip_direct_chat_message( - prepare_result: PrepareResult, - chat_id: ChatId, - thread_root_message_index: Option, - message_id: MessageId, - transfer: CompletedCryptoTransaction, - state: &mut RuntimeState, -) -> Response { - if let Some(chat) = state.data.direct_chats.get_mut(&chat_id) { - let now = state.env.now(); - match chat.events.tip_message( - prepare_result.my_user_id, - chat.them, - EventIndex::default(), - thread_root_message_index, - message_id, - transfer.clone(), - now, - ) { +fn tip_direct_chat_message(args: TipMessageArgs, state: &mut RuntimeState) -> Response { + if let Some(chat) = state.data.direct_chats.get_mut(&args.recipient.into()) { + match chat.events.tip_message(args.clone(), EventIndex::default()) { TipMessageResult::Success => { let c2c_args = user_canister::c2c_tip_message::Args { - thread_root_message_index, - message_id, - transfer, - username: prepare_result.username, - display_name: prepare_result.display_name, + thread_root_message_index: args.thread_root_message_index, + message_id: args.message_id, + ledger: args.ledger, + token: args.token, + amount: args.amount, + username: state.data.username.value.clone(), + display_name: state.data.display_name.value.clone(), user_avatar_id: state.data.avatar.value.as_ref().map(|a| a.id), }; state.data.fire_and_forget_handler.send( - chat_id.into(), + args.recipient.into(), "c2c_tip_message_msgpack".to_string(), msgpack::serialize_then_unwrap(c2c_args), ); diff --git a/backend/integration_tests/src/client/user.rs b/backend/integration_tests/src/client/user.rs index 313e6d5be5..2a09efe7e4 100644 --- a/backend/integration_tests/src/client/user.rs +++ b/backend/integration_tests/src/client/user.rs @@ -32,14 +32,11 @@ generate_update_call!(undelete_messages); pub mod happy_path { use crate::rng::random_message_id; - use crate::utils::now_nanos; use crate::User; - use candid::Principal; use ic_test_state_machine_client::StateMachine; - use types::icrc1::Account; use types::{ - icrc1, CanisterId, Chat, ChatId, CommunityId, Cryptocurrency, EventIndex, EventsResponse, MessageContentInitial, - MessageId, PendingCryptoTransaction, Reaction, Rules, TextContent, TimestampMillis, UserId, + CanisterId, Chat, ChatId, CommunityId, Cryptocurrency, EventIndex, EventsResponse, MessageContentInitial, MessageId, + Reaction, Rules, TextContent, TimestampMillis, UserId, }; pub fn send_text_message( @@ -266,17 +263,13 @@ pub mod happy_path { sender.canister(), &user_canister::tip_message::Args { chat, + recipient, thread_root_message_index: None, message_id, - transfer: PendingCryptoTransaction::ICRC1(icrc1::PendingCryptoTransaction { - ledger, - token, - amount, - to: Account::from(Principal::from(recipient)), - fee, - memo: None, - created: now_nanos(env), - }), + ledger, + token, + amount, + fee, }, ); diff --git a/backend/libraries/chat_events/src/chat_event_internal.rs b/backend/libraries/chat_events/src/chat_event_internal.rs index c55a9b5d50..f43480f795 100644 --- a/backend/libraries/chat_events/src/chat_event_internal.rs +++ b/backend/libraries/chat_events/src/chat_event_internal.rs @@ -1,5 +1,4 @@ use crate::incr; -use itertools::Itertools; use ledger_utils::format_crypto_amount; use search::Document; use serde::{Deserialize, Serialize}; @@ -8,15 +7,15 @@ use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use types::{ is_default, is_empty_slice, AudioContent, AvatarChanged, BlobReference, CanisterId, ChannelId, Chat, ChatId, ChatMetrics, - CommunityId, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, CustomContent, DeletedBy, - DirectChatCreated, EventIndex, EventsTimeToLiveUpdated, FileContent, GiphyContent, GroupCreated, GroupDescriptionChanged, - GroupFrozen, GroupGateUpdated, GroupInviteCodeChanged, GroupNameChanged, GroupReplyContext, GroupRulesChanged, - GroupUnfrozen, GroupVisibilityChanged, ImageContent, MemberJoined, MemberLeft, MembersAdded, MembersAddedToDefaultChannel, - MembersRemoved, Message, MessageContent, MessageContentInitial, MessageId, MessageIndex, MessagePinned, - MessageReminderContent, MessageReminderCreatedContent, MessageUnpinned, MultiUserChat, PermissionsChanged, - PollContentInternal, PrizeContent, PrizeContentInternal, PrizeWinnerContent, Proposal, ProposalContent, Reaction, - ReplyContext, ReportedMessage, ReportedMessageInternal, RoleChanged, TextContent, ThreadSummary, TimestampMillis, UserId, - UsersBlocked, UsersInvited, UsersUnblocked, VideoContent, + CommunityId, CryptoContent, CryptoTransaction, Cryptocurrency, CustomContent, DeletedBy, DirectChatCreated, EventIndex, + EventsTimeToLiveUpdated, FileContent, GiphyContent, GroupCreated, GroupDescriptionChanged, GroupFrozen, GroupGateUpdated, + GroupInviteCodeChanged, GroupNameChanged, GroupReplyContext, GroupRulesChanged, GroupUnfrozen, GroupVisibilityChanged, + ImageContent, MemberJoined, MemberLeft, MembersAdded, MembersAddedToDefaultChannel, MembersRemoved, Message, + MessageContent, MessageContentInitial, MessageId, MessageIndex, MessagePinned, MessageReminderContent, + MessageReminderCreatedContent, MessageUnpinned, MultiUserChat, PermissionsChanged, PollContentInternal, PrizeContent, + PrizeContentInternal, PrizeWinnerContent, Proposal, ProposalContent, Reaction, ReplyContext, ReportedMessage, + ReportedMessageInternal, RoleChanged, TextContent, ThreadSummary, TimestampMillis, Tips, UserId, UsersBlocked, + UsersInvited, UsersUnblocked, VideoContent, }; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -151,7 +150,7 @@ pub struct MessageInternal { #[serde(rename = "r", default, skip_serializing_if = "is_empty_slice")] pub reactions: Vec<(Reaction, HashSet)>, #[serde(rename = "ti", default, skip_serializing_if = "is_empty_slice")] - pub tips: Vec<(UserId, CompletedCryptoTransaction)>, + pub tips: Tips, #[serde(rename = "u", default, skip_serializing_if = "Option::is_none")] pub last_updated: Option, #[serde(rename = "e", default, skip_serializing_if = "Option::is_none")] @@ -166,15 +165,6 @@ pub struct MessageInternal { impl MessageInternal { pub fn hydrate(&self, my_user_id: Option) -> Message { - let mut tips = Vec::new(); - for (ledger, group) in &self.tips.iter().group_by(|(_, transfer)| transfer.ledger_canister_id()) { - let mut per_user = Vec::new(); - for (user_id, transfers) in &group.group_by(|(user_id, _)| user_id) { - per_user.push((*user_id, transfers.map(|(_, transfer)| transfer.units()).sum())); - } - tips.push((ledger, per_user)); - } - Message { message_index: self.message_index, message_id: self.message_id, @@ -190,7 +180,7 @@ impl MessageInternal { .iter() .map(|(r, u)| (r.clone(), u.iter().copied().collect())) .collect(), - tips, + tips: self.tips.clone(), edited: self.last_edited.is_some(), forwarded: self.forwarded, thread_summary: self.thread_summary.as_ref().map(|t| t.hydrate()), @@ -773,8 +763,7 @@ mod tests { }; use candid::Principal; use std::collections::HashSet; - use types::icrc1::{Account, CryptoAccount}; - use types::{icrc1, CompletedCryptoTransaction, Cryptocurrency, EventWrapperInternal, Reaction, TextContent}; + use types::{EventWrapperInternal, Reaction, TextContent, Tips}; #[test] fn serialize_with_max_defaults() { @@ -785,7 +774,7 @@ mod tests { content: MessageContentInternal::Text(TextContent { text: "123".to_string() }), replies_to: None, reactions: Vec::new(), - tips: Vec::new(), + tips: Tips::default(), last_updated: None, last_edited: None, deleted_by: None, @@ -820,6 +809,8 @@ mod tests { #[test] fn serialize_with_no_defaults() { let principal = Principal::from_text("4bkt6-4aaaa-aaaaf-aaaiq-cai").unwrap(); + let mut tips = Tips::default(); + tips.push(principal, principal.into(), 1); let message = MessageInternal { message_index: 1.into(), message_id: 1.into(), @@ -830,20 +821,7 @@ mod tests { event_index: 1.into(), }), reactions: vec![(Reaction::new("1".to_string()), HashSet::from([principal.into()]))], - tips: vec![( - principal.into(), - CompletedCryptoTransaction::ICRC1(icrc1::CompletedCryptoTransaction { - ledger: principal, - token: Cryptocurrency::InternetComputer, - amount: 1, - from: CryptoAccount::Account(Account::from(principal)), - to: CryptoAccount::Account(Account::from(principal)), - fee: 1, - memo: None, - created: 1, - block_index: 1, - }), - )], + tips, last_updated: Some(1), last_edited: Some(1), deleted_by: Some(DeletedByInternal { @@ -872,13 +850,13 @@ mod tests { let event_bytes = msgpack::serialize_then_unwrap(&event); let event_bytes_len = event_bytes.len(); - // Before optimisation: 619 - // After optimisation: 383 - assert_eq!(message_bytes_len, 383); + // Before optimisation: 438 + // After optimisation: 202 + assert_eq!(message_bytes_len, 202); - // Before optimisation: 681 - // After optimisation: 401 - assert_eq!(event_bytes_len, 401); + // Before optimisation: 500 + // After optimisation: 220 + assert_eq!(event_bytes_len, 220); let _deserialized: EventWrapperInternal = msgpack::deserialize_then_unwrap(&event_bytes); } diff --git a/backend/libraries/chat_events/src/chat_events.rs b/backend/libraries/chat_events/src/chat_events.rs index 7a5c07ad26..4dcab63ede 100644 --- a/backend/libraries/chat_events/src/chat_events.rs +++ b/backend/libraries/chat_events/src/chat_events.rs @@ -17,7 +17,7 @@ use types::{ EventsTimeToLiveUpdated, GroupCanisterThreadDetails, GroupCreated, GroupFrozen, GroupUnfrozen, HydratedMention, Mention, Message, MessageContentInitial, MessageId, MessageIndex, MessageMatch, Milliseconds, MultiUserChat, PollVotes, PrizeWinnerContent, ProposalUpdate, PushEventResult, PushIfNotContains, RangeSet, Reaction, RegisterVoteResult, - TimestampMillis, Timestamped, UserId, VoteOperation, + TimestampMillis, Timestamped, Tips, UserId, VoteOperation, }; use types::{Hash, MessageReport, ReportedMessageInternal}; @@ -109,7 +109,7 @@ impl ChatEvents { content: args.content, replies_to: args.replies_to, reactions: Vec::new(), - tips: Vec::new(), + tips: Tips::default(), last_updated: None, last_edited: None, deleted_by: None, @@ -541,40 +541,33 @@ impl ChatEvents { } } - #[allow(clippy::too_many_arguments)] - pub fn tip_message( - &mut self, - user_id: UserId, - recipient: UserId, - min_visible_event_index: EventIndex, - thread_root_message_index: Option, - message_id: MessageId, - transfer: CompletedCryptoTransaction, - now: TimestampMillis, - ) -> TipMessageResult { + pub fn tip_message(&mut self, args: TipMessageArgs, min_visible_event_index: EventIndex) -> TipMessageResult { use TipMessageResult::*; - if let Some((message, event_index)) = - self.message_internal_mut(min_visible_event_index, thread_root_message_index, message_id.into(), now) - { - if message.sender == user_id { + if let Some((message, event_index)) = self.message_internal_mut( + min_visible_event_index, + args.thread_root_message_index, + args.message_id.into(), + args.now, + ) { + if message.sender == args.user_id { return CannotTipSelf; } - if message.sender != recipient { + if message.sender != args.recipient { return RecipientMismatch; } - message.tips.push((user_id, transfer)); - message.last_updated = Some(now); + message.tips.push(args.ledger, args.user_id, args.amount); + message.last_updated = Some(args.now); self.last_updated_timestamps - .mark_updated(thread_root_message_index, event_index, now); + .mark_updated(args.thread_root_message_index, event_index, args.now); add_to_metrics( &mut self.metrics, &mut self.per_user_metrics, - user_id, + args.user_id, |m| incr(&mut m.tips), - now, + args.now, ); Success @@ -1340,6 +1333,18 @@ pub enum AddRemoveReactionResult { MessageNotFound, } +#[derive(Clone)] +pub struct TipMessageArgs { + pub user_id: UserId, + pub recipient: UserId, + pub thread_root_message_index: Option, + pub message_id: MessageId, + pub ledger: CanisterId, + pub token: Cryptocurrency, + pub amount: u128, + pub now: TimestampMillis, +} + pub enum TipMessageResult { Success, MessageNotFound, diff --git a/backend/libraries/group_chat_core/src/lib.rs b/backend/libraries/group_chat_core/src/lib.rs index 257b910fc6..51461732f5 100644 --- a/backend/libraries/group_chat_core/src/lib.rs +++ b/backend/libraries/group_chat_core/src/lib.rs @@ -1,6 +1,6 @@ use chat_events::{ AddRemoveReactionArgs, ChatEventInternal, ChatEvents, ChatEventsListReader, DeleteMessageResult, - DeleteUndeleteMessagesArgs, MessageContentInternal, PushMessageArgs, Reader, UndeleteMessageResult, + DeleteUndeleteMessagesArgs, MessageContentInternal, PushMessageArgs, Reader, TipMessageArgs, UndeleteMessageResult, }; use lazy_static::lazy_static; use regex_lite::Regex; @@ -8,14 +8,13 @@ use search::Query; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use types::{ - AccessGate, AvatarChanged, CompletedCryptoTransaction, ContentValidationError, CryptoTransaction, Document, EventIndex, - EventWrapper, EventsResponse, FieldTooLongResult, FieldTooShortResult, GroupDescriptionChanged, GroupGateUpdated, - GroupNameChanged, GroupPermissionRole, GroupPermissions, GroupReplyContext, GroupRole, GroupRulesChanged, GroupSubtype, - GroupVisibilityChanged, HydratedMention, InvalidPollReason, MemberLeft, MembersRemoved, Message, MessageContent, - MessageContentInitial, MessageId, MessageIndex, MessageMatch, MessagePinned, MessageUnpinned, MessagesResponse, - Milliseconds, OptionUpdate, OptionalGroupPermissions, PermissionsChanged, PushEventResult, Reaction, RoleChanged, Rules, - SelectedGroupUpdates, ThreadPreview, TimestampMillis, Timestamped, UpdatedRules, UserId, UsersBlocked, UsersInvited, - Version, Versioned, VersionedRules, + AccessGate, AvatarChanged, ContentValidationError, CryptoTransaction, Document, EventIndex, EventWrapper, EventsResponse, + FieldTooLongResult, FieldTooShortResult, GroupDescriptionChanged, GroupGateUpdated, GroupNameChanged, GroupPermissionRole, + GroupPermissions, GroupReplyContext, GroupRole, GroupRulesChanged, GroupSubtype, GroupVisibilityChanged, HydratedMention, + InvalidPollReason, MemberLeft, MembersRemoved, Message, MessageContent, MessageContentInitial, MessageId, MessageIndex, + MessageMatch, MessagePinned, MessageUnpinned, MessagesResponse, Milliseconds, OptionUpdate, OptionalGroupPermissions, + PermissionsChanged, PushEventResult, Reaction, RoleChanged, Rules, SelectedGroupUpdates, ThreadPreview, TimestampMillis, + Timestamped, UpdatedRules, UserId, UsersBlocked, UsersInvited, Version, Versioned, VersionedRules, }; use utils::document_validation::validate_avatar; use utils::text_validation::{ @@ -867,18 +866,10 @@ impl GroupChatCore { } } - pub fn tip_message( - &mut self, - user_id: UserId, - recipient: UserId, - thread_root_message_index: Option, - message_id: MessageId, - transfer: CompletedCryptoTransaction, - now: TimestampMillis, - ) -> TipMessageResult { + pub fn tip_message(&mut self, args: TipMessageArgs) -> TipMessageResult { use TipMessageResult::*; - if let Some(member) = self.members.get(&user_id) { + if let Some(member) = self.members.get(&args.user_id) { if member.suspended.value { return UserSuspended; } @@ -888,17 +879,7 @@ impl GroupChatCore { let min_visible_event_index = member.min_visible_event_index(); - self.events - .tip_message( - user_id, - recipient, - min_visible_event_index, - thread_root_message_index, - message_id, - transfer, - now, - ) - .into() + self.events.tip_message(args, min_visible_event_index).into() } else { UserNotInGroup } diff --git a/backend/libraries/types/src/message.rs b/backend/libraries/types/src/message.rs index daf709c2ce..d031f90069 100644 --- a/backend/libraries/types/src/message.rs +++ b/backend/libraries/types/src/message.rs @@ -3,6 +3,7 @@ use crate::{ }; use candid::CandidType; use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct Message { @@ -13,7 +14,7 @@ pub struct Message { pub replies_to: Option, pub reactions: Vec<(Reaction, Vec)>, #[serde(default)] - pub tips: Vec<(CanisterId, Vec<(UserId, u128)>)>, + pub tips: Tips, pub thread_summary: Option, pub edited: bool, pub forwarded: bool, @@ -30,3 +31,34 @@ pub struct ReplyContext { pub struct GroupReplyContext { pub event_index: EventIndex, } + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Default)] +pub struct Tips(Vec<(CanisterId, Vec<(UserId, u128)>)>); + +impl Deref for Tips { + type Target = Vec<(CanisterId, Vec<(UserId, u128)>)>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Tips { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Tips { + pub fn push(&mut self, ledger: CanisterId, user_id: UserId, amount: u128) { + if let Some((_, tips)) = self.iter_mut().find(|(c, _)| *c == ledger) { + if let Some((_, total)) = tips.iter_mut().find(|(u, _)| *u == user_id) { + *total += amount; + } else { + tips.push((user_id, amount)); + } + } else { + self.0.push((ledger, vec![(user_id, amount)])); + } + } +}