Parsing and printing the basic info with old links

This commit is contained in:
Natsu Kagami 2019-12-24 14:42:56 -05:00
parent 0cc9b318ad
commit e37ae361fd
13 changed files with 422 additions and 54 deletions

25
Cargo.lock generated
View file

@ -5,6 +5,14 @@ name = "adler32"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "aho-corasick"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "anyhow"
version = "1.0.25"
@ -1000,7 +1008,10 @@ name = "regex"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1323,6 +1334,14 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thread_local"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "threadpool"
version = "1.7.1"
@ -1802,12 +1821,15 @@ version = "0.1.0"
dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)",
"rustbreak 2.0.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"serenity 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"youmubot-osu 0.1.0",
]
[[package]]
@ -1817,11 +1839,13 @@ dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
"serenity 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[metadata]
"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
"checksum anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9267dff192e68f3399525901e709a48c1d3982c9c072fa32f2127a0cb0babf14"
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea"
@ -1972,6 +1996,7 @@ dependencies = [
"checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238"
"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
"checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6"

View file

@ -11,3 +11,6 @@ serenity = "0.7"
chrono = "0.4.10"
reqwest = "0.9.24"
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
serde_json = "1"

View file

@ -2,6 +2,9 @@ pub mod models;
pub mod request;
#[cfg(test)]
mod test;
use models::*;
use request::builders::*;
use request::*;
@ -25,13 +28,11 @@ impl Client {
&self,
client: &HTTPClient,
kind: BeatmapRequestKind,
f: impl FnOnce(BeatmapRequestBuilder) -> BeatmapRequestBuilder,
f: impl FnOnce(&mut BeatmapRequestBuilder) -> &mut BeatmapRequestBuilder,
) -> Result<Vec<Beatmap>, Error> {
let res = f(BeatmapRequestBuilder::new(kind))
.build(client)
.query(&[("k", &self.key)])
.send()?
.json()?;
let mut r = BeatmapRequestBuilder::new(kind);
f(&mut r);
let res = r.build(client).query(&[("k", &self.key)]).send()?.json()?;
Ok(res)
}
}

View file

@ -25,15 +25,15 @@ impl<'de> Deserialize<'de> for Beatmap {
bpm: parse_from_str(&raw.bpm)?,
creator: raw.creator,
creator_id: parse_from_str(&raw.creator_id)?,
source: raw.source,
source: raw.source.filter(|v| !v.is_empty()),
genre: parse_genre(&raw.genre_id)?,
language: parse_language(&raw.language_id)?,
tags: raw.tags.split_whitespace().map(|v| v.to_owned()).collect(),
difficulty_name: raw.version,
difficulty: Difficulty {
stars: parse_from_str(&raw.difficultyrating)?,
aim: parse_from_str(&raw.diff_aim)?,
speed: parse_from_str(&raw.diff_speed)?,
aim: raw.diff_aim.map(parse_from_str).transpose()?,
speed: raw.diff_speed.map(parse_from_str).transpose()?,
cs: parse_from_str(&raw.diff_size)?,
od: parse_from_str(&raw.diff_overall)?,
ar: parse_from_str(&raw.diff_approach)?,
@ -41,7 +41,7 @@ impl<'de> Deserialize<'de> for Beatmap {
count_normal: parse_from_str(&raw.count_normal)?,
count_slider: parse_from_str(&raw.count_slider)?,
count_spinner: parse_from_str(&raw.count_spinner)?,
max_combo: parse_from_str(&raw.max_combo)?,
max_combo: raw.max_combo.map(parse_from_str).transpose()?,
},
drain_length: parse_duration(&raw.hit_length)?,
total_length: parse_duration(&raw.total_length)?,
@ -61,8 +61,8 @@ fn parse_mode<E: de::Error>(s: impl AsRef<str>) -> Result<Mode, E> {
Ok(match t {
0 => Std,
1 => Taiko,
2 => Mania,
3 => Catch,
2 => Catch,
3 => Mania,
_ => return Err(E::custom(format!("invalid value {} for mode", t))),
})
}
@ -83,7 +83,7 @@ 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(E::custom(format!("invalid value {} for language", t))),
})
}
@ -101,7 +101,7 @@ 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(E::custom(format!("invalid value {} for genre", t))),
})
}
@ -110,14 +110,14 @@ fn parse_duration<E: de::Error>(s: impl AsRef<str>) -> Result<Duration, E> {
}
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())))
T::from_str(s.as_ref()).map_err(|_| E::custom(format!("invalid value {}", s.as_ref())))
}
fn parse_bool<E: de::Error>(b: impl AsRef<str>) -> Result<bool, E> {
match b.as_ref() {
"1" => Ok(true),
"0" => Ok(false),
_ => Err(E::custom("Invalid value for bool")),
_ => Err(E::custom("invalid value for bool")),
}
}
@ -127,11 +127,15 @@ fn parse_approval_status<E: de::Error>(b: &raw::Beatmap) -> Result<ApprovalStatu
"4" => Loved,
"3" => Qualified,
"2" => Approved,
"1" => Ranked(parse_date(&b.approved_date)?),
"1" => Ranked(parse_date(
b.approved_date
.as_ref()
.ok_or(E::custom("expected approved date got none"))?,
)?),
"0" => Pending,
"-1" => WIP,
"-2" => Graveyarded,
_ => return Err(E::custom("Invalid value for approval status")),
_ => return Err(E::custom("invalid value for approval status")),
})
}
@ -142,15 +146,15 @@ fn parse_date<E: de::Error>(date: impl AsRef<str>) -> Result<DateTime<Utc>, E> {
date.as_ref(),
(&[
Item::Numeric(Numeric::Year, Pad::Zero),
Item::Literal("/"),
Item::Literal("-"),
Item::Numeric(Numeric::Month, Pad::Zero),
Item::Literal("/"),
Item::Literal("-"),
Item::Numeric(Numeric::Day, Pad::Zero),
Item::Space(""),
Item::Numeric(Numeric::Hour, Pad::Zero),
Item::Literal("-"),
Item::Literal(":"),
Item::Numeric(Numeric::Minute, Pad::Zero),
Item::Literal("-"),
Item::Literal(":"),
Item::Numeric(Numeric::Second, Pad::Zero),
])
.iter(),

View file

@ -18,8 +18,8 @@ pub enum ApprovalStatus {
#[derive(Debug)]
pub struct Difficulty {
pub stars: f64,
pub aim: f64,
pub speed: f64,
pub aim: Option<f64>,
pub speed: Option<f64>,
pub cs: f64,
pub od: f64,
@ -29,7 +29,7 @@ pub struct Difficulty {
pub count_normal: u64,
pub count_slider: u64,
pub count_spinner: u64,
pub max_combo: u64,
pub max_combo: Option<u64>,
}
#[derive(Debug)]
@ -61,17 +61,27 @@ pub enum Language {
Spanish,
Italian,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mode {
Std,
Taiko,
Mania,
Catch,
Mania,
}
impl ToString for Mode {
fn to_string(&self) -> String {
(*self as u64).to_string()
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use Mode::*;
write!(
f,
"{}",
match self {
Std => "osu!",
Taiko => "osu!taiko",
Mania => "osu!mania",
Catch => "osu!catch",
}
)
}
}

View file

@ -1,10 +1,10 @@
use serde::Deserialize;
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
pub(crate) struct Beatmap {
pub approved: String,
pub submit_date: String,
pub approved_date: String,
pub approved_date: Option<String>,
pub last_update: String,
pub artist: String,
pub beatmap_id: String,
@ -13,8 +13,8 @@ pub(crate) struct Beatmap {
pub creator: String,
pub creator_id: String,
pub difficultyrating: String,
pub diff_aim: String,
pub diff_speed: String,
pub diff_aim: Option<String>,
pub diff_speed: Option<String>,
pub diff_size: String,
pub diff_overall: String,
pub diff_approach: String,
@ -36,7 +36,7 @@ pub(crate) struct Beatmap {
pub count_normal: String,
pub count_slider: String,
pub count_spinner: String,
pub max_combo: String,
pub max_combo: Option<String>,
pub download_unavailable: String,
pub audio_unavailable: String,
}

View file

@ -15,9 +15,12 @@ impl<T: ToQuery> ToQuery for Option<T> {
}
}
impl ToQuery for Mode {
impl ToQuery for (Mode, bool) {
fn to_query(&self) -> Vec<(&'static str, String)> {
vec![("m", (*self as u8).to_string())]
vec![
("m", (self.0 as u8).to_string()),
("a", (self.1 as u8).to_string()),
]
}
}
@ -74,27 +77,24 @@ pub mod builders {
pub struct BeatmapRequestBuilder {
kind: BeatmapRequestKind,
since: Option<DateTime<Utc>>,
mode: Mode,
converted: bool,
mode: Option<(Mode, /* Converted */ bool)>,
}
impl BeatmapRequestBuilder {
pub(crate) fn new(kind: BeatmapRequestKind) -> Self {
BeatmapRequestBuilder {
kind,
since: None,
mode: Mode::Std,
converted: false,
mode: None,
}
}
pub fn since(&mut self, since: DateTime<Utc>) -> &Self {
pub fn since(&mut self, since: DateTime<Utc>) -> &mut Self {
self.since = Some(since);
self
}
pub fn mode(&mut self, mode: Mode, converted: bool) -> &Self {
self.mode = mode;
self.converted = converted;
pub fn mode(&mut self, mode: Mode, converted: bool) -> &mut Self {
self.mode = Some((mode, converted));
self
}
@ -104,14 +104,6 @@ pub mod builders {
.query(&self.kind.to_query())
.query(&self.since.map(|v| ("since", v)).to_query())
.query(&self.mode.to_query())
.query(
&(if self.converted {
Some(("a", "1".to_owned()))
} else {
None
})
.to_query(),
)
}
}
}

9
youmubot-osu/src/test.rs Normal file

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,9 @@ chrono = "0.4.9"
rand = "0.7.2"
static_assertions = "1.1.0"
reqwest = "0.9.24"
regex = "1"
lazy_static = "1"
youmubot-osu = { path = "../youmubot-osu" }
[dependencies.rustbreak]
version = "2.0.0-rc3"

View file

@ -12,6 +12,7 @@ mod args;
pub mod admin;
pub mod community;
pub mod fun;
pub mod osu;
pub use admin::ADMIN_GROUP;
pub use community::COMMUNITY_GROUP;

View file

@ -0,0 +1,303 @@
use crate::commands::args::Duration;
use crate::http;
use lazy_static::lazy_static;
use regex::Regex;
use serenity::{
builder::CreateEmbed,
framework::standard::{macros::group, CommandResult},
model::{channel::Message, id::ChannelId},
prelude::*,
utils::MessageBuilder,
};
use youmubot_osu::{
models::{Beatmap, Mode},
request::BeatmapRequestKind,
};
group!({
name: "osu",
options: {
prefix: "osu",
description: "osu! related commands.",
},
commands: [],
});
lazy_static! {
static ref OLD_LINK_REGEX: Regex = Regex::new(
r"https?://osu\.ppy\.sh/(?P<link_type>s|b)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>\d))?(?:\+(?P<mods>[A-Z]+))?"
).unwrap();
static ref NEW_LINK_REGEX: Regex = Regex::new(
r"https?://osu\.ppy\.sh/beatmapsets/(?P<set_id>\d+)/?(?:\#(?P<mode>osu|taiko|fruits|mania)(?:/(?P<beatmap_id>\d+)|/?))?(?:\+(?P<mods>[A-Z]+))?"
).unwrap();
}
pub fn hook(ctx: &mut Context, msg: &Message) -> () {
Some(msg)
.filter(|&m| !m.author.bot) // Don't react to bot messages
.map(|m| {
if let Err(v) = handle_old_links(ctx, &m.content, m.channel_id) {
println!("Error on old link handling: {:?}", v);
}
});
}
fn handle_old_links(ctx: &mut Context, content: impl AsRef<str>, ch: ChannelId) -> CommandResult {
let data = ctx.data.write();
let reqwest = data.get::<http::HTTP>().unwrap();
let osu = data.get::<http::Osu>().unwrap();
for capture in OLD_LINK_REGEX.captures_iter(content.as_ref()) {
let req_type = capture.name("link_type").unwrap().as_str();
let req = match req_type {
"b" => BeatmapRequestKind::Beatmap(capture["id"].parse()?),
"s" => BeatmapRequestKind::Beatmapset(capture["id"].parse()?),
_ => continue,
};
let mode = capture
.name("mode")
.map(|v| v.as_str().parse())
.transpose()?
.and_then(|v| {
Some(match v {
0 => Mode::Std,
1 => Mode::Taiko,
2 => Mode::Catch,
3 => Mode::Mania,
_ => return None,
})
});
let mut beatmaps = osu.beatmaps(reqwest, req, |v| match mode {
Some(m) => v.mode(m, true),
None => v,
})?;
match req_type {
"b" => {
for beatmap in beatmaps.iter() {
if let Err(v) = ch.send_message(&ctx, |m| {
m.content(
MessageBuilder::new()
.push("Beatmap information for ")
.push_mono_safe(&capture[0])
.build(),
)
.embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), b))
}) {
println!("Error on printing beatmap {}: {:?}", beatmap.beatmap_id, v);
}
}
}
"s" => {
beatmaps.sort_by(|a, b| {
(mode.unwrap_or(a.mode) as u8, a.difficulty.stars)
.partial_cmp(&(mode.unwrap_or(b.mode) as u8, b.difficulty.stars))
.unwrap()
});
ch.send_message(&ctx, |m| {
m.content(
MessageBuilder::new()
.push("Beatmapset information for ")
.push_mono_safe(&capture[0])
.build(),
)
.embed(|b| beatmapset_embed(&beatmaps, mode, b))
})?;
}
_ => (),
}
}
Ok(())
}
const NEW_MODE_NAMES: [&'static str; 4] = ["osu", "taiko", "fruits", "mania"];
fn format_mode(actual: Mode, original: Mode) -> String {
if actual == original {
format!("{}", actual)
} else {
format!("{} (converted)", actual)
}
}
fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a mut CreateEmbed {
c.title(
MessageBuilder::new()
.push_bold_safe(&b.artist)
.push(" - ")
.push_bold_safe(&b.title)
.push(" [")
.push_bold_safe(&b.difficulty_name)
.push("]")
.build(),
)
.author(|a| {
a.name(&b.creator)
.url(format!("https://osu.ppy.sh/users/{}", b.creator_id))
.icon_url(format!("https://a.ppy.sh/{}", b.creator_id))
})
.url(format!(
"https://osu.ppy.sh/beatmapsets/{}/#{}/{}",
b.beatmapset_id, NEW_MODE_NAMES[b.mode as usize], b.beatmap_id
))
.thumbnail(format!("https://b.ppy.sh/thumb/{}l.jpg", b.beatmapset_id))
.image(format!(
"https://assets.ppy.sh/beatmaps/{}/covers/cover.jpg",
b.beatmapset_id
))
.color(0xffb6c1)
.field(
"Star Difficulty",
format!("{:.2}", b.difficulty.stars),
false,
)
.field(
"Length",
MessageBuilder::new()
.push_bold_safe(Duration(b.total_length))
.push(" (")
.push_bold_safe(Duration(b.drain_length))
.push(" drain)")
.build(),
false,
)
.field("Circle Size", format!("{:.1}", b.difficulty.cs), true)
.field("Approach Rate", format!("{:.1}", b.difficulty.ar), true)
.field(
"Overall Difficulty",
format!("{:.1}", b.difficulty.od),
true,
)
.field("HP Drain", format!("{:.1}", b.difficulty.hp), true)
.field("BPM", b.bpm.round(), true)
.fields(b.difficulty.max_combo.map(|v| ("Max combo", v, true)))
.field("Mode", format_mode(m, b.mode), true)
.fields(b.source.as_ref().map(|v| ("Source", v, true)))
.field(
"Tags",
b.tags
.iter()
.map(|v| MessageBuilder::new().push_mono_safe(v).build())
.take(10)
.chain(std::iter::once("...".to_owned()))
.collect::<Vec<_>>()
.join(" "),
false,
)
.description(
MessageBuilder::new()
.push_line({
let link = format!("https://osu.ppy.sh/beatmapsets/{}/download", b.beatmap_id);
format!(
"Download: [[Link]]({}) [[No Video]]({}?noVideo=1)",
link, link
)
})
.push_line(format!(
"Beatmapset: https://osu.ppy.sh/beatmapsets/{}/#{}",
b.beatmapset_id, NEW_MODE_NAMES[b.mode as usize],
))
.build(),
)
}
const MAX_DIFFS: usize = 25 - 4;
fn beatmapset_embed<'a>(
bs: &'_ [Beatmap],
m: Option<Mode>,
c: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed {
let too_many_diffs = bs.len() > MAX_DIFFS;
let b: &Beatmap = &bs[0];
c.title(
MessageBuilder::new()
.push_bold_safe(&b.artist)
.push(" - ")
.push_bold_safe(&b.title)
.build(),
)
.author(|a| {
a.name(&b.creator)
.url(format!("https://osu.ppy.sh/users/{}", b.creator_id))
.icon_url(format!("https://a.ppy.sh/{}", b.creator_id))
})
.url(format!(
"https://osu.ppy.sh/beatmapsets/{}",
b.beatmapset_id,
))
// .thumbnail(format!("https://b.ppy.sh/thumb/{}l.jpg", b.beatmapset_id))
.image(format!(
"https://assets.ppy.sh/beatmaps/{}/covers/cover.jpg",
b.beatmapset_id
))
.color(0xffb6c1)
.description(
MessageBuilder::new()
.push_line({
let link = format!("https://osu.ppy.sh/beatmapsets/{}/download", b.beatmap_id);
format!(
"Download: [[Link]]({}) [[No Video]]({}?noVideo=1)",
link, link
)
})
.build(),
)
.field(
"Length",
MessageBuilder::new()
.push_bold_safe(Duration(b.total_length))
.build(),
true,
)
.field("BPM", b.bpm.round(), true)
.fields(b.source.as_ref().map(|v| ("Source", v, false)))
.field(
"Tags",
b.tags
.iter()
.map(|v| MessageBuilder::new().push_mono_safe(v).build())
.take(10)
.chain(std::iter::once("...".to_owned()))
.collect::<Vec<_>>()
.join(" "),
false,
)
.footer(|f| {
if too_many_diffs {
f.text(format!(
"This map has {} diffs, we are showing the first {}.",
bs.len(),
MAX_DIFFS
))
} else {
f
}
})
.fields(bs.iter().take(MAX_DIFFS).map(|b: &Beatmap| {
(
format!("[{}]", b.difficulty_name),
MessageBuilder::new()
.push(format!(
"[[Link]](https://osu.ppy.sh/beatmapsets/{}/#{}/{})",
b.beatmapset_id,
NEW_MODE_NAMES[m.unwrap_or(b.mode) as usize],
b.beatmap_id
))
.push(", ")
.push_bold(format!("{:.2}", b.difficulty.stars))
.push(", ")
.push_bold_line(format_mode(m.unwrap_or(b.mode), b.mode))
.push("CS")
.push_bold(format!("{:.1}", b.difficulty.cs))
.push(", AR")
.push_bold(format!("{:.1}", b.difficulty.ar))
.push(", OD")
.push_bold(format!("{:.1}", b.difficulty.od))
.push(", HP")
.push_bold(format!("{:.1}", b.difficulty.hp))
.push(", ⌛ ")
.push_bold(format!("{}", Duration(b.drain_length)))
.build(),
false,
)
}))
}

View file

@ -1,7 +1,14 @@
use serenity::prelude::TypeMapKey;
use youmubot_osu::Client as OsuClient;
pub(crate) struct HTTP;
impl TypeMapKey for HTTP {
type Value = reqwest::Client;
}
pub(crate) struct Osu;
impl TypeMapKey for Osu {
type Value = OsuClient;
}

View file

@ -3,20 +3,27 @@ use dotenv::var;
use reqwest;
use serenity::{
framework::standard::{DispatchError, StandardFramework},
model::gateway,
model::{channel::Message, gateway},
prelude::*,
};
use youmubot_osu::Client as OsuClient;
mod commands;
mod db;
mod http;
const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [commands::osu::hook];
struct Handler;
impl EventHandler for Handler {
fn ready(&self, _: Context, ready: gateway::Ready) {
println!("{} is connected!", ready.user.name);
}
fn message(&self, mut ctx: Context, message: Message) {
MESSAGE_HOOKS.iter().for_each(|f| f(&mut ctx, &message));
}
}
fn main() {
@ -39,6 +46,9 @@ fn main() {
{
let mut data = client.data.write();
data.insert::<http::HTTP>(reqwest::Client::new());
data.insert::<http::Osu>(OsuClient::new(
var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
));
}
// Create handler threads