From dcdb3fdd20377b6d310a91cf3d933c4946ea0dd9 Mon Sep 17 00:00:00 2001 From: Iris Date: Fri, 18 Aug 2023 18:00:10 +0200 Subject: [PATCH 1/2] feat: achievements features --- src/endpoints/achievements/fetch.rs | 115 +++++++++++++++++++ src/endpoints/achievements/mod.rs | 2 + src/endpoints/achievements/verify_default.rs | 49 ++++++++ src/endpoints/mod.rs | 7 +- src/main.rs | 8 ++ src/models.rs | 49 ++++++++ src/utils.rs | 52 ++++++++- 7 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 src/endpoints/achievements/fetch.rs create mode 100644 src/endpoints/achievements/mod.rs create mode 100644 src/endpoints/achievements/verify_default.rs diff --git a/src/endpoints/achievements/fetch.rs b/src/endpoints/achievements/fetch.rs new file mode 100644 index 00000000..d57f2d89 --- /dev/null +++ b/src/endpoints/achievements/fetch.rs @@ -0,0 +1,115 @@ +use std::sync::Arc; + +use crate::{ + models::{AchievementCategoryDocument, AchievementQuery, AppState, UserAchievements}, + utils::get_error, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use futures::stream::StreamExt; +use mongodb::bson::{doc, from_document}; +use starknet::core::types::FieldElement; + +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + if query.addr == FieldElement::ZERO { + return get_error("Please connect your wallet first".to_string()); + } + let addr = FieldElement::to_string(&query.addr); + let achievement_categories = state + .db + .collection::("achievement_categories"); + let pipeline = vec![ + doc! { + "$lookup": { + "from": "achievements", + "localField": "id", + "foreignField": "category_id", + "as": "achievement" + } + }, + doc! {"$unwind": "$achievement" }, + doc! { + "$lookup": { + "from": "achieved", + "let": { "achievement_id": "$achievement.id" }, + "pipeline": [ + { "$match": { + "$expr": { + "$and": [ + { "$eq": ["$achievement_id", "$$achievement_id"] }, + { "$eq": ["$addr", addr] } + ] + } + } } + ], + "as": "achieved" + } + }, + doc! { + "$project": { + "_id": 0, + "category_name": "$name", + "category_desc": "$desc", + "achievements": { + "name": "$achievement.name", + "short_desc": "$achievement.short_desc", + "title": { + "$cond": [ + { "$eq": [{ "$size": "$achieved" }, 0] }, + "$achievement.todo_title", + "$achievement.done_title" + ] + }, + "desc": { + "$cond": [ + { "$eq": [{ "$size": "$achieved" }, 0] }, + "$achievement.todo_desc", + "$achievement.done_desc" + ] + }, + "completed": { "$ne": [{ "$size": "$achieved" }, 0] }, + "verify_type": "$achievement.verify_type" + } + } + }, + doc! { + "$group": { + "_id": { "category_name": "$category_name", "category_desc": "$category_desc" }, + "achievements": { "$push": "$achievements" } + } + }, + doc! { + "$project": { + "category_name": "$_id.category_name", + "category_desc": "$_id.category_desc", + "achievements": 1, + "_id": 0 + } + }, + ]; + + match achievement_categories.aggregate(pipeline, None).await { + Ok(mut cursor) => { + let mut achievements: Vec = Vec::new(); + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + if let Ok(achievement) = from_document::(document) { + achievements.push(achievement); + } + } + _ => continue, + } + } + (StatusCode::OK, Json(achievements)).into_response() + } + Err(e) => get_error(format!("Error fetching user achievements: {}", e)), + } +} diff --git a/src/endpoints/achievements/mod.rs b/src/endpoints/achievements/mod.rs new file mode 100644 index 00000000..9b152b7f --- /dev/null +++ b/src/endpoints/achievements/mod.rs @@ -0,0 +1,2 @@ +pub mod fetch; +pub mod verify_default; diff --git a/src/endpoints/achievements/verify_default.rs b/src/endpoints/achievements/verify_default.rs new file mode 100644 index 00000000..cd62c6d8 --- /dev/null +++ b/src/endpoints/achievements/verify_default.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use crate::{ + models::{AchievedDocument, AppState, VerifyAchievementQuery}, + utils::{get_error, AchievementsTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use mongodb::bson::doc; +use serde_json::json; +use starknet::core::types::FieldElement; + +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let addr = query.addr; + if addr == FieldElement::ZERO { + return get_error("Please connect your wallet first".to_string()); + } + let achievement_id = query.id; + let achieved_collection = state.db.collection::("achieved"); + let filter = doc! { + "addr": FieldElement::to_string(&addr), + "achievement_id": achievement_id + }; + match achieved_collection.find_one(filter, None).await { + Ok(Some(_)) => (StatusCode::OK, Json(json!({"achieved": true}))).into_response(), + Ok(None) => match state.get_achievement(achievement_id).await { + Ok(Some(achievement)) => { + // todo: add verifying logic here + match state + .upsert_completed_achievement(addr, achievement_id) + .await + { + Ok(_) => (StatusCode::OK, Json(json!({"achieved": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + } + } + Ok(None) => get_error("Achievement not found".to_string()), + Err(e) => get_error(format!("Error querying achievement : {}", e)), + }, + Err(e) => get_error(format!("Error querying user achievement : {}", e)), + } +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 5be7088e..5325e4a1 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -1,5 +1,6 @@ -pub mod quests; -pub mod get_quiz; +pub mod achievements; pub mod get_quest; pub mod get_quests; -pub mod get_tasks; \ No newline at end of file +pub mod get_quiz; +pub mod get_tasks; +pub mod quests; diff --git a/src/main.rs b/src/main.rs index 58a5941b..175061aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -159,6 +159,14 @@ async fn main() { "/quests/example/verify_quiz", post(endpoints::quests::example::verify_quiz::handler), ) + .route( + "/achievements/verify_default", + get(endpoints::achievements::verify_default::handler), + ) + .route( + "/achievements/fetch", + get(endpoints::achievements::fetch::handler), + ) .with_state(shared_state) .layer(cors); diff --git a/src/models.rs b/src/models.rs index 63b55e88..478f5532 100644 --- a/src/models.rs +++ b/src/models.rs @@ -65,3 +65,52 @@ pub_struct!(Deserialize; VerifyQuizQuery { quiz_name: String, user_answers_list: Vec>, }); + +pub_struct!(Deserialize; AchievementQuery { + addr: FieldElement, +}); + +pub_struct!(Deserialize; VerifyAchievementQuery { + addr: FieldElement, + id: u32, +}); + +pub_struct!(Debug, Serialize, Deserialize; AchievedDocument { + addr: String, + achievement_id: u32, +}); + +pub_struct!(Debug, Serialize, Deserialize; AchievementDocument { + id: u32, + category_id: u32, + name: String, + img_url: String, + short_desc: String, + todo_title: String, + todo_desc: String, + done_title: String, + done_desc: String, + verify_type: String, + verify_endpoint: String, +}); + +pub_struct!(Debug, Serialize, Deserialize; AchievementCategoryDocument { + id: u32, + name: String, + desc: String, +}); + +pub_struct!(Debug, Serialize, Deserialize; UserAchievements { + category_name: String, + category_desc: String, + achievements: Vec, +}); + +pub_struct!(Debug, Serialize, Deserialize; UserAchievement { + name: String, + short_desc: String, + title: String, + desc: String, + completed: bool, + verify_type: String, +}); diff --git a/src/utils.rs b/src/utils.rs index 84e1c0d2..18969716 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,7 @@ -use crate::models::{AppState, CompletedTasks}; +use crate::{ + endpoints::achievements, + models::{AchievementDocument, AppState, CompletedTasks}, +}; use async_trait::async_trait; use axum::{ http::StatusCode, @@ -90,3 +93,50 @@ pub fn to_hex(felt: FieldElement) -> String { } result } + +#[async_trait] +pub trait AchievementsTrait { + async fn upsert_completed_achievement( + &self, + addr: FieldElement, + achievement_id: u32, + ) -> Result; + + async fn get_achievement( + &self, + achievement_id: u32, + ) -> Result, mongodb::error::Error>; +} + +#[async_trait] +impl AchievementsTrait for AppState { + async fn upsert_completed_achievement( + &self, + addr: FieldElement, + achievement_id: u32, + ) -> Result { + let achieved_collection: Collection = self.db.collection("achieved"); + let filter = doc! { "addr": addr.to_string(), "achievement_id": achievement_id }; + let update = + doc! { "$setOnInsert": { "addr": addr.to_string(), "achievement_id": achievement_id } }; + let options = UpdateOptions::builder().upsert(true).build(); + + let result = achieved_collection + .update_one(filter, update, options) + .await; + result + } + + async fn get_achievement( + &self, + achievement_id: u32, + ) -> Result, mongodb::error::Error> { + let achievements_collection: Collection = + self.db.collection("achievements"); + let query = doc! { + "id": achievement_id + }; + let result = achievements_collection.find_one(query, None).await; + result + } +} From fc031c994155527398e472a13faa1e6dcca9c6e8 Mon Sep 17 00:00:00 2001 From: Iris Date: Fri, 18 Aug 2023 18:34:27 +0200 Subject: [PATCH 2/2] fix: allow address 0 in fetch --- src/endpoints/achievements/fetch.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/endpoints/achievements/fetch.rs b/src/endpoints/achievements/fetch.rs index d57f2d89..31c7b1f6 100644 --- a/src/endpoints/achievements/fetch.rs +++ b/src/endpoints/achievements/fetch.rs @@ -18,9 +18,6 @@ pub async fn handler( State(state): State>, Query(query): Query, ) -> impl IntoResponse { - if query.addr == FieldElement::ZERO { - return get_error("Please connect your wallet first".to_string()); - } let addr = FieldElement::to_string(&query.addr); let achievement_categories = state .db