diff --git a/Cargo.lock b/Cargo.lock index 4303bad03d..d26f0b52e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2821,6 +2821,7 @@ dependencies = [ "chat_events", "constants", "event_store_producer", + "event_store_producer_cdk_runtime", "group_community_common", "ic-stable-structures", "itertools 0.13.0", diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index 8fc1ebc071..2edaa0405b 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Log error if end video call job fails ([#7066](https://github.com/open-chat-labs/open-chat/pull/7066)) +- 2-stage bot messages + bot context in messages ([#7060](https://github.com/open-chat-labs/open-chat/pull/7060)) ### Fixed diff --git a/backend/canisters/community/api/can.did b/backend/canisters/community/api/can.did index f9089402a8..4ecbc90a99 100644 --- a/backend/canisters/community/api/can.did +++ b/backend/canisters/community/api/can.did @@ -903,6 +903,7 @@ type SendMessageResponse = variant { CommunityFrozen; RulesNotAccepted; CommunityRulesNotAccepted; + MessageAlreadyExists; }; type SendMessageSuccess = record { diff --git a/backend/canisters/community/api/src/updates/send_message.rs b/backend/canisters/community/api/src/updates/send_message.rs index f25dc34a50..f2f37c0648 100644 --- a/backend/canisters/community/api/src/updates/send_message.rs +++ b/backend/canisters/community/api/src/updates/send_message.rs @@ -41,6 +41,7 @@ pub enum Response { InvalidRequest(String), CommunityFrozen, RulesNotAccepted, + MessageAlreadyExists, CommunityRulesNotAccepted, UserLapsed, } diff --git a/backend/canisters/community/impl/src/lib.rs b/backend/canisters/community/impl/src/lib.rs index 14f303c3e9..fff3a10145 100644 --- a/backend/canisters/community/impl/src/lib.rs +++ b/backend/canisters/community/impl/src/lib.rs @@ -313,7 +313,7 @@ impl RuntimeState { } } - pub fn caller(&self, bot_id: Option) -> CallerResult { + pub fn verified_caller(&self, mut bot_context: Option) -> CallerResult { use CallerResult::*; let caller = self.env.caller(); @@ -322,37 +322,32 @@ impl RuntimeState { return Success(Caller::OCBot(OPENCHAT_BOT_USER_ID)); } - if let Some(bot_id) = bot_id { - if let Some(bot) = self.data.bots.get(&bot_id) { - if let Some(member) = self.data.members.get_by_user_id(&bot.added_by) { - if member.suspended().value { - return Suspended; - } else if member.lapsed().value { - return Lapsed; - } else { - return Success(Caller::BotV2(BotCaller { - user_id: bot.added_by, - bot_id, - })); - } - } - } - } else if let Some(member) = self.data.members.get(caller) { - if member.suspended().value { - return Suspended; - } else if member.lapsed().value { - return Lapsed; + let user_or_principal = bot_context.as_ref().map(|bc| bc.initiator.into()).unwrap_or(caller); + + let Some(member) = self.data.members.get(user_or_principal) else { + return NotFound; + }; + + if member.suspended().value { + return Suspended; + } else if member.lapsed().value { + return Lapsed; + } + + if let Some(bot_context) = bot_context.take() { + if self.data.bots.get(&bot_context.bot).is_some() { + Success(Caller::BotV2(bot_context)) } else { - return match member.user_type { - UserType::User => Success(Caller::User(member.user_id)), - UserType::BotV2 => NotFound, - UserType::Bot => Success(Caller::Bot(member.user_id)), - UserType::OcControlledBot => Success(Caller::OCBot(member.user_id)), - }; + NotFound + } + } else { + match member.user_type { + UserType::User => Success(Caller::User(member.user_id)), + UserType::BotV2 => NotFound, + UserType::Bot => Success(Caller::Bot(member.user_id)), + UserType::OcControlledBot => Success(Caller::OCBot(member.user_id)), } } - - NotFound } } diff --git a/backend/canisters/community/impl/src/updates/c2c_handle_bot_action.rs b/backend/canisters/community/impl/src/updates/c2c_handle_bot_action.rs index fc4e70c4a9..de21574886 100644 --- a/backend/canisters/community/impl/src/updates/c2c_handle_bot_action.rs +++ b/backend/canisters/community/impl/src/updates/c2c_handle_bot_action.rs @@ -6,7 +6,7 @@ use canister_tracing_macros::trace; use community_canister::c2c_handle_bot_action::*; use community_canister::send_message; use types::bot_actions::MessageContent; -use types::{BotAction, Chat, HandleBotActionsError, MessageContentInitial}; +use types::{BotAction, BotCaller, Chat, HandleBotActionsError, MessageContentInitial}; use utils::bots::can_execute_bot_command; #[update(guard = "caller_is_local_user_index", msgpack = true)] @@ -31,8 +31,8 @@ fn c2c_handle_bot_action_impl(args: Args, state: &mut RuntimeState) -> Response }; match args.action { - BotAction::SendMessage(content) => { - let content = match content { + BotAction::SendMessage(action) => { + let content = match action.content { MessageContent::Text(text_content) => MessageContentInitial::Text(text_content), MessageContent::Image(image_content) => MessageContentInitial::Image(image_content), MessageContent::Video(video_content) => MessageContentInitial::Video(video_content), @@ -59,7 +59,12 @@ fn c2c_handle_bot_action_impl(args: Args, state: &mut RuntimeState) -> Response message_filter_failed: None, new_achievement: false, }, - Some(args.bot.user_id), + Some(BotCaller { + bot: args.bot.user_id, + initiator: args.initiator, + command_text: args.command_text, + finalised: action.finalised, + }), state, ) { send_message::Response::Success(_) => Ok(()), @@ -80,10 +85,7 @@ fn is_bot_permitted_to_execute_command(args: &Args, state: &RuntimeState) -> boo }; // Get the permissions granted to the user in this community/channel - let Some(granted_to_user) = state - .data - .get_user_permissions_for_bot_commands(&args.commanded_by, &channel_id) - else { + let Some(granted_to_user) = state.data.get_user_permissions_for_bot_commands(&args.initiator, &channel_id) else { return false; }; diff --git a/backend/canisters/community/impl/src/updates/edit_message.rs b/backend/canisters/community/impl/src/updates/edit_message.rs index 4efc64a067..6b512a6865 100644 --- a/backend/canisters/community/impl/src/updates/edit_message.rs +++ b/backend/canisters/community/impl/src/updates/edit_message.rs @@ -52,6 +52,7 @@ fn edit_message_impl(args: Args, state: &mut RuntimeState) -> Response { message_id: args.message_id, content: args.content, block_level_markdown: args.block_level_markdown, + finalise_bot_message: false, now, }, Some(&mut state.data.event_store_client), diff --git a/backend/canisters/community/impl/src/updates/send_message.rs b/backend/canisters/community/impl/src/updates/send_message.rs index ff99784a28..7f4fda864b 100644 --- a/backend/canisters/community/impl/src/updates/send_message.rs +++ b/backend/canisters/community/impl/src/updates/send_message.rs @@ -13,8 +13,8 @@ use lazy_static::lazy_static; use regex_lite::Regex; use std::str::FromStr; use types::{ - Achievement, Caller, ChannelId, ChannelMessageNotification, Chat, EventIndex, EventWrapper, Message, MessageContent, - MessageIndex, Notification, TimestampMillis, User, UserId, Version, + Achievement, BotCaller, Caller, ChannelId, ChannelMessageNotification, Chat, EventIndex, EventWrapper, Message, + MessageContent, MessageIndex, Notification, TimestampMillis, User, UserId, Version, }; use user_canister::{CommunityCanisterEvent, MessageActivity, MessageActivityEvent}; @@ -34,8 +34,8 @@ fn c2c_send_message(args: C2CArgs) -> C2CResponse { mutate_state(|state| c2c_send_message_impl(args, state)) } -pub(crate) fn send_message_impl(args: Args, bot_id: Option, state: &mut RuntimeState) -> Response { - let caller = match state.caller(bot_id) { +pub(crate) fn send_message_impl(args: Args, bot: Option, state: &mut RuntimeState) -> Response { + let caller = match state.verified_caller(bot) { CallerResult::Success(caller) => caller, CallerResult::NotFound => return UserNotInCommunity, CallerResult::Suspended => return UserSuspended, @@ -86,7 +86,7 @@ pub(crate) fn send_message_impl(args: Args, bot_id: Option, state: &mut } fn c2c_send_message_impl(args: C2CArgs, state: &mut RuntimeState) -> C2CResponse { - let caller = match state.caller(None) { + let caller = match state.verified_caller(None) { CallerResult::Success(caller) => caller, CallerResult::NotFound => return UserNotInCommunity, CallerResult::Suspended => return UserSuspended, @@ -202,105 +202,107 @@ fn process_send_message_result( let content = &message_event.event.content; let community_id = state.env.canister_id().into(); - let notification = Notification::ChannelMessage(ChannelMessageNotification { - community_id, - channel_id, - thread_root_message_index, - message_index: message_event.event.message_index, - event_index: message_event.index, - community_name: state.data.name.value.clone(), - channel_name, - sender: caller.agent(), - sender_name: sender_username, - sender_display_name, - message_type: content.message_type(), - message_text: content - .notification_text(&users_mentioned.mentioned_directly, &users_mentioned.user_groups_mentioned), - image_url: content.notification_image_url(), - community_avatar_id: state.data.avatar.as_ref().map(|d| d.id), - channel_avatar_id, - crypto_transfer: content.notification_crypto_transfer_details(&users_mentioned.mentioned_directly), - }); - state.push_notification(result.users_to_notify, notification); - register_timer_jobs(channel_id, thread_root_message_index, message_event, now, &mut state.data); - if new_achievement && !caller.is_bot() { - for a in result - .message_event - .event - .achievements(false, thread_root_message_index.is_some()) - { - state.data.notify_user_of_achievement(caller.agent(), a); + if !result.unfinalised_bot_message { + let notification = Notification::ChannelMessage(ChannelMessageNotification { + community_id, + channel_id, + thread_root_message_index, + message_index: message_event.event.message_index, + event_index: message_event.index, + community_name: state.data.name.value.clone(), + channel_name, + sender: caller.agent(), + sender_name: sender_username, + sender_display_name, + message_type: content.message_type(), + message_text: content + .notification_text(&users_mentioned.mentioned_directly, &users_mentioned.user_groups_mentioned), + image_url: content.notification_image_url(), + community_avatar_id: state.data.avatar.as_ref().map(|d| d.id), + channel_avatar_id, + crypto_transfer: content.notification_crypto_transfer_details(&users_mentioned.mentioned_directly), + }); + state.push_notification(result.users_to_notify, notification); + + if new_achievement && !caller.is_bot() { + for a in result + .message_event + .event + .achievements(false, thread_root_message_index.is_some()) + { + state.data.notify_user_of_achievement(caller.agent(), a); + } } - } - - let mut activity_events = Vec::new(); - if let MessageContent::Crypto(c) = &message_event.event.content { - let recipient_is_human = state - .data - .members - .get_by_user_id(&c.recipient) - .map_or(false, |m| !m.user_type.is_bot()); + let mut activity_events = Vec::new(); - if recipient_is_human { - state + if let MessageContent::Crypto(c) = &message_event.event.content { + let recipient_is_human = state .data - .notify_user_of_achievement(c.recipient, Achievement::ReceivedCrypto); + .members + .get_by_user_id(&c.recipient) + .map_or(false, |m| !m.user_type.is_bot()); - activity_events.push((c.recipient, MessageActivity::Crypto)); - } - } + if recipient_is_human { + state + .data + .notify_user_of_achievement(c.recipient, Achievement::ReceivedCrypto); - if let Some(channel) = state.data.channels.get(&channel_id) { - for user_id in users_mentioned.all_users_mentioned { - if user_id != caller.initiator() - && channel.chat.members.get(&user_id).map_or(false, |m| !m.user_type().is_bot()) - { - activity_events.push((user_id, MessageActivity::Mention)); + activity_events.push((c.recipient, MessageActivity::Crypto)); } } - if let Some(replying_to_event_index) = message_event - .event - .replies_to - .as_ref() - .filter(|r| r.chat_if_other.is_none()) - .map(|r| r.event_index) - { - if let Some((message, _)) = channel.chat.events.message_internal( - EventIndex::default(), - thread_root_message_index, - replying_to_event_index.into(), - ) { - if message.sender != caller.initiator() - && channel - .chat - .members - .get(&message.sender) - .map_or(false, |m| !m.user_type().is_bot()) + if let Some(channel) = state.data.channels.get(&channel_id) { + for user_id in users_mentioned.all_users_mentioned { + if user_id != caller.initiator() + && channel.chat.members.get(&user_id).map_or(false, |m| !m.user_type().is_bot()) { - activity_events.push((message.sender, MessageActivity::QuoteReply)); + activity_events.push((user_id, MessageActivity::Mention)); + } + } + + if let Some(replying_to_event_index) = message_event + .event + .replies_to + .as_ref() + .filter(|r| r.chat_if_other.is_none()) + .map(|r| r.event_index) + { + if let Some((message, _)) = channel.chat.events.message_internal( + EventIndex::default(), + thread_root_message_index, + replying_to_event_index.into(), + ) { + if message.sender != caller.initiator() + && channel + .chat + .members + .get(&message.sender) + .map_or(false, |m| !m.user_type().is_bot()) + { + activity_events.push((message.sender, MessageActivity::QuoteReply)); + } } } } - } - for (user_id, activity) in activity_events { - state.data.user_event_sync_queue.push( - user_id, - CommunityCanisterEvent::MessageActivity(MessageActivityEvent { - chat: Chat::Channel(community_id, channel_id), - thread_root_message_index, - message_index, - message_id, - event_index, - activity, - timestamp: now, - user_id: Some(caller.agent()), - }), - ); + for (user_id, activity) in activity_events { + state.data.user_event_sync_queue.push( + user_id, + CommunityCanisterEvent::MessageActivity(MessageActivityEvent { + chat: Chat::Channel(community_id, channel_id), + thread_root_message_index, + message_index, + message_id, + event_index, + activity, + timestamp: now, + user_id: Some(caller.agent()), + }), + ); + } } handle_activity_notification(state); @@ -321,6 +323,7 @@ fn process_send_message_result( SendMessageResult::UserSuspended => UserSuspended, SendMessageResult::UserLapsed => UserLapsed, SendMessageResult::RulesNotAccepted => RulesNotAccepted, + SendMessageResult::MessageAlreadyExists => MessageAlreadyExists, SendMessageResult::InvalidRequest(error) => InvalidRequest(error), } } diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index 1fe655849c..ba7b54af77 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Log error if end video call job fails ([#7066](https://github.com/open-chat-labs/open-chat/pull/7066)) +- 2-stage bot messages + bot context in messages ([#7060](https://github.com/open-chat-labs/open-chat/pull/7060)) ## [[2.0.1516](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1516-group)] - 2024-12-13 diff --git a/backend/canisters/group/api/can.did b/backend/canisters/group/api/can.did index fe111d8685..fd87eabf0e 100644 --- a/backend/canisters/group/api/can.did +++ b/backend/canisters/group/api/can.did @@ -29,6 +29,7 @@ type SendMessageResponse = variant { UserLapsed; ChatFrozen; RulesNotAccepted; + MessageAlreadyExists; }; type SendMessageSuccess = record { diff --git a/backend/canisters/group/api/src/updates/send_message_v2.rs b/backend/canisters/group/api/src/updates/send_message_v2.rs index b01302a868..a2bf5b5689 100644 --- a/backend/canisters/group/api/src/updates/send_message_v2.rs +++ b/backend/canisters/group/api/src/updates/send_message_v2.rs @@ -39,6 +39,7 @@ pub enum Response { InvalidRequest(String), ChatFrozen, RulesNotAccepted, + MessageAlreadyExists, } #[ts_export(group, send_message)] diff --git a/backend/canisters/group/impl/src/lib.rs b/backend/canisters/group/impl/src/lib.rs index 3ce68f52bd..0a0c46b02e 100644 --- a/backend/canisters/group/impl/src/lib.rs +++ b/backend/canisters/group/impl/src/lib.rs @@ -433,7 +433,7 @@ impl RuntimeState { } } - pub fn caller(&self, bot_id: Option) -> CallerResult { + pub fn verified_caller(&self, mut bot_context: Option) -> CallerResult { use CallerResult::*; let caller = self.env.caller(); @@ -442,33 +442,30 @@ impl RuntimeState { return Success(Caller::OCBot(OPENCHAT_BOT_USER_ID)); } - if let Some(bot_id) = bot_id { - if let Some(bot) = self.data.chat.bots.get(&bot_id) { - if let Some(member) = self.data.get_member(bot.added_by.into()) { - if member.suspended().value { - return Suspended; - } else { - return Success(Caller::BotV2(BotCaller { - user_id: bot.added_by, - bot_id, - })); - } - } - } - } else if let Some(member) = self.data.get_member(caller) { - if member.suspended().value { - return Suspended; + let user_or_principal = bot_context.as_ref().map(|bc| bc.initiator.into()).unwrap_or(caller); + + let Some(member) = self.data.get_member(user_or_principal) else { + return NotFound; + }; + + if member.suspended().value { + return Suspended; + } + + if let Some(bot_context) = bot_context.take() { + if self.data.chat.bots.get(&bot_context.bot).is_some() { + Success(Caller::BotV2(bot_context)) } else { - return match member.user_type() { - UserType::User => Success(Caller::User(member.user_id())), - UserType::BotV2 => NotFound, - UserType::Bot => Success(Caller::Bot(member.user_id())), - UserType::OcControlledBot => Success(Caller::OCBot(member.user_id())), - }; + NotFound + } + } else { + match member.user_type() { + UserType::User => Success(Caller::User(member.user_id())), + UserType::BotV2 => NotFound, + UserType::Bot => Success(Caller::Bot(member.user_id())), + UserType::OcControlledBot => Success(Caller::OCBot(member.user_id())), } } - - NotFound } } diff --git a/backend/canisters/group/impl/src/new_joiner_rewards.rs b/backend/canisters/group/impl/src/new_joiner_rewards.rs index 4189a1238e..4fd2ad60f5 100644 --- a/backend/canisters/group/impl/src/new_joiner_rewards.rs +++ b/backend/canisters/group/impl/src/new_joiner_rewards.rs @@ -86,6 +86,7 @@ fn send_reward_transferred_message(user_id: UserId, transfer: nns::CompletedCryp block_level_markdown: false, correlation_id: 0, now: state.env.now(), + bot_context: None, }, Some(&mut state.data.event_store_client), ); diff --git a/backend/canisters/group/impl/src/updates/c2c_handle_bot_action.rs b/backend/canisters/group/impl/src/updates/c2c_handle_bot_action.rs index d6a3b6df34..7aa48a8c81 100644 --- a/backend/canisters/group/impl/src/updates/c2c_handle_bot_action.rs +++ b/backend/canisters/group/impl/src/updates/c2c_handle_bot_action.rs @@ -7,7 +7,7 @@ use group_canister::c2c_handle_bot_action::*; use group_canister::send_message_v2; use types::bot_actions::MessageContent; use types::HandleBotActionsError; -use types::{BotAction, MessageContentInitial}; +use types::{BotAction, BotCaller, MessageContentInitial}; use utils::bots::can_execute_bot_command; #[update(guard = "caller_is_local_user_index", msgpack = true)] @@ -28,8 +28,8 @@ fn c2c_handle_bot_action_impl(args: Args, state: &mut RuntimeState) -> Response } match args.action { - BotAction::SendMessage(content) => { - let content = match content { + BotAction::SendMessage(action) => { + let content = match action.content { MessageContent::Text(text_content) => MessageContentInitial::Text(text_content), MessageContent::Image(image_content) => MessageContentInitial::Image(image_content), MessageContent::Video(video_content) => MessageContentInitial::Video(video_content), @@ -55,7 +55,12 @@ fn c2c_handle_bot_action_impl(args: Args, state: &mut RuntimeState) -> Response new_achievement: false, correlation_id: 0, }, - Some(args.bot.user_id), + Some(BotCaller { + bot: args.bot.user_id, + initiator: args.initiator, + command_text: args.command_text, + finalised: action.finalised, + }), state, ) { send_message_v2::Response::Success(_) => Ok(()), @@ -72,7 +77,7 @@ fn is_bot_permitted_to_execute_command(args: &Args, state: &RuntimeState) -> boo }; // Get the permissions granted to the user in this community/channel - let Some(granted_to_user) = state.data.get_user_permissions_for_bot_commands(&args.commanded_by) else { + let Some(granted_to_user) = state.data.get_user_permissions_for_bot_commands(&args.initiator) else { return false; }; diff --git a/backend/canisters/group/impl/src/updates/edit_message.rs b/backend/canisters/group/impl/src/updates/edit_message.rs index 4310056279..a50cec1503 100644 --- a/backend/canisters/group/impl/src/updates/edit_message.rs +++ b/backend/canisters/group/impl/src/updates/edit_message.rs @@ -36,6 +36,7 @@ fn edit_message_impl(args: Args, state: &mut RuntimeState) -> Response { message_id: args.message_id, content: args.content, block_level_markdown: args.block_level_markdown, + finalise_bot_message: false, now, }; diff --git a/backend/canisters/group/impl/src/updates/send_message.rs b/backend/canisters/group/impl/src/updates/send_message.rs index ee2869988f..f7c4acc940 100644 --- a/backend/canisters/group/impl/src/updates/send_message.rs +++ b/backend/canisters/group/impl/src/updates/send_message.rs @@ -7,8 +7,8 @@ use group_canister::c2c_send_message::{Args as C2CArgs, Response as C2CResponse} use group_canister::send_message_v2::{Response::*, *}; use group_chat_core::SendMessageResult; use types::{ - Achievement, Caller, Chat, EventIndex, EventWrapper, GroupMessageNotification, Message, MessageContent, MessageIndex, - Notification, TimestampMillis, User, UserId, + Achievement, BotCaller, Caller, Chat, EventIndex, EventWrapper, GroupMessageNotification, Message, MessageContent, + MessageIndex, Notification, TimestampMillis, User, }; use user_canister::{GroupCanisterEvent, MessageActivity, MessageActivityEvent}; @@ -28,12 +28,12 @@ fn c2c_send_message(args: C2CArgs) -> C2CResponse { mutate_state(|state| c2c_send_message_impl(args, state)) } -pub(crate) fn send_message_impl(args: Args, bot_id: Option, state: &mut RuntimeState) -> Response { +pub(crate) fn send_message_impl(args: Args, bot: Option, state: &mut RuntimeState) -> Response { if state.data.is_frozen() { return ChatFrozen; } - let caller = match state.caller(bot_id) { + let caller = match state.verified_caller(bot) { CallerResult::Success(caller) => caller, CallerResult::NotFound => return CallerNotInGroup, CallerResult::Suspended => return UserSuspended, @@ -74,7 +74,7 @@ fn c2c_send_message_impl(args: C2CArgs, state: &mut RuntimeState) -> C2CResponse return ChatFrozen; } - let caller = match state.caller(None) { + let caller = match state.verified_caller(None) { CallerResult::Success(caller) => caller, CallerResult::NotFound => return CallerNotInGroup, CallerResult::Suspended => return UserSuspended, @@ -136,102 +136,104 @@ fn process_send_message_result( register_timer_jobs(thread_root_message_index, message_event, now, &mut state.data); - let content = &message_event.event.content; - let chat_id = state.env.canister_id().into(); + if !result.unfinalised_bot_message { + let content = &message_event.event.content; + let chat_id = state.env.canister_id().into(); - let notification = Notification::GroupMessage(GroupMessageNotification { - chat_id, - thread_root_message_index, - message_index, - event_index, - group_name: state.data.chat.name.value.clone(), - sender: caller.agent(), - sender_name: sender_username, - sender_display_name, - message_type: content.message_type(), - message_text: content.notification_text(&mentioned, &[]), - image_url: content.notification_image_url(), - group_avatar_id: state.data.chat.avatar.as_ref().map(|d| d.id), - crypto_transfer: content.notification_crypto_transfer_details(&mentioned), - }); - state.push_notification(result.users_to_notify, notification); + let notification = Notification::GroupMessage(GroupMessageNotification { + chat_id, + thread_root_message_index, + message_index, + event_index, + group_name: state.data.chat.name.value.clone(), + sender: caller.agent(), + sender_name: sender_username, + sender_display_name, + message_type: content.message_type(), + message_text: content.notification_text(&mentioned, &[]), + image_url: content.notification_image_url(), + group_avatar_id: state.data.chat.avatar.as_ref().map(|d| d.id), + crypto_transfer: content.notification_crypto_transfer_details(&mentioned), + }); + state.push_notification(result.users_to_notify, notification); - if new_achievement && !caller.is_bot() { - for a in message_event.event.achievements(false, thread_root_message_index.is_some()) { - state.data.notify_user_of_achievement(caller.agent(), a); + if new_achievement && !caller.is_bot() { + for a in message_event.event.achievements(false, thread_root_message_index.is_some()) { + state.data.notify_user_of_achievement(caller.agent(), a); + } } - } - let mut activity_events = Vec::new(); + let mut activity_events = Vec::new(); - if let MessageContent::Crypto(c) = content { - if state - .data - .chat - .members - .get(&c.recipient) - .map_or(false, |m| !m.user_type().is_bot()) - { - state - .data - .notify_user_of_achievement(c.recipient, Achievement::ReceivedCrypto); - - activity_events.push((c.recipient, MessageActivity::Crypto)); - } - } - - for user in mentioned { - if user.user_id != caller.initiator() - && state + if let MessageContent::Crypto(c) = content { + if state .data .chat .members - .get(&user.user_id) + .get(&c.recipient) .map_or(false, |m| !m.user_type().is_bot()) - { - activity_events.push((user.user_id, MessageActivity::Mention)); + { + state + .data + .notify_user_of_achievement(c.recipient, Achievement::ReceivedCrypto); + + activity_events.push((c.recipient, MessageActivity::Crypto)); + } } - } - if let Some(replying_to_event_index) = message_event - .event - .replies_to - .as_ref() - .filter(|r| r.chat_if_other.is_none()) - .map(|r| r.event_index) - { - if let Some((message, _)) = state.data.chat.events.message_internal( - EventIndex::default(), - thread_root_message_index, - replying_to_event_index.into(), - ) { - if message.sender != caller.initiator() + for user in mentioned { + if user.user_id != caller.initiator() && state .data .chat .members - .get(&message.sender) + .get(&user.user_id) .map_or(false, |m| !m.user_type().is_bot()) { - activity_events.push((message.sender, MessageActivity::QuoteReply)); + activity_events.push((user.user_id, MessageActivity::Mention)); } } - } - for (user_id, activity) in activity_events { - state.data.user_event_sync_queue.push( - user_id, - GroupCanisterEvent::MessageActivity(MessageActivityEvent { - chat: Chat::Group(chat_id), + if let Some(replying_to_event_index) = message_event + .event + .replies_to + .as_ref() + .filter(|r| r.chat_if_other.is_none()) + .map(|r| r.event_index) + { + if let Some((message, _)) = state.data.chat.events.message_internal( + EventIndex::default(), thread_root_message_index, - message_index, - message_id, - event_index, - activity, - timestamp: now, - user_id: Some(caller.agent()), - }), - ); + replying_to_event_index.into(), + ) { + if message.sender != caller.initiator() + && state + .data + .chat + .members + .get(&message.sender) + .map_or(false, |m| !m.user_type().is_bot()) + { + activity_events.push((message.sender, MessageActivity::QuoteReply)); + } + } + } + + for (user_id, activity) in activity_events { + state.data.user_event_sync_queue.push( + user_id, + GroupCanisterEvent::MessageActivity(MessageActivityEvent { + chat: Chat::Group(chat_id), + thread_root_message_index, + message_index, + message_id, + event_index, + activity, + timestamp: now, + user_id: Some(caller.agent()), + }), + ); + } } handle_activity_notification(state); @@ -252,6 +254,7 @@ fn process_send_message_result( SendMessageResult::UserSuspended => UserSuspended, SendMessageResult::UserLapsed => NotAuthorized, SendMessageResult::RulesNotAccepted => RulesNotAccepted, + SendMessageResult::MessageAlreadyExists => MessageAlreadyExists, SendMessageResult::InvalidRequest(error) => InvalidRequest(error), } } diff --git a/backend/canisters/local_user_index/CHANGELOG.md b/backend/canisters/local_user_index/CHANGELOG.md index 6ffa1f3637..682fe5cae7 100644 --- a/backend/canisters/local_user_index/CHANGELOG.md +++ b/backend/canisters/local_user_index/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Changed + +- Rename fields in access token & c2c_handle_bot_action::Args ([#7060](https://github.com/open-chat-labs/open-chat/pull/7060)) + ## [[2.0.1513](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1513-local_user_index)] - 2024-12-13 ### Added diff --git a/backend/canisters/local_user_index/impl/src/queries/access_token.rs b/backend/canisters/local_user_index/impl/src/queries/access_token.rs index 6c4a08c623..375e22868a 100644 --- a/backend/canisters/local_user_index/impl/src/queries/access_token.rs +++ b/backend/canisters/local_user_index/impl/src/queries/access_token.rs @@ -64,7 +64,7 @@ async fn access_token(args: Args) -> Response { } AccessTokenType::BotCommand(bc) => { let custom_claims = BotCommandClaims { - user_id: bc.user_id, + initiator: bc.user_id, bot: bc.bot, chat: bc.chat, thread_root_message_index: bc.thread_root_message_index, diff --git a/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs b/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs index 93533d70b2..72ca07959f 100644 --- a/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs +++ b/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs @@ -44,7 +44,7 @@ fn validate(args: Args, state: &RuntimeState) -> Result Response { message_id: args.message_id, content: args.content.clone(), block_level_markdown: args.block_level_markdown, + finalise_bot_message: false, now, }; diff --git a/backend/canisters/user/impl/src/updates/send_message.rs b/backend/canisters/user/impl/src/updates/send_message.rs index f613f4c398..2774854672 100644 --- a/backend/canisters/user/impl/src/updates/send_message.rs +++ b/backend/canisters/user/impl/src/updates/send_message.rs @@ -242,6 +242,7 @@ fn send_message_impl( block_level_markdown: args.block_level_markdown, correlation_id: args.correlation_id, now, + bot_context: None, }; let chat = if let Some(c) = state.data.direct_chats.get_mut(&recipient.into()) { @@ -359,6 +360,7 @@ async fn send_to_bot_canister(recipient: UserId, message_index: MessageIndex, ar block_level_markdown: args.block_level_markdown, correlation_id: 0, now, + bot_context: None, }; chat.push_message(false, push_message_args, None, Some(&mut state.data.event_store_client)); diff --git a/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs b/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs index bc296f1620..8e09155161 100644 --- a/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs +++ b/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs @@ -116,6 +116,7 @@ async fn send_message_with_transfer_to_channel( Response::UserLapsed => UserLapsed, Response::CommunityFrozen => CommunityFrozen, Response::RulesNotAccepted => RulesNotAccepted, + Response::MessageAlreadyExists => MessageAlreadyExists, Response::CommunityRulesNotAccepted => CommunityRulesNotAccepted, Response::MessageEmpty | Response::InvalidPoll(_) @@ -238,6 +239,7 @@ async fn send_message_with_transfer_to_group( Response::UserLapsed => UserLapsed, Response::ChatFrozen => ChatFrozen, Response::RulesNotAccepted => RulesNotAccepted, + Response::MessageAlreadyExists => MessageAlreadyExists, Response::MessageEmpty | Response::InvalidPoll(_) | Response::NotAuthorized diff --git a/backend/canisters/user/impl/src/updates/start_video_call.rs b/backend/canisters/user/impl/src/updates/start_video_call.rs index 1d4c1c4baa..db33c96f49 100644 --- a/backend/canisters/user/impl/src/updates/start_video_call.rs +++ b/backend/canisters/user/impl/src/updates/start_video_call.rs @@ -102,6 +102,7 @@ pub fn handle_start_video_call( block_level_markdown: false, correlation_id: 0, now, + bot_context: None, }; let chat = if let Some(c) = state.data.direct_chats.get_mut(&other.into()) { diff --git a/backend/canisters/user_index/impl/src/updates/c2c_report_message.rs b/backend/canisters/user_index/impl/src/updates/c2c_report_message.rs index 006de5c992..5beb01031a 100644 --- a/backend/canisters/user_index/impl/src/updates/c2c_report_message.rs +++ b/backend/canisters/user_index/impl/src/updates/c2c_report_message.rs @@ -193,6 +193,7 @@ https://github.com/open-chat-labs/open-chat/commit/e93865ea29b5bab8a9f0b01052938 edited: false, forwarded: false, block_level_markdown: false, + bot_context: None, }; let report = construct_html_report(Chat::Group(chat_id), None, &message, true); @@ -229,6 +230,7 @@ https://github.com/open-chat-labs/open-chat/commit/e93865ea29b5bab8a9f0b01052938 edited: false, forwarded: false, block_level_markdown: false, + bot_context: None, }; let report = construct_html_report(chat, None, &message, true); @@ -269,6 +271,7 @@ https://github.com/open-chat-labs/open-chat/commit/e93865ea29b5bab8a9f0b01052938 edited: false, forwarded: false, block_level_markdown: false, + bot_context: None, }; let report = construct_html_report(chat, None, &message, true); diff --git a/backend/integration_tests/src/bot_tests.rs b/backend/integration_tests/src/bot_tests.rs index ab57bc091f..cdb20207b8 100644 --- a/backend/integration_tests/src/bot_tests.rs +++ b/backend/integration_tests/src/bot_tests.rs @@ -7,7 +7,7 @@ use std::collections::HashSet; use std::ops::Deref; use std::time::Duration; use testing::rng::{random_from_u128, random_string}; -use types::bot_actions::MessageContent; +use types::bot_actions::{BotMessageAction, MessageContent}; use types::{ AccessTokenType, BotAction, BotCommandArgs, Chat, ChatEvent, ChatId, MessagePermission, SlashCommandPermissions, SlashCommandSchema, TextContent, @@ -122,7 +122,7 @@ fn bot_smoke_test() { println!("ACCESS TOKEN: {access_token}"); - // Call execute_bot_command as bot + // Call execute_bot_command as bot - unfinalised message let username = user.username(); let text = format!("Hello {username}"); let response = client::local_user_index::execute_bot_command( @@ -130,7 +130,44 @@ fn bot_smoke_test() { bot_principal, canister_ids.local_user_index, &local_user_index_canister::execute_bot_command::Args { - action: BotAction::SendMessage(MessageContent::Text(TextContent { text: text.clone() })), + action: BotAction::SendMessage(BotMessageAction { + content: MessageContent::Text(TextContent { text: text.clone() }), + finalised: false, + }), + jwt: access_token.clone(), + }, + ); + + if response.is_err() { + panic!("'execute_bot_command' error: {response:?}"); + } + + // Call `events` and confirm the latest event is a text message from the bot + let response = client::group::happy_path::events(env, &user, group_id, 0.into(), true, 5, 10); + + let latest_event = response.events.last().expect("Expected some channel events"); + let ChatEvent::Message(message) = &latest_event.event else { + panic!("Expected latest event to be a message: {latest_event:?}"); + }; + let types::MessageContent::Text(text_content) = &message.content else { + panic!("Expected message to be text"); + }; + assert_eq!(text_content.text, text); + assert!(!message.edited); + assert!(message.bot_context.is_some()); + assert!(!message.bot_context.as_ref().unwrap().finalised); + + // Call execute_bot_command as bot - finalised message + let text = "Hello world".to_string(); + let response = client::local_user_index::execute_bot_command( + env, + bot_principal, + canister_ids.local_user_index, + &local_user_index_canister::execute_bot_command::Args { + action: BotAction::SendMessage(BotMessageAction { + content: MessageContent::Text(TextContent { text: text.clone() }), + finalised: true, + }), jwt: access_token, }, ); @@ -150,6 +187,10 @@ fn bot_smoke_test() { panic!("Expected message to be text"); }; assert_eq!(text_content.text, text); + + assert!(message.edited); + assert!(message.bot_context.is_some()); + assert!(message.bot_context.as_ref().unwrap().finalised); } fn init_test_data(env: &mut PocketIc, canister_ids: &CanisterIds, controller: Principal) -> TestData { diff --git a/backend/libraries/chat_events/src/chat_event_internal.rs b/backend/libraries/chat_events/src/chat_event_internal.rs index ad874d18c9..41c4dff3af 100644 --- a/backend/libraries/chat_events/src/chat_event_internal.rs +++ b/backend/libraries/chat_events/src/chat_event_internal.rs @@ -4,13 +4,13 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::ops::DerefMut; use types::{ - is_default, AccessGate, AccessGateConfigInternal, AvatarChanged, BotAdded, BotRemoved, BotUpdated, ChannelId, Chat, ChatId, - CommunityId, DeletedBy, DirectChatCreated, EventIndex, EventWrapperInternal, EventsTimeToLiveUpdated, ExternalUrlUpdated, - GroupCreated, GroupDescriptionChanged, GroupFrozen, GroupGateUpdated, GroupInviteCodeChanged, GroupNameChanged, - GroupReplyContext, GroupRulesChanged, GroupUnfrozen, GroupVisibilityChanged, MemberJoinedInternal, MemberLeft, - MembersAdded, MembersAddedToDefaultChannel, MembersRemoved, Message, MessageContent, MessageId, MessageIndex, - MessagePinned, MessageUnpinned, MultiUserChat, PermissionsChanged, PushIfNotContains, Reaction, ReplyContext, RoleChanged, - ThreadSummary, TimestampMillis, Tips, UserId, UsersBlocked, UsersInvited, UsersUnblocked, + is_default, AccessGate, AccessGateConfigInternal, AvatarChanged, BotAdded, BotMessageContext, BotRemoved, BotUpdated, + ChannelId, Chat, ChatId, CommunityId, DeletedBy, DirectChatCreated, EventIndex, EventWrapperInternal, + EventsTimeToLiveUpdated, ExternalUrlUpdated, GroupCreated, GroupDescriptionChanged, GroupFrozen, GroupGateUpdated, + GroupInviteCodeChanged, GroupNameChanged, GroupReplyContext, GroupRulesChanged, GroupUnfrozen, GroupVisibilityChanged, + MemberJoinedInternal, MemberLeft, MembersAdded, MembersAddedToDefaultChannel, MembersRemoved, Message, MessageContent, + MessageId, MessageIndex, MessagePinned, MessageUnpinned, MultiUserChat, PermissionsChanged, PushIfNotContains, Reaction, + ReplyContext, RoleChanged, ThreadSummary, TimestampMillis, Tips, UserId, UsersBlocked, UsersInvited, UsersUnblocked, }; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -184,6 +184,8 @@ pub struct MessageInternal { pub sender: UserId, #[serde(rename = "c")] pub content: MessageContentInternal, + #[serde(rename = "bc", default, skip_serializing_if = "Option::is_none")] + pub bot_context: Option, #[serde(rename = "p", default, skip_serializing_if = "Option::is_none")] pub replies_to: Option, #[serde(rename = "r", default, skip_serializing_if = "Vec::is_empty")] @@ -213,6 +215,7 @@ impl MessageInternal { } else { self.content.hydrate(my_user_id) }, + bot_context: self.bot_context.clone(), replies_to: self.replies_to.as_ref().map(|r| r.hydrate()), reactions: self .reactions @@ -480,7 +483,7 @@ mod tests { }; use candid::Principal; use std::collections::HashSet; - use types::{EventWrapperInternal, Reaction, Tips}; + use types::{BotMessageContext, EventWrapperInternal, Reaction, Tips}; #[test] fn serialize_with_max_defaults() { @@ -489,6 +492,7 @@ mod tests { message_id: 1u64.into(), sender: Principal::from_text("4bkt6-4aaaa-aaaaf-aaaiq-cai").unwrap().into(), content: MessageContentInternal::Text(TextContentInternal { text: "123".to_string() }), + bot_context: None, replies_to: None, reactions: Vec::new(), tips: Tips::default(), @@ -528,6 +532,11 @@ mod tests { message_id: 1u64.into(), sender: principal.into(), content: MessageContentInternal::Text(TextContentInternal { text: "123".to_string() }), + bot_context: Some(BotMessageContext { + initiator: principal.into(), + command_text: "weather london".to_string(), + finalised: true, + }), replies_to: Some(ReplyContextInternal { chat_if_other: Some((ChatInternal::Group(principal.into()), Some(1.into()))), event_index: 1.into(), @@ -563,7 +572,7 @@ mod tests { let event_bytes = msgpack::serialize_then_unwrap(&event); let event_bytes_len = event_bytes.len(); - assert_eq!(message_bytes_len, 165); + assert_eq!(message_bytes_len, 230); assert_eq!(event_bytes_len, message_bytes_len + 18); 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 c8f10a758b..0700927230 100644 --- a/backend/libraries/chat_events/src/chat_events.rs +++ b/backend/libraries/chat_events/src/chat_events.rs @@ -19,15 +19,15 @@ use std::mem; use std::ops::DerefMut; use tracing::error; use types::{ - AcceptP2PSwapResult, CallParticipant, CancelP2PSwapResult, CanisterId, Chat, ChatType, CompleteP2PSwapResult, - CompletedCryptoTransaction, Cryptocurrency, DirectChatCreated, EventContext, EventIndex, EventWrapper, - EventWrapperInternal, EventsTimeToLiveUpdated, GroupCanisterThreadDetails, GroupCreated, GroupFrozen, GroupUnfrozen, Hash, - HydratedMention, Mention, Message, MessageContent, MessageContentInitial, MessageEditedEventPayload, MessageEventPayload, - MessageId, MessageIndex, MessageMatch, MessageReport, MessageTippedEventPayload, Milliseconds, MultiUserChat, - P2PSwapAccepted, P2PSwapCompleted, P2PSwapCompletedEventPayload, P2PSwapContent, P2PSwapStatus, PendingCryptoTransaction, - PollVotes, ProposalUpdate, PushEventResult, Reaction, ReactionAddedEventPayload, RegisterVoteResult, ReserveP2PSwapResult, - ReserveP2PSwapSuccess, TimestampMillis, TimestampNanos, Timestamped, Tips, UserId, VideoCall, VideoCallEndedEventPayload, - VideoCallParticipants, VideoCallPresence, VoteOperation, + AcceptP2PSwapResult, BotMessageContext, CallParticipant, CancelP2PSwapResult, CanisterId, Chat, ChatType, + CompleteP2PSwapResult, CompletedCryptoTransaction, Cryptocurrency, DirectChatCreated, EventContext, EventIndex, + EventWrapper, EventWrapperInternal, EventsTimeToLiveUpdated, GroupCanisterThreadDetails, GroupCreated, GroupFrozen, + GroupUnfrozen, Hash, HydratedMention, Mention, Message, MessageContent, MessageContentInitial, MessageEditedEventPayload, + MessageEventPayload, MessageId, MessageIndex, MessageMatch, MessageReport, MessageTippedEventPayload, Milliseconds, + MultiUserChat, P2PSwapAccepted, P2PSwapCompleted, P2PSwapCompletedEventPayload, P2PSwapContent, P2PSwapStatus, + PendingCryptoTransaction, PollVotes, ProposalUpdate, PushEventResult, Reaction, ReactionAddedEventPayload, + RegisterVoteResult, ReserveP2PSwapResult, ReserveP2PSwapSuccess, TimestampMillis, TimestampNanos, Timestamped, Tips, + UserId, VideoCall, VideoCallEndedEventPayload, VideoCallParticipants, VideoCallPresence, VoteOperation, }; #[derive(Serialize, Deserialize)] @@ -222,6 +222,7 @@ impl ChatEvents { message_id: args.message_id, sender: args.sender, content: args.content, + bot_context: args.bot_context, replies_to: args.replies_to, reactions: Vec::new(), tips: Tips::default(), @@ -362,6 +363,12 @@ impl ChatEvents { let already_edited = message.last_edited.is_some(); message.last_edited = Some(args.now); + if args.finalise_bot_message { + if let Some(bot_context) = message.bot_context.as_mut() { + bot_context.finalised = true; + } + } + if let Some(client) = event_store_client { let new_length = message.content.text_length(); let payload = MessageEditedEventPayload { @@ -1025,6 +1032,7 @@ impl ChatEvents { block_index: transaction.index(), prize_message: message_index, }), + bot_context: None, mentioned: Vec::new(), replies_to: None, forwarded: false, @@ -1500,6 +1508,7 @@ impl ChatEvents { notes, }], }), + bot_context: None, mentioned: Vec::new(), replies_to: Some(ReplyContextInternal { chat_if_other: Some((chat.into(), thread_root_message_index)), @@ -2231,6 +2240,7 @@ pub struct PushMessageArgs { pub thread_root_message_index: Option, pub message_id: MessageId, pub content: MessageContentInternal, + pub bot_context: Option, pub mentioned: Vec, pub replies_to: Option, pub forwarded: bool, @@ -2247,6 +2257,7 @@ pub struct EditMessageArgs { pub message_id: MessageId, pub content: MessageContentInitial, pub block_level_markdown: Option, + pub finalise_bot_message: bool, pub now: TimestampMillis, } diff --git a/backend/libraries/chat_events/src/chat_events_list.rs b/backend/libraries/chat_events/src/chat_events_list.rs index cd319fd298..743b5ccae1 100644 --- a/backend/libraries/chat_events/src/chat_events_list.rs +++ b/backend/libraries/chat_events/src/chat_events_list.rs @@ -777,6 +777,7 @@ mod tests { content: MessageContentInternal::Text(TextContentInternal { text: "hello".to_string(), }), + bot_context: None, mentioned: Vec::new(), replies_to: None, now, diff --git a/backend/libraries/chat_events/src/stable_memory/tests.rs b/backend/libraries/chat_events/src/stable_memory/tests.rs index 54dbaabdf7..f5bb480237 100644 --- a/backend/libraries/chat_events/src/stable_memory/tests.rs +++ b/backend/libraries/chat_events/src/stable_memory/tests.rs @@ -486,6 +486,7 @@ fn generate_value(content: MessageContentInternal) -> EventWrapperInternal, + message_id: MessageId, + content: MessageContentInitial, + replies_to: Option, + mentioned: &[UserId], + rules_accepted: Option, + suppressed: bool, + block_level_markdown: bool, + now: TimestampMillis, + ) -> SendMessageResult { + use SendMessageResult::*; + + let PrepareSendMessageSuccess { + min_visible_event_index, + everyone_mentioned, + } = match self.prepare_send_message( + caller, + thread_root_message_index, + &content.clone().into(), + rules_accepted, + now, + ) { + PrepareSendMessageResult::Success(success) => success, + PrepareSendMessageResult::UserLapsed => return UserLapsed, + PrepareSendMessageResult::UserSuspended => return UserSuspended, + PrepareSendMessageResult::UserNotInGroup => return UserNotInGroup, + PrepareSendMessageResult::RulesNotAccepted => return RulesNotAccepted, + PrepareSendMessageResult::NotAuthorized => return NotAuthorized, + }; + + let edit_message_args = EditMessageArgs { + sender: caller.agent(), + min_visible_event_index, + thread_root_message_index, + message_id, + content, + block_level_markdown: Some(block_level_markdown), + finalise_bot_message: true, + now, + }; + + self.events.edit_message::(edit_message_args, None); + + let reader = self + .events + .events_reader(min_visible_event_index, thread_root_message_index) + .unwrap(); + + let message_event = reader.message_event(message_id.into(), Some(caller.agent())).unwrap(); + + let users_to_notify = self.build_users_to_notify( + thread_root_message_index, + min_visible_event_index, + replies_to, + &message_event, + mentioned, + suppressed, + everyone_mentioned, + now, + ); + + SendMessageResult::Success(SendMessageSuccess { + message_event, + users_to_notify, + unfinalised_bot_message: false, + }) + } + + fn build_users_to_notify( + &mut self, + thread_root_message_index: Option, + min_visible_event_index: EventIndex, + replies_to: Option, + message_event: &EventWrapper, + mentioned: &[UserId], + everyone_mentioned: bool, + suppressed: bool, + now: TimestampMillis, + ) -> Vec { + let message = &message_event.event; + let message_index = message.message_index; + let sender = message.sender; + let message_id = message.message_id; + + let user_being_replied_to = replies_to + .as_ref() + .and_then(|r| self.get_user_being_replied_to(r, min_visible_event_index, thread_root_message_index)); let mentions: HashSet<_> = mentioned.iter().copied().chain(user_being_replied_to).collect(); @@ -785,10 +930,7 @@ impl GroupChatCore { users_to_notify.remove(user_id); } - Success(SendMessageSuccess { - message_event, - users_to_notify: users_to_notify.iter().copied().collect(), - }) + users_to_notify.iter().copied().collect() } fn prepare_send_message( @@ -2044,12 +2186,14 @@ pub enum SendMessageResult { UserSuspended, UserLapsed, RulesNotAccepted, + MessageAlreadyExists, InvalidRequest(String), } pub struct SendMessageSuccess { pub message_event: EventWrapper, pub users_to_notify: Vec, + pub unfinalised_bot_message: bool, } pub enum AddRemoveReactionResult { diff --git a/backend/libraries/types/can.did b/backend/libraries/types/can.did index fcc156280e..8c4069a743 100644 --- a/backend/libraries/types/can.did +++ b/backend/libraries/types/can.did @@ -586,6 +586,7 @@ type Message = record { message_id : MessageId; sender : UserId; content : MessageContent; + bot_context : opt BotMessageContext; replies_to : opt ReplyContext; reactions : vec record { text; vec UserId }; tips : vec record { CanisterId; vec record { UserId; nat } }; @@ -2178,4 +2179,10 @@ type BotGroupDetails = record { user_id : UserId; added_by : UserId; permissions : SlashCommandPermissions; -} +}; + +type BotMessageContext = record { + initiator : principal; + command_text : text; + finalised : bool; +}; diff --git a/backend/libraries/types/src/bot_actions.rs b/backend/libraries/types/src/bot_actions.rs index 087c10acf9..43eaa4e8d1 100644 --- a/backend/libraries/types/src/bot_actions.rs +++ b/backend/libraries/types/src/bot_actions.rs @@ -7,7 +7,13 @@ use serde::Serialize; #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub enum BotAction { - SendMessage(MessageContent), + SendMessage(BotMessageAction), +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct BotMessageAction { + pub content: MessageContent, + pub finalised: bool, } #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] @@ -26,8 +32,8 @@ impl BotAction { let mut permissions_required = SlashCommandPermissions::default(); match self { - BotAction::SendMessage(content) => { - let permission = match content { + BotAction::SendMessage(action) => { + let permission = match action.content { MessageContent::Text(_) => MessagePermission::Text, MessageContent::Image(_) => MessagePermission::Image, MessageContent::Video(_) => MessagePermission::Video, diff --git a/backend/libraries/types/src/c2c_handle_bot_action.rs b/backend/libraries/types/src/c2c_handle_bot_action.rs index 217023a1e5..d565a393bf 100644 --- a/backend/libraries/types/src/c2c_handle_bot_action.rs +++ b/backend/libraries/types/src/c2c_handle_bot_action.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] pub struct Args { pub bot: User, - pub commanded_by: UserId, + pub initiator: UserId, pub chat: Chat, pub thread_root_message_index: Option, pub message_id: MessageId, diff --git a/backend/libraries/types/src/caller.rs b/backend/libraries/types/src/caller.rs index 1adf7a6590..4a0ea36648 100644 --- a/backend/libraries/types/src/caller.rs +++ b/backend/libraries/types/src/caller.rs @@ -10,8 +10,10 @@ pub enum Caller { #[derive(Clone)] pub struct BotCaller { - pub user_id: UserId, - pub bot_id: UserId, + pub initiator: UserId, + pub bot: UserId, + pub command_text: String, + pub finalised: bool, } impl Caller { @@ -19,7 +21,7 @@ impl Caller { match self { Caller::User(user_id) => *user_id, Caller::Bot(user_id) => *user_id, - Caller::BotV2(bot_caller) => bot_caller.bot_id, + Caller::BotV2(bot_caller) => bot_caller.bot, Caller::OCBot(user_id) => *user_id, } } @@ -28,7 +30,7 @@ impl Caller { match self { Caller::User(user_id) => *user_id, Caller::Bot(user_id) => *user_id, - Caller::BotV2(bot_caller) => bot_caller.user_id, + Caller::BotV2(bot_caller) => bot_caller.initiator, Caller::OCBot(user_id) => *user_id, } } @@ -39,8 +41,8 @@ impl Caller { } impl From<&Caller> for UserType { - fn from(channel: &Caller) -> Self { - match channel { + fn from(caller: &Caller) -> Self { + match caller { Caller::User(_) => UserType::User, Caller::Bot(_) => UserType::Bot, Caller::BotV2(_) => UserType::BotV2, diff --git a/backend/libraries/types/src/claims.rs b/backend/libraries/types/src/claims.rs index 02329f5af8..abe5279a77 100644 --- a/backend/libraries/types/src/claims.rs +++ b/backend/libraries/types/src/claims.rs @@ -34,7 +34,7 @@ pub struct StartVideoCallClaims { #[derive(Serialize, Deserialize)] pub struct BotCommandClaims { - pub user_id: UserId, + pub initiator: UserId, pub bot: UserId, pub chat: Chat, pub thread_root_message_index: Option, diff --git a/backend/libraries/types/src/message.rs b/backend/libraries/types/src/message.rs index 0dffbbc778..c6c3cf248e 100644 --- a/backend/libraries/types/src/message.rs +++ b/backend/libraries/types/src/message.rs @@ -1,5 +1,6 @@ use crate::{ - Achievement, CanisterId, Chat, EventIndex, MessageContent, MessageId, MessageIndex, Reaction, ThreadSummary, UserId, + Achievement, BotCaller, CanisterId, Chat, EventIndex, MessageContent, MessageId, MessageIndex, Reaction, ThreadSummary, + UserId, }; use candid::CandidType; use serde::{Deserialize, Serialize}; @@ -13,6 +14,7 @@ pub struct Message { pub message_id: MessageId, pub sender: UserId, pub content: MessageContent, + pub bot_context: Option, pub replies_to: Option, pub reactions: Vec<(Reaction, Vec)>, pub tips: Tips, @@ -243,3 +245,21 @@ pub struct P2PSwapContentEventPayload { pub type DeletedContentEventPayload = (); pub type VideoCallContentEventPayload = (); pub type CustomContentEventPayload = (); + +#[ts_export] +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct BotMessageContext { + pub initiator: UserId, + pub command_text: String, + pub finalised: bool, +} + +impl From<&BotCaller> for BotMessageContext { + fn from(caller: &BotCaller) -> Self { + BotMessageContext { + initiator: caller.initiator, + command_text: caller.command_text.clone(), + finalised: caller.finalised, + } + } +}