diff --git a/commands.txt b/commands.txt index 1a427ec818..59433359a9 100644 --- a/commands.txt +++ b/commands.txt @@ -20,5 +20,8 @@ dfx canister call chats send_message '(45_292_552_032_833_753, "whats up?")' -- Group Chat dfx identity use matt dfx canister call user_mgmt get_principal '("Hamish")' -dfx canister call chats create_group_chat '(vec {principal "uhpjy-crxbe-6chrq-vadba-aqrmz-f4d2e-h7nxj-7hrso-ebvqe-gnygw-tae"}, "Xmas 2020")' +dfx canister call chats create_group_chat '(vec { principal "uhpjy-crxbe-6chrq-vadba-aqrmz-f4d2e-h7nxj-7hrso-ebvqe-gnygw-tae" }, "Xmas 2020")' dfx canister call chats send_message '(12_699_180_841_646_933_661, "Are you coming to mine for xmas?")' + +-- Get Users +dfx canister call user_mgmt get_users '(vec { record { id = principal "uhpjy-crxbe-6chrq-vadba-aqrmz-f4d2e-h7nxj-7hrso-ebvqe-gnygw-tae"; cached_version = opt 1 } })' \ No newline at end of file diff --git a/src/chats/src/domain/chat.rs b/src/chats/src/domain/chat.rs index 49f15ce917..26a6e3ea05 100644 --- a/src/chats/src/domain/chat.rs +++ b/src/chats/src/domain/chat.rs @@ -3,9 +3,9 @@ use ic_types::Principal; use enum_dispatch::enum_dispatch; use highway::{HighwayHasher, HighwayHash}; use serde::Deserialize; +use shared::timestamp::Timestamp; use crate::domain::direct_chat::{DirectChat, DirectChatSummary}; use crate::domain::group_chat::{GroupChat, GroupChatSummary}; -use crate::Timestamp; #[enum_dispatch(Chat)] #[derive(CandidType, Deserialize)] diff --git a/src/chats/src/domain/chat_list.rs b/src/chats/src/domain/chat_list.rs index 55cabd026e..fd21201f26 100644 --- a/src/chats/src/domain/chat_list.rs +++ b/src/chats/src/domain/chat_list.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, hash_map::Entry::{Occupied, Vacant}}; use ic_types::Principal; -use shared::StableState; -use crate::Timestamp; +use shared::timestamp::Timestamp; +use shared::upgrade::StableState; use super::chat::{Chat, ChatEnum, ChatId, ChatSummary}; use super::direct_chat::DirectChat; use super::group_chat::GroupChat; diff --git a/src/chats/src/domain/direct_chat.rs b/src/chats/src/domain/direct_chat.rs index fa38fb0c0a..6c81b364bd 100644 --- a/src/chats/src/domain/direct_chat.rs +++ b/src/chats/src/domain/direct_chat.rs @@ -1,7 +1,7 @@ use ic_cdk::export::candid::CandidType; use ic_types::Principal; use serde::Deserialize; -use crate::Timestamp; +use shared::timestamp::Timestamp; use super::chat::*; #[derive(CandidType, Deserialize)] diff --git a/src/chats/src/domain/group_chat.rs b/src/chats/src/domain/group_chat.rs index 032a01ce7c..06a01b5c6b 100644 --- a/src/chats/src/domain/group_chat.rs +++ b/src/chats/src/domain/group_chat.rs @@ -2,7 +2,7 @@ use std::cmp::max; use ic_cdk::export::candid::CandidType; use ic_types::Principal; use serde::Deserialize; -use crate::Timestamp; +use shared::timestamp::Timestamp; use super::chat::*; #[derive(CandidType, Deserialize)] diff --git a/src/chats/src/lib.rs b/src/chats/src/lib.rs index 4807fe692b..87762aa351 100644 --- a/src/chats/src/lib.rs +++ b/src/chats/src/lib.rs @@ -2,10 +2,4 @@ mod api; mod domain; mod queries; mod updates; -mod upgrade; - -pub(crate) fn get_current_timestamp() -> Timestamp { - ic_cdk::api::time() as Timestamp -} - -pub type Timestamp = u64; \ No newline at end of file +mod upgrade; \ No newline at end of file diff --git a/src/chats/src/updates/create_direct_chat.rs b/src/chats/src/updates/create_direct_chat.rs index ff889c10e6..fc70d57664 100644 --- a/src/chats/src/updates/create_direct_chat.rs +++ b/src/chats/src/updates/create_direct_chat.rs @@ -1,12 +1,13 @@ use ic_cdk::storage; use ic_types::Principal; +use shared::timestamp; use crate::domain::chat::ChatId; use crate::domain::chat_list::ChatList; pub fn update(recipient: Principal, text: String) -> Option { let chat_list: &mut ChatList = storage::get_mut(); let me = ic_cdk::caller(); - let now = crate::get_current_timestamp(); + let now = timestamp::now(); chat_list.create_direct_chat(me, recipient, text, now) } \ No newline at end of file diff --git a/src/chats/src/updates/create_group_chat.rs b/src/chats/src/updates/create_group_chat.rs index 1df35c6010..ccb5916879 100644 --- a/src/chats/src/updates/create_group_chat.rs +++ b/src/chats/src/updates/create_group_chat.rs @@ -1,12 +1,13 @@ use ic_cdk::storage; use ic_types::Principal; +use shared::timestamp; use crate::domain::chat::ChatId; use crate::domain::chat_list::ChatList; pub fn update(participants: Vec, subject: String) -> Option { let chat_list: &mut ChatList = storage::get_mut(); let me = ic_cdk::caller(); - let now = crate::get_current_timestamp(); + let now = timestamp::now(); chat_list.create_group_chat(me, participants, subject, now) } \ No newline at end of file diff --git a/src/chats/src/updates/send_message.rs b/src/chats/src/updates/send_message.rs index 37c6d9b585..b9f84de503 100644 --- a/src/chats/src/updates/send_message.rs +++ b/src/chats/src/updates/send_message.rs @@ -1,4 +1,5 @@ use ic_cdk::storage; +use shared::timestamp; use crate::domain::chat::{Chat, ChatId}; use crate::domain::chat_list::ChatList; @@ -7,7 +8,7 @@ pub fn update(chat_id: ChatId, text: String) -> Option { let me = ic_cdk::caller(); if let Some(chat) = chat_list.get_mut(chat_id, &me) { - let now = crate::get_current_timestamp(); + let now = timestamp::now(); let message_id = chat.push_message(&me, text, now); return Some(message_id); } diff --git a/src/chats/src/upgrade.rs b/src/chats/src/upgrade.rs index d205dce0ec..52194b3787 100644 --- a/src/chats/src/upgrade.rs +++ b/src/chats/src/upgrade.rs @@ -1,12 +1,13 @@ use ic_cdk_macros::*; +use shared::upgrade; use crate::domain::chat_list::ChatList; #[pre_upgrade] fn pre_upgrade() { - shared::pre_upgrade::(); + upgrade::pre_upgrade::(); } #[post_upgrade] fn post_upgrade() { - shared::post_upgrade::(); + upgrade::post_upgrade::(); } \ No newline at end of file diff --git a/src/shared/src/lib.rs b/src/shared/src/lib.rs index ab2e5dd069..3120a5884b 100644 --- a/src/shared/src/lib.rs +++ b/src/shared/src/lib.rs @@ -1,30 +1,2 @@ -use std::mem; -use ic_cdk::export::candid::CandidType; -use ic_cdk::storage; -use serde::de::DeserializeOwned; - -pub fn pre_upgrade() { - let from_storage: &mut T = storage::get_mut(); - - let val = mem::take(from_storage); - - let to_save = val.drain(); - - storage::stable_save((to_save,)).unwrap(); -} - -pub fn post_upgrade() { - let (saved,) = storage::stable_restore().unwrap(); - - let val = T::fill(saved); - - let from_storage: &mut T = storage::get_mut(); - - *from_storage = val; -} - -pub trait StableState: 'static + Default { - type State: CandidType + DeserializeOwned; - fn drain(self) -> Self::State; - fn fill(source: Self::State) -> Self; -} \ No newline at end of file +pub mod timestamp; +pub mod upgrade; \ No newline at end of file diff --git a/src/shared/src/timestamp.rs b/src/shared/src/timestamp.rs new file mode 100644 index 0000000000..6d922571e0 --- /dev/null +++ b/src/shared/src/timestamp.rs @@ -0,0 +1,5 @@ +pub type Timestamp = u64; + +pub fn now() -> Timestamp { + ic_cdk::api::time() as Timestamp +} diff --git a/src/shared/src/upgrade.rs b/src/shared/src/upgrade.rs new file mode 100644 index 0000000000..bfeb8ff731 --- /dev/null +++ b/src/shared/src/upgrade.rs @@ -0,0 +1,30 @@ +use std::mem; +use ic_cdk::export::candid::CandidType; +use ic_cdk::storage; +use serde::de::DeserializeOwned; + +pub fn pre_upgrade() { + let from_storage: &mut T = storage::get_mut(); + + let val = mem::take(from_storage); + + let to_save = val.drain(); + + storage::stable_save((to_save,)).unwrap(); +} + +pub fn post_upgrade() { + let (saved,) = storage::stable_restore().unwrap(); + + let val = T::fill(saved); + + let from_storage: &mut T = storage::get_mut(); + + *from_storage = val; +} + +pub trait StableState: 'static + Default { + type State: CandidType + DeserializeOwned; + fn drain(self) -> Self::State; + fn fill(source: Self::State) -> Self; +} \ No newline at end of file diff --git a/src/user_mgmt/Cargo.toml b/src/user_mgmt/Cargo.toml index 9c5c560ca1..c16b40c795 100644 --- a/src/user_mgmt/Cargo.toml +++ b/src/user_mgmt/Cargo.toml @@ -14,6 +14,6 @@ crate-type = ["cdylib"] ic-cdk = "0.2.0" ic-cdk-macros = "0.2.0" ic-types = "0.1.1" -bimap = "0.5.3" +multi-map = "1.3.0" serde = "1.0.111" shared = { path = "../shared", version = "0.1.0" } diff --git a/src/user_mgmt/src/api.rs b/src/user_mgmt/src/api.rs index 74a41f86ff..d3e1d42038 100644 --- a/src/user_mgmt/src/api.rs +++ b/src/user_mgmt/src/api.rs @@ -1,10 +1,16 @@ use ic_cdk_macros::*; use ic_types::Principal; +use crate::domain::user_store::{RegisterUserResult, UpdateUsernameResult, UserSummary}; use crate::queries::*; use crate::updates::*; #[update] -pub fn set_username(username: String) -> bool { +pub fn register_user(username: String) -> RegisterUserResult { + register_user::update(username) +} + +#[update] +pub fn update_username(username: String) -> UpdateUsernameResult { set_username::update(username) } @@ -16,4 +22,9 @@ pub fn get_username() -> Option { #[query] pub fn get_principal(username: String) -> Option { get_principal::query(&username) +} + +#[query] +pub fn get_users(users: Vec) -> Vec { + get_users::query(users) } \ No newline at end of file diff --git a/src/user_mgmt/src/domain/mod.rs b/src/user_mgmt/src/domain/mod.rs index 337c7dc047..bd9b6d2237 100644 --- a/src/user_mgmt/src/domain/mod.rs +++ b/src/user_mgmt/src/domain/mod.rs @@ -1 +1 @@ -pub mod user_data; \ No newline at end of file +pub mod user_store; diff --git a/src/user_mgmt/src/domain/user_data.rs b/src/user_mgmt/src/domain/user_data.rs deleted file mode 100644 index 49e7452008..0000000000 --- a/src/user_mgmt/src/domain/user_data.rs +++ /dev/null @@ -1,56 +0,0 @@ -use ic_types::Principal; -use bimap::BiMap; -use shared::StableState; - -#[derive(Default)] -pub struct UserData { - user_map: BiMap -} - -impl UserData { - pub fn set_username(&mut self, principal: Principal, username: String) -> SetUsernameResponse { - let previous_username: Option<&String> = self.user_map.get_by_left(&principal); - - if previous_username.is_some() && previous_username.unwrap() == &username { - return SetUsernameResponse::SuccessNoChange; - } - - if self.user_map.contains_right(&username) { - return SetUsernameResponse::UsernameTaken; - } - - self.user_map.insert(principal, username); - - SetUsernameResponse::Success - } - - pub fn get_username(&self, principal: &Principal) -> Option { - self.user_map.get_by_left(principal).map(|s| s.clone()) - } - - pub fn get_principal(&self, username: &String) -> Option { - self.user_map.get_by_right(username).map(|p| p.clone()) - } -} - -impl StableState for UserData { - type State = Vec<(Principal, String)>; - - fn drain(self) -> Vec<(Principal, String)> { - self.user_map.into_iter().collect() - } - - fn fill(vec: Vec<(Principal, String)>) -> UserData { - let user_map: BiMap = vec.into_iter().collect(); - - UserData { - user_map - } - } -} - -pub enum SetUsernameResponse { - Success, - SuccessNoChange, - UsernameTaken -} \ No newline at end of file diff --git a/src/user_mgmt/src/domain/user_store.rs b/src/user_mgmt/src/domain/user_store.rs new file mode 100644 index 0000000000..5305bdf074 --- /dev/null +++ b/src/user_mgmt/src/domain/user_store.rs @@ -0,0 +1,134 @@ +use ic_cdk::export::candid::CandidType; +use ic_types::Principal; +use multi_map::MultiMap; +use serde::Deserialize; +use shared::upgrade::StableState; +use shared::timestamp::Timestamp; +use crate::queries::get_users::GetUserRequest; + +#[derive(Default)] +pub struct UserStore { + data: MultiMap +} + +impl UserStore { + pub fn register_user(&mut self, principal: Principal, username: String, now: Timestamp) -> RegisterUserResult { + if self.data.contains_key(&principal) { return RegisterUserResult::UserExists; } + if self.data.contains_key_alt(&username) { return RegisterUserResult::UsernameExists; } + + let user = User { + id: principal.clone(), + username: username.clone(), + joined: now, + last_updated: now, + version: 1 + }; + + self.data.insert(principal, username, user); + + RegisterUserResult::Success + } + + pub fn update_username(&mut self, principal: Principal, username: String, now: Timestamp) -> UpdateUsernameResult { + if let Some(match_by_username) = self.data.get_alt(&username) { + return if match_by_username.id == principal { + UpdateUsernameResult::SuccessNoChange + } else { + UpdateUsernameResult::UsernameTaken + }; + } + + if let Some(mut user) = self.data.remove(&principal) { + user.username = username.clone(); + user.last_updated = now; + user.version += 1; + + self.data.insert(principal, username, user); + + UpdateUsernameResult::Success + } else { + UpdateUsernameResult::UserNotFound + } + } + + pub fn get_username(&self, principal: &Principal) -> Option { + self.data.get(principal).map(|u| u.username.clone()) + } + + pub fn get_principal(&self, username: &String) -> Option { + self.data.get_alt(username).map(|u| u.id.clone()) + } + + pub fn get_users(&self, users: Vec) -> Vec { + users + .iter() + .filter_map(|r| self.data.get(&r.id).map(|u| (u, r.cached_version))) + .filter(|(u, v)| v.is_none() || v.unwrap() < u.version) + .map(|(u, _)| UserSummary::new(u)) + .collect() + } +} + +impl StableState for UserStore { + type State = Vec; + + fn drain(self) -> Vec { + self.data + .into_iter() + .map(|(_, (_, u))| u) + .collect() + } + + fn fill(users: Vec) -> UserStore { + let mut data = MultiMap::with_capacity(users.len()); + + for user in users { + data.insert(user.id.clone(), user.username.clone(), user); + } + + UserStore { + data + } + } +} + +#[derive(CandidType, Deserialize, Debug)] +pub struct User { + id: Principal, + username: String, + joined: Timestamp, + last_updated: Timestamp, + version: u32 +} + +#[derive(CandidType)] +pub struct UserSummary { + id: Principal, + username: String, + version: u32 +} + +impl UserSummary { + fn new(user: &User) -> UserSummary { + UserSummary { + id: user.id.clone(), + username: user.username.clone(), + version: user.version + } + } +} + +#[derive(CandidType)] +pub enum RegisterUserResult { + Success, + UserExists, + UsernameExists +} + +#[derive(CandidType)] +pub enum UpdateUsernameResult { + Success, + SuccessNoChange, + UsernameTaken, + UserNotFound +} \ No newline at end of file diff --git a/src/user_mgmt/src/queries/get_principal.rs b/src/user_mgmt/src/queries/get_principal.rs index b22f37e27a..3fb169aea9 100644 --- a/src/user_mgmt/src/queries/get_principal.rs +++ b/src/user_mgmt/src/queries/get_principal.rs @@ -1,9 +1,9 @@ use ic_cdk::storage; use ic_types::Principal; -use crate::domain::user_data::UserData; +use crate::domain::user_store::UserStore; pub fn query(username: &String) -> Option { - let user_data: &UserData = storage::get(); + let user_store: &UserStore = storage::get(); - user_data.get_principal(username) + user_store.get_principal(username) } \ No newline at end of file diff --git a/src/user_mgmt/src/queries/get_username.rs b/src/user_mgmt/src/queries/get_username.rs index e442d32ba5..48431f43f7 100644 --- a/src/user_mgmt/src/queries/get_username.rs +++ b/src/user_mgmt/src/queries/get_username.rs @@ -1,10 +1,10 @@ use ic_cdk::storage; -use crate::domain::user_data::UserData; +use crate::domain::user_store::UserStore; pub fn query() -> Option { let principal = ic_cdk::caller(); - let user_data: &UserData = storage::get(); + let user_store: &UserStore = storage::get(); - user_data.get_username(&principal) + user_store.get_username(&principal) } \ No newline at end of file diff --git a/src/user_mgmt/src/queries/get_users.rs b/src/user_mgmt/src/queries/get_users.rs new file mode 100644 index 0000000000..427cc909a9 --- /dev/null +++ b/src/user_mgmt/src/queries/get_users.rs @@ -0,0 +1,16 @@ +use ic_cdk::export::candid::Principal; +use ic_cdk::storage; +use serde::Deserialize; +use crate::domain::user_store::{UserStore, UserSummary}; + +pub fn query(users: Vec) -> Vec { + let user_store: &UserStore = storage::get(); + + user_store.get_users(users) +} + +#[derive(Deserialize)] +pub struct GetUserRequest { + pub id: Principal, + pub cached_version: Option +} \ No newline at end of file diff --git a/src/user_mgmt/src/queries/mod.rs b/src/user_mgmt/src/queries/mod.rs index 7cf2dcfd13..5ae0952e63 100644 --- a/src/user_mgmt/src/queries/mod.rs +++ b/src/user_mgmt/src/queries/mod.rs @@ -1,2 +1,3 @@ pub mod get_principal; -pub mod get_username; \ No newline at end of file +pub mod get_username; +pub mod get_users; diff --git a/src/user_mgmt/src/updates/mod.rs b/src/user_mgmt/src/updates/mod.rs index 5e938d095e..9a1cea8ffb 100644 --- a/src/user_mgmt/src/updates/mod.rs +++ b/src/user_mgmt/src/updates/mod.rs @@ -1 +1,2 @@ +pub mod register_user; pub mod set_username; \ No newline at end of file diff --git a/src/user_mgmt/src/updates/register_user.rs b/src/user_mgmt/src/updates/register_user.rs new file mode 100644 index 0000000000..9369a409e6 --- /dev/null +++ b/src/user_mgmt/src/updates/register_user.rs @@ -0,0 +1,12 @@ +use ic_cdk::storage; +use shared::timestamp; +use crate::domain::user_store::{RegisterUserResult, UserStore}; + +pub fn update(username: String) -> RegisterUserResult { + let principal = ic_cdk::caller(); + let now = timestamp::now(); + + let user_store: &mut UserStore = storage::get_mut(); + + user_store.register_user(principal, username, now) +} \ No newline at end of file diff --git a/src/user_mgmt/src/updates/set_username.rs b/src/user_mgmt/src/updates/set_username.rs index 2b865372ec..e1d64dde6a 100644 --- a/src/user_mgmt/src/updates/set_username.rs +++ b/src/user_mgmt/src/updates/set_username.rs @@ -1,16 +1,12 @@ use ic_cdk::storage; -use crate::domain::user_data::{SetUsernameResponse, UserData}; +use shared::timestamp; +use crate::domain::user_store::{UpdateUsernameResult, UserStore}; -pub fn update(username: String) -> bool { +pub fn update(username: String) -> UpdateUsernameResult { let principal = ic_cdk::caller(); - - let user_data: &mut UserData = storage::get_mut(); + let now = timestamp::now(); - let result = user_data.set_username(principal, username); + let user_store: &mut UserStore = storage::get_mut(); - match result { - SetUsernameResponse::Success => true, - SetUsernameResponse::SuccessNoChange => true, - SetUsernameResponse::UsernameTaken => false - } + user_store.update_username(principal, username, now) } \ No newline at end of file diff --git a/src/user_mgmt/src/upgrade.rs b/src/user_mgmt/src/upgrade.rs index d232616f7f..74e632bcfc 100644 --- a/src/user_mgmt/src/upgrade.rs +++ b/src/user_mgmt/src/upgrade.rs @@ -1,12 +1,13 @@ use ic_cdk_macros::*; -use crate::domain::user_data::UserData; +use shared::upgrade; +use crate::domain::user_store::UserStore; #[pre_upgrade] fn pre_upgrade() { - shared::pre_upgrade::(); + upgrade::pre_upgrade::(); } #[post_upgrade] fn post_upgrade() { - shared::post_upgrade::(); + upgrade::post_upgrade::(); } \ No newline at end of file diff --git a/src/user_mgmt/user_mgmt.did b/src/user_mgmt/user_mgmt.did index 2b451d1a4f..91c203fdf4 100644 --- a/src/user_mgmt/user_mgmt.did +++ b/src/user_mgmt/user_mgmt.did @@ -1,5 +1,35 @@ +type RegisterUserResult = + variant { + Success; + UserExists; + UsernameExists + }; + +type UpdateUsernameResult = + variant { + Success; + SuccessNoChange; + UsernameTaken; + UserNotFound + }; + +type GetUserRequest = + record { + id: principal; + cached_version: opt nat32; + }; + +type UserSummary = + record { + id: principal; + username: text; + version: nat32; + }; + service : { - set_username: (text) -> (bool); - get_username: () -> (opt text) query; + register_user: (text) -> (RegisterUserResult); + update_username: (text) -> (UpdateUsernameResult); get_principal: (text) -> (opt principal) query; + get_username: () -> (opt text) query; + get_users: (vec GetUserRequest) -> (vec UserSummary) query; } \ No newline at end of file