aboutsummaryrefslogblamecommitdiff
path: root/src/commands/osu.rs
blob: 5e0b5633a8f796e84938b13ffaf327dc8f7c4975 (plain) (tree)



















                                        


                                                                      










                                                                    






                                               

                                               




                                                                                                

                                 

                  


































                                                 



                                                                         

                                        








                                                              






                                                                       



















                                                                       


             

            
















                                                    
                    
























                                                      



                                                 

                                        





                                                                     


                     



                                                            
                                                 



                                                                                     
                          



                                                                            




                 

            


          
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,
}

/// This is kinda loose, and we should really be caching the osu token
///
/// Eh well, this *works* (sort of)
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(())
}