diff --git a/Cargo.lock b/Cargo.lock index 1c71c7f997..92ffcc48ab 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/airdrop_bot/CHANGELOG.md b/backend/canisters/airdrop_bot/CHANGELOG.md index 47ee6ef49b..2bd28b73ae 100644 --- a/backend/canisters/airdrop_bot/CHANGELOG.md +++ b/backend/canisters/airdrop_bot/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1510](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1510-airdrop_bot)] - 2024-12-13 + ### Changed - Make `ChannelId` comparisons use their 32bit representation ([#6885](https://github.com/open-chat-labs/open-chat/pull/6885)) diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index 52e8e2d17d..2edaa0405b 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### 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 + +- Fix `RoleChanged` events which were serialized under the wrong name (part 2) ([#7059](https://github.com/open-chat-labs/open-chat/pull/7059)) + +## [[2.0.1515](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1515-community)] - 2024-12-13 + ### Added - Update community bot config ([#7018](https://github.com/open-chat-labs/open-chat/pull/7018)) 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 1894a1a30e..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 } } @@ -409,7 +404,6 @@ struct Data { user_cache: UserCache, user_event_sync_queue: GroupedTimerJobQueue, stable_memory_keys_to_garbage_collect: Vec, - #[serde(default)] bots: GroupBots, } diff --git a/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs index 56a4a253e0..0778503cb5 100644 --- a/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs @@ -21,10 +21,10 @@ fn post_upgrade(args: Args) { let (mut data, errors, logs, traces): (Data, Vec, Vec, Vec) = msgpack::deserialize(reader).unwrap(); - canister_logger::init_with_logs(data.test_mode, errors, logs, traces); - data.members.move_member_ids_into_channel_links_map(); data.events.fix_role_changed_events(); + canister_logger::init_with_logs(data.test_mode, errors, logs, traces); + let env = init_env(data.rng_seed); init_state(env, data, args.wasm_version); diff --git a/backend/canisters/community/impl/src/model/events.rs b/backend/canisters/community/impl/src/model/events.rs index 75dfa382d5..d55775a6bb 100644 --- a/backend/canisters/community/impl/src/model/events.rs +++ b/backend/canisters/community/impl/src/model/events.rs @@ -4,10 +4,11 @@ use serde::{Deserialize, Serialize}; use tracing::info; use types::{ AvatarChanged, BannerChanged, BotAdded, BotRemoved, BotUpdated, ChannelDeleted, ChannelId, ChatId, CommunityMembersRemoved, - CommunityPermissionsChanged, CommunityRoleChanged, CommunityUsersBlocked, CommunityVisibilityChanged, EventIndex, + CommunityPermissionsChanged, CommunityRole, CommunityUsersBlocked, CommunityVisibilityChanged, EventIndex, EventWrapperInternal, GroupCreated, GroupDescriptionChanged, GroupFrozen, GroupInviteCodeChanged, GroupNameChanged, - GroupRulesChanged, GroupUnfrozen, PrimaryLanguageChanged, TimestampMillis, UserId, UsersInvited, UsersUnblocked, + GroupUnfrozen, PrimaryLanguageChanged, TimestampMillis, UserId, UsersInvited, UsersUnblocked, }; +use user_canister::token_swap_status::CandidType; mod stable_memory; @@ -75,7 +76,7 @@ pub enum CommunityEventInternal { #[serde(rename = "dc", alias = "DescriptionChanged")] DescriptionChanged(Box), #[serde(rename = "rc", alias = "RulesChanged")] - RulesChanged(Box), + RulesChanged(Box), #[serde(rename = "ac", alias = "AvatarChanged")] AvatarChanged(Box), #[serde(rename = "bc", alias = "BannerChanged")] @@ -85,7 +86,7 @@ pub enum CommunityEventInternal { #[serde(rename = "mr", alias = "MembersRemoved")] MembersRemoved(Box), #[serde(rename = "rl", alias = "RoleChanged")] - RoleChanged(Box), + RoleChanged(Box), #[serde(rename = "ub", alias = "UsersBlocked")] UsersBlocked(Box), #[serde(rename = "uu", alias = "UsersUnblocked")] @@ -123,6 +124,21 @@ pub enum RulesChangedOrRoleChanged { RoleChanged(CommunityRoleChanged), } +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct GroupRulesChanged { + pub enabled: bool, + pub prev_enabled: bool, + pub changed_by: UserId, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct CommunityRoleChanged { + pub user_ids: Vec, + pub changed_by: UserId, + pub old_role: CommunityRole, + pub new_role: CommunityRole, +} + impl CommunityEvents { pub fn fix_role_changed_events(&mut self) { let mut count_updated = 0; @@ -134,7 +150,12 @@ impl CommunityEvents { timestamp: event_wrapper.timestamp, correlation_id: 0, expires_at: event_wrapper.expires_at, - event: CommunityEventInternal::RoleChanged(Box::new(role)), + event: CommunityEventInternal::RoleChanged(Box::new(types::CommunityRoleChanged { + user_ids: role.user_ids, + changed_by: role.changed_by, + old_role: role.old_role, + new_role: role.new_role, + })), }); count_updated += 1; } diff --git a/backend/canisters/community/impl/src/model/members.rs b/backend/canisters/community/impl/src/model/members.rs index 90be6d342b..3c06587d4f 100644 --- a/backend/canisters/community/impl/src/model/members.rs +++ b/backend/canisters/community/impl/src/model/members.rs @@ -3,13 +3,11 @@ use crate::model::user_groups::{UserGroup, UserGroups}; use candid::Principal; use constants::calculate_summary_updates_data_removal_cutoff; use group_community_common::{Member, MemberUpdate, Members}; -use principal_to_user_id_map::{deserialize_principal_to_user_id_map_from_heap, PrincipalToUserIdMap}; +use principal_to_user_id_map::PrincipalToUserIdMap; use rand::RngCore; -use serde::de::{SeqAccess, Visitor}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use std::collections::btree_map::Entry::Vacant; use std::collections::{BTreeMap, BTreeSet}; -use std::fmt::Formatter; use types::{ is_default, ChannelId, CommunityMember, CommunityPermissions, CommunityRole, PushIfNotContains, TimestampMillis, Timestamped, UserId, UserType, Version, @@ -24,15 +22,11 @@ const MAX_MEMBERS_PER_COMMUNITY: u32 = 100_000; #[derive(Serialize, Deserialize)] pub struct CommunityMembers { members_map: MembersStableStorage, - #[serde(alias = "member_channel_links", deserialize_with = "deserialize_members_and_channels")] members_and_channels: BTreeMap>, member_channel_links_removed: BTreeMap<(UserId, ChannelId), TimestampMillis>, user_groups: UserGroups, // This includes the userIds of community members and also users invited to the community - #[serde(deserialize_with = "deserialize_principal_to_user_id_map_from_heap")] principal_to_user_id_map: PrincipalToUserIdMap, - #[deprecated] - member_ids: Vec, owners: BTreeSet, admins: BTreeSet, bots: BTreeMap, @@ -46,15 +40,6 @@ pub struct CommunityMembers { } impl CommunityMembers { - pub fn move_member_ids_into_channel_links_map(&mut self) { - #[allow(deprecated)] - for user_id in std::mem::take(&mut self.member_ids) { - if let Vacant(e) = self.members_and_channels.entry(user_id) { - e.insert(Vec::new()); - } - } - } - pub fn new( creator_principal: Principal, creator_user_id: UserId, @@ -86,7 +71,6 @@ impl CommunityMembers { member_channel_links_removed: BTreeMap::new(), user_groups: UserGroups::default(), principal_to_user_id_map, - member_ids: Vec::new(), owners: [creator_user_id].into_iter().collect(), admins: BTreeSet::new(), bots: if creator_user_type.is_bot() { @@ -839,31 +823,6 @@ impl From<&CommunityMemberInternal> for CommunityMember { } } -struct MemberChannelLinksVisitor; - -impl<'de> Visitor<'de> for MemberChannelLinksVisitor { - type Value = BTreeMap>; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("a sequence") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut map: BTreeMap> = BTreeMap::new(); - while let Some((user_id, channel_id)) = seq.next_element()? { - map.entry(user_id).or_default().push(channel_id); - } - Ok(map) - } -} - -fn deserialize_members_and_channels<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { - d.deserialize_seq(MemberChannelLinksVisitor) -} - #[cfg(test)] mod tests { use super::*; diff --git a/backend/canisters/community/impl/src/timer_job_types.rs b/backend/canisters/community/impl/src/timer_job_types.rs index 638e36d6c6..2c3c1cfaa5 100644 --- a/backend/canisters/community/impl/src/timer_job_types.rs +++ b/backend/canisters/community/impl/src/timer_job_types.rs @@ -447,6 +447,9 @@ impl Job for MarkP2PSwapExpiredJob { impl Job for MarkVideoCallEndedJob { fn execute(self) { - mutate_state(|state| end_video_call_impl(self.0, state)); + let response = mutate_state(|state| end_video_call_impl(self.0.clone(), state)); + if !matches!(response, community_canister::end_video_call::Response::Success) { + error!(?response, args = ?self.0, "Failed to mark video call ended"); + } } } 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/cycles_dispenser/CHANGELOG.md b/backend/canisters/cycles_dispenser/CHANGELOG.md index f03a49bb88..3db306b91f 100644 --- a/backend/canisters/cycles_dispenser/CHANGELOG.md +++ b/backend/canisters/cycles_dispenser/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1511](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1511-cycles_dispenser)] - 2024-12-13 + ### Changed - Expose size of each virtual stable memory in metrics ([#6981](https://github.com/open-chat-labs/open-chat/pull/6981)) diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index 635f114360..ba7b54af77 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### 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 + ### Added - Update group bot config ([#7018](https://github.com/open-chat-labs/open-chat/pull/7018)) 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 5d858b678b..0a0c46b02e 100644 --- a/backend/canisters/group/impl/src/lib.rs +++ b/backend/canisters/group/impl/src/lib.rs @@ -22,7 +22,7 @@ use instruction_counts_log::{InstructionCountEntry, InstructionCountFunctionId, use model::user_event_batch::UserEventBatch; use msgpack::serialize_then_unwrap; use notifications_canister::c2c_push_notification; -use principal_to_user_id_map::{deserialize_principal_to_user_id_map_from_heap, PrincipalToUserIdMap}; +use principal_to_user_id_map::PrincipalToUserIdMap; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use stable_memory_map::{BaseKeyPrefix, ChatEventKeyPrefix}; @@ -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,40 +442,36 @@ 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 } } #[derive(Serialize, Deserialize)] struct Data { pub chat: GroupChatCore, - #[serde(deserialize_with = "deserialize_principal_to_user_id_map_from_heap")] pub principal_to_user_id_map: PrincipalToUserIdMap, pub group_index_canister_id: CanisterId, pub local_group_index_canister_id: CanisterId, 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/timer_job_types.rs b/backend/canisters/group/impl/src/timer_job_types.rs index 55ff5c02cb..05e108e230 100644 --- a/backend/canisters/group/impl/src/timer_job_types.rs +++ b/backend/canisters/group/impl/src/timer_job_types.rs @@ -386,6 +386,9 @@ impl Job for MarkP2PSwapExpiredJob { impl Job for MarkVideoCallEndedJob { fn execute(self) { - mutate_state(|state| end_video_call_impl(self.0, state)); + let response = mutate_state(|state| end_video_call_impl(self.0.clone(), state)); + if !matches!(response, group_canister::end_video_call::Response::Success) { + error!(?response, args = ?self.0, "Failed to mark video call ended"); + } } } 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/group_index/CHANGELOG.md b/backend/canisters/group_index/CHANGELOG.md index 195f546947..34558b32bd 100644 --- a/backend/canisters/group_index/CHANGELOG.md +++ b/backend/canisters/group_index/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1509](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1509-group_index)] - 2024-12-13 + ### Changed - Expose size of each virtual stable memory in metrics ([#6981](https://github.com/open-chat-labs/open-chat/pull/6981)) diff --git a/backend/canisters/local_group_index/CHANGELOG.md b/backend/canisters/local_group_index/CHANGELOG.md index 6138510a9b..a6230eb7cf 100644 --- a/backend/canisters/local_group_index/CHANGELOG.md +++ b/backend/canisters/local_group_index/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1514](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1514-local_group_index)] - 2024-12-13 + ### Added - Expose the cycles top-ups of Group/Community canisters ([#7053](https://github.com/open-chat-labs/open-chat/pull/7053)) diff --git a/backend/canisters/local_user_index/CHANGELOG.md b/backend/canisters/local_user_index/CHANGELOG.md index c293329236..682fe5cae7 100644 --- a/backend/canisters/local_user_index/CHANGELOG.md +++ b/backend/canisters/local_user_index/CHANGELOG.md @@ -6,6 +6,12 @@ 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 - Expose the cycles top-ups of User canisters ([#7053](https://github.com/open-chat-labs/open-chat/pull/7053)) diff --git a/backend/canisters/local_user_index/impl/src/lib.rs b/backend/canisters/local_user_index/impl/src/lib.rs index 9f9b1e56c4..55de583b69 100644 --- a/backend/canisters/local_user_index/impl/src/lib.rs +++ b/backend/canisters/local_user_index/impl/src/lib.rs @@ -289,7 +289,6 @@ impl RuntimeState { struct Data { pub local_users: LocalUserMap, pub global_users: GlobalUserMap, - #[serde(default)] pub bots: BotsMap, pub child_canister_wasms: ChildCanisterWasms, pub user_index_canister_id: CanisterId, @@ -300,7 +299,6 @@ struct Data { pub cycles_dispenser_canister_id: CanisterId, pub escrow_canister_id: CanisterId, pub internet_identity_canister_id: CanisterId, - #[serde(default = "website_canister_id")] pub website_canister_id: CanisterId, pub canisters_requiring_upgrade: CanistersRequiringUpgrade, pub canister_pool: canister::Pool, @@ -324,10 +322,6 @@ struct Data { pub cycles_balance_check_queue: VecDeque, } -fn website_canister_id() -> CanisterId { - CanisterId::from_text("6hsbt-vqaaa-aaaaf-aaafq-cai").unwrap() -} - #[derive(Serialize, Deserialize)] pub struct FailedMessageUsers { pub sender: UserId, diff --git a/backend/canisters/local_user_index/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/local_user_index/impl/src/lifecycle/post_upgrade.rs index 3d6596f31c..8d8adf6c53 100644 --- a/backend/canisters/local_user_index/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/local_user_index/impl/src/lifecycle/post_upgrade.rs @@ -1,13 +1,12 @@ use crate::lifecycle::{init_env, init_state}; use crate::memory::{get_stable_memory_map_memory, get_upgrades_memory}; -use crate::{mutate_state, Data}; +use crate::Data; use canister_logger::LogEntry; use canister_tracing_macros::trace; use ic_cdk::post_upgrade; use local_user_index_canister::post_upgrade::Args; use stable_memory::get_reader; use tracing::info; -use types::CanisterId; use utils::cycles::init_cycles_dispenser_client; #[post_upgrade] @@ -28,10 +27,4 @@ fn post_upgrade(args: Args) { init_state(env, data, args.wasm_version); info!(version = %args.wasm_version, "Post-upgrade complete"); - - mutate_state(|state| { - if state.data.test_mode { - state.data.website_canister_id = CanisterId::from_text("pfs7b-iqaaa-aaaaf-abs7q-cai").unwrap(); - } - }) } diff --git a/backend/canisters/local_user_index/impl/src/model/global_user_map.rs b/backend/canisters/local_user_index/impl/src/model/global_user_map.rs index dd29eb6554..91b7230f26 100644 --- a/backend/canisters/local_user_index/impl/src/model/global_user_map.rs +++ b/backend/canisters/local_user_index/impl/src/model/global_user_map.rs @@ -1,6 +1,6 @@ use candid::Principal; use local_user_index_canister::GlobalUser; -use principal_to_user_id_map::{deserialize_principal_to_user_id_map_from_heap, PrincipalToUserIdMap}; +use principal_to_user_id_map::PrincipalToUserIdMap; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use types::{TimestampMillis, UniquePersonProof, UserId, UserType}; @@ -8,11 +8,9 @@ use types::{TimestampMillis, UniquePersonProof, UserId, UserType}; #[derive(Serialize, Deserialize, Default)] pub struct GlobalUserMap { user_id_to_principal: HashMap, - #[serde(deserialize_with = "deserialize_principal_to_user_id_map_from_heap")] principal_to_user_id: PrincipalToUserIdMap, unique_person_proofs: HashMap, platform_moderators: HashSet, - #[serde(alias = "bots")] legacy_bots: HashSet, oc_controlled_bot_users: HashSet, diamond_membership_expiry_dates: HashMap, 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, Vec) = msgpack::deserialize(reader).unwrap(); + let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = + msgpack::deserialize(reader).unwrap(); - // TODO: After release change this to - // let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = msgpack::deserialize(reader).unwrap(); - canister_logger::init_with_logs(data.test_mode, Vec::new(), logs, traces); + canister_logger::init_with_logs(data.test_mode, errors, logs, traces); let env = init_env(data.rng_seed); init_cycles_dispenser_client(data.cycles_dispenser_canister_id, data.test_mode); diff --git a/backend/canisters/notifications_index/CHANGELOG.md b/backend/canisters/notifications_index/CHANGELOG.md index 4a9ed8fdb4..ddc6f537ce 100644 --- a/backend/canisters/notifications_index/CHANGELOG.md +++ b/backend/canisters/notifications_index/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1518](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1518-notifications_index)] - 2024-12-13 + ### Added - Add an error log with http endpoint ([#6608](https://github.com/open-chat-labs/open-chat/pull/6608)) diff --git a/backend/canisters/notifications_index/impl/src/lib.rs b/backend/canisters/notifications_index/impl/src/lib.rs index ba9e0d538d..d40336a4f6 100644 --- a/backend/canisters/notifications_index/impl/src/lib.rs +++ b/backend/canisters/notifications_index/impl/src/lib.rs @@ -3,7 +3,7 @@ use crate::model::subscriptions::Subscriptions; use candid::Principal; use canister_state_macros::canister_state; use notifications_index_canister::{NotificationsIndexEvent, SubscriptionAdded, SubscriptionRemoved}; -use principal_to_user_id_map::{deserialize_principal_to_user_id_map_from_heap, PrincipalToUserIdMap}; +use principal_to_user_id_map::PrincipalToUserIdMap; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -119,10 +119,6 @@ struct Data { pub push_service_principals: HashSet, pub user_index_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, - #[serde( - alias = "principal_to_user_id", - deserialize_with = "deserialize_principal_to_user_id_map_from_heap" - )] pub principal_to_user_id_map: PrincipalToUserIdMap, pub subscriptions: Subscriptions, pub notifications_canister_wasm_for_new_canisters: CanisterWasm, diff --git a/backend/canisters/notifications_index/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/notifications_index/impl/src/lifecycle/post_upgrade.rs index 506655f4be..cd384aa0a5 100644 --- a/backend/canisters/notifications_index/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/notifications_index/impl/src/lifecycle/post_upgrade.rs @@ -17,11 +17,10 @@ fn post_upgrade(args: Args) { let memory = get_upgrades_memory(); let reader = get_reader(&memory); - let (data, logs, traces): (Data, Vec, Vec) = msgpack::deserialize(reader).unwrap(); + let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = + msgpack::deserialize(reader).unwrap(); - // TODO: After release change this to - // let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = msgpack::deserialize(reader).unwrap(); - canister_logger::init_with_logs(data.test_mode, Vec::new(), logs, traces); + canister_logger::init_with_logs(data.test_mode, errors, logs, traces); let env = init_env(data.rng_seed); init_cycles_dispenser_client(data.cycles_dispenser_canister_id, data.test_mode); diff --git a/backend/canisters/online_users/CHANGELOG.md b/backend/canisters/online_users/CHANGELOG.md index 86d09d5c36..ac7ae2ba39 100644 --- a/backend/canisters/online_users/CHANGELOG.md +++ b/backend/canisters/online_users/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1517](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1517-online_users)] - 2024-12-13 + ### Added - Reinstate some candid endpoints ([#6468](https://github.com/open-chat-labs/open-chat/pull/6468)) diff --git a/backend/canisters/online_users/impl/src/lib.rs b/backend/canisters/online_users/impl/src/lib.rs index b402e9ab98..9d16a5c6f4 100644 --- a/backend/canisters/online_users/impl/src/lib.rs +++ b/backend/canisters/online_users/impl/src/lib.rs @@ -66,7 +66,6 @@ impl RuntimeState { #[derive(Serialize, Deserialize)] struct Data { pub last_online_dates: LastOnlineDates, - #[serde(skip_deserializing)] pub principal_to_user_id_map: PrincipalToUserIdMap, pub user_index_canister_id: CanisterId, pub cycles_dispenser_canister_id: CanisterId, diff --git a/backend/canisters/online_users/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/online_users/impl/src/lifecycle/post_upgrade.rs index fb3465f30e..adb78fcd76 100644 --- a/backend/canisters/online_users/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/online_users/impl/src/lifecycle/post_upgrade.rs @@ -17,11 +17,10 @@ fn post_upgrade(args: Args) { let memory = get_upgrades_memory(); let reader = get_reader(&memory); - let (data, logs, traces): (Data, Vec, Vec) = msgpack::deserialize(reader).unwrap(); + let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = + msgpack::deserialize(reader).unwrap(); - // TODO: After release change this to - // let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = msgpack::deserialize(reader).unwrap(); - canister_logger::init_with_logs(data.test_mode, Vec::new(), logs, traces); + canister_logger::init_with_logs(data.test_mode, errors, logs, traces); let env = init_env(data.rng_seed); init_cycles_dispenser_client(data.cycles_dispenser_canister_id, data.test_mode); diff --git a/backend/canisters/storage_bucket/CHANGELOG.md b/backend/canisters/storage_bucket/CHANGELOG.md index 7315125904..56ac3e7d26 100644 --- a/backend/canisters/storage_bucket/CHANGELOG.md +++ b/backend/canisters/storage_bucket/CHANGELOG.md @@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed +- Push any remaining events still queued in the old events system ([#7065](https://github.com/open-chat-labs/open-chat/pull/7065)) + +## [[2.0.1522](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1522-storage_bucket)] - 2024-12-16 + +### Changed + - Expose size of each virtual stable memory in metrics ([#6981](https://github.com/open-chat-labs/open-chat/pull/6981)) - Avoid having to regenerate rng seed after each upgrade ([#7043](https://github.com/open-chat-labs/open-chat/pull/7043)) - Use `GroupedTimerJobQueue` to sync events to storage index ([#7047](https://github.com/open-chat-labs/open-chat/pull/7047)) diff --git a/backend/canisters/storage_bucket/impl/src/jobs/remove_expired_files.rs b/backend/canisters/storage_bucket/impl/src/jobs/remove_expired_files.rs index 296dea024d..ce8f242c40 100644 --- a/backend/canisters/storage_bucket/impl/src/jobs/remove_expired_files.rs +++ b/backend/canisters/storage_bucket/impl/src/jobs/remove_expired_files.rs @@ -1,4 +1,4 @@ -use crate::model::index_sync_state::EventToSync; +use crate::model::index_event_batch::EventToSync; use crate::{mutate_state, RuntimeState}; use ic_cdk_timers::TimerId; use std::cell::Cell; diff --git a/backend/canisters/storage_bucket/impl/src/lib.rs b/backend/canisters/storage_bucket/impl/src/lib.rs index b2103daba8..b740718e8a 100644 --- a/backend/canisters/storage_bucket/impl/src/lib.rs +++ b/backend/canisters/storage_bucket/impl/src/lib.rs @@ -1,6 +1,6 @@ use crate::model::files::{Files, RemoveFileResult}; -use crate::model::index_event_batch::IndexEventBatch; -use crate::model::index_sync_state::{EventToSync, IndexSyncState}; +use crate::model::index_event_batch::{EventToSync, IndexEventBatch}; +use crate::model::index_sync_state::IndexSyncState; use crate::model::users::Users; use candid::{CandidType, Principal}; use canister_state_macros::canister_state; diff --git a/backend/canisters/storage_bucket/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/storage_bucket/impl/src/lifecycle/post_upgrade.rs index 7b3b2c2de6..caa9c2bb15 100644 --- a/backend/canisters/storage_bucket/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/storage_bucket/impl/src/lifecycle/post_upgrade.rs @@ -1,5 +1,6 @@ use crate::lifecycle::{init_env, init_state}; use crate::memory::get_upgrades_memory; +use crate::model::index_event_batch::EventToSync; use crate::Data; use canister_logger::LogEntry; use canister_tracing_macros::trace; @@ -14,9 +15,26 @@ fn post_upgrade(args: Args) { let memory = get_upgrades_memory(); let reader = get_reader(&memory); - let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = + let (mut data, errors, logs, traces): (Data, Vec, Vec, Vec) = msgpack::deserialize(reader).unwrap(); + data.index_event_sync_queue.set_defer_processing(true); + #[allow(deprecated)] + { + if let Some(args) = data.index_sync_state.args_to_retry.take() { + for added in args.files_added { + data.push_event_to_index(EventToSync::FileAdded(added)); + } + for removed in args.files_removed { + data.push_event_to_index(EventToSync::FileRemoved(removed)); + } + } + for event in std::mem::take(&mut data.index_sync_state.queue) { + data.push_event_to_index(event) + } + } + data.index_event_sync_queue.set_defer_processing(false); + canister_logger::init_with_logs(data.test_mode, errors, logs, traces); let env = init_env(data.rng_seed); diff --git a/backend/canisters/storage_bucket/impl/src/model/index_event_batch.rs b/backend/canisters/storage_bucket/impl/src/model/index_event_batch.rs index 05e5233bbc..d59f3f0e66 100644 --- a/backend/canisters/storage_bucket/impl/src/model/index_event_batch.rs +++ b/backend/canisters/storage_bucket/impl/src/model/index_event_batch.rs @@ -1,8 +1,9 @@ -use crate::model::index_sync_state::EventToSync; use crate::model::users::FileStatusInternal; use crate::{mutate_state, DATA_LIMIT_BYTES, MAX_EVENTS_TO_SYNC_PER_BATCH}; +use candid::Deserialize; +use serde::Serialize; use timer_job_queues::{TimerJobItem, TimerJobItemGroup}; -use types::CanisterId; +use types::{CanisterId, FileAdded, FileRemoved}; use utils::canister::should_retry_failed_c2c_call; pub struct IndexEventBatch { @@ -10,6 +11,12 @@ pub struct IndexEventBatch { events: Vec<(EventToSync, u64)>, } +#[derive(Serialize, Deserialize)] +pub enum EventToSync { + FileAdded(FileAdded), + FileRemoved(FileRemoved), +} + impl TimerJobItem for IndexEventBatch { async fn process(&self) -> Result<(), bool> { let mut args = storage_index_canister::c2c_sync_bucket::Args::default(); diff --git a/backend/canisters/storage_bucket/impl/src/model/index_sync_state.rs b/backend/canisters/storage_bucket/impl/src/model/index_sync_state.rs index 8154f80f87..376414eba3 100644 --- a/backend/canisters/storage_bucket/impl/src/model/index_sync_state.rs +++ b/backend/canisters/storage_bucket/impl/src/model/index_sync_state.rs @@ -1,22 +1,13 @@ +use crate::model::index_event_batch::EventToSync; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use storage_index_canister::c2c_sync_bucket::Args; -use types::{FileAdded, FileRemoved}; // We want to send events to the index in order, so while a sync is in progress we avoid sending // more events in case the first batch fails and the second succeeds. If a sync fails, the args that // were sent are stored so that they can be retried again. #[derive(Serialize, Deserialize, Default)] pub struct IndexSyncState { - queue: VecDeque, - in_progress: bool, - args_to_retry: Option, -} - -impl IndexSyncState {} - -#[derive(Serialize, Deserialize)] -pub enum EventToSync { - FileAdded(FileAdded), - FileRemoved(FileRemoved), + pub queue: VecDeque, + pub args_to_retry: Option, } diff --git a/backend/canisters/storage_bucket/impl/src/updates/c2c_sync_index.rs b/backend/canisters/storage_bucket/impl/src/updates/c2c_sync_index.rs index 08d2fa318d..9f8de7f092 100644 --- a/backend/canisters/storage_bucket/impl/src/updates/c2c_sync_index.rs +++ b/backend/canisters/storage_bucket/impl/src/updates/c2c_sync_index.rs @@ -1,6 +1,6 @@ use crate::guards::caller_is_storage_index_canister; use crate::model::files::RemoveFileResult; -use crate::model::index_sync_state::EventToSync; +use crate::model::index_event_batch::EventToSync; use crate::{mutate_state, RuntimeState, MAX_EVENTS_TO_SYNC_PER_BATCH}; use canister_tracing_macros::trace; use ic_cdk::update; diff --git a/backend/canisters/storage_bucket/impl/src/updates/forward_file.rs b/backend/canisters/storage_bucket/impl/src/updates/forward_file.rs index 467d89153e..9c82cd64d2 100644 --- a/backend/canisters/storage_bucket/impl/src/updates/forward_file.rs +++ b/backend/canisters/storage_bucket/impl/src/updates/forward_file.rs @@ -1,6 +1,6 @@ use crate::guards::caller_is_known_user; use crate::model::files::ForwardFileResult; -use crate::model::index_sync_state::EventToSync; +use crate::model::index_event_batch::EventToSync; use crate::model::users::{FileStatusInternal, IndexSyncComplete}; use crate::{mutate_state, RuntimeState}; use canister_tracing_macros::trace; diff --git a/backend/canisters/storage_bucket/impl/src/updates/upload_chunk.rs b/backend/canisters/storage_bucket/impl/src/updates/upload_chunk.rs index 8c3d94d79b..58de50767c 100644 --- a/backend/canisters/storage_bucket/impl/src/updates/upload_chunk.rs +++ b/backend/canisters/storage_bucket/impl/src/updates/upload_chunk.rs @@ -1,6 +1,6 @@ use crate::guards::caller_is_known_user; use crate::model::files::{PutChunkArgs, PutChunkResult}; -use crate::model::index_sync_state::EventToSync; +use crate::model::index_event_batch::EventToSync; use crate::model::users::{FileStatusInternal, IndexSyncComplete}; use crate::{mutate_state, RuntimeState}; use canister_tracing_macros::trace; diff --git a/backend/canisters/storage_index/CHANGELOG.md b/backend/canisters/storage_index/CHANGELOG.md index 9977945e5e..9527092911 100644 --- a/backend/canisters/storage_index/CHANGELOG.md +++ b/backend/canisters/storage_index/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1521](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1521-storage_index)] - 2024-12-16 + ### Changed - Update the canister creation fee to 0.5T ([#6700](https://github.com/open-chat-labs/open-chat/pull/6700)) diff --git a/backend/canisters/storage_index/impl/src/lib.rs b/backend/canisters/storage_index/impl/src/lib.rs index d573fb2b32..a74c7e1fb7 100644 --- a/backend/canisters/storage_index/impl/src/lib.rs +++ b/backend/canisters/storage_index/impl/src/lib.rs @@ -1,5 +1,4 @@ -use crate::model::bucket_event_batch::BucketEventBatch; -use crate::model::bucket_sync_state::EventToSync; +use crate::model::bucket_event_batch::{BucketEventBatch, EventToSync}; use crate::model::buckets::{BucketRecord, Buckets}; use crate::model::files::Files; use candid::{CandidType, Principal}; @@ -104,7 +103,6 @@ struct Data { pub users: HashMap, pub files: Files, pub buckets: Buckets, - #[serde(default = "bucket_event_sync_queue")] pub bucket_event_sync_queue: GroupedTimerJobQueue, pub canisters_requiring_upgrade: CanistersRequiringUpgrade, pub total_cycles_spent_on_canisters: Cycles, @@ -113,10 +111,6 @@ struct Data { pub test_mode: bool, } -fn bucket_event_sync_queue() -> GroupedTimerJobQueue { - GroupedTimerJobQueue::new(5, false) -} - impl Data { fn new( user_controllers: Vec, diff --git a/backend/canisters/storage_index/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/storage_index/impl/src/lifecycle/post_upgrade.rs index c115414511..65ddf3f852 100644 --- a/backend/canisters/storage_index/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/storage_index/impl/src/lifecycle/post_upgrade.rs @@ -14,17 +14,9 @@ fn post_upgrade(args: Args) { let memory = get_upgrades_memory(); let reader = get_reader(&memory); - let (mut data, errors, logs, traces): (Data, Vec, Vec, Vec) = + let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = msgpack::deserialize(reader).unwrap(); - data.bucket_event_sync_queue.set_defer_processing(true); - for bucket in data.buckets.iter_mut() { - #[allow(deprecated)] - data.bucket_event_sync_queue - .push_many(bucket.canister_id, bucket.sync_state.take()); - } - data.bucket_event_sync_queue.set_defer_processing(false); - canister_logger::init_with_logs(data.test_mode, errors, logs, traces); let env = init_env(data.rng_seed); diff --git a/backend/canisters/storage_index/impl/src/model/bucket_event_batch.rs b/backend/canisters/storage_index/impl/src/model/bucket_event_batch.rs index 27a4618475..6985fffcc1 100644 --- a/backend/canisters/storage_index/impl/src/model/bucket_event_batch.rs +++ b/backend/canisters/storage_index/impl/src/model/bucket_event_batch.rs @@ -1,7 +1,8 @@ -use crate::model::bucket_sync_state::EventToSync; use crate::MAX_EVENTS_TO_SYNC_PER_BATCH; +use candid::Principal; +use serde::{Deserialize, Serialize}; use timer_job_queues::{TimerJobItem, TimerJobItemGroup}; -use types::CanisterId; +use types::{AccessorId, CanisterId, FileId}; use utils::canister::should_retry_failed_c2c_call; pub struct BucketEventBatch { @@ -9,6 +10,15 @@ pub struct BucketEventBatch { events: Vec, } +#[derive(Serialize, Deserialize, Clone)] +pub enum EventToSync { + UserAdded(Principal), + UserRemoved(Principal), + AccessorRemoved(AccessorId), + UserIdUpdated(Principal, Principal), + FileToRemove(FileId), +} + impl TimerJobItem for BucketEventBatch { async fn process(&self) -> Result<(), bool> { let mut args = storage_bucket_canister::c2c_sync_index::Args::default(); diff --git a/backend/canisters/storage_index/impl/src/model/bucket_sync_state.rs b/backend/canisters/storage_index/impl/src/model/bucket_sync_state.rs deleted file mode 100644 index e457040240..0000000000 --- a/backend/canisters/storage_index/impl/src/model/bucket_sync_state.rs +++ /dev/null @@ -1,50 +0,0 @@ -use candid::Principal; -use serde::{Deserialize, Serialize}; -use std::collections::VecDeque; -use storage_bucket_canister::c2c_sync_index::Args; -use types::{AccessorId, FileId}; - -// We want to send events to the each bucket in order, so while a sync is in progress we avoid sending -// more events in case the first batch fails and the second succeeds. If a sync fails, the args that -// were sent are stored so that they can be retried again. -#[derive(Serialize, Deserialize, Default)] -pub struct BucketSyncState { - queue: VecDeque, - in_progress: bool, - args_to_retry: Option, -} - -impl BucketSyncState { - pub fn take(&mut self) -> Vec { - assert!(!self.in_progress); - let mut events = Vec::new(); - if let Some(args) = self.args_to_retry.take() { - for principal in args.users_added { - events.push(EventToSync::UserAdded(principal)); - } - for principal in args.users_removed { - events.push(EventToSync::UserRemoved(principal)); - } - for principal in args.accessors_removed { - events.push(EventToSync::AccessorRemoved(principal)); - } - for (old, new) in args.user_ids_updated { - events.push(EventToSync::UserIdUpdated(old, new)); - } - for file_id in args.files_to_remove { - events.push(EventToSync::FileToRemove(file_id)); - } - } - events.extend(std::mem::take(&mut self.queue)); - events - } -} - -#[derive(Serialize, Deserialize, Clone)] -pub enum EventToSync { - UserAdded(Principal), - UserRemoved(Principal), - AccessorRemoved(AccessorId), - UserIdUpdated(Principal, Principal), - FileToRemove(FileId), -} diff --git a/backend/canisters/storage_index/impl/src/model/buckets.rs b/backend/canisters/storage_index/impl/src/model/buckets.rs index 9e17e7fd8c..a5bc7a6e75 100644 --- a/backend/canisters/storage_index/impl/src/model/buckets.rs +++ b/backend/canisters/storage_index/impl/src/model/buckets.rs @@ -1,4 +1,3 @@ -use crate::model::bucket_sync_state::BucketSyncState; use crate::BucketMetrics; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -96,10 +95,6 @@ impl Buckets { pub fn iter_full_buckets(&self) -> impl Iterator { self.full_buckets.values() } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.active_buckets.iter_mut().chain(self.full_buckets.values_mut()) - } } #[derive(Serialize, Deserialize)] @@ -108,20 +103,16 @@ pub struct BucketRecord { pub wasm_version: BuildVersion, pub bytes_used: u64, pub bytes_remaining: i64, - #[deprecated] - pub sync_state: BucketSyncState, pub cycle_top_ups: Vec, } impl BucketRecord { pub fn new(canister_id: CanisterId, wasm_version: BuildVersion) -> BucketRecord { - #[allow(deprecated)] BucketRecord { canister_id, wasm_version, bytes_used: 0, bytes_remaining: 0, - sync_state: BucketSyncState::default(), cycle_top_ups: Vec::new(), } } diff --git a/backend/canisters/storage_index/impl/src/model/mod.rs b/backend/canisters/storage_index/impl/src/model/mod.rs index 117cdf25e8..8de7ea779f 100644 --- a/backend/canisters/storage_index/impl/src/model/mod.rs +++ b/backend/canisters/storage_index/impl/src/model/mod.rs @@ -1,4 +1,3 @@ pub mod bucket_event_batch; -pub mod bucket_sync_state; pub mod buckets; pub mod files; diff --git a/backend/canisters/storage_index/impl/src/updates/add_or_update_users.rs b/backend/canisters/storage_index/impl/src/updates/add_or_update_users.rs index 58e47bcc7a..67740c754c 100644 --- a/backend/canisters/storage_index/impl/src/updates/add_or_update_users.rs +++ b/backend/canisters/storage_index/impl/src/updates/add_or_update_users.rs @@ -1,5 +1,5 @@ use crate::guards::caller_is_user_controller; -use crate::model::bucket_sync_state::EventToSync; +use crate::model::bucket_event_batch::EventToSync; use crate::{mutate_state, RuntimeState, UserRecordInternal}; use canister_tracing_macros::trace; use ic_cdk::update; diff --git a/backend/canisters/storage_index/impl/src/updates/c2c_update_user_principal.rs b/backend/canisters/storage_index/impl/src/updates/c2c_update_user_principal.rs index 56f944f181..8aaae64c78 100644 --- a/backend/canisters/storage_index/impl/src/updates/c2c_update_user_principal.rs +++ b/backend/canisters/storage_index/impl/src/updates/c2c_update_user_principal.rs @@ -1,5 +1,5 @@ use crate::guards::caller_is_user_controller; -use crate::model::bucket_sync_state::EventToSync; +use crate::model::bucket_event_batch::EventToSync; use crate::{mutate_state, RuntimeState}; use canister_api_macros::update; use canister_tracing_macros::trace; diff --git a/backend/canisters/storage_index/impl/src/updates/remove_accessor.rs b/backend/canisters/storage_index/impl/src/updates/remove_accessor.rs index 90454be6a1..b59b2402c2 100644 --- a/backend/canisters/storage_index/impl/src/updates/remove_accessor.rs +++ b/backend/canisters/storage_index/impl/src/updates/remove_accessor.rs @@ -1,5 +1,5 @@ use crate::guards::caller_is_user_controller; -use crate::model::bucket_sync_state::EventToSync; +use crate::model::bucket_event_batch::EventToSync; use crate::{mutate_state, RuntimeState}; use canister_tracing_macros::trace; use ic_cdk::update; diff --git a/backend/canisters/storage_index/impl/src/updates/remove_user.rs b/backend/canisters/storage_index/impl/src/updates/remove_user.rs index 220d6e2e0f..ad644a29c9 100644 --- a/backend/canisters/storage_index/impl/src/updates/remove_user.rs +++ b/backend/canisters/storage_index/impl/src/updates/remove_user.rs @@ -1,5 +1,5 @@ use crate::guards::caller_is_user_controller; -use crate::model::bucket_sync_state::EventToSync; +use crate::model::bucket_event_batch::EventToSync; use crate::{mutate_state, RuntimeState}; use canister_tracing_macros::trace; use ic_cdk::update; diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index e2b54bcd7b..260fb25148 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Implement new lightweight search index for searching messages ([#7029](https://github.com/open-chat-labs/open-chat/pull/7029)) - Make `MessageId` comparisons use their 64bit representation ([#7030](https://github.com/open-chat-labs/open-chat/pull/7030)) - Notify CHIT updates via LocalUserIndex ([#7033](https://github.com/open-chat-labs/open-chat/pull/7033)) +- 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)) ### Removed diff --git a/backend/canisters/user/api/src/updates/send_message_with_transfer_to_channel.rs b/backend/canisters/user/api/src/updates/send_message_with_transfer_to_channel.rs index 5e367c5ccf..bb1cf543ea 100644 --- a/backend/canisters/user/api/src/updates/send_message_with_transfer_to_channel.rs +++ b/backend/canisters/user/api/src/updates/send_message_with_transfer_to_channel.rs @@ -44,6 +44,7 @@ pub enum Response { UserLapsed, CommunityFrozen, RulesNotAccepted, + MessageAlreadyExists, CommunityRulesNotAccepted, Retrying(String, CompletedCryptoTransaction), PinRequired, diff --git a/backend/canisters/user/api/src/updates/send_message_with_transfer_to_group.rs b/backend/canisters/user/api/src/updates/send_message_with_transfer_to_group.rs index 99aff9c316..73ebe506d2 100644 --- a/backend/canisters/user/api/src/updates/send_message_with_transfer_to_group.rs +++ b/backend/canisters/user/api/src/updates/send_message_with_transfer_to_group.rs @@ -41,6 +41,7 @@ pub enum Response { UserLapsed, ChatFrozen, RulesNotAccepted, + MessageAlreadyExists, Retrying(String, CompletedCryptoTransaction), PinRequired, PinIncorrect(Milliseconds), diff --git a/backend/canisters/user/impl/src/timer_job_types.rs b/backend/canisters/user/impl/src/timer_job_types.rs index 166568392e..6c0a6ba359 100644 --- a/backend/canisters/user/impl/src/timer_job_types.rs +++ b/backend/canisters/user/impl/src/timer_job_types.rs @@ -389,6 +389,9 @@ impl Job for SendMessageToChannelJob { impl Job for MarkVideoCallEndedJob { fn execute(self) { - mutate_state(|state| end_video_call_impl(self.0, state)); + let response = mutate_state(|state| end_video_call_impl(self.0.clone(), state)); + if !matches!(response, user_canister::end_video_call::Response::Success) { + error!(?response, args = ?self.0, "Failed to mark video call ended"); + } } } diff --git a/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs b/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs index 770280454b..d73afbe6fa 100644 --- a/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs +++ b/backend/canisters/user/impl/src/updates/c2c_notify_user_canister_events.rs @@ -194,6 +194,7 @@ fn edit_message(args: user_canister::EditMessageArgs, caller_user_id: UserId, st message_id: args.message_id, content: args.content.into(), block_level_markdown: args.block_level_markdown, + finalise_bot_message: false, now, }, None, diff --git a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs index c7d0363473..2c77be2435 100644 --- a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs +++ b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs @@ -152,6 +152,7 @@ pub(crate) fn handle_message_impl(args: HandleMessageArgs, state: &mut RuntimeSt block_level_markdown: args.block_level_markdown, correlation_id: 0, now: args.now, + bot_context: None, }; let message_id = push_message_args.message_id; diff --git a/backend/canisters/user/impl/src/updates/edit_message.rs b/backend/canisters/user/impl/src/updates/edit_message.rs index d52c91477b..710089db11 100644 --- a/backend/canisters/user/impl/src/updates/edit_message.rs +++ b/backend/canisters/user/impl/src/updates/edit_message.rs @@ -34,6 +34,7 @@ fn edit_message_impl(args: Args, state: &mut RuntimeState) -> 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/CHANGELOG.md b/backend/canisters/user_index/CHANGELOG.md index 58b559cd4c..97fb6a6265 100644 --- a/backend/canisters/user_index/CHANGELOG.md +++ b/backend/canisters/user_index/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.1508](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1508-user_index)] - 2024-12-13 + ### Added - Implement `register_bot` including bot schema ([#6928](https://github.com/open-chat-labs/open-chat/pull/6928)) diff --git a/backend/canisters/user_index/impl/src/lib.rs b/backend/canisters/user_index/impl/src/lib.rs index ef88406874..1eea5cbd04 100644 --- a/backend/canisters/user_index/impl/src/lib.rs +++ b/backend/canisters/user_index/impl/src/lib.rs @@ -344,7 +344,6 @@ struct Data { pub escrow_canister_id: CanisterId, pub translations_canister_id: CanisterId, pub event_store_client: EventStoreClient, - #[serde(skip_deserializing, default = "storage_index_user_sync_queue")] pub storage_index_user_sync_queue: GroupedTimerJobQueue, pub user_index_event_sync_queue: CanisterEventSyncQueue, pub pending_payments_queue: PendingPaymentsQueue, @@ -359,7 +358,6 @@ struct Data { pub neuron_controllers_for_initial_airdrop: HashMap, pub nns_governance_canister_id: CanisterId, pub internet_identity_canister_id: CanisterId, - #[serde(default = "website_canister_id")] pub website_canister_id: CanisterId, pub platform_moderators_group: Option, pub reported_messages: ReportedMessages, @@ -381,14 +379,6 @@ struct Data { pub upload_wasm_chunks_whitelist: Vec, } -fn storage_index_user_sync_queue() -> GroupedTimerJobQueue { - GroupedTimerJobQueue::new(1, false) -} - -fn website_canister_id() -> CanisterId { - CanisterId::from_text("6hsbt-vqaaa-aaaaf-aaafq-cai").unwrap() -} - impl Data { #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs index 6d6e0cb33d..d2761e052e 100644 --- a/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs @@ -1,17 +1,13 @@ use crate::lifecycle::{init_env, init_state}; use crate::memory::get_upgrades_memory; -use crate::{mutate_state, Data, ONE_GB, ONE_MB}; +use crate::Data; use canister_logger::LogEntry; use canister_tracing_macros::trace; use ic_cdk::post_upgrade; use stable_memory::get_reader; -use std::collections::BTreeMap; -use storage_index_canister::add_or_update_users::UserConfig; use tracing::info; -use types::CanisterId; use user_index_canister::post_upgrade::Args; use utils::cycles::init_cycles_dispenser_client; -use utils::env::Environment; #[post_upgrade] #[trace] @@ -19,40 +15,15 @@ fn post_upgrade(args: Args) { let memory = get_upgrades_memory(); let reader = get_reader(&memory); - let (mut data, errors, logs, traces): (Data, Vec, Vec, Vec) = + let (data, errors, logs, traces): (Data, Vec, Vec, Vec) = msgpack::deserialize(reader).unwrap(); canister_logger::init_with_logs(data.test_mode, errors, logs, traces); let env = init_env(data.rng_seed, data.oc_key_pair.is_initialised()); - let mut user_id_lengths: BTreeMap = BTreeMap::new(); - let now = env.now(); - data.storage_index_user_sync_queue.set_defer_processing(true); - for user in data.users.iter() { - let length = user.user_id.as_slice().len(); - *user_id_lengths.entry(length).or_default() += 1; - - data.storage_index_user_sync_queue.push( - data.storage_index_canister_id, - UserConfig { - user_id: user.principal, - byte_limit: if user.diamond_membership_details.is_active(now) { ONE_GB } else { 100 * ONE_MB }, - }, - ); - } - data.storage_index_user_sync_queue.set_defer_processing(false); - - info!(?user_id_lengths, "UserId lengths"); - init_cycles_dispenser_client(data.cycles_dispenser_canister_id, data.test_mode); init_state(env, data, args.wasm_version); info!(version = %args.wasm_version, "Post-upgrade complete"); - - mutate_state(|state| { - if state.data.test_mode { - state.data.website_canister_id = CanisterId::from_text("pfs7b-iqaaa-aaaaf-abs7q-cai").unwrap(); - } - }) } 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/principal_to_user_id_map/src/lib.rs b/backend/libraries/principal_to_user_id_map/src/lib.rs index aec18624c1..9b1ff8179c 100644 --- a/backend/libraries/principal_to_user_id_map/src/lib.rs +++ b/backend/libraries/principal_to_user_id_map/src/lib.rs @@ -1,8 +1,6 @@ use ic_principal::Principal; -use serde::de::MapAccess; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use stable_memory_map::{with_map, with_map_mut, KeyPrefix, PrincipalToUserIdKeyPrefix}; -use std::fmt::Formatter; use types::UserId; #[derive(Serialize, Deserialize, Default)] @@ -37,33 +35,6 @@ impl PrincipalToUserIdMap { } } -pub fn deserialize_principal_to_user_id_map_from_heap<'de, D: Deserializer<'de>>( - d: D, -) -> Result { - d.deserialize_map(Visitor) -} - -struct Visitor; - -impl<'a> serde::de::Visitor<'a> for Visitor { - type Value = PrincipalToUserIdMap; - - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { - formatter.write_str("a map of (Principal, UserId)") - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'a>, - { - let mut result = PrincipalToUserIdMap::default(); - while let Some((principal, user_id)) = map.next_entry()? { - result.insert(principal, user_id); - } - Ok(result) - } -} - fn bytes_to_user_id(bytes: Vec) -> UserId { UserId::from(Principal::from_slice(&bytes)) } 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, + } + } +} diff --git a/frontend/app/package.json b/frontend/app/package.json index 34843d2df5..c0d0cbe883 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -61,6 +61,7 @@ "@dfinity/candid": "^2.1.1", "@dfinity/identity": "^2.1.1", "@dfinity/principal": "^2.1.1", + "@fingerprintjs/botd": "^1.9.1", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@memefighter/maker-core": "^0.0.17", "@metamask/sdk": "^0.28.4", diff --git a/frontend/app/src/components/Button.svelte b/frontend/app/src/components/Button.svelte index 79bf216054..5ccc0c6d7c 100644 --- a/frontend/app/src/components/Button.svelte +++ b/frontend/app/src/components/Button.svelte @@ -1,10 +1,5 @@ - + + {#if $currentTheme.name === "signals"} diff --git a/frontend/app/src/components/SecureButton.svelte b/frontend/app/src/components/SecureButton.svelte new file mode 100644 index 0000000000..f871a0aec0 --- /dev/null +++ b/frontend/app/src/components/SecureButton.svelte @@ -0,0 +1,49 @@ + + + diff --git a/frontend/app/src/components/home/PrizeContent.svelte b/frontend/app/src/components/home/PrizeContent.svelte index ffea1650f5..636d2c7bfa 100644 --- a/frontend/app/src/components/home/PrizeContent.svelte +++ b/frontend/app/src/components/home/PrizeContent.svelte @@ -1,5 +1,4 @@