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 reqwest::Client as HTTPClient;
use serenity::framework::standard::CommandError as Error;
use std::convert::TryInto;
/// Client is the client that will perform calls to the osu! api server.
pub struct Client {
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 {
/// Create a new client from the given API key.
pub fn new(key: impl AsRef<str>) -> Client {
@ -42,8 +53,8 @@ impl Client {
) -> Result<Vec<Beatmap>, Error> {
let mut r = BeatmapRequestBuilder::new(kind);
f(&mut r);
let res = self.build_request(client, r.build(client))?.json()?;
Ok(res)
let res: Vec<raw::Beatmap> = self.build_request(client, r.build(client))?.json()?;
Ok(vec_try_into(res)?)
}
pub fn user(
@ -54,7 +65,8 @@ impl Client {
) -> Result<Option<User>, Error> {
let mut r = UserRequestBuilder::new(user);
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())
}
@ -66,7 +78,8 @@ impl Client {
) -> Result<Vec<Score>, Error> {
let mut r = ScoreRequestBuilder::new(beatmap_id);
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
res.iter_mut().for_each(|v| {
@ -102,7 +115,8 @@ impl Client {
) -> Result<Vec<Score>, Error> {
let mut r = UserScoreRequestBuilder::new(u, user);
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)
}
}

View file

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

View file

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

View file

@ -1,17 +1,43 @@
use super::*;
use chrono::{
format::{parse, Item, Numeric, Pad, Parsed},
DateTime, Duration, Utc,
DateTime, ParseError as ChronoParseError, Utc,
};
use serde::{de, Deserialize, Deserializer};
use std::str::FromStr;
use std::convert::TryFrom;
use std::time::Duration;
use std::{error::Error, fmt, str::FromStr};
impl<'de> Deserialize<'de> for Score {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw: raw::Score = raw::Score::deserialize(deserializer)?;
/// Errors that can be identified from parsing.
#[derive(Debug)]
pub enum ParseError {
InvalidValue { field: &'static str, value: String },
FromStr(String),
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 {
id: raw.score_id.map(parse_from_str).transpose()?,
user_id: parse_from_str(&raw.user_id)?,
@ -41,12 +67,9 @@ impl<'de> Deserialize<'de> for Score {
}
}
impl<'de> Deserialize<'de> for User {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw: raw::User = raw::User::deserialize(deserializer)?;
impl TryFrom<raw::User> for User {
type Error = ParseError;
fn try_from(raw: raw::User) -> Result<Self, Self::Error> {
Ok(User {
id: parse_from_str(&raw.user_id)?,
username: raw.username,
@ -80,12 +103,9 @@ impl<'de> Deserialize<'de> for User {
}
}
impl<'de> Deserialize<'de> for Beatmap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw: raw::Beatmap = raw::Beatmap::deserialize(deserializer)?;
impl TryFrom<raw::Beatmap> for Beatmap {
type Error = ParseError;
fn try_from(raw: raw::Beatmap) -> Result<Self, Self::Error> {
Ok(Beatmap {
approval: parse_approval_status(&raw)?,
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 {
display_html: s.display_html,
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)?;
use Mode::*;
Ok(match t {
@ -147,11 +167,16 @@ fn parse_mode<E: de::Error>(s: impl AsRef<str>) -> Result<Mode, E> {
1 => Taiko,
2 => Catch,
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)?;
use Language::*;
Ok(match t {
@ -167,11 +192,16 @@ fn parse_language<E: de::Error>(s: impl AsRef<str>) -> Result<Language, E> {
9 => Swedish,
10 => Spanish,
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)?;
use Genre::*;
Ok(match t {
@ -185,45 +215,57 @@ fn parse_genre<E: de::Error>(s: impl AsRef<str>) -> Result<Genre, E> {
7 => Novelty,
9 => HipHop,
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> {
Ok(Duration::seconds(parse_from_str(s)?))
fn parse_duration(s: impl AsRef<str>) -> ParseResult<Duration> {
Ok(Duration::from_secs(parse_from_str(s)?))
}
fn parse_from_str<T: FromStr, E: de::Error>(s: impl AsRef<str>) -> Result<T, E> {
T::from_str(s.as_ref()).map_err(|_| E::custom(format!("invalid value {}", s.as_ref())))
fn parse_from_str<T: FromStr>(s: impl AsRef<str>) -> ParseResult<T> {
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() {
"1" => Ok(true),
"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::*;
Ok(match &b.approved[..] {
"4" => Loved,
"3" => Qualified,
"2" => Approved,
"1" => Ranked(parse_date(
b.approved_date
.as_ref()
.ok_or(E::custom("expected approved date got none"))?,
b.approved_date.as_ref().ok_or(ParseError::NoApprovalDate)?,
)?),
"0" => Pending,
"-1" => WIP,
"-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();
parse(
&mut parsed,
@ -243,6 +285,8 @@ fn parse_date<E: de::Error>(date: impl AsRef<str>) -> Result<DateTime<Utc>, E> {
])
.iter(),
)
.map_err(E::custom)?;
parsed.to_datetime_with_timezone(&Utc {}).map_err(E::custom)
.map_err(ParseError::DateParseError)?;
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()))?;
}
Some(v) => {
let until = Utc::now() + v.0;
let until = Utc::now() + chrono::Duration::from_std(v.0)?;
let until = server_ban
.periodical_bans
.entry(user.id)

View file

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

View file

@ -1,5 +1,4 @@
use crate::commands::args::Duration as ParseDuration;
use chrono::Duration;
use serenity::framework::standard::CommandError as Error;
use serenity::prelude::*;
use serenity::{
@ -12,6 +11,7 @@ use serenity::{
};
use std::collections::HashMap as Map;
use std::thread;
use std::time::Duration;
#[command]
#[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 _duration = args.single::<ParseDuration>()?;
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))?;
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))?;
// Start sleeping
thread::sleep(duration.to_std()?);
thread::sleep(*duration);
let result = collect_reactions(ctx, &panel, &choices)?;
if result.len() == 0 {

View file

@ -18,6 +18,7 @@ use youmubot_osu::{
Client as OsuClient,
};
mod cache;
mod 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)
}
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]
#[description = "Save the given username as your username."]
#[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 osu: &OsuClient = data.get::<http::Osu>().unwrap();
let user = osu
@ -180,6 +198,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
)?
.into_iter()
.next()
.map(|v| BeatmapWithMode(v, mode))
.unwrap();
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",
msg.author
))
.embed(|m| score_embed(&recent_play, &beatmap, &user, &mode, None, m))
.embed(|m| score_embed(&recent_play, &beatmap, &user, None, m))
})?;
Ok(())
@ -222,8 +241,10 @@ fn get_user(ctx: &mut Context, msg: &Message, args: Args, mode: Mode) -> Command
.into_iter()
.next()
.map(|m| {
osu.beatmaps(reqwest, BeatmapRequestKind::Beatmap(m.beatmap_id), |f| f)
.map(|map| (m, map.into_iter().next().unwrap()))
osu.beatmaps(reqwest, BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
f.mode(mode, true)
})
.map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode)))
})
.transpose()?;
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>(
s: &Score,
b: &Beatmap,
bm: &BeatmapWithMode,
u: &User,
mode: &Mode,
top_record: Option<u8>,
m: &'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 {
Rank::SS | Rank::SSH => format!("SS"),
_ if s.perfect => format!("{:2}% FC", accuracy),
@ -293,7 +315,12 @@ fn score_embed<'a>(
.push_bold(format!("{:.2}", b.difficulty.stars))
.push(", ")
.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_bold(format!("{:.1}", b.difficulty.cs))
@ -313,7 +340,7 @@ fn score_embed<'a>(
fn user_embed<'a>(
u: User,
best: Option<(Score, Beatmap)>,
best: Option<(Score, BeatmapWithMode)>,
m: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed {
m.title(u.username)
@ -356,6 +383,7 @@ fn user_embed<'a>(
false,
)
.fields(best.map(|(v, map)| {
let map = map.0;
(
"Best Record",
MessageBuilder::new()
@ -364,7 +392,14 @@ fn user_embed<'a>(
v.pp.unwrap() /*Top record should have pp*/
))
.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(format!(
"[{} - {}]({})",