mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-20 01:08:55 +00:00
Improve osu (#10)
* Add rank event parsing * Refactor osu announcer * Increase api limits * Register user is locked
This commit is contained in:
parent
145cb01bd0
commit
f05afb2b80
6 changed files with 509 additions and 197 deletions
|
@ -3,15 +3,18 @@ use super::{embeds::score_embed, BeatmapWithMode};
|
|||
use crate::{
|
||||
discord::beatmap_cache::BeatmapMetaCache,
|
||||
discord::cache::save_beatmap,
|
||||
discord::oppai_cache::BeatmapCache,
|
||||
models::{Mode, Score},
|
||||
discord::oppai_cache::{BeatmapCache, BeatmapContent},
|
||||
models::{Mode, Score, User, UserEventRank},
|
||||
request::UserID,
|
||||
Client as Osu,
|
||||
};
|
||||
use announcer::MemberToChannels;
|
||||
use serenity::{
|
||||
http::CacheHttp,
|
||||
model::id::{ChannelId, UserId},
|
||||
model::{
|
||||
channel::Message,
|
||||
id::{ChannelId, UserId},
|
||||
},
|
||||
CacheAndHttp,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
@ -22,12 +25,14 @@ pub const ANNOUNCER_KEY: &'static str = "osu";
|
|||
|
||||
/// The announcer struct implementing youmubot_prelude::Announcer
|
||||
pub struct Announcer {
|
||||
client: Osu,
|
||||
client: Arc<Osu>,
|
||||
}
|
||||
|
||||
impl Announcer {
|
||||
pub fn new(client: Osu) -> Self {
|
||||
Self { client }
|
||||
Self {
|
||||
client: Arc::new(client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,65 +111,56 @@ impl Announcer {
|
|||
mode: Mode,
|
||||
d: AppData,
|
||||
) -> Result<Option<f64>, Error> {
|
||||
let days_since_last_update = (chrono::Utc::now() - osu_user.last_update).num_days() + 1;
|
||||
let last_update = osu_user.last_update.clone();
|
||||
let (scores, user) = {
|
||||
let scores = self.scan_user(osu_user, mode).await?;
|
||||
let user = self
|
||||
.client
|
||||
.user(UserID::ID(osu_user.id), |f| f.mode(mode))
|
||||
.user(UserID::ID(osu_user.id), |f| {
|
||||
f.mode(mode)
|
||||
.event_days(days_since_last_update.min(31) as u8)
|
||||
})
|
||||
.await?
|
||||
.ok_or(Error::msg("user not found"))?;
|
||||
(scores, user)
|
||||
};
|
||||
let client = self.client.clone();
|
||||
let pp = user.pp;
|
||||
spawn_future(async move {
|
||||
scores
|
||||
.into_iter()
|
||||
.map(|(rank, score)| {
|
||||
let d = d.clone();
|
||||
async move {
|
||||
let data = d.read().await;
|
||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
let beatmap = cache.get_beatmap_default(score.beatmap_id).await?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let r: Result<_> =
|
||||
Ok((rank, score, BeatmapWithMode(beatmap, mode), content));
|
||||
r
|
||||
}
|
||||
})
|
||||
.collect::<stream::FuturesOrdered<_>>()
|
||||
.filter_map(|v| future::ready(v.ok()))
|
||||
.for_each(move |(rank, score, beatmap, content)| {
|
||||
let channels = channels.clone();
|
||||
let d = d.clone();
|
||||
let c = c.clone();
|
||||
let user = user.clone();
|
||||
async move {
|
||||
let data = d.read().await;
|
||||
for channel in (&channels).iter() {
|
||||
if let Err(e) = channel
|
||||
.send_message(c.http(), |c| {
|
||||
c.content(format!("New top record from {}!", user_id.mention()))
|
||||
.embed(|e| {
|
||||
score_embed(
|
||||
&score,
|
||||
&beatmap,
|
||||
&content,
|
||||
&user,
|
||||
Some(rank),
|
||||
e,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
{
|
||||
dbg!(e);
|
||||
}
|
||||
save_beatmap(&*data, *channel, &beatmap).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
let event_scores = user
|
||||
.events
|
||||
.iter()
|
||||
.filter_map(|u| u.to_event_rank())
|
||||
.filter(|u| u.mode == mode && u.date > last_update)
|
||||
.map(|ev| CollectedScore::from_event(&*client, &user, ev, user_id, &channels[..]))
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.filter_map(|u| future::ready(u.ok_or_print()))
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let top_scores = scores.into_iter().filter_map(|(rank, score)| {
|
||||
if score.date > last_update {
|
||||
Some(CollectedScore::from_top_score(
|
||||
&user,
|
||||
score,
|
||||
mode,
|
||||
rank,
|
||||
user_id,
|
||||
&channels[..],
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let ctx = Context { data: d, c };
|
||||
event_scores
|
||||
.into_iter()
|
||||
.chain(top_scores)
|
||||
.map(|v| v.send_message(&ctx))
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.ok_or_print();
|
||||
});
|
||||
Ok(pp)
|
||||
}
|
||||
|
@ -183,3 +179,147 @@ impl Announcer {
|
|||
Ok(scores)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Context {
|
||||
data: AppData,
|
||||
c: Arc<CacheAndHttp>,
|
||||
}
|
||||
|
||||
struct CollectedScore<'a> {
|
||||
pub user: &'a User,
|
||||
pub score: Score,
|
||||
pub mode: Mode,
|
||||
pub kind: ScoreType,
|
||||
|
||||
pub discord_user: UserId,
|
||||
pub channels: &'a [ChannelId],
|
||||
}
|
||||
|
||||
impl<'a> CollectedScore<'a> {
|
||||
fn from_top_score(
|
||||
user: &'a User,
|
||||
score: Score,
|
||||
mode: Mode,
|
||||
rank: u8,
|
||||
discord_user: UserId,
|
||||
channels: &'a [ChannelId],
|
||||
) -> Self {
|
||||
Self {
|
||||
user,
|
||||
score,
|
||||
mode,
|
||||
kind: ScoreType::TopRecord(rank),
|
||||
discord_user,
|
||||
channels,
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_event(
|
||||
osu: &Osu,
|
||||
user: &'a User,
|
||||
event: UserEventRank,
|
||||
discord_user: UserId,
|
||||
channels: &'a [ChannelId],
|
||||
) -> Result<CollectedScore<'a>> {
|
||||
let scores = osu
|
||||
.scores(event.beatmap_id, |f| {
|
||||
f.user(UserID::ID(user.id)).mode(event.mode)
|
||||
})
|
||||
.await?;
|
||||
let score = match scores.into_iter().next() {
|
||||
Some(v) => v,
|
||||
None => return Err(Error::msg("cannot get score for map...")),
|
||||
};
|
||||
Ok(Self {
|
||||
user,
|
||||
score,
|
||||
mode: event.mode,
|
||||
kind: ScoreType::WorldRecord(event.rank),
|
||||
discord_user,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CollectedScore<'a> {
|
||||
async fn send_message(self, ctx: &Context) -> Result<Vec<Message>> {
|
||||
let (bm, content) = self.get_beatmap(&ctx).await?;
|
||||
self.channels
|
||||
.into_iter()
|
||||
.map(|c| self.send_message_to(*c, ctx, &bm, &content))
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.try_collect()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_beatmap(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
) -> Result<(
|
||||
BeatmapWithMode,
|
||||
impl std::ops::Deref<Target = BeatmapContent>,
|
||||
)> {
|
||||
let data = ctx.data.read().await;
|
||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
let beatmap = cache.get_beatmap_default(self.score.beatmap_id).await?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
Ok((BeatmapWithMode(beatmap, self.mode), content))
|
||||
}
|
||||
|
||||
async fn send_message_to(
|
||||
&self,
|
||||
channel: ChannelId,
|
||||
ctx: &Context,
|
||||
bm: &BeatmapWithMode,
|
||||
content: &BeatmapContent,
|
||||
) -> Result<Message> {
|
||||
let m = channel
|
||||
.send_message(ctx.c.http(), |c| {
|
||||
c.content(match self.kind {
|
||||
ScoreType::TopRecord(_) => {
|
||||
format!("New top record from {}!", self.discord_user.mention())
|
||||
}
|
||||
ScoreType::WorldRecord(_) => {
|
||||
format!("New best score from {}!", self.discord_user.mention())
|
||||
}
|
||||
})
|
||||
.embed(|e| {
|
||||
let mut b = score_embed(&self.score, &bm, content, self.user);
|
||||
match self.kind {
|
||||
ScoreType::TopRecord(rank) => b.top_record(rank),
|
||||
ScoreType::WorldRecord(rank) => b.world_record(rank),
|
||||
}
|
||||
.build(e)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
save_beatmap(&*ctx.data.read().await, channel, &bm).ok_or_print();
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
|
||||
enum ScoreType {
|
||||
TopRecord(u8),
|
||||
WorldRecord(u16),
|
||||
}
|
||||
|
||||
trait OkPrint {
|
||||
type Output;
|
||||
fn ok_or_print(self) -> Option<Self::Output>;
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Debug> OkPrint for Result<T, E> {
|
||||
type Output = T;
|
||||
|
||||
fn ok_or_print(self) -> Option<Self::Output> {
|
||||
match self {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
eprintln!("Error: {:?}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,16 +160,49 @@ pub fn beatmapset_embed<'a>(
|
|||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn score_embed<'a>(
|
||||
s: &Score,
|
||||
bm: &BeatmapWithMode,
|
||||
content: &BeatmapContent,
|
||||
u: &User,
|
||||
pub(crate) struct ScoreEmbedBuilder<'a> {
|
||||
s: &'a Score,
|
||||
bm: &'a BeatmapWithMode,
|
||||
content: &'a BeatmapContent,
|
||||
u: &'a User,
|
||||
top_record: Option<u8>,
|
||||
m: &'a mut CreateEmbed,
|
||||
) -> &'a mut CreateEmbed {
|
||||
let mode = bm.mode();
|
||||
let b = &bm.0;
|
||||
world_record: Option<u16>,
|
||||
}
|
||||
|
||||
impl<'a> ScoreEmbedBuilder<'a> {
|
||||
pub fn top_record(&mut self, rank: u8) -> &mut Self {
|
||||
self.top_record = Some(rank);
|
||||
self
|
||||
}
|
||||
pub fn world_record(&mut self, rank: u16) -> &mut Self {
|
||||
self.world_record = Some(rank);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn score_embed<'a>(
|
||||
s: &'a Score,
|
||||
bm: &'a BeatmapWithMode,
|
||||
content: &'a BeatmapContent,
|
||||
u: &'a User,
|
||||
) -> ScoreEmbedBuilder<'a> {
|
||||
ScoreEmbedBuilder {
|
||||
s,
|
||||
bm,
|
||||
content,
|
||||
u,
|
||||
top_record: None,
|
||||
world_record: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ScoreEmbedBuilder<'a> {
|
||||
pub fn build<'b>(&self, m: &'b mut CreateEmbed) -> &'b mut CreateEmbed {
|
||||
let mode = self.bm.mode();
|
||||
let b = &self.bm.0;
|
||||
let s = self.s;
|
||||
let content = self.content;
|
||||
let u = self.u;
|
||||
let accuracy = s.accuracy(mode);
|
||||
let stars = mode
|
||||
.to_oppai_mode()
|
||||
|
@ -220,7 +253,7 @@ pub(crate) fn score_embed<'a>(
|
|||
pp.map(|v| v.1)
|
||||
};
|
||||
let pp_gained = s.pp.map(|full_pp| {
|
||||
top_record
|
||||
self.top_record
|
||||
.map(|top| {
|
||||
let after_pp = u.pp.unwrap();
|
||||
let effective_pp = full_pp * (0.95f64).powi(top as i32 - 1);
|
||||
|
@ -240,14 +273,19 @@ pub(crate) fn score_embed<'a>(
|
|||
.max_combo
|
||||
.map(|max| format!("**{}x**/{}x", s.max_combo, max))
|
||||
.unwrap_or_else(|| format!("**{}x**", s.max_combo));
|
||||
let top_record = top_record
|
||||
let top_record = self
|
||||
.top_record
|
||||
.map(|v| format!("| #{} top record!", v))
|
||||
.unwrap_or("".to_owned());
|
||||
let world_record = self
|
||||
.world_record
|
||||
.map(|v| format!("| #{} on Global Rankings!", v))
|
||||
.unwrap_or("".to_owned());
|
||||
let diff = b.difficulty.apply_mods(s.mods, Some(stars));
|
||||
m.author(|f| f.name(&u.username).url(u.link()).icon_url(u.avatar_url()))
|
||||
.color(0xffb6c1)
|
||||
.title(format!(
|
||||
"{} | {} - {} [{}] {} ({:.2}\\*) by {} | {} {}",
|
||||
"{} | {} - {} [{}] {} ({:.2}\\*) by {} | {} {} {}",
|
||||
u.username,
|
||||
b.artist,
|
||||
b.title,
|
||||
|
@ -256,7 +294,8 @@ pub(crate) fn score_embed<'a>(
|
|||
stars,
|
||||
b.creator,
|
||||
score_line,
|
||||
top_record
|
||||
top_record,
|
||||
world_record,
|
||||
))
|
||||
.description(format!(
|
||||
r#"**Beatmap**: {} - {} [{}]**{} **
|
||||
|
@ -298,6 +337,7 @@ pub(crate) fn score_embed<'a>(
|
|||
m.footer(|f| f.text("Star difficulty does not reflect game mods."));
|
||||
}
|
||||
m
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn user_embed<'a>(
|
||||
|
|
|
@ -23,6 +23,7 @@ mod db;
|
|||
pub(crate) mod embeds;
|
||||
mod hook;
|
||||
pub(crate) mod oppai_cache;
|
||||
mod register_user;
|
||||
mod server_rank;
|
||||
|
||||
use db::OsuUser;
|
||||
|
@ -95,6 +96,7 @@ pub fn setup(
|
|||
catch,
|
||||
mania,
|
||||
save,
|
||||
forcesave,
|
||||
recent,
|
||||
last,
|
||||
check,
|
||||
|
@ -168,14 +170,32 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
let user: Option<User> = osu.user(UserID::Auto(user), |f| f).await?;
|
||||
match user {
|
||||
Some(u) => {
|
||||
OsuSavedUsers::open(&*data).borrow_mut()?.insert(
|
||||
msg.author.id,
|
||||
OsuUser {
|
||||
id: u.id,
|
||||
last_update: chrono::Utc::now(),
|
||||
pp: vec![],
|
||||
},
|
||||
);
|
||||
let check_beatmap_id = register_user::user_register_beatmap_id(&u);
|
||||
let check = osu
|
||||
.user_recent(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(1))
|
||||
.await?
|
||||
.into_iter()
|
||||
.take(1)
|
||||
.any(|s| s.beatmap_id == check_beatmap_id);
|
||||
if !check {
|
||||
let msg = msg.reply(&ctx, format!("To set your osu username, please make your most recent play be the following map: `/b/{}` in **osu! standard** mode! It does **not** have to be a pass.", check_beatmap_id));
|
||||
let beatmap = osu
|
||||
.beatmaps(
|
||||
crate::request::BeatmapRequestKind::Beatmap(check_beatmap_id),
|
||||
|f| f,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
msg.await?
|
||||
.edit(&ctx, |f| {
|
||||
f.embed(|e| beatmap_embed(&beatmap, Mode::Std, Mods::NOMOD, None, e))
|
||||
})
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
add_user(msg.author.id, u.id, &*data)?;
|
||||
msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
|
@ -192,6 +212,55 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[description = "Save the given username as someone's username."]
|
||||
#[owners_only]
|
||||
#[usage = "[ping user]/[username or user_id]"]
|
||||
#[num_args(2)]
|
||||
pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let target = args.single::<serenity::model::id::UserId>()?;
|
||||
|
||||
let user = args.single::<String>()?;
|
||||
let user: Option<User> = osu.user(UserID::Auto(user), |f| f).await?;
|
||||
match user {
|
||||
Some(u) => {
|
||||
add_user(target, u.id, &*data)?;
|
||||
msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
.push("user has been set to ")
|
||||
.push_mono_safe(u.username)
|
||||
.build(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
msg.reply(&ctx, "user not found...").await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_user(target: serenity::model::id::UserId, user_id: u64, data: &TypeMap) -> Result<()> {
|
||||
OsuSavedUsers::open(data).borrow_mut()?.insert(
|
||||
target,
|
||||
OsuUser {
|
||||
id: user_id,
|
||||
last_update: chrono::Utc::now(),
|
||||
pp: vec![],
|
||||
},
|
||||
);
|
||||
OsuUserBests::open(data)
|
||||
.borrow_mut()?
|
||||
.iter_mut()
|
||||
.for_each(|(_, r)| {
|
||||
r.remove(&target);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ModeArg(Mode);
|
||||
|
||||
impl FromStr for ModeArg {
|
||||
|
@ -438,7 +507,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
"{}: here is the play that you requested",
|
||||
msg.author
|
||||
))
|
||||
.embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user, None, m))
|
||||
.embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user).build(m))
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
@ -537,7 +606,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||
for score in scores.iter() {
|
||||
msg.channel_id
|
||||
.send_message(&ctx, |c| {
|
||||
c.embed(|m| score_embed(&score, &bm, &content, &user, None, m))
|
||||
c.embed(|m| score_embed(&score, &bm, &content, &user).build(m))
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
@ -601,7 +670,11 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
"{}: here is the play that you requested",
|
||||
msg.author
|
||||
))
|
||||
.embed(|m| score_embed(&top_play, &beatmap, &content, &user, Some(rank), m))
|
||||
.embed(|m| {
|
||||
score_embed(&top_play, &beatmap, &content, &user)
|
||||
.top_record(rank)
|
||||
.build(m)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
|
19
youmubot-osu/src/discord/register_user.rs
Normal file
19
youmubot-osu/src/discord/register_user.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use crate::models::User;
|
||||
|
||||
const BEATMAP_IDS: [u64; 100] = [
|
||||
2469345, 2084862, 2486881, 2330357, 2546607, 1655981, 1626537, 888015, 1062394, 1319547,
|
||||
1852572, 1944926, 2129143, 1057509, 2022718, 1097543, 1736329, 1056207, 930249, 1936782,
|
||||
1919312, 1570203, 2201460, 1495498, 965549, 2428358, 2118444, 1849433, 820619, 999944, 1571309,
|
||||
1055147, 1619555, 338682, 1438917, 954692, 824891, 2026320, 764014, 2237466, 2058788, 1969946,
|
||||
1892257, 1473301, 2336704, 774965, 657509, 1031604, 898576, 714001, 1872396, 831705, 1917082,
|
||||
978326, 795232, 1814494, 713867, 2077126, 1612329, 1314214, 1849273, 1829925, 1640362, 801158,
|
||||
431957, 1054501, 1627148, 816600, 1857519, 1080094, 1642274, 1232440, 1843653, 953586, 2044362,
|
||||
1489536, 951053, 1069111, 2154507, 1007699, 1099936, 1077323, 1874119, 909032, 760466, 1911308,
|
||||
1820921, 1231520, 954254, 425779, 1586059, 2198684, 1040044, 799913, 994933, 969681, 888016,
|
||||
1100327, 1063410, 2078961,
|
||||
];
|
||||
|
||||
pub fn user_register_beatmap_id(u: &User) -> u64 {
|
||||
let now = chrono::Utc::now();
|
||||
BEATMAP_IDS[(u.id + (now.timestamp() / 3600) as u64) as usize % BEATMAP_IDS.len()]
|
||||
}
|
|
@ -13,7 +13,7 @@ use std::convert::TryInto;
|
|||
use youmubot_prelude::{ratelimit::Ratelimit, *};
|
||||
|
||||
/// The number of requests per minute to the osu! server.
|
||||
const REQUESTS_PER_MINUTE: usize = 60;
|
||||
const REQUESTS_PER_MINUTE: usize = 100;
|
||||
|
||||
/// Client is the client that will perform calls to the osu! api server.
|
||||
pub struct Client {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
@ -11,6 +12,10 @@ pub(crate) mod raw;
|
|||
pub use mods::Mods;
|
||||
use serenity::utils::MessageBuilder;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref EVENT_RANK_REGEX: Regex = Regex::new(r#"^.+achieved rank #(\d+) on .+\((.+)\)$"#).unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
pub enum ApprovalStatus {
|
||||
Loved,
|
||||
|
@ -264,6 +269,17 @@ impl Mode {
|
|||
})
|
||||
}
|
||||
|
||||
/// Parse from the display output of the enum itself.
|
||||
pub fn parse_from_display(s: &str) -> Option<Self> {
|
||||
Some(match s {
|
||||
"osu!" => Mode::Std,
|
||||
"osu!taiko" => Mode::Taiko,
|
||||
"osu!mania" => Mode::Catch,
|
||||
"osu!catch" => Mode::Mania,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse from the new site's convention.
|
||||
pub fn parse_from_new_site(s: &str) -> Option<Self> {
|
||||
Some(match s {
|
||||
|
@ -377,6 +393,30 @@ pub struct UserEvent {
|
|||
pub epic_factor: u8,
|
||||
}
|
||||
|
||||
/// Represents a "achieved rank #x on beatmap" event.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UserEventRank {
|
||||
pub beatmap_id: u64,
|
||||
pub rank: u16,
|
||||
pub mode: Mode,
|
||||
pub date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl UserEvent {
|
||||
/// Try to parse the event into a "rank" event.
|
||||
pub fn to_event_rank(&self) -> Option<UserEventRank> {
|
||||
let captures = EVENT_RANK_REGEX.captures(self.display_html.as_str())?;
|
||||
let rank: u16 = captures.get(1)?.as_str().parse().ok()?;
|
||||
let mode: Mode = Mode::parse_from_display(captures.get(2)?.as_str())?;
|
||||
Some(UserEventRank {
|
||||
beatmap_id: self.beatmap_id?,
|
||||
date: self.date.clone(),
|
||||
mode,
|
||||
rank,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: u64,
|
||||
|
|
Loading…
Add table
Reference in a new issue