diff --git a/src/endpoints/analytics/get_quest_activity.rs b/src/endpoints/analytics/get_quest_activity.rs new file mode 100644 index 00000000..c04e0a5d --- /dev/null +++ b/src/endpoints/analytics/get_quest_activity.rs @@ -0,0 +1,97 @@ +use crate::models::{QuestTaskDocument}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use mongodb::bson::doc; +use std::sync::Arc; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct GetQuestsQuery { + id: u32, +} + + +#[route(get, "/analytics/get_quest_activity", crate::endpoints::analytics::get_quest_activity)] +pub async fn handler(State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let quest_id = query.id; + let day_wise_distribution = vec![ + doc! { + "$match": doc! { + "quest_id": quest_id + } + }, + doc! { + "$group": doc! { + "_id": null, + "ids": doc! { + "$push": "$id" + } + } + }, + doc! { + "$lookup": doc! { + "from": "completed_tasks", + "localField": "ids", + "foreignField": "task_id", + "as": "matching_documents" + } + }, + doc! { + "$unwind": "$matching_documents" + }, + doc! { + "$replaceRoot": doc! { + "newRoot": "$matching_documents" + } + }, + doc! { + "$addFields": doc! { + "createdDate": doc! { + "$toDate": "$timestamp" + } + } + }, + doc! { + "$group": doc! { + "_id": doc! { + "$dateToString": doc! { + "format": "%Y-%m-%d %d", + "date": "$createdDate" + } + }, + "participants": doc! { + "$sum": 1 + } + } + }, + doc! { + "$sort": doc! { + "_id": 1 + } + }, + ]; + + match state.db.collection::("tasks").aggregate(day_wise_distribution, None).await { + Ok(mut cursor) => { + let mut day_wise_distribution = Vec::new(); + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + day_wise_distribution.push(document); + } + _ => continue, + } + } + return (StatusCode::OK, Json(day_wise_distribution)).into_response(); + } + Err(_) => get_error("Error querying quest".to_string()), + } +} diff --git a/src/endpoints/analytics/get_quest_participation.rs b/src/endpoints/analytics/get_quest_participation.rs new file mode 100644 index 00000000..825e8a4b --- /dev/null +++ b/src/endpoints/analytics/get_quest_participation.rs @@ -0,0 +1,138 @@ +use crate::models::QuestTaskDocument; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use mongodb::bson::doc; +use serde::Deserialize; +use std::sync::Arc; + +#[derive(Deserialize)] +pub struct GetQuestsQuery { + id: u32, +} + +#[route( +get, +"/analytics/get_quest_participation", +crate::endpoints::analytics::get_quest_participation +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let quest_id = query.id; + let day_wise_distribution = vec![ + doc! { + "$match": doc! { + "quest_id": quest_id + } + }, + doc! { + "$group": doc! { + "_id": null, + "ids": doc! { + "$push": "$id" + }, + "otherDetails": doc! { + "$push": "$$ROOT" + } + } + }, + doc! { + "$lookup": doc! { + "from": "completed_tasks", + "localField": "ids", + "foreignField": "task_id", + "as": "matching_documents" + } + }, + doc! { + "$unwind": "$matching_documents" + }, + doc! { + "$group": doc! { + "_id": "$matching_documents.task_id", + "count": doc! { + "$sum": 1 + }, + "details": doc! { + "$first": "$otherDetails" + } + } + }, + doc! { + "$project": doc! { + "_id": 1, + "count": 1, + "otherDetails": doc! { + "$filter": doc! { + "input": "$details", + "as": "detail", + "cond": doc! { + "$eq": [ + "$$detail.id", + "$_id" + ] + } + } + } + } + }, + doc! { + "$unwind": "$otherDetails" + }, + doc! { + "$replaceRoot": doc! { + "newRoot": doc! { + "$mergeObjects": [ + "$matching_documents", + "$otherDetails", + doc! { + "participants": "$count" + } + ] + } + } + }, + doc! { + "$project": doc! { + "otherDetails": 0, + "_id":0, + "verify_endpoint": 0, + "verify_endpoint_type": 0, + "verify_redirect":0, + "href": 0, + "cta": 0, + "id": 0, + "quest_id": 0, + + } + }, + ]; + + match state + .db + .collection::("tasks") + .aggregate(day_wise_distribution, None) + .await + { + Ok(mut cursor) => { + let mut task_activity = Vec::new(); + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + task_activity.push(document); + } + _ => continue, + } + } + return (StatusCode::OK, Json(task_activity)).into_response(); + } + Err(_) => get_error("Error querying tasks".to_string()), + } +} diff --git a/src/endpoints/analytics/get_unique_visitors.rs b/src/endpoints/analytics/get_unique_visitors.rs new file mode 100644 index 00000000..9c508c3d --- /dev/null +++ b/src/endpoints/analytics/get_unique_visitors.rs @@ -0,0 +1,60 @@ +use crate::models::QuestTaskDocument; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::TryStreamExt; +use mongodb::bson::doc; +use serde::Deserialize; +use std::sync::Arc; + +#[derive(Deserialize)] +pub struct GetQuestsQuery { + id: u32, +} + +#[route( +get, +"/analytics/get_unique_visitors", +crate::endpoints::analytics::get_unique_visitors +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let quest_id = query.id; + let page_id = "quest_".to_owned() + quest_id.to_string().as_str(); + let total_viewers_pipeline = vec![ + doc! { + "$match": doc! { + "viewed_page_id": page_id + } + }, + doc! { + "$count":"total_viewers" + }, + ]; + + match state + .db + .collection::("unique_viewers") + .aggregate(total_viewers_pipeline, None) + .await + { + Ok(mut cursor) => { + let mut result = 0; + return match cursor.try_next().await { + Ok(Some(doc)) => { + result = doc.get("total_viewers").unwrap().as_i32().unwrap(); + (StatusCode::OK, Json(result)).into_response() + } + Ok(None) => (StatusCode::OK, Json(result)).into_response(), + Err(_) => get_error("Error querying quest".to_string()), + }; + } + Err(_) => get_error("Error querying quest".to_string()), + } +} diff --git a/src/endpoints/analytics/mod.rs b/src/endpoints/analytics/mod.rs new file mode 100644 index 00000000..e809e03e --- /dev/null +++ b/src/endpoints/analytics/mod.rs @@ -0,0 +1,3 @@ +pub mod get_quest_activity; +pub mod get_quest_participation; +pub mod get_unique_visitors; \ No newline at end of file diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 187e827d..a349eb6c 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -13,3 +13,4 @@ pub mod leaderboard; pub mod quest_boost; pub mod quests; pub mod get_boosted_quests; +pub mod analytics; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ac2ac576..9aac0b64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,6 +64,7 @@ async fn main() { acc.merge(r.to_router(shared_state.clone())) }) .layer(cors); + let addr = SocketAddr::from(([0, 0, 0, 0], conf.server.port)); println!("server: listening on http://0.0.0.0:{}", conf.server.port); axum::Server::bind(&addr) diff --git a/src/models.rs b/src/models.rs index 652d8852..c171505a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -46,12 +46,29 @@ pub_struct!(Debug, Serialize, Deserialize; QuestDocument { pub_struct!(Deserialize; CompletedTasks { address: String, task_id: u32, + timestamp: i64, }); #[derive(Debug, Serialize, Deserialize)] pub struct CompletedTaskDocument { address: String, task_id: u32, + timestamp: i64, +} + + +#[derive(Debug, Serialize, Deserialize)] +pub struct QuestTaskDocument { + id: u32, + quest_id: u32, + name: String, + desc: String, + cta: String, + verify_endpoint: String, + verify_endpoint_type: String, + verify_redirect: Option, + href: String, + quiz_name: Option, } pub_struct!(Serialize; Reward { @@ -80,6 +97,12 @@ pub_struct!(Deserialize; VerifyQuizQuery { user_answers_list: Vec>, }); +pub_struct!(Deserialize; UniquePageVisit { + viewer_ip: String, + viewed_page_id: String, + timestamp: i64, +}); + pub_struct!(Deserialize; AchievementQuery { addr: FieldElement, }); @@ -92,6 +115,7 @@ pub_struct!(Deserialize; VerifyAchievementQuery { pub_struct!(Debug, Serialize, Deserialize; AchievedDocument { addr: String, achievement_id: u32, + timestamp: i64, }); pub_struct!(Debug, Serialize, Deserialize; AchievementDocument { diff --git a/src/utils.rs b/src/utils.rs index 059cea95..54f6bf69 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -125,8 +125,10 @@ impl CompletedTasksTrait for AppState { ) -> Result { let completed_tasks_collection: Collection = self.db.collection("completed_tasks"); + let created_at = Utc::now().timestamp_millis(); let filter = doc! { "address": addr.to_string(), "task_id": task_id }; - let update = doc! { "$setOnInsert": { "address": addr.to_string(), "task_id": task_id } }; + let update = doc! { "$setOnInsert": { "address": addr.to_string(), "task_id": task_id , "timestamp":created_at} }; + let options = UpdateOptions::builder().upsert(true).build(); let result = completed_tasks_collection @@ -240,7 +242,7 @@ impl CompletedTasksTrait for AppState { experience.into(), timestamp, ) - .await; + .await; } Err(_e) => { get_error("Error querying quests".to_string()); @@ -292,9 +294,10 @@ impl AchievementsTrait for AppState { achievement_id: u32, ) -> Result { let achieved_collection: Collection = self.db.collection("achieved"); + let created_at = Utc::now().timestamp_millis(); let filter = doc! { "addr": addr.to_string(), "achievement_id": achievement_id }; let update = - doc! { "$setOnInsert": { "addr": addr.to_string(), "achievement_id": achievement_id } }; + doc! { "$setOnInsert": { "addr": addr.to_string(), "achievement_id": achievement_id , "timestamp":created_at } }; let options = UpdateOptions::builder().upsert(true).build(); let result = achieved_collection @@ -329,7 +332,7 @@ impl AchievementsTrait for AppState { experience.into(), timestamp, ) - .await; + .await; } None => {} }