mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +00:00
Move to std::time::Duration
... to allow serde on all youmubot-osu structs
This commit is contained in:
parent
3af0b56755
commit
c7da9526e6
8 changed files with 202 additions and 88 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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!(
|
||||||
"[{} - {}]({})",
|
"[{} - {}]({})",
|
||||||
|
|
Loading…
Add table
Reference in a new issue