Set up testing infrastructure
This commit is contained in:
parent
e74ddfd106
commit
cba88c5def
18 changed files with 494 additions and 176 deletions
src
|
@ -73,6 +73,7 @@ impl ImageVec {
|
|||
// }}}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JacketCache {
|
||||
jackets: Vec<(u32, ImageVec)>,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::array;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::Utc;
|
||||
|
@ -6,9 +7,7 @@ use num::traits::Euclid;
|
|||
use num::CheckedDiv;
|
||||
use num::Rational32;
|
||||
use num::Zero;
|
||||
use poise::serenity_prelude::{
|
||||
Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
|
||||
};
|
||||
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp};
|
||||
use rusqlite::Row;
|
||||
|
||||
use crate::arcaea::chart::{Chart, Song};
|
||||
|
@ -21,7 +20,7 @@ use super::score::{Score, ScoringSystem};
|
|||
// {{{ Create play
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreatePlay {
|
||||
discord_attachment_id: Option<AttachmentId>,
|
||||
discord_attachment_id: Option<NonZeroU64>,
|
||||
|
||||
// Scoring details
|
||||
score: Score,
|
||||
|
@ -41,8 +40,8 @@ impl CreatePlay {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn with_attachment(mut self, attachment: &Attachment) -> Self {
|
||||
self.discord_attachment_id = Some(attachment.id);
|
||||
pub fn with_attachment(mut self, attachment_id: NonZeroU64) -> Self {
|
||||
self.discord_attachment_id = Some(attachment_id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
@ -110,12 +110,12 @@ async fn info(
|
|||
/// Show the best score on a given chart
|
||||
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||
async fn best(
|
||||
ctx: Context<'_>,
|
||||
mut ctx: Context<'_>,
|
||||
#[rest]
|
||||
#[description = "Name of chart to show (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
let user = get_user!(&mut ctx);
|
||||
|
||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||
let play = ctx
|
||||
|
@ -164,13 +164,13 @@ async fn best(
|
|||
/// Show the best score on a given chart
|
||||
#[poise::command(prefix_command, slash_command, user_cooldown = 10)]
|
||||
async fn plot(
|
||||
ctx: Context<'_>,
|
||||
mut ctx: Context<'_>,
|
||||
scoring_system: Option<ScoringSystem>,
|
||||
#[rest]
|
||||
#[description = "Name of chart to show (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
let user = get_user!(&mut ctx);
|
||||
let scoring_system = scoring_system.unwrap_or_default();
|
||||
|
||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||
|
|
210
src/commands/discord.rs
Normal file
210
src/commands/discord.rs
Normal file
|
@ -0,0 +1,210 @@
|
|||
use std::num::NonZeroU64;
|
||||
|
||||
use poise::serenity_prelude::{futures::future::join_all, CreateAttachment, CreateMessage};
|
||||
|
||||
use crate::{
|
||||
context::{Error, UserContext},
|
||||
timed,
|
||||
};
|
||||
|
||||
// {{{ Trait
|
||||
pub trait MessageContext {
|
||||
/// Get the user context held by the message
|
||||
fn data(&self) -> &UserContext;
|
||||
fn author_id(&self) -> u64;
|
||||
|
||||
/// Reply to the current message
|
||||
async fn reply(&mut self, text: &str) -> Result<(), Error>;
|
||||
|
||||
/// Deliver a message containing references to files.
|
||||
async fn send_files(
|
||||
&mut self,
|
||||
attachments: impl IntoIterator<Item = CreateAttachment>,
|
||||
message: CreateMessage,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
// {{{ Input attachments
|
||||
type Attachment;
|
||||
|
||||
fn is_image(attachment: &Self::Attachment) -> bool;
|
||||
fn filename(attachment: &Self::Attachment) -> &str;
|
||||
fn attachment_id(attachment: &Self::Attachment) -> NonZeroU64;
|
||||
|
||||
/// Downloads a single file
|
||||
async fn download(&self, attachment: &Self::Attachment) -> Result<Vec<u8>, Error>;
|
||||
|
||||
/// Downloads every image
|
||||
async fn download_images<'a>(
|
||||
&self,
|
||||
attachments: &'a Vec<Self::Attachment>,
|
||||
) -> Result<Vec<(&'a Self::Attachment, Vec<u8>)>, Error> {
|
||||
let download_tasks = attachments
|
||||
.iter()
|
||||
.filter(|file| Self::is_image(file))
|
||||
.map(|file| async move { (file, self.download(file).await) });
|
||||
|
||||
let downloaded = timed!("dowload_files", { join_all(download_tasks).await });
|
||||
downloaded
|
||||
.into_iter()
|
||||
.map(|(file, bytes)| Ok((file, bytes?)))
|
||||
.collect::<Result<_, Error>>()
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Poise implementation
|
||||
impl<'a, 'b> MessageContext
|
||||
for poise::Context<'a, UserContext, Box<dyn std::error::Error + Send + Sync + 'b>>
|
||||
{
|
||||
type Attachment = poise::serenity_prelude::Attachment;
|
||||
|
||||
fn data(&self) -> &UserContext {
|
||||
Self::data(*self)
|
||||
}
|
||||
|
||||
fn author_id(&self) -> u64 {
|
||||
self.author().id.get()
|
||||
}
|
||||
|
||||
async fn reply(&mut self, text: &str) -> Result<(), Error> {
|
||||
Self::reply(*self, text).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_files(
|
||||
&mut self,
|
||||
attachments: impl IntoIterator<Item = CreateAttachment>,
|
||||
message: CreateMessage,
|
||||
) -> Result<(), Error> {
|
||||
self.channel_id()
|
||||
.send_files(self.http(), attachments, message)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// {{{ Input attachments
|
||||
fn attachment_id(attachment: &Self::Attachment) -> NonZeroU64 {
|
||||
NonZeroU64::new(attachment.id.get()).unwrap()
|
||||
}
|
||||
|
||||
fn filename(attachment: &Self::Attachment) -> &str {
|
||||
&attachment.filename
|
||||
}
|
||||
|
||||
fn is_image(attachment: &Self::Attachment) -> bool {
|
||||
attachment.dimensions().is_some()
|
||||
}
|
||||
|
||||
async fn download(&self, attachment: &Self::Attachment) -> Result<Vec<u8>, Error> {
|
||||
let res = poise::serenity_prelude::Attachment::download(attachment).await?;
|
||||
Ok(res)
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Testing context
|
||||
pub mod mock {
|
||||
use std::{env, fs, path::PathBuf};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct MockContext {
|
||||
pub user_id: u64,
|
||||
pub data: UserContext,
|
||||
pub messages: Vec<(CreateMessage, Vec<CreateAttachment>)>,
|
||||
}
|
||||
|
||||
impl MockContext {
|
||||
pub fn new(data: UserContext) -> Self {
|
||||
Self {
|
||||
data,
|
||||
user_id: 666,
|
||||
messages: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to(&self, path: &PathBuf) -> Result<(), Error> {
|
||||
if env::var("SHIMMERING_TEST_REGEN").unwrap_or_default() == "1" {
|
||||
fs::remove_dir_all(path)?;
|
||||
}
|
||||
|
||||
fs::create_dir_all(path)?;
|
||||
for (i, (message, attachments)) in self.messages.iter().enumerate() {
|
||||
let dir = path.join(format!("{i}"));
|
||||
fs::create_dir_all(&dir)?;
|
||||
let message_file = dir.join("message.toml");
|
||||
|
||||
if message_file.exists() {
|
||||
assert_eq!(
|
||||
toml::to_string_pretty(message)?,
|
||||
fs::read_to_string(message_file)?
|
||||
);
|
||||
} else {
|
||||
fs::write(&message_file, toml::to_string_pretty(message)?)?;
|
||||
}
|
||||
|
||||
for attachment in attachments {
|
||||
let path = dir.join(&attachment.filename);
|
||||
|
||||
if path.exists() {
|
||||
assert_eq!(&attachment.data, &fs::read(path)?);
|
||||
} else {
|
||||
fs::write(&path, &attachment.data)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageContext for MockContext {
|
||||
fn author_id(&self) -> u64 {
|
||||
self.user_id
|
||||
}
|
||||
|
||||
fn data(&self) -> &UserContext {
|
||||
&self.data
|
||||
}
|
||||
|
||||
async fn reply(&mut self, text: &str) -> Result<(), Error> {
|
||||
self.messages
|
||||
.push((CreateMessage::new().content(text), Vec::new()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_files(
|
||||
&mut self,
|
||||
attachments: impl IntoIterator<Item = CreateAttachment>,
|
||||
message: CreateMessage,
|
||||
) -> Result<(), Error> {
|
||||
self.messages
|
||||
.push((message, attachments.into_iter().collect()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// {{{ Input attachments
|
||||
type Attachment = PathBuf;
|
||||
|
||||
fn filename(attachment: &Self::Attachment) -> &str {
|
||||
attachment.file_name().unwrap().to_str().unwrap()
|
||||
}
|
||||
|
||||
// This is a dumb implementation, but it works for testing...
|
||||
fn is_image(attachment: &Self::Attachment) -> bool {
|
||||
let ext = attachment.extension().unwrap();
|
||||
ext == "png" || ext == "jpg" || ext == "webp"
|
||||
}
|
||||
|
||||
fn attachment_id(_attachment: &Self::Attachment) -> NonZeroU64 {
|
||||
NonZeroU64::new(666).unwrap()
|
||||
}
|
||||
|
||||
async fn download(&self, attachment: &Self::Attachment) -> Result<Vec<u8>, Error> {
|
||||
let res = tokio::fs::read(attachment).await?;
|
||||
Ok(res)
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
}
|
||||
// }}}
|
|
@ -1,6 +1,7 @@
|
|||
use crate::context::{Context, Error};
|
||||
|
||||
pub mod chart;
|
||||
pub mod discord;
|
||||
pub mod score;
|
||||
pub mod stats;
|
||||
pub mod utils;
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use crate::arcaea::play::{CreatePlay, Play};
|
||||
use crate::arcaea::score::Score;
|
||||
use crate::context::{Context, Error};
|
||||
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
|
||||
use crate::user::{discord_id_to_discord_user, User};
|
||||
use crate::{edit_reply, get_user, timed};
|
||||
use crate::{get_user, timed};
|
||||
use image::DynamicImage;
|
||||
use poise::serenity_prelude::futures::future::join_all;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use poise::serenity_prelude::CreateMessage;
|
||||
use poise::{serenity_prelude as serenity, CreateReply};
|
||||
|
||||
use super::discord::MessageContext;
|
||||
|
||||
// {{{ Score
|
||||
/// Score management
|
||||
|
@ -24,13 +23,13 @@ pub async fn score(_ctx: Context<'_>) -> Result<(), Error> {
|
|||
}
|
||||
// }}}
|
||||
// {{{ Score magic
|
||||
/// Identify scores from attached images.
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn magic(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Images containing scores"] files: Vec<serenity::Attachment>,
|
||||
// {{{ Implementation
|
||||
async fn magic_impl<C: MessageContext>(
|
||||
ctx: &mut C,
|
||||
files: Vec<C::Attachment>,
|
||||
) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
let user = get_user!(ctx);
|
||||
let files = ctx.download_images(&files).await?;
|
||||
|
||||
if files.len() == 0 {
|
||||
ctx.reply("No images found attached to message").await?;
|
||||
|
@ -39,30 +38,9 @@ pub async fn magic(
|
|||
|
||||
let mut embeds = Vec::with_capacity(files.len());
|
||||
let mut attachments = Vec::with_capacity(files.len());
|
||||
let handle = ctx
|
||||
.reply(format!("Processed 0/{} scores", files.len()))
|
||||
.await?;
|
||||
|
||||
let mut analyzer = ImageAnalyzer::default();
|
||||
|
||||
// {{{ Download files
|
||||
let download_tasks = files
|
||||
.iter()
|
||||
.filter(|file| file.dimensions().is_some())
|
||||
.map(|file| async move { (file, file.download().await) });
|
||||
|
||||
let downloaded = timed!("dowload_files", { join_all(download_tasks).await });
|
||||
|
||||
if downloaded.len() < files.len() {
|
||||
ctx.reply("One or more of the attached files are not images!")
|
||||
.await?;
|
||||
}
|
||||
// }}}
|
||||
|
||||
for (i, (file, bytes)) in downloaded.into_iter().enumerate() {
|
||||
let bytes = bytes?;
|
||||
|
||||
let start = Instant::now();
|
||||
for (i, (attachment, bytes)) in files.into_iter().enumerate() {
|
||||
// {{{ Preapare image
|
||||
let mut image = timed!("decode image", { image::load_from_memory(&bytes)? });
|
||||
let mut grayscale_image = timed!("grayscale image", {
|
||||
|
@ -109,7 +87,14 @@ pub async fn magic(
|
|||
|
||||
// edit_reply!(ctx, handle, "Image {}: reading score", i + 1).await?;
|
||||
let score = timed!("read_score", {
|
||||
analyzer.read_score(ctx.data(), Some(chart.note_count), &grayscale_image, kind)?
|
||||
analyzer
|
||||
.read_score(ctx.data(), Some(chart.note_count), &grayscale_image, kind)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"Could not read score for chart {} [{:?}]: {err}",
|
||||
song.title, chart.difficulty
|
||||
)
|
||||
})?
|
||||
});
|
||||
|
||||
// {{{ Build play
|
||||
|
@ -117,7 +102,7 @@ pub async fn magic(
|
|||
Score::resolve_distibution_ambiguities(score, note_distribution, chart.note_count);
|
||||
|
||||
let play = CreatePlay::new(score)
|
||||
.with_attachment(file)
|
||||
.with_attachment(C::attachment_id(attachment))
|
||||
.with_fars(maybe_fars)
|
||||
.with_max_recall(max_recall)
|
||||
.save(&ctx.data(), &user, &chart)?;
|
||||
|
@ -136,41 +121,113 @@ pub async fn magic(
|
|||
|
||||
if let Err(err) = result {
|
||||
analyzer
|
||||
.send_discord_error(ctx, &image, &file.filename, err)
|
||||
.send_discord_error(ctx, &image, C::filename(&attachment), err)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let took = start.elapsed();
|
||||
|
||||
edit_reply!(
|
||||
ctx,
|
||||
handle,
|
||||
"Processed {}/{} scores. Last score took {took:?} to process.",
|
||||
i + 1,
|
||||
files.len()
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
handle.delete(ctx).await?;
|
||||
|
||||
if embeds.len() > 0 {
|
||||
ctx.channel_id()
|
||||
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
|
||||
ctx.send_files(attachments, CreateMessage::new().embeds(embeds))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
// {{{ Tests
|
||||
#[cfg(test)]
|
||||
mod magic_tests {
|
||||
use std::{path::PathBuf, process::Command, str::FromStr};
|
||||
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
||||
use crate::{
|
||||
commands::discord::mock::MockContext,
|
||||
context::{connect_db, get_shared_context},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
macro_rules! with_ctx {
|
||||
($test_path:expr, $f:expr) => {{
|
||||
let mut data = (*get_shared_context().await).clone();
|
||||
let dir = tempfile::tempdir()?;
|
||||
let path = dir.path().join("db.sqlite");
|
||||
println!("path {path:?}");
|
||||
data.db = connect_db(SqliteConnectionManager::file(path));
|
||||
|
||||
Command::new("scripts/import-charts.py")
|
||||
.env("SHIMMERING_DATA_DIR", dir.path().to_str().unwrap())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let mut ctx = MockContext::new(data);
|
||||
User::create_from_context(&ctx)?;
|
||||
|
||||
let res: Result<(), Error> = $f(&mut ctx).await;
|
||||
res?;
|
||||
|
||||
ctx.write_to(&PathBuf::from_str($test_path)?)?;
|
||||
Ok(())
|
||||
}};
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_pics() -> Result<(), Error> {
|
||||
with_ctx!("test/commands/score/magic/no_pics", async |ctx| {
|
||||
magic_impl(ctx, vec![]).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_pic() -> Result<(), Error> {
|
||||
with_ctx!("test/commands/score/magic/single_pic", async |ctx| {
|
||||
magic_impl(
|
||||
ctx,
|
||||
vec![PathBuf::from_str("test/screenshots/alter_ego.jpg")?],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn weird_kerning() -> Result<(), Error> {
|
||||
with_ctx!("test/commands/score/magic/weird_kerning", async |ctx| {
|
||||
magic_impl(
|
||||
ctx,
|
||||
vec![
|
||||
PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?,
|
||||
PathBuf::from_str("test/screenshots/genocider_24_kerning.jpg")?,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
||||
/// Identify scores from attached images.
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn magic(
|
||||
mut ctx: Context<'_>,
|
||||
#[description = "Images containing scores"] files: Vec<serenity::Attachment>,
|
||||
) -> Result<(), Error> {
|
||||
magic_impl(&mut ctx, files).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// }}}
|
||||
// {{{ Score delete
|
||||
/// Delete scores, given their IDs.
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn delete(
|
||||
ctx: Context<'_>,
|
||||
mut ctx: Context<'_>,
|
||||
#[description = "Id of score to delete"] ids: Vec<u32>,
|
||||
) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
let user = get_user!(&mut ctx);
|
||||
|
||||
if ids.len() == 0 {
|
||||
ctx.reply("Empty ID list provided").await?;
|
||||
|
|
|
@ -43,7 +43,7 @@ pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> {
|
|||
// }}}
|
||||
// {{{ Render best plays
|
||||
async fn best_plays(
|
||||
ctx: &Context<'_>,
|
||||
ctx: &mut Context<'_>,
|
||||
user: &User,
|
||||
scoring_system: ScoringSystem,
|
||||
grid_size: (u32, u32),
|
||||
|
@ -403,7 +403,7 @@ async fn best_plays(
|
|||
ImageBuffer::from_raw(width, height, drawer.canvas.buffer.into_vec()).unwrap(),
|
||||
);
|
||||
|
||||
debug_image_log(&image)?;
|
||||
debug_image_log(&image);
|
||||
|
||||
if image.height() > 4096 {
|
||||
image = image.resize(4096, 4096, image::imageops::FilterType::Nearest);
|
||||
|
@ -426,10 +426,10 @@ async fn best_plays(
|
|||
// {{{ B30
|
||||
/// Show the 30 best scores
|
||||
#[poise::command(prefix_command, slash_command, user_cooldown = 30)]
|
||||
pub async fn b30(ctx: Context<'_>, scoring_system: Option<ScoringSystem>) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
pub async fn b30(mut ctx: Context<'_>, scoring_system: Option<ScoringSystem>) -> Result<(), Error> {
|
||||
let user = get_user!(&mut ctx);
|
||||
best_plays(
|
||||
&ctx,
|
||||
&mut ctx,
|
||||
&user,
|
||||
scoring_system.unwrap_or_default(),
|
||||
(5, 6),
|
||||
|
@ -440,15 +440,15 @@ pub async fn b30(ctx: Context<'_>, scoring_system: Option<ScoringSystem>) -> Res
|
|||
|
||||
#[poise::command(prefix_command, slash_command, hide_in_help, global_cooldown = 5)]
|
||||
pub async fn bany(
|
||||
ctx: Context<'_>,
|
||||
mut ctx: Context<'_>,
|
||||
scoring_system: Option<ScoringSystem>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
let user = get_user!(&mut ctx);
|
||||
assert_is_pookie!(ctx, user);
|
||||
best_plays(
|
||||
&ctx,
|
||||
&mut ctx,
|
||||
&user,
|
||||
scoring_system.unwrap_or_default(),
|
||||
(width, height),
|
||||
|
@ -460,8 +460,8 @@ pub async fn bany(
|
|||
// {{{ Meta
|
||||
/// Show stats about the bot itself.
|
||||
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||
async fn meta(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
async fn meta(mut ctx: Context<'_>) -> Result<(), Error> {
|
||||
let user = get_user!(&mut ctx);
|
||||
let conn = ctx.data().db.get()?;
|
||||
let song_count: usize = conn
|
||||
.prepare_cached("SELECT count() as count FROM songs")?
|
||||
|
|
|
@ -35,7 +35,7 @@ macro_rules! reply_errors {
|
|||
match $value {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
$ctx.reply(format!("{err}")).await?;
|
||||
crate::commands::discord::MessageContext::reply($ctx, &format!("{err}")).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ pub type Context<'a> = poise::Context<'a, UserContext, Error>;
|
|||
pub type DbConnection = r2d2::Pool<SqliteConnectionManager>;
|
||||
|
||||
// Custom user data passed to all command functions
|
||||
#[derive(Clone)]
|
||||
pub struct UserContext {
|
||||
pub db: DbConnection,
|
||||
pub song_cache: SongCache,
|
||||
|
@ -32,36 +33,34 @@ pub struct UserContext {
|
|||
pub kazesawa_bold_measurements: CharMeasurements,
|
||||
}
|
||||
|
||||
pub fn connect_db(manager: SqliteConnectionManager) -> DbConnection {
|
||||
timed!("create_sqlite_pool", {
|
||||
Pool::new(manager.with_init(|conn| {
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
static MIGRATIONS: LazyLock<Migrations> = LazyLock::new(|| {
|
||||
Migrations::from_directory(&MIGRATIONS_DIR).expect("Could not load migrations")
|
||||
});
|
||||
|
||||
MIGRATIONS
|
||||
.to_latest(conn)
|
||||
.expect("Could not run migrations");
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
.expect("Could not open sqlite database.")
|
||||
})
|
||||
}
|
||||
|
||||
impl UserContext {
|
||||
#[inline]
|
||||
pub async fn new() -> Result<Self, Error> {
|
||||
timed!("create_context", {
|
||||
fs::create_dir_all(get_data_dir())?;
|
||||
|
||||
// {{{ Connect to database
|
||||
let db = timed!("create_sqlite_pool", {
|
||||
Pool::new(
|
||||
SqliteConnectionManager::file(&format!(
|
||||
"{}/db.sqlite",
|
||||
get_data_dir().to_str().unwrap()
|
||||
))
|
||||
.with_init(|conn| {
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
static MIGRATIONS: LazyLock<Migrations> = LazyLock::new(|| {
|
||||
Migrations::from_directory(&MIGRATIONS_DIR)
|
||||
.expect("Could not load migrations")
|
||||
});
|
||||
|
||||
MIGRATIONS
|
||||
.to_latest(conn)
|
||||
.expect("Could not run migrations");
|
||||
|
||||
Ok(())
|
||||
}),
|
||||
)
|
||||
.expect("Could not open sqlite database.")
|
||||
});
|
||||
// }}}
|
||||
let db = connect_db(SqliteConnectionManager::file(&format!(
|
||||
"{}/db.sqlite",
|
||||
get_data_dir().to_str().unwrap()
|
||||
)));
|
||||
|
||||
let mut song_cache = timed!("make_song_cache", { SongCache::new(&db)? });
|
||||
let jacket_cache = timed!("make_jacket_cache", { JacketCache::new(&mut song_cache)? });
|
||||
|
@ -93,3 +92,9 @@ impl UserContext {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_shared_context() -> &'static UserContext {
|
||||
static CELL: tokio::sync::OnceCell<UserContext> = tokio::sync::OnceCell::const_new();
|
||||
CELL.get_or_init(async || UserContext::new().await.unwrap())
|
||||
.await
|
||||
}
|
||||
|
|
30
src/logs.rs
30
src/logs.rs
|
@ -10,7 +10,7 @@ use std::{env, ops::Deref, path::PathBuf, sync::OnceLock, time::Instant};
|
|||
|
||||
use image::{DynamicImage, EncodableLayout, ImageBuffer, PixelWithColorType};
|
||||
|
||||
use crate::{assets::get_path, context::Error};
|
||||
use crate::assets::get_path;
|
||||
|
||||
#[inline]
|
||||
fn should_save_debug_images() -> bool {
|
||||
|
@ -31,30 +31,30 @@ fn get_startup_time() -> Instant {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn debug_image_log(image: &DynamicImage) -> Result<(), Error> {
|
||||
pub fn debug_image_log(image: &DynamicImage) {
|
||||
if should_save_debug_images() {
|
||||
image.save(get_log_dir().join(format!(
|
||||
"{:0>15}.png",
|
||||
get_startup_time().elapsed().as_nanos()
|
||||
)))?;
|
||||
image
|
||||
.save(get_log_dir().join(format!(
|
||||
"{:0>15}.png",
|
||||
get_startup_time().elapsed().as_nanos()
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn debug_image_buffer_log<P, C>(image: &ImageBuffer<P, C>) -> Result<(), Error>
|
||||
pub fn debug_image_buffer_log<P, C>(image: &ImageBuffer<P, C>)
|
||||
where
|
||||
P: PixelWithColorType,
|
||||
[P::Subpixel]: EncodableLayout,
|
||||
C: Deref<Target = [P::Subpixel]>,
|
||||
{
|
||||
if should_save_debug_images() {
|
||||
image.save(get_log_dir().join(format!(
|
||||
"./logs/{:0>15}.png",
|
||||
get_startup_time().elapsed().as_nanos()
|
||||
)))?;
|
||||
image
|
||||
.save(get_log_dir().join(format!(
|
||||
"{:0>15}.png",
|
||||
get_startup_time().elapsed().as_nanos()
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#![feature(try_blocks)]
|
||||
#![feature(thread_local)]
|
||||
#![feature(generic_arg_infer)]
|
||||
#![feature(lazy_cell_consume)]
|
||||
|
||||
mod arcaea;
|
||||
mod assets;
|
||||
|
|
|
@ -164,7 +164,7 @@ impl ComponentsWithBounds {
|
|||
binarisation_threshold,
|
||||
ThresholdType::Binary,
|
||||
);
|
||||
debug_image_buffer_log(&image)?;
|
||||
debug_image_buffer_log(&image);
|
||||
|
||||
let background = Luma([u8::MAX]);
|
||||
let components = connected_components(&image, Connectivity::Eight, background);
|
||||
|
@ -223,6 +223,7 @@ impl ComponentsWithBounds {
|
|||
}
|
||||
// }}}
|
||||
// {{{ Char measurements
|
||||
#[derive(Clone)]
|
||||
pub struct CharMeasurements {
|
||||
chars: Vec<(char, ComponentVec)>,
|
||||
|
||||
|
@ -258,7 +259,7 @@ impl CharMeasurements {
|
|||
.ok_or_else(|| "Failed to turn buffer into canvas")?;
|
||||
let image = DynamicImage::ImageRgb8(buffer);
|
||||
|
||||
debug_image_log(&image)?;
|
||||
debug_image_log(&image);
|
||||
|
||||
let components = ComponentsWithBounds::from_image(&image, 100)?;
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ use crate::arcaea::chart::{Chart, Difficulty, Song, DIFFICULTY_MENU_PIXEL_COLORS
|
|||
use crate::arcaea::jacket::IMAGE_VEC_DIM;
|
||||
use crate::arcaea::score::Score;
|
||||
use crate::bitmap::{Color, Rect};
|
||||
use crate::context::{Context, Error, UserContext};
|
||||
use crate::commands::discord::MessageContext;
|
||||
use crate::context::{Error, UserContext};
|
||||
use crate::levenshtein::edit_distance;
|
||||
use crate::logs::debug_image_log;
|
||||
use crate::recognition::fuzzy_song_name::guess_chart_name;
|
||||
|
@ -61,7 +62,7 @@ impl ImageAnalyzer {
|
|||
self.last_rect = Some((ui_rect, rect));
|
||||
|
||||
let result = self.crop(image, rect);
|
||||
debug_image_log(&result)?;
|
||||
debug_image_log(&result);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
@ -80,7 +81,7 @@ impl ImageAnalyzer {
|
|||
let result = self.crop(image, rect);
|
||||
let result = result.resize(size.0, size.1, FilterType::Nearest);
|
||||
|
||||
debug_image_log(&result)?;
|
||||
debug_image_log(&result);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
@ -88,7 +89,7 @@ impl ImageAnalyzer {
|
|||
// {{{ Error handling
|
||||
pub async fn send_discord_error(
|
||||
&mut self,
|
||||
ctx: Context<'_>,
|
||||
ctx: &mut impl MessageContext,
|
||||
image: &DynamicImage,
|
||||
filename: &str,
|
||||
err: impl Display,
|
||||
|
@ -112,14 +113,12 @@ impl ImageAnalyzer {
|
|||
));
|
||||
|
||||
let msg = CreateMessage::default().embed(embed);
|
||||
ctx.channel_id()
|
||||
.send_files(ctx.http(), [error_attachement], msg)
|
||||
.await?;
|
||||
ctx.send_files([error_attachement], msg).await?;
|
||||
} else {
|
||||
embed = embed.title("An error occurred");
|
||||
|
||||
let msg = CreateMessage::default().embed(embed);
|
||||
ctx.channel_id().send_files(ctx.http(), [], msg).await?;
|
||||
ctx.send_files([], msg).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -347,7 +346,8 @@ impl ImageAnalyzer {
|
|||
out[i] = ctx
|
||||
.kazesawa_bold_measurements
|
||||
.recognise(&image, "0123456789", Some(30))?
|
||||
.parse()?;
|
||||
.parse()
|
||||
.unwrap_or(100000); // This will get discarded as making no sense
|
||||
}
|
||||
|
||||
println!("Ditribution {out:?}");
|
||||
|
|
|
@ -60,7 +60,7 @@ impl UIMeasurementRect {
|
|||
pub const UI_RECT_COUNT: usize = 15;
|
||||
// }}}
|
||||
// {{{ Measurement
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UIMeasurement {
|
||||
dimensions: [u32; 2],
|
||||
datapoints: [u32; UI_RECT_COUNT * 4],
|
||||
|
@ -87,7 +87,7 @@ impl UIMeasurement {
|
|||
}
|
||||
// }}}
|
||||
// {{{ Measurements
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UIMeasurements {
|
||||
pub measurements: Vec<UIMeasurement>,
|
||||
}
|
||||
|
|
32
src/user.rs
32
src/user.rs
|
@ -3,7 +3,10 @@ use std::str::FromStr;
|
|||
use poise::serenity_prelude::UserId;
|
||||
use rusqlite::Row;
|
||||
|
||||
use crate::context::{Context, Error, UserContext};
|
||||
use crate::{
|
||||
commands::discord::MessageContext,
|
||||
context::{Context, Error, UserContext},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
|
@ -22,8 +25,31 @@ impl User {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn from_context(ctx: &Context<'_>) -> Result<Self, Error> {
|
||||
let id = ctx.author().id.get().to_string();
|
||||
pub fn create_from_context(ctx: &impl MessageContext) -> Result<Self, Error> {
|
||||
let discord_id = ctx.author_id().to_string();
|
||||
let user_id: u32 = ctx
|
||||
.data()
|
||||
.db
|
||||
.get()?
|
||||
.prepare_cached(
|
||||
"
|
||||
INSERT INTO users(discord_id) VALUES (?)
|
||||
RETURNING id
|
||||
",
|
||||
)?
|
||||
.query_map([&discord_id], |row| row.get("id"))?
|
||||
.next()
|
||||
.ok_or_else(|| "Failed to create user")??;
|
||||
|
||||
Ok(Self {
|
||||
discord_id,
|
||||
id: user_id,
|
||||
is_pookie: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_context(ctx: &impl MessageContext) -> Result<Self, Error> {
|
||||
let id = ctx.author_id();
|
||||
let user = ctx
|
||||
.data()
|
||||
.db
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue