Skip to content

Commit

Permalink
Achievements BE part 1 (#5962)
Browse files Browse the repository at this point in the history
Co-authored-by: Julian Jelfs <[email protected]>
  • Loading branch information
megrogan and julianjelfs authored Jun 26, 2024
1 parent dd7b5dd commit f09d153
Show file tree
Hide file tree
Showing 30 changed files with 333 additions and 28 deletions.
4 changes: 4 additions & 0 deletions backend/canisters/local_user_index/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [unreleased]

### Changed

- In `ChitEarnedReason::Achievement` replaced `String` with `Achievement` ([#5962](https://github.com/open-chat-labs/open-chat/pull/5962))

## [[2.0.1198](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1198-local_user_index)] - 2024-06-07

### Changed
Expand Down
4 changes: 4 additions & 0 deletions backend/canisters/user/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [unreleased]

### Changed

- Added `achievements` ([#5962](https://github.com/open-chat-labs/open-chat/pull/5962))

## [[2.0.1213](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1213-user)] - 2024-06-24

### Changed
Expand Down
13 changes: 13 additions & 0 deletions backend/canisters/user/api/can.did
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ type TipMessageResponse = variant {
InternalError : record { text; CompletedCryptoTransaction };
};

type MarkAchievementsSeenArgs = record {
last_seen : TimestampMillis;
};

type MarkAchievementsSeenResponse = variant {
Success;
};

type MarkReadArgs = record {
messages_read : vec ChatMessagesRead;
community_messages_read : vec CommunityMessagesRead;
Expand Down Expand Up @@ -857,6 +865,8 @@ type InitialStateResponse = variant {
suspended : bool;
pin_number_settings : opt PinNumberSettings;
local_user_index_canister_id : CanisterId;
achievements : vec ChitEarned;
achievements_last_seen : TimestampMillis;
};
};

Expand Down Expand Up @@ -911,6 +921,8 @@ type UpdatesResponse = variant {
NoChange;
};
suspended : opt bool;
achievements : vec ChitEarned;
achievements_last_seen : opt TimestampMillis;
};
SuccessNoUpdates;
};
Expand Down Expand Up @@ -1159,6 +1171,7 @@ service : {
add_reaction : (AddReactionArgs) -> (AddReactionResponse);
remove_reaction : (RemoveReactionArgs) -> (RemoveReactionResponse);
tip_message : (TipMessageArgs) -> (TipMessageResponse);
mark_achievements_seen : (MarkAchievementsSeenArgs) -> (MarkAchievementsSeenResponse);
mark_read : (MarkReadArgs) -> (MarkReadResponse);
block_user : (BlockUserArgs) -> (BlockUserResponse);
unblock_user : (UnblockUserArgs) -> (UnblockUserResponse);
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fn main() {
generate_candid_method!(user, leave_community, update);
generate_candid_method!(user, leave_group, update);
generate_candid_method!(user, manage_favourite_chats, update);
generate_candid_method!(user, mark_achievements_seen, update);
generate_candid_method!(user, mark_read, update);
generate_candid_method!(user, mute_notifications, update);
generate_candid_method!(user, pin_chat_v2, update);
Expand Down
4 changes: 3 additions & 1 deletion backend/canisters/user/api/src/queries/initial_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use candid::CandidType;
use serde::{Deserialize, Serialize};
use types::{CanisterId, Chat, ChatId, DirectChatSummary, Empty, GroupChatSummary, TimestampMillis, UserId};
use types::{CanisterId, Chat, ChatId, ChitEarned, DirectChatSummary, Empty, GroupChatSummary, TimestampMillis, UserId};

pub type Args = Empty;

Expand All @@ -21,6 +21,8 @@ pub struct SuccessResult {
pub suspended: bool,
pub pin_number_settings: Option<PinNumberSettings>,
pub local_user_index_canister_id: CanisterId,
pub achievements: Vec<ChitEarned>,
pub achievements_last_seen: TimestampMillis,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
Expand Down
6 changes: 5 additions & 1 deletion backend/canisters/user/api/src/queries/updates.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::initial_state::PinNumberSettings;
use candid::CandidType;
use serde::{Deserialize, Serialize};
use types::{Chat, ChatId, CommunityId, DirectChatSummary, DirectChatSummaryUpdates, OptionUpdate, TimestampMillis, UserId};
use types::{
Chat, ChatId, ChitEarned, CommunityId, DirectChatSummary, DirectChatSummaryUpdates, OptionUpdate, TimestampMillis, UserId,
};

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
Expand All @@ -28,6 +30,8 @@ pub struct SuccessResult {
pub blocked_users: Option<Vec<UserId>>,
pub suspended: Option<bool>,
pub pin_number_settings: OptionUpdate<PinNumberSettings>,
pub achievements: Vec<ChitEarned>,
pub achievements_last_seen: Option<TimestampMillis>,
}

#[derive(CandidType, Serialize, Deserialize, Clone, Debug)]
Expand Down
13 changes: 13 additions & 0 deletions backend/canisters/user/api/src/updates/mark_achievements_seen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use candid::CandidType;
use serde::{Deserialize, Serialize};
use types::TimestampMillis;

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
pub last_seen: TimestampMillis,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub enum Response {
Success,
}
1 change: 1 addition & 0 deletions backend/canisters/user/api/src/updates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub mod join_video_call;
pub mod leave_community;
pub mod leave_group;
pub mod manage_favourite_chats;
pub mod mark_achievements_seen;
pub mod mark_read;
pub mod mute_notifications;
pub mod pin_chat_v2;
Expand Down
20 changes: 18 additions & 2 deletions backend/canisters/user/impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ use std::collections::HashSet;
use std::ops::Deref;
use std::time::Duration;
use types::{
BuildVersion, CanisterId, Chat, ChatId, ChatMetrics, CommunityId, Cryptocurrency, Cycles, Document, Notification,
TimestampMillis, Timestamped, UserId,
Achievement, BuildVersion, CanisterId, Chat, ChatId, ChatMetrics, ChitEarned, ChitEarnedReason, CommunityId,
Cryptocurrency, Cycles, Document, Notification, TimestampMillis, Timestamped, UserId,
};
use user_canister::{NamedAccount, UserCanisterEvent};
use utils::canister_event_sync_queue::CanisterEventSyncQueue;
Expand Down Expand Up @@ -173,6 +173,16 @@ impl RuntimeState {
},
}
}

pub fn insert_achievement(&mut self, achievement: Achievement) {
if self.data.achievements.insert(achievement.clone()) {
self.data.chit_events.push(ChitEarned {
amount: achievement.chit_reward() as i32,
timestamp: self.env.now(),
reason: ChitEarnedReason::Achievement(achievement),
});
}
}
}

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -214,6 +224,10 @@ struct Data {
pub pin_number: PinNumber,
pub btc_address: Option<String>,
pub chit_events: ChitEarnedEvents,
#[serde(default)]
pub achievements: HashSet<Achievement>,
#[serde(default)]
pub achievements_last_seen: TimestampMillis,
pub rng_seed: [u8; 32],
}

Expand Down Expand Up @@ -272,6 +286,8 @@ impl Data {
pin_number: PinNumber::default(),
btc_address: None,
chit_events: ChitEarnedEvents::default(),
achievements: HashSet::new(),
achievements_last_seen: 0,
rng_seed: [0; 32],
}
}
Expand Down
76 changes: 74 additions & 2 deletions backend/canisters/user/impl/src/lifecycle/post_upgrade.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::lifecycle::{init_env, init_state};
use crate::memory::get_upgrades_memory;
use crate::{mutate_state, Data};
use crate::{mutate_state, Data, RuntimeState};
use canister_logger::LogEntry;
use canister_tracing_macros::trace;
use chat_events::Reader;
use ic_cdk::post_upgrade;
use stable_memory::get_reader;
use std::time::Duration;
use tracing::info;
use types::{Empty, Milliseconds};
use types::{Achievement, Empty, Milliseconds, UserId};
use user_canister::post_upgrade::Args;
use utils::time::DAY_IN_MS;

Expand Down Expand Up @@ -37,6 +38,8 @@ fn post_upgrade(args: Args) {
ic_cdk_timers::set_timer(Duration::ZERO, mark_user_canister_empty);
}
});

mutate_state(initialize_achievements);
}

fn mark_user_canister_empty() {
Expand All @@ -49,3 +52,72 @@ fn mark_user_canister_empty() {
);
})
}

fn initialize_achievements(state: &mut RuntimeState) {
let longest_streak = state.data.chit_events.init_streak();

let me: UserId = state.env.canister_id().into();
let now = state.env.now();

if state.data.group_chats.len() > 0 {
state.insert_achievement(Achievement::JoinedGroup);
}

if state.data.communities.len() > 0 {
state.insert_achievement(Achievement::JoinedCommunity);
}

if state.data.direct_chats.iter().any(|c| {
c.events
.main_events_reader()
.iter_latest_messages(None)
.any(|m| m.event.sender == me)
}) {
state.insert_achievement(Achievement::SentDirectMessage);
}

if state.data.direct_chats.iter().any(|c| {
c.events
.main_events_reader()
.iter_latest_messages(None)
.any(|m| m.event.sender == c.them)
}) {
state.insert_achievement(Achievement::ReceivedDirectMessage);
}

if state.data.avatar.value.is_some() {
state.insert_achievement(Achievement::SetAvatar);
}

if !state.data.bio.value.is_empty() {
state.insert_achievement(Achievement::SetBio);
}

if state.data.display_name.value.is_some() {
state.insert_achievement(Achievement::SetDisplayName);
}

if let Some(diamond_expires) = state.data.diamond_membership_expires_at {
state.insert_achievement(Achievement::UpgradedToDiamond);

if (diamond_expires - now) > (5 * 365 * DAY_IN_MS) {
state.insert_achievement(Achievement::UpgradedToGoldDiamond);
}
}

if longest_streak >= 3 {
state.insert_achievement(Achievement::Streak3);
}

if longest_streak >= 7 {
state.insert_achievement(Achievement::Streak7);
}

if longest_streak >= 14 {
state.insert_achievement(Achievement::Streak14);
}

if longest_streak >= 30 {
state.insert_achievement(Achievement::Streak30);
}
}
55 changes: 50 additions & 5 deletions backend/canisters/user/impl/src/model/chit.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
use serde::{Deserialize, Serialize};
use types::{ChitEarned, TimestampMillis};
use std::cmp::max;
use types::{ChitEarned, ChitEarnedReason, TimestampMillis};
use utils::streak::Streak;

#[derive(Serialize, Deserialize, Default)]
pub struct ChitEarnedEvents {
events: Vec<ChitEarned>,
#[serde(default)]
streak: Streak,
}

impl ChitEarnedEvents {
pub fn init_streak(&mut self) -> u16 {
let mut max_streak: u16 = 0;

for event in self.events.iter() {
if matches!(event.reason, ChitEarnedReason::DailyClaim) {
self.streak.claim(event.timestamp);

max_streak = max(max_streak, self.streak.days(event.timestamp))
}
}

max_streak
}

pub fn push(&mut self, event: ChitEarned) {
let mut sort = false;

Expand All @@ -16,6 +34,10 @@ impl ChitEarnedEvents {
}
}

if matches!(event.reason, ChitEarnedReason::DailyClaim) {
self.streak.claim(event.timestamp);
}

self.events.push(event);

if sort {
Expand Down Expand Up @@ -51,11 +73,33 @@ impl ChitEarnedEvents {

(page, self.events.len() as u32)
}

pub fn achievements(&self, since: Option<TimestampMillis>) -> Vec<ChitEarned> {
self.events
.iter()
.rev()
.take_while(|e| since.map_or(true, |ts| e.timestamp > ts))
.filter(|e| matches!(e.reason, ChitEarnedReason::Achievement(_)))
.cloned()
.collect()
}

pub fn has_achievements_since(&self, since: TimestampMillis) -> bool {
self.events
.iter()
.rev()
.take_while(|e| e.timestamp > since)
.any(|e| matches!(e.reason, ChitEarnedReason::Achievement(_)))
}

pub fn streak(&self, now: TimestampMillis) -> u16 {
self.streak.days(now)
}
}

#[cfg(test)]
mod tests {
use types::ChitEarnedReason;
use types::{Achievement, ChitEarnedReason};

use super::*;

Expand Down Expand Up @@ -130,6 +174,7 @@ mod tests {

fn init_test_data() -> ChitEarnedEvents {
ChitEarnedEvents {
streak: Streak::default(),
events: vec![
ChitEarned {
amount: 200,
Expand All @@ -149,7 +194,7 @@ mod tests {
ChitEarned {
amount: 500,
timestamp: 13,
reason: ChitEarnedReason::Achievement("Bio".to_string()),
reason: ChitEarnedReason::Achievement(Achievement::SetBio),
},
ChitEarned {
amount: 300,
Expand All @@ -159,12 +204,12 @@ mod tests {
ChitEarned {
amount: 500,
timestamp: 15,
reason: ChitEarnedReason::Achievement("Avatar".to_string()),
reason: ChitEarnedReason::Achievement(Achievement::SetAvatar),
},
ChitEarned {
amount: 500,
timestamp: 16,
reason: ChitEarnedReason::Achievement("First message".to_string()),
reason: ChitEarnedReason::Achievement(Achievement::SentDirectMessage),
},
],
}
Expand Down
2 changes: 2 additions & 0 deletions backend/canisters/user/impl/src/queries/initial_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@ fn initial_state_impl(state: &RuntimeState) -> Response {
suspended: state.data.suspended.value,
pin_number_settings: state.data.pin_number.enabled().then(|| state.data.pin_number.settings(now)),
local_user_index_canister_id: state.data.local_user_index_canister_id,
achievements: state.data.chit_events.achievements(None),
achievements_last_seen: state.data.achievements_last_seen,
})
}
Loading

0 comments on commit f09d153

Please sign in to comment.