osu: make commands aware of user's preferred mode (#54)

* Add preferred_mode to sql database

* Update username and preferred mode

* Make commands aware of preferred mode

* Fetch user extras to display information

* Show user information on forcesave
This commit is contained in:
Natsu Kagami 2024-10-31 14:04:08 +01:00 committed by GitHub
parent c5354e30ad
commit 7d490774e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 410 additions and 170 deletions

View file

@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n preferred_mode as \"preferred_mode: u8\",\n failures as \"failures: u8\"\n FROM osu_users WHERE user_id = ?",
"describe": {
"columns": [
{
"name": "user_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "username",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "id: i64",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "preferred_mode: u8",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
false,
false
]
},
"hash": "25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n failures as \"failures: u8\"\n FROM osu_users", "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n preferred_mode as \"preferred_mode: u8\",\n failures as \"failures: u8\"\n FROM osu_users",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -19,9 +19,14 @@
"type_info": "Int64" "type_info": "Int64"
}, },
{ {
"name": "failures: u8", "name": "preferred_mode: u8",
"ordinal": 3, "ordinal": 3,
"type_info": "Int64" "type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
} }
], ],
"parameters": { "parameters": {
@ -31,8 +36,9 @@
false, false,
true, true,
false, false,
false,
false false
] ]
}, },
"hash": "246e26a34c042872a77f53a84d62da31db069cced20e3b0f96a40c3c7dd99783" "hash": "54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c"
} }

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT\n INTO osu_users(user_id, username, id, preferred_mode, failures)\n VALUES(?, ?, ?, ?, ?)\n ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n SET\n username = excluded.username,\n preferred_mode = excluded.preferred_mode,\n failures = excluded.failures\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "b9c63ef764711088cfbb58ce2ed1f46e3521357ec1d7c062bef762d3e4267378"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n failures as \"failures: u8\"\n FROM osu_users WHERE id = ?", "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n preferred_mode as \"preferred_mode: u8\",\n failures as \"failures: u8\"\n FROM osu_users WHERE id = ?",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -19,9 +19,14 @@
"type_info": "Int64" "type_info": "Int64"
}, },
{ {
"name": "failures: u8", "name": "preferred_mode: u8",
"ordinal": 3, "ordinal": 3,
"type_info": "Int64" "type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
} }
], ],
"parameters": { "parameters": {
@ -31,8 +36,9 @@
false, false,
true, true,
false, false,
false,
false false
] ]
}, },
"hash": "9b7788f4d7144fe00f4bc9004c88dc8562ff3d7a931fc3f1dc039cc55fe3195a" "hash": "e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a"
} }

View file

@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n preferred_mode as \"preferred_mode: u8\",\n failures as \"failures: u8\"\n FROM osu_users WHERE user_id = ?",
"describe": {
"columns": [
{
"name": "user_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "username",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "id: i64",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "preferred_mode: u8",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
false,
false
]
},
"hash": "25dcc59341e6375ee6a55aa014aecc54be42e1e8787ae5a06a61bb8ba8e9c366"
}

View file

@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n preferred_mode as \"preferred_mode: u8\",\n failures as \"failures: u8\"\n FROM osu_users",
"describe": {
"columns": [
{
"name": "user_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "username",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "id: i64",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "preferred_mode: u8",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
true,
false,
false,
false
]
},
"hash": "54f54f669244fbdf1ad68664290d8f32f0bda74ceee62d10c84ac03b710c828c"
}

View file

@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT\n INTO osu_users(user_id, username, id, failures)\n VALUES(?, ?, ?, ?)\n ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n SET\n username = excluded.username,\n failures = excluded.failures\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "a5d8dccaaf80b2673c5c0e689c01a90861788ca84221baaaf19cd159ed3062c9"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT\n INTO osu_users(user_id, username, id, preferred_mode, failures)\n VALUES(?, ?, ?, ?, ?)\n ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n SET\n username = excluded.username,\n preferred_mode = excluded.preferred_mode,\n failures = excluded.failures\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "b9c63ef764711088cfbb58ce2ed1f46e3521357ec1d7c062bef762d3e4267378"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n failures as \"failures: u8\"\n FROM osu_users WHERE user_id = ?", "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n preferred_mode as \"preferred_mode: u8\",\n failures as \"failures: u8\"\n FROM osu_users WHERE id = ?",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -19,9 +19,14 @@
"type_info": "Int64" "type_info": "Int64"
}, },
{ {
"name": "failures: u8", "name": "preferred_mode: u8",
"ordinal": 3, "ordinal": 3,
"type_info": "Int64" "type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
} }
], ],
"parameters": { "parameters": {
@ -31,8 +36,9 @@
false, false,
true, true,
false, false,
false,
false false
] ]
}, },
"hash": "ae7f57eb92e0bec8439e682ab3ca10732991ffe803d05b09e908ecb4a74c0566" "hash": "e313b2e91d0da80a94e9a1030f94cf30b025c9ac0ac4d5bcc12656459c5e083a"
} }

View file

@ -0,0 +1,4 @@
-- Add migration script here
ALTER TABLE osu_users
ADD COLUMN preferred_mode INT NOT NULL DEFAULT 0 CHECK (preferred_mode >= 0 AND preferred_mode < 4);

View file

@ -9,6 +9,7 @@ pub struct OsuUser {
pub username: Option<String>, // should always be there pub username: Option<String>, // should always be there
pub id: i64, pub id: i64,
pub modes: Map<u8, OsuUserMode>, pub modes: Map<u8, OsuUserMode>,
pub preferred_mode: u8,
/// Number of consecutive update failures /// Number of consecutive update failures
pub failures: u8, pub failures: u8,
} }
@ -97,6 +98,7 @@ mod raw {
pub user_id: i64, pub user_id: i64,
pub username: Option<String>, // should always be there pub username: Option<String>, // should always be there
pub id: i64, pub id: i64,
pub preferred_mode: u8,
pub failures: u8, pub failures: u8,
} }
} }
@ -108,6 +110,7 @@ impl OsuUser {
username: r.username, username: r.username,
id: r.id, id: r.id,
modes, modes,
preferred_mode: r.preferred_mode,
failures: r.failures, failures: r.failures,
} }
} }
@ -119,6 +122,7 @@ impl OsuUser {
user_id as "user_id: i64", user_id as "user_id: i64",
username, username,
id as "id: i64", id as "id: i64",
preferred_mode as "preferred_mode: u8",
failures as "failures: u8" failures as "failures: u8"
FROM osu_users WHERE user_id = ?"#, FROM osu_users WHERE user_id = ?"#,
user_id user_id
@ -141,6 +145,7 @@ impl OsuUser {
user_id as "user_id: i64", user_id as "user_id: i64",
username, username,
id as "id: i64", id as "id: i64",
preferred_mode as "preferred_mode: u8",
failures as "failures: u8" failures as "failures: u8"
FROM osu_users WHERE id = ?"#, FROM osu_users WHERE id = ?"#,
osu_id osu_id
@ -164,6 +169,7 @@ impl OsuUser {
user_id as "user_id: i64", user_id as "user_id: i64",
username, username,
id as "id: i64", id as "id: i64",
preferred_mode as "preferred_mode: u8",
failures as "failures: u8" failures as "failures: u8"
FROM osu_users"#, FROM osu_users"#,
) )
@ -200,16 +206,18 @@ impl OsuUser {
query!( query!(
r#"INSERT r#"INSERT
INTO osu_users(user_id, username, id, failures) INTO osu_users(user_id, username, id, preferred_mode, failures)
VALUES(?, ?, ?, ?) VALUES(?, ?, ?, ?, ?)
ON CONFLICT (user_id) WHERE id = ? DO UPDATE ON CONFLICT (user_id) WHERE id = ? DO UPDATE
SET SET
username = excluded.username, username = excluded.username,
preferred_mode = excluded.preferred_mode,
failures = excluded.failures failures = excluded.failures
"#, "#,
self.user_id, self.user_id,
self.username, self.username,
self.id, self.id,
self.preferred_mode,
self.failures, self.failures,
self.user_id, self.user_id,
) )

View file

@ -122,6 +122,10 @@ impl Announcer {
.unwrap_or(0), .unwrap_or(0),
last_update: now, last_update: now,
}; };
if u.username != user.username {
user.username = u.username.clone().into();
}
user.preferred_mode = u.preferred_mode;
let last = user.modes.insert(mode, stats); let last = user.modes.insert(mode, stats);
// broadcast // broadcast

View file

@ -113,6 +113,7 @@ pub struct OsuUser {
pub username: Cow<'static, str>, pub username: Cow<'static, str>,
pub id: u64, pub id: u64,
pub modes: Map<Mode, OsuUserMode>, pub modes: Map<Mode, OsuUserMode>,
pub preferred_mode: Mode,
/// More than 5 failures => gone /// More than 5 failures => gone
pub failures: u8, pub failures: u8,
} }
@ -136,6 +137,7 @@ impl From<OsuUser> for model::OsuUser {
.into_iter() .into_iter()
.map(|(k, v)| (k as u8, v.into())) .map(|(k, v)| (k as u8, v.into()))
.collect(), .collect(),
preferred_mode: u.preferred_mode as u8,
failures: u.failures, failures: u.failures,
} }
} }
@ -152,11 +154,21 @@ impl From<model::OsuUser> for OsuUser {
.into_iter() .into_iter()
.map(|(k, v)| (k.into(), v.into())) .map(|(k, v)| (k.into(), v.into()))
.collect(), .collect(),
preferred_mode: u.preferred_mode.into(),
failures: u.failures, failures: u.failures,
} }
} }
} }
impl From<OsuUser> for crate::models::UserHeader {
fn from(value: OsuUser) -> Self {
Self {
id: value.id as u64,
username: value.username.to_string(),
}
}
}
impl From<OsuUserMode> for model::OsuUserMode { impl From<OsuUserMode> for model::OsuUserMode {
fn from(m: OsuUserMode) -> Self { fn from(m: OsuUserMode) -> Self {
Self { Self {

View file

@ -1,6 +1,6 @@
use super::BeatmapWithMode; use super::{BeatmapWithMode, UserExtras};
use crate::{ use crate::{
discord::oppai_cache::{Accuracy, BeatmapContent, BeatmapInfo, BeatmapInfoWithPP}, discord::oppai_cache::{Accuracy, BeatmapContent, BeatmapInfoWithPP},
models::{Beatmap, Difficulty, Mode, Mods, Rank, Score, User}, models::{Beatmap, Difficulty, Mode, Mods, Rank, Score, User},
UserHeader, UserHeader,
}; };
@ -479,13 +479,13 @@ impl<'a> ScoreEmbedBuilder<'a> {
} }
} }
pub(crate) fn user_embed( pub(crate) fn user_embed(u: User, ex: UserExtras) -> CreateEmbed {
u: User,
map_length: f64,
map_age: i64,
best: Option<(Score, BeatmapWithMode, BeatmapInfo)>,
) -> CreateEmbed {
let mut stats = Vec::<(&'static str, String, bool)>::new(); let mut stats = Vec::<(&'static str, String, bool)>::new();
let UserExtras {
map_length,
map_age,
best_score: best,
} = ex;
if map_length > 0.0 { if map_length > 0.0 {
stats.push(( stats.push((
"Weighted Map Length", "Weighted Map Length",

View file

@ -8,7 +8,7 @@ use serenity::all::{
}; };
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::Mods; use crate::{Mods, UserHeader};
use super::{ use super::{
display::ScoreListStyle, display::ScoreListStyle,
@ -70,8 +70,9 @@ pub fn handle_check_button<'a>(
return Ok(()); return Ok(());
} }
}; };
let header = UserHeader::from(user.clone());
let scores = super::do_check(&env, &bm, Mods::NOMOD, &crate::UserID::ID(user.id)).await?; let scores = super::do_check(&env, &bm, Mods::NOMOD, &header).await?;
if scores.is_empty() { if scores.is_empty() {
comp.create_followup( comp.create_followup(
&ctx, &ctx,

View file

@ -1,7 +1,6 @@
use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc}; use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc};
use chrono::Utc; use chrono::Utc;
use future::try_join;
use futures_util::join; use futures_util::join;
use interaction::{beatmap_components, score_components}; use interaction::{beatmap_components, score_components};
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
@ -33,7 +32,7 @@ use crate::{
models::{Beatmap, Mode, Mods, Score, User}, models::{Beatmap, Mode, Mods, Score, User},
mods::UnparsedMods, mods::UnparsedMods,
request::{BeatmapRequestKind, UserID}, request::{BeatmapRequestKind, UserID},
OsuClient as OsuHttpClient, OsuClient as OsuHttpClient, UserHeader,
}; };
mod announcer; mod announcer;
@ -141,6 +140,7 @@ pub async fn setup(
#[prefix = "osu"] #[prefix = "osu"]
#[description = "osu! related commands."] #[description = "osu! related commands."]
#[commands( #[commands(
user,
std, std,
taiko, taiko,
catch, catch,
@ -156,9 +156,19 @@ pub async fn setup(
show_leaderboard, show_leaderboard,
clean_cache clean_cache
)] )]
#[default_command(std)] #[default_command(user)]
struct Osu; struct Osu;
#[command]
#[description = "Receive information about an user in their preferred mode."]
#[usage = "[username or user_id = your saved username]"]
#[max_args(1)]
pub async fn user(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
get_user(ctx, &env, msg, args, None).await
}
#[command] #[command]
#[aliases("osu", "osu!")] #[aliases("osu", "osu!")]
#[description = "Receive information about an user in osu!std mode."] #[description = "Receive information about an user in osu!std mode."]
@ -235,7 +245,13 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
} }
}; };
async fn find_score(client: &OsuHttpClient, u: &User) -> Result<Option<(Score, Mode)>> { async fn find_score(client: &OsuHttpClient, u: &User) -> Result<Option<(Score, Mode)>> {
for mode in &[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania] { for mode in &[
u.preferred_mode,
Mode::Std,
Mode::Taiko,
Mode::Catch,
Mode::Mania,
] {
let scores = client let scores = client
.user_best(UserID::ID(u.id), |f| f.mode(*mode)) .user_best(UserID::ID(u.id), |f| f.mode(*mode))
.await?; .await?;
@ -269,10 +285,11 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let reply = msg.reply( let reply = msg.reply(
&ctx, &ctx,
format!( format!(
"To set your osu username, please make your most recent play \ "To set your osu username to **{}**, please make your most recent play \
be the following map: `/b/{}` in **{}** mode! \ be the following map: `/b/{}` in **{}** mode! \
It does **not** have to be a pass, and **NF** can be used! \ It does **not** have to be a pass, and **NF** can be used! \
React to this message with 👌 within 5 minutes when you're done!", React to this message with 👌 within 5 minutes when you're done!",
u.username,
score.beatmap_id, score.beatmap_id,
mode.as_str_new_site() mode.as_str_new_site()
), ),
@ -306,7 +323,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.message_id(reply.id) .message_id(reply.id)
.author_id(msg.author.id) .author_id(msg.author.id)
.filter(move |r| r.emoji == emoji) .filter(move |r| r.emoji == emoji)
.timeout(std::time::Duration::from_secs(300)) .timeout(std::time::Duration::from_secs(300) + beatmap.difficulty.total_length)
.next() .next()
.await; .await;
if let Some(ur) = user_reaction { if let Some(ur) = user_reaction {
@ -319,19 +336,40 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
} }
}; };
if !completed { if !completed {
reply
.edit(
&ctx,
EditMessage::new()
.content(format!(
"Setting username to **{}** failed due to timeout. Please try again!",
u.username
))
.embeds(vec![])
.components(vec![]),
)
.await?;
reaction.delete(&ctx).await?; reaction.delete(&ctx).await?;
return Ok(()); return Ok(());
} }
let username = u.username.clone(); let username = u.username.clone();
add_user(msg.author.id, u, &env).await?; add_user(msg.author.id, &u, &env).await?;
msg.reply( let ex = UserExtras::from_user(&env, &u, mode).await?;
msg.channel_id
.send_message(
&ctx, &ctx,
CreateMessage::new()
.reference_message(msg)
.content(
MessageBuilder::new() MessageBuilder::new()
.push("user has been set to ") .push("Youmu is now tracking user ")
.push_mono_safe(username) .push(msg.author.mention().to_string())
.push(" with osu! account ")
.push_bold_safe(username)
.build(), .build(),
) )
.add_embed(user_embed(u, ex)),
)
.await?; .await?;
Ok(()) Ok(())
} }
@ -355,14 +393,23 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
.await?; .await?;
match user { match user {
Some(u) => { Some(u) => {
add_user(target, u, &env).await?; add_user(target, &u, &env).await?;
msg.reply( let ex = UserExtras::from_user(&env, &u, u.preferred_mode).await?;
msg.channel_id
.send_message(
&ctx, &ctx,
CreateMessage::new()
.reference_message(msg)
.content(
MessageBuilder::new() MessageBuilder::new()
.push("user has been set to ") .push("Youmu is now tracking user ")
.push_mono_safe(username) .push(target.mention().to_string())
.push(" with osu! account ")
.push_bold_safe(username)
.build(), .build(),
) )
.embed(user_embed(u, ex)),
)
.await?; .await?;
} }
None => { None => {
@ -372,10 +419,12 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
Ok(()) Ok(())
} }
async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv) -> Result<()> { async fn add_user(target: serenity::model::id::UserId, user: &User, env: &OsuEnv) -> Result<()> {
let modes = [Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania] let modes = [Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]
.into_iter() .into_iter()
.map(|mode| async move { .map(|mode| {
let mode = mode.clone();
async move {
let pp = async { let pp = async {
env.client env.client
.user(&UserID::ID(user.id), |f| f.mode(mode)) .user(&UserID::ID(user.id), |f| f.mode(mode))
@ -384,37 +433,20 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
.unwrap_or(None) .unwrap_or(None)
.and_then(|u| u.pp) .and_then(|u| u.pp)
}; };
let map_length_age = async { let map_length_age = UserExtras::from_user(env, user, mode);
let scores = env let (pp, ex) = join!(pp, map_length_age);
.client pp.zip(ex.ok()).map(|(pp, ex)| {
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
.await
.pls_ok()
.unwrap_or_else(std::vec::Vec::new);
(
calculate_weighted_map_length(&scores, &env.beatmaps, mode)
.await
.pls_ok(),
calculate_weighted_map_age(&scores, &env.beatmaps, mode)
.await
.pls_ok(),
)
};
let (pp, (map_length, map_age)) = join!(pp, map_length_age);
pp.zip(map_length)
.zip(map_age)
.map(|((pp, map_length), map_age)| {
( (
mode, mode,
OsuUserMode { OsuUserMode {
pp, pp,
map_length, map_length: ex.map_length,
map_age, map_age: ex.map_age,
last_update: Utc::now(), last_update: Utc::now(),
}, },
) )
}) })
}
}) })
.collect::<stream::FuturesOrdered<_>>() .collect::<stream::FuturesOrdered<_>>()
.filter_map(future::ready) .filter_map(future::ready)
@ -423,7 +455,8 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
let u = OsuUser { let u = OsuUser {
user_id: target, user_id: target,
username: user.username.into(), username: user.username.clone().into(),
preferred_mode: user.preferred_mode,
id: user.id, id: user.id,
failures: 0, failures: 0,
modes, modes,
@ -432,6 +465,47 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
Ok(()) Ok(())
} }
/// Stores extra information to create an user embed.
pub(crate) struct UserExtras {
pub map_length: f64,
pub map_age: i64,
pub best_score: Option<(Score, BeatmapWithMode, BeatmapInfo)>,
}
impl UserExtras {
// Collect UserExtras from the given user.
pub async fn from_user(env: &OsuEnv, user: &User, mode: Mode) -> Result<Self> {
let scores = env
.client
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
.await
.pls_ok()
.unwrap_or_else(std::vec::Vec::new);
let (length, age) = join!(
calculate_weighted_map_length(&scores, &env.beatmaps, mode),
calculate_weighted_map_age(&scores, &env.beatmaps, mode)
);
let best = if let Some(s) = scores.into_iter().next() {
let beatmap = env.beatmaps.get_beatmap(s.beatmap_id, mode).await?;
let info = env
.oppai
.get_beatmap(s.beatmap_id)
.await?
.get_info_with(mode, &s.mods)?;
Some((s, BeatmapWithMode(beatmap, mode), info))
} else {
None
};
Ok(Self {
map_length: length.unwrap_or(0.0),
map_age: age.unwrap_or(0),
best_score: best,
})
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct ModeArg(Mode); struct ModeArg(Mode);
@ -448,24 +522,6 @@ impl FromStr for ModeArg {
} }
} }
async fn to_user_id_query(
s: Option<UsernameArg>,
env: &OsuEnv,
author: serenity::all::UserId,
) -> Result<UserID, Error> {
let id = match s {
Some(UsernameArg::Raw(s)) => return Ok(UserID::from_string(s)),
Some(UsernameArg::Tagged(r)) => r,
None => author,
};
env.saved_users
.by_user_id(id)
.await?
.map(|u| UserID::Username(u.username.to_string()))
.ok_or_else(|| Error::msg("No saved account found"))
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
enum Nth { enum Nth {
#[default] #[default]
@ -496,7 +552,7 @@ struct ListingArgs {
pub nth: Nth, pub nth: Nth,
pub style: ScoreListStyle, pub style: ScoreListStyle,
pub mode: Mode, pub mode: Mode,
pub user: UserID, pub user: UserHeader,
} }
impl ListingArgs { impl ListingArgs {
@ -508,13 +564,10 @@ impl ListingArgs {
) -> Result<ListingArgs> { ) -> Result<ListingArgs> {
let nth = args.single::<Nth>().unwrap_or(Nth::All); let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or(default_style); let style = args.single::<ScoreListStyle>().unwrap_or(default_style);
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0; let mode_override = args.single::<ModeArg>().map(|v| v.0).ok();
let user = to_user_id_query( let (mode, user) =
args.quoted().trimmed().single::<UsernameArg>().ok(), user_header_from_args(args.single::<UsernameArg>().ok(), env, msg).await?;
&env, let mode = mode_override.unwrap_or(mode);
msg.author.id,
)
.await?;
Ok(Self { Ok(Self {
nth, nth,
style, style,
@ -524,6 +577,35 @@ impl ListingArgs {
} }
} }
async fn user_header_from_args(
arg: Option<UsernameArg>,
env: &OsuEnv,
msg: &Message,
) -> Result<(Mode, UserHeader)> {
let (mode, user) = match arg {
Some(UsernameArg::Raw(r)) => {
let user = env
.client
.user(&UserID::Username(r), |f| f)
.await?
.ok_or(Error::msg("User not found"))?;
(user.preferred_mode, user.into())
}
Some(UsernameArg::Tagged(t)) => {
let user = env.saved_users.by_user_id(t).await?.ok_or_else(|| {
Error::msg(format!("{} does not have a saved account!", t.mention()))
})?;
(user.preferred_mode, user.into())
}
None => {
let user = env.saved_users.by_user_id(msg.author.id).await?
.ok_or(Error::msg("You do not have a saved account! Use `osu save` command to save your osu! account."))?;
(user.preferred_mode, user.into())
}
};
Ok((mode, user))
}
#[command] #[command]
#[aliases("rs", "rc", "r")] #[aliases("rs", "rc", "r")]
#[description = "Gets an user's recent play"] #[description = "Gets an user's recent play"]
@ -542,11 +624,6 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
} = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::Table).await?; } = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::Table).await?;
let osu_client = &env.client; let osu_client = &env.client;
let user = osu_client
.user(&user, |f| f.mode(mode))
.await?
.ok_or_else(|| Error::msg("User not found"))?;
let plays = osu_client let plays = osu_client
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?; .await?;
@ -581,7 +658,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
CreateMessage::new() CreateMessage::new()
.content("Here is the play that you requested".to_string()) .content("Here is the play that you requested".to_string())
.embed( .embed(
score_embed(play, &beatmap_mode, &content, &user) score_embed(play, &beatmap_mode, &content, user)
.footer(format!("Attempt #{}", attempts)) .footer(format!("Attempt #{}", attempts))
.build(), .build(),
) )
@ -616,10 +693,6 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let osu_client = &env.client; let osu_client = &env.client;
let user = osu_client
.user(&user, |f| f.mode(mode))
.await?
.ok_or_else(|| Error::msg("User not found"))?;
let plays = osu_client let plays = osu_client
.user_pins(UserID::ID(user.id), |f| f.mode(mode).limit(50)) .user_pins(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?; .await?;
@ -648,7 +721,7 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
&ctx, &ctx,
CreateMessage::new() CreateMessage::new()
.content("Here is the play that you requested".to_string()) .content("Here is the play that you requested".to_string())
.embed(score_embed(play, &beatmap_mode, &content, &user).build()) .embed(score_embed(play, &beatmap_mode, &content, user).build())
.components(vec![score_components(msg.guild_id)]) .components(vec![score_components(msg.guild_id)])
.reference_message(msg), .reference_message(msg),
) )
@ -803,7 +876,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
.single::<ScoreListStyle>() .single::<ScoreListStyle>()
.unwrap_or(ScoreListStyle::Grid); .unwrap_or(ScoreListStyle::Grid);
let username_arg = args.single::<UsernameArg>().ok(); let username_arg = args.single::<UsernameArg>().ok();
let user = to_user_id_query(username_arg, &env, msg.author.id).await?; let (_, user) = user_header_from_args(username_arg, &env, msg).await?;
let scores = do_check(&env, &bm, &mods, &user).await?; let scores = do_check(&env, &bm, &mods, &user).await?;
@ -816,7 +889,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
&ctx, &ctx,
format!( format!(
"Here are the scores by `{}` on `{}`!", "Here are the scores by `{}` on `{}`!",
&user, &user.username,
bm.short_link(&mods) bm.short_link(&mods)
), ),
) )
@ -832,16 +905,12 @@ pub(crate) async fn do_check(
env: &OsuEnv, env: &OsuEnv,
bm: &BeatmapWithMode, bm: &BeatmapWithMode,
mods: &Mods, mods: &Mods,
user: &UserID, user: &UserHeader,
) -> Result<Vec<Score>> { ) -> Result<Vec<Score>> {
let BeatmapWithMode(b, m) = bm; let BeatmapWithMode(b, m) = bm;
let osu_client = &env.client; let osu_client = &env.client;
let user = osu_client
.user(user, |f| f)
.await?
.ok_or_else(|| Error::msg("User not found"))?;
let mut scores = osu_client let mut scores = osu_client
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(*m)) .scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(*m))
.await? .await?
@ -871,10 +940,6 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
user, user,
} = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::default()).await?; } = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::default()).await?;
let osu_client = &env.client; let osu_client = &env.client;
let user = osu_client
.user(&user, |f| f.mode(mode))
.await?
.ok_or_else(|| Error::msg("User not found"))?;
let plays = osu_client let plays = osu_client
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100)) .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
@ -898,7 +963,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
msg.author msg.author
)) ))
.embed( .embed(
score_embed(&play, &beatmap, &content, &user) score_embed(&play, &beatmap, &content, user)
.top_record(nth + 1) .top_record(nth + 1)
.build(), .build(),
) )
@ -945,33 +1010,18 @@ async fn get_user(
env: &OsuEnv, env: &OsuEnv,
msg: &Message, msg: &Message,
mut args: Args, mut args: Args,
mode: Mode, mode_override: impl Into<Option<Mode>>,
) -> CommandResult { ) -> CommandResult {
let user = to_user_id_query(args.single::<UsernameArg>().ok(), env, msg.author.id).await?; let (mode, user) = user_header_from_args(args.single::<UsernameArg>().ok(), env, msg).await?;
let osu_client = &env.client; let mode = mode_override.into().unwrap_or(mode);
let meta_cache = &env.beatmaps; let user = env
let user = osu_client.user(&user, |f| f.mode(mode)).await?; .client
.user(&UserID::ID(user.id), |f| f.mode(mode))
.await?;
match user { match user {
Some(u) => { Some(u) => {
let bests = osu_client let ex = UserExtras::from_user(env, &u, mode).await?;
.user_best(UserID::ID(u.id), |f| f.limit(100).mode(mode))
.await?;
let map_length = calculate_weighted_map_length(&bests, meta_cache, mode);
let map_age = calculate_weighted_map_age(&bests, meta_cache, mode);
let (map_length, map_age) = try_join(map_length, map_age).await?;
let best = match bests.into_iter().next() {
Some(m) => {
let beatmap = meta_cache.get_beatmap(m.beatmap_id, mode).await?;
let info = env
.oppai
.get_beatmap(m.beatmap_id)
.await?
.get_info_with(mode, &m.mods)?;
Some((m, BeatmapWithMode(beatmap, mode), info))
}
None => None,
};
msg.channel_id msg.channel_id
.send_message( .send_message(
&ctx, &ctx,
@ -980,7 +1030,7 @@ async fn get_user(
"{}: here is the user that you requested", "{}: here is the user that you requested",
msg.author msg.author
)) ))
.embed(user_embed(u, map_length, map_age, best)), .embed(user_embed(u, ex)),
) )
.await?; .await?;
} }

View file

@ -488,7 +488,6 @@ impl UserEvent {
pub struct UserHeader { pub struct UserHeader {
pub id: u64, pub id: u64,
pub username: String, pub username: String,
pub country: String,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -497,6 +496,7 @@ pub struct User {
pub username: String, pub username: String,
pub joined: DateTime<Utc>, pub joined: DateTime<Utc>,
pub country: String, pub country: String,
pub preferred_mode: Mode,
// History // History
pub count_300: u64, pub count_300: u64,
pub count_100: u64, pub count_100: u64,
@ -544,7 +544,6 @@ impl<'a> From<&'a User> for UserHeader {
Self { Self {
id: u.id, id: u.id,
username: u.username.clone(), username: u.username.clone(),
country: u.country.clone(),
} }
} }
} }
@ -554,7 +553,6 @@ impl From<User> for UserHeader {
Self { Self {
id: u.id, id: u.id,
username: u.username, username: u.username,
country: u.country,
} }
} }
} }

View file

@ -80,6 +80,7 @@ impl User {
username: user.username.into_string(), username: user.username.into_string(),
joined: time_to_utc(user.join_date), joined: time_to_utc(user.join_date),
country: user.country_code.to_string(), country: user.country_code.to_string(),
preferred_mode: user.mode.into(),
count_300: 0, // why do we even want this count_300: 0, // why do we even want this
count_100: 0, // why do we even want this count_100: 0, // why do we even want this
count_50: 0, // why do we even want this count_50: 0, // why do we even want this

View file

@ -132,8 +132,8 @@ pub mod builders {
} }
} }
pub fn mode(&mut self, mode: Mode) -> &mut Self { pub fn mode(&mut self, mode: impl Into<Option<Mode>>) -> &mut Self {
self.mode = Some(mode); self.mode = mode.into();
self self
} }
@ -185,8 +185,8 @@ pub mod builders {
self self
} }
pub fn mode(&mut self, mode: Mode) -> &mut Self { pub fn mode(&mut self, mode: impl Into<Option<Mode>>) -> &mut Self {
self.mode = Some(mode); self.mode = mode.into();
self self
} }