use std::time::Duration;
use crate::{Context, Error};
use poise::serenity_prelude as serenity;
use reqwest::{header, ClientBuilder};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct OsuTokenResponse {
pub access_token: String,
}
#[derive(Serialize)]
struct OsuTokenRequest {
pub client_id: u32,
pub client_secret: String,
pub grant_type: String,
pub scope: String,
}
async fn setup_reqwest() -> Result<reqwest::Client, Error> {
let client_id = std::env::var("OSU_CLIENT_ID").unwrap();
let client_secret = std::env::var("OSU_CLIENT_SECRET").unwrap();
let token_req = OsuTokenRequest {
client_id: client_id.parse::<u32>().unwrap(),
client_secret,
grant_type: "client_credentials".into(),
scope: "public".into(),
};
let req = reqwest::Client::new()
.post("https://osu.ppy.sh/oauth/token")
.json(&token_req)
.send()
.await?
.json::<OsuTokenResponse>()
.await?;
let mut headers = header::HeaderMap::new();
headers.insert(
"Authorization",
header::HeaderValue::from_str(format!("Bearer {}", req.access_token).as_str()).unwrap(),
);
Ok(ClientBuilder::new()
.default_headers(headers)
.build()
.unwrap())
}
#[derive(Deserialize, Serialize, Clone, Debug)]
struct OsuUser {
pub username: String,
pub avatar_url: String,
pub country_code: String,
pub is_supporter: bool,
pub join_date: chrono::DateTime<chrono::Utc>,
pub statistics: OsuUserStats,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
struct OsuUserStats {
pub global_rank: Option<u32>,
pub pp: f32,
pub hit_accuracy: Option<f32>,
pub grade_counts: OsuUserStatsGrades,
pub country_rank: Option<u32>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
struct OsuUserStatsGrades {
pub ss: u32,
pub s: u32,
pub a: u32,
}
/// Gets an osu profile by username
///
/// Usage:
/// ~osup <username>
/// Examples:
/// ~osup muirrum
#[poise::command(slash_command, prefix_command)]
pub async fn osup(
ctx: Context<'_>,
#[description = "The osu! username or ID to look up"] lookup: String,
) -> Result<(), Error> {
let client = setup_reqwest().await?;
let mut res = client
.get(format!(
"https://osu.ppy.sh/api/v2/users/{}?key=username",
lookup
))
.send()
.await?
.json::<OsuUser>()
.await?;
res.country_code = res.country_code.to_lowercase();
ctx.send(|m| {
m.embed(|e| {
e.title(format!("osu! Profile: {}", res.clone().username));
e.thumbnail(res.clone().avatar_url);
e.field(
"Ranks",
format!(
":map: #{}\n:flag_{}: #{}",
res.clone().statistics.global_rank.unwrap_or(0),
res.clone().country_code,
res.clone().statistics.country_rank.unwrap_or(0u32)
),
true,
);
e.field(
"Stats",
format!(
"**PP:** {}\n**Acc:** {}%",
res.clone().statistics.pp,
res.clone().statistics.hit_accuracy.unwrap_or(0.0)
),
false,
);
e
});
m
})
.await?;
Ok(())
}
#[derive(Deserialize, Debug, Clone)]
struct OsuBeatMap {
pub id: u32,
pub mode: String,
pub status: String,
pub version: String,
pub total_length: u32,
pub difficulty_rating: f32,
pub bpm: u32,
pub last_updated: chrono::DateTime<chrono::Utc>,
pub passcount: u32,
pub playcount: u32,
pub beatmapset: OsuBeatMapSet,
pub url: String,
}
#[derive(Deserialize, Debug, Clone)]
struct OsuBeatMapSet {
pub id: u32,
pub nsfw: bool,
pub title: String,
pub artist: String,
pub covers: OsuBeatMapSetCovers,
pub creator: String,
pub tags: String,
pub submitted_date: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize, Debug, Clone)]
struct OsuBeatMapSetCovers {
#[serde(rename = "list@2x")]
pub list2: String,
}
/// Looks up an osu! beatmap by its ID
///
/// Usage:
/// ~osubm <id>
#[poise::command(slash_command, prefix_command)]
pub async fn osubm(
ctx: Context<'_>,
#[description = "The beatmap ID"] bm_id: u32,
) -> Result<(), Error> {
let client = setup_reqwest().await?;
let mut res = client
.get(format!("https://osu.ppy.sh/api/v2/beatmaps/{}", bm_id))
.send()
.await?
.json::<OsuBeatMap>()
.await?;
ctx.send(|m| {
m.embed(|e| {
e.title(format!(
"osu! Beatmap: {} by {}",
res.beatmapset.title, res.beatmapset.creator
));
e.image(res.beatmapset.covers.list2);
e.description(format!(
"**Link:** {}\n**Length:** {} **BPM:** {}\n**Difficulty:** {}:star:",
res.url, res.total_length, res.bpm, res.difficulty_rating
));
e.footer(|f| {
f.text(format!(
"BM ID {} | BM Set ID {}\nCreated {}",
res.id, res.beatmapset.id, res.beatmapset.submitted_date
));
f
});
e
});
m
})
.await?;
Ok(())
}