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",
"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": {
"columns": [
{
@ -19,9 +19,14 @@
"type_info": "Int64"
},
{
"name": "failures: u8",
"name": "preferred_mode: u8",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
true,
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",
"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": {
"columns": [
{
@ -19,9 +19,14 @@
"type_info": "Int64"
},
{
"name": "failures: u8",
"name": "preferred_mode: u8",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
true,
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",
"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": {
"columns": [
{
@ -19,9 +19,14 @@
"type_info": "Int64"
},
{
"name": "failures: u8",
"name": "preferred_mode: u8",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "failures: u8",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
@ -31,8 +36,9 @@
false,
true,
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 id: i64,
pub modes: Map<u8, OsuUserMode>,
pub preferred_mode: u8,
/// Number of consecutive update failures
pub failures: u8,
}
@ -97,6 +98,7 @@ mod raw {
pub user_id: i64,
pub username: Option<String>, // should always be there
pub id: i64,
pub preferred_mode: u8,
pub failures: u8,
}
}
@ -108,6 +110,7 @@ impl OsuUser {
username: r.username,
id: r.id,
modes,
preferred_mode: r.preferred_mode,
failures: r.failures,
}
}
@ -119,6 +122,7 @@ impl OsuUser {
user_id as "user_id: i64",
username,
id as "id: i64",
preferred_mode as "preferred_mode: u8",
failures as "failures: u8"
FROM osu_users WHERE user_id = ?"#,
user_id
@ -141,6 +145,7 @@ impl OsuUser {
user_id as "user_id: i64",
username,
id as "id: i64",
preferred_mode as "preferred_mode: u8",
failures as "failures: u8"
FROM osu_users WHERE id = ?"#,
osu_id
@ -164,6 +169,7 @@ impl OsuUser {
user_id as "user_id: i64",
username,
id as "id: i64",
preferred_mode as "preferred_mode: u8",
failures as "failures: u8"
FROM osu_users"#,
)
@ -200,16 +206,18 @@ impl OsuUser {
query!(
r#"INSERT
INTO osu_users(user_id, username, id, failures)
VALUES(?, ?, ?, ?)
INTO osu_users(user_id, username, id, preferred_mode, failures)
VALUES(?, ?, ?, ?, ?)
ON CONFLICT (user_id) WHERE id = ? DO UPDATE
SET
username = excluded.username,
preferred_mode = excluded.preferred_mode,
failures = excluded.failures
"#,
self.user_id,
self.username,
self.id,
self.preferred_mode,
self.failures,
self.user_id,
)

View file

@ -122,6 +122,10 @@ impl Announcer {
.unwrap_or(0),
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);
// broadcast

View file

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

View file

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

View file

@ -8,7 +8,7 @@ use serenity::all::{
};
use youmubot_prelude::*;
use crate::Mods;
use crate::{Mods, UserHeader};
use super::{
display::ScoreListStyle,
@ -70,8 +70,9 @@ pub fn handle_check_button<'a>(
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() {
comp.create_followup(
&ctx,

View file

@ -1,7 +1,6 @@
use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc};
use chrono::Utc;
use future::try_join;
use futures_util::join;
use interaction::{beatmap_components, score_components};
use rand::seq::IteratorRandom;
@ -33,7 +32,7 @@ use crate::{
models::{Beatmap, Mode, Mods, Score, User},
mods::UnparsedMods,
request::{BeatmapRequestKind, UserID},
OsuClient as OsuHttpClient,
OsuClient as OsuHttpClient, UserHeader,
};
mod announcer;
@ -141,6 +140,7 @@ pub async fn setup(
#[prefix = "osu"]
#[description = "osu! related commands."]
#[commands(
user,
std,
taiko,
catch,
@ -156,9 +156,19 @@ pub async fn setup(
show_leaderboard,
clean_cache
)]
#[default_command(std)]
#[default_command(user)]
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]
#[aliases("osu", "osu!")]
#[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)>> {
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
.user_best(UserID::ID(u.id), |f| f.mode(*mode))
.await?;
@ -269,10 +285,11 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let reply = msg.reply(
&ctx,
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! \
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!",
u.username,
score.beatmap_id,
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)
.author_id(msg.author.id)
.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()
.await;
if let Some(ur) = user_reaction {
@ -319,20 +336,41 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
}
};
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?;
return Ok(());
}
let username = u.username.clone();
add_user(msg.author.id, u, &env).await?;
msg.reply(
&ctx,
MessageBuilder::new()
.push("user has been set to ")
.push_mono_safe(username)
.build(),
)
.await?;
add_user(msg.author.id, &u, &env).await?;
let ex = UserExtras::from_user(&env, &u, mode).await?;
msg.channel_id
.send_message(
&ctx,
CreateMessage::new()
.reference_message(msg)
.content(
MessageBuilder::new()
.push("Youmu is now tracking user ")
.push(msg.author.mention().to_string())
.push(" with osu! account ")
.push_bold_safe(username)
.build(),
)
.add_embed(user_embed(u, ex)),
)
.await?;
Ok(())
}
@ -355,15 +393,24 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
.await?;
match user {
Some(u) => {
add_user(target, u, &env).await?;
msg.reply(
&ctx,
MessageBuilder::new()
.push("user has been set to ")
.push_mono_safe(username)
.build(),
)
.await?;
add_user(target, &u, &env).await?;
let ex = UserExtras::from_user(&env, &u, u.preferred_mode).await?;
msg.channel_id
.send_message(
&ctx,
CreateMessage::new()
.reference_message(msg)
.content(
MessageBuilder::new()
.push("Youmu is now tracking user ")
.push(target.mention().to_string())
.push(" with osu! account ")
.push_bold_safe(username)
.build(),
)
.embed(user_embed(u, ex)),
)
.await?;
}
None => {
msg.reply(&ctx, "user not found...").await?;
@ -372,49 +419,34 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
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]
.into_iter()
.map(|mode| async move {
let pp = async {
env.client
.user(&UserID::ID(user.id), |f| f.mode(mode))
.await
.pls_ok()
.unwrap_or(None)
.and_then(|u| u.pp)
};
let map_length_age = async {
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);
(
calculate_weighted_map_length(&scores, &env.beatmaps, mode)
.map(|mode| {
let mode = mode.clone();
async move {
let pp = async {
env.client
.user(&UserID::ID(user.id), |f| f.mode(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)| {
.pls_ok()
.unwrap_or(None)
.and_then(|u| u.pp)
};
let map_length_age = UserExtras::from_user(env, user, mode);
let (pp, ex) = join!(pp, map_length_age);
pp.zip(ex.ok()).map(|(pp, ex)| {
(
mode,
OsuUserMode {
pp,
map_length,
map_age,
map_length: ex.map_length,
map_age: ex.map_age,
last_update: Utc::now(),
},
)
})
}
})
.collect::<stream::FuturesOrdered<_>>()
.filter_map(future::ready)
@ -423,7 +455,8 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
let u = OsuUser {
user_id: target,
username: user.username.into(),
username: user.username.clone().into(),
preferred_mode: user.preferred_mode,
id: user.id,
failures: 0,
modes,
@ -432,6 +465,47 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
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)]
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)]
enum Nth {
#[default]
@ -496,7 +552,7 @@ struct ListingArgs {
pub nth: Nth,
pub style: ScoreListStyle,
pub mode: Mode,
pub user: UserID,
pub user: UserHeader,
}
impl ListingArgs {
@ -508,13 +564,10 @@ impl ListingArgs {
) -> Result<ListingArgs> {
let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or(default_style);
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user = to_user_id_query(
args.quoted().trimmed().single::<UsernameArg>().ok(),
&env,
msg.author.id,
)
.await?;
let mode_override = args.single::<ModeArg>().map(|v| v.0).ok();
let (mode, user) =
user_header_from_args(args.single::<UsernameArg>().ok(), env, msg).await?;
let mode = mode_override.unwrap_or(mode);
Ok(Self {
nth,
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]
#[aliases("rs", "rc", "r")]
#[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?;
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
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?;
@ -581,7 +658,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
CreateMessage::new()
.content("Here is the play that you requested".to_string())
.embed(
score_embed(play, &beatmap_mode, &content, &user)
score_embed(play, &beatmap_mode, &content, user)
.footer(format!("Attempt #{}", attempts))
.build(),
)
@ -616,10 +693,6 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
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
.user_pins(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?;
@ -648,7 +721,7 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
&ctx,
CreateMessage::new()
.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)])
.reference_message(msg),
)
@ -803,7 +876,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
.single::<ScoreListStyle>()
.unwrap_or(ScoreListStyle::Grid);
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?;
@ -816,7 +889,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
&ctx,
format!(
"Here are the scores by `{}` on `{}`!",
&user,
&user.username,
bm.short_link(&mods)
),
)
@ -832,16 +905,12 @@ pub(crate) async fn do_check(
env: &OsuEnv,
bm: &BeatmapWithMode,
mods: &Mods,
user: &UserID,
user: &UserHeader,
) -> Result<Vec<Score>> {
let BeatmapWithMode(b, m) = bm;
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
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(*m))
.await?
@ -871,10 +940,6 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
user,
} = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::default()).await?;
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
.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
))
.embed(
score_embed(&play, &beatmap, &content, &user)
score_embed(&play, &beatmap, &content, user)
.top_record(nth + 1)
.build(),
)
@ -945,33 +1010,18 @@ async fn get_user(
env: &OsuEnv,
msg: &Message,
mut args: Args,
mode: Mode,
mode_override: impl Into<Option<Mode>>,
) -> CommandResult {
let user = to_user_id_query(args.single::<UsernameArg>().ok(), env, msg.author.id).await?;
let osu_client = &env.client;
let meta_cache = &env.beatmaps;
let user = osu_client.user(&user, |f| f.mode(mode)).await?;
let (mode, user) = user_header_from_args(args.single::<UsernameArg>().ok(), env, msg).await?;
let mode = mode_override.into().unwrap_or(mode);
let user = env
.client
.user(&UserID::ID(user.id), |f| f.mode(mode))
.await?;
match user {
Some(u) => {
let bests = osu_client
.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,
};
let ex = UserExtras::from_user(env, &u, mode).await?;
msg.channel_id
.send_message(
&ctx,
@ -980,7 +1030,7 @@ async fn get_user(
"{}: here is the user that you requested",
msg.author
))
.embed(user_embed(u, map_length, map_age, best)),
.embed(user_embed(u, ex)),
)
.await?;
}

View file

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

View file

@ -80,6 +80,7 @@ impl User {
username: user.username.into_string(),
joined: time_to_utc(user.join_date),
country: user.country_code.to_string(),
preferred_mode: user.mode.into(),
count_300: 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

View file

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