mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-05-24 09:10:49 +00:00
Move to SQLite (#13)
This commit is contained in:
parent
750ddb7762
commit
1799b70bc1
50 changed files with 2122 additions and 394 deletions
|
@ -16,8 +16,10 @@ lazy_static = "1"
|
|||
regex = "1"
|
||||
oppai-rs = "0.2"
|
||||
dashmap = "4"
|
||||
bincode = "1"
|
||||
|
||||
youmubot-db = { path = "../youmubot-db" }
|
||||
youmubot-db-sql = { path = "../youmubot-db-sql" }
|
||||
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -17,11 +17,11 @@ use serenity::{
|
|||
},
|
||||
CacheAndHttp,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
/// osu! announcer's unique announcer key.
|
||||
pub const ANNOUNCER_KEY: &'static str = "osu";
|
||||
pub const ANNOUNCER_KEY: &str = "osu";
|
||||
|
||||
/// The announcer struct implementing youmubot_prelude::Announcer
|
||||
pub struct Announcer {
|
||||
|
@ -45,11 +45,14 @@ impl youmubot_prelude::Announcer for Announcer {
|
|||
channels: MemberToChannels,
|
||||
) -> Result<()> {
|
||||
// For each user...
|
||||
let data = OsuSavedUsers::open(&*d.read().await).borrow()?.clone();
|
||||
let data = d.read().await;
|
||||
let data = data.get::<OsuSavedUsers>().unwrap();
|
||||
let now = chrono::Utc::now();
|
||||
let data = data
|
||||
let users = data.all().await?;
|
||||
users
|
||||
.into_iter()
|
||||
.map(|(user_id, osu_user)| {
|
||||
.map(|mut osu_user| {
|
||||
let user_id = osu_user.user_id;
|
||||
let channels = &channels;
|
||||
let ctx = Context {
|
||||
c: c.clone(),
|
||||
|
@ -59,62 +62,33 @@ impl youmubot_prelude::Announcer for Announcer {
|
|||
async move {
|
||||
let channels = channels.channels_of(ctx.c.clone(), user_id).await;
|
||||
if channels.is_empty() {
|
||||
return (user_id, osu_user); // We don't wanna update an user without any active server
|
||||
return; // We don't wanna update an user without any active server
|
||||
}
|
||||
let pp = match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
s.handle_user_mode(&ctx, now, &osu_user, user_id, channels.clone(), *m)
|
||||
})
|
||||
.collect::<stream::FuturesOrdered<_>>()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
match std::array::IntoIter::new([
|
||||
Mode::Std,
|
||||
Mode::Taiko,
|
||||
Mode::Catch,
|
||||
Mode::Mania,
|
||||
])
|
||||
.map(|m| s.handle_user_mode(&ctx, now, &osu_user, user_id, channels.clone(), m))
|
||||
.collect::<stream::FuturesOrdered<_>>()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Ok(v) => {
|
||||
osu_user.last_update = now;
|
||||
osu_user.pp = v.try_into().unwrap();
|
||||
data.save(osu_user).await.pls_ok();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("osu: Cannot update {}: {}", osu_user.id, e);
|
||||
return (
|
||||
user_id,
|
||||
OsuUser {
|
||||
failures: Some(osu_user.failures.unwrap_or(0) + 1),
|
||||
..osu_user
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
(
|
||||
user_id,
|
||||
OsuUser {
|
||||
pp,
|
||||
last_update: now,
|
||||
failures: None,
|
||||
..osu_user
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.collect::<HashMap<_, _>>()
|
||||
.collect::<()>()
|
||||
.await;
|
||||
// Update users
|
||||
let db = &*d.read().await;
|
||||
let mut db = OsuSavedUsers::open(db);
|
||||
let mut db = db.borrow_mut()?;
|
||||
data.into_iter()
|
||||
.for_each(|(k, v)| match db.get(&k).map(|v| v.last_update.clone()) {
|
||||
Some(d) if d > now => (),
|
||||
_ => {
|
||||
if v.failures.unwrap_or(0) > 5 {
|
||||
eprintln!(
|
||||
"osu: Removing user {} [{}] due to 5 consecutive failures",
|
||||
k, v.id
|
||||
);
|
||||
// db.remove(&k);
|
||||
} else {
|
||||
db.insert(k, v);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -129,9 +103,9 @@ impl Announcer {
|
|||
user_id: UserId,
|
||||
channels: Vec<ChannelId>,
|
||||
mode: Mode,
|
||||
) -> Result<Option<f64>, Error> {
|
||||
) -> Result<Option<f32>, Error> {
|
||||
let days_since_last_update = (now - osu_user.last_update).num_days() + 1;
|
||||
let last_update = osu_user.last_update.clone();
|
||||
let last_update = osu_user.last_update;
|
||||
let (scores, user) = {
|
||||
let scores = self.scan_user(osu_user, mode).await?;
|
||||
let user = self
|
||||
|
@ -141,7 +115,7 @@ impl Announcer {
|
|||
.event_days(days_since_last_update.min(31) as u8)
|
||||
})
|
||||
.await?
|
||||
.ok_or(Error::msg("user not found"))?;
|
||||
.ok_or_else(|| Error::msg("user not found"))?;
|
||||
(scores, user)
|
||||
};
|
||||
let client = self.client.clone();
|
||||
|
@ -181,7 +155,7 @@ impl Announcer {
|
|||
.await
|
||||
.pls_ok();
|
||||
});
|
||||
Ok(pp)
|
||||
Ok(pp.map(|v| v as f32))
|
||||
}
|
||||
|
||||
async fn scan_user(&self, u: &OsuUser, mode: Mode) -> Result<Vec<(u8, Score)>, Error> {
|
||||
|
@ -265,20 +239,14 @@ 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()
|
||||
.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>,
|
||||
)> {
|
||||
async fn get_beatmap(&self, ctx: &Context) -> Result<(BeatmapWithMode, BeatmapContent)> {
|
||||
let data = ctx.data.read().await;
|
||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
|
@ -314,7 +282,9 @@ impl<'a> CollectedScore<'a> {
|
|||
})
|
||||
})
|
||||
.await?;
|
||||
save_beatmap(&*ctx.data.read().await, channel, &bm).pls_ok();
|
||||
save_beatmap(&*ctx.data.read().await, channel, &bm)
|
||||
.await
|
||||
.pls_ok();
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,15 @@ use crate::{
|
|||
models::{ApprovalStatus, Beatmap, Mode},
|
||||
Client,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
use youmubot_db_sql::{models::osu as models, Pool};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
/// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling.
|
||||
/// Does not cache non-Ranked beatmaps.
|
||||
pub struct BeatmapMetaCache {
|
||||
client: Arc<Client>,
|
||||
cache: DashMap<(u64, Mode), Beatmap>,
|
||||
beatmapsets: DashMap<u64, Vec<u64>>,
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
impl TypeMapKey for BeatmapMetaCache {
|
||||
|
@ -20,13 +19,20 @@ impl TypeMapKey for BeatmapMetaCache {
|
|||
|
||||
impl BeatmapMetaCache {
|
||||
/// Create a new beatmap cache.
|
||||
pub fn new(client: Arc<Client>) -> Self {
|
||||
BeatmapMetaCache {
|
||||
client,
|
||||
cache: DashMap::new(),
|
||||
beatmapsets: DashMap::new(),
|
||||
pub fn new(client: Arc<Client>, pool: Pool) -> Self {
|
||||
BeatmapMetaCache { client, pool }
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn to_cached_beatmap(beatmap: &Beatmap, mode: Option<Mode>) -> models::CachedBeatmap {
|
||||
models::CachedBeatmap {
|
||||
beatmap_id: beatmap.beatmap_id as i64,
|
||||
mode: mode.unwrap_or(beatmap.mode) as u8,
|
||||
cached_at: chrono::Utc::now(),
|
||||
beatmap: bincode::serialize(&beatmap).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_if_possible(&self, id: u64, mode: Option<Mode>) -> Result<Beatmap> {
|
||||
let beatmap = self
|
||||
.client
|
||||
|
@ -37,15 +43,29 @@ impl BeatmapMetaCache {
|
|||
f
|
||||
})
|
||||
.await
|
||||
.and_then(|v| v.into_iter().next().ok_or(Error::msg("beatmap not found")))?;
|
||||
.and_then(|v| {
|
||||
v.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Error::msg("beatmap not found"))
|
||||
})?;
|
||||
if let ApprovalStatus::Ranked(_) = beatmap.approval {
|
||||
self.cache.insert((id, beatmap.mode), beatmap.clone());
|
||||
let mut c = Self::to_cached_beatmap(&beatmap, mode);
|
||||
c.store(&self.pool).await.pls_ok();
|
||||
};
|
||||
Ok(beatmap)
|
||||
}
|
||||
|
||||
async fn get_beatmap_db(&self, id: u64, mode: Mode) -> Result<Option<Beatmap>> {
|
||||
Ok(
|
||||
models::CachedBeatmap::by_id(id as i64, mode as u8, &self.pool)
|
||||
.await?
|
||||
.map(|v| bincode::deserialize(&v.beatmap[..]).unwrap()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the given beatmap
|
||||
pub async fn get_beatmap(&self, id: u64, mode: Mode) -> Result<Beatmap> {
|
||||
match self.cache.get(&(id, mode)).map(|v| v.clone()) {
|
||||
match self.get_beatmap_db(id, mode).await? {
|
||||
Some(v) => Ok(v),
|
||||
None => self.insert_if_possible(id, Some(mode)).await,
|
||||
}
|
||||
|
@ -53,51 +73,45 @@ impl BeatmapMetaCache {
|
|||
|
||||
/// Get a beatmap without a mode...
|
||||
pub async fn get_beatmap_default(&self, id: u64) -> Result<Beatmap> {
|
||||
Ok(
|
||||
match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||
.iter()
|
||||
.find_map(|&mode| {
|
||||
self.cache
|
||||
.get(&(id, mode))
|
||||
.filter(|b| b.mode == mode)
|
||||
.map(|b| b.clone())
|
||||
}) {
|
||||
Some(v) => v,
|
||||
None => self.insert_if_possible(id, None).await?,
|
||||
},
|
||||
)
|
||||
for mode in std::array::IntoIter::new([Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]) {
|
||||
if let Ok(Some(bm)) = self.get_beatmap_db(id, mode).await {
|
||||
if bm.mode == mode {
|
||||
return Ok(bm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.insert_if_possible(id, None).await
|
||||
}
|
||||
|
||||
/// Get a beatmapset from its ID.
|
||||
pub async fn get_beatmapset(&self, id: u64) -> Result<Vec<Beatmap>> {
|
||||
match self.beatmapsets.get(&id).map(|v| v.clone()) {
|
||||
Some(v) => {
|
||||
v.into_iter()
|
||||
.map(|id| self.get_beatmap_default(id))
|
||||
.collect::<stream::FuturesOrdered<_>>()
|
||||
.try_collect()
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
let mut beatmaps = self
|
||||
.client
|
||||
.beatmaps(crate::BeatmapRequestKind::Beatmapset(id), |f| f)
|
||||
.await?;
|
||||
if beatmaps.is_empty() {
|
||||
return Err(Error::msg("beatmapset not found"));
|
||||
}
|
||||
beatmaps.sort_by_key(|b| (b.mode as u8, (b.difficulty.stars * 1000.0) as u64)); // Cast so that Ord is maintained
|
||||
if let ApprovalStatus::Ranked(_) = &beatmaps[0].approval {
|
||||
// Save each beatmap.
|
||||
beatmaps.iter().for_each(|b| {
|
||||
self.cache.insert((b.beatmap_id, b.mode), b.clone());
|
||||
});
|
||||
// Save the beatmapset mapping.
|
||||
self.beatmapsets
|
||||
.insert(id, beatmaps.iter().map(|v| v.beatmap_id).collect());
|
||||
}
|
||||
Ok(beatmaps)
|
||||
}
|
||||
let bms = models::CachedBeatmap::by_beatmapset(id as i64, &self.pool).await?;
|
||||
if !bms.is_empty() {
|
||||
return Ok(bms
|
||||
.into_iter()
|
||||
.map(|v| bincode::deserialize(&v.beatmap[..]).unwrap())
|
||||
.collect());
|
||||
}
|
||||
let mut beatmaps = self
|
||||
.client
|
||||
.beatmaps(crate::BeatmapRequestKind::Beatmapset(id), |f| f)
|
||||
.await?;
|
||||
if beatmaps.is_empty() {
|
||||
return Err(Error::msg("beatmapset not found"));
|
||||
}
|
||||
beatmaps.sort_by_key(|b| (b.mode as u8, (b.difficulty.stars * 1000.0) as u64)); // Cast so that Ord is maintained
|
||||
if let ApprovalStatus::Ranked(_) = &beatmaps[0].approval {
|
||||
// Save each beatmap.
|
||||
let mut t = self.pool.begin().await?;
|
||||
for b in &beatmaps {
|
||||
let mut b = Self::to_cached_beatmap(&b, None);
|
||||
b.store(&mut t).await?;
|
||||
// Save the beatmapset mapping.
|
||||
b.link_beatmapset(id as i64, &mut t).await?;
|
||||
}
|
||||
t.commit().await?;
|
||||
}
|
||||
Ok(beatmaps)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,28 +4,27 @@ use serenity::model::id::ChannelId;
|
|||
use youmubot_prelude::*;
|
||||
|
||||
/// Save the beatmap into the server data storage.
|
||||
pub(crate) fn save_beatmap(
|
||||
pub(crate) async fn save_beatmap(
|
||||
data: &TypeMap,
|
||||
channel_id: ChannelId,
|
||||
bm: &BeatmapWithMode,
|
||||
) -> Result<()> {
|
||||
OsuLastBeatmap::open(data)
|
||||
.borrow_mut()?
|
||||
.insert(channel_id, (bm.0.clone(), bm.mode()));
|
||||
data.get::<OsuLastBeatmap>()
|
||||
.unwrap()
|
||||
.save(channel_id, &bm.0, bm.1)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the last beatmap requested from this channel.
|
||||
pub(crate) fn get_beatmap(
|
||||
pub(crate) async fn get_beatmap(
|
||||
data: &TypeMap,
|
||||
channel_id: ChannelId,
|
||||
) -> Result<Option<BeatmapWithMode>> {
|
||||
let db = OsuLastBeatmap::open(data);
|
||||
let db = db.borrow()?;
|
||||
|
||||
Ok(db
|
||||
.get(&channel_id)
|
||||
.cloned()
|
||||
.map(|(a, b)| BeatmapWithMode(a, b)))
|
||||
data.get::<OsuLastBeatmap>()
|
||||
.unwrap()
|
||||
.by_channel(channel_id)
|
||||
.await
|
||||
.map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode)))
|
||||
}
|
||||
|
|
|
@ -1,28 +1,217 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use youmubot_db_sql::{models::osu as models, models::osu_user as model, Pool};
|
||||
|
||||
use crate::models::{Beatmap, Mode, Score};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::model::id::{ChannelId, UserId};
|
||||
use std::collections::HashMap;
|
||||
use youmubot_db::DB;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
/// Save the user IDs.
|
||||
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
||||
pub struct OsuSavedUsers {
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
impl TypeMapKey for OsuSavedUsers {
|
||||
type Value = OsuSavedUsers;
|
||||
}
|
||||
|
||||
impl OsuSavedUsers {
|
||||
/// Create a new database wrapper.
|
||||
pub fn new(pool: Pool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
impl OsuSavedUsers {
|
||||
/// Get all users
|
||||
pub async fn all(&self) -> Result<Vec<OsuUser>> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
model::OsuUser::all(&mut conn)
|
||||
.map(|v| v.map(OsuUser::from).map_err(Error::from))
|
||||
.try_collect()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get an user by their user_id.
|
||||
pub async fn by_user_id(&self, user_id: UserId) -> Result<Option<OsuUser>> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let u = model::OsuUser::by_user_id(user_id.0 as i64, &mut conn)
|
||||
.await?
|
||||
.map(OsuUser::from);
|
||||
Ok(u)
|
||||
}
|
||||
|
||||
/// Save the given user.
|
||||
pub async fn save(&self, u: OsuUser) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
Ok(model::OsuUser::from(u).store(&mut conn).await?)
|
||||
}
|
||||
|
||||
/// Save the given user as a completely new user.
|
||||
pub async fn new_user(&self, u: OsuUser) -> Result<()> {
|
||||
let mut t = self.pool.begin().await?;
|
||||
model::OsuUser::delete(u.user_id.0 as i64, &mut t).await?;
|
||||
model::OsuUser::from(u).store(&mut t).await?;
|
||||
t.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Save each channel's last requested beatmap.
|
||||
pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
||||
pub struct OsuLastBeatmap(Pool);
|
||||
|
||||
/// Save each beatmap's plays by user.
|
||||
pub type OsuUserBests =
|
||||
DB<HashMap<(u64, Mode) /* Beatmap ID and Mode */, HashMap<UserId, Vec<Score>>>>;
|
||||
impl TypeMapKey for OsuLastBeatmap {
|
||||
type Value = OsuLastBeatmap;
|
||||
}
|
||||
|
||||
impl OsuLastBeatmap {
|
||||
pub fn new(pool: Pool) -> Self {
|
||||
Self(pool)
|
||||
}
|
||||
}
|
||||
|
||||
impl OsuLastBeatmap {
|
||||
pub async fn by_channel(&self, id: impl Into<ChannelId>) -> Result<Option<(Beatmap, Mode)>> {
|
||||
let last_beatmap = models::LastBeatmap::by_channel_id(id.into().0 as i64, &self.0).await?;
|
||||
Ok(match last_beatmap {
|
||||
Some(lb) => Some((bincode::deserialize(&lb.beatmap[..])?, lb.mode.into())),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save(
|
||||
&self,
|
||||
channel: impl Into<ChannelId>,
|
||||
beatmap: &Beatmap,
|
||||
mode: Mode,
|
||||
) -> Result<()> {
|
||||
let b = models::LastBeatmap {
|
||||
channel_id: channel.into().0 as i64,
|
||||
beatmap: bincode::serialize(beatmap)?,
|
||||
mode: mode as u8,
|
||||
};
|
||||
b.store(&self.0).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Save each channel's last requested beatmap.
|
||||
pub struct OsuUserBests(Pool);
|
||||
|
||||
impl TypeMapKey for OsuUserBests {
|
||||
type Value = OsuUserBests;
|
||||
}
|
||||
|
||||
impl OsuUserBests {
|
||||
pub fn new(pool: Pool) -> Self {
|
||||
Self(pool)
|
||||
}
|
||||
}
|
||||
|
||||
impl OsuUserBests {
|
||||
pub async fn by_beatmap(&self, beatmap_id: u64, mode: Mode) -> Result<Vec<(UserId, Score)>> {
|
||||
let scores = models::UserBestScore::by_map(beatmap_id as i64, mode as u8, &self.0).await?;
|
||||
Ok(scores
|
||||
.into_iter()
|
||||
.map(|us| {
|
||||
(
|
||||
UserId(us.user_id as u64),
|
||||
bincode::deserialize(&us.score[..]).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn save(
|
||||
&self,
|
||||
user: impl Into<UserId>,
|
||||
mode: Mode,
|
||||
scores: impl IntoIterator<Item = Score>,
|
||||
) -> Result<()> {
|
||||
let user = user.into();
|
||||
scores
|
||||
.into_iter()
|
||||
.map(|score| models::UserBestScore {
|
||||
user_id: user.0 as i64,
|
||||
beatmap_id: score.beatmap_id as i64,
|
||||
mode: mode as u8,
|
||||
mods: score.mods.bits() as i64,
|
||||
cached_at: Utc::now(),
|
||||
score: bincode::serialize(&score).unwrap(),
|
||||
})
|
||||
.map(|mut us| async move { us.store(&self.0).await })
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.try_collect::<()>()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// An osu! saved user.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct OsuUser {
|
||||
pub user_id: UserId,
|
||||
pub id: u64,
|
||||
pub last_update: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub pp: Vec<Option<f64>>,
|
||||
pub pp: [Option<f32>; 4],
|
||||
/// More than 5 failures => gone
|
||||
pub failures: Option<u8>,
|
||||
pub failures: u8,
|
||||
}
|
||||
|
||||
impl From<OsuUser> for model::OsuUser {
|
||||
fn from(u: OsuUser) -> Self {
|
||||
Self {
|
||||
user_id: u.user_id.0 as i64,
|
||||
id: u.id as i64,
|
||||
last_update: u.last_update,
|
||||
pp_std: u.pp[Mode::Std as usize],
|
||||
pp_taiko: u.pp[Mode::Taiko as usize],
|
||||
pp_mania: u.pp[Mode::Mania as usize],
|
||||
pp_catch: u.pp[Mode::Catch as usize],
|
||||
failures: u.failures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<model::OsuUser> for OsuUser {
|
||||
fn from(u: model::OsuUser) -> Self {
|
||||
Self {
|
||||
user_id: UserId(u.user_id as u64),
|
||||
id: u.id as u64,
|
||||
last_update: u.last_update,
|
||||
pp: [u.pp_std, u.pp_taiko, u.pp_mania, u.pp_catch],
|
||||
failures: u.failures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod legacy {
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::models::{Beatmap, Mode, Score};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::model::id::{ChannelId, UserId};
|
||||
use std::collections::HashMap;
|
||||
use youmubot_db::DB;
|
||||
|
||||
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
||||
|
||||
/// An osu! saved user.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct OsuUser {
|
||||
pub id: u64,
|
||||
pub last_update: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub pp: Vec<Option<f64>>,
|
||||
/// More than 5 failures => gone
|
||||
pub failures: Option<u8>,
|
||||
}
|
||||
|
||||
/// Save each channel's last requested beatmap.
|
||||
pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
||||
|
||||
/// Save each beatmap's plays by user.
|
||||
pub type OsuUserBests =
|
||||
DB<HashMap<(u64, Mode) /* Beatmap ID and Mode */, HashMap<UserId, Vec<Score>>>>;
|
||||
}
|
||||
|
|
|
@ -93,10 +93,10 @@ mod beatmapset {
|
|||
|
||||
let map = &self.maps[page];
|
||||
let info = match &self.infos[page] {
|
||||
Some(info) => info.clone(),
|
||||
Some(info) => *info,
|
||||
None => {
|
||||
let info = self.get_beatmap_info(ctx, map).await;
|
||||
self.infos[page] = Some(info.clone());
|
||||
self.infos[page] = Some(info);
|
||||
info
|
||||
}
|
||||
};
|
||||
|
@ -125,7 +125,8 @@ mod beatmapset {
|
|||
m.channel_id,
|
||||
&BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)),
|
||||
)
|
||||
.ok();
|
||||
.await
|
||||
.pls_ok();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
|
|
@ -199,6 +199,7 @@ pub(crate) fn score_embed<'a>(
|
|||
}
|
||||
|
||||
impl<'a> ScoreEmbedBuilder<'a> {
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
pub fn build<'b>(&self, m: &'b mut CreateEmbed) -> &'b mut CreateEmbed {
|
||||
let mode = self.bm.mode();
|
||||
let b = &self.bm.0;
|
||||
|
@ -213,8 +214,8 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
|||
.as_ref()
|
||||
.map(|info| info.stars as f64)
|
||||
.unwrap_or(b.difficulty.stars);
|
||||
let score_line = match &s.rank {
|
||||
Rank::SS | Rank::SSH => format!("SS"),
|
||||
let score_line = match s.rank {
|
||||
Rank::SS | Rank::SSH => "SS".to_string(),
|
||||
_ if s.perfect => format!("{:.2}% FC", accuracy),
|
||||
Rank::F => {
|
||||
let display = info
|
||||
|
@ -224,7 +225,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
|||
* 100.0
|
||||
})
|
||||
.map(|p| format!("FAILED @ {:.2}%", p))
|
||||
.unwrap_or("FAILED".to_owned());
|
||||
.unwrap_or_else(|| "FAILED".to_owned());
|
||||
format!("{:.2}% {} combo [{}]", accuracy, s.max_combo, display)
|
||||
}
|
||||
v => format!(
|
||||
|
@ -267,7 +268,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
|||
pp.as_ref()
|
||||
.map(|(_, original)| format!("{} ({:.2}pp if FC?)", original, value))
|
||||
})
|
||||
.or(pp.map(|v| v.1))
|
||||
.or_else(|| pp.map(|v| v.1))
|
||||
} else {
|
||||
pp.map(|v| v.1)
|
||||
};
|
||||
|
@ -295,11 +296,11 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
|||
let top_record = self
|
||||
.top_record
|
||||
.map(|v| format!("| #{} top record!", v))
|
||||
.unwrap_or("".to_owned());
|
||||
.unwrap_or_else(|| "".to_owned());
|
||||
let world_record = self
|
||||
.world_record
|
||||
.map(|v| format!("| #{} on Global Rankings!", v))
|
||||
.unwrap_or("".to_owned());
|
||||
.unwrap_or_else(|| "".to_owned());
|
||||
let diff = b.difficulty.apply_mods(s.mods, Some(stars));
|
||||
let creator = if b.difficulty_name.contains("'s") {
|
||||
"".to_owned()
|
||||
|
@ -364,11 +365,11 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn user_embed<'a>(
|
||||
pub(crate) fn user_embed(
|
||||
u: User,
|
||||
best: Option<(Score, BeatmapWithMode, Option<BeatmapInfo>)>,
|
||||
m: &'a mut CreateEmbed,
|
||||
) -> &'a mut CreateEmbed {
|
||||
m: &mut CreateEmbed,
|
||||
) -> &mut CreateEmbed {
|
||||
m.title(u.username)
|
||||
.url(format!("https://osu.ppy.sh/users/{}", u.id))
|
||||
.color(0xffb6c1)
|
||||
|
@ -377,7 +378,7 @@ pub(crate) fn user_embed<'a>(
|
|||
.field(
|
||||
"Performance Points",
|
||||
u.pp.map(|v| format!("{:.2}pp", v))
|
||||
.unwrap_or("Inactive".to_owned()),
|
||||
.unwrap_or_else(|| "Inactive".to_owned()),
|
||||
false,
|
||||
)
|
||||
.field("World Rank", format!("#{}", grouped_number(u.rank)), true)
|
||||
|
@ -436,7 +437,7 @@ pub(crate) fn user_embed<'a>(
|
|||
Duration(
|
||||
(Utc::now() - v.date)
|
||||
.to_std()
|
||||
.unwrap_or(std::time::Duration::from_secs(1))
|
||||
.unwrap_or_else(|_| std::time::Duration::from_secs(1))
|
||||
)
|
||||
))
|
||||
.push("on ")
|
||||
|
|
|
@ -50,6 +50,7 @@ pub fn hook<'a>(
|
|||
msg.channel_id,
|
||||
&bm,
|
||||
)
|
||||
.await
|
||||
.pls_ok();
|
||||
}
|
||||
EmbedType::Beatmapset(b) => {
|
||||
|
|
|
@ -52,14 +52,15 @@ impl TypeMapKey for OsuClient {
|
|||
/// - Hooks. Hooks are completely opt-in.
|
||||
///
|
||||
pub fn setup(
|
||||
path: &std::path::Path,
|
||||
_path: &std::path::Path,
|
||||
data: &mut TypeMap,
|
||||
announcers: &mut AnnouncerHandler,
|
||||
) -> CommandResult {
|
||||
let sql_client = data.get::<SQLClient>().unwrap().clone();
|
||||
// Databases
|
||||
OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.yaml"))?;
|
||||
OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.yaml"))?;
|
||||
OsuUserBests::insert_into(&mut *data, &path.join("osu_user_bests.yaml"))?;
|
||||
data.insert::<OsuSavedUsers>(OsuSavedUsers::new(sql_client.clone()));
|
||||
data.insert::<OsuLastBeatmap>(OsuLastBeatmap::new(sql_client.clone()));
|
||||
data.insert::<OsuUserBests>(OsuUserBests::new(sql_client.clone()));
|
||||
|
||||
// Locks
|
||||
data.insert::<server_rank::update_lock::UpdateLock>(
|
||||
|
@ -75,9 +76,12 @@ pub fn setup(
|
|||
};
|
||||
let osu_client = Arc::new(make_client());
|
||||
data.insert::<OsuClient>(osu_client.clone());
|
||||
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(http_client));
|
||||
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(
|
||||
http_client,
|
||||
sql_client.clone(),
|
||||
));
|
||||
data.insert::<beatmap_cache::BeatmapMetaCache>(beatmap_cache::BeatmapMetaCache::new(
|
||||
osu_client,
|
||||
osu_client, sql_client,
|
||||
));
|
||||
|
||||
// Announcer
|
||||
|
@ -196,7 +200,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
add_user(msg.author.id, u.id, &*data)?;
|
||||
add_user(msg.author.id, u.id, &*data).await?;
|
||||
msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
|
@ -227,7 +231,7 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
|
|||
let user: Option<User> = osu.user(UserID::Auto(user), |f| f).await?;
|
||||
match user {
|
||||
Some(u) => {
|
||||
add_user(target, u.id, &*data)?;
|
||||
add_user(target, u.id, &*data).await?;
|
||||
msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
|
@ -244,22 +248,15 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
|
|||
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,
|
||||
failures: None,
|
||||
last_update: chrono::Utc::now(),
|
||||
pp: vec![],
|
||||
},
|
||||
);
|
||||
OsuUserBests::open(data)
|
||||
.borrow_mut()?
|
||||
.iter_mut()
|
||||
.for_each(|(_, r)| {
|
||||
r.remove(&target);
|
||||
});
|
||||
async fn add_user(target: serenity::model::id::UserId, user_id: u64, data: &TypeMap) -> Result<()> {
|
||||
let u = OsuUser {
|
||||
user_id: target,
|
||||
id: user_id,
|
||||
failures: 0,
|
||||
last_update: chrono::Utc::now(),
|
||||
pp: [None, None, None, None],
|
||||
};
|
||||
data.get::<OsuSavedUsers>().unwrap().new_user(u).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -278,7 +275,7 @@ impl FromStr for ModeArg {
|
|||
}
|
||||
}
|
||||
|
||||
fn to_user_id_query(
|
||||
async fn to_user_id_query(
|
||||
s: Option<UsernameArg>,
|
||||
data: &TypeMap,
|
||||
msg: &Message,
|
||||
|
@ -289,12 +286,12 @@ fn to_user_id_query(
|
|||
None => msg.author.id,
|
||||
};
|
||||
|
||||
let db = OsuSavedUsers::open(data);
|
||||
let db = db.borrow()?;
|
||||
db.get(&id)
|
||||
.cloned()
|
||||
data.get::<OsuSavedUsers>()
|
||||
.unwrap()
|
||||
.by_user_id(id)
|
||||
.await?
|
||||
.map(|u| UserID::ID(u.id))
|
||||
.ok_or(Error::msg("No saved account found"))
|
||||
.ok_or_else(|| Error::msg("No saved account found"))
|
||||
}
|
||||
|
||||
enum Nth {
|
||||
|
@ -307,7 +304,7 @@ impl FromStr for Nth {
|
|||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s == "--all" || s == "-a" || s == "##" {
|
||||
Ok(Nth::All)
|
||||
} else if !s.starts_with("#") {
|
||||
} else if !s.starts_with('#') {
|
||||
Err(Error::msg("Not an order"))
|
||||
} else {
|
||||
let v = s.split_at("#".len()).1.parse()?;
|
||||
|
@ -385,13 +382,13 @@ async fn list_plays<'a>(
|
|||
.ok()
|
||||
.map(|pp| format!("{:.2}pp [?]", pp))
|
||||
})
|
||||
.unwrap_or("-".to_owned()));
|
||||
.unwrap_or_else(|| "-".to_owned()));
|
||||
r
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<stream::FuturesOrdered<_>>()
|
||||
.map(|v| v.unwrap_or("-".to_owned()))
|
||||
.map(|v| v.unwrap_or_else(|_| "-".to_owned()))
|
||||
.collect::<Vec<String>>();
|
||||
let (beatmaps, pp) = future::join(beatmaps, pp).await;
|
||||
|
||||
|
@ -494,7 +491,7 @@ async fn list_plays<'a>(
|
|||
page + 1,
|
||||
total_pages
|
||||
));
|
||||
if let None = mode.to_oppai_mode() {
|
||||
if mode.to_oppai_mode().is_none() {
|
||||
m.push_line("Note: star difficulty doesn't reflect mods applied.");
|
||||
} else {
|
||||
m.push_line("[?] means pp was predicted by oppai-rs.");
|
||||
|
@ -521,7 +518,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
let data = ctx.data.read().await;
|
||||
let nth = args.single::<Nth>().unwrap_or(Nth::All);
|
||||
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg)?;
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
|
||||
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
|
@ -529,7 +526,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
let user = osu
|
||||
.user(user, |f| f.mode(mode))
|
||||
.await?
|
||||
.ok_or(Error::msg("User not found"))?;
|
||||
.ok_or_else(|| Error::msg("User not found"))?;
|
||||
match nth {
|
||||
Nth::Nth(nth) => {
|
||||
let recent_play = osu
|
||||
|
@ -537,14 +534,14 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
.await?
|
||||
.into_iter()
|
||||
.last()
|
||||
.ok_or(Error::msg("No such play"))?;
|
||||
.ok_or_else(|| Error::msg("No such play"))?;
|
||||
let beatmap = meta_cache.get_beatmap(recent_play.beatmap_id, mode).await?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let beatmap_mode = BeatmapWithMode(beatmap, mode);
|
||||
|
||||
msg.channel_id
|
||||
.send_message(&ctx, |m| {
|
||||
m.content(format!("Here is the play that you requested",))
|
||||
m.content("Here is the play that you requested".to_string())
|
||||
.embed(|m| {
|
||||
score_embed(&recent_play, &beatmap_mode, &content, &user).build(m)
|
||||
})
|
||||
|
@ -553,7 +550,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
.await?;
|
||||
|
||||
// Save the beatmap...
|
||||
cache::save_beatmap(&*data, msg.channel_id, &beatmap_mode)?;
|
||||
cache::save_beatmap(&*data, msg.channel_id, &beatmap_mode).await?;
|
||||
}
|
||||
Nth::All => {
|
||||
let plays = osu
|
||||
|
@ -585,7 +582,7 @@ impl FromStr for OptBeatmapset {
|
|||
#[max_args(2)]
|
||||
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let b = cache::get_beatmap(&*data, msg.channel_id)?;
|
||||
let b = cache::get_beatmap(&*data, msg.channel_id).await?;
|
||||
let beatmapset = args.find::<OptBeatmapset>().is_ok();
|
||||
|
||||
match b {
|
||||
|
@ -636,7 +633,7 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
#[max_args(1)]
|
||||
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let bm = cache::get_beatmap(&*data, msg.channel_id)?;
|
||||
let bm = cache::get_beatmap(&*data, msg.channel_id).await?;
|
||||
|
||||
match bm {
|
||||
None => {
|
||||
|
@ -648,11 +645,11 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||
let m = bm.1;
|
||||
let username_arg = args.single::<UsernameArg>().ok();
|
||||
let user_id = match username_arg.as_ref() {
|
||||
Some(UsernameArg::Tagged(v)) => Some(v.clone()),
|
||||
Some(UsernameArg::Tagged(v)) => Some(*v),
|
||||
None => Some(msg.author.id),
|
||||
_ => None,
|
||||
};
|
||||
let user = to_user_id_query(username_arg, &*data, msg)?;
|
||||
let user = to_user_id_query(username_arg, &*data, msg).await?;
|
||||
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
|
@ -662,7 +659,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||
let user = osu
|
||||
.user(user, |f| f)
|
||||
.await?
|
||||
.ok_or(Error::msg("User not found"))?;
|
||||
.ok_or_else(|| Error::msg("User not found"))?;
|
||||
let scores = osu
|
||||
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))
|
||||
.await?;
|
||||
|
@ -681,11 +678,11 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||
|
||||
if let Some(user_id) = user_id {
|
||||
// Save to database
|
||||
OsuUserBests::open(&*data)
|
||||
.borrow_mut()?
|
||||
.entry((bm.0.beatmap_id, bm.1))
|
||||
.or_default()
|
||||
.insert(user_id, scores);
|
||||
data.get::<OsuUserBests>()
|
||||
.unwrap()
|
||||
.save(user_id, m, scores)
|
||||
.await
|
||||
.pls_ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -706,7 +703,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.map(|ModeArg(t)| t)
|
||||
.unwrap_or(Mode::Std);
|
||||
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg)?;
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
|
||||
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
|
||||
|
@ -714,7 +711,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
let user = osu
|
||||
.user(user, |f| f.mode(mode))
|
||||
.await?
|
||||
.ok_or(Error::msg("User not found"))?;
|
||||
.ok_or_else(|| Error::msg("User not found"))?;
|
||||
|
||||
match nth {
|
||||
Nth::Nth(nth) => {
|
||||
|
@ -727,7 +724,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
let top_play = top_play
|
||||
.into_iter()
|
||||
.last()
|
||||
.ok_or(Error::msg("No such play"))?;
|
||||
.ok_or_else(|| Error::msg("No such play"))?;
|
||||
let beatmap = meta_cache.get_beatmap(top_play.beatmap_id, mode).await?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let beatmap = BeatmapWithMode(beatmap, mode);
|
||||
|
@ -747,7 +744,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.await?;
|
||||
|
||||
// Save the beatmap...
|
||||
cache::save_beatmap(&*data, msg.channel_id, &beatmap)?;
|
||||
cache::save_beatmap(&*data, msg.channel_id, &beatmap).await?;
|
||||
}
|
||||
Nth::All => {
|
||||
let plays = osu
|
||||
|
@ -761,7 +758,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
|
||||
async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg)?;
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let user = osu.user(user, |f| f.mode(mode)).await?;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::{ffi::CString, sync::Arc};
|
||||
use youmubot_db_sql::{models::osu as models, Pool};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
pub use oppai_rs::Accuracy as OppaiAccuracy;
|
||||
|
@ -7,7 +8,7 @@ pub use oppai_rs::Accuracy as OppaiAccuracy;
|
|||
#[derive(Debug)]
|
||||
pub struct BeatmapContent {
|
||||
id: u64,
|
||||
content: CString,
|
||||
content: Arc<CString>,
|
||||
}
|
||||
|
||||
/// the output of "one" oppai run.
|
||||
|
@ -50,7 +51,7 @@ impl BeatmapContent {
|
|||
oppai.mods(mods.into());
|
||||
let objects = oppai.num_objects();
|
||||
let stars = oppai.stars();
|
||||
Ok(BeatmapInfo { stars, objects })
|
||||
Ok(BeatmapInfo { objects, stars })
|
||||
}
|
||||
|
||||
pub fn get_possible_pp_with(
|
||||
|
@ -71,24 +72,21 @@ impl BeatmapContent {
|
|||
];
|
||||
let objects = oppai.num_objects();
|
||||
let stars = oppai.stars();
|
||||
Ok((BeatmapInfo { stars, objects }, pp))
|
||||
Ok((BeatmapInfo { objects, stars }, pp))
|
||||
}
|
||||
}
|
||||
|
||||
/// A central cache for the beatmaps.
|
||||
pub struct BeatmapCache {
|
||||
client: ratelimit::Ratelimit<reqwest::Client>,
|
||||
cache: dashmap::DashMap<u64, Arc<BeatmapContent>>,
|
||||
pool: Pool,
|
||||
}
|
||||
|
||||
impl BeatmapCache {
|
||||
/// Create a new cache.
|
||||
pub fn new(client: reqwest::Client) -> Self {
|
||||
pub fn new(client: reqwest::Client, pool: Pool) -> Self {
|
||||
let client = ratelimit::Ratelimit::new(client, 5, std::time::Duration::from_secs(1));
|
||||
BeatmapCache {
|
||||
client,
|
||||
cache: dashmap::DashMap::new(),
|
||||
}
|
||||
BeatmapCache { client, pool }
|
||||
}
|
||||
|
||||
async fn download_beatmap(&self, id: u64) -> Result<BeatmapContent> {
|
||||
|
@ -103,20 +101,39 @@ impl BeatmapCache {
|
|||
.await?;
|
||||
Ok(BeatmapContent {
|
||||
id,
|
||||
content: CString::new(content.into_iter().collect::<Vec<_>>())?,
|
||||
content: Arc::new(CString::new(content.into_iter().collect::<Vec<_>>())?),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_beatmap_db(&self, id: u64) -> Result<Option<BeatmapContent>> {
|
||||
Ok(models::CachedBeatmapContent::by_id(id as i64, &self.pool)
|
||||
.await?
|
||||
.map(|v| BeatmapContent {
|
||||
id,
|
||||
content: Arc::new(CString::new(v.content).unwrap()),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn save_beatmap(&self, b: &BeatmapContent) -> Result<()> {
|
||||
let mut bc = models::CachedBeatmapContent {
|
||||
beatmap_id: b.id as i64,
|
||||
cached_at: chrono::Utc::now(),
|
||||
content: b.content.as_ref().clone().into_bytes(),
|
||||
};
|
||||
bc.store(&self.pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a beatmap from the cache.
|
||||
pub async fn get_beatmap(
|
||||
&self,
|
||||
id: u64,
|
||||
) -> Result<impl std::ops::Deref<Target = BeatmapContent>> {
|
||||
if !self.cache.contains_key(&id) {
|
||||
self.cache
|
||||
.insert(id, Arc::new(self.download_beatmap(id).await?));
|
||||
pub async fn get_beatmap(&self, id: u64) -> Result<BeatmapContent> {
|
||||
match self.get_beatmap_db(id).await? {
|
||||
Some(v) => Ok(v),
|
||||
None => {
|
||||
let m = self.download_beatmap(id).await?;
|
||||
self.save_beatmap(&m).await?;
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
Ok(self.cache.get(&id).unwrap().clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,12 +28,15 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
|||
let mode = args.single::<ModeArg>().map(|v| v.0).unwrap_or(Mode::Std);
|
||||
let guild = m.guild_id.expect("Guild-only command");
|
||||
let member_cache = data.get::<MemberCache>().unwrap();
|
||||
let users = OsuSavedUsers::open(&*data).borrow()?.clone();
|
||||
let users = users
|
||||
let users = data
|
||||
.get::<OsuSavedUsers>()
|
||||
.unwrap()
|
||||
.all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(user_id, osu_user)| async move {
|
||||
.map(|osu_user| async move {
|
||||
member_cache
|
||||
.query(&ctx, user_id, guild)
|
||||
.query(&ctx, osu_user.user_id, guild)
|
||||
.await
|
||||
.and_then(|member| {
|
||||
osu_user
|
||||
|
@ -41,11 +44,11 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
|||
.get(mode as usize)
|
||||
.cloned()
|
||||
.and_then(|pp| pp)
|
||||
.map(|pp| (pp, member.distinct(), osu_user.last_update.clone()))
|
||||
.map(|pp| (pp, member.distinct(), osu_user.last_update))
|
||||
})
|
||||
})
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.filter_map(|v| future::ready(v))
|
||||
.filter_map(future::ready)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let last_update = users.iter().map(|(_, _, a)| a).min().cloned();
|
||||
|
@ -176,7 +179,7 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma
|
|||
}
|
||||
Some(v) => v,
|
||||
};
|
||||
let bm = match get_beatmap(&*data, m.channel_id)? {
|
||||
let bm = match get_beatmap(&*data, m.channel_id).await? {
|
||||
Some(bm) => bm,
|
||||
None => {
|
||||
m.reply(&ctx, "No beatmap queried on this channel.").await?;
|
||||
|
@ -191,15 +194,15 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma
|
|||
// Run a check on everyone in the server basically.
|
||||
let all_server_users: Vec<(UserId, Vec<Score>)> = {
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let osu_users = OsuSavedUsers::open(&*data);
|
||||
let osu_users = osu_users
|
||||
.borrow()?
|
||||
.iter()
|
||||
.map(|(&user_id, osu_user)| (user_id, osu_user.id))
|
||||
.collect::<Vec<_>>();
|
||||
let osu_users = data
|
||||
.get::<OsuSavedUsers>()
|
||||
.unwrap()
|
||||
.all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|osu_user| (osu_user.user_id, osu_user.id));
|
||||
let beatmap_id = bm.0.beatmap_id;
|
||||
osu_users
|
||||
.into_iter()
|
||||
.map(|(user_id, osu_id)| {
|
||||
member_cache
|
||||
.query(&ctx, user_id, guild)
|
||||
|
@ -222,12 +225,13 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma
|
|||
let updated_users = all_server_users.len();
|
||||
// Update everything.
|
||||
{
|
||||
let mut osu_user_bests = OsuUserBests::open(&*data);
|
||||
let mut osu_user_bests = osu_user_bests.borrow_mut()?;
|
||||
let user_bests = osu_user_bests.entry((bm.0.beatmap_id, bm.1)).or_default();
|
||||
all_server_users.into_iter().for_each(|(member, scores)| {
|
||||
user_bests.insert(member, scores);
|
||||
})
|
||||
let db = data.get::<OsuUserBests>().unwrap();
|
||||
all_server_users
|
||||
.into_iter()
|
||||
.map(|(u, scores)| db.save(u, mode, scores))
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.try_collect::<()>()
|
||||
.await?;
|
||||
}
|
||||
// Signal update complete.
|
||||
running_reaction.delete(&ctx).await.ok();
|
||||
|
@ -255,7 +259,7 @@ pub async fn leaderboard(ctx: &Context, m: &Message, args: Args) -> CommandResul
|
|||
let sort_order = OrderBy::from(args.rest());
|
||||
|
||||
let data = ctx.data.read().await;
|
||||
let bm = match get_beatmap(&*data, m.channel_id)? {
|
||||
let bm = match get_beatmap(&*data, m.channel_id).await? {
|
||||
Some(bm) => bm,
|
||||
None => {
|
||||
m.reply(&ctx, "No beatmap queried on this channel.").await?;
|
||||
|
@ -272,7 +276,6 @@ async fn show_leaderboard(
|
|||
order: OrderBy,
|
||||
) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let mut osu_user_bests = OsuUserBests::open(&*data);
|
||||
|
||||
// Get oppai map.
|
||||
let mode = bm.1;
|
||||
|
@ -294,8 +297,12 @@ async fn show_leaderboard(
|
|||
|
||||
// Run a check on the user once too!
|
||||
{
|
||||
let osu_users = OsuSavedUsers::open(&*data);
|
||||
let user = osu_users.borrow()?.get(&m.author.id).map(|v| v.id);
|
||||
let user = data
|
||||
.get::<OsuSavedUsers>()
|
||||
.unwrap()
|
||||
.by_user_id(m.author.id)
|
||||
.await?
|
||||
.map(|v| v.id);
|
||||
if let Some(id) = user {
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
if let Ok(scores) = osu
|
||||
|
@ -303,11 +310,11 @@ async fn show_leaderboard(
|
|||
.await
|
||||
{
|
||||
if !scores.is_empty() {
|
||||
osu_user_bests
|
||||
.borrow_mut()?
|
||||
.entry((bm.0.beatmap_id, bm.1))
|
||||
.or_default()
|
||||
.insert(m.author.id, scores);
|
||||
data.get::<OsuUserBests>()
|
||||
.unwrap()
|
||||
.save(m.author.id, mode, scores)
|
||||
.await
|
||||
.pls_ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -316,39 +323,27 @@ async fn show_leaderboard(
|
|||
let guild = m.guild_id.expect("Guild-only command");
|
||||
let member_cache = data.get::<MemberCache>().unwrap();
|
||||
let scores = {
|
||||
const NO_SCORES: &'static str = "No scores have been recorded for this beatmap.";
|
||||
const NO_SCORES: &str = "No scores have been recorded for this beatmap.";
|
||||
|
||||
let users = osu_user_bests
|
||||
.borrow()?
|
||||
.get(&(bm.0.beatmap_id, bm.1))
|
||||
.cloned();
|
||||
let users = match users {
|
||||
None => {
|
||||
m.reply(&ctx, NO_SCORES).await?;
|
||||
return Ok(());
|
||||
}
|
||||
Some(v) if v.is_empty() => {
|
||||
m.reply(&ctx, NO_SCORES).await?;
|
||||
return Ok(());
|
||||
}
|
||||
Some(v) => v,
|
||||
};
|
||||
let scores = data
|
||||
.get::<OsuUserBests>()
|
||||
.unwrap()
|
||||
.by_beatmap(bm.0.beatmap_id, bm.1)
|
||||
.await?;
|
||||
if scores.is_empty() {
|
||||
m.reply(&ctx, NO_SCORES).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut scores: Vec<(f64, String, Score)> = users
|
||||
let mut scores: Vec<(f64, String, Score)> = scores
|
||||
.into_iter()
|
||||
.map(|(user_id, scores)| {
|
||||
.map(|(user_id, score)| {
|
||||
member_cache
|
||||
.query(&ctx, user_id, guild)
|
||||
.map(|m| m.map(move |m| (m.distinct(), scores)))
|
||||
.map(|m| m.map(move |m| (m.distinct(), score)))
|
||||
})
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.filter_map(|v| future::ready(v))
|
||||
.flat_map(|(user, scores)| {
|
||||
scores
|
||||
.into_iter()
|
||||
.map(move |v| future::ready((user.clone(), v.clone())))
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
})
|
||||
.filter_map(future::ready)
|
||||
.filter_map(|(user, score)| {
|
||||
future::ready(
|
||||
score
|
||||
|
@ -395,8 +390,8 @@ async fn show_leaderboard(
|
|||
return Box::pin(future::ready(Ok(false)));
|
||||
}
|
||||
let total_len = scores.len();
|
||||
let scores = (&scores[start..end]).iter().cloned().collect::<Vec<_>>();
|
||||
let bm = (bm.0.clone(), bm.1.clone());
|
||||
let scores = (&scores[start..end]).to_vec();
|
||||
let bm = (bm.0.clone(), bm.1);
|
||||
Box::pin(async move {
|
||||
// username width
|
||||
let uw = scores
|
||||
|
@ -428,7 +423,7 @@ async fn show_leaderboard(
|
|||
.iter()
|
||||
.map(|(pp, _, s)| match order {
|
||||
OrderBy::PP => format!("{:.2}", pp),
|
||||
OrderBy::Score => format!("{}", crate::discord::embeds::grouped_number(s.score)),
|
||||
OrderBy::Score => crate::discord::embeds::grouped_number(s.score),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let pw = pp.iter().map(|v| v.len()).max().unwrap_or(pp_label.len());
|
||||
|
@ -512,11 +507,10 @@ async fn show_leaderboard(
|
|||
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
|
||||
));
|
||||
if let crate::models::ApprovalStatus::Ranked(_) = bm.0.approval {
|
||||
} else {
|
||||
if order == OrderBy::PP {
|
||||
content.push_line("PP was calculated by `oppai-rs`, **not** official values.");
|
||||
}
|
||||
} else if order == OrderBy::PP {
|
||||
content.push_line("PP was calculated by `oppai-rs`, **not** official values.");
|
||||
}
|
||||
|
||||
m.edit(&ctx, |f| f.content(content.build())).await?;
|
||||
Ok(true)
|
||||
})
|
||||
|
|
|
@ -39,7 +39,7 @@ impl Client {
|
|||
REQUESTS_PER_MINUTE,
|
||||
std::time::Duration::from_secs(60),
|
||||
);
|
||||
Client { key, client }
|
||||
Client { client, key }
|
||||
}
|
||||
|
||||
pub(crate) async fn build_request(&self, url: &str) -> Result<reqwest::RequestBuilder> {
|
||||
|
|
|
@ -131,10 +131,7 @@ impl Difficulty {
|
|||
|
||||
/// Format the difficulty info into a short summary.
|
||||
pub fn format_info(&self, mode: Mode, mods: Mods, original_beatmap: &Beatmap) -> String {
|
||||
let is_not_ranked = match original_beatmap.approval {
|
||||
ApprovalStatus::Ranked(_) => false,
|
||||
_ => true,
|
||||
};
|
||||
let is_not_ranked = !matches!(original_beatmap.approval, ApprovalStatus::Ranked(_));
|
||||
let three_lines = is_not_ranked;
|
||||
let bpm = (self.bpm * 100.0).round() / 100.0;
|
||||
MessageBuilder::new()
|
||||
|
@ -243,6 +240,18 @@ pub enum Mode {
|
|||
Mania,
|
||||
}
|
||||
|
||||
impl From<u8> for Mode {
|
||||
fn from(n: u8) -> Self {
|
||||
match n {
|
||||
0 => Self::Std,
|
||||
1 => Self::Taiko,
|
||||
2 => Self::Catch,
|
||||
3 => Self::Mania,
|
||||
_ => panic!("Unknown mode {}", n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use Mode::*;
|
||||
|
@ -292,7 +301,7 @@ impl Mode {
|
|||
}
|
||||
|
||||
/// Returns the mode string in the new convention.
|
||||
pub fn to_str_new_site(&self) -> &'static str {
|
||||
pub fn as_str_new_site(&self) -> &'static str {
|
||||
match self {
|
||||
Mode::Std => "osu",
|
||||
Mode::Taiko => "taiko",
|
||||
|
@ -332,7 +341,7 @@ pub struct Beatmap {
|
|||
pub pass_count: u64,
|
||||
}
|
||||
|
||||
const NEW_MODE_NAMES: [&'static str; 4] = ["osu", "taiko", "fruits", "mania"];
|
||||
const NEW_MODE_NAMES: [&str; 4] = ["osu", "taiko", "fruits", "mania"];
|
||||
|
||||
impl Beatmap {
|
||||
pub fn beatmapset_link(&self) -> String {
|
||||
|
@ -368,10 +377,11 @@ impl Beatmap {
|
|||
"/b/{}{}{}",
|
||||
self.beatmap_id,
|
||||
match override_mode {
|
||||
Some(mode) if mode != self.mode => format!("/{}", mode.to_str_new_site()),
|
||||
Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()),
|
||||
_ => "".to_owned(),
|
||||
},
|
||||
mods.map(|m| format!("{}", m)).unwrap_or("".to_owned()),
|
||||
mods.map(|m| format!("{}", m))
|
||||
.unwrap_or_else(|| "".to_owned()),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -410,7 +420,7 @@ impl UserEvent {
|
|||
let mode: Mode = Mode::parse_from_display(captures.get(2)?.as_str())?;
|
||||
Some(UserEventRank {
|
||||
beatmap_id: self.beatmap_id?,
|
||||
date: self.date.clone(),
|
||||
date: self.date,
|
||||
mode,
|
||||
rank,
|
||||
})
|
||||
|
|
|
@ -43,7 +43,7 @@ bitflags::bitflags! {
|
|||
}
|
||||
}
|
||||
|
||||
const MODS_WITH_NAMES: &[(Mods, &'static str)] = &[
|
||||
const MODS_WITH_NAMES: &[(Mods, &str)] = &[
|
||||
(Mods::NF, "NF"),
|
||||
(Mods::EZ, "EZ"),
|
||||
(Mods::TD, "TD"),
|
||||
|
@ -75,7 +75,7 @@ impl std::str::FromStr for Mods {
|
|||
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
|
||||
let mut res = Self::default();
|
||||
// Strip leading +
|
||||
if s.starts_with("+") {
|
||||
if s.starts_with('+') {
|
||||
s = &s[1..];
|
||||
}
|
||||
while s.len() >= 2 {
|
||||
|
@ -109,7 +109,7 @@ impl std::str::FromStr for Mods {
|
|||
v => return Err(format!("{} is not a valid mod", v)),
|
||||
}
|
||||
}
|
||||
if s.len() > 0 {
|
||||
if !s.is_empty() {
|
||||
Err("String of odd length is not a mod string".to_owned())
|
||||
} else {
|
||||
Ok(res)
|
||||
|
|
|
@ -82,7 +82,7 @@ impl TryFrom<raw::User> for User {
|
|||
played_time: raw
|
||||
.total_seconds_played
|
||||
.map(parse_duration)
|
||||
.unwrap_or(Ok(Duration::from_secs(0)))?,
|
||||
.unwrap_or_else(|| Ok(Duration::from_secs(0)))?,
|
||||
ranked_score: raw.ranked_score.map(parse_from_str).unwrap_or(Ok(0))?,
|
||||
total_score: raw.total_score.map(parse_from_str).unwrap_or(Ok(0))?,
|
||||
count_ss: raw.count_rank_ss.map(parse_from_str).unwrap_or(Ok(0))?,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue