Skip to content

Commit

Permalink
Register endpoint for external achievements (#6367)
Browse files Browse the repository at this point in the history
  • Loading branch information
megrogan authored Sep 11, 2024
1 parent eebb324 commit 55d1849
Show file tree
Hide file tree
Showing 17 changed files with 204 additions and 63 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/canisters/local_user_index/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ pub struct ChitEarned {

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExternalAchievementAwarded {
#[serde(default)]
pub id: u32,
pub user_id: UserId,
pub name: String,
pub chit_reward: u32,
Expand Down
21 changes: 19 additions & 2 deletions backend/canisters/user_index/api/can.did
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ type AddPlatformOperatorResponse = variant {
Success;
};

type RegisterExternalAchievementArgs = record {
id : nat32;
name : text;
logo : text;
url : text;
canister_id : CanisterId;
chit_reward : nat32;
expires : TimestampMillis;
chit_budget : nat32;
};

type RegisterExternalAchievementResponse = variant {
Success;
};

type RemovePlatformOperatorArgs = record {
user_id : UserId;
};
Expand Down Expand Up @@ -268,8 +283,9 @@ type ExternalAchievementsResponse = variant {
};

type ExternalAchievement = record {
id : nat32;
name : text;
logo_id : nat;
url : text;
chit_reward : nat32;
};

Expand Down Expand Up @@ -393,8 +409,8 @@ type AddReferralCodesResponse = variant {
};

type AwardExternalAchievementArgs = record {
achievement_id : nat32;
user_id : UserId;
name : text;
};

type AwardExternalAchievementResponse = variant {
Expand Down Expand Up @@ -461,6 +477,7 @@ service : {
// Only callable by SNS governance canister
add_platform_moderator : (AddPlatformModeratorArgs) -> (AddPlatformModeratorResponse);
add_platform_operator : (AddPlatformOperatorArgs) -> (AddPlatformOperatorResponse);
register_external_achievement : (RegisterExternalAchievementArgs) -> (RegisterExternalAchievementResponse);
remove_platform_moderator : (RemovePlatformModeratorArgs) -> (RemovePlatformModeratorResponse);
remove_platform_operator : (RemovePlatformOperatorArgs) -> (RemovePlatformOperatorResponse);
assign_platform_moderators_group : (AssignPlatformModeratorsGroupArgs) -> (AssignPlatformModeratorsGroupResponse);
Expand Down
8 changes: 5 additions & 3 deletions backend/canisters/user_index/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use candid::Principal;
use serde::{Deserialize, Serialize};
use types::{
CanisterId, ChannelLatestMessageIndex, ChatId, CommunityId, Document, MessageContent, MessageContentInitial, MessageId,
MessageIndex, TimestampMillis, UniquePersonProof, User, UserId,
CanisterId, ChannelLatestMessageIndex, ChatId, CommunityId, MessageContent, MessageContentInitial, MessageId, MessageIndex,
TimestampMillis, UniquePersonProof, User, UserId,
};

mod lifecycle;
Expand Down Expand Up @@ -79,8 +79,10 @@ pub struct UserDeleted {

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExternalAchievementInitial {
pub id: u32,
pub name: String,
pub logo: Document,
pub logo: String,
pub url: String,
pub canister_id: CanisterId,
pub chit_reward: u32,
pub expires: TimestampMillis,
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user_index/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fn main() {
generate_candid_method!(user_index, award_external_achievement, update);
generate_candid_method!(user_index, mark_suspected_bot, update);
generate_candid_method!(user_index, pay_for_diamond_membership, update);
generate_candid_method!(user_index, register_external_achievement, update);
generate_candid_method!(user_index, remove_platform_moderator, update);
generate_candid_method!(user_index, remove_platform_operator, update);
generate_candid_method!(user_index, set_diamond_membership_fees, update);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ pub struct SuccessResult {
#[ts_export(user_index, external_achievements)]
#[derive(CandidType, Debug)]
pub struct ExternalAchievement {
pub id: u32,
pub name: String,
pub logo_id: u128,
pub url: String,
pub chit_reward: u32,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use types::UserId;

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
pub achievement_id: u32,
pub user_id: UserId,
pub name: String,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user_index/api/src/updates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod mark_local_user_index_full;
pub mod mark_suspected_bot;
pub mod modclub_callback;
pub mod pay_for_diamond_membership;
pub mod register_external_achievement;
pub mod remove_platform_moderator;
pub mod remove_platform_operator;
pub mod remove_sms_messages;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use candid::CandidType;
use human_readable::{HumanReadablePrincipal, ToHumanReadable};
use serde::Serialize;
use ts_export::ts_export;
use types::{CanisterId, TimestampMillis};

#[ts_export(user_index, pay_for_diamond_membership)]
#[derive(CandidType, Debug)]
pub struct Args {
pub id: u32,
pub name: String,
pub logo: String,
pub url: String,
pub canister_id: CanisterId,
pub chit_reward: u32,
pub expires: TimestampMillis,
pub chit_budget: u32,
}

#[ts_export(user_index, pay_for_diamond_membership)]
#[derive(CandidType, Debug)]
pub enum Response {
Success,
}

#[derive(Serialize)]
pub struct HumanReadableArgs {
id: u32,
name: String,
logo: String,
url: String,
canister_id: HumanReadablePrincipal,
chit_reward: u32,
expires: TimestampMillis,
chit_budget: u32,
}

impl ToHumanReadable for Args {
type Target = HumanReadableArgs;

fn to_human_readable(&self) -> Self::Target {
HumanReadableArgs {
id: self.id,
name: self.name.clone(),
logo: self.logo.clone(),
url: self.url.clone(),
canister_id: self.canister_id.into(),
chit_reward: self.chit_reward,
expires: self.expires,
chit_budget: self.chit_budget,
}
}
}
1 change: 1 addition & 0 deletions backend/canisters/user_index/impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ canister_tracing_macros = { path = "../../../libraries/canister_tracing_macros"
chat_events = { path = "../../../libraries/chat_events" }
community_canister = { path = "../../community/api" }
community_canister_c2c_client = { path = "../../community/c2c_client" }
dataurl = { workspace = true }
event_store_producer = { workspace = true, features = ["json"] }
event_store_producer_cdk_runtime = { workspace = true }
futures = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use std::collections::HashSet;

use serde::{Deserialize, Serialize};
use types::{CanisterId, Document, TimestampMillis, UserId};
use std::collections::{HashMap, HashSet};
use types::{CanisterId, TimestampMillis, UserId};
use user_index_canister::ExternalAchievementInitial;

#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct ExternalAchievements {
achievements: Vec<ExternalAchievementInternal>,
achievements: HashMap<u32, ExternalAchievementInternal>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExternalAchievementInternal {
pub name: String,
pub logo: Document,
pub logo: String,
pub url: String,
pub canister_id: CanisterId,
pub chit_reward: u32,
pub registered: TimestampMillis,
Expand All @@ -24,36 +24,39 @@ pub struct ExternalAchievementInternal {
}

impl ExternalAchievements {
#[allow(dead_code)]
pub fn register(&mut self, achievement: ExternalAchievementInitial, now: TimestampMillis) -> bool {
if self.achievements.iter().any(|a| a.name == achievement.name) {
if self
.achievements
.iter()
.any(|(id, a)| a.name == achievement.name || *id == achievement.id)
{
return false;
}

self.achievements.push(ExternalAchievementInternal {
name: achievement.name,
logo: achievement.logo,
canister_id: achievement.canister_id,
chit_reward: achievement.chit_reward,
registered: now,
expires: achievement.expires,
initial_chit_budget: achievement.chit_budget,
remaining_chit_budget: achievement.chit_budget,
budget_exhausted: None,
awarded: HashSet::new(),
});
self.achievements.insert(
achievement.id,
ExternalAchievementInternal {
name: achievement.name,
logo: achievement.logo,
url: achievement.url,
canister_id: achievement.canister_id,
chit_reward: achievement.chit_reward,
registered: now,
expires: achievement.expires,
initial_chit_budget: achievement.chit_budget,
remaining_chit_budget: achievement.chit_budget,
budget_exhausted: None,
awarded: HashSet::new(),
},
);

// TODO: Create a timer to delete the awarded users HashSet once the achievement has expired

true
}

pub fn iter(&self) -> impl Iterator<Item = &ExternalAchievementInternal> {
self.achievements.iter()
}

pub fn award(&mut self, user_id: UserId, name: &str, caller: CanisterId, now: TimestampMillis) -> AwardResult {
let Some(achievement) = self.achievements.iter_mut().find(|a| a.name == name) else {
pub fn award(&mut self, id: u32, user_id: UserId, caller: CanisterId, now: TimestampMillis) -> AwardResult {
let Some(achievement) = self.achievements.get_mut(&id) else {
return AwardResult::NotFound;
};

Expand All @@ -80,17 +83,28 @@ impl ExternalAchievements {
}

AwardResult::Success(AwardSuccessResult {
name: achievement.name.clone(),
chit_reward: achievement.chit_reward,
remaining_chit_budget: achievement.remaining_chit_budget,
})
}

pub fn get(&self, id: u32) -> Option<&ExternalAchievementInternal> {
self.achievements.get(&id)
}

pub fn iter(&self) -> impl Iterator<Item = (&u32, &ExternalAchievementInternal)> {
self.achievements.iter()
}

pub fn metrics(&self) -> Vec<ExternalAchievementMetrics> {
self.achievements
.iter()
.map(|a| ExternalAchievementMetrics {
.map(|(id, a)| ExternalAchievementMetrics {
id: *id,
name: a.name.clone(),
logo_id: a.logo.id,
logo_len: a.logo.len(),
url: a.url.clone(),
canister_id: a.canister_id,
chit_reward: a.chit_reward,
registered: a.registered,
Expand All @@ -114,14 +128,17 @@ pub enum AwardResult {
}

pub struct AwardSuccessResult {
pub name: String,
pub chit_reward: u32,
pub remaining_chit_budget: u32,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExternalAchievementMetrics {
pub id: u32,
pub name: String,
pub logo_id: u128,
pub logo_len: usize,
pub url: String,
pub canister_id: CanisterId,
pub chit_reward: u32,
pub registered: TimestampMillis,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn external_achievements_impl(args: Args, state: &RuntimeState) -> Response {
let mut achievements_removed = Vec::new();
let mut latest_update: TimestampMillis = 0;

for achievement in state.data.external_achievements.iter() {
for (id, achievement) in state.data.external_achievements.iter() {
let add = achievement.registered > args.updates_since;
let remove = achievement.expires > args.updates_since
|| achievement.budget_exhausted.map_or(false, |ts| ts > args.updates_since);
Expand All @@ -29,8 +29,9 @@ fn external_achievements_impl(args: Args, state: &RuntimeState) -> Response {

if add ^ remove {
let a = ExternalAchievement {
id: *id,
name: achievement.name.clone(),
logo_id: achievement.logo.id,
url: achievement.url.clone(),
chit_reward: achievement.chit_reward,
};

Expand Down
35 changes: 23 additions & 12 deletions backend/canisters/user_index/impl/src/queries/http_request.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::{read_state, RuntimeState};
use candid::Principal;
use http_request::{build_json_response, encode_logs, extract_route, get_document, Route};
use dataurl::DataUrl;
use http_request::{build_json_response, encode_logs, extract_route, Route};
use ic_cdk::query;
use std::collections::BTreeMap;
use types::{HttpRequest, HttpResponse, TimestampMillis, UserId};
use types::{HeaderField, HttpRequest, HttpResponse, TimestampMillis, UserId};
use utils::time::MonthKey;

#[query]
Expand Down Expand Up @@ -64,17 +65,27 @@ fn http_request(request: HttpRequest) -> HttpResponse {
return build_json_response(&state.data.chit_bands(size, month_key.year(), month_key.month()));
}
"achievement_logo" => {
let logo_id = parts.get(1).and_then(|s| (*s).parse::<u128>().ok());
let document = logo_id.and_then(|id| {
state
.data
.external_achievements
.iter()
.find(|a| a.logo.id == id)
.map(|a| &a.logo)
});
let id = parts.get(1).and_then(|s| (*s).parse::<u32>().ok());
let Some(logo) =
id.and_then(|achievement_id| state.data.external_achievements.get(achievement_id).map(|a| a.logo.clone()))
else {
return HttpResponse::not_found();
};

return get_document(logo_id, document, "achievement_logo");
let url = DataUrl::parse(&logo).unwrap();

return HttpResponse {
status_code: 200,
headers: vec![
HeaderField("Content-Type".to_string(), url.get_media_type().to_string()),
HeaderField(
"Cache-Control".to_string(),
"public, max-age=100000000, immutable".to_string(),
),
],
body: url.get_data().to_vec(),
streaming_strategy: None,
};
}
_ => (),
}
Expand Down
Loading

0 comments on commit 55d1849

Please sign in to comment.