diff --git a/Cargo.lock b/Cargo.lock index e27163a275..913bc58dd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,9 @@ dependencies = [ name = "airdrop_bot_canister" version = "0.1.0" dependencies = [ - "bot_api", "candid", "candid_gen", + "legacy_bot_api", "serde", "types", ] @@ -906,28 +906,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bot_api" -version = "0.1.0" -dependencies = [ - "serde", - "types", - "user_canister", -] - -[[package]] -name = "bot_c2c_client" -version = "0.1.0" -dependencies = [ - "bot_api", - "candid", - "canister_client", - "ic-cdk 0.17.0", - "msgpack", - "tracing", - "types", -] - [[package]] name = "bumpalo" version = "3.14.0" @@ -1123,6 +1101,7 @@ dependencies = [ "proposals_bot_canister", "registry_canister", "registry_canister_client", + "serde", "sha256", "sign_in_with_email_canister", "storage_index_canister", @@ -1208,6 +1187,7 @@ dependencies = [ "online_users_canister", "proposals_bot_canister", "registry_canister", + "serde", "sha256", "sign_in_with_email_canister", "storage_index_canister", @@ -2700,6 +2680,29 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "greet_bot_canister_impl" +version = "0.1.0" +dependencies = [ + "candid", + "canister_client", + "getrandom 0.2.15", + "http 1.1.0", + "ic-cdk 0.17.0", + "ic-cdk-timers", + "ic-http-certification", + "ic-stable-structures", + "ic_principal", + "jwt", + "local_user_index_canister", + "local_user_index_canister_c2c_client", + "msgpack", + "rmp-serde", + "serde", + "serde_json", + "types", +] + [[package]] name = "group" version = "0.10.0" @@ -3538,6 +3541,21 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "ic-http-certification" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff0b97e949845039149dc5e7ea6a7c12ee4333bb402e37bc507904643c7b3e41" +dependencies = [ + "candid", + "http 0.2.11", + "ic-certification", + "ic-representation-independent-hash", + "serde", + "thiserror", + "urlencoding", +] + [[package]] name = "ic-ledger-types" version = "0.14.0" @@ -3555,9 +3573,9 @@ dependencies = [ [[package]] name = "ic-representation-independent-hash" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d9c969c80e9b445255341da79772680f503ef856b95b3ddf162b41d096df1f" +checksum = "08ae59483e377cd9aad94ec339ed1d2583b0d5929cab989328dac2d853b2f570" dependencies = [ "leb128", "sha2 0.10.8", @@ -4418,6 +4436,28 @@ dependencies = [ "types", ] +[[package]] +name = "legacy_bot_api" +version = "0.1.0" +dependencies = [ + "serde", + "types", + "user_canister", +] + +[[package]] +name = "legacy_bot_c2c_client" +version = "0.1.0" +dependencies = [ + "candid", + "canister_client", + "ic-cdk 0.17.0", + "legacy_bot_api", + "msgpack", + "tracing", + "types", +] + [[package]] name = "libc" version = "0.2.155" @@ -8232,8 +8272,6 @@ name = "user_canister_impl" version = "0.1.0" dependencies = [ "async-trait", - "bot_api", - "bot_c2c_client", "candid", "canister_api_macros", "canister_client", @@ -8273,6 +8311,8 @@ dependencies = [ "kongswap_canister", "kongswap_canister_c2c_client", "ledger_utils", + "legacy_bot_api", + "legacy_bot_c2c_client", "local_user_index_canister", "local_user_index_canister_c2c_client", "msgpack", diff --git a/Cargo.toml b/Cargo.toml index 21da57bdd1..289ca70e0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ - "backend/bots/api", - "backend/bots/c2c_client", + "backend/bots/examples/greet", "backend/canisters/airdrop_bot/api", "backend/canisters/airdrop_bot/client", "backend/canisters/airdrop_bot/impl", @@ -106,6 +105,8 @@ members = [ "backend/external_canisters/sonic/api", "backend/external_canisters/sonic/c2c_client", "backend/integration_tests", + "backend/legacy_bots/api", + "backend/legacy_bots/c2c_client", "backend/libraries/activity_notification_state", "backend/libraries/candid_gen", "backend/libraries/canister_agent_utils", @@ -186,6 +187,7 @@ futures = "0.3.30" getrandom = { version = "0.2.15", features = ["custom"] } hex = "0.4.3" hmac-sha256 = { version = "1.1.7", features = ["traits010"] } +http = "1.1.0" ic-agent = "0.39.1" ic-canister-sig-creation = "1.1.0" ic-captcha = "1.0.0" @@ -195,6 +197,7 @@ ic-cdk-macros = "0.17.0" ic-cdk-timers = "0.11.0" ic-certificate-verification = "2.4.0" ic-certification = "2.5.0" +ic-http-certification = "2.5.0" ic-ledger-types = "0.14.0" ic_principal = "0.1.1" ic-stable-structures = "0.6.7" diff --git a/backend/bots/examples/greet/can.did b/backend/bots/examples/greet/can.did new file mode 100644 index 0000000000..55cd2bbb0f --- /dev/null +++ b/backend/bots/examples/greet/can.did @@ -0,0 +1,18 @@ +type BotApiCallResponse = variant { + Ok; + Err : variant { + Invalid : text; + CanisterError : variant { + NotAuthorized; + Frozen; + Other : text; + }; + C2CError: record { + 0 : nat32; + 1 : text; + }; + }; +}; + +service : { +}; diff --git a/backend/bots/examples/greet/cargo.toml b/backend/bots/examples/greet/cargo.toml new file mode 100644 index 0000000000..1c923e1d81 --- /dev/null +++ b/backend/bots/examples/greet/cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "greet_bot_canister_impl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +candid = { workspace = true } +canister_client = { path = "../../../libraries/canister_client" } +getrandom = { workspace = true } +http = { workspace = true } +ic-cdk = { workspace = true } +ic-cdk-timers = { workspace = true } +ic-http-certification = { workspace = true } +ic_principal = { workspace = true } +ic-stable-structures = { workspace = true } +jwt = { path = "../../../libraries/jwt" } +local_user_index_canister = { path = "../../../canisters/local_user_index/api" } +local_user_index_canister_c2c_client = { path = "../../../canisters/local_user_index/c2c_client" } +msgpack = { path = "../../../libraries/msgpack" } +rmp-serde = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +types = { path = "../../../libraries/types" } diff --git a/backend/bots/examples/greet/src/commands/greet.rs b/backend/bots/examples/greet/src/commands/greet.rs new file mode 100644 index 0000000000..906a703e34 --- /dev/null +++ b/backend/bots/examples/greet/src/commands/greet.rs @@ -0,0 +1,37 @@ +use local_user_index_canister::execute_bot_command::{self, BotApiCallError}; +use types::{ + bot_actions::{BotMessageAction, MessageContent}, + BotAction, BotCommandClaims, TextContent, +}; + +use crate::execute::{execute_bot_command, InternalError, Message, SuccessResult}; + +pub async fn greet(bot: BotCommandClaims, access_token: &str) -> Result { + let user_id = bot.initiator; + let content = MessageContent::Text(TextContent { + text: format!("hello @UserId({user_id})"), + }); + + let args = execute_bot_command::Args { + action: BotAction::SendMessage(BotMessageAction { + content: content.clone(), + finalised: true, + }), + jwt: access_token.to_string(), + }; + + match execute_bot_command(bot.bot_api_gateway, &args).await { + Ok(Ok(_)) => Ok(SuccessResult { + message: Some(Message { + id: bot.message_id, + content, + }), + }), + Ok(Err(error)) => match error { + BotApiCallError::C2CError(code, message) => Err(InternalError::C2CError(code, message)), + BotApiCallError::CanisterError(canister_error) => Err(InternalError::CanisterError(canister_error)), + BotApiCallError::Invalid(text) => Err(InternalError::Invalid(text)), + }, + Err((code, message)) => Err(InternalError::C2CError(code as i32, message)), + } +} diff --git a/backend/bots/examples/greet/src/commands/mod.rs b/backend/bots/examples/greet/src/commands/mod.rs new file mode 100644 index 0000000000..cd98614d5c --- /dev/null +++ b/backend/bots/examples/greet/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod greet; diff --git a/backend/bots/examples/greet/src/definition.rs b/backend/bots/examples/greet/src/definition.rs new file mode 100644 index 0000000000..15a71e63e5 --- /dev/null +++ b/backend/bots/examples/greet/src/definition.rs @@ -0,0 +1,19 @@ +use std::collections::HashSet; + +use types::{BotDefinition, MessagePermission, SlashCommandPermissions, SlashCommandSchema}; + +pub fn definition() -> BotDefinition { + BotDefinition { + description: "Ths bot provides a single command `greet`".to_string(), + commands: vec![SlashCommandSchema { + name: "greet".to_string(), + description: Some("This will greet the caller".to_string()), + params: vec![], + permissions: SlashCommandPermissions { + community: HashSet::new(), + chat: HashSet::new(), + message: HashSet::from_iter([MessagePermission::Text]), + }, + }], + } +} diff --git a/backend/bots/examples/greet/src/env.rs b/backend/bots/examples/greet/src/env.rs new file mode 100644 index 0000000000..4ad7e8b54d --- /dev/null +++ b/backend/bots/examples/greet/src/env.rs @@ -0,0 +1,16 @@ +use candid::Principal; +use types::{Nanoseconds, TimestampMillis, TimestampNanos}; + +const NANOS_PER_MILLISECOND: Nanoseconds = 1_000_000; + +pub fn now() -> TimestampMillis { + now_nanos() / NANOS_PER_MILLISECOND +} + +pub fn now_nanos() -> TimestampNanos { + ic_cdk::api::time() +} + +pub fn canister_id() -> Principal { + ic_cdk::id() +} diff --git a/backend/bots/examples/greet/src/execute.rs b/backend/bots/examples/greet/src/execute.rs new file mode 100644 index 0000000000..bfabbb6947 --- /dev/null +++ b/backend/bots/examples/greet/src/execute.rs @@ -0,0 +1,78 @@ +use canister_client::generate_candid_c2c_call; +use jwt::{verify_jwt, Claims}; +use local_user_index_canister::execute_bot_command; +use serde::Serialize; +use types::{bot_actions::MessageContent, BotCommandClaims, HandleBotActionsError, MessageId}; + +use crate::{ + commands::greet::greet, + env, + state::{self, State}, +}; + +pub enum ExecuteResponse { + Success(SuccessResult), + BadRequest(BadRequest), + InternalError(InternalError), +} + +#[derive(Serialize)] +pub struct SuccessResult { + pub message: Option, +} + +#[derive(Serialize)] +pub struct Message { + pub id: MessageId, + pub content: MessageContent, +} + +#[derive(Serialize)] +pub enum BadRequest { + AccessTokenNotFound, + AccessTokenInvalid, + AccessTokenExpired, + CommandNotFound, + ArgsInvalid, +} + +#[derive(Serialize)] +pub enum InternalError { + Invalid(String), + CanisterError(HandleBotActionsError), + C2CError(i32, String), +} + +pub async fn execute(access_token: &str) -> ExecuteResponse { + let bot = match state::read(|state| prepare(access_token, state)) { + Ok(c) => c, + Err(response) => return response, + }; + + let result = match bot.command_name.as_str() { + "greet" => greet(bot, access_token).await, + _ => return ExecuteResponse::BadRequest(BadRequest::CommandNotFound), + }; + + match result { + Ok(success) => ExecuteResponse::Success(success), + Err(internal_error) => ExecuteResponse::InternalError(internal_error), + } +} + +fn prepare(access_token: &str, state: &State) -> Result { + let oc_public_key_pem = state.oc_public_key(); + + let claims = verify_jwt::>(access_token, oc_public_key_pem).map_err(|error| { + ic_cdk::println!("Access token invalid: {:?}, error: {:?}", access_token, error); + ExecuteResponse::BadRequest(BadRequest::AccessTokenInvalid) + })?; + + if claims.exp_ms() < env::now() { + return Err(ExecuteResponse::BadRequest(BadRequest::AccessTokenExpired)); + } + + Ok(claims.into_custom()) +} + +generate_candid_c2c_call!(execute_bot_command); diff --git a/backend/bots/examples/greet/src/http_request.rs b/backend/bots/examples/greet/src/http_request.rs new file mode 100644 index 0000000000..87894ab0af --- /dev/null +++ b/backend/bots/examples/greet/src/http_request.rs @@ -0,0 +1,93 @@ +use ic_cdk::{query, update}; +use ic_http_certification::{HttpRequest, HttpResponse}; +use serde::Serialize; +use std::str; + +use crate::{ + definition::definition, + execute::{execute, ExecuteResponse}, +}; + +#[query] +fn http_request(request: HttpRequest) -> HttpResponse { + if request.method.to_ascii_uppercase() == "GET" { + // Return the `bot definition` regardless of the path + let body = to_json(&definition()); + + return text_response(200, body); + } + + if request.method.to_ascii_uppercase() == "POST" { + if let Ok(path) = request.get_path() { + if path == "/execute" { + return upgrade(); + } + } + } + + not_found() +} + +#[update] +async fn http_request_update(request: HttpRequest) -> HttpResponse { + if request.method.to_ascii_uppercase() == "POST" { + if let Ok(path) = request.get_path() { + if path == "/execute" { + let (status_code, body) = match str::from_utf8(&request.body) { + Ok(access_token) => match execute(access_token).await { + ExecuteResponse::Success(result) => (200, to_json(&result)), + ExecuteResponse::BadRequest(bad_request) => (400, to_json(&bad_request)), + ExecuteResponse::InternalError(internal_error) => (500, to_json(&internal_error)), + }, + Err(error) => (400, format!("Invalid access token: {:?}", error)), + }; + + return text_response(status_code, body); + } + } + } + + not_found() +} + +fn text_response(status_code: u16, body: String) -> HttpResponse { + HttpResponse { + status_code, + headers: vec![ + ("content-type".to_string(), "text/plain".to_string()), + ("content-length".to_string(), body.len().to_string()), + ("Access-Control-Allow-Origin".to_string(), "*".to_string()), + ("Access-Control-Allow-Headers".to_string(), "*".to_string()), + ], + body: body.into_bytes(), + upgrade: Some(false), + } +} + +fn not_found() -> HttpResponse { + HttpResponse { + status_code: 404, + headers: Vec::new(), + body: Vec::new(), + upgrade: None, + } +} + +fn upgrade() -> HttpResponse { + HttpResponse { + status_code: 200, + headers: vec![ + ("Access-Control-Allow-Origin".to_string(), "*".to_string()), + ("Access-Control-Allow-Headers".to_string(), "*".to_string()), + ], + body: Vec::new(), + upgrade: Some(true), + } +} + +fn to_json(value: &T) -> String +where + T: ?Sized + Serialize, +{ + serde_json::to_string(value).unwrap() +} diff --git a/backend/bots/examples/greet/src/lib.rs b/backend/bots/examples/greet/src/lib.rs new file mode 100644 index 0000000000..718d0173de --- /dev/null +++ b/backend/bots/examples/greet/src/lib.rs @@ -0,0 +1,8 @@ +pub mod commands; +pub mod definition; +pub mod env; +pub mod execute; +pub mod http_request; +pub mod lifecycle; +pub mod memory; +pub mod state; diff --git a/backend/bots/examples/greet/src/lifecycle/init.rs b/backend/bots/examples/greet/src/lifecycle/init.rs new file mode 100644 index 0000000000..e3cefd17c6 --- /dev/null +++ b/backend/bots/examples/greet/src/lifecycle/init.rs @@ -0,0 +1,16 @@ +use crate::state; +use crate::state::State; +use candid::CandidType; +use ic_cdk::init; +use serde::{Deserialize, Serialize}; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct InitOrUpgradeArgs { + pub oc_public_key: String, + pub test_mode: bool, +} + +#[init] +fn init(args: InitOrUpgradeArgs) { + state::init(State::new(args.oc_public_key, args.test_mode)); +} diff --git a/backend/bots/examples/greet/src/lifecycle/mod.rs b/backend/bots/examples/greet/src/lifecycle/mod.rs new file mode 100644 index 0000000000..dc39ebd9c3 --- /dev/null +++ b/backend/bots/examples/greet/src/lifecycle/mod.rs @@ -0,0 +1,5 @@ +mod init; +mod post_upgrade; +mod pre_upgrade; + +const READER_WRITER_BUFFER_SIZE: usize = 1024 * 1024; // 1MB diff --git a/backend/bots/examples/greet/src/lifecycle/post_upgrade.rs b/backend/bots/examples/greet/src/lifecycle/post_upgrade.rs new file mode 100644 index 0000000000..50aaa13a66 --- /dev/null +++ b/backend/bots/examples/greet/src/lifecycle/post_upgrade.rs @@ -0,0 +1,22 @@ +use crate::lifecycle::READER_WRITER_BUFFER_SIZE; +use crate::memory::get_upgrades_memory; +use crate::state; +use crate::state::State; +use ic_cdk::post_upgrade; +use ic_stable_structures::reader::{BufferedReader, Reader}; +use serde::Deserialize; + +use super::init::InitOrUpgradeArgs; + +#[post_upgrade] +fn post_upgrade(args: InitOrUpgradeArgs) { + let memory = get_upgrades_memory(); + let reader = BufferedReader::new(READER_WRITER_BUFFER_SIZE, Reader::new(&memory, 0)); + let mut deserializer = rmp_serde::Deserializer::new(reader); + + let mut state = State::deserialize(&mut deserializer).unwrap(); + + state.update(args.oc_public_key, args.test_mode); + + state::init(state); +} diff --git a/backend/bots/examples/greet/src/lifecycle/pre_upgrade.rs b/backend/bots/examples/greet/src/lifecycle/pre_upgrade.rs new file mode 100644 index 0000000000..15694ec766 --- /dev/null +++ b/backend/bots/examples/greet/src/lifecycle/pre_upgrade.rs @@ -0,0 +1,16 @@ +use crate::lifecycle::READER_WRITER_BUFFER_SIZE; +use crate::memory::get_upgrades_memory; +use crate::state; +use ic_cdk::pre_upgrade; +use ic_stable_structures::writer::{BufferedWriter, Writer}; +use serde::Serialize; + +#[pre_upgrade] +fn pre_upgrade() { + let mut memory = get_upgrades_memory(); + let writer = BufferedWriter::new(READER_WRITER_BUFFER_SIZE, Writer::new(&mut memory, 0)); + let mut serializer = rmp_serde::Serializer::new(writer).with_struct_map(); + + let state = state::take(); + state.serialize(&mut serializer).unwrap() +} diff --git a/backend/bots/examples/greet/src/memory.rs b/backend/bots/examples/greet/src/memory.rs new file mode 100644 index 0000000000..0ae84e1d18 --- /dev/null +++ b/backend/bots/examples/greet/src/memory.rs @@ -0,0 +1,21 @@ +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + DefaultMemoryImpl, +}; + +const UPGRADES: MemoryId = MemoryId::new(0); + +pub type Memory = VirtualMemory; + +thread_local! { + static MEMORY_MANAGER: MemoryManager + = MemoryManager::init_with_bucket_size(DefaultMemoryImpl::default(), 128); +} + +pub fn get_upgrades_memory() -> Memory { + get_memory(UPGRADES) +} + +fn get_memory(id: MemoryId) -> Memory { + MEMORY_MANAGER.with(|m| m.get(id)) +} diff --git a/backend/bots/examples/greet/src/state.rs b/backend/bots/examples/greet/src/state.rs new file mode 100644 index 0000000000..8f9374cdd4 --- /dev/null +++ b/backend/bots/examples/greet/src/state.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; + +thread_local! { + static STATE: RefCell> = RefCell::default(); +} + +#[derive(Serialize, Deserialize)] +pub struct State { + oc_public_key: String, + test_mode: bool, +} + +const STATE_ALREADY_INITIALIZED: &str = "State has already been initialized"; +const STATE_NOT_INITIALIZED: &str = "State has not been initialized"; + +pub fn init(state: State) { + STATE.with_borrow_mut(|s| { + if s.is_some() { + panic!("{}", STATE_ALREADY_INITIALIZED); + } else { + *s = Some(state); + } + }) +} + +pub fn read R, R>(f: F) -> R { + STATE.with_borrow(|s| f(s.as_ref().expect(STATE_NOT_INITIALIZED))) +} + +pub fn mutate R, R>(f: F) -> R { + STATE.with_borrow_mut(|s| f(s.as_mut().expect(STATE_NOT_INITIALIZED))) +} + +pub fn take() -> State { + STATE.take().expect(STATE_NOT_INITIALIZED) +} + +impl State { + pub fn new(oc_public_key: String, test_mode: bool) -> State { + State { + oc_public_key, + test_mode, + } + } + + pub fn update(&mut self, oc_public_key: String, test_mode: bool) { + self.oc_public_key = oc_public_key; + self.test_mode = test_mode; + } + + pub fn oc_public_key(&self) -> &str { + &self.oc_public_key + } + + pub fn test_mode(&self) -> bool { + self.test_mode + } +} + +pub enum AuthResult { + Success, + RequiresUpgrade, + LinkExpired, + CodeIncorrect, + LinkInvalid(String), +} diff --git a/backend/canisters/airdrop_bot/api/Cargo.toml b/backend/canisters/airdrop_bot/api/Cargo.toml index f23ecd92a3..885c35ff60 100644 --- a/backend/canisters/airdrop_bot/api/Cargo.toml +++ b/backend/canisters/airdrop_bot/api/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bot_api = { path = "../../../bots/api" } +legacy_bot_api = { path = "../../../legacy_bots/api" } candid = { workspace = true } candid_gen = { path = "../../../libraries/candid_gen" } serde = { workspace = true } diff --git a/backend/canisters/airdrop_bot/api/src/updates/handle_direct_message.rs b/backend/canisters/airdrop_bot/api/src/updates/handle_direct_message.rs index 924a492d2c..edaae9e743 100644 --- a/backend/canisters/airdrop_bot/api/src/updates/handle_direct_message.rs +++ b/backend/canisters/airdrop_bot/api/src/updates/handle_direct_message.rs @@ -1 +1 @@ -pub use bot_api::handle_direct_message::{Response::*, *}; +pub use legacy_bot_api::handle_direct_message::{Response::*, *}; diff --git a/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs b/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs index 871f5fb063..625378d68e 100644 --- a/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs +++ b/backend/canisters/local_user_index/impl/src/updates/execute_bot_command.rs @@ -31,7 +31,7 @@ fn validate(args: Args, state: &RuntimeState) -> Result>(&args.jwt, state.data.oc_key_pair.public_key_pem()) .map_err(|error| format!("Access token invalid: {error:?}"))?; - if claims.exp() < state.env.now() { + if claims.exp_ms() < state.env.now() { return Err("Access token expired".to_string()); } diff --git a/backend/canisters/user/impl/Cargo.toml b/backend/canisters/user/impl/Cargo.toml index 52cb6a6775..7ede3471b5 100644 --- a/backend/canisters/user/impl/Cargo.toml +++ b/backend/canisters/user/impl/Cargo.toml @@ -11,8 +11,8 @@ crate-type = ["cdylib"] [dependencies] async-trait = { workspace = true } -bot_api = { path = "../../../bots/api" } -bot_c2c_client = { path = "../../../bots/c2c_client" } +legacy_bot_api = { path = "../../../legacy_bots/api" } +legacy_bot_c2c_client = { path = "../../../legacy_bots/c2c_client" } candid = { workspace = true } canister_api_macros = { path = "../../../libraries/canister_api_macros" } canister_client = { path = "../../../libraries/canister_client" } diff --git a/backend/canisters/user/impl/src/updates/send_message.rs b/backend/canisters/user/impl/src/updates/send_message.rs index 2774854672..79fb927659 100644 --- a/backend/canisters/user/impl/src/updates/send_message.rs +++ b/backend/canisters/user/impl/src/updates/send_message.rs @@ -285,7 +285,7 @@ fn send_message_impl( ic_cdk::spawn(send_to_bot_canister( recipient, message_event.event.message_index, - bot_api::handle_direct_message::Args::new(send_message_args, sender_name), + legacy_bot_api::handle_direct_message::Args::new(send_message_args, sender_name), )); } else { state.push_user_canister_event( @@ -341,9 +341,13 @@ fn send_message_impl( } } -async fn send_to_bot_canister(recipient: UserId, message_index: MessageIndex, args: bot_api::handle_direct_message::Args) { - match bot_c2c_client::handle_direct_message(recipient.into(), &args).await { - Ok(bot_api::handle_direct_message::Response::Success(result)) => { +async fn send_to_bot_canister( + recipient: UserId, + message_index: MessageIndex, + args: legacy_bot_api::handle_direct_message::Args, +) { + match legacy_bot_c2c_client::handle_direct_message(recipient.into(), &args).await { + Ok(legacy_bot_api::handle_direct_message::Response::Success(result)) => { mutate_state(|state| { if let Some(chat) = state.data.direct_chats.get_mut(&recipient.into()) { let now = state.env.now(); diff --git a/backend/integration_tests/src/diamond_membership_tests.rs b/backend/integration_tests/src/diamond_membership_tests.rs index dd9c633803..35e8d17b5f 100644 --- a/backend/integration_tests/src/diamond_membership_tests.rs +++ b/backend/integration_tests/src/diamond_membership_tests.rs @@ -60,7 +60,7 @@ fn can_upgrade_to_diamond(pay_in_chat: bool, lifetime: bool) { let public_key = client::user_index::happy_path::public_key(env, canister_ids.user_index); let claims: Claims = verify_jwt(&diamond_response.proof_jwt, &public_key).unwrap(); - let claims_expiry = claims.exp(); + let claims_expiry = claims.exp_ms(); assert!(now < claims_expiry && claims_expiry < now + DAY_IN_MS); assert_eq!(claims.claim_type(), "diamond_membership"); assert_eq!(claims.custom().expires_at, diamond_response.expires_at); diff --git a/backend/integration_tests/src/setup.rs b/backend/integration_tests/src/setup.rs index d69c189654..ac0faa1e01 100644 --- a/backend/integration_tests/src/setup.rs +++ b/backend/integration_tests/src/setup.rs @@ -1,4 +1,4 @@ -use crate::client::{create_canister, create_canister_with_id, install_canister}; +use crate::client::{create_canister, create_canister_with_id, install_canister, user_index}; use crate::env::VIDEO_CALL_OPERATOR; use crate::utils::tick_many; use crate::{client, wasms, CanisterIds, TestEnv, T}; @@ -9,6 +9,7 @@ use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; use icrc_ledger_types::icrc1::account::Account; use pocket_ic::{PocketIc, PocketIcBuilder}; use rand::{rngs::StdRng, Rng, SeedableRng}; +use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::env; use std::path::Path; @@ -86,6 +87,7 @@ fn install_canisters(env: &mut PocketIc, controller: Principal) -> CanisterIds { let identity_canister_id = create_canister(env, controller); let online_users_canister_id = create_canister(env, controller); let airdrop_bot_canister_id = create_canister(env, controller); + let greet_bot_canister_id = create_canister(env, controller); let proposals_bot_canister_id = create_canister(env, controller); let storage_index_canister_id = create_canister(env, controller); let cycles_dispenser_canister_id = create_canister(env, controller); @@ -103,6 +105,7 @@ fn install_canisters(env: &mut PocketIc, controller: Principal) -> CanisterIds { let escrow_canister_wasm = wasms::ESCROW.clone(); let event_relay_canister_wasm = wasms::EVENT_RELAY.clone(); let event_store_canister_wasm = wasms::EVENT_STORE.clone(); + let greet_bot_canister_wasm = wasms::GREET_BOT.clone(); let group_canister_wasm = wasms::GROUP.clone(); let group_index_canister_wasm = wasms::GROUP_INDEX.clone(); let icp_ledger_canister_wasm = wasms::ICP_LEDGER.clone(); @@ -245,6 +248,21 @@ fn install_canisters(env: &mut PocketIc, controller: Principal) -> CanisterIds { online_users_init_args, ); + let oc_public_key = user_index::happy_path::public_key(env, user_index_canister_id); + + let greet_bot_init_args = GreetBotArgs { + test_mode, + oc_public_key, + }; + + install_canister( + env, + controller, + greet_bot_canister_id, + greet_bot_canister_wasm, + greet_bot_init_args, + ); + let proposals_bot_init_args = proposals_bot_canister::init::Args { service_owner_principals: vec![controller], user_index_canister_id, @@ -616,3 +634,9 @@ struct SnsWasmCanisterInitPayload { access_controls_enabled: bool, sns_subnet_ids: Vec, } + +#[derive(CandidType, Serialize, Deserialize, Debug)] +struct GreetBotArgs { + pub oc_public_key: String, + pub test_mode: bool, +} diff --git a/backend/integration_tests/src/wasms.rs b/backend/integration_tests/src/wasms.rs index 9093fa4535..34aff47aa8 100644 --- a/backend/integration_tests/src/wasms.rs +++ b/backend/integration_tests/src/wasms.rs @@ -12,6 +12,7 @@ lazy_static! { pub static ref ESCROW: CanisterWasm = get_canister_wasm("escrow"); pub static ref EVENT_RELAY: CanisterWasm = get_canister_wasm("event_relay"); pub static ref EVENT_STORE: CanisterWasm = get_canister_wasm("event_store"); + pub static ref GREET_BOT: CanisterWasm = get_canister_wasm("greet_bot"); pub static ref GROUP: CanisterWasm = get_canister_wasm("group"); pub static ref GROUP_INDEX: CanisterWasm = get_canister_wasm("group_index"); pub static ref ICP_LEDGER: CanisterWasm = get_canister_wasm("icp_ledger"); diff --git a/backend/bots/api/Cargo.toml b/backend/legacy_bots/api/Cargo.toml similarity index 92% rename from backend/bots/api/Cargo.toml rename to backend/legacy_bots/api/Cargo.toml index 14dabd9c0a..2f5d284d57 100644 --- a/backend/bots/api/Cargo.toml +++ b/backend/legacy_bots/api/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bot_api" +name = "legacy_bot_api" version = "0.1.0" edition = "2021" diff --git a/backend/bots/api/src/lib.rs b/backend/legacy_bots/api/src/lib.rs similarity index 100% rename from backend/bots/api/src/lib.rs rename to backend/legacy_bots/api/src/lib.rs diff --git a/backend/bots/api/src/updates/handle_direct_message.rs b/backend/legacy_bots/api/src/updates/handle_direct_message.rs similarity index 100% rename from backend/bots/api/src/updates/handle_direct_message.rs rename to backend/legacy_bots/api/src/updates/handle_direct_message.rs diff --git a/backend/bots/api/src/updates/mod.rs b/backend/legacy_bots/api/src/updates/mod.rs similarity index 100% rename from backend/bots/api/src/updates/mod.rs rename to backend/legacy_bots/api/src/updates/mod.rs diff --git a/backend/bots/c2c_client/Cargo.toml b/backend/legacy_bots/c2c_client/Cargo.toml similarity index 85% rename from backend/bots/c2c_client/Cargo.toml rename to backend/legacy_bots/c2c_client/Cargo.toml index 36e4422170..1029e235ae 100644 --- a/backend/bots/c2c_client/Cargo.toml +++ b/backend/legacy_bots/c2c_client/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "bot_c2c_client" +name = "legacy_bot_c2c_client" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bot_api = { path = "../api" } +legacy_bot_api = { path = "../api" } candid = { workspace = true } canister_client = { path = "../../libraries/canister_client" } ic-cdk = { workspace = true } diff --git a/backend/bots/c2c_client/src/lib.rs b/backend/legacy_bots/c2c_client/src/lib.rs similarity index 82% rename from backend/bots/c2c_client/src/lib.rs rename to backend/legacy_bots/c2c_client/src/lib.rs index 1300102ba7..a90d10f558 100644 --- a/backend/bots/c2c_client/src/lib.rs +++ b/backend/legacy_bots/c2c_client/src/lib.rs @@ -1,5 +1,5 @@ -use bot_api::*; use canister_client::generate_c2c_call; +use legacy_bot_api::*; // Queries diff --git a/backend/libraries/canister_agent_utils/src/lib.rs b/backend/libraries/canister_agent_utils/src/lib.rs index a519b2bf3b..9597f174d7 100644 --- a/backend/libraries/canister_agent_utils/src/lib.rs +++ b/backend/libraries/canister_agent_utils/src/lib.rs @@ -18,6 +18,7 @@ pub enum CanisterName { Escrow, EventRelay, EventStore, + GreetBot, Group, GroupIndex, Identity, @@ -61,6 +62,7 @@ impl FromStr for CanisterName { "notifications" => Ok(CanisterName::Notifications), "notifications_index" => Ok(CanisterName::NotificationsIndex), "online_users" => Ok(CanisterName::OnlineUsers), + "greet_bot" => Ok(CanisterName::GreetBot), "proposals_bot" => Ok(CanisterName::ProposalsBot), "registry" => Ok(CanisterName::Registry), "sign_in_with_email" => Ok(CanisterName::SignInWithEmail), @@ -85,6 +87,7 @@ impl Display for CanisterName { CanisterName::Escrow => "escrow", CanisterName::EventRelay => "event_relay", CanisterName::EventStore => "event_store", + CanisterName::GreetBot => "greet_bot", CanisterName::Group => "group", CanisterName::GroupIndex => "group_index", CanisterName::Identity => "identity", @@ -121,6 +124,7 @@ pub struct CanisterIds { pub notifications: CanisterId, pub identity: CanisterId, pub online_users: CanisterId, + pub greet_bot: CanisterId, pub proposals_bot: CanisterId, pub airdrop_bot: CanisterId, pub storage_index: CanisterId, diff --git a/backend/libraries/jwt/src/lib.rs b/backend/libraries/jwt/src/lib.rs index 90665597b3..ae3317e954 100644 --- a/backend/libraries/jwt/src/lib.rs +++ b/backend/libraries/jwt/src/lib.rs @@ -26,7 +26,11 @@ impl Claims { } } - pub fn exp(&self) -> TimestampMillis { + pub fn exp(&self) -> u64 { + self.exp + } + + pub fn exp_ms(&self) -> TimestampMillis { self.exp * 1000 } @@ -37,6 +41,10 @@ impl Claims { pub fn custom(&self) -> &T { &self.custom } + + pub fn into_custom(self) -> T { + self.custom + } } pub fn sign_and_encode_token( diff --git a/backend/tools/canister_installer/Cargo.toml b/backend/tools/canister_installer/Cargo.toml index 9331d39c2f..612a3c67e1 100644 --- a/backend/tools/canister_installer/Cargo.toml +++ b/backend/tools/canister_installer/Cargo.toml @@ -29,6 +29,7 @@ online_users_canister = { path = "../../canisters/online_users/api" } proposals_bot_canister = { path = "../../canisters/proposals_bot/api" } registry_canister = { path = "../../canisters/registry/api" } registry_canister_client = { path = "../../canisters/registry/client" } +serde = { workspace = true } sha256 = { path = "../../libraries/sha256" } sign_in_with_email_canister = { workspace = true } sign_in_with_email_canister_test_utils = { workspace = true } diff --git a/backend/tools/canister_installer/src/lib.rs b/backend/tools/canister_installer/src/lib.rs index 42c5167450..e8e7da1d0e 100644 --- a/backend/tools/canister_installer/src/lib.rs +++ b/backend/tools/canister_installer/src/lib.rs @@ -1,8 +1,9 @@ -use candid::Principal; +use candid::{CandidType, Principal}; use canister_agent_utils::{build_ic_agent, get_canister_wasm, install_wasm, set_controllers, CanisterIds, CanisterName}; use constants::{SNS_GOVERNANCE_CANISTER_ID, SNS_LEDGER_CANISTER_ID}; use ic_agent::{Agent, Identity}; use ic_utils::interfaces::ManagementCanister; +use serde::Serialize; use sha256::sha256; use types::{BuildVersion, CanisterWasm, Cycles}; @@ -33,6 +34,7 @@ async fn install_service_canisters_impl( set_controllers(management_canister, &canister_ids.notifications_index, controllers.clone()), set_controllers(management_canister, &canister_ids.identity, controllers.clone()), set_controllers(management_canister, &canister_ids.online_users, controllers.clone()), + set_controllers(management_canister, &canister_ids.greet_bot, controllers.clone()), set_controllers(management_canister, &canister_ids.proposals_bot, controllers.clone()), set_controllers(management_canister, &canister_ids.airdrop_bot, controllers.clone()), set_controllers(management_canister, &canister_ids.storage_index, controllers.clone()), @@ -174,6 +176,12 @@ async fn install_service_canisters_impl( test_mode, }; + let greet_bot_canister_wasm = get_canister_wasm(CanisterName::GreetBot, version); + let greet_bot_init_args = GreetBotArgs { + test_mode, + oc_public_key: "".to_string(), + }; + let storage_index_canister_wasm = get_canister_wasm(CanisterName::StorageIndex, version); let storage_index_init_args = storage_index_canister::init::Args { governance_principals: vec![principal], @@ -415,7 +423,7 @@ async fn install_service_canisters_impl( ) .await; - futures::future::join4( + futures::future::join5( install_wasm( management_canister, &canister_ids.sign_in_with_email, @@ -440,6 +448,12 @@ async fn install_service_canisters_impl( &airdrop_bot_canister_wasm.module, airdrop_bot_init_args, ), + install_wasm( + management_canister, + &canister_ids.greet_bot, + &greet_bot_canister_wasm.module, + greet_bot_init_args, + ), ) .await; @@ -627,3 +641,9 @@ mod siws { pub runtime_features: Option>, } } + +#[derive(CandidType, Serialize, Debug)] +struct GreetBotArgs { + pub oc_public_key: String, + pub test_mode: bool, +} diff --git a/backend/tools/canister_installer/src/main.rs b/backend/tools/canister_installer/src/main.rs index cef6b79b7f..64f34ad9e5 100644 --- a/backend/tools/canister_installer/src/main.rs +++ b/backend/tools/canister_installer/src/main.rs @@ -16,6 +16,7 @@ async fn main() { notifications: opts.notifications, identity: opts.identity, online_users: opts.online_users, + greet_bot: opts.greet_bot, proposals_bot: opts.proposals_bot, airdrop_bot: opts.airdrop_bot, storage_index: opts.storage_index, @@ -80,6 +81,9 @@ struct Opts { #[arg(long)] online_users: CanisterId, + #[arg(long)] + greet_bot: CanisterId, + #[arg(long)] proposals_bot: CanisterId, diff --git a/backend/tools/canister_upgrader/Cargo.toml b/backend/tools/canister_upgrader/Cargo.toml index 174c09d560..009dd96fa0 100644 --- a/backend/tools/canister_upgrader/Cargo.toml +++ b/backend/tools/canister_upgrader/Cargo.toml @@ -25,6 +25,7 @@ notifications_index_canister_client = { path = "../../canisters/notifications_in online_users_canister = { path = "../../canisters/online_users/api" } proposals_bot_canister = { path = "../../canisters/proposals_bot/api" } registry_canister = { path = "../../canisters/registry/api" } +serde = { workspace = true } sha256 = { path = "../../libraries/sha256" } sign_in_with_email_canister = { workspace = true } storage_index_canister = { path = "../../canisters/storage_index/api" } diff --git a/backend/tools/canister_upgrader/src/lib.rs b/backend/tools/canister_upgrader/src/lib.rs index d209db4cff..fc30f709f3 100644 --- a/backend/tools/canister_upgrader/src/lib.rs +++ b/backend/tools/canister_upgrader/src/lib.rs @@ -160,6 +160,21 @@ pub async fn upgrade_airdrop_bot_canister( println!("Airdrop bot canister upgraded"); } +pub async fn upgrade_greet_bot_canister(_identity: Box, _url: String, _greet_bot_canister_id: CanisterId) { + // TODO + // upgrade_top_level_canister( + // identity, + // url, + // greet_bot_canister_id, + // version, + // greet_bot_canister_impl::init::InitOrUpgradeArgs { }, + // CanisterName::AirdropBot, + // ) + // .await; + + println!("Greet bot canister upgraded"); +} + pub async fn upgrade_storage_index_canister( identity: Box, url: String, diff --git a/backend/tools/canister_upgrader/src/main.rs b/backend/tools/canister_upgrader/src/main.rs index 32a326a7e4..fdf2852c06 100644 --- a/backend/tools/canister_upgrader/src/main.rs +++ b/backend/tools/canister_upgrader/src/main.rs @@ -11,6 +11,7 @@ async fn main() { match opts.canister_to_upgrade { CanisterName::AirdropBot => upgrade_airdrop_bot_canister(identity, opts.url, opts.airdrop_bot, opts.version).await, + CanisterName::GreetBot => upgrade_greet_bot_canister(identity, opts.url, opts.greet_bot).await, CanisterName::Community => upgrade_community_canister(identity, opts.url, opts.group_index, opts.version).await, CanisterName::CyclesDispenser => { upgrade_cycles_dispenser_canister(identity, opts.url, opts.cycles_dispenser, opts.version).await @@ -87,6 +88,9 @@ struct Opts { #[arg(long)] airdrop_bot: CanisterId, + #[arg(long)] + greet_bot: CanisterId, + #[arg(long)] storage_index: CanisterId, diff --git a/canister_ids.json b/canister_ids.json index c74d26f20b..b9ef682f81 100644 --- a/canister_ids.json +++ b/canister_ids.json @@ -24,6 +24,10 @@ "ic": "64dy3-wqaaa-aaaaf-biicq-cai", "ic_test": "63c6p-3iaaa-aaaaf-biica-cai" }, + "greet_bot": { + "ic": "vckqk-oyaaa-aaaad-aaeda-cai", + "ic_test": "vckqk-oyaaa-aaaad-aaeda-cai" + }, "group_index": { "ic": "4ijyc-kiaaa-aaaaf-aaaja-cai", "ic_test": "7kifq-3yaaa-aaaaf-ab2cq-cai" diff --git a/dfx.json b/dfx.json index 1ca5a2d7bf..925d805ccf 100644 --- a/dfx.json +++ b/dfx.json @@ -73,6 +73,12 @@ "wasm": "wasms/airdrop_bot.wasm.gz", "build": "./scripts/generate-wasm.sh airdrop_bot" }, + "greet_bot": { + "type": "custom", + "candid": "backend/bots/examples/greet/can.did", + "wasm": "wasms/greet_bot.wasm.gz", + "build": "./scripts/generate-wasm.sh greet_bot" + }, "user": { "type": "custom", "candid": "backend/canisters/user/api/can.did", diff --git a/frontend/openchat-client/src/openchat.ts b/frontend/openchat-client/src/openchat.ts index 86cbad2ab0..72aec21495 100644 --- a/frontend/openchat-client/src/openchat.ts +++ b/frontend/openchat-client/src/openchat.ts @@ -7758,7 +7758,7 @@ export class OpenChat extends EventTarget { #callBotCommandEndpoint(bot: ExternalBotCommandInstance, token: string): Promise { const headers = new Headers(); headers.append("Content-type", "text/plain"); - return fetch(`${bot.endpoint}/execute_command`, { + return fetch(`${bot.endpoint}/execute`, { method: "POST", headers: headers, body: token, diff --git a/scripts/deploy-local.sh b/scripts/deploy-local.sh index 5c03fe7758..6ac0084f50 100755 --- a/scripts/deploy-local.sh +++ b/scripts/deploy-local.sh @@ -31,6 +31,7 @@ dfx --identity $IDENTITY canister create --no-wallet --with-cycles 1000000000000 dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 identity dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 online_users dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 proposals_bot +dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 greet_bot dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 airdrop_bot dfx --identity $IDENTITY canister create --no-wallet --with-cycles 1000000000000000 storage_index dfx --identity $IDENTITY canister create --no-wallet --with-cycles 1000000000000000 cycles_dispenser diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 174166511f..4eff93e8aa 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -41,6 +41,7 @@ LOCAL_GROUP_INDEX_CANISTER_ID=$(dfx canister --network $NETWORK id local_group_i NOTIFICATIONS_CANISTER_ID=$(dfx canister --network $NETWORK id notifications) IDENTITY_CANISTER_ID=$(dfx canister --network $NETWORK id identity) ONLINE_USERS_CANISTER_ID=$(dfx canister --network $NETWORK id online_users) +GREET_BOT_CANISTER_ID=$(dfx canister --network $NETWORK id greet_bot) PROPOSALS_BOT_CANISTER_ID=$(dfx canister --network $NETWORK id proposals_bot) AIRDROP_BOT_CANISTER_ID=$(dfx canister --network $NETWORK id airdrop_bot) STORAGE_INDEX_CANISTER_ID=$(dfx canister --network $NETWORK id storage_index) @@ -70,6 +71,7 @@ cargo run \ --notifications $NOTIFICATIONS_CANISTER_ID \ --identity $IDENTITY_CANISTER_ID \ --online-users $ONLINE_USERS_CANISTER_ID \ + --greet-bot $GREET_BOT_CANISTER_ID \ --proposals-bot $PROPOSALS_BOT_CANISTER_ID \ --airdrop-bot $AIRDROP_BOT_CANISTER_ID \ --storage-index $STORAGE_INDEX_CANISTER_ID \