mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58: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::{
|
use crate::{
|
||||||
discord::beatmap_cache::BeatmapMetaCache,
|
discord::beatmap_cache::BeatmapMetaCache,
|
||||||
discord::cache::save_beatmap,
|
discord::cache::save_beatmap,
|
||||||
discord::oppai_cache::BeatmapCache,
|
discord::oppai_cache::{BeatmapCache, BeatmapContent},
|
||||||
models::{Mode, Score},
|
models::{Mode, Score, User, UserEventRank},
|
||||||
request::UserID,
|
request::UserID,
|
||||||
Client as Osu,
|
Client as Osu,
|
||||||
};
|
};
|
||||||
use announcer::MemberToChannels;
|
use announcer::MemberToChannels;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
http::CacheHttp,
|
http::CacheHttp,
|
||||||
model::id::{ChannelId, UserId},
|
model::{
|
||||||
|
channel::Message,
|
||||||
|
id::{ChannelId, UserId},
|
||||||
|
},
|
||||||
CacheAndHttp,
|
CacheAndHttp,
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
@ -22,12 +25,14 @@ pub const ANNOUNCER_KEY: &'static str = "osu";
|
||||||
|
|
||||||
/// The announcer struct implementing youmubot_prelude::Announcer
|
/// The announcer struct implementing youmubot_prelude::Announcer
|
||||||
pub struct Announcer {
|
pub struct Announcer {
|
||||||
client: Osu,
|
client: Arc<Osu>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Announcer {
|
impl Announcer {
|
||||||
pub fn new(client: Osu) -> Self {
|
pub fn new(client: Osu) -> Self {
|
||||||
Self { client }
|
Self {
|
||||||
|
client: Arc::new(client),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,65 +111,56 @@ impl Announcer {
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
d: AppData,
|
d: AppData,
|
||||||
) -> Result<Option<f64>, Error> {
|
) -> 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, user) = {
|
||||||
let scores = self.scan_user(osu_user, mode).await?;
|
let scores = self.scan_user(osu_user, mode).await?;
|
||||||
let user = self
|
let user = self
|
||||||
.client
|
.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?
|
.await?
|
||||||
.ok_or(Error::msg("user not found"))?;
|
.ok_or(Error::msg("user not found"))?;
|
||||||
(scores, user)
|
(scores, user)
|
||||||
};
|
};
|
||||||
|
let client = self.client.clone();
|
||||||
let pp = user.pp;
|
let pp = user.pp;
|
||||||
spawn_future(async move {
|
spawn_future(async move {
|
||||||
scores
|
let event_scores = user
|
||||||
.into_iter()
|
.events
|
||||||
.map(|(rank, score)| {
|
.iter()
|
||||||
let d = d.clone();
|
.filter_map(|u| u.to_event_rank())
|
||||||
async move {
|
.filter(|u| u.mode == mode && u.date > last_update)
|
||||||
let data = d.read().await;
|
.map(|ev| CollectedScore::from_event(&*client, &user, ev, user_id, &channels[..]))
|
||||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
.filter_map(|u| future::ready(u.ok_or_print()))
|
||||||
let beatmap = cache.get_beatmap_default(score.beatmap_id).await?;
|
.collect::<Vec<_>>()
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
.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)
|
Ok(pp)
|
||||||
}
|
}
|
||||||
|
@ -183,3 +179,147 @@ impl Announcer {
|
||||||
Ok(scores)
|
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>(
|
pub(crate) struct ScoreEmbedBuilder<'a> {
|
||||||
s: &Score,
|
s: &'a Score,
|
||||||
bm: &BeatmapWithMode,
|
bm: &'a BeatmapWithMode,
|
||||||
content: &BeatmapContent,
|
content: &'a BeatmapContent,
|
||||||
u: &User,
|
u: &'a User,
|
||||||
top_record: Option<u8>,
|
top_record: Option<u8>,
|
||||||
m: &'a mut CreateEmbed,
|
world_record: Option<u16>,
|
||||||
) -> &'a mut CreateEmbed {
|
}
|
||||||
let mode = bm.mode();
|
|
||||||
let b = &bm.0;
|
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 accuracy = s.accuracy(mode);
|
||||||
let stars = mode
|
let stars = mode
|
||||||
.to_oppai_mode()
|
.to_oppai_mode()
|
||||||
|
@ -220,7 +253,7 @@ pub(crate) fn score_embed<'a>(
|
||||||
pp.map(|v| v.1)
|
pp.map(|v| v.1)
|
||||||
};
|
};
|
||||||
let pp_gained = s.pp.map(|full_pp| {
|
let pp_gained = s.pp.map(|full_pp| {
|
||||||
top_record
|
self.top_record
|
||||||
.map(|top| {
|
.map(|top| {
|
||||||
let after_pp = u.pp.unwrap();
|
let after_pp = u.pp.unwrap();
|
||||||
let effective_pp = full_pp * (0.95f64).powi(top as i32 - 1);
|
let effective_pp = full_pp * (0.95f64).powi(top as i32 - 1);
|
||||||
|
@ -240,14 +273,19 @@ pub(crate) fn score_embed<'a>(
|
||||||
.max_combo
|
.max_combo
|
||||||
.map(|max| format!("**{}x**/{}x", s.max_combo, max))
|
.map(|max| format!("**{}x**/{}x", s.max_combo, max))
|
||||||
.unwrap_or_else(|| format!("**{}x**", s.max_combo));
|
.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))
|
.map(|v| format!("| #{} top record!", v))
|
||||||
.unwrap_or("".to_owned());
|
.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));
|
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()))
|
m.author(|f| f.name(&u.username).url(u.link()).icon_url(u.avatar_url()))
|
||||||
.color(0xffb6c1)
|
.color(0xffb6c1)
|
||||||
.title(format!(
|
.title(format!(
|
||||||
"{} | {} - {} [{}] {} ({:.2}\\*) by {} | {} {}",
|
"{} | {} - {} [{}] {} ({:.2}\\*) by {} | {} {} {}",
|
||||||
u.username,
|
u.username,
|
||||||
b.artist,
|
b.artist,
|
||||||
b.title,
|
b.title,
|
||||||
|
@ -256,7 +294,8 @@ pub(crate) fn score_embed<'a>(
|
||||||
stars,
|
stars,
|
||||||
b.creator,
|
b.creator,
|
||||||
score_line,
|
score_line,
|
||||||
top_record
|
top_record,
|
||||||
|
world_record,
|
||||||
))
|
))
|
||||||
.description(format!(
|
.description(format!(
|
||||||
r#"**Beatmap**: {} - {} [{}]**{} **
|
r#"**Beatmap**: {} - {} [{}]**{} **
|
||||||
|
@ -299,6 +338,7 @@ pub(crate) fn score_embed<'a>(
|
||||||
}
|
}
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn user_embed<'a>(
|
pub(crate) fn user_embed<'a>(
|
||||||
u: User,
|
u: User,
|
||||||
|
|
|
@ -23,6 +23,7 @@ mod db;
|
||||||
pub(crate) mod embeds;
|
pub(crate) mod embeds;
|
||||||
mod hook;
|
mod hook;
|
||||||
pub(crate) mod oppai_cache;
|
pub(crate) mod oppai_cache;
|
||||||
|
mod register_user;
|
||||||
mod server_rank;
|
mod server_rank;
|
||||||
|
|
||||||
use db::OsuUser;
|
use db::OsuUser;
|
||||||
|
@ -95,6 +96,7 @@ pub fn setup(
|
||||||
catch,
|
catch,
|
||||||
mania,
|
mania,
|
||||||
save,
|
save,
|
||||||
|
forcesave,
|
||||||
recent,
|
recent,
|
||||||
last,
|
last,
|
||||||
check,
|
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?;
|
let user: Option<User> = osu.user(UserID::Auto(user), |f| f).await?;
|
||||||
match user {
|
match user {
|
||||||
Some(u) => {
|
Some(u) => {
|
||||||
OsuSavedUsers::open(&*data).borrow_mut()?.insert(
|
let check_beatmap_id = register_user::user_register_beatmap_id(&u);
|
||||||
msg.author.id,
|
let check = osu
|
||||||
OsuUser {
|
.user_recent(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(1))
|
||||||
id: u.id,
|
.await?
|
||||||
last_update: chrono::Utc::now(),
|
.into_iter()
|
||||||
pp: vec![],
|
.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(
|
msg.reply(
|
||||||
&ctx,
|
&ctx,
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
|
@ -192,6 +212,55 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
Ok(())
|
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);
|
struct ModeArg(Mode);
|
||||||
|
|
||||||
impl FromStr for ModeArg {
|
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",
|
"{}: here is the play that you requested",
|
||||||
msg.author
|
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?;
|
.await?;
|
||||||
|
|
||||||
|
@ -537,7 +606,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
for score in scores.iter() {
|
for score in scores.iter() {
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx, |c| {
|
.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?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -601,7 +670,11 @@ pub async fn top(ctx: &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(&top_play, &beatmap, &content, &user, Some(rank), m))
|
.embed(|m| {
|
||||||
|
score_embed(&top_play, &beatmap, &content, &user)
|
||||||
|
.top_record(rank)
|
||||||
|
.build(m)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.await?;
|
.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, *};
|
use youmubot_prelude::{ratelimit::Ratelimit, *};
|
||||||
|
|
||||||
/// The number of requests per minute to the osu! server.
|
/// 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.
|
/// Client is the client that will perform calls to the osu! api server.
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -11,6 +12,10 @@ pub(crate) mod raw;
|
||||||
pub use mods::Mods;
|
pub use mods::Mods;
|
||||||
use serenity::utils::MessageBuilder;
|
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)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub enum ApprovalStatus {
|
pub enum ApprovalStatus {
|
||||||
Loved,
|
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.
|
/// Parse from the new site's convention.
|
||||||
pub fn parse_from_new_site(s: &str) -> Option<Self> {
|
pub fn parse_from_new_site(s: &str) -> Option<Self> {
|
||||||
Some(match s {
|
Some(match s {
|
||||||
|
@ -377,6 +393,30 @@ pub struct UserEvent {
|
||||||
pub epic_factor: u8,
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
|
Loading…
Add table
Reference in a new issue