Move to std::time::Duration

... to allow serde on all youmubot-osu structs
This commit is contained in:
Natsu Kagami 2020-01-13 20:09:12 -05:00
parent 3af0b56755
commit c7da9526e6
8 changed files with 202 additions and 88 deletions

View file

@ -10,12 +10,23 @@ use request::builders::*;
use request::*; use request::*;
use reqwest::Client as HTTPClient; use reqwest::Client as HTTPClient;
use serenity::framework::standard::CommandError as Error; use serenity::framework::standard::CommandError as Error;
use std::convert::TryInto;
/// Client is the client that will perform calls to the osu! api server. /// Client is the client that will perform calls to the osu! api server.
pub struct Client { pub struct Client {
key: String, key: String,
} }
fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> {
let mut res = Vec::with_capacity(v.len());
for u in v.into_iter() {
res.push(u.try_into()?);
}
Ok(res)
}
impl Client { impl Client {
/// Create a new client from the given API key. /// Create a new client from the given API key.
pub fn new(key: impl AsRef<str>) -> Client { pub fn new(key: impl AsRef<str>) -> Client {
@ -42,8 +53,8 @@ impl Client {
) -> Result<Vec<Beatmap>, Error> { ) -> Result<Vec<Beatmap>, Error> {
let mut r = BeatmapRequestBuilder::new(kind); let mut r = BeatmapRequestBuilder::new(kind);
f(&mut r); f(&mut r);
let res = self.build_request(client, r.build(client))?.json()?; let res: Vec<raw::Beatmap> = self.build_request(client, r.build(client))?.json()?;
Ok(res) Ok(vec_try_into(res)?)
} }
pub fn user( pub fn user(
@ -54,7 +65,8 @@ impl Client {
) -> Result<Option<User>, Error> { ) -> Result<Option<User>, Error> {
let mut r = UserRequestBuilder::new(user); let mut r = UserRequestBuilder::new(user);
f(&mut r); f(&mut r);
let res: Vec<_> = self.build_request(client, r.build(client))?.json()?; let res: Vec<raw::User> = self.build_request(client, r.build(client))?.json()?;
let res = vec_try_into(res)?;
Ok(res.into_iter().next()) Ok(res.into_iter().next())
} }
@ -66,7 +78,8 @@ impl Client {
) -> Result<Vec<Score>, Error> { ) -> Result<Vec<Score>, Error> {
let mut r = ScoreRequestBuilder::new(beatmap_id); let mut r = ScoreRequestBuilder::new(beatmap_id);
f(&mut r); f(&mut r);
let mut res: Vec<Score> = self.build_request(client, r.build(client))?.json()?; let res: Vec<raw::Score> = self.build_request(client, r.build(client))?.json()?;
let mut res: Vec<Score> = vec_try_into(res)?;
// with a scores request you need to fill the beatmap ids yourself // with a scores request you need to fill the beatmap ids yourself
res.iter_mut().for_each(|v| { res.iter_mut().for_each(|v| {
@ -102,7 +115,8 @@ impl Client {
) -> Result<Vec<Score>, Error> { ) -> Result<Vec<Score>, Error> {
let mut r = UserScoreRequestBuilder::new(u, user); let mut r = UserScoreRequestBuilder::new(u, user);
f(&mut r); f(&mut r);
let res = self.build_request(client, r.build(client))?.json()?; let res: Vec<raw::Score> = self.build_request(client, r.build(client))?.json()?;
let res = vec_try_into(res)?;
Ok(res) Ok(res)
} }
} }

View file

@ -1,13 +1,15 @@
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use std::time::Duration;
pub mod deser;
pub mod mods; pub mod mods;
pub mod parse;
pub(crate) mod raw; pub(crate) mod raw;
pub use mods::Mods; pub use mods::Mods;
#[derive(Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum ApprovalStatus { pub enum ApprovalStatus {
Loved, Loved,
Qualified, Qualified,
@ -28,7 +30,7 @@ impl fmt::Display for ApprovalStatus {
} }
} }
#[derive(Debug)] #[derive(Debug, Deserialize, Serialize)]
pub struct Difficulty { pub struct Difficulty {
pub stars: f64, pub stars: f64,
pub aim: Option<f64>, pub aim: Option<f64>,
@ -45,7 +47,7 @@ pub struct Difficulty {
pub max_combo: Option<u64>, pub max_combo: Option<u64>,
} }
#[derive(Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub enum Genre { pub enum Genre {
Any, Any,
Unspecified, Unspecified,
@ -70,7 +72,7 @@ impl fmt::Display for Genre {
} }
} }
#[derive(Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Language { pub enum Language {
Any, Any,
Other, Other,
@ -92,7 +94,7 @@ impl fmt::Display for Language {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Mode { pub enum Mode {
Std, Std,
Taiko, Taiko,
@ -116,7 +118,7 @@ impl fmt::Display for Mode {
} }
} }
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize)]
pub struct Beatmap { pub struct Beatmap {
// Beatmapset info // Beatmapset info
pub approval: ApprovalStatus, pub approval: ApprovalStatus,
@ -169,7 +171,7 @@ impl Beatmap {
} }
} }
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize)]
pub struct UserEvent { pub struct UserEvent {
pub display_html: String, pub display_html: String,
pub beatmap_id: u64, pub beatmap_id: u64,
@ -178,7 +180,7 @@ pub struct UserEvent {
pub epic_factor: u8, pub epic_factor: u8,
} }
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize)]
pub struct User { pub struct User {
pub id: u64, pub id: u64,
pub username: String, pub username: String,
@ -216,7 +218,7 @@ impl User {
} }
} }
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum Rank { pub enum Rank {
SS, SS,
SSH, SSH,
@ -253,7 +255,7 @@ impl fmt::Display for Rank {
} }
} }
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize)]
pub struct Score { pub struct Score {
pub id: Option<u64>, // No id if you fail pub id: Option<u64>, // No id if you fail
pub user_id: u64, pub user_id: u64,

View file

@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
bitflags::bitflags! { bitflags::bitflags! {
/// The mods available to osu! /// The mods available to osu!
#[derive(std::default::Default)] #[derive(std::default::Default, Serialize, Deserialize)]
pub struct Mods: u64 { pub struct Mods: u64 {
const NOMOD = 0; const NOMOD = 0;
const NF = 1 << 0; const NF = 1 << 0;

View file

@ -1,17 +1,43 @@
use super::*; use super::*;
use chrono::{ use chrono::{
format::{parse, Item, Numeric, Pad, Parsed}, format::{parse, Item, Numeric, Pad, Parsed},
DateTime, Duration, Utc, DateTime, ParseError as ChronoParseError, Utc,
}; };
use serde::{de, Deserialize, Deserializer}; use std::convert::TryFrom;
use std::str::FromStr; use std::time::Duration;
use std::{error::Error, fmt, str::FromStr};
impl<'de> Deserialize<'de> for Score { /// Errors that can be identified from parsing.
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> #[derive(Debug)]
where pub enum ParseError {
D: Deserializer<'de>, InvalidValue { field: &'static str, value: String },
{ FromStr(String),
let raw: raw::Score = raw::Score::deserialize(deserializer)?; NoApprovalDate,
DateParseError(ChronoParseError),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ParseError::*;
match self {
InvalidValue {
ref field,
ref value,
} => write!(f, "Invalid value `{}` for {}", value, field),
FromStr(ref s) => write!(f, "Invalid value `{}` parsing from string", s),
NoApprovalDate => write!(f, "Approval date expected but not found"),
DateParseError(ref r) => write!(f, "Error parsing date: {}", r),
}
}
}
impl Error for ParseError {}
type ParseResult<T> = Result<T, ParseError>;
impl TryFrom<raw::Score> for Score {
type Error = ParseError;
fn try_from(raw: raw::Score) -> Result<Self, Self::Error> {
Ok(Score { Ok(Score {
id: raw.score_id.map(parse_from_str).transpose()?, id: raw.score_id.map(parse_from_str).transpose()?,
user_id: parse_from_str(&raw.user_id)?, user_id: parse_from_str(&raw.user_id)?,
@ -41,12 +67,9 @@ impl<'de> Deserialize<'de> for Score {
} }
} }
impl<'de> Deserialize<'de> for User { impl TryFrom<raw::User> for User {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> type Error = ParseError;
where fn try_from(raw: raw::User) -> Result<Self, Self::Error> {
D: Deserializer<'de>,
{
let raw: raw::User = raw::User::deserialize(deserializer)?;
Ok(User { Ok(User {
id: parse_from_str(&raw.user_id)?, id: parse_from_str(&raw.user_id)?,
username: raw.username, username: raw.username,
@ -80,12 +103,9 @@ impl<'de> Deserialize<'de> for User {
} }
} }
impl<'de> Deserialize<'de> for Beatmap { impl TryFrom<raw::Beatmap> for Beatmap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> type Error = ParseError;
where fn try_from(raw: raw::Beatmap) -> Result<Self, Self::Error> {
D: Deserializer<'de>,
{
let raw: raw::Beatmap = raw::Beatmap::deserialize(deserializer)?;
Ok(Beatmap { Ok(Beatmap {
approval: parse_approval_status(&raw)?, approval: parse_approval_status(&raw)?,
submit_date: parse_date(&raw.submit_date)?, submit_date: parse_date(&raw.submit_date)?,
@ -129,7 +149,7 @@ impl<'de> Deserialize<'de> for Beatmap {
} }
} }
fn parse_user_event<E: de::Error>(s: raw::UserEvent) -> Result<UserEvent, E> { fn parse_user_event(s: raw::UserEvent) -> ParseResult<UserEvent> {
Ok(UserEvent { Ok(UserEvent {
display_html: s.display_html, display_html: s.display_html,
beatmap_id: parse_from_str(&s.beatmap_id)?, beatmap_id: parse_from_str(&s.beatmap_id)?,
@ -139,7 +159,7 @@ fn parse_user_event<E: de::Error>(s: raw::UserEvent) -> Result<UserEvent, E> {
}) })
} }
fn parse_mode<E: de::Error>(s: impl AsRef<str>) -> Result<Mode, E> { fn parse_mode(s: impl AsRef<str>) -> ParseResult<Mode> {
let t: u8 = parse_from_str(s)?; let t: u8 = parse_from_str(s)?;
use Mode::*; use Mode::*;
Ok(match t { Ok(match t {
@ -147,11 +167,16 @@ fn parse_mode<E: de::Error>(s: impl AsRef<str>) -> Result<Mode, E> {
1 => Taiko, 1 => Taiko,
2 => Catch, 2 => Catch,
3 => Mania, 3 => Mania,
_ => return Err(E::custom(format!("invalid value {} for mode", t))), _ => {
return Err(ParseError::InvalidValue {
field: "mode",
value: t.to_string(),
})
}
}) })
} }
fn parse_language<E: de::Error>(s: impl AsRef<str>) -> Result<Language, E> { fn parse_language(s: impl AsRef<str>) -> ParseResult<Language> {
let t: u8 = parse_from_str(s)?; let t: u8 = parse_from_str(s)?;
use Language::*; use Language::*;
Ok(match t { Ok(match t {
@ -167,11 +192,16 @@ fn parse_language<E: de::Error>(s: impl AsRef<str>) -> Result<Language, E> {
9 => Swedish, 9 => Swedish,
10 => Spanish, 10 => Spanish,
11 => Italian, 11 => Italian,
_ => return Err(E::custom(format!("invalid value {} for language", t))), _ => {
return Err(ParseError::InvalidValue {
field: "langugae",
value: t.to_string(),
})
}
}) })
} }
fn parse_genre<E: de::Error>(s: impl AsRef<str>) -> Result<Genre, E> { fn parse_genre(s: impl AsRef<str>) -> ParseResult<Genre> {
let t: u8 = parse_from_str(s)?; let t: u8 = parse_from_str(s)?;
use Genre::*; use Genre::*;
Ok(match t { Ok(match t {
@ -185,45 +215,57 @@ fn parse_genre<E: de::Error>(s: impl AsRef<str>) -> Result<Genre, E> {
7 => Novelty, 7 => Novelty,
9 => HipHop, 9 => HipHop,
10 => Electronic, 10 => Electronic,
_ => return Err(E::custom(format!("invalid value {} for genre", t))), _ => {
return Err(ParseError::InvalidValue {
field: "genre",
value: t.to_string(),
})
}
}) })
} }
fn parse_duration<E: de::Error>(s: impl AsRef<str>) -> Result<Duration, E> { fn parse_duration(s: impl AsRef<str>) -> ParseResult<Duration> {
Ok(Duration::seconds(parse_from_str(s)?)) Ok(Duration::from_secs(parse_from_str(s)?))
} }
fn parse_from_str<T: FromStr, E: de::Error>(s: impl AsRef<str>) -> Result<T, E> { fn parse_from_str<T: FromStr>(s: impl AsRef<str>) -> ParseResult<T> {
T::from_str(s.as_ref()).map_err(|_| E::custom(format!("invalid value {}", s.as_ref()))) let v = s.as_ref();
T::from_str(v).map_err(|_| ParseError::FromStr(v.to_owned()))
} }
fn parse_bool<E: de::Error>(b: impl AsRef<str>) -> Result<bool, E> { fn parse_bool(b: impl AsRef<str>) -> ParseResult<bool> {
match b.as_ref() { match b.as_ref() {
"1" => Ok(true), "1" => Ok(true),
"0" => Ok(false), "0" => Ok(false),
_ => Err(E::custom("invalid value for bool")), t => Err(ParseError::InvalidValue {
field: "bool",
value: t.to_owned(),
}),
} }
} }
fn parse_approval_status<E: de::Error>(b: &raw::Beatmap) -> Result<ApprovalStatus, E> { fn parse_approval_status(b: &raw::Beatmap) -> ParseResult<ApprovalStatus> {
use ApprovalStatus::*; use ApprovalStatus::*;
Ok(match &b.approved[..] { Ok(match &b.approved[..] {
"4" => Loved, "4" => Loved,
"3" => Qualified, "3" => Qualified,
"2" => Approved, "2" => Approved,
"1" => Ranked(parse_date( "1" => Ranked(parse_date(
b.approved_date b.approved_date.as_ref().ok_or(ParseError::NoApprovalDate)?,
.as_ref()
.ok_or(E::custom("expected approved date got none"))?,
)?), )?),
"0" => Pending, "0" => Pending,
"-1" => WIP, "-1" => WIP,
"-2" => Graveyarded, "-2" => Graveyarded,
_ => return Err(E::custom("invalid value for approval status")), t => {
return Err(ParseError::InvalidValue {
field: "approval status",
value: t.to_owned(),
})
}
}) })
} }
fn parse_date<E: de::Error>(date: impl AsRef<str>) -> Result<DateTime<Utc>, E> { fn parse_date(date: impl AsRef<str>) -> ParseResult<DateTime<Utc>> {
let mut parsed = Parsed::new(); let mut parsed = Parsed::new();
parse( parse(
&mut parsed, &mut parsed,
@ -243,6 +285,8 @@ fn parse_date<E: de::Error>(date: impl AsRef<str>) -> Result<DateTime<Utc>, E> {
]) ])
.iter(), .iter(),
) )
.map_err(E::custom)?; .map_err(ParseError::DateParseError)?;
parsed.to_datetime_with_timezone(&Utc {}).map_err(E::custom) parsed
.to_datetime_with_timezone(&Utc {})
.map_err(ParseError::DateParseError)
} }

View file

@ -61,7 +61,7 @@ pub fn soft_ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResu
msg.reply(&ctx, format!("⛓ Soft-banning user {}.", user.tag()))?; msg.reply(&ctx, format!("⛓ Soft-banning user {}.", user.tag()))?;
} }
Some(v) => { Some(v) => {
let until = Utc::now() + v.0; let until = Utc::now() + chrono::Duration::from_std(v.0)?;
let until = server_ban let until = server_ban
.periodical_bans .periodical_bans
.entry(user.id) .entry(user.id)

View file

@ -1,8 +1,8 @@
pub use duration::Duration; pub use duration::Duration;
mod duration { mod duration {
use chrono::Duration as StdDuration;
use std::fmt; use std::fmt;
use std::time::Duration as StdDuration;
use String as Error; use String as Error;
// Parse a single duration unit // Parse a single duration unit
fn parse_duration_string(s: &str) -> Result<StdDuration, Error> { fn parse_duration_string(s: &str) -> Result<StdDuration, Error> {
@ -18,7 +18,7 @@ mod duration {
.try_fold( .try_fold(
ParseStep { ParseStep {
current_value: None, current_value: None,
current_duration: StdDuration::zero(), current_duration: StdDuration::from_secs(0),
}, },
|s, item| match (item, s.current_value) { |s, item| match (item, s.current_value) {
('0'..='9', v) => Ok(ParseStep { ('0'..='9', v) => Ok(ParseStep {
@ -30,13 +30,13 @@ mod duration {
current_value: None, current_value: None,
current_duration: s.current_duration current_duration: s.current_duration
+ match item.to_ascii_lowercase() { + match item.to_ascii_lowercase() {
's' => StdDuration::seconds, 's' => StdDuration::from_secs(1),
'm' => StdDuration::minutes, 'm' => StdDuration::from_secs(60),
'h' => StdDuration::hours, 'h' => StdDuration::from_secs(60 * 60),
'd' => StdDuration::days, 'd' => StdDuration::from_secs(60 * 60 * 24),
'w' => StdDuration::weeks, 'w' => StdDuration::from_secs(60 * 60 * 24 * 7),
_ => return Err(Error::from("Not a valid duration")), _ => return Err(Error::from("Not a valid duration")),
}(v as i64), } * (v as u32),
}), }),
}, },
) )
@ -65,9 +65,27 @@ mod duration {
} }
} }
impl Duration {
fn num_weeks(&self) -> u64 {
self.0.as_secs() / (60 * 60 * 24 * 7)
}
fn num_days(&self) -> u64 {
self.0.as_secs() / (60 * 60 * 24)
}
fn num_hours(&self) -> u64 {
self.0.as_secs() / (60 * 60)
}
fn num_minutes(&self) -> u64 {
self.0.as_secs() / 60
}
fn num_seconds(&self) -> u64 {
self.0.as_secs()
}
}
impl fmt::Display for Duration { impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let d = &self.0; let d = self;
// weeks // weeks
let weeks = d.num_weeks(); let weeks = d.num_weeks();
let days = d.num_days() - d.num_weeks() * 7; let days = d.num_days() - d.num_weeks() * 7;
@ -103,17 +121,17 @@ mod duration {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use chrono::Duration as StdDuration; use std::time::Duration as StdDuration;
#[test] #[test]
fn test_parse_success() { fn test_parse_success() {
let tests = [ let tests = [
( (
"2D2h1m", "2D2h1m",
StdDuration::seconds(2 * 60 * 60 * 24 + 2 * 60 * 60 + 1 * 60), StdDuration::from_secs(2 * 60 * 60 * 24 + 2 * 60 * 60 + 1 * 60),
), ),
( (
"1W2D3h4m5s", "1W2D3h4m5s",
StdDuration::seconds( StdDuration::from_secs(
1 * 7 * 24 * 60 * 60 + // 1W 1 * 7 * 24 * 60 * 60 + // 1W
2 * 24 * 60 * 60 + // 2D 2 * 24 * 60 * 60 + // 2D
3 * 60 * 60 + // 3h 3 * 60 * 60 + // 3h
@ -123,7 +141,7 @@ mod duration {
), ),
( (
"1W2D3h4m5s6W", "1W2D3h4m5s6W",
StdDuration::seconds( StdDuration::from_secs(
1 * 7 * 24 * 60 * 60 + // 1W 1 * 7 * 24 * 60 * 60 + // 1W
2 * 24 * 60 * 60 + // 2D 2 * 24 * 60 * 60 + // 2D
3 * 60 * 60 + // 3h 3 * 60 * 60 + // 3h

View file

@ -1,5 +1,4 @@
use crate::commands::args::Duration as ParseDuration; use crate::commands::args::Duration as ParseDuration;
use chrono::Duration;
use serenity::framework::standard::CommandError as Error; use serenity::framework::standard::CommandError as Error;
use serenity::prelude::*; use serenity::prelude::*;
use serenity::{ use serenity::{
@ -12,6 +11,7 @@ use serenity::{
}; };
use std::collections::HashMap as Map; use std::collections::HashMap as Map;
use std::thread; use std::thread;
use std::time::Duration;
#[command] #[command]
#[description = "🎌 Cast a poll upon everyone and ask them for opinions!"] #[description = "🎌 Cast a poll upon everyone and ask them for opinions!"]
@ -25,7 +25,7 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
let args = args.quoted(); let args = args.quoted();
let _duration = args.single::<ParseDuration>()?; let _duration = args.single::<ParseDuration>()?;
let duration = &_duration.0; let duration = &_duration.0;
if *duration < Duration::minutes(2) || *duration > Duration::days(1) { if *duration < Duration::from_secs(2 * 60) || *duration > Duration::from_secs(60 * 60 * 24) {
msg.reply(ctx, format!("😒 Invalid duration ({}). The voting time should be between **2 minutes** and **1 day**.", _duration))?; msg.reply(ctx, format!("😒 Invalid duration ({}). The voting time should be between **2 minutes** and **1 day**.", _duration))?;
return Ok(()); return Ok(());
} }
@ -95,7 +95,7 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
.try_for_each(|(v, _)| panel.react(&ctx, *v))?; .try_for_each(|(v, _)| panel.react(&ctx, *v))?;
// Start sleeping // Start sleeping
thread::sleep(duration.to_std()?); thread::sleep(*duration);
let result = collect_reactions(ctx, &panel, &choices)?; let result = collect_reactions(ctx, &panel, &choices)?;
if result.len() == 0 { if result.len() == 0 {

View file

@ -18,6 +18,7 @@ use youmubot_osu::{
Client as OsuClient, Client as OsuClient,
}; };
mod cache;
mod hook; mod hook;
pub use hook::hook; pub use hook::hook;
@ -68,6 +69,25 @@ pub fn mania(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult {
get_user(ctx, msg, args, Mode::Mania) get_user(ctx, msg, args, Mode::Mania)
} }
pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode);
impl BeatmapWithMode {
/// Whether this beatmap-with-mode is a converted beatmap.
fn is_converted(&self) -> bool {
self.0.mode != self.1
}
fn mode(&self) -> Mode {
self.1
}
}
impl AsRef<Beatmap> for BeatmapWithMode {
fn as_ref(&self) -> &Beatmap {
&self.0
}
}
#[command] #[command]
#[description = "Save the given username as your username."] #[description = "Save the given username as your username."]
#[usage = "[username or user_id]"] #[usage = "[username or user_id]"]
@ -160,8 +180,6 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
} }
}; };
dbg!((nth, &mode, &user, &args));
let reqwest = data.get::<http::HTTP>().unwrap(); let reqwest = data.get::<http::HTTP>().unwrap();
let osu: &OsuClient = data.get::<http::Osu>().unwrap(); let osu: &OsuClient = data.get::<http::Osu>().unwrap();
let user = osu let user = osu
@ -180,6 +198,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
)? )?
.into_iter() .into_iter()
.next() .next()
.map(|v| BeatmapWithMode(v, mode))
.unwrap(); .unwrap();
msg.channel_id.send_message(&ctx, |m| { msg.channel_id.send_message(&ctx, |m| {
@ -187,7 +206,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
"{}: here is the play that you requested", "{}: here is the play that you requested",
msg.author msg.author
)) ))
.embed(|m| score_embed(&recent_play, &beatmap, &user, &mode, None, m)) .embed(|m| score_embed(&recent_play, &beatmap, &user, None, m))
})?; })?;
Ok(()) Ok(())
@ -222,8 +241,10 @@ fn get_user(ctx: &mut Context, msg: &Message, args: Args, mode: Mode) -> Command
.into_iter() .into_iter()
.next() .next()
.map(|m| { .map(|m| {
osu.beatmaps(reqwest, BeatmapRequestKind::Beatmap(m.beatmap_id), |f| f) osu.beatmaps(reqwest, BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
.map(|map| (m, map.into_iter().next().unwrap())) f.mode(mode, true)
})
.map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode)))
}) })
.transpose()?; .transpose()?;
msg.channel_id.send_message(&ctx, |m| { msg.channel_id.send_message(&ctx, |m| {
@ -241,13 +262,14 @@ fn get_user(ctx: &mut Context, msg: &Message, args: Args, mode: Mode) -> Command
fn score_embed<'a>( fn score_embed<'a>(
s: &Score, s: &Score,
b: &Beatmap, bm: &BeatmapWithMode,
u: &User, u: &User,
mode: &Mode,
top_record: Option<u8>, top_record: Option<u8>,
m: &'a mut CreateEmbed, m: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed { ) -> &'a mut CreateEmbed {
let accuracy = s.accuracy(*mode); let mode = bm.mode();
let b = &bm.0;
let accuracy = s.accuracy(mode);
let score_line = match &s.rank { let score_line = match &s.rank {
Rank::SS | Rank::SSH => format!("SS"), Rank::SS | Rank::SSH => format!("SS"),
_ if s.perfect => format!("{:2}% FC", accuracy), _ if s.perfect => format!("{:2}% FC", accuracy),
@ -293,7 +315,12 @@ fn score_embed<'a>(
.push_bold(format!("{:.2}", b.difficulty.stars)) .push_bold(format!("{:.2}", b.difficulty.stars))
.push(", ") .push(", ")
.push_bold_line( .push_bold_line(
b.mode.to_string() + if b.mode == *mode { "" } else { " (Converted)" }, b.mode.to_string()
+ if bm.is_converted() {
""
} else {
" (Converted)"
},
) )
.push("CS") .push("CS")
.push_bold(format!("{:.1}", b.difficulty.cs)) .push_bold(format!("{:.1}", b.difficulty.cs))
@ -313,7 +340,7 @@ fn score_embed<'a>(
fn user_embed<'a>( fn user_embed<'a>(
u: User, u: User,
best: Option<(Score, Beatmap)>, best: Option<(Score, BeatmapWithMode)>,
m: &'a mut CreateEmbed, m: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed { ) -> &'a mut CreateEmbed {
m.title(u.username) m.title(u.username)
@ -356,6 +383,7 @@ fn user_embed<'a>(
false, false,
) )
.fields(best.map(|(v, map)| { .fields(best.map(|(v, map)| {
let map = map.0;
( (
"Best Record", "Best Record",
MessageBuilder::new() MessageBuilder::new()
@ -364,7 +392,14 @@ fn user_embed<'a>(
v.pp.unwrap() /*Top record should have pp*/ v.pp.unwrap() /*Top record should have pp*/
)) ))
.push(" - ") .push(" - ")
.push_line(format!("{:.1} ago", Duration(Utc::now() - v.date))) .push_line(format!(
"{:.1} ago",
Duration(
(Utc::now() - v.date)
.to_std()
.unwrap_or(std::time::Duration::from_secs(1))
)
))
.push("on ") .push("on ")
.push(format!( .push(format!(
"[{} - {}]({})", "[{} - {}]({})",