Skip to content

Commit

Permalink
feat: add influence quest (#228)
Browse files Browse the repository at this point in the history
  • Loading branch information
ayushtom authored Jun 25, 2024
1 parent b85482a commit 5a0df62
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
109 changes: 109 additions & 0 deletions src/endpoints/quests/influence/claimable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use crate::models::{AppState, CompletedTaskDocument, Reward, RewardResponse};
use crate::utils::{get_error, get_nft};
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 starknet::{
core::types::FieldElement,
signers::{LocalWallet, SigningKey},
};
use std::sync::Arc;

const QUEST_ID: u32 = 32;
const TASK_IDS: &[u32] = &[153, 154, 155, 156];
const LAST_TASK: u32 = TASK_IDS[3];
const NFT_LEVEL: u32 = 44;

#[derive(Deserialize)]
pub struct ClaimableQuery {
addr: FieldElement,
}

#[route(
get,
"/quests/influence/claimable",
crate::endpoints::quests::influence::claimable
)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<ClaimableQuery>,
) -> impl IntoResponse {
let collection = state
.db
.collection::<CompletedTaskDocument>("completed_tasks");

let pipeline = vec![
doc! {
"$match": {
"address": &query.addr.to_string(),
"task_id": { "$in": TASK_IDS },
},
},
doc! {
"$lookup": {
"from": "tasks",
"localField": "task_id",
"foreignField": "id",
"as": "task",
},
},
doc! {
"$match": {
"task.quest_id": QUEST_ID,
},
},
doc! {
"$group": {
"_id": "$address",
"completed_tasks": { "$push": "$task_id" },
},
},
doc! {
"$match": {
"completed_tasks": { "$all": TASK_IDS },
},
},
];

let completed_tasks = collection.aggregate(pipeline, None).await;
match completed_tasks {
Ok(mut tasks_cursor) => {
if tasks_cursor.next().await.is_none() {
return get_error("User hasn't completed all tasks".into());
}

let signer = LocalWallet::from(SigningKey::from_secret_scalar(
state.conf.nft_contract.private_key,
));

let mut rewards = vec![];

let Ok((token_id, sig)) =
get_nft(QUEST_ID, LAST_TASK, &query.addr, NFT_LEVEL, &signer).await
else {
return get_error("Signature failed".into());
};

rewards.push(Reward {
task_id: LAST_TASK,
nft_contract: state.conf.nft_contract.address.clone(),
token_id: token_id.to_string(),
sig: (sig.r, sig.s),
});

if rewards.is_empty() {
get_error("No rewards found for this user".into())
} else {
(StatusCode::OK, Json(RewardResponse { rewards })).into_response()
}
}
Err(_) => get_error("Error querying rewards".into()),
}
}
153 changes: 153 additions & 0 deletions src/endpoints/quests/influence/discord_fw_callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
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 axum_auto_routes::route;
use mongodb::bson::doc;
use reqwest::header::AUTHORIZATION;
use serde::Deserialize;
use starknet::core::types::FieldElement;

#[derive(Deserialize)]
pub struct DiscordOAuthCallbackQuery {
code: String,
state: FieldElement,
}

#[derive(Deserialize, Debug)]
pub struct Guild {
id: String,
#[allow(dead_code)]
name: String,
}

#[route(
get,
"/quests/influence/discord_fw_callback",
crate::endpoints::quests::influence::discord_fw_callback
)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<DiscordOAuthCallbackQuery>,
) -> impl IntoResponse {
let quest_id = 32;
let task_id = 155;
let guild_id = "814990637178290177";
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/influence/discord_fw_callback",
state.conf.variables.api_link
),
),
("grant_type", &"authorization_code".to_string()),
("scope", &"guilds".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<Guild> = 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 Influence's Discord server".to_string(),
)
}

async fn exchange_authorization_code(
params: [(&str, &String); 6],
) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let res = client
.post("https://discord.com/api/oauth2/token")
.form(&params)
.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
),
)))
}
}
}
5 changes: 5 additions & 0 deletions src/endpoints/quests/influence/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod discord_fw_callback;
pub mod verify_twitter_fw;
pub mod verify_twitter_rw;
pub mod verify_twitter_rw_2;
pub mod claimable;
30 changes: 30 additions & 0 deletions src/endpoints/quests/influence/verify_twitter_fw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::sync::Arc;

use crate::{
models::{AppState, VerifyQuery},
utils::{get_error, CompletedTasksTrait},
};
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use axum_auto_routes::route;
use serde_json::json;

#[route(
get,
"/quests/influence/verify_twitter_fw",
crate::endpoints::quests::influence::verify_twitter_fw
)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<VerifyQuery>,
) -> impl IntoResponse {
let task_id = 154;
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)),
}
}
30 changes: 30 additions & 0 deletions src/endpoints/quests/influence/verify_twitter_rw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::sync::Arc;

use crate::{
models::{AppState, VerifyQuery},
utils::{get_error, CompletedTasksTrait},
};
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use axum_auto_routes::route;
use serde_json::json;

#[route(
get,
"/quests/influence/verify_twitter_rw",
crate::endpoints::quests::influence::verify_twitter_rw
)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<VerifyQuery>,
) -> impl IntoResponse {
let task_id = 153;
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)),
}
}
30 changes: 30 additions & 0 deletions src/endpoints/quests/influence/verify_twitter_rw_2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::sync::Arc;

use crate::{
models::{AppState, VerifyQuery},
utils::{get_error, CompletedTasksTrait},
};
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use axum_auto_routes::route;
use serde_json::json;

#[route(
get,
"/quests/influence/verify_twitter_rw_2",
crate::endpoints::quests::influence::verify_twitter_rw_2
)]
pub async fn handler(
State(state): State<Arc<AppState>>,
Query(query): Query<VerifyQuery>,
) -> impl IntoResponse {
let task_id = 156;
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)),
}
}
1 change: 1 addition & 0 deletions src/endpoints/quests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ pub mod verify_twitter_rw;
pub mod verify_twitter_fw;
pub mod bountive;
pub mod sithswap;
pub mod influence;

0 comments on commit 5a0df62

Please sign in to comment.