Move to SQLite (#13)

This commit is contained in:
Natsu Kagami 2021-06-19 22:36:17 +09:00 committed by GitHub
parent 750ddb7762
commit 1799b70bc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2122 additions and 394 deletions

View file

@ -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]

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)))
}

View file

@ -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>>>>;
}

View file

@ -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)
}

View file

@ -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 ")

View file

@ -50,6 +50,7 @@ pub fn hook<'a>(
msg.channel_id,
&bm,
)
.await
.pls_ok();
}
EmbedType::Beatmapset(b) => {

View file

@ -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?;

View file

@ -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())
}
}

View file

@ -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)
})

View file

@ -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> {

View file

@ -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,
})

View file

@ -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)

View file

@ -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))?,