diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index 3c44a44..d349e30 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -109,7 +109,11 @@ pub fn beatmap_embed<'a>( ) }) .push_line(format!(" [[Beatmapset]]({})", b.beatmapset_link())) - .push_line(&b.approval) + .push_line(format!( + "Short link: `{}`", + b.short_link(Some(m), Some(mods)) + )) + .push_bold_line(&b.approval) .push("Language: ") .push_bold(&b.language) .push(" | Genre: ") @@ -209,7 +213,11 @@ pub fn beatmapset_embed<'a>( ( format!("[{}]", b.difficulty_name), MessageBuilder::new() - .push(format!("[[Link]]({})", b.link())) + .push(format!( + "[[Link]]({}) (`{}`)", + b.link(), + b.short_link(m, None) + )) .push(", ") .push_bold(format!("{:.2}⭐", b.difficulty.stars)) .push(", ") @@ -311,7 +319,11 @@ pub(crate) fn score_embed<'a>( .field( "Map stats", MessageBuilder::new() - .push(format!("[[Link]]({})", b.link())) + .push(format!( + "[[Link]]({}) (`{}`)", + b.link(), + b.short_link(Some(mode), Some(s.mods)) + )) .push(", ") .push_bold(format!("{:.2}⭐", stars)) .push(", ") @@ -346,7 +358,7 @@ pub(crate) fn score_embed<'a>( pub(crate) fn user_embed<'a>( u: User, - best: Option<(Score, BeatmapWithMode)>, + best: Option<(Score, BeatmapWithMode, Option)>, m: &'a mut CreateEmbed, ) -> &'a mut CreateEmbed { m.title(u.username) @@ -388,8 +400,8 @@ pub(crate) fn user_embed<'a>( ), false, ) - .fields(best.map(|(v, map)| { - let map = map.0; + .fields(best.map(|(v, map, info)| { + let BeatmapWithMode(map, mode) = map; ( "Best Record", MessageBuilder::new() @@ -413,8 +425,12 @@ pub(crate) fn user_embed<'a>( MessageBuilder::new().push_bold_safe(&map.title).build(), map.link() )) - .push(format!(" [{}]", map.difficulty_name)) - .push(format!(" ({:.1}⭐)", map.difficulty.stars)) + .push_line(format!(" [{}]", map.difficulty_name)) + .push(format!( + "{:.1}⭐ | `{}`", + info.map(|i| i.stars as f64).unwrap_or(map.difficulty.stars), + map.short_link(Some(mode), Some(v.mods)) + )) .build(), false, ) diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index 51c9ecc..4469593 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -1,5 +1,6 @@ use super::OsuClient; use crate::{ + discord::beatmap_cache::BeatmapMetaCache, discord::oppai_cache::{BeatmapCache, BeatmapInfo}, models::{Beatmap, Mode, Mods}, request::BeatmapRequestKind, @@ -24,6 +25,9 @@ lazy_static! { static ref NEW_LINK_REGEX: Regex = Regex::new( r"https?://osu\.ppy\.sh/beatmapsets/(?P\d+)/?(?:\#(?Posu|taiko|fruits|mania)(?:/(?P\d+)|/?))?(?:\+(?P[A-Z]+))?" ).unwrap(); + static ref SHORT_LINK_REGEX: Regex = Regex::new( + r"/b/(?P\d+)(?:/(?Posu|taiko|fruits|mania))?(?:\+(?P[A-Z]+))?" + ).unwrap(); } pub fn hook(ctx: &mut Context, msg: &Message) -> () { @@ -33,8 +37,13 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () { let mut v = move || -> CommandResult { let old_links = handle_old_links(ctx, &msg.content)?; let new_links = handle_new_links(ctx, &msg.content)?; + let short_links = handle_short_links(ctx, &msg, &msg.content)?; let mut last_beatmap = None; - for l in old_links.into_iter().chain(new_links.into_iter()) { + for l in old_links + .into_iter() + .chain(new_links.into_iter()) + .chain(short_links.into_iter()) + { if let Err(v) = msg.channel_id.send_message(&ctx, |m| match l.embed { EmbedType::Beatmap(b, info, mods) => { let t = handle_beatmap(&b, info, l.link, l.mode, mods, m); @@ -137,15 +146,9 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result> = Vec::new(); let cache = ctx.data.get_cloned::(); for capture in NEW_LINK_REGEX.captures_iter(content) { - let mode = capture.name("mode").and_then(|v| { - Some(match v.as_str() { - "osu" => Mode::Std, - "taiko" => Mode::Taiko, - "fruits" => Mode::Catch, - "mania" => Mode::Mania, - _ => return None, - }) - }); + let mode = capture + .name("mode") + .and_then(|v| Mode::parse_from_new_site(v.as_str())); let link = capture.get(0).unwrap().as_str(); let req = match capture.name("beatmap_id") { Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?), @@ -163,8 +166,7 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result(ctx: &mut Context, content: &'a str) -> Result( + ctx: &mut Context, + msg: &Message, + content: &'a str, +) -> Result>, Error> { + if let Some(guild_id) = msg.guild_id { + if announcer::announcer_of(ctx, crate::discord::announcer::ANNOUNCER_KEY, guild_id)? + != Some(msg.channel_id) + { + // Disable if we are not in the server's announcer channel + return Ok(vec![]); + } + } + let osu = ctx.data.get_cloned::(); + let cache = ctx.data.get_cloned::(); + Ok(SHORT_LINK_REGEX + .captures_iter(content) + .map(|capture| -> Result<_, Error> { + let mode = capture + .name("mode") + .and_then(|v| Mode::parse_from_new_site(v.as_str())); + let id: u64 = capture.name("id").unwrap().as_str().parse()?; + let beatmap = match mode { + Some(mode) => osu.get_beatmap(id, mode), + None => osu.get_beatmap_default(id), + }?; + let mods = capture + .name("mods") + .and_then(|v| Mods::from_str(v.as_str()).ok()) + .unwrap_or(Mods::NOMOD); + let info = mode + .unwrap_or(beatmap.mode) + .to_oppai_mode() + .and_then(|mode| { + cache + .get_beatmap(beatmap.beatmap_id) + .and_then(|b| b.get_info_with(Some(mode), mods)) + .ok() + }); + Ok(ToPrint { + embed: EmbedType::Beatmap(beatmap, info, mods), + link: capture.get(0).unwrap().as_str(), + mode, + }) + }) + .filter_map(|v| v.ok()) + .collect()) +} + fn handle_beatmap<'a, 'b>( beatmap: &Beatmap, info: Option, diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 131a338..fb72d97 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -272,8 +272,12 @@ fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> Command .map(|info| info.stars as f64) .unwrap_or(b.difficulty.stars); format!( - "[{:.1}*] {} - {} [{}] (#{})", - stars, b.artist, b.title, b.difficulty_name, b.beatmap_id + "[{:.1}*] {} - {} [{}] ({})", + stars, + b.artist, + b.title, + b.difficulty_name, + b.short_link(Some(mode), Some(plays[i].mods)), ) } else { "FETCH_FAILED".to_owned() @@ -554,18 +558,26 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult { let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; let osu = ctx.data.get_cloned::(); + let cache = ctx.data.get_cloned::(); let user = osu.user(user, |f| f.mode(mode))?; + let oppai = ctx.data.get_cloned::(); match user { Some(u) => { let best = osu .user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode))? .into_iter() .next() - .map(|m| { - osu.beatmaps(BeatmapRequestKind::Beatmap(m.beatmap_id), |f| { - f.mode(mode, true) - }) - .map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode))) + .map(|m| -> Result<_, Error> { + let beatmap = cache.get_beatmap(m.beatmap_id, mode)?; + let info = mode + .to_oppai_mode() + .map(|mode| -> Result<_, Error> { + Ok(oppai + .get_beatmap(m.beatmap_id)? + .get_info_with(Some(mode), m.mods)?) + }) + .transpose()?; + Ok((m, BeatmapWithMode(beatmap, mode), info)) }) .transpose()?; msg.channel_id.send_message(&ctx, |m| { diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 06c883e..c568887 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -198,6 +198,27 @@ impl Mode { _ => return None, }) } + + /// Parse from the new site's convention. + pub fn parse_from_new_site(s: &str) -> Option { + Some(match s { + "osu" => Mode::Std, + "taiko" => Mode::Taiko, + "fruits" => Mode::Catch, + "mania" => Mode::Mania, + _ => return None, + }) + } + + /// Returns the mode string in the new convention. + pub fn to_str_new_site(&self) -> &'static str { + match self { + Mode::Std => "osu", + Mode::Taiko => "taiko", + Mode::Catch => "fruits", + Mode::Mania => "mania", + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -249,6 +270,19 @@ impl Beatmap { ) } + /// Return a parsable short link. + pub fn short_link(&self, override_mode: Option, mods: Option) -> String { + format!( + "/b/{}{}{}", + self.beatmap_id, + match override_mode { + Some(mode) if mode != self.mode => format!("/{}", mode.to_str_new_site()), + _ => "".to_owned(), + }, + mods.map(|m| format!("{}", m)).unwrap_or("".to_owned()), + ) + } + /// Link to the cover image of the beatmap. pub fn cover_url(&self) -> String { format!(