diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index 8be71d826f..9398d3823a 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Handle transfer fee changing in either direction ([#6064](https://github.com/open-chat-labs/open-chat/pull/6064)) - Bypass gates if user is invited ([#6110](https://github.com/open-chat-labs/open-chat/pull/6110)) - Return `is_invited` when previewing community/channel ([#6113](https://github.com/open-chat-labs/open-chat/pull/6113)) +- Use `UserType` rather than `is_bot` and `is_oc_controlled_bot` ([#6116](https://github.com/open-chat-labs/open-chat/pull/6116)) ### Fixed diff --git a/backend/canisters/community/api/src/lifecycle/init.rs b/backend/canisters/community/api/src/lifecycle/init.rs index 4ded00f110..85668d4295 100644 --- a/backend/canisters/community/api/src/lifecycle/init.rs +++ b/backend/canisters/community/api/src/lifecycle/init.rs @@ -1,6 +1,8 @@ use candid::{CandidType, Principal}; use serde::{Deserialize, Serialize}; -use types::{AccessGate, BuildVersion, CanisterId, CommunityPermissions, Document, Milliseconds, Rules, SourceGroup, UserId}; +use types::{ + AccessGate, BuildVersion, CanisterId, CommunityPermissions, Document, Milliseconds, Rules, SourceGroup, UserId, UserType, +}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { @@ -14,6 +16,8 @@ pub struct Args { pub primary_language: String, pub created_by_principal: Principal, pub created_by_user_id: UserId, + #[serde(default)] + pub created_by_user_type: UserType, pub mark_active_duration: Milliseconds, pub user_index_canister_id: CanisterId, pub local_user_index_canister_id: CanisterId, diff --git a/backend/canisters/community/api/src/updates/c2c_join_channel.rs b/backend/canisters/community/api/src/updates/c2c_join_channel.rs index 7ae5f8c00f..7aef325837 100644 --- a/backend/canisters/community/api/src/updates/c2c_join_channel.rs +++ b/backend/canisters/community/api/src/updates/c2c_join_channel.rs @@ -2,7 +2,7 @@ use candid::{CandidType, Principal}; use serde::{Deserialize, Serialize}; use types::{ ChannelId, CommunityCanisterChannelSummary, CommunityCanisterCommunitySummary, GateCheckFailedReason, TimestampMillis, - UniquePersonProof, UserId, VerifiedCredentialGateArgs, + UniquePersonProof, UserId, UserType, VerifiedCredentialGateArgs, }; #[derive(CandidType, Serialize, Deserialize, Debug)] @@ -13,6 +13,8 @@ pub struct Args { pub invite_code: Option, pub is_platform_moderator: bool, pub is_bot: bool, + #[serde(default)] + pub user_type: UserType, pub diamond_membership_expires_at: Option, pub verified_credential_args: Option, pub unique_person_proof: Option, diff --git a/backend/canisters/community/api/src/updates/c2c_join_community.rs b/backend/canisters/community/api/src/updates/c2c_join_community.rs index e1a17cfff0..936fade00c 100644 --- a/backend/canisters/community/api/src/updates/c2c_join_community.rs +++ b/backend/canisters/community/api/src/updates/c2c_join_community.rs @@ -1,7 +1,7 @@ use candid::{CandidType, Principal}; use serde::{Deserialize, Serialize}; use types::{ - CommunityCanisterCommunitySummary, GateCheckFailedReason, TimestampMillis, UniquePersonProof, UserId, + CommunityCanisterCommunitySummary, GateCheckFailedReason, TimestampMillis, UniquePersonProof, UserId, UserType, VerifiedCredentialGateArgs, }; @@ -12,6 +12,8 @@ pub struct Args { pub invite_code: Option, pub is_platform_moderator: bool, pub is_bot: bool, + #[serde(default)] + pub user_type: UserType, pub diamond_membership_expires_at: Option, pub verified_credential_args: Option, pub unique_person_proof: Option, diff --git a/backend/canisters/community/impl/src/jobs/import_groups.rs b/backend/canisters/community/impl/src/jobs/import_groups.rs index 1f9c4ec028..2336d2f191 100644 --- a/backend/canisters/community/impl/src/jobs/import_groups.rs +++ b/backend/canisters/community/impl/src/jobs/import_groups.rs @@ -12,7 +12,7 @@ use std::cell::Cell; use std::collections::HashMap; use std::time::Duration; use tracing::{info, trace}; -use types::{ChannelId, ChannelLatestMessageIndex, Chat, ChatId, Empty, UserId, UsersBlocked}; +use types::{ChannelId, ChannelLatestMessageIndex, Chat, ChatId, Empty, UserId, UserType, UsersBlocked}; use utils::consts::OPENCHAT_BOT_USER_ID; const PAGE_SIZE: u32 = 19 * 102 * 1024; // Roughly 1.9MB (1.9 * 1024 * 1024) @@ -166,12 +166,12 @@ pub(crate) async fn process_channel_members(group_id: ChatId, channel_id: Channe let (members_to_add_to_community, local_user_index_canister_id) = mutate_state(|state| { let channel = state.data.channels.get(&channel_id).unwrap(); - let mut to_add: HashMap = HashMap::new(); - for (user_id, is_bot) in channel.chat.members.iter().map(|m| (m.user_id, m.is_bot)) { + let mut to_add: HashMap = HashMap::new(); + for (user_id, user_type) in channel.chat.members.iter().map(|m| (m.user_id, m.user_type)) { if let Some(member) = state.data.members.get_by_user_id_mut(&user_id) { member.channels.insert(channel_id); } else { - to_add.insert(user_id, is_bot); + to_add.insert(user_id, user_type); } } diff --git a/backend/canisters/community/impl/src/lib.rs b/backend/canisters/community/impl/src/lib.rs index 72cb632b1d..062bf75b49 100644 --- a/backend/canisters/community/impl/src/lib.rs +++ b/backend/canisters/community/impl/src/lib.rs @@ -26,7 +26,7 @@ use std::time::Duration; use types::{ AccessGate, Achievement, BuildVersion, CanisterId, ChatMetrics, CommunityCanisterCommunitySummary, CommunityMembership, CommunityPermissions, CommunityRole, Cryptocurrency, Cycles, Document, Empty, FrozenGroupInfo, Milliseconds, Notification, - PaymentGate, Rules, TimestampMillis, Timestamped, UserId, + PaymentGate, Rules, TimestampMillis, Timestamped, UserId, UserType, }; use types::{CommunityId, SNS_FEE_SHARE_PERCENT}; use user_canister::c2c_notify_achievement; @@ -341,6 +341,7 @@ impl Data { community_id: CommunityId, created_by_principal: Principal, created_by_user_id: UserId, + created_by_user_type: UserType, is_public: bool, name: String, description: String, @@ -370,13 +371,20 @@ impl Data { let channels = Channels::new( community_id, created_by_user_id, + created_by_user_type, default_channels, default_channel_rules, is_public, rng, now, ); - let members = CommunityMembers::new(created_by_principal, created_by_user_id, channels.public_channel_ids(), now); + let members = CommunityMembers::new( + created_by_principal, + created_by_user_id, + created_by_user_type, + channels.public_channel_ids(), + now, + ); let events = CommunityEvents::new(name.clone(), description.clone(), created_by_user_id, now); Data { diff --git a/backend/canisters/community/impl/src/lifecycle/init.rs b/backend/canisters/community/impl/src/lifecycle/init.rs index 6ad9d03822..ee550208dd 100644 --- a/backend/canisters/community/impl/src/lifecycle/init.rs +++ b/backend/canisters/community/impl/src/lifecycle/init.rs @@ -19,6 +19,7 @@ fn init(args: Args) { env.canister_id().into(), args.created_by_principal, args.created_by_user_id, + args.created_by_user_type, args.is_public, args.name, args.description, diff --git a/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs index 92ef9aaf23..788abb8773 100644 --- a/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/community/impl/src/lifecycle/post_upgrade.rs @@ -1,7 +1,7 @@ use crate::jobs::import_groups::finalize_group_import; use crate::lifecycle::{init_env, init_state}; use crate::memory::get_upgrades_memory; -use crate::{read_state, Data}; +use crate::{mutate_state, read_state, Data}; use canister_logger::LogEntry; use canister_tracing_macros::trace; use community_canister::post_upgrade::Args; @@ -9,6 +9,7 @@ use ic_cdk::post_upgrade; use instruction_counts_log::InstructionCountFunctionId; use stable_memory::get_reader; use tracing::info; +use utils::consts::OPENCHAT_BOT_USER_ID; #[post_upgrade] #[trace] @@ -31,6 +32,12 @@ fn post_upgrade(args: Args) { info!(version = %args.wasm_version, "Post-upgrade complete"); + mutate_state(|state| { + let oc_controlled_bot_users = vec![OPENCHAT_BOT_USER_ID, state.data.proposals_bot_user_id]; + state.data.members.set_user_types(&oc_controlled_bot_users); + state.data.channels.set_user_types(&oc_controlled_bot_users); + }); + read_state(|state| { let now = state.env.now(); state diff --git a/backend/canisters/community/impl/src/model/channels.rs b/backend/canisters/community/impl/src/model/channels.rs index 9d88d7ffe3..9e0e415df5 100644 --- a/backend/canisters/community/impl/src/model/channels.rs +++ b/backend/canisters/community/impl/src/model/channels.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use types::{ ChannelId, ChannelMatch, CommunityCanisterChannelSummary, CommunityCanisterChannelSummaryUpdates, CommunityId, GroupMembership, GroupMembershipUpdates, GroupPermissionRole, GroupPermissions, MultiUserChat, Rules, TimestampMillis, - Timestamped, UserId, MAX_THREADS_IN_SUMMARY, + Timestamped, UserId, UserType, MAX_THREADS_IN_SUMMARY, }; use super::members::CommunityMembers; @@ -28,9 +28,17 @@ pub struct Channel { } impl Channels { + pub fn set_user_types(&mut self, oc_controlled_bots: &[UserId]) { + for channel in self.channels.values_mut() { + channel.chat.set_user_types(oc_controlled_bots); + } + } + + #[allow(clippy::too_many_arguments)] pub fn new( community_id: CommunityId, created_by: UserId, + created_by_user_type: UserType, default_channels: Vec, default_channel_rules: Option, is_community_public: bool, @@ -48,6 +56,7 @@ impl Channels { community_id, name, created_by, + created_by_user_type, default_channel_rules.clone(), is_community_public, rng.gen(), @@ -182,6 +191,7 @@ impl Channel { community_id: CommunityId, name: String, created_by: UserId, + created_by_user_type: UserType, channel_rules: Option, is_community_public: bool, anonymized_id: u128, @@ -207,7 +217,7 @@ impl Channel { permissions, None, None, - false, + created_by_user_type, anonymized_id, now, ), diff --git a/backend/canisters/community/impl/src/model/members.rs b/backend/canisters/community/impl/src/model/members.rs index 54b55ad6f5..4cd3c8f540 100644 --- a/backend/canisters/community/impl/src/model/members.rs +++ b/backend/canisters/community/impl/src/model/members.rs @@ -4,7 +4,9 @@ use rand::RngCore; use serde::{Deserialize, Serialize}; use std::collections::hash_map::Entry::Vacant; use std::collections::{HashMap, HashSet}; -use types::{ChannelId, CommunityMember, CommunityPermissions, CommunityRole, TimestampMillis, Timestamped, UserId, Version}; +use types::{ + ChannelId, CommunityMember, CommunityPermissions, CommunityRole, TimestampMillis, Timestamped, UserId, UserType, Version, +}; const MAX_MEMBERS_PER_COMMUNITY: u32 = 100_000; @@ -21,9 +23,17 @@ pub struct CommunityMembers { } impl CommunityMembers { + pub fn set_user_types(&mut self, oc_controlled_bot_users: &[UserId]) { + for (user_id, member) in self.members.iter_mut().filter(|(_, m)| m.is_bot) { + member.user_type = + if oc_controlled_bot_users.contains(user_id) { UserType::OcControlledBot } else { UserType::Bot }; + } + } + pub fn new( creator_principal: Principal, creator_user_id: UserId, + creator_user_type: UserType, public_channels: Vec, now: TimestampMillis, ) -> CommunityMembers { @@ -35,7 +45,8 @@ impl CommunityMembers { channels: public_channels.into_iter().collect(), channels_removed: Vec::new(), rules_accepted: Some(Timestamped::new(Version::zero(), now)), - is_bot: false, + is_bot: creator_user_type.is_bot(), + user_type: creator_user_type, display_name: Timestamped::default(), }; @@ -50,7 +61,7 @@ impl CommunityMembers { } } - pub fn add(&mut self, user_id: UserId, principal: Principal, is_bot: bool, now: TimestampMillis) -> AddResult { + pub fn add(&mut self, user_id: UserId, principal: Principal, user_type: UserType, now: TimestampMillis) -> AddResult { if self.blocked.contains(&user_id) { AddResult::Blocked } else { @@ -64,7 +75,8 @@ impl CommunityMembers { channels: HashSet::new(), channels_removed: Vec::new(), rules_accepted: None, - is_bot, + is_bot: user_type.is_bot(), + user_type, display_name: Timestamped::default(), }; e.insert(member.clone()); @@ -342,6 +354,8 @@ pub struct CommunityMemberInternal { pub channels_removed: Vec>, pub rules_accepted: Option>, pub is_bot: bool, + #[serde(default)] + pub user_type: UserType, display_name: Timestamped>, } diff --git a/backend/canisters/community/impl/src/updates/add_members_to_channel.rs b/backend/canisters/community/impl/src/updates/add_members_to_channel.rs index 5a1ed669cd..f9980bb3c7 100644 --- a/backend/canisters/community/impl/src/updates/add_members_to_channel.rs +++ b/backend/canisters/community/impl/src/updates/add_members_to_channel.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use std::iter::zip; use types::{ AccessGate, AddedToChannelNotification, CanisterId, ChannelId, EventIndex, MembersAdded, MessageIndex, Notification, - TimestampNanos, UserId, + TimestampNanos, UserId, UserType, }; #[update] @@ -22,7 +22,7 @@ async fn add_members_to_channel(args: Args) -> Response { Err(response) => return response, }; - let mut users_to_add: Vec = Vec::new(); + let mut users_to_add: Vec<(UserId, UserType)> = Vec::new(); let mut users_failed_gate_check: Vec = Vec::new(); let mut users_failed_with_error: Vec = Vec::new(); @@ -31,7 +31,7 @@ async fn add_members_to_channel(args: Args) -> Response { match local_user_index_canister_c2c_client::c2c_diamond_membership_expiry_dates( prepare_result.local_user_index_canister_id, &local_user_index_canister::c2c_diamond_membership_expiry_dates::Args { - user_ids: prepare_result.users_to_add.clone(), + user_ids: prepare_result.users_to_add.iter().map(|(u, _)| *u).collect(), }, ) .await @@ -48,7 +48,7 @@ async fn add_members_to_channel(args: Args) -> Response { let futures: Vec<_> = prepare_result .users_to_add .iter() - .map(|user_id| { + .map(|(user_id, _)| { check_if_passes_gate( gate.clone(), CheckGateArgs { @@ -65,9 +65,9 @@ async fn add_members_to_channel(args: Args) -> Response { let results = futures::future::join_all(futures).await; - for (user_id, result) in zip(prepare_result.users_to_add, results) { + for ((user_id, user_type), result) in zip(prepare_result.users_to_add, results) { match result { - CheckIfPassesGateResult::Success => users_to_add.push(user_id), + CheckIfPassesGateResult::Success => users_to_add.push((user_id, user_type)), CheckIfPassesGateResult::Failed(reason) => { users_failed_gate_check.push(UserFailedGateCheck { user_id, reason }) } @@ -89,8 +89,8 @@ async fn add_members_to_channel(args: Args) -> Response { users_to_add, prepare_result.users_already_in_channel, users_failed_gate_check, + prepare_result.users_not_in_community, users_failed_with_error, - prepare_result.is_bot, state, ) }) @@ -98,11 +98,11 @@ async fn add_members_to_channel(args: Args) -> Response { struct PrepareResult { user_id: UserId, - users_to_add: Vec, + users_to_add: Vec<(UserId, UserType)>, users_already_in_channel: Vec, + users_not_in_community: Vec, gate: Option, local_user_index_canister_id: CanisterId, - is_bot: bool, member_display_name: Option, this_canister: CanisterId, now_nanos: TimestampNanos, @@ -132,19 +132,28 @@ fn prepare(args: &Args, state: &RuntimeState) -> Result return Err(NotAuthorized); } - let (users_already_in_channel, users_to_add): (Vec<_>, Vec<_>) = args - .user_ids - .iter() - .copied() - .partition(|id| channel.chat.members.contains(id)); + let mut users_to_add = Vec::new(); + let mut users_already_in_channel = Vec::new(); + let mut users_not_in_community = Vec::new(); + for user_id in args.user_ids.iter() { + if let Some(member) = state.data.members.get_by_user_id(user_id) { + if !channel.chat.members.contains(user_id) { + users_to_add.push((*user_id, member.user_type)); + } else { + users_already_in_channel.push(*user_id); + } + } else { + users_not_in_community.push(*user_id); + } + } Ok(PrepareResult { user_id, users_to_add, users_already_in_channel, + users_not_in_community, gate: channel.chat.gate.as_ref().cloned(), local_user_index_canister_id: state.data.local_user_index_canister_id, - is_bot: member.is_bot, member_display_name: member.display_name().value.clone(), this_canister: state.env.canister_id(), now_nanos: state.env.now_nanos(), @@ -166,11 +175,11 @@ fn commit( added_by_name: String, added_by_display_name: Option, channel_id: ChannelId, - users_to_add: Vec, + users_to_add: Vec<(UserId, UserType)>, mut users_already_in_channel: Vec, users_failed_gate_check: Vec, + _users_not_in_community: Vec, mut users_failed_with_error: Vec, - is_bot: bool, state: &mut RuntimeState, ) -> Response { if let Some(channel) = state.data.channels.get_mut(&channel_id) { @@ -188,14 +197,14 @@ fn commit( let mut users_added: Vec = Vec::new(); let mut users_limit_reached: Vec = Vec::new(); - for user_id in users_to_add { + for (user_id, user_type) in users_to_add { match channel.chat.members.add( user_id, now, min_visible_event_index, min_visible_message_index, channel.chat.is_public.value, - is_bot, + user_type, ) { AddResult::Success(_) => { users_added.push(user_id); diff --git a/backend/canisters/community/impl/src/updates/c2c_join_channel.rs b/backend/canisters/community/impl/src/updates/c2c_join_channel.rs index 2023aa5961..ecf4861a91 100644 --- a/backend/canisters/community/impl/src/updates/c2c_join_channel.rs +++ b/backend/canisters/community/impl/src/updates/c2c_join_channel.rs @@ -30,6 +30,7 @@ async fn c2c_join_channel(args: Args) -> Response { invite_code: args.invite_code, is_platform_moderator: args.is_platform_moderator, is_bot: args.is_bot, + user_type: args.user_type, diamond_membership_expires_at: args.diamond_membership_expires_at, verified_credential_args: args.verified_credential_args.clone(), unique_person_proof: args.unique_person_proof.clone(), @@ -241,7 +242,7 @@ pub(crate) fn join_channel_unchecked( min_visible_event_index, min_visible_message_index, notifications_muted, - member.is_bot, + member.user_type, ); match &result { diff --git a/backend/canisters/community/impl/src/updates/c2c_join_community.rs b/backend/canisters/community/impl/src/updates/c2c_join_community.rs index 661492df1a..f78318d8f4 100644 --- a/backend/canisters/community/impl/src/updates/c2c_join_community.rs +++ b/backend/canisters/community/impl/src/updates/c2c_join_community.rs @@ -112,7 +112,7 @@ pub(crate) fn join_community_impl(args: &Args, state: &mut RuntimeState) -> Resu .push_event(CommunityEventInternal::UsersUnblocked(Box::new(event)), now); } - match state.data.members.add(args.user_id, args.principal, args.is_bot, now) { + match state.data.members.add(args.user_id, args.principal, args.user_type, now) { AddResult::Success(_) => { let invitation = state.data.invited_users.remove(&args.user_id, now); diff --git a/backend/canisters/community/impl/src/updates/create_channel.rs b/backend/canisters/community/impl/src/updates/create_channel.rs index 5a01caa2df..f1bef12ad0 100644 --- a/backend/canisters/community/impl/src/updates/create_channel.rs +++ b/backend/canisters/community/impl/src/updates/create_channel.rs @@ -11,7 +11,7 @@ use community_canister::create_channel::{Response::*, *}; use group_chat_core::GroupChatCore; use rand::Rng; use std::collections::HashMap; -use types::{AccessGate, ChannelId, MultiUserChat, TimestampMillis, UserId}; +use types::{AccessGate, ChannelId, MultiUserChat, TimestampMillis, UserId, UserType}; use utils::document_validation::validate_avatar; use utils::text_validation::{ validate_description, validate_group_name, validate_rules, NameValidationError, RulesValidationError, @@ -45,6 +45,7 @@ fn c2c_create_proposals_channel(args: Args) -> Response { invite_code: None, is_platform_moderator: false, is_bot: true, + user_type: UserType::OcControlledBot, diamond_membership_expires_at: None, verified_credential_args: None, unique_person_proof: None, @@ -131,7 +132,7 @@ fn create_channel_impl( permissions, args.gate.clone(), args.events_ttl, - member.is_bot, + member.user_type, state.env.rng().gen(), now, ); diff --git a/backend/canisters/community/impl/src/updates/send_message.rs b/backend/canisters/community/impl/src/updates/send_message.rs index 370e11696b..2093e0c71f 100644 --- a/backend/canisters/community/impl/src/updates/send_message.rs +++ b/backend/canisters/community/impl/src/updates/send_message.rs @@ -15,7 +15,7 @@ use regex_lite::Regex; use std::str::FromStr; use types::{ Achievement, ChannelId, ChannelMessageNotification, EventWrapper, Message, MessageContent, MessageIndex, Notification, - TimestampMillis, User, UserId, Version, + TimestampMillis, User, UserId, UserType, Version, }; #[update(candid = true, msgpack = true)] @@ -37,7 +37,7 @@ fn c2c_send_message(args: C2CArgs) -> C2CResponse { fn send_message_impl(args: Args, state: &mut RuntimeState) -> Response { let Caller { user_id, - is_bot, + user_type, display_name, } = match validate_caller(args.community_rules_accepted, state) { Ok(ok) => ok, @@ -50,7 +50,7 @@ fn send_message_impl(args: Args, state: &mut RuntimeState) -> Response { let result = channel.chat.validate_and_send_message( user_id, - is_bot, + user_type, args.thread_root_message_index, args.message_id, args.content, @@ -88,7 +88,7 @@ fn send_message_impl(args: Args, state: &mut RuntimeState) -> Response { fn c2c_send_message_impl(args: C2CArgs, state: &mut RuntimeState) -> C2CResponse { let Caller { user_id, - is_bot, + user_type, display_name, } = match validate_caller(args.community_rules_accepted, state) { Ok(ok) => ok, @@ -96,7 +96,7 @@ fn c2c_send_message_impl(args: C2CArgs, state: &mut RuntimeState) -> C2CResponse }; // Bots can't call this c2c endpoint since it skips the validation - if is_bot && user_id != state.data.proposals_bot_user_id && user_id != OPENCHAT_BOT_USER_ID { + if user_type.is_bot() && !user_type.is_oc_controlled_bot() { return NotAuthorized; } @@ -142,7 +142,7 @@ fn c2c_send_message_impl(args: C2CArgs, state: &mut RuntimeState) -> C2CResponse struct Caller { user_id: UserId, - is_bot: bool, + user_type: UserType, display_name: Option, } @@ -171,14 +171,14 @@ fn validate_caller(community_rules_accepted: Option, state: &mut Runtim Ok(Caller { user_id: member.user_id, - is_bot: member.is_bot, + user_type: member.user_type, display_name: member.display_name().value.clone(), }) } } else if caller == state.data.user_index_canister_id { Ok(Caller { user_id: OPENCHAT_BOT_USER_ID, - is_bot: true, + user_type: UserType::OcControlledBot, display_name: None, }) } else { diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index 21765deed0..9943c91fbf 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Handle transfer fee changing in either direction ([#6064](https://github.com/open-chat-labs/open-chat/pull/6064)) - Bypass gates if user is invited ([#6110](https://github.com/open-chat-labs/open-chat/pull/6110)) - Return `is_invited` when previewing a group ([#6113](https://github.com/open-chat-labs/open-chat/pull/6113)) +- Use `UserType` rather than `is_bot` and `is_oc_controlled_bot` ([#6116](https://github.com/open-chat-labs/open-chat/pull/6116)) ### Fixed diff --git a/backend/canisters/group/api/src/lifecycle/init.rs b/backend/canisters/group/api/src/lifecycle/init.rs index 93cf047584..78f4513ff1 100644 --- a/backend/canisters/group/api/src/lifecycle/init.rs +++ b/backend/canisters/group/api/src/lifecycle/init.rs @@ -1,6 +1,8 @@ use candid::{CandidType, Principal}; use serde::{Deserialize, Serialize}; -use types::{AccessGate, BuildVersion, CanisterId, Document, GroupPermissions, GroupSubtype, Milliseconds, Rules, UserId}; +use types::{ + AccessGate, BuildVersion, CanisterId, Document, GroupPermissions, GroupSubtype, Milliseconds, Rules, UserId, UserType, +}; #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct Args { @@ -14,6 +16,8 @@ pub struct Args { pub permissions_v2: Option, pub created_by_principal: Principal, pub created_by_user_id: UserId, + #[serde(default)] + pub created_by_user_type: UserType, pub events_ttl: Option, pub mark_active_duration: Milliseconds, pub user_index_canister_id: CanisterId, diff --git a/backend/canisters/group/api/src/updates/c2c_join_group.rs b/backend/canisters/group/api/src/updates/c2c_join_group.rs index 52076bfa96..cb127bbb85 100644 --- a/backend/canisters/group/api/src/updates/c2c_join_group.rs +++ b/backend/canisters/group/api/src/updates/c2c_join_group.rs @@ -1,7 +1,7 @@ use candid::{CandidType, Principal}; use serde::{Deserialize, Serialize}; use types::{ - GateCheckFailedReason, GroupCanisterGroupChatSummary, TimestampMillis, UniquePersonProof, UserId, + GateCheckFailedReason, GroupCanisterGroupChatSummary, TimestampMillis, UniquePersonProof, UserId, UserType, VerifiedCredentialGateArgs, }; @@ -13,6 +13,8 @@ pub struct Args { pub correlation_id: u64, pub is_platform_moderator: bool, pub is_bot: bool, + #[serde(default)] + pub user_type: UserType, pub diamond_membership_expires_at: Option, pub verified_credential_args: Option, pub unique_person_proof: Option, diff --git a/backend/canisters/group/impl/src/lib.rs b/backend/canisters/group/impl/src/lib.rs index ec7aa15fe4..1fa65739fd 100644 --- a/backend/canisters/group/impl/src/lib.rs +++ b/backend/canisters/group/impl/src/lib.rs @@ -29,7 +29,7 @@ use types::{ AccessGate, Achievement, BuildVersion, CanisterId, ChatId, ChatMetrics, CommunityId, Cryptocurrency, Cycles, Document, Empty, EventIndex, FrozenGroupInfo, GroupCanisterGroupChatSummary, GroupMembership, GroupPermissions, GroupSubtype, MessageIndex, Milliseconds, MultiUserChat, Notification, PaymentGate, Rules, TimestampMillis, Timestamped, UserId, - MAX_THREADS_IN_SUMMARY, SNS_FEE_SHARE_PERCENT, + UserType, MAX_THREADS_IN_SUMMARY, SNS_FEE_SHARE_PERCENT, }; use user_canister::c2c_notify_achievement; use utils::consts::OPENCHAT_BOT_USER_ID; @@ -236,7 +236,7 @@ impl RuntimeState { args.min_visible_event_index, args.min_visible_message_index, args.mute_notifications, - args.is_bot, + args.user_type, ); if matches!(result, AddMemberResult::Success(_) | AddMemberResult::AlreadyInGroup) { @@ -476,6 +476,7 @@ impl Data { history_visible_to_new_joiners: bool, creator_principal: Principal, creator_user_id: UserId, + creator_user_type: UserType, events_ttl: Option, now: TimestampMillis, mark_active_duration: Milliseconds, @@ -507,7 +508,7 @@ impl Data { permissions.unwrap_or_default(), gate, events_ttl, - proposals_bot_user_id == creator_user_id, + creator_user_type, anonymized_chat_id, now, ); @@ -705,7 +706,7 @@ struct AddMemberArgs { min_visible_event_index: EventIndex, min_visible_message_index: MessageIndex, mute_notifications: bool, - is_bot: bool, + user_type: UserType, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/backend/canisters/group/impl/src/lifecycle/init.rs b/backend/canisters/group/impl/src/lifecycle/init.rs index 3d007007ed..56230330e9 100644 --- a/backend/canisters/group/impl/src/lifecycle/init.rs +++ b/backend/canisters/group/impl/src/lifecycle/init.rs @@ -25,6 +25,7 @@ fn init(args: Args) { args.history_visible_to_new_joiners, args.created_by_principal, args.created_by_user_id, + args.created_by_user_type, args.events_ttl, env.now(), args.mark_active_duration, diff --git a/backend/canisters/group/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/group/impl/src/lifecycle/post_upgrade.rs index e556f640f9..710a9d8c88 100644 --- a/backend/canisters/group/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/group/impl/src/lifecycle/post_upgrade.rs @@ -1,6 +1,6 @@ use crate::lifecycle::{init_env, init_state}; use crate::memory::get_upgrades_memory; -use crate::{read_state, Data}; +use crate::{mutate_state, read_state, Data}; use canister_logger::LogEntry; use canister_tracing_macros::trace; use group_canister::post_upgrade::Args; @@ -8,6 +8,7 @@ use ic_cdk::post_upgrade; use instruction_counts_log::InstructionCountFunctionId; use stable_memory::get_reader; use tracing::info; +use utils::consts::OPENCHAT_BOT_USER_ID; #[post_upgrade] #[trace] @@ -24,6 +25,11 @@ fn post_upgrade(args: Args) { info!(version = %args.wasm_version, "Post-upgrade complete"); + mutate_state(|state| { + let oc_controlled_bot_users = vec![OPENCHAT_BOT_USER_ID, state.data.proposals_bot_user_id]; + state.data.chat.set_user_types(&oc_controlled_bot_users); + }); + read_state(|state| { let now = state.env.now(); state diff --git a/backend/canisters/group/impl/src/updates/c2c_join_group.rs b/backend/canisters/group/impl/src/updates/c2c_join_group.rs index dde34f290f..3f1244b28f 100644 --- a/backend/canisters/group/impl/src/updates/c2c_join_group.rs +++ b/backend/canisters/group/impl/src/updates/c2c_join_group.rs @@ -114,7 +114,7 @@ fn c2c_join_group_impl(args: Args, state: &mut RuntimeState) -> Response { min_visible_event_index, min_visible_message_index, mute_notifications: state.data.chat.is_public.value, - is_bot: args.is_bot, + user_type: args.user_type, }) { AddResult::Success(participant) => { let invitation = state.data.chat.invited_users.remove(&args.user_id, now); diff --git a/backend/canisters/group/impl/src/updates/send_message.rs b/backend/canisters/group/impl/src/updates/send_message.rs index ae766429b3..cb8414555a 100644 --- a/backend/canisters/group/impl/src/updates/send_message.rs +++ b/backend/canisters/group/impl/src/updates/send_message.rs @@ -9,7 +9,7 @@ use group_canister::send_message_v2::{Response::*, *}; use group_chat_core::SendMessageResult; use types::{ Achievement, EventWrapper, GroupMessageNotification, Message, MessageContent, MessageIndex, Notification, TimestampMillis, - User, UserId, + User, UserId, UserType, }; #[update(candid = true, msgpack = true)] @@ -30,12 +30,12 @@ fn c2c_send_message(args: C2CArgs) -> C2CResponse { fn send_message_impl(args: Args, state: &mut RuntimeState) -> Response { match validate_caller(state) { - Ok(Caller { user_id, is_bot }) => { + Ok(Caller { user_id, user_type }) => { let now = state.env.now(); let result = state.data.chat.validate_and_send_message( user_id, - is_bot, + user_type, args.thread_root_message_index, args.message_id, args.content, @@ -68,9 +68,9 @@ fn send_message_impl(args: Args, state: &mut RuntimeState) -> Response { fn c2c_send_message_impl(args: C2CArgs, state: &mut RuntimeState) -> C2CResponse { match validate_caller(state) { - Ok(Caller { user_id, is_bot }) => { + Ok(Caller { user_id, user_type }) => { // Bots can't call this c2c endpoint since it skips the validation - if is_bot && user_id != state.data.proposals_bot_user_id && user_id != OPENCHAT_BOT_USER_ID { + if user_type.is_bot() && !user_type.is_oc_controlled_bot() { return NotAuthorized; } @@ -108,7 +108,7 @@ fn c2c_send_message_impl(args: C2CArgs, state: &mut RuntimeState) -> C2CResponse struct Caller { user_id: UserId, - is_bot: bool, + user_type: UserType, } fn validate_caller(state: &RuntimeState) -> Result { @@ -123,13 +123,13 @@ fn validate_caller(state: &RuntimeState) -> Result { } else { Ok(Caller { user_id: member.user_id, - is_bot: member.is_bot, + user_type: member.user_type, }) } } else if caller == state.data.user_index_canister_id { Ok(Caller { user_id: OPENCHAT_BOT_USER_ID, - is_bot: true, + user_type: UserType::OcControlledBot, }) } else { Err(CallerNotInGroup) diff --git a/backend/canisters/local_group_index/CHANGELOG.md b/backend/canisters/local_group_index/CHANGELOG.md index 585a5f152b..6a20d49d6e 100644 --- a/backend/canisters/local_group_index/CHANGELOG.md +++ b/backend/canisters/local_group_index/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Changed + +- Use `UserType` rather than `is_bot` and `is_oc_controlled_bot` ([#6116](https://github.com/open-chat-labs/open-chat/pull/6116)) + ### Removed - Remove `Invited` gate ([#6112](https://github.com/open-chat-labs/open-chat/pull/6112)) diff --git a/backend/canisters/local_group_index/impl/src/updates/c2c_create_community.rs b/backend/canisters/local_group_index/impl/src/updates/c2c_create_community.rs index 7239e1d473..1afe91243f 100644 --- a/backend/canisters/local_group_index/impl/src/updates/c2c_create_community.rs +++ b/backend/canisters/local_group_index/impl/src/updates/c2c_create_community.rs @@ -5,7 +5,7 @@ use canister_tracing_macros::trace; use community_canister::init::Args as InitCommunityCanisterArgs; use event_store_producer::EventBuilder; use local_group_index_canister::c2c_create_community::{Response::*, *}; -use types::{BuildVersion, CanisterId, CanisterWasm, CommunityCreatedEventPayload, CommunityId, Cycles, UserId}; +use types::{BuildVersion, CanisterId, CanisterWasm, CommunityCreatedEventPayload, CommunityId, Cycles, UserId, UserType}; use utils::canister; use utils::canister::CreateAndInstallError; use utils::consts::{min_cycles_balance, CREATE_CANISTER_CYCLES_FEE}; @@ -96,6 +96,7 @@ fn prepare(args: Args, state: &mut RuntimeState) -> Result permissions: args.permissions.unwrap_or_default(), created_by_principal: args.created_by_user_principal, created_by_user_id: args.created_by_user_id, + created_by_user_type: UserType::User, mark_active_duration: MARK_ACTIVE_DURATION, group_index_canister_id: state.data.group_index_canister_id, local_group_index_canister_id: state.env.canister_id(), diff --git a/backend/canisters/local_group_index/impl/src/updates/c2c_create_group.rs b/backend/canisters/local_group_index/impl/src/updates/c2c_create_group.rs index a373023236..0b4731758e 100644 --- a/backend/canisters/local_group_index/impl/src/updates/c2c_create_group.rs +++ b/backend/canisters/local_group_index/impl/src/updates/c2c_create_group.rs @@ -5,7 +5,7 @@ use canister_tracing_macros::trace; use event_store_producer::EventBuilder; use group_canister::init::Args as InitGroupCanisterArgs; use local_group_index_canister::c2c_create_group::{Response::*, *}; -use types::{BuildVersion, CanisterId, CanisterWasm, ChatId, Cycles, GroupCreatedEventPayload, UserId}; +use types::{BuildVersion, CanisterId, CanisterWasm, ChatId, Cycles, GroupCreatedEventPayload, UserId, UserType}; use utils::canister; use utils::canister::CreateAndInstallError; use utils::consts::{min_cycles_balance, CREATE_CANISTER_CYCLES_FEE}; @@ -97,6 +97,7 @@ fn prepare(args: Args, state: &mut RuntimeState) -> Result permissions_v2: args.permissions_v2, created_by_principal: args.created_by_user_principal, created_by_user_id: args.created_by_user_id, + created_by_user_type: UserType::User, events_ttl: args.events_ttl, mark_active_duration: MARK_ACTIVE_DURATION, group_index_canister_id: state.data.group_index_canister_id, diff --git a/backend/canisters/local_user_index/CHANGELOG.md b/backend/canisters/local_user_index/CHANGELOG.md index 7fe5acf8e2..823e6a3d66 100644 --- a/backend/canisters/local_user_index/CHANGELOG.md +++ b/backend/canisters/local_user_index/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add `is_oc_controlled_bot` to `GlobalUser` ([#6115](https://github.com/open-chat-labs/open-chat/pull/6115)) +### Changed + +- Use `UserType` rather than `is_bot` and `is_oc_controlled_bot` ([#6116](https://github.com/open-chat-labs/open-chat/pull/6116)) + ### Removed - Remove `Invited` gate ([#6112](https://github.com/open-chat-labs/open-chat/pull/6112)) diff --git a/backend/canisters/local_user_index/api/src/lib.rs b/backend/canisters/local_user_index/api/src/lib.rs index 94de3e53b0..d9040d719f 100644 --- a/backend/canisters/local_user_index/api/src/lib.rs +++ b/backend/canisters/local_user_index/api/src/lib.rs @@ -4,7 +4,7 @@ use types::nns::CryptoAmount; use types::{ CanisterId, ChannelLatestMessageIndex, ChatId, ChitEarnedReason, CommunityId, Cryptocurrency, DiamondMembershipPlanDuration, MessageContent, MessageContentInitial, MessageId, MessageIndex, PhoneNumber, ReferralType, - SuspensionDuration, TimestampMillis, UniquePersonProof, UpdateUserPrincipalArgs, User, UserId, + SuspensionDuration, TimestampMillis, UniquePersonProof, UpdateUserPrincipalArgs, User, UserId, UserType, }; mod lifecycle; @@ -75,7 +75,7 @@ pub struct UserRegistered { #[serde(default)] pub is_bot: bool, #[serde(default)] - pub is_oc_controlled_bot: bool, + pub user_type: UserType, pub referred_by: Option, } @@ -171,7 +171,7 @@ pub struct GlobalUser { pub diamond_membership_expires_at: Option, pub unique_person_proof: Option, #[serde(default)] - pub is_oc_controlled_bot: bool, + pub user_type: UserType, } #[derive(Serialize, Deserialize, Clone, Debug)] 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 3ffa502020..8e45320dac 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 @@ -2,7 +2,7 @@ use candid::Principal; use local_user_index_canister::GlobalUser; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; -use types::{TimestampMillis, UniquePersonProof, UserId}; +use types::{TimestampMillis, UniquePersonProof, UserId, UserType}; #[derive(Serialize, Deserialize, Default)] pub struct GlobalUserMap { @@ -17,14 +17,14 @@ pub struct GlobalUserMap { } impl GlobalUserMap { - pub fn add(&mut self, principal: Principal, user_id: UserId, is_bot: bool, is_oc_controlled_bot: bool) { + pub fn add(&mut self, principal: Principal, user_id: UserId, user_type: UserType) { self.user_id_to_principal.insert(user_id, principal); self.principal_to_user_id.insert(principal, user_id); - if is_bot { + if user_type.is_bot() { self.bots.insert(user_id); - if is_oc_controlled_bot { + if user_type.is_oc_controlled_bot() { self.oc_controlled_bot_users.insert(user_id); } } @@ -102,6 +102,13 @@ impl GlobalUserMap { fn hydrate_user(&self, user_id: UserId, principal: Principal) -> GlobalUser { let is_bot = self.bots.contains(&user_id); + let user_type = if !is_bot { + UserType::User + } else if self.oc_controlled_bot_users.contains(&user_id) { + UserType::OcControlledBot + } else { + UserType::Bot + }; GlobalUser { user_id, @@ -110,7 +117,7 @@ impl GlobalUserMap { is_platform_moderator: self.platform_moderators.contains(&user_id), diamond_membership_expires_at: self.diamond_membership_expiry_dates.get(&user_id).copied(), unique_person_proof: self.unique_person_proofs.get(&user_id).cloned(), - is_oc_controlled_bot: is_bot && self.oc_controlled_bot_users.contains(&user_id), + user_type, } } } diff --git a/backend/canisters/local_user_index/impl/src/updates/c2c_notify_user_index_events.rs b/backend/canisters/local_user_index/impl/src/updates/c2c_notify_user_index_events.rs index 81fa1f3a5d..34967b4f99 100644 --- a/backend/canisters/local_user_index/impl/src/updates/c2c_notify_user_index_events.rs +++ b/backend/canisters/local_user_index/impl/src/updates/c2c_notify_user_index_events.rs @@ -73,10 +73,7 @@ fn handle_event(event: Event, state: &mut RuntimeState) { ); } Event::UserRegistered(ev) => { - state - .data - .global_users - .add(ev.user_principal, ev.user_id, ev.is_bot, ev.is_oc_controlled_bot); + state.data.global_users.add(ev.user_principal, ev.user_id, ev.user_type); if let Some(referred_by) = ev.referred_by { if state.data.local_users.get(&referred_by).is_some() { diff --git a/backend/canisters/local_user_index/impl/src/updates/join_channel.rs b/backend/canisters/local_user_index/impl/src/updates/join_channel.rs index 62e6c39890..9d50ef1a68 100644 --- a/backend/canisters/local_user_index/impl/src/updates/join_channel.rs +++ b/backend/canisters/local_user_index/impl/src/updates/join_channel.rs @@ -20,6 +20,7 @@ async fn join_channel(args: Args) -> Response { invite_code: args.invite_code, is_platform_moderator: user_details.is_platform_moderator, is_bot: user_details.is_bot, + user_type: user_details.user_type, diamond_membership_expires_at: user_details.diamond_membership_expires_at, verified_credential_args: args.verified_credential_args.clone(), unique_person_proof: user_details.unique_person_proof.clone(), diff --git a/backend/canisters/local_user_index/impl/src/updates/join_community.rs b/backend/canisters/local_user_index/impl/src/updates/join_community.rs index 91e4443869..c1b10c79f3 100644 --- a/backend/canisters/local_user_index/impl/src/updates/join_community.rs +++ b/backend/canisters/local_user_index/impl/src/updates/join_community.rs @@ -19,6 +19,7 @@ async fn join_community(args: Args) -> Response { invite_code: args.invite_code, is_platform_moderator: user_details.is_platform_moderator, is_bot: user_details.is_bot, + user_type: user_details.user_type, diamond_membership_expires_at: user_details.diamond_membership_expires_at, verified_credential_args: args.verified_credential_args, unique_person_proof: user_details.unique_person_proof.clone(), diff --git a/backend/canisters/local_user_index/impl/src/updates/join_group.rs b/backend/canisters/local_user_index/impl/src/updates/join_group.rs index 133bcda189..5aac69fe04 100644 --- a/backend/canisters/local_user_index/impl/src/updates/join_group.rs +++ b/backend/canisters/local_user_index/impl/src/updates/join_group.rs @@ -23,6 +23,7 @@ async fn join_group(args: Args) -> Response { correlation_id: args.correlation_id, is_platform_moderator: user_details.is_platform_moderator, is_bot: user_details.is_bot, + user_type: user_details.user_type, diamond_membership_expires_at: user_details.diamond_membership_expires_at, verified_credential_args: args.verified_credential_args.clone(), unique_person_proof: user_details.unique_person_proof.clone(), diff --git a/backend/canisters/local_user_index/impl/src/updates/register_user.rs b/backend/canisters/local_user_index/impl/src/updates/register_user.rs index d0ad04b789..b27bb3f38e 100644 --- a/backend/canisters/local_user_index/impl/src/updates/register_user.rs +++ b/backend/canisters/local_user_index/impl/src/updates/register_user.rs @@ -5,7 +5,7 @@ use canister_tracing_macros::trace; use ic_cdk::update; use ledger_utils::default_ledger_account; use local_user_index_canister::register_user::{Response::*, *}; -use types::{BuildVersion, CanisterId, CanisterWasm, Cycles, MessageContentInitial, TextContent, UserId}; +use types::{BuildVersion, CanisterId, CanisterWasm, Cycles, MessageContentInitial, TextContent, UserId, UserType}; use user_canister::init::Args as InitUserCanisterArgs; use user_canister::{Event as UserEvent, ReferredUserRegistered}; use user_index_canister::{Event as UserIndexEvent, UserRegistered}; @@ -191,7 +191,7 @@ fn commit( ) { let now = state.env.now(); state.data.local_users.add(user_id, principal, wasm_version, now); - state.data.global_users.add(principal, user_id, false, false); + state.data.global_users.add(principal, user_id, UserType::User); state.push_event_to_user_index(UserIndexEvent::UserRegistered(Box::new(UserRegistered { principal, diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index f0db64f71a..52dd30b8e1 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Improve check for empty and dormant users ([#6073](https://github.com/open-chat-labs/open-chat/pull/6073)) - Store CHIT balances per month ([#6087](https://github.com/open-chat-labs/open-chat/pull/6087)) - Hack to include all built up CHIT in the July airdrop ([#6104](https://github.com/open-chat-labs/open-chat/pull/6104)) +- Use `UserType` rather than `is_bot` and `is_oc_controlled_bot` ([#6116](https://github.com/open-chat-labs/open-chat/pull/6116)) ## [[2.0.1243](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1243-user)] - 2024-07-17 diff --git a/backend/canisters/user/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/user/impl/src/lifecycle/post_upgrade.rs index e83adaba65..ee6bc98b55 100644 --- a/backend/canisters/user/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/user/impl/src/lifecycle/post_upgrade.rs @@ -1,6 +1,6 @@ use crate::lifecycle::{init_env, init_state}; use crate::memory::get_upgrades_memory; -use crate::{read_state, Data}; +use crate::{mutate_state, read_state, Data}; use canister_logger::LogEntry; use canister_tracing_macros::trace; use ic_cdk::post_upgrade; @@ -28,6 +28,12 @@ fn post_upgrade(args: Args) { info!(version = %args.wasm_version, "Post-upgrade complete"); + mutate_state(|state| { + for chat in state.data.direct_chats.iter_mut() { + chat.set_user_type(); + } + }); + read_state(|state| { if state.data.direct_chats.len() <= 1 && state.data.group_chats.len() == 0 diff --git a/backend/canisters/user/impl/src/model/direct_chat.rs b/backend/canisters/user/impl/src/model/direct_chat.rs index 465f9daa0f..060014beea 100644 --- a/backend/canisters/user/impl/src/model/direct_chat.rs +++ b/backend/canisters/user/impl/src/model/direct_chat.rs @@ -5,9 +5,10 @@ use serde::{Deserialize, Serialize}; use std::cmp::min; use types::{ DirectChatSummary, DirectChatSummaryUpdates, EventWrapper, Message, MessageId, MessageIndex, Milliseconds, OptionUpdate, - TimestampMillis, Timestamped, UserId, + TimestampMillis, Timestamped, UserId, UserType, }; use user_canister::SendMessageArgs; +use utils::consts::OPENCHAT_BOT_USER_ID; #[derive(Serialize, Deserialize)] pub struct DirectChat { @@ -20,13 +21,21 @@ pub struct DirectChat { pub notifications_muted: Timestamped, pub archived: Timestamped, pub is_bot: bool, + #[serde(default)] + pub user_type: UserType, pub unconfirmed: Vec, } impl DirectChat { + pub fn set_user_type(&mut self) { + if self.is_bot { + self.user_type = if self.them == OPENCHAT_BOT_USER_ID { UserType::OcControlledBot } else { UserType::Bot }; + } + } + pub fn new( them: UserId, - is_bot: bool, + user_type: UserType, events_ttl: Option, anonymized_chat_id: u128, now: TimestampMillis, @@ -40,7 +49,8 @@ impl DirectChat { read_by_them_up_to: Timestamped::new(None, now), notifications_muted: Timestamped::new(false, now), archived: Timestamped::new(false, now), - is_bot, + is_bot: user_type.is_bot(), + user_type, unconfirmed: Vec::new(), } } diff --git a/backend/canisters/user/impl/src/model/direct_chats.rs b/backend/canisters/user/impl/src/model/direct_chats.rs index 72b8e165ac..b943e699e4 100644 --- a/backend/canisters/user/impl/src/model/direct_chats.rs +++ b/backend/canisters/user/impl/src/model/direct_chats.rs @@ -3,7 +3,7 @@ use chat_events::{ChatInternal, ChatMetricsInternal}; use serde::{Deserialize, Serialize}; use std::collections::hash_map::Entry::Vacant; use std::collections::{BTreeSet, HashMap}; -use types::{ChatId, TimestampMillis, Timestamped, UserId}; +use types::{ChatId, TimestampMillis, Timestamped, UserId, UserType}; #[derive(Serialize, Deserialize, Default)] pub struct DirectChats { @@ -25,12 +25,12 @@ impl DirectChats { pub fn create( &mut self, their_user_id: UserId, - is_bot: bool, + their_user_type: UserType, anonymized_id: u128, now: TimestampMillis, ) -> &mut DirectChat { if let Vacant(e) = self.direct_chats.entry(their_user_id.into()) { - e.insert(DirectChat::new(their_user_id, is_bot, None, anonymized_id, now)) + e.insert(DirectChat::new(their_user_id, their_user_type, None, anonymized_id, now)) } else { unreachable!() } diff --git a/backend/canisters/user/impl/src/openchat_bot.rs b/backend/canisters/user/impl/src/openchat_bot.rs index dcf1173c76..56a95eff31 100644 --- a/backend/canisters/user/impl/src/openchat_bot.rs +++ b/backend/canisters/user/impl/src/openchat_bot.rs @@ -2,7 +2,7 @@ use crate::updates::c2c_send_messages::{handle_message_impl, HandleMessageArgs}; use crate::{RuntimeState, BASIC_GROUP_CREATION_LIMIT, PREMIUM_GROUP_CREATION_LIMIT}; use chat_events::{MessageContentInternal, TextContentInternal}; use ic_ledger_types::Tokens; -use types::{ChannelId, CommunityId, EventWrapper, Message, SuspensionDuration, User, UserId}; +use types::{ChannelId, CommunityId, EventWrapper, Message, SuspensionDuration, User, UserId, UserType}; use user_canister::{C2CReplyContext, PhoneNumberConfirmed, ReferredUserRegistered, StorageUpgraded, UserSuspended}; use utils::consts::{OPENCHAT_BOT_USERNAME, OPENCHAT_BOT_USER_ID}; use utils::format::format_to_decimal_places; @@ -158,7 +158,7 @@ pub(crate) fn send_message_with_reply( content, replies_to, forwarding: false, - is_bot: true, + sender_user_type: UserType::OcControlledBot, sender_avatar_id: None, push_message_sent_event: true, mute_notification, 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 7c12e0399c..360a54b511 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 @@ -12,7 +12,8 @@ use chat_events::{ use event_store_producer_cdk_runtime::CdkRuntime; use ledger_utils::format_crypto_amount_with_symbol; use types::{ - Achievement, DirectMessageTipped, DirectReactionAddedNotification, EventIndex, Notification, UserId, VideoCallPresence, + Achievement, DirectMessageTipped, DirectReactionAddedNotification, EventIndex, Notification, UserId, UserType, + VideoCallPresence, }; use user_canister::c2c_notify_user_canister_events::{Response::*, *}; use user_canister::{SendMessagesArgs, ToggleReactionArgs, UserCanisterEvent}; @@ -24,10 +25,11 @@ async fn c2c_notify_user_canister_events(args: Args) -> Response { run_regular_jobs(); let caller_user_id = match read_state(get_sender_status) { - crate::updates::c2c_send_messages::SenderStatus::Ok(user_id) => user_id, + crate::updates::c2c_send_messages::SenderStatus::Ok(user_id, UserType::User) => user_id, + crate::updates::c2c_send_messages::SenderStatus::Ok(..) => panic!("This request is from an OpenChat bot user"), crate::updates::c2c_send_messages::SenderStatus::Blocked => return Blocked, crate::updates::c2c_send_messages::SenderStatus::UnknownUser(local_user_index_canister_id, user_id) => { - if !verify_user(local_user_index_canister_id, user_id, false).await { + if !matches!(verify_user(local_user_index_canister_id, user_id).await, Some(UserType::User)) { panic!("This request is not from an OpenChat user"); } user_id @@ -138,7 +140,7 @@ fn send_messages(args: SendMessagesArgs, sender: UserId, state: &mut RuntimeStat content: message.content, replies_to: message.replies_to, forwarding: message.forwarding, - is_bot: false, + sender_user_type: UserType::User, sender_avatar_id: args.sender_avatar_id, push_message_sent_event: false, mute_notification: message.message_filter_failed.is_some(), 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 8fa3853063..d7ac222b50 100644 --- a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs +++ b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs @@ -6,7 +6,7 @@ use ic_cdk::update; use rand::Rng; use types::{ CanisterId, DirectMessageNotification, EventWrapper, Message, MessageId, MessageIndex, Notification, TimestampMillis, User, - UserId, + UserId, UserType, }; use user_canister::C2CReplyContext; @@ -17,19 +17,21 @@ async fn c2c_handle_bot_messages( ) -> user_canister::c2c_handle_bot_messages::Response { let (sender_status, now) = read_state(|state| (get_sender_status(state), state.env.now())); - let sender = match sender_status { - SenderStatus::Ok(user_id) => user_id, + let (sender, sender_user_type) = match sender_status { + SenderStatus::Ok(user_id, user_type) => (user_id, user_type), SenderStatus::Blocked => return user_canister::c2c_handle_bot_messages::Response::Blocked, SenderStatus::UnknownUser(local_user_index_canister_id, user_id) => { - if !verify_user(local_user_index_canister_id, user_id, true).await { - panic!("This request is not from a bot registered with OpenChat"); - } - user_id + let user_type = match verify_user(local_user_index_canister_id, user_id).await { + Some(UserType::Bot) => UserType::Bot, + Some(UserType::OcControlledBot) => UserType::OcControlledBot, + _ => panic!("This request is not from a bot registered with OpenChat"), + }; + (user_id, user_type) } }; for message in args.messages.iter() { - if let Err(error) = message.content.validate_for_new_message(true, true, false, now) { + if let Err(error) = message.content.validate_for_new_message(true, sender_user_type, false, now) { return user_canister::c2c_handle_bot_messages::Response::ContentValidationError(error); } } @@ -48,7 +50,7 @@ async fn c2c_handle_bot_messages( content: message.content.into(), replies_to: None, forwarding: false, - is_bot: true, + sender_user_type, sender_avatar_id: None, push_message_sent_event: true, mentioned: Vec::new(), @@ -73,7 +75,7 @@ pub(crate) struct HandleMessageArgs { pub content: MessageContentInternal, pub replies_to: Option, pub forwarding: bool, - pub is_bot: bool, + pub sender_user_type: UserType, pub sender_avatar_id: Option, pub push_message_sent_event: bool, pub mute_notification: bool, @@ -83,7 +85,7 @@ pub(crate) struct HandleMessageArgs { } pub(crate) enum SenderStatus { - Ok(UserId), + Ok(UserId, UserType), Blocked, UnknownUser(CanisterId, UserId), } @@ -93,22 +95,22 @@ pub(crate) fn get_sender_status(state: &RuntimeState) -> SenderStatus { if state.data.blocked_users.contains(&sender) { SenderStatus::Blocked - } else if state.data.direct_chats.get(&sender.into()).is_some() { - SenderStatus::Ok(sender) + } else if let Some(user_type) = state.data.direct_chats.get(&sender.into()).map(|c| c.user_type) { + SenderStatus::Ok(sender, user_type) } else { SenderStatus::UnknownUser(state.data.local_user_index_canister_id, sender) } } -pub(crate) async fn verify_user(local_user_index_canister_id: CanisterId, user_id: UserId, is_bot: bool) -> bool { +pub(crate) async fn verify_user(local_user_index_canister_id: CanisterId, user_id: UserId) -> Option { let args = local_user_index_canister::c2c_lookup_user::Args { user_id_or_principal: user_id.into(), }; if let Ok(response) = local_user_index_canister_c2c_client::c2c_lookup_user(local_user_index_canister_id, &args).await { if let local_user_index_canister::c2c_lookup_user::Response::Success(r) = response { - r.is_bot == is_bot + Some(r.user_type) } else { - false + None } } else { panic!("Failed to call local_user_index to verify user"); @@ -126,7 +128,7 @@ pub(crate) fn handle_message_impl(args: HandleMessageArgs, state: &mut RuntimeSt state .data .direct_chats - .create(args.sender, args.is_bot, state.env.rng().gen(), args.now) + .create(args.sender, args.sender_user_type, state.env.rng().gen(), args.now) }; let thread_root_message_index = args.thread_root_message_id.map(|id| chat.main_message_id_to_index(id)); @@ -139,7 +141,7 @@ pub(crate) fn handle_message_impl(args: HandleMessageArgs, state: &mut RuntimeSt mentioned: Vec::new(), replies_to, forwarded: args.forwarding, - sender_is_bot: args.is_bot, + sender_is_bot: args.sender_user_type.is_bot(), block_level_markdown: args.block_level_markdown, correlation_id: 0, now: args.now, @@ -154,7 +156,7 @@ pub(crate) fn handle_message_impl(args: HandleMessageArgs, state: &mut RuntimeSt args.push_message_sent_event.then_some(&mut state.data.event_store_client), ); - if args.is_bot { + if args.sender_user_type.is_bot() { chat.mark_read_up_to(message_event.event.message_index, false, args.now); } diff --git a/backend/canisters/user/impl/src/updates/send_message.rs b/backend/canisters/user/impl/src/updates/send_message.rs index 7d033dd762..5367bd598f 100644 --- a/backend/canisters/user/impl/src/updates/send_message.rs +++ b/backend/canisters/user/impl/src/updates/send_message.rs @@ -12,7 +12,7 @@ use rand::Rng; use types::{ Achievement, BlobReference, CanisterId, Chat, ChatId, CompletedCryptoTransaction, ContentValidationError, CryptoTransaction, EventWrapper, Message, MessageContent, MessageContentInitial, MessageId, MessageIndex, P2PSwapLocation, - TimestampMillis, UserId, + TimestampMillis, UserId, UserType, }; use user_canister::send_message_v2::{Response::*, *}; use user_canister::{C2CReplyContext, SendMessageArgs, SendMessagesArgs, UserCanisterEvent}; @@ -25,21 +25,21 @@ use utils::consts::{MEMO_MESSAGE, OPENCHAT_BOT_USER_ID}; async fn send_message_v2(mut args: Args) -> Response { run_regular_jobs(); - let (my_user_id, user_type) = match mutate_state(|state| validate_request(&args, state)) { + let (my_user_id, recipient_type) = match mutate_state(|state| validate_request(&args, state)) { ValidateRequestResult::Valid(u, t) => (u, t), ValidateRequestResult::Invalid(response) => return response, ValidateRequestResult::RecipientUnknown(u, local_user_index_canister_id) => { let c2c_args = local_user_index_canister::c2c_lookup_user::Args { user_id_or_principal: args.recipient.into(), }; - match local_user_index_canister_c2c_client::c2c_lookup_user(local_user_index_canister_id, &c2c_args).await { - Ok(local_user_index_canister::c2c_lookup_user::Response::Success(result)) if result.is_bot => { - (u, UserType::Bot) - } - Ok(local_user_index_canister::c2c_lookup_user::Response::Success(_)) => (u, UserType::User), - Ok(local_user_index_canister::c2c_lookup_user::Response::UserNotFound) => return RecipientNotFound, - Err(error) => return InternalError(format!("{error:?}")), - } + let user_type = + match local_user_index_canister_c2c_client::c2c_lookup_user(local_user_index_canister_id, &c2c_args).await { + Ok(local_user_index_canister::c2c_lookup_user::Response::Success(result)) if result.is_bot => UserType::Bot, + Ok(local_user_index_canister::c2c_lookup_user::Response::Success(_)) => UserType::User, + Ok(local_user_index_canister::c2c_lookup_user::Response::UserNotFound) => return RecipientNotFound, + Err(error) => return InternalError(format!("{error:?}")), + }; + (u, RecipientType::Other(user_type)) } }; @@ -49,9 +49,6 @@ async fn send_message_v2(mut args: Args) -> Response { // the message to contain the completed transfer. match &mut args.content { MessageContentInitial::Crypto(c) => { - if user_type.is_self() { - return InvalidRequest("Cannot send crypto to yourself".to_string()); - } let mut pending_transaction = match &c.transfer { CryptoTransaction::Pending(t) => t.clone().set_memo(&MEMO_MESSAGE), _ => return InvalidRequest("Transaction must be of type 'Pending'".to_string()), @@ -61,7 +58,7 @@ async fn send_message_v2(mut args: Args) -> Response { } // When transferring to bot users, each user transfers to their own subaccount, this way it // is trivial for the bots to keep track of each user's funds - if user_type.is_bot() { + if recipient_type.user_type().is_bot() { pending_transaction.set_recipient(args.recipient.into(), Principal::from(my_user_id).into()); } @@ -106,28 +103,37 @@ async fn send_message_v2(mut args: Args) -> Response { _ => {} }; - mutate_state(|state| send_message_impl(args, completed_transfer, p2p_swap_id, user_type, state)) + mutate_state(|state| send_message_impl(args, completed_transfer, p2p_swap_id, recipient_type, state)) } -enum UserType { +#[derive(Copy, Clone)] +enum RecipientType { _Self, - User, - Bot, + Other(UserType), } -impl UserType { +impl RecipientType { fn is_self(&self) -> bool { - matches!(self, UserType::_Self) + matches!(self, RecipientType::_Self) } - fn is_bot(&self) -> bool { - matches!(self, UserType::Bot) + fn user_type(self) -> UserType { + self.into() + } +} + +impl From for UserType { + fn from(value: RecipientType) -> Self { + match value { + RecipientType::_Self => UserType::User, + RecipientType::Other(u) => u, + } } } #[allow(clippy::large_enum_variant)] enum ValidateRequestResult { - Valid(UserId, UserType), + Valid(UserId, RecipientType), Invalid(Response), RecipientUnknown(UserId, CanisterId), // UserId, UserIndexCanisterId } @@ -166,7 +172,10 @@ fn validate_request(args: &Args, state: &mut RuntimeState) -> ValidateRequestRes } } - if let Err(error) = args.content.validate_for_new_message(true, false, args.forwarding, now) { + if let Err(error) = args + .content + .validate_for_new_message(true, UserType::User, args.forwarding, now) + { ValidateRequestResult::Invalid(match error { ContentValidationError::Empty => MessageEmpty, ContentValidationError::TextTooLong(max_length) => TextTooLong(max_length), @@ -181,24 +190,16 @@ fn validate_request(args: &Args, state: &mut RuntimeState) -> ValidateRequestRes } }) } else if args.recipient == my_user_id { - if matches!( - args.content, - MessageContentInitial::Crypto(_) | MessageContentInitial::P2PSwap(_) - ) { + if args.content.contains_crypto_transfer() { ValidateRequestResult::Invalid(TransferCannotBeToSelf) } else { - ValidateRequestResult::Valid(my_user_id, UserType::_Self) + ValidateRequestResult::Valid(my_user_id, RecipientType::_Self) } } else if let Some(chat) = state.data.direct_chats.get(&args.recipient.into()) { - let user_type = if chat.is_bot { - if matches!(args.content, MessageContentInitial::P2PSwap(_)) { - return ValidateRequestResult::Invalid(InvalidRequest("Cannot open a P2P swap with a bot".to_string())); - } - UserType::Bot - } else { - UserType::User - }; - ValidateRequestResult::Valid(my_user_id, user_type) + if chat.user_type.is_bot() && matches!(args.content, MessageContentInitial::P2PSwap(_)) { + return ValidateRequestResult::Invalid(InvalidRequest("Cannot open a P2P swap with a bot".to_string())); + } + ValidateRequestResult::Valid(my_user_id, RecipientType::Other(chat.user_type)) } else { ValidateRequestResult::RecipientUnknown(my_user_id, state.data.local_user_index_canister_id) } @@ -208,7 +209,7 @@ fn send_message_impl( args: Args, completed_transfer: Option, p2p_swap_id: Option, - user_type: UserType, + recipient_type: RecipientType, state: &mut RuntimeState, ) -> Response { let now = state.env.now(); @@ -241,12 +242,12 @@ fn send_message_impl( state .data .direct_chats - .create(recipient, user_type.is_bot(), state.env.rng().gen(), now) + .create(recipient, recipient_type.into(), state.env.rng().gen(), now) }; let message_event = chat.push_message(true, push_message_args, None, Some(&mut state.data.event_store_client)); - if !user_type.is_self() { + if !recipient_type.is_self() { let send_message_args = SendMessageArgs { thread_root_message_id: args.thread_root_message_index.map(|i| chat.main_message_index_to_id(i)), message_id: args.message_id, @@ -271,7 +272,7 @@ fn send_message_impl( let sender_name = state.data.username.value.clone(); let sender_display_name = state.data.display_name.value.clone(); - if user_type.is_bot() { + if recipient_type.user_type().is_bot() { ic_cdk::spawn(send_to_bot_canister( recipient, message_event.event.message_index, 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 e2d2bcfc8e..7fe3cf2879 100644 --- a/backend/canisters/user/impl/src/updates/start_video_call.rs +++ b/backend/canisters/user/impl/src/updates/start_video_call.rs @@ -6,7 +6,7 @@ use chat_events::{CallParticipantInternal, MessageContentInternal, PushMessageAr use ic_cdk::update; use rand::Rng; use types::{ - DirectMessageNotification, EventWrapper, Message, MessageId, MessageIndex, Milliseconds, Notification, UserId, + DirectMessageNotification, EventWrapper, Message, MessageId, MessageIndex, Milliseconds, Notification, UserId, UserType, VideoCallPresence, VideoCallType, }; use user_canister::start_video_call::{Response::*, *}; @@ -107,7 +107,10 @@ pub fn handle_start_video_call( let chat = if let Some(c) = state.data.direct_chats.get_mut(&other.into()) { c } else { - state.data.direct_chats.create(other, false, state.env.rng().gen(), now) + state + .data + .direct_chats + .create(other, UserType::User, state.env.rng().gen(), now) }; let mute_notification = their_message_index.is_some() || chat.notifications_muted.value; diff --git a/backend/canisters/user_index/CHANGELOG.md b/backend/canisters/user_index/CHANGELOG.md index 57a993c3d8..8132241fa4 100644 --- a/backend/canisters/user_index/CHANGELOG.md +++ b/backend/canisters/user_index/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Set `is_oc_controlled_bot` to true when registering the ProposalsBot ([#6115](https://github.com/open-chat-labs/open-chat/pull/6115)) +- Use `UserType` rather than `is_bot` and `is_oc_controlled_bot` ([#6116](https://github.com/open-chat-labs/open-chat/pull/6116)) ## [[2.0.1255](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1255-user_index)] - 2024-07-25 diff --git a/backend/canisters/user_index/impl/src/lib.rs b/backend/canisters/user_index/impl/src/lib.rs index 5157c30a58..c89b32a132 100644 --- a/backend/canisters/user_index/impl/src/lib.rs +++ b/backend/canisters/user_index/impl/src/lib.rs @@ -27,7 +27,7 @@ use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::time::Duration; use types::{ BuildVersion, CanisterId, CanisterWasm, ChatId, Cryptocurrency, Cycles, DiamondMembershipFees, Milliseconds, - TimestampMillis, Timestamped, UserId, + TimestampMillis, Timestamped, UserId, UserType, }; use utils::canister::{CanistersRequiringUpgrade, FailedUpgradeCount}; use utils::canister_event_sync_queue::CanisterEventSyncQueue; @@ -397,8 +397,7 @@ impl Data { "ProposalsBot".to_string(), 0, None, - true, - true, + UserType::OcControlledBot, ); data 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 c163dddb67..2b05422dd1 100644 --- a/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs +++ b/backend/canisters/user_index/impl/src/lifecycle/post_upgrade.rs @@ -1,11 +1,13 @@ use crate::lifecycle::{init_env, init_state}; use crate::memory::get_upgrades_memory; -use crate::Data; +use crate::{mutate_state, Data}; use canister_logger::LogEntry; use canister_tracing_macros::trace; use ic_cdk::post_upgrade; +use local_user_index_canister::UserRegistered; use stable_memory::get_reader; use tracing::info; +use types::UserType; use user_index_canister::post_upgrade::Args; use utils::cycles::init_cycles_dispenser_client; @@ -23,5 +25,19 @@ fn post_upgrade(args: Args) { init_cycles_dispenser_client(data.cycles_dispenser_canister_id, data.test_mode); init_state(env, data, args.wasm_version); + mutate_state(|state| { + state.push_event_to_all_local_user_indexes( + local_user_index_canister::Event::UserRegistered(UserRegistered { + user_id: state.data.proposals_bot_canister_id.into(), + user_principal: state.data.proposals_bot_canister_id, + username: "ProposalsBot".to_string(), + is_bot: true, + user_type: UserType::OcControlledBot, + referred_by: None, + }), + None, + ); + }); + info!(version = %args.wasm_version, "Post-upgrade complete"); } diff --git a/backend/canisters/user_index/impl/src/model/user.rs b/backend/canisters/user_index/impl/src/model/user.rs index b6894bdfa6..3ac6303e25 100644 --- a/backend/canisters/user_index/impl/src/model/user.rs +++ b/backend/canisters/user_index/impl/src/model/user.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use types::{ is_default, is_empty_slice, CyclesTopUp, CyclesTopUpInternal, PhoneNumber, RegistrationFee, SuspensionAction, SuspensionDuration, TimestampMillis, UniquePersonProof, UserId, UserSummary, UserSummaryStable, UserSummaryV2, - UserSummaryVolatile, + UserSummaryVolatile, UserType, }; use utils::time::MonthKey; @@ -40,8 +40,8 @@ pub struct User { pub referred_by: Option, #[serde(rename = "ib", default, skip_serializing_if = "is_default")] pub is_bot: bool, - #[serde(rename = "ocb", default, skip_serializing_if = "is_default")] - pub is_oc_controlled_bot: bool, + #[serde(rename = "ut", default, skip_serializing_if = "is_default")] + pub user_type: UserType, #[serde(rename = "sd", default, skip_serializing_if = "Option::is_none")] pub suspension_details: Option, #[serde( @@ -103,8 +103,7 @@ impl User { username: String, now: TimestampMillis, referred_by: Option, - is_bot: bool, - is_oc_controlled_bot: bool, + user_type: UserType, ) -> User { #[allow(deprecated)] User { @@ -121,8 +120,8 @@ impl User { account_billing: AccountBilling::default(), phone_status: PhoneStatus::None, referred_by, - is_bot, - is_oc_controlled_bot, + is_bot: user_type.is_bot(), + user_type, suspension_details: None, diamond_membership_details: DiamondMembershipDetailsInternal::default(), moderation_flags_enabled: 0, @@ -246,7 +245,7 @@ impl Default for User { phone_status: PhoneStatus::None, referred_by: None, is_bot: false, - is_oc_controlled_bot: false, + user_type: UserType::User, suspension_details: None, diamond_membership_details: DiamondMembershipDetailsInternal::default(), moderation_flags_enabled: 0, diff --git a/backend/canisters/user_index/impl/src/model/user_map.rs b/backend/canisters/user_index/impl/src/model/user_map.rs index a927c72e45..6a69b0c7bf 100644 --- a/backend/canisters/user_index/impl/src/model/user_map.rs +++ b/backend/canisters/user_index/impl/src/model/user_map.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeSet, HashMap}; use std::ops::RangeFrom; use tracing::info; -use types::{CyclesTopUp, Milliseconds, SuspensionDuration, TimestampMillis, UniquePersonProof, UserId}; +use types::{CyclesTopUp, Milliseconds, SuspensionDuration, TimestampMillis, UniquePersonProof, UserId, UserType}; use utils::case_insensitive_hash_map::CaseInsensitiveHashMap; use utils::time::MonthKey; @@ -61,13 +61,12 @@ impl UserMap { username: String, now: TimestampMillis, referred_by: Option, - is_bot: bool, - is_oc_controlled_bot: bool, + user_type: UserType, ) { self.username_to_user_id.insert(&username, user_id); self.principal_to_user_id.insert(principal, user_id); - let user = User::new(principal, user_id, username, now, referred_by, is_bot, is_oc_controlled_bot); + let user = User::new(principal, user_id, username, now, referred_by, user_type); self.users.insert(user_id, user); if let Some(ref_by) = referred_by { @@ -380,8 +379,7 @@ impl UserMap { user.username.clone(), user.date_created, None, - false, - false, + UserType::User, ); self.update(user, date_created, false); } @@ -453,9 +451,9 @@ mod tests { let user_id2: UserId = Principal::from_slice(&[3, 2]).into(); let user_id3: UserId = Principal::from_slice(&[3, 3]).into(); - user_map.register(principal1, user_id1, username1.clone(), 1, None, false, false); - user_map.register(principal2, user_id2, username2.clone(), 2, None, false, false); - user_map.register(principal3, user_id3, username3.clone(), 3, None, false, false); + user_map.register(principal1, user_id1, username1.clone(), 1, None, UserType::User); + user_map.register(principal2, user_id2, username2.clone(), 2, None, UserType::User); + user_map.register(principal3, user_id3, username3.clone(), 3, None, UserType::User); let principal_to_user_id: Vec<_> = user_map .principal_to_user_id @@ -492,7 +490,7 @@ mod tests { let user_id = Principal::from_slice(&[1, 1]).into(); - user_map.register(principal, user_id, username1, 1, None, false, false); + user_map.register(principal, user_id, username1, 1, None, UserType::User); if let Some(original) = user_map.get_by_principal(&principal) { let mut updated = original.clone(); diff --git a/backend/canisters/user_index/impl/src/timer_job_types.rs b/backend/canisters/user_index/impl/src/timer_job_types.rs index 8454cd029a..748f923d39 100644 --- a/backend/canisters/user_index/impl/src/timer_job_types.rs +++ b/backend/canisters/user_index/impl/src/timer_job_types.rs @@ -262,6 +262,7 @@ impl Job for JoinUserToGroup { correlation_id: 0, is_platform_moderator: state.data.platform_moderators.contains(&self.user_id), is_bot: u.is_bot, + user_type: u.user_type, diamond_membership_expires_at: state .data .users diff --git a/backend/canisters/user_index/impl/src/updates/add_local_user_index_canister.rs b/backend/canisters/user_index/impl/src/updates/add_local_user_index_canister.rs index 45865b66cf..ca293a8c1b 100644 --- a/backend/canisters/user_index/impl/src/updates/add_local_user_index_canister.rs +++ b/backend/canisters/user_index/impl/src/updates/add_local_user_index_canister.rs @@ -78,7 +78,7 @@ fn commit(canister_id: CanisterId, wasm_version: BuildVersion, state: &mut Runti user_principal: user.principal, username: user.username.clone(), is_bot: user.is_bot, - is_oc_controlled_bot: user.is_oc_controlled_bot, + user_type: user.user_type, referred_by: user.referred_by, }), ) diff --git a/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs b/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs index 0cd3819891..c923a1afc6 100644 --- a/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs +++ b/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs @@ -10,7 +10,7 @@ use local_user_index_canister::{ UserJoinedGroup, UserRegistered, UsernameChanged, }; use storage_index_canister::add_or_update_users::UserConfig; -use types::{CanisterId, MessageContent, TextContent, UserId}; +use types::{CanisterId, MessageContent, TextContent, UserId, UserType}; use user_index_canister::c2c_notify_events::{Response::*, *}; use user_index_canister::Event; @@ -130,7 +130,7 @@ fn process_new_user( state .data .users - .register(caller, user_id, username.clone(), now, referred_by, false, false); + .register(caller, user_id, username.clone(), now, referred_by, UserType::User); state.data.local_index_map.add_user(local_user_index_canister_id, user_id); @@ -140,7 +140,7 @@ fn process_new_user( user_principal: caller, username: username.clone(), is_bot: false, - is_oc_controlled_bot: false, + user_type: UserType::User, referred_by, }), Some(local_user_index_canister_id), diff --git a/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs b/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs index 180d6873f5..7d3d0eca43 100644 --- a/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs +++ b/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs @@ -3,7 +3,7 @@ use canister_tracing_macros::trace; use event_store_producer::EventBuilder; use ic_cdk::update; use local_user_index_canister::{Event, UserRegistered}; -use types::{Cycles, UserId}; +use types::{Cycles, UserId, UserType}; use user_index_canister::c2c_register_bot::{Response::*, *}; use utils::text_validation::{validate_username, UsernameValidationError}; @@ -48,7 +48,7 @@ fn c2c_register_bot_impl(args: Args, state: &mut RuntimeState) -> Response { state .data .users - .register(caller, user_id, args.username.clone(), now, None, true, false); + .register(caller, user_id, args.username.clone(), now, None, UserType::Bot); state.push_event_to_all_local_user_indexes( Event::UserRegistered(UserRegistered { @@ -56,7 +56,7 @@ fn c2c_register_bot_impl(args: Args, state: &mut RuntimeState) -> Response { user_principal: caller, username: args.username, is_bot: true, - is_oc_controlled_bot: false, + user_type: UserType::Bot, referred_by: None, }), None, diff --git a/backend/libraries/group_chat_core/src/lib.rs b/backend/libraries/group_chat_core/src/lib.rs index e0a65a3b0f..bb913cfb80 100644 --- a/backend/libraries/group_chat_core/src/lib.rs +++ b/backend/libraries/group_chat_core/src/lib.rs @@ -17,8 +17,8 @@ use types::{ MessageContentInitial, MessageId, MessageIndex, MessageMatch, MessagePermissions, MessagePinned, MessageUnpinned, MessagesResponse, Milliseconds, MultiUserChat, OptionUpdate, OptionalGroupPermissions, OptionalMessagePermissions, PermissionsChanged, PushEventResult, PushIfNotContains, Reaction, RoleChanged, Rules, SelectedGroupUpdates, ThreadPreview, - TimestampMillis, Timestamped, UpdatedRules, UserId, UsersBlocked, UsersInvited, Version, Versioned, VersionedRules, - VideoCall, + TimestampMillis, Timestamped, UpdatedRules, UserId, UserType, UsersBlocked, UsersInvited, Version, Versioned, + VersionedRules, VideoCall, }; use utils::document_validation::validate_avatar; use utils::text_validation::{ @@ -60,6 +60,12 @@ pub struct GroupChatCore { #[allow(clippy::too_many_arguments)] impl GroupChatCore { + pub fn set_user_types(&mut self, oc_controlled_bots: &[UserId]) { + for (user_id, member) in self.members.members.iter_mut().filter(|(_, m)| m.is_bot) { + member.user_type = if oc_controlled_bots.contains(user_id) { UserType::OcControlledBot } else { UserType::Bot }; + } + } + pub fn new( chat: MultiUserChat, created_by: UserId, @@ -73,11 +79,11 @@ impl GroupChatCore { permissions: GroupPermissions, gate: Option, events_ttl: Option, - is_bot: bool, + created_by_user_type: UserType, anonymized_chat_id: u128, now: TimestampMillis, ) -> GroupChatCore { - let members = GroupMembers::new(created_by, is_bot, now); + let members = GroupMembers::new(created_by, created_by_user_type, now); let events = ChatEvents::new_group_chat( chat, name.clone(), @@ -532,7 +538,7 @@ impl GroupChatCore { pub fn validate_and_send_message( &mut self, sender: UserId, - sender_is_bot: bool, + sender_user_type: UserType, thread_root_message_index: Option, message_id: MessageId, content: MessageContentInitial, @@ -548,7 +554,7 @@ impl GroupChatCore { ) -> SendMessageResult { use SendMessageResult::*; - if let Err(error) = content.validate_for_new_message(false, sender_is_bot, forwarding, now) { + if let Err(error) = content.validate_for_new_message(false, sender_user_type, forwarding, now) { return match error { ContentValidationError::Empty => MessageEmpty, ContentValidationError::TextTooLong(max_length) => TextTooLong(max_length), @@ -605,7 +611,7 @@ impl GroupChatCore { min_visible_event_index, mentions_disabled, everyone_mentioned, - sender_is_bot, + sender_user_type, } = match self.prepare_send_message( sender, thread_root_message_index, @@ -642,7 +648,7 @@ impl GroupChatCore { mentioned: if !suppressed { mentioned.clone() } else { Vec::new() }, replies_to: replies_to.as_ref().map(|r| r.into()), forwarded: forwarding, - sender_is_bot, + sender_is_bot: sender_user_type.is_bot(), block_level_markdown, correlation_id: 0, now, @@ -721,7 +727,7 @@ impl GroupChatCore { min_visible_event_index: EventIndex::default(), mentions_disabled: true, everyone_mentioned: false, - sender_is_bot: true, + sender_user_type: UserType::OcControlledBot, }); } @@ -756,7 +762,7 @@ impl GroupChatCore { min_visible_event_index: member.min_visible_event_index(), mentions_disabled: false, everyone_mentioned: member.role.can_mention_everyone(permissions) && is_everyone_mentioned(content), - sender_is_bot: member.is_bot, + sender_user_type: member.user_type, }) } @@ -2069,5 +2075,5 @@ struct PrepareSendMessageSuccess { min_visible_event_index: EventIndex, mentions_disabled: bool, everyone_mentioned: bool, - sender_is_bot: bool, + sender_user_type: UserType, } diff --git a/backend/libraries/group_chat_core/src/members.rs b/backend/libraries/group_chat_core/src/members.rs index 44db1d702a..296d855776 100644 --- a/backend/libraries/group_chat_core/src/members.rs +++ b/backend/libraries/group_chat_core/src/members.rs @@ -12,7 +12,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Formatter; use types::{ is_default, is_empty_btreemap, is_empty_hashset, is_empty_slice, EventIndex, GroupMember, GroupPermissions, - HydratedMention, MessageIndex, TimestampMillis, Timestamped, UserId, Version, MAX_RETURNED_MENTIONS, + HydratedMention, MessageIndex, TimestampMillis, Timestamped, UserId, UserType, Version, MAX_RETURNED_MENTIONS, }; const MAX_MEMBERS_PER_GROUP: u32 = 100_000; @@ -40,7 +40,7 @@ pub enum MemberUpdate { #[allow(clippy::too_many_arguments)] impl GroupMembers { - pub fn new(creator_user_id: UserId, is_bot: bool, now: TimestampMillis) -> GroupMembers { + pub fn new(creator_user_id: UserId, user_type: UserType, now: TimestampMillis) -> GroupMembers { let member = GroupMemberInternal { user_id: creator_user_id, date_added: now, @@ -54,7 +54,8 @@ impl GroupMembers { proposal_votes: BTreeMap::default(), suspended: Timestamped::default(), rules_accepted: Some(Timestamped::new(Version::zero(), now)), - is_bot, + is_bot: user_type.is_bot(), + user_type, }; GroupMembers { @@ -74,7 +75,7 @@ impl GroupMembers { min_visible_event_index: EventIndex, min_visible_message_index: MessageIndex, notifications_muted: bool, - is_bot: bool, + user_type: UserType, ) -> AddResult { if self.blocked.contains(&user_id) { AddResult::Blocked @@ -96,7 +97,8 @@ impl GroupMembers { proposal_votes: BTreeMap::default(), suspended: Timestamped::default(), rules_accepted: None, - is_bot, + is_bot: user_type.is_bot(), + user_type, }; e.insert(member.clone()); self.updates.insert((now, user_id, MemberUpdate::Added)); @@ -336,7 +338,8 @@ pub struct GroupMemberInternal { pub rules_accepted: Option>, #[serde(rename = "b", default, skip_serializing_if = "is_default")] pub is_bot: bool, - + #[serde(rename = "ut", default, skip_serializing_if = "is_default")] + pub user_type: UserType, #[serde(rename = "me", default, skip_serializing_if = "is_default")] min_visible_event_index: EventIndex, #[serde(rename = "mm", default, skip_serializing_if = "is_default")] @@ -456,7 +459,7 @@ mod tests { use crate::{GroupMemberInternal, Mentions}; use candid::Principal; use std::collections::{BTreeMap, HashSet}; - use types::{Timestamped, Version}; + use types::{Timestamped, UserType, Version}; #[test] fn serialize_with_max_defaults() { @@ -474,6 +477,7 @@ mod tests { min_visible_message_index: 0.into(), rules_accepted: Some(Timestamped::new(Version::zero(), 1)), is_bot: false, + user_type: UserType::User, }; let member_bytes = msgpack::serialize_then_unwrap(&member); @@ -503,12 +507,13 @@ mod tests { min_visible_message_index: 1.into(), rules_accepted: Some(Timestamped::new(Version::zero(), 1)), is_bot: true, + user_type: UserType::Bot, }; let member_bytes = msgpack::serialize_then_unwrap(&member); let member_bytes_len = member_bytes.len(); - assert_eq!(member_bytes_len, 120); + assert_eq!(member_bytes_len, 127); let _deserialized: GroupMemberInternal = msgpack::deserialize_then_unwrap(&member_bytes); } diff --git a/backend/libraries/types/src/message_content.rs b/backend/libraries/types/src/message_content.rs index 21e1d26dfc..5a4b74422d 100644 --- a/backend/libraries/types/src/message_content.rs +++ b/backend/libraries/types/src/message_content.rs @@ -2,7 +2,7 @@ use crate::polls::{InvalidPollReason, PollConfig, PollVotes}; use crate::{ Achievement, CanisterId, CompletedCryptoTransaction, CryptoTransaction, CryptoTransferDetails, Cryptocurrency, MessageIndex, Milliseconds, P2PSwapAccepted, P2PSwapCancelled, P2PSwapCompleted, P2PSwapExpired, P2PSwapReserved, - P2PSwapStatus, ProposalContent, TimestampMillis, TokenInfo, TotalVotes, User, UserId, VideoCallType, + P2PSwapStatus, ProposalContent, TimestampMillis, TokenInfo, TotalVotes, User, UserId, UserType, VideoCallType, }; use candid::CandidType; use serde::{Deserialize, Serialize}; @@ -250,7 +250,7 @@ impl MessageContentInitial { pub fn validate_for_new_message( &self, is_direct_chat: bool, - sender_is_bot: bool, + sender_user_type: UserType, forwarding: bool, now: TimestampMillis, ) -> Result<(), ContentValidationError> { @@ -279,11 +279,6 @@ impl MessageContentInitial { return Err(ContentValidationError::PrizeEndDateInThePast); } } - MessageContentInitial::P2PSwap(_) => { - if sender_is_bot { - return Err(ContentValidationError::Unauthorized); - } - } MessageContentInitial::GovernanceProposal(_) | MessageContentInitial::MessageReminderCreated(_) | MessageContentInitial::MessageReminder(_) => { @@ -292,6 +287,10 @@ impl MessageContentInitial { _ => {} }; + if self.contains_crypto_transfer() && sender_user_type.is_bot() && !sender_user_type.is_oc_controlled_bot() { + return Err(ContentValidationError::Unauthorized); + } + let is_empty = match self { MessageContentInitial::Text(t) => t.text.is_empty(), MessageContentInitial::Image(i) => i.blob_reference.is_none(), diff --git a/backend/libraries/types/src/user.rs b/backend/libraries/types/src/user.rs index 7d06dd914b..b1ca991e36 100644 --- a/backend/libraries/types/src/user.rs +++ b/backend/libraries/types/src/user.rs @@ -62,3 +62,21 @@ pub struct UserDetails { pub is_platform_operator: bool, pub is_diamond_member: bool, } + +#[derive(CandidType, Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum UserType { + #[default] + User, + Bot, + OcControlledBot, +} + +impl UserType { + pub fn is_bot(&self) -> bool { + matches!(self, UserType::Bot | UserType::OcControlledBot) + } + + pub fn is_oc_controlled_bot(&self) -> bool { + matches!(self, UserType::OcControlledBot) + } +}