diff --git a/Cargo.lock b/Cargo.lock index 5fd728395a..8462bd9b03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1267,6 +1267,7 @@ dependencies = [ name = "community_canister_c2c_client" version = "0.1.0" dependencies = [ + "candid", "canister_client", "community_canister", "ic-cdk 0.11.3", @@ -3664,10 +3665,13 @@ version = "0.1.0" dependencies = [ "candid", "candid_gen", + "community_canister", + "group_canister", "ic-ledger-types", "serde", "serde_bytes", "types", + "user_canister", ] [[package]] diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index 7bbbb876e9..15023a6908 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Support getting batches of chat events via LocalUserIndex ([#4848](https://github.com/open-chat-labs/open-chat/pull/4848)) + ### Changed - Make events private for payment gated chats ([#4843](https://github.com/open-chat-labs/open-chat/pull/4843)) diff --git a/backend/canisters/community/api/src/queries/c2c_events.rs b/backend/canisters/community/api/src/queries/c2c_events.rs new file mode 100644 index 0000000000..38390c65e0 --- /dev/null +++ b/backend/canisters/community/api/src/queries/c2c_events.rs @@ -0,0 +1,4 @@ +use types::RelayedArgs; + +pub type Args = RelayedArgs; +pub type Response = crate::EventsResponse; diff --git a/backend/canisters/community/api/src/queries/c2c_events_by_index.rs b/backend/canisters/community/api/src/queries/c2c_events_by_index.rs new file mode 100644 index 0000000000..dea8ef736f --- /dev/null +++ b/backend/canisters/community/api/src/queries/c2c_events_by_index.rs @@ -0,0 +1,4 @@ +use types::RelayedArgs; + +pub type Args = RelayedArgs; +pub type Response = crate::EventsResponse; diff --git a/backend/canisters/community/api/src/queries/c2c_events_window.rs b/backend/canisters/community/api/src/queries/c2c_events_window.rs new file mode 100644 index 0000000000..f77564a88e --- /dev/null +++ b/backend/canisters/community/api/src/queries/c2c_events_window.rs @@ -0,0 +1,4 @@ +use types::RelayedArgs; + +pub type Args = RelayedArgs; +pub type Response = crate::EventsResponse; diff --git a/backend/canisters/community/api/src/queries/mod.rs b/backend/canisters/community/api/src/queries/mod.rs index ce18d100ed..67f68f9214 100644 --- a/backend/canisters/community/api/src/queries/mod.rs +++ b/backend/canisters/community/api/src/queries/mod.rs @@ -1,3 +1,6 @@ +pub mod c2c_events; +pub mod c2c_events_by_index; +pub mod c2c_events_window; pub mod c2c_summary; pub mod channel_summary; pub mod channel_summary_updates; diff --git a/backend/canisters/community/c2c_client/Cargo.toml b/backend/canisters/community/c2c_client/Cargo.toml index 2cf47045ef..cebf69a80c 100644 --- a/backend/canisters/community/c2c_client/Cargo.toml +++ b/backend/canisters/community/c2c_client/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +candid = { workspace = true } canister_client = { path = "../../../libraries/canister_client" } community_canister = { path = "../api" } ic-cdk = { workspace = true } diff --git a/backend/canisters/community/c2c_client/src/lib.rs b/backend/canisters/community/c2c_client/src/lib.rs index c020e61750..84e3c4f4bb 100644 --- a/backend/canisters/community/c2c_client/src/lib.rs +++ b/backend/canisters/community/c2c_client/src/lib.rs @@ -2,6 +2,9 @@ use canister_client::generate_c2c_call; use community_canister::*; // Queries +generate_c2c_call!(c2c_events); +generate_c2c_call!(c2c_events_by_index); +generate_c2c_call!(c2c_events_window); // Updates generate_c2c_call!(c2c_create_proposals_channel); diff --git a/backend/canisters/community/impl/src/queries/events.rs b/backend/canisters/community/impl/src/queries/events.rs index aef3788b9a..6d3e3a671b 100644 --- a/backend/canisters/community/impl/src/queries/events.rs +++ b/backend/canisters/community/impl/src/queries/events.rs @@ -1,20 +1,29 @@ +use crate::guards::caller_is_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use candid::Principal; +use canister_api_macros::query_msgpack; +use community_canister::c2c_events::Args as C2CArgs; use community_canister::events::{Response::*, *}; use group_chat_core::EventsResult; use ic_cdk_macros::query; #[query] fn events(args: Args) -> Response { - read_state(|state| events_impl(args, state)) + read_state(|state| events_impl(args, None, state)) } -fn events_impl(args: Args, state: &RuntimeState) -> Response { +#[query_msgpack(guard = "caller_is_local_user_index")] +fn c2c_events(args: C2CArgs) -> Response { + read_state(|state| events_impl(args.args, Some(args.caller), state)) +} + +fn events_impl(args: Args, on_behalf_of: Option, state: &RuntimeState) -> Response { if let Err(now) = check_replica_up_to_date(args.latest_known_update, state) { return ReplicaNotUpToDateV2(now); } - let caller = state.env.caller(); + let caller = on_behalf_of.unwrap_or_else(|| state.env.caller()); let user_id = state.data.members.get(caller).map(|m| m.user_id); if user_id.is_none() && (!state.data.is_public || state.data.has_payment_gate()) { diff --git a/backend/canisters/community/impl/src/queries/events_by_index.rs b/backend/canisters/community/impl/src/queries/events_by_index.rs index 736e7a3c92..73e09e0ad4 100644 --- a/backend/canisters/community/impl/src/queries/events_by_index.rs +++ b/backend/canisters/community/impl/src/queries/events_by_index.rs @@ -1,20 +1,29 @@ +use crate::guards::caller_is_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use candid::Principal; +use canister_api_macros::query_msgpack; +use community_canister::c2c_events_by_index::Args as C2CArgs; use community_canister::events_by_index::{Response::*, *}; use group_chat_core::EventsResult; use ic_cdk_macros::query; #[query] fn events_by_index(args: Args) -> Response { - read_state(|state| events_by_index_impl(args, state)) + read_state(|state| events_by_index_impl(args, None, state)) } -fn events_by_index_impl(args: Args, state: &RuntimeState) -> Response { +#[query_msgpack(guard = "caller_is_local_user_index")] +fn c2c_events_by_index(args: C2CArgs) -> Response { + read_state(|state| events_by_index_impl(args.args, Some(args.caller), state)) +} + +fn events_by_index_impl(args: Args, on_behalf_of: Option, state: &RuntimeState) -> Response { if let Err(now) = check_replica_up_to_date(args.latest_known_update, state) { return ReplicaNotUpToDateV2(now); } - let caller = state.env.caller(); + let caller = on_behalf_of.unwrap_or_else(|| state.env.caller()); let user_id = state.data.members.get(caller).map(|m| m.user_id); if user_id.is_none() && (!state.data.is_public || state.data.has_payment_gate()) { diff --git a/backend/canisters/community/impl/src/queries/events_window.rs b/backend/canisters/community/impl/src/queries/events_window.rs index adf619a62c..9822bc3f05 100644 --- a/backend/canisters/community/impl/src/queries/events_window.rs +++ b/backend/canisters/community/impl/src/queries/events_window.rs @@ -1,20 +1,29 @@ +use crate::guards::caller_is_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use candid::Principal; +use canister_api_macros::query_msgpack; +use community_canister::c2c_events_window::Args as C2CArgs; use community_canister::events_window::{Response::*, *}; use group_chat_core::EventsResult; use ic_cdk_macros::query; #[query] fn events_window(args: Args) -> Response { - read_state(|state| events_window_impl(args, state)) + read_state(|state| events_window_impl(args, None, state)) } -fn events_window_impl(args: Args, state: &RuntimeState) -> Response { +#[query_msgpack(guard = "caller_is_local_user_index")] +fn c2c_events_window(args: C2CArgs) -> Response { + read_state(|state| events_window_impl(args.args, Some(args.caller), state)) +} + +fn events_window_impl(args: Args, on_behalf_of: Option, state: &RuntimeState) -> Response { if let Err(now) = check_replica_up_to_date(args.latest_known_update, state) { return ReplicaNotUpToDateV2(now); } - let caller = state.env.caller(); + let caller = on_behalf_of.unwrap_or_else(|| state.env.caller()); let user_id = state.data.members.get(caller).map(|m| m.user_id); if user_id.is_none() && (!state.data.is_public || state.data.has_payment_gate()) { diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index 158fc270c5..59130c09be 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Support getting batches of chat events via LocalUserIndex ([#4848](https://github.com/open-chat-labs/open-chat/pull/4848)) + ### Changed - Make events private for payment gated chats ([#4843](https://github.com/open-chat-labs/open-chat/pull/4843)) diff --git a/backend/canisters/group/api/src/queries/c2c_events.rs b/backend/canisters/group/api/src/queries/c2c_events.rs new file mode 100644 index 0000000000..38390c65e0 --- /dev/null +++ b/backend/canisters/group/api/src/queries/c2c_events.rs @@ -0,0 +1,4 @@ +use types::RelayedArgs; + +pub type Args = RelayedArgs; +pub type Response = crate::EventsResponse; diff --git a/backend/canisters/group/api/src/queries/c2c_events_by_index.rs b/backend/canisters/group/api/src/queries/c2c_events_by_index.rs new file mode 100644 index 0000000000..dea8ef736f --- /dev/null +++ b/backend/canisters/group/api/src/queries/c2c_events_by_index.rs @@ -0,0 +1,4 @@ +use types::RelayedArgs; + +pub type Args = RelayedArgs; +pub type Response = crate::EventsResponse; diff --git a/backend/canisters/group/api/src/queries/c2c_events_window.rs b/backend/canisters/group/api/src/queries/c2c_events_window.rs new file mode 100644 index 0000000000..f77564a88e --- /dev/null +++ b/backend/canisters/group/api/src/queries/c2c_events_window.rs @@ -0,0 +1,4 @@ +use types::RelayedArgs; + +pub type Args = RelayedArgs; +pub type Response = crate::EventsResponse; diff --git a/backend/canisters/group/api/src/queries/mod.rs b/backend/canisters/group/api/src/queries/mod.rs index c930315408..6b68f12244 100644 --- a/backend/canisters/group/api/src/queries/mod.rs +++ b/backend/canisters/group/api/src/queries/mod.rs @@ -1,4 +1,7 @@ +pub mod c2c_events; +pub mod c2c_events_by_index; pub mod c2c_events_internal; +pub mod c2c_events_window; pub mod c2c_name_and_members; pub mod c2c_summary; pub mod c2c_summary_updates; diff --git a/backend/canisters/group/c2c_client/src/lib.rs b/backend/canisters/group/c2c_client/src/lib.rs index 351799c00a..738363e789 100644 --- a/backend/canisters/group/c2c_client/src/lib.rs +++ b/backend/canisters/group/c2c_client/src/lib.rs @@ -2,7 +2,10 @@ use canister_client::{generate_c2c_call, generate_candid_c2c_call}; use group_canister::*; // Queries +generate_c2c_call!(c2c_events); +generate_c2c_call!(c2c_events_by_index); generate_c2c_call!(c2c_events_internal); +generate_c2c_call!(c2c_events_window); generate_c2c_call!(c2c_name_and_members); generate_c2c_call!(c2c_summary); generate_c2c_call!(c2c_summary_updates); diff --git a/backend/canisters/group/impl/src/guards.rs b/backend/canisters/group/impl/src/guards.rs index 22cf457a2c..a81f477b34 100644 --- a/backend/canisters/group/impl/src/guards.rs +++ b/backend/canisters/group/impl/src/guards.rs @@ -24,6 +24,14 @@ pub fn caller_is_group_index_or_local_group_index() -> Result<(), String> { } } +pub fn caller_is_local_user_index() -> Result<(), String> { + if read_state(|state| state.is_caller_local_user_index()) { + Ok(()) + } else { + Err("Caller is not the local_user_index".to_string()) + } +} + pub fn caller_is_local_group_index() -> Result<(), String> { if read_state(|state| state.is_caller_local_group_index()) { Ok(()) diff --git a/backend/canisters/group/impl/src/queries/events.rs b/backend/canisters/group/impl/src/queries/events.rs index cc0ba1abd8..a47b5ca5d0 100644 --- a/backend/canisters/group/impl/src/queries/events.rs +++ b/backend/canisters/group/impl/src/queries/events.rs @@ -1,20 +1,29 @@ +use crate::guards::caller_is_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use candid::Principal; +use canister_api_macros::query_msgpack; +use group_canister::c2c_events::Args as C2CArgs; use group_canister::events::{Response::*, *}; use group_chat_core::EventsResult; use ic_cdk_macros::query; #[query] fn events(args: Args) -> Response { - read_state(|state| events_impl(args, state)) + read_state(|state| events_impl(args, None, state)) } -fn events_impl(args: Args, state: &RuntimeState) -> Response { +#[query_msgpack(guard = "caller_is_local_user_index")] +fn c2c_events(args: C2CArgs) -> Response { + read_state(|state| events_impl(args.args, Some(args.caller), state)) +} + +fn events_impl(args: Args, on_behalf_of: Option, state: &RuntimeState) -> Response { if let Err(now) = check_replica_up_to_date(args.latest_known_update, state) { return ReplicaNotUpToDateV2(now); } - let caller = state.env.caller(); + let caller = on_behalf_of.unwrap_or_else(|| state.env.caller()); let user_id = state.data.lookup_user_id(caller); match state.data.chat.events( diff --git a/backend/canisters/group/impl/src/queries/events_by_index.rs b/backend/canisters/group/impl/src/queries/events_by_index.rs index a45c202e46..35346441e2 100644 --- a/backend/canisters/group/impl/src/queries/events_by_index.rs +++ b/backend/canisters/group/impl/src/queries/events_by_index.rs @@ -1,20 +1,29 @@ +use crate::guards::caller_is_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use candid::Principal; +use canister_api_macros::query_msgpack; +use group_canister::c2c_events_by_index::Args as C2CArgs; use group_canister::events_by_index::{Response::*, *}; use group_chat_core::EventsResult; use ic_cdk_macros::query; #[query] fn events_by_index(args: Args) -> Response { - read_state(|state| events_by_index_impl(args, state)) + read_state(|state| events_by_index_impl(args, None, state)) } -fn events_by_index_impl(args: Args, state: &RuntimeState) -> Response { +#[query_msgpack(guard = "caller_is_local_user_index")] +fn c2c_events_by_index(args: C2CArgs) -> Response { + read_state(|state| events_by_index_impl(args.args, Some(args.caller), state)) +} + +fn events_by_index_impl(args: Args, on_behalf_of: Option, state: &RuntimeState) -> Response { if let Err(now) = check_replica_up_to_date(args.latest_known_update, state) { return ReplicaNotUpToDateV2(now); } - let caller = state.env.caller(); + let caller = on_behalf_of.unwrap_or_else(|| state.env.caller()); let user_id = state.data.lookup_user_id(caller); match state diff --git a/backend/canisters/group/impl/src/queries/events_window.rs b/backend/canisters/group/impl/src/queries/events_window.rs index 13c441f25b..23bea3077a 100644 --- a/backend/canisters/group/impl/src/queries/events_window.rs +++ b/backend/canisters/group/impl/src/queries/events_window.rs @@ -1,20 +1,29 @@ +use crate::guards::caller_is_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use candid::Principal; +use canister_api_macros::query_msgpack; +use group_canister::c2c_events_window::Args as C2CArgs; use group_canister::events_window::{Response::*, *}; use group_chat_core::EventsResult; use ic_cdk_macros::query; #[query] fn events_window(args: Args) -> Response { - read_state(|state| events_window_impl(args, state)) + read_state(|state| events_window_impl(args, None, state)) } -fn events_window_impl(args: Args, state: &RuntimeState) -> Response { +#[query_msgpack(guard = "caller_is_local_user_index")] +fn c2c_events_window(args: C2CArgs) -> Response { + read_state(|state| events_window_impl(args.args, Some(args.caller), state)) +} + +fn events_window_impl(args: Args, on_behalf_of: Option, state: &RuntimeState) -> Response { if let Err(now) = check_replica_up_to_date(args.latest_known_update, state) { return ReplicaNotUpToDateV2(now); } - let caller = state.env.caller(); + let caller = on_behalf_of.unwrap_or_else(|| state.env.caller()); let user_id = state.data.lookup_user_id(caller); match state.data.chat.events_window( diff --git a/backend/canisters/local_user_index/CHANGELOG.md b/backend/canisters/local_user_index/CHANGELOG.md index 7f0c6e382b..a110466347 100644 --- a/backend/canisters/local_user_index/CHANGELOG.md +++ b/backend/canisters/local_user_index/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Support getting batches of chat events via LocalUserIndex ([#4848](https://github.com/open-chat-labs/open-chat/pull/4848)) + ### Changed - Add `local_user_index_canister_id` to group/community summaries ([#4857](https://github.com/open-chat-labs/open-chat/pull/4857)) diff --git a/backend/canisters/local_user_index/api/Cargo.toml b/backend/canisters/local_user_index/api/Cargo.toml index e8ed4bccdf..e2634e55d1 100644 --- a/backend/canisters/local_user_index/api/Cargo.toml +++ b/backend/canisters/local_user_index/api/Cargo.toml @@ -8,7 +8,10 @@ edition = "2021" [dependencies] candid = { workspace = true } candid_gen = { path = "../../../libraries/candid_gen" } +community_canister = { path = "../../community/api" } +group_canister = { path = "../../group/api" } ic-ledger-types = { workspace = true } serde = { workspace = true } -serde_bytes = "0.11" -types = { path = "../../../libraries/types" } \ No newline at end of file +serde_bytes = { workspace = true } +types = { path = "../../../libraries/types" } +user_canister = { path = "../../user/api" } \ No newline at end of file diff --git a/backend/canisters/local_user_index/api/src/queries/chat_events.rs b/backend/canisters/local_user_index/api/src/queries/chat_events.rs new file mode 100644 index 0000000000..5713d05cfd --- /dev/null +++ b/backend/canisters/local_user_index/api/src/queries/chat_events.rs @@ -0,0 +1,92 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::{ChannelId, ChatId, CommunityId, EventIndex, MessageIndex, TimestampMillis, UserId}; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub requests: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct EventsArgs { + pub context: EventsContext, + pub args: EventsArgsInner, + pub latest_known_update: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum EventsContext { + Direct(UserId), + Group(ChatId, Option), + Channel(CommunityId, ChannelId, Option), +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum EventsArgsInner { + Page(EventsPageArgs), + ByIndex(EventsByIndexArgs), + Window(EventsWindowArgs), +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct EventsPageArgs { + pub start_index: EventIndex, + pub ascending: bool, + pub max_messages: u32, + pub max_events: u32, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct EventsByIndexArgs { + pub events: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct EventsWindowArgs { + pub mid_point: MessageIndex, + pub max_messages: u32, + pub max_events: u32, +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum Response { + Success(Vec), +} + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum EventsResponse { + Success(types::EventsResponse), + NotFound, + ReplicaNotUpToDate(TimestampMillis), + InternalError(String), +} + +impl From for EventsResponse { + fn from(value: community_canister::EventsResponse) -> Self { + match value { + community_canister::EventsResponse::Success(events) => EventsResponse::Success(events), + community_canister::EventsResponse::ReplicaNotUpToDateV2(ts) => EventsResponse::ReplicaNotUpToDate(ts), + _ => EventsResponse::NotFound, + } + } +} + +impl From for EventsResponse { + fn from(value: group_canister::EventsResponse) -> Self { + match value { + group_canister::EventsResponse::Success(events) => EventsResponse::Success(events), + group_canister::EventsResponse::ReplicaNotUpToDateV2(ts) => EventsResponse::ReplicaNotUpToDate(ts), + _ => EventsResponse::NotFound, + } + } +} + +impl From for EventsResponse { + fn from(value: user_canister::EventsResponse) -> Self { + match value { + user_canister::EventsResponse::Success(events) => EventsResponse::Success(events), + user_canister::EventsResponse::ReplicaNotUpToDateV2(ts) => EventsResponse::ReplicaNotUpToDate(ts), + _ => EventsResponse::NotFound, + } + } +} diff --git a/backend/canisters/local_user_index/api/src/queries/mod.rs b/backend/canisters/local_user_index/api/src/queries/mod.rs index 347a0120a9..c4cd2f6b61 100644 --- a/backend/canisters/local_user_index/api/src/queries/mod.rs +++ b/backend/canisters/local_user_index/api/src/queries/mod.rs @@ -1,3 +1,4 @@ pub mod c2c_can_push_notifications; pub mod c2c_lookup_user; pub mod c2c_user_principals; +pub mod chat_events; diff --git a/backend/canisters/local_user_index/c2c_client/src/lib.rs b/backend/canisters/local_user_index/c2c_client/src/lib.rs index 25867590b8..57c326554a 100644 --- a/backend/canisters/local_user_index/c2c_client/src/lib.rs +++ b/backend/canisters/local_user_index/c2c_client/src/lib.rs @@ -7,6 +7,7 @@ use types::CanisterId; generate_c2c_call!(c2c_can_push_notifications); generate_c2c_call!(c2c_lookup_user); generate_c2c_call!(c2c_user_principals); +generate_c2c_call!(chat_events); // Updates generate_c2c_call!(c2c_notify_low_balance); diff --git a/backend/canisters/local_user_index/impl/src/queries/chat_events.rs b/backend/canisters/local_user_index/impl/src/queries/chat_events.rs new file mode 100644 index 0000000000..38a7d6e596 --- /dev/null +++ b/backend/canisters/local_user_index/impl/src/queries/chat_events.rs @@ -0,0 +1,178 @@ +use crate::guards::caller_is_openchat_user; +use crate::read_state; +use candid::Principal; +use ic_cdk::api::call::CallResult; +use ic_cdk::query; +use local_user_index_canister::chat_events::{Response::*, *}; +use types::UserId; + +#[query(composite = true, guard = "caller_is_openchat_user")] +async fn chat_events(args: Args) -> Response { + let user = read_state(|state| state.calling_user()); + + let futures: Vec<_> = args + .requests + .into_iter() + .map(|r| make_c2c_call(r, user.principal, user.user_id)) + .collect(); + + let results = futures::future::join_all(futures).await; + + Success(results) +} + +async fn make_c2c_call(events_args: EventsArgs, principal: Principal, user_id: UserId) -> EventsResponse { + match events_args.context { + EventsContext::Direct(them) => match events_args.args { + EventsArgsInner::Page(args) => map_response( + user_canister_c2c_client::events( + user_id.into(), + &user_canister::events::Args { + user_id: them, + thread_root_message_index: None, + start_index: args.start_index, + ascending: args.ascending, + max_messages: args.max_messages, + max_events: args.max_events, + latest_known_update: events_args.latest_known_update, + }, + ) + .await, + ), + EventsArgsInner::ByIndex(args) => map_response( + user_canister_c2c_client::events_by_index( + user_id.into(), + &user_canister::events_by_index::Args { + user_id: them, + thread_root_message_index: None, + events: args.events, + latest_known_update: events_args.latest_known_update, + }, + ) + .await, + ), + EventsArgsInner::Window(args) => map_response( + user_canister_c2c_client::events_window( + user_id.into(), + &user_canister::events_window::Args { + user_id: them, + thread_root_message_index: None, + mid_point: args.mid_point, + max_messages: args.max_messages, + max_events: args.max_events, + latest_known_update: events_args.latest_known_update, + }, + ) + .await, + ), + }, + EventsContext::Group(chat_id, thread_root_message_index) => match events_args.args { + EventsArgsInner::Page(args) => map_response( + group_canister_c2c_client::c2c_events( + chat_id.into(), + &group_canister::c2c_events::Args { + caller: principal, + args: group_canister::events::Args { + thread_root_message_index, + start_index: args.start_index, + ascending: args.ascending, + max_messages: args.max_messages, + max_events: args.max_events, + latest_known_update: events_args.latest_known_update, + }, + }, + ) + .await, + ), + EventsArgsInner::ByIndex(args) => map_response( + group_canister_c2c_client::c2c_events_by_index( + chat_id.into(), + &group_canister::c2c_events_by_index::Args { + caller: principal, + args: group_canister::events_by_index::Args { + thread_root_message_index, + events: args.events, + latest_known_update: events_args.latest_known_update, + }, + }, + ) + .await, + ), + EventsArgsInner::Window(args) => map_response( + group_canister_c2c_client::c2c_events_window( + chat_id.into(), + &group_canister::c2c_events_window::Args { + caller: principal, + args: group_canister::events_window::Args { + thread_root_message_index, + mid_point: args.mid_point, + max_messages: args.max_messages, + max_events: args.max_events, + latest_known_update: events_args.latest_known_update, + }, + }, + ) + .await, + ), + }, + EventsContext::Channel(community_id, channel_id, thread_root_message_index) => match events_args.args { + EventsArgsInner::Page(args) => map_response( + community_canister_c2c_client::c2c_events( + community_id.into(), + &community_canister::c2c_events::Args { + caller: principal, + args: community_canister::events::Args { + channel_id, + thread_root_message_index, + start_index: args.start_index, + ascending: args.ascending, + max_messages: args.max_messages, + max_events: args.max_events, + latest_known_update: events_args.latest_known_update, + }, + }, + ) + .await, + ), + EventsArgsInner::ByIndex(args) => map_response( + community_canister_c2c_client::c2c_events_by_index( + community_id.into(), + &community_canister::c2c_events_by_index::Args { + caller: principal, + args: community_canister::events_by_index::Args { + channel_id, + thread_root_message_index, + events: args.events, + latest_known_update: events_args.latest_known_update, + }, + }, + ) + .await, + ), + EventsArgsInner::Window(args) => map_response( + community_canister_c2c_client::c2c_events_window( + community_id.into(), + &community_canister::c2c_events_window::Args { + caller: principal, + args: community_canister::events_window::Args { + channel_id, + thread_root_message_index, + mid_point: args.mid_point, + max_messages: args.max_messages, + max_events: args.max_events, + latest_known_update: events_args.latest_known_update, + }, + }, + ) + .await, + ), + }, + } +} + +fn map_response>(response: CallResult) -> EventsResponse { + match response { + Ok(result) => result.into(), + Err(error) => EventsResponse::InternalError(format!("{error:?}")), + } +} diff --git a/backend/canisters/local_user_index/impl/src/queries/mod.rs b/backend/canisters/local_user_index/impl/src/queries/mod.rs index 0a305182b4..59937bab24 100644 --- a/backend/canisters/local_user_index/impl/src/queries/mod.rs +++ b/backend/canisters/local_user_index/impl/src/queries/mod.rs @@ -1,4 +1,5 @@ pub mod c2c_can_push_notifications; pub mod c2c_lookup_user; pub mod c2c_user_principals; +pub mod chat_events; pub mod http_request; diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index 7675f657c4..d497dacfde 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Support getting batches of chat events via LocalUserIndex ([#4848](https://github.com/open-chat-labs/open-chat/pull/4848)) + ### Changed - In modclub reports only show public message links ([#4847](https://github.com/open-chat-labs/open-chat/pull/4847)) diff --git a/backend/canisters/user/c2c_client/src/lib.rs b/backend/canisters/user/c2c_client/src/lib.rs index 52ebaa266a..b82bf92af9 100644 --- a/backend/canisters/user/c2c_client/src/lib.rs +++ b/backend/canisters/user/c2c_client/src/lib.rs @@ -20,3 +20,6 @@ generate_c2c_call!(c2c_tip_message); generate_c2c_call!(c2c_toggle_reaction); generate_c2c_call!(c2c_undelete_messages); generate_c2c_call!(c2c_vote_on_proposal); +generate_c2c_call!(events); +generate_c2c_call!(events_by_index); +generate_c2c_call!(events_window); diff --git a/backend/canisters/user/impl/src/guards.rs b/backend/canisters/user/impl/src/guards.rs index 1079a57421..5eb1efc622 100644 --- a/backend/canisters/user/impl/src/guards.rs +++ b/backend/canisters/user/impl/src/guards.rs @@ -24,6 +24,14 @@ pub fn caller_is_local_user_index() -> Result<(), String> { } } +pub fn caller_is_owner_or_local_user_index() -> Result<(), String> { + if read_state(|state| state.is_caller_owner() || state.is_caller_local_user_index()) { + Ok(()) + } else { + Err("Caller is not the canister owner or the local user index".to_owned()) + } +} + pub fn caller_is_group_index() -> Result<(), String> { if read_state(|state| state.is_caller_group_index()) { Ok(()) diff --git a/backend/canisters/user/impl/src/queries/events.rs b/backend/canisters/user/impl/src/queries/events.rs index 6a0d400bd1..6bed8b5db8 100644 --- a/backend/canisters/user/impl/src/queries/events.rs +++ b/backend/canisters/user/impl/src/queries/events.rs @@ -1,12 +1,12 @@ -use crate::guards::caller_is_owner; +use crate::guards::caller_is_owner_or_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use canister_api_macros::query_candid_and_msgpack; use chat_events::Reader; -use ic_cdk_macros::query; use types::{EventOrExpiredRange, EventsResponse}; use user_canister::events::{Response::*, *}; -#[query(guard = "caller_is_owner")] +#[query_candid_and_msgpack(guard = "caller_is_owner_or_local_user_index")] fn events(args: Args) -> Response { read_state(|state| events_impl(args, state)) } diff --git a/backend/canisters/user/impl/src/queries/events_by_index.rs b/backend/canisters/user/impl/src/queries/events_by_index.rs index da899cba92..6fc0046433 100644 --- a/backend/canisters/user/impl/src/queries/events_by_index.rs +++ b/backend/canisters/user/impl/src/queries/events_by_index.rs @@ -1,12 +1,12 @@ -use crate::guards::caller_is_owner; +use crate::guards::caller_is_owner_or_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use canister_api_macros::query_candid_and_msgpack; use chat_events::Reader; -use ic_cdk_macros::query; use types::{EventOrExpiredRange, EventsResponse}; use user_canister::events_by_index::{Response::*, *}; -#[query(guard = "caller_is_owner")] +#[query_candid_and_msgpack(guard = "caller_is_owner_or_local_user_index")] fn events_by_index(args: Args) -> Response { read_state(|state| events_by_index_impl(args, state)) } diff --git a/backend/canisters/user/impl/src/queries/events_window.rs b/backend/canisters/user/impl/src/queries/events_window.rs index a6d16fee3c..b1d25f86c9 100644 --- a/backend/canisters/user/impl/src/queries/events_window.rs +++ b/backend/canisters/user/impl/src/queries/events_window.rs @@ -1,12 +1,12 @@ -use crate::guards::caller_is_owner; +use crate::guards::caller_is_owner_or_local_user_index; use crate::queries::check_replica_up_to_date; use crate::{read_state, RuntimeState}; +use canister_api_macros::query_candid_and_msgpack; use chat_events::Reader; -use ic_cdk_macros::query; use types::{EventOrExpiredRange, EventsResponse}; use user_canister::events_window::{Response::*, *}; -#[query(guard = "caller_is_owner")] +#[query_candid_and_msgpack(guard = "caller_is_owner_or_local_user_index")] fn events_window(args: Args) -> Response { read_state(|state| events_window_impl(args, state)) } diff --git a/backend/integration_tests/src/client/local_user_index.rs b/backend/integration_tests/src/client/local_user_index.rs index 1c170a77c5..4976f419a4 100644 --- a/backend/integration_tests/src/client/local_user_index.rs +++ b/backend/integration_tests/src/client/local_user_index.rs @@ -1,7 +1,8 @@ -use crate::generate_update_call; +use crate::{generate_query_call, generate_update_call}; use local_user_index_canister::*; // Queries +generate_query_call!(chat_events); // Updates generate_update_call!(invite_users_to_channel); diff --git a/backend/integration_tests/src/get_chat_events_tests.rs b/backend/integration_tests/src/get_chat_events_tests.rs new file mode 100644 index 0000000000..ddbe449e4c --- /dev/null +++ b/backend/integration_tests/src/get_chat_events_tests.rs @@ -0,0 +1,116 @@ +use crate::env::ENV; +use crate::rng::random_string; +use crate::{client, CanisterIds, TestEnv, User}; +use candid::Principal; +use local_user_index_canister::chat_events::{EventsArgs, EventsArgsInner, EventsByIndexArgs, EventsContext}; +use pocket_ic::PocketIc; +use std::ops::Deref; +use types::{ChannelId, ChatEvent, ChatId, CommunityId}; + +#[test] +fn send_message_succeeds() { + let mut wrapper = ENV.deref().get(); + let TestEnv { + env, + canister_ids, + controller, + } = wrapper.env(); + + let TestData { + user1, + user2, + group_id1, + group_id2, + community_id, + channel_id, + } = init_test_data(env, canister_ids, *controller); + + for i in 0..5 { + client::user::happy_path::send_text_message(env, &user1, user2.user_id, format!("User: {i}"), None); + client::group::happy_path::send_text_message(env, &user1, group_id1, None, format!("Group1: {i}"), None); + client::group::happy_path::send_text_message(env, &user1, group_id2, None, format!("Group2: {i}"), None); + client::community::happy_path::send_text_message( + env, + &user1, + community_id, + channel_id, + None, + format!("Channel: {i}"), + None, + ); + } + + let local_user_index_canister::chat_events::Response::Success(responses) = client::local_user_index::chat_events( + env, + user1.principal, + canister_ids.local_user_index, + &local_user_index_canister::chat_events::Args { + requests: vec![ + EventsArgs { + context: EventsContext::Direct(user2.user_id), + args: EventsArgsInner::ByIndex(EventsByIndexArgs { events: vec![1.into()] }), + latest_known_update: None, + }, + EventsArgs { + context: EventsContext::Group(group_id1, None), + args: EventsArgsInner::ByIndex(EventsByIndexArgs { events: vec![2.into()] }), + latest_known_update: None, + }, + EventsArgs { + context: EventsContext::Group(group_id2, None), + args: EventsArgsInner::ByIndex(EventsByIndexArgs { events: vec![3.into()] }), + latest_known_update: None, + }, + EventsArgs { + context: EventsContext::Channel(community_id, channel_id, None), + args: EventsArgsInner::ByIndex(EventsByIndexArgs { events: vec![4.into()] }), + latest_known_update: None, + }, + ], + }, + ); + + assert_is_message_with_text(responses.get(0).unwrap(), "User: 0"); + assert_is_message_with_text(responses.get(1).unwrap(), "Group1: 1"); + assert_is_message_with_text(responses.get(2).unwrap(), "Group2: 2"); + assert_is_message_with_text(responses.get(3).unwrap(), "Channel: 3"); +} + +fn assert_is_message_with_text(response: &local_user_index_canister::chat_events::EventsResponse, text: &str) { + if let local_user_index_canister::chat_events::EventsResponse::Success(result) = response { + assert_eq!(result.events.len(), 1); + if let ChatEvent::Message(message) = &result.events.first().unwrap().event { + assert_eq!(message.content.text().unwrap(), text); + return; + } + } + panic!("{response:?}") +} + +fn init_test_data(env: &mut PocketIc, canister_ids: &CanisterIds, controller: Principal) -> TestData { + let user1 = client::register_diamond_user(env, canister_ids, controller); + let user2 = client::local_user_index::happy_path::register_user(env, canister_ids.local_user_index); + + let group_id1 = client::user::happy_path::create_group(env, &user1, &random_string(), true, true); + let group_id2 = client::user::happy_path::create_group(env, &user1, &random_string(), true, true); + let community_id = client::user::happy_path::create_community(env, &user1, &random_string(), true, vec![random_string()]); + let channel_id = client::community::happy_path::create_channel(env, user1.principal, community_id, true, random_string()); + + TestData { + user1, + user2, + group_id1, + group_id2, + community_id, + channel_id, + } +} + +struct TestData { + user1: User, + user2: User, + group_id1: ChatId, + group_id2: ChatId, + community_id: CommunityId, + channel_id: ChannelId, +} diff --git a/backend/integration_tests/src/lib.rs b/backend/integration_tests/src/lib.rs index 778e7c780e..50bacb902d 100644 --- a/backend/integration_tests/src/lib.rs +++ b/backend/integration_tests/src/lib.rs @@ -18,6 +18,7 @@ mod env; mod fire_and_forget_handler_tests; mod freeze_group_tests; mod gated_group_tests; +mod get_chat_events_tests; mod join_group_tests; mod last_online_date_tests; mod notification_tests; diff --git a/backend/libraries/types/src/lib.rs b/backend/libraries/types/src/lib.rs index bdb5181731..6dc2876623 100644 --- a/backend/libraries/types/src/lib.rs +++ b/backend/libraries/types/src/lib.rs @@ -53,6 +53,7 @@ mod range_set; mod reactions; mod referral_codes; mod registration_fee; +mod relayed_args; mod source_group; mod subscription; mod suspension_duration; @@ -115,6 +116,7 @@ pub use proposals::*; pub use reactions::*; pub use referral_codes::*; pub use registration_fee::*; +pub use relayed_args::*; pub use source_group::*; pub use subscription::*; pub use suspension_duration::*; diff --git a/backend/libraries/types/src/relayed_args.rs b/backend/libraries/types/src/relayed_args.rs new file mode 100644 index 0000000000..c6ecc55a9c --- /dev/null +++ b/backend/libraries/types/src/relayed_args.rs @@ -0,0 +1,8 @@ +use candid::{CandidType, Principal}; +use serde::{Deserialize, Serialize}; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct RelayedArgs { + pub caller: Principal, + pub args: T, +}