diff --git a/src/config.rs b/src/config.rs index 953fb3ee..2e336787 100644 --- a/src/config.rs +++ b/src/config.rs @@ -63,6 +63,7 @@ pub_struct!(Clone, Deserialize; Quests { braavos: Braavos, element: Element, nostra: Pairs, + focustree: Api , }); pub_struct!(Clone, Deserialize; Twitter { @@ -122,6 +123,10 @@ pub_struct!(Clone, Deserialize; Starkscan { api_key: String, }); +pub_struct!(Clone, Deserialize; Api { + api_endpoint: String, +}); + pub_struct!(Clone, Deserialize; Achievement { contract: FieldElement, }); diff --git a/src/endpoints/achievements/verify_briq.rs b/src/endpoints/achievements/verify_briq.rs index 21784dcc..7cbbe57a 100644 --- a/src/endpoints/achievements/verify_briq.rs +++ b/src/endpoints/achievements/verify_briq.rs @@ -13,6 +13,7 @@ use axum::{ use mongodb::bson::doc; use serde_json::json; use starknet::core::types::FieldElement; +use crate::utils::fetch_json_from_url; pub async fn handler( State(state): State>, @@ -46,7 +47,7 @@ pub async fn handler( Ok(response) => { if let Some(sets) = response.get("sets") { match sets { - serde_json::Value::Array(sets_array) => { + serde_json::Value::Array(sets_array ) => { for set in sets_array.iter() { if let serde_json::Value::String(set_str) = set { let url = format!( @@ -104,17 +105,6 @@ pub async fn handler( } } -pub async fn fetch_json_from_url(url: String) -> Result { - let client = reqwest::Client::new(); - match client.get(url).send().await { - Ok(response) => match response.json::().await { - Ok(json) => Ok(json), - Err(e) => Err(format!("Failed to get JSON response: {}", e)), - }, - Err(e) => Err(format!("Failed to send request: {}", e)), - } -} - pub async fn check_for_ducks(properties: &serde_json::Value) -> bool { if let Some(serde_json::Value::Array(value_arr)) = properties.get("collections").and_then(|c| c.get("value")) diff --git a/src/endpoints/quests/element/briq/verify_own_briq.rs b/src/endpoints/quests/element/briq/verify_own_briq.rs index 456510fe..72df4be8 100644 --- a/src/endpoints/quests/element/briq/verify_own_briq.rs +++ b/src/endpoints/quests/element/briq/verify_own_briq.rs @@ -12,6 +12,7 @@ use axum::{ }; use serde_json::json; use starknet::core::types::FieldElement; +use crate::utils::fetch_json_from_url; pub async fn handler( State(state): State>, @@ -74,14 +75,3 @@ pub async fn handler( Err(e) => get_error(e), } } - -pub async fn fetch_json_from_url(url: String) -> Result { - let client = reqwest::Client::new(); - match client.get(url).send().await { - Ok(response) => match response.json::().await { - Ok(json) => Ok(json), - Err(e) => Err(format!("Failed to get JSON response: {}", e)), - }, - Err(e) => Err(format!("Failed to send request: {}", e)), - } -} diff --git a/src/endpoints/quests/focustree/engagement/discord_fw_callback.rs b/src/endpoints/quests/focustree/engagement/discord_fw_callback.rs new file mode 100644 index 00000000..a977d4a3 --- /dev/null +++ b/src/endpoints/quests/focustree/engagement/discord_fw_callback.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use crate::utils::CompletedTasksTrait; +use crate::{ + models::AppState, + utils::{get_error_redirect, success_redirect}, +}; +use axum::{ + extract::{Query, State}, + response::IntoResponse, +}; +use mongodb::bson::doc; +use reqwest::header::AUTHORIZATION; +use serde::Deserialize; +use starknet::core::types::FieldElement; + +#[derive(Deserialize)] +pub struct TwitterOAuthCallbackQuery { + code: String, + state: FieldElement, +} + +#[derive(Deserialize)] +pub struct Guild { + id: String, + #[allow(dead_code)] + name: String, +} + +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let quest_id = 21; + let task_id = 87; + let guild_id = "986385497888792598"; + let authorization_code = &query.code; + let error_redirect_uri = format!( + "{}/quest/{}?task_id={}&res=false", + state.conf.variables.app_link, quest_id, task_id + ); + + // Exchange the authorization code for an access token + let params = [ + ("client_id", &state.conf.discord.oauth2_clientid), + ("client_secret", &state.conf.discord.oauth2_secret), + ("code", &authorization_code.to_string()), + ( + "redirect_uri", + &format!( + "{}/quests/focustree/discord_fw_callback", + state.conf.variables.api_link + ), + ), + ("grant_type", &"authorization_code".to_string()), + ]; + let access_token = match exchange_authorization_code(params).await { + Ok(token) => token, + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!("Failed to exchange authorization code: {}", e), + ); + } + }; + + // Get user guild information + let client = reqwest::Client::new(); + let response_result = client + .get("https://discord.com/api/users/@me/guilds") + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .send() + .await; + let response: Vec = match response_result { + Ok(response) => { + let json_result = response.json().await; + match json_result { + Ok(json) => json, + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!( + "Failed to get JSON response while fetching user info: {}", + e + ), + ); + } + } + } + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!("Failed to send request to get user info: {}", e), + ); + } + }; + + for guild in response { + if guild.id == guild_id { + match state.upsert_completed_task(query.state, task_id).await { + Ok(_) => { + let redirect_uri = format!( + "{}/quest/{}?task_id={}&res=true", + state.conf.variables.app_link, quest_id, task_id + ); + return success_redirect(redirect_uri); + } + Err(e) => return get_error_redirect(error_redirect_uri, format!("{}", e)), + } + } + } + + get_error_redirect( + error_redirect_uri, + "You're not part of AVNU's Discord server".to_string(), + ) +} + +async fn exchange_authorization_code( + params: [(&str, &String); 5], +) -> Result> { + let client = reqwest::Client::new(); + let res = client + .post("https://discord.com/api/oauth2/token") + .form(¶ms) + .send() + .await?; + let json: serde_json::Value = res.json().await?; + match json["access_token"].as_str() { + Some(s) => Ok(s.to_string()), + None => { + println!( + "Failed to get 'access_token' from JSON response : {:?}", + json + ); + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to get 'access_token' from JSON response : {:?}", + json + ), + ))) + } + } +} diff --git a/src/endpoints/quests/focustree/engagement/mod.rs b/src/endpoints/quests/focustree/engagement/mod.rs new file mode 100644 index 00000000..620ad0a1 --- /dev/null +++ b/src/endpoints/quests/focustree/engagement/mod.rs @@ -0,0 +1,2 @@ +pub mod discord_fw_callback; +pub mod verify_twitter_rt; \ No newline at end of file diff --git a/src/endpoints/quests/focustree/engagement/verify_twitter_rt.rs b/src/endpoints/quests/focustree/engagement/verify_twitter_rt.rs new file mode 100644 index 00000000..b478eb77 --- /dev/null +++ b/src/endpoints/quests/focustree/engagement/verify_twitter_rt.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use serde_json::json; + +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let task_id = 88; + match state.upsert_completed_task(query.addr, task_id).await { + Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + } +} diff --git a/src/endpoints/quests/focustree/claimable.rs b/src/endpoints/quests/focustree/introduction/claimable.rs similarity index 100% rename from src/endpoints/quests/focustree/claimable.rs rename to src/endpoints/quests/focustree/introduction/claimable.rs diff --git a/src/endpoints/quests/focustree/introduction/mod.rs b/src/endpoints/quests/focustree/introduction/mod.rs new file mode 100644 index 00000000..833b55d5 --- /dev/null +++ b/src/endpoints/quests/focustree/introduction/mod.rs @@ -0,0 +1,3 @@ +pub mod claimable; +pub mod verify_twitter_fw; +pub mod verify_twitter_rt; \ No newline at end of file diff --git a/src/endpoints/quests/focustree/verify_twitter_fw.rs b/src/endpoints/quests/focustree/introduction/verify_twitter_fw.rs similarity index 100% rename from src/endpoints/quests/focustree/verify_twitter_fw.rs rename to src/endpoints/quests/focustree/introduction/verify_twitter_fw.rs diff --git a/src/endpoints/quests/focustree/verify_twitter_rt.rs b/src/endpoints/quests/focustree/introduction/verify_twitter_rt.rs similarity index 100% rename from src/endpoints/quests/focustree/verify_twitter_rt.rs rename to src/endpoints/quests/focustree/introduction/verify_twitter_rt.rs diff --git a/src/endpoints/quests/focustree/mod.rs b/src/endpoints/quests/focustree/mod.rs index 4861397e..1cc5ce78 100644 --- a/src/endpoints/quests/focustree/mod.rs +++ b/src/endpoints/quests/focustree/mod.rs @@ -1,3 +1,2 @@ -pub mod claimable; -pub mod verify_twitter_fw; -pub mod verify_twitter_rt; +pub mod introduction; +pub mod engagement; diff --git a/src/main.rs b/src/main.rs index cb3dde7f..9a7181f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -319,15 +319,23 @@ async fn main() { ) .route( "/quests/focustree/verify_twitter_fw", - get(endpoints::quests::focustree::verify_twitter_fw::handler), + get(endpoints::quests::focustree::introduction::verify_twitter_fw::handler), ) .route( "/quests/focustree/verify_twitter_rt", - get(endpoints::quests::focustree::verify_twitter_rt::handler), + get(endpoints::quests::focustree::introduction::verify_twitter_rt::handler), ) .route( "/quests/focustree/claimable", - get(endpoints::quests::focustree::claimable::handler), + get(endpoints::quests::focustree::introduction::claimable::handler), + ) + .route( + "/quests/focustree/discord_fw_callback", + get(endpoints::quests::focustree::engagement::discord_fw_callback::handler), + ) + .route( + "/quests/focustree/verify_twitter_rw_user", + get(endpoints::quests::focustree::engagement::verify_twitter_rt::handler), ) .route( "/quests/element/element/verify_is_eligible", diff --git a/src/models.rs b/src/models.rs index 57c2b736..6626e1d9 100644 --- a/src/models.rs +++ b/src/models.rs @@ -29,6 +29,7 @@ pub_struct!(Debug, Serialize, Deserialize; QuestDocument { logo: String, rewards_img: String, rewards_title: String, + rewards_description: Option, rewards_nfts: Vec, img_card: String, title_card: String, @@ -67,6 +68,11 @@ pub_struct!(Deserialize; VerifyQuery { addr: FieldElement, }); +pub_struct!(Deserialize; EmailQuery { + addr: FieldElement, + email: String, +}); + pub_struct!(Deserialize; VerifyQuizQuery { addr: FieldElement, quiz_name: String, diff --git a/src/utils.rs b/src/utils.rs index 02108960..000a66b6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -368,12 +368,24 @@ impl DeployedTimesTrait for AppState { } } +pub async fn fetch_json_from_url(url: String) -> Result { + let client = reqwest::Client::new(); + match client.get(url).send().await { + Ok(response) => match response.json::().await { + Ok(json) => Ok(json), + Err(e) => Err(format!("Failed to get JSON response: {}", e)), + }, + Err(e) => Err(format!("Failed to send request: {}", e)), + } +} + pub async fn update_leaderboard( view_collection: Collection, address: String, experience: i64, timestamp: f64, ) { + // get current experience and new experience to it let mut old_experience = 0; let filter = doc! { "_id": &*address }; @@ -605,3 +617,4 @@ pub fn run_boosts_raffle(db: &Database, interval: u64) { interval, )); } +