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 { 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::().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::().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, pub statistics: OsuUserStats, } #[derive(Deserialize, Serialize, Clone, Debug)] struct OsuUserStats { pub global_rank: Option, pub pp: f32, pub hit_accuracy: Option, pub grade_counts: OsuUserStatsGrades, pub country_rank: Option, } #[derive(Deserialize, Serialize, Clone, Debug)] struct OsuUserStatsGrades { pub ss: u32, pub s: u32, pub a: u32, } /// Gets an osu profile by username /// /// Usage: /// ~osup /// 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::().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, pub passcount: u32, pub playcount: u32, pub beatmapset: OsuBeatMapSet, } #[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, } #[derive(Deserialize, Debug, Clone)] struct OsuBeatMapSetCovers { #[serde(rename = "list@2x")] pub list2: String, } /// Looks up an osu! beatmap by its ID /// /// Usage: /// ~osubm #[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::().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!("**Length:** {} **BPM:** {}\n**Difficulty:** {}:star:", 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(()) }