Implement a score hook

This commit is contained in:
Natsu Kagami 2024-06-19 21:12:15 +02:00 committed by Natsu Kagami
parent 7a98dc21a9
commit 7f15a4698a
3 changed files with 92 additions and 10 deletions

View file

@ -2,6 +2,7 @@ use super::BeatmapWithMode;
use crate::{ use crate::{
discord::oppai_cache::{Accuracy, BeatmapContent, BeatmapInfo, BeatmapInfoWithPP}, discord::oppai_cache::{Accuracy, BeatmapContent, BeatmapInfo, BeatmapInfoWithPP},
models::{Beatmap, Difficulty, Mode, Mods, Rank, Score, User}, models::{Beatmap, Difficulty, Mode, Mods, Rank, Score, User},
UserHeader,
}; };
use serenity::{ use serenity::{
all::CreateAttachment, all::CreateAttachment,
@ -243,7 +244,7 @@ pub(crate) struct ScoreEmbedBuilder<'a> {
s: &'a Score, s: &'a Score,
bm: &'a BeatmapWithMode, bm: &'a BeatmapWithMode,
content: &'a BeatmapContent, content: &'a BeatmapContent,
u: &'a User, u: UserHeader,
top_record: Option<u8>, top_record: Option<u8>,
world_record: Option<u16>, world_record: Option<u16>,
footer: Option<String>, footer: Option<String>,
@ -268,13 +269,13 @@ pub(crate) fn score_embed<'a>(
s: &'a Score, s: &'a Score,
bm: &'a BeatmapWithMode, bm: &'a BeatmapWithMode,
content: &'a BeatmapContent, content: &'a BeatmapContent,
u: &'a User, u: impl Into<UserHeader>,
) -> ScoreEmbedBuilder<'a> { ) -> ScoreEmbedBuilder<'a> {
ScoreEmbedBuilder { ScoreEmbedBuilder {
s, s,
bm, bm,
content, content,
u, u: u.into(),
top_record: None, top_record: None,
world_record: None, world_record: None,
footer: None, footer: None,
@ -288,7 +289,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
let b = &self.bm.0; let b = &self.bm.0;
let s = self.s; let s = self.s;
let content = self.content; let content = self.content;
let u = self.u; let u = &self.u;
let accuracy = s.accuracy(mode); let accuracy = s.accuracy(mode);
let info = content.get_info_with(mode, s.mods).ok(); let info = content.get_info_with(mode, s.mods).ok();
let stars = info let stars = info

View file

@ -1,15 +1,22 @@
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use futures_util::stream::FuturesOrdered;
use itertools::Itertools;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use pagination::paginate_from_fn; use pagination::paginate_from_fn;
use regex::Regex; use regex::Regex;
use serenity::all::EditMessage; use serenity::{
use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder}; all::{EditMessage, EMBED_MAX_COUNT},
builder::CreateMessage,
model::channel::Message,
utils::MessageBuilder,
};
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::discord::OsuEnv; use crate::discord::embeds::score_embed;
use crate::discord::{BeatmapWithMode, OsuEnv};
use crate::{ use crate::{
discord::oppai_cache::BeatmapInfoWithPP, discord::oppai_cache::BeatmapInfoWithPP,
models::{Beatmap, Mode, Mods}, models::{Beatmap, Mode, Mods},
@ -18,6 +25,7 @@ use crate::{
use super::embeds::beatmap_embed; use super::embeds::beatmap_embed;
lazy_static! { lazy_static! {
// Beatmap(set) hooks
pub(crate) static ref OLD_LINK_REGEX: Regex = Regex::new( pub(crate) 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]+))?" r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>\d))?(?:\+(?P<mods>[A-Z]+))?"
).unwrap(); ).unwrap();
@ -27,8 +35,81 @@ lazy_static! {
pub(crate) static ref SHORT_LINK_REGEX: Regex = Regex::new( pub(crate) static ref SHORT_LINK_REGEX: Regex = Regex::new(
r"(?:^|\s|\W)(?P<main>/b/(?P<id>\d+)(?:/(?P<mode>osu|taiko|fruits|mania))?(?:\+(?P<mods>[A-Z]+))?)" r"(?:^|\s|\W)(?P<main>/b/(?P<id>\d+)(?:/(?P<mode>osu|taiko|fruits|mania))?(?:\+(?P<mods>[A-Z]+))?)"
).unwrap(); ).unwrap();
// Score hook
pub(crate) static ref SCORE_LINK_REGEX: Regex = Regex::new(
r"(?:https?://)?osu\.ppy\.sh/scores/(?P<score_id>\d+)"
).unwrap();
} }
/// React to /scores/{id} links.
pub fn score_hook<'a>(
ctx: &'a Context,
msg: &'a Message,
) -> std::pin::Pin<Box<dyn future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
if msg.author.bot {
return Ok(());
}
let env = {
let data = ctx.data.read().await;
data.get::<OsuEnv>().unwrap().clone()
};
let scores = SCORE_LINK_REGEX
.captures_iter(&msg.content)
.filter_map(|caps| caps.name("score_id"))
.filter_map(|score_id| score_id.as_str().parse::<u64>().ok())
.map(|id| env.client.score(id))
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.filter_map(|score| score.pls_ok().flatten());
let embed_chunks = scores
.map(|score| async move {
let env = {
let data = ctx.data.read().await;
data.get::<OsuEnv>().unwrap().clone()
};
let bm = env
.beatmaps
.get_beatmap(score.beatmap_id, score.mode)
.await?;
let content = env.oppai.get_beatmap(score.beatmap_id).await?;
let header = env.client.user_header(score.user_id).await?.unwrap();
Ok(score_embed(&score, &BeatmapWithMode(bm, score.mode), &content, header).build())
as Result<_>
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.filter_map(|v| v.pls_ok())
.chunks(EMBED_MAX_COUNT)
.into_iter()
.map(|chunk| chunk.collect::<Vec<_>>())
.collect::<Vec<_>>();
for embeds in embed_chunks {
msg.channel_id
.send_message(
&ctx,
CreateMessage::new()
.reference_message(msg)
.content("Here are the scores mentioned in the message!")
.embeds(embeds),
)
.await
.pls_ok();
}
Ok(())
})
}
/// React to .osz and .osu uploads.
pub fn dot_osu_hook<'a>( pub fn dot_osu_hook<'a>(
ctx: &'a Context, ctx: &'a Context,
msg: &'a Message, msg: &'a Message,

View file

@ -16,7 +16,7 @@ use serenity::{
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserBests}; use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserBests};
use embeds::{beatmap_embed, score_embed, user_embed}; use embeds::{beatmap_embed, score_embed, user_embed};
use hook::SHORT_LINK_REGEX; use hook::SHORT_LINK_REGEX;
pub use hook::{dot_osu_hook, hook}; pub use hook::{dot_osu_hook, hook, score_hook};
use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND}; use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND};
use youmubot_prelude::announcer::AnnouncerHandler; use youmubot_prelude::announcer::AnnouncerHandler;
use youmubot_prelude::{stream::FuturesUnordered, *}; use youmubot_prelude::{stream::FuturesUnordered, *};
@ -44,7 +44,7 @@ mod server_rank;
pub(crate) struct OsuClient; pub(crate) struct OsuClient;
impl TypeMapKey for OsuClient { impl TypeMapKey for OsuClient {
type Value = Arc<OsuHttpClient>; type Value = Arc<crate::Client>;
} }
/// The environment for osu! app commands. /// The environment for osu! app commands.
@ -56,7 +56,7 @@ pub struct OsuEnv {
pub(crate) last_beatmaps: OsuLastBeatmap, pub(crate) last_beatmaps: OsuLastBeatmap,
pub(crate) user_bests: OsuUserBests, pub(crate) user_bests: OsuUserBests,
// clients // clients
pub(crate) client: Arc<OsuHttpClient>, pub(crate) client: Arc<crate::Client>,
pub(crate) oppai: BeatmapCache, pub(crate) oppai: BeatmapCache,
pub(crate) beatmaps: BeatmapMetaCache, pub(crate) beatmaps: BeatmapMetaCache,
} }