Added edit_img command

+ Refactored image editing so it's all handled in the same way
+ Added pipeline concept for more advanced edits
This commit is contained in:
Joey Hines 2025-07-06 20:38:09 -06:00
parent fe99a6f768
commit dfffe202df
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
9 changed files with 477 additions and 293 deletions

2
Cargo.lock generated
View File

@ -1093,7 +1093,7 @@ dependencies = [
[[package]] [[package]]
name = "fren" name = "fren"
version = "1.7.0" version = "1.8.0"
dependencies = [ dependencies = [
"axum 0.8.1", "axum 0.8.1",
"base64 0.22.1", "base64 0.22.1",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "fren" name = "fren"
version = "1.7.0" version = "1.8.0"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,200 +1,14 @@
use crate::album_manager::{AlbumQuery, ImageQuery, ImageSort}; use crate::album_manager::AlbumQuery;
use crate::discord::Context; use crate::discord::Context;
use crate::error::Error; use crate::error::Error;
use crate::models::motivation::{Motivation, MotivationConfig}; use crate::image_manipulation::{
use magick_rust::{CompositeOperator, DrawingWand, FilterType, MagickWand, PixelWand}; BaseImage, ModifyImage, ModifyImageArgError, create_green_screen_image,
use poise::serenity_prelude::builder::{CreateAttachment, CreateMessage}; create_motivation_image, create_overlay_image, image_pipeline,
use rand::Rng;
use rand::distr::{Distribution, StandardUniform};
use reqwest::{Client, Url};
use serde::Deserialize;
use std::borrow::Cow;
#[derive(Debug, Deserialize)]
pub enum Alignment {
Left,
Center,
Right,
}
impl Alignment {
pub fn offset(&self) -> f64 {
match self {
Alignment::Left => 0.25,
Alignment::Center => 0.5,
Alignment::Right => 0.75,
}
}
}
impl Distribution<Alignment> for StandardUniform {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Alignment {
match rng.random_range(0..=2) {
0 => Alignment::Left,
1 => Alignment::Center,
_ => Alignment::Right,
}
}
}
pub async fn create_greenscreen_image(
background_image_url: Url,
foreground_image_url: Url,
alignment: Alignment,
) -> Result<Vec<u8>, Error> {
let client = Client::new();
let background_image_blob = client
.get(background_image_url)
.send()
.await?
.bytes()
.await?
.to_vec();
let foreground_image_blob = client
.get(foreground_image_url)
.send()
.await?
.bytes()
.await?
.to_vec();
let background_wand = MagickWand::new();
let foreground_wand = MagickWand::new();
background_wand.read_image_blob(background_image_blob)?;
foreground_wand.read_image_blob(foreground_image_blob)?;
let background_height = background_wand.get_image_height();
let background_width = background_wand.get_image_width();
let foreground_height = foreground_wand.get_image_height();
let foreground_width = foreground_wand.get_image_width();
let ratio = foreground_height as f64 / foreground_width as f64;
let (scale_height, scale_width) =
if foreground_height > background_height && foreground_width > background_width {
(
background_height as f64 / foreground_height as f64,
background_width as f64 / foreground_width as f64,
)
} else if foreground_height > background_height {
let scale = background_height as f64 / foreground_height as f64;
(scale, scale * ratio)
} else if foreground_width > background_width {
let scale = background_width as f64 / foreground_width as f64;
(scale * ratio, scale)
} else {
(1.0, 1.0)
}; };
use crate::models::motivation::MotivationConfig;
foreground_wand.scale_image(scale_width * 0.6, scale_height * 0.6, FilterType::Lanczos2)?; use poise::serenity_prelude::builder::{CreateAttachment, CreateMessage};
use std::borrow::Cow;
let pos_x = background_width as f64 * alignment.offset() use std::str::FromStr;
- foreground_wand.get_image_width() as f64 / 2.0;
let pos_y = background_height - foreground_wand.get_image_height();
background_wand.compose_images(
&foreground_wand,
CompositeOperator::Atop,
true,
pos_x as isize,
pos_y as isize,
)?;
Ok(background_wand.write_image_blob("png")?)
}
pub async fn create_overlay_image(
background_image_url: Url,
overlay_image_url: Url,
) -> Result<Vec<u8>, Error> {
let client = Client::new();
let background_image_blob = client
.get(background_image_url)
.send()
.await?
.bytes()
.await?
.to_vec();
let overlay_image_blob = client
.get(overlay_image_url)
.send()
.await?
.bytes()
.await?
.to_vec();
let background_wand = MagickWand::new();
let foreground_wand = MagickWand::new();
background_wand.read_image_blob(background_image_blob)?;
foreground_wand.read_image_blob(overlay_image_blob)?;
let overlay_height = background_wand.get_image_height();
let overlay_width = background_wand.get_image_width();
foreground_wand.resize_image(overlay_width, overlay_height, FilterType::Lanczos2)?;
background_wand.compose_images(&foreground_wand, CompositeOperator::Atop, true, 0, 0)?;
Ok(background_wand.write_image_blob("png")?)
}
pub async fn create_motivation_image(motivation: Motivation) -> Result<Vec<u8>, Error> {
let client = Client::new();
let motivation_image_blob = client
.get(motivation.image.link)
.send()
.await?
.bytes()
.await?
.to_vec();
let text = format!("{} {}", motivation.action, motivation.goal);
let mut wand = MagickWand::new();
let mut border_wand = PixelWand::new();
wand.read_image_blob(motivation_image_blob)?;
border_wand.set_color(&motivation.border_color)?;
let width = wand.get_image_width();
let border = width / 100;
wand.border_image(
&border_wand,
border,
border,
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
)?;
border_wand.set_color("black")?;
let width = wand.get_image_width();
let border = width * 20 / 100;
wand.border_image(
&border_wand,
border,
border,
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
)?;
let text_pos_x = wand.get_image_width() as f64 / 2.0;
let text_pos_y = wand.get_image_height() as f64 - (wand.get_image_height() as f64 * 0.05);
let mut text_wand = DrawingWand::new();
let mut text_color_wand = PixelWand::new();
text_color_wand.set_color("white")?;
text_wand.set_fill_color(&text_color_wand);
text_wand.set_font_size(0.07 * (wand.get_image_width() as f64));
text_wand.set_text_alignment(magick_rust::AlignType::Center);
wand.annotate_image(&text_wand, text_pos_x, text_pos_y, 0.0, &text)?;
Ok(wand.write_image_blob("jpg")?)
}
/// Become motivated /// Become motivated
#[poise::command(prefix_command, category = "Image")] #[poise::command(prefix_command, category = "Image")]
@ -202,15 +16,8 @@ pub async fn motivation(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Album to use for the motivation"] album_name: Option<String>, #[description = "Album to use for the motivation"] album_name: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let motivation = MotivationConfig::generate_motivation( let image =
&ctx.data().db, create_motivation_image(&ctx.data().db, &ctx.data().picox, "white", album_name).await?;
&ctx.data().picox,
"white",
album_name,
)
.await?;
let image = create_motivation_image(motivation).await?;
ctx.channel_id() ctx.channel_id()
.send_message( .send_message(
@ -234,38 +41,8 @@ pub async fn green_screen(
#[description = "Album to use as the foreground image"] foreground: Option<String>, #[description = "Album to use as the foreground image"] foreground: Option<String>,
#[description = "Album to use for the background image"] background: Option<String>, #[description = "Album to use for the background image"] background: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let foreground = ctx
.data()
.picox
.query_image(ImageQuery {
album: foreground,
tags: vec![],
order: ImageSort::Random,
limit: 1,
})
.await?
.first()
.cloned()
.unwrap();
let background = ctx
.data()
.picox
.query_image(ImageQuery {
album: background,
tags: vec![],
order: ImageSort::Random,
limit: 1,
})
.await?
.first()
.cloned()
.unwrap();
let alignment: Alignment = rand::random();
let image = let image =
create_greenscreen_image(background.link.clone(), foreground.link.clone(), alignment) create_green_screen_image(&ctx.data().db, &ctx.data().picox, foreground, background)
.await?; .await?;
ctx.channel_id() ctx.channel_id()
@ -283,42 +60,15 @@ pub async fn green_screen(
Ok(()) Ok(())
} }
/// Border /// Overlay an image on another
#[poise::command(prefix_command, category = "Image")] #[poise::command(prefix_command, category = "Image")]
pub async fn overlay( pub async fn overlay(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Album to use as the border image"] overlay: Option<String>, #[description = "Album to use as the border image"] overlay: Option<String>,
#[description = "Album to use for the background image"] background: Option<String>, #[description = "Album to use for the background image"] background: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let foreground = ctx let image =
.data() create_overlay_image(&ctx.data().db, &ctx.data().picox, overlay, background).await?;
.picox
.query_image(ImageQuery {
album: overlay,
tags: vec![],
order: ImageSort::Random,
limit: 1,
})
.await?
.first()
.cloned()
.unwrap();
let background = ctx
.data()
.picox
.query_image(ImageQuery {
album: background,
tags: vec![],
order: ImageSort::Random,
limit: 1,
})
.await?
.first()
.cloned()
.unwrap();
let image = create_overlay_image(background.link.clone(), foreground.link.clone()).await?;
ctx.channel_id() ctx.channel_id()
.send_message( .send_message(
@ -335,6 +85,46 @@ pub async fn overlay(
Ok(()) Ok(())
} }
/// Mix and match editing tricks to create art for the soul
#[poise::command(prefix_command, category = "Image")]
pub async fn edit_img(
ctx: Context<'_>,
#[description = "Base image album to use for your master piece, use 'any' if you want a random image"]
base_image_album: String,
#[description = "One or many edits to make"] pipeline: Vec<String>,
) -> Result<(), Error> {
let base_image = if base_image_album.eq_ignore_ascii_case("any") {
BaseImage::ByAlbum { album_name: None }
} else {
BaseImage::ByAlbum {
album_name: Some(base_image_album),
}
};
let pipeline: Result<Vec<ModifyImage>, ModifyImageArgError> = pipeline
.iter()
.map(|s| ModifyImage::from_str(s.as_str()))
.collect();
let pipeline = pipeline?;
let image = image_pipeline(&ctx.data().db, &ctx.data().picox, base_image, pipeline).await?;
ctx.channel_id()
.send_message(
&ctx,
CreateMessage::new()
.content("I fear this is, the most perfect art created by humans. its all over, we have peacked:")
.add_file(CreateAttachment::bytes(
Cow::from(image),
"overlay.jpg".to_string(),
)),
)
.await?;
Ok(())
}
/// Add an album to the pool of images to be used for motivations /// Add an album to the pool of images to be used for motivations
#[poise::command(prefix_command, category = "Image")] #[poise::command(prefix_command, category = "Image")]
pub async fn motivation_add_album( pub async fn motivation_add_album(

View File

@ -339,6 +339,7 @@ pub async fn run_bot(global_data: GlobalData) {
image::motivation_add_album(), image::motivation_add_album(),
image::green_screen(), image::green_screen(),
image::overlay(), image::overlay(),
image::edit_img(),
movie::add_movie(), movie::add_movie(),
movie::list_movies(), movie::list_movies(),
movie::rate_movie(), movie::rate_movie(),

View File

@ -1,9 +1,8 @@
use crate::config::GlobalData; use crate::config::GlobalData;
use crate::discord::image::create_motivation_image;
use crate::discord::{Context, get_role}; use crate::discord::{Context, get_role};
use crate::error::Error; use crate::error::Error;
use crate::image_manipulation::create_motivation_image;
use crate::inventory::{InventoryError, ItemData, ItemType, Operation, nft_value}; use crate::inventory::{InventoryError, ItemData, ItemType, Operation, nft_value};
use crate::models::motivation::MotivationConfig;
use crate::models::task::{Task, TaskType}; use crate::models::task::{Task, TaskType};
use crate::user::{User, UserError}; use crate::user::{User, UserError};
use poise::serenity_prelude::all::{ use poise::serenity_prelude::all::{
@ -432,14 +431,8 @@ pub async fn restock_shop(
tokio::fs::create_dir(&global_data.cfg.nft_path).await? tokio::fs::create_dir(&global_data.cfg.nft_path).await?
} }
let nft_motivation = MotivationConfig::generate_motivation( let nft =
&global_data.db, create_motivation_image(&global_data.db, &global_data.picox, "gold", None).await?;
&global_data.picox,
"gold",
None,
)
.await?;
let nft = create_motivation_image(nft_motivation).await?;
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
hasher.write(&nft); hasher.write(&nft);
let nft_hash = hasher.finish(); let nft_hash = hasher.finish();

View File

@ -1,4 +1,5 @@
use crate::discord::voices::VoiceError; use crate::discord::voices::VoiceError;
use crate::image_manipulation::ModifyImageArgError;
use crate::user; use crate::user;
use magick_rust::MagickError; use magick_rust::MagickError;
use serde::ser::StdError; use serde::ser::StdError;
@ -22,6 +23,8 @@ pub enum Error {
IoError(std::io::Error), IoError(std::io::Error),
VoiceError(VoiceError), VoiceError(VoiceError),
CTAError(cta_api::Error), CTAError(cta_api::Error),
NoImageFound,
PipelineArgumentError(ModifyImageArgError),
} }
impl StdError for Error {} impl StdError for Error {}
@ -86,6 +89,12 @@ impl From<cta_api::Error> for Error {
} }
} }
impl From<ModifyImageArgError> for Error {
fn from(value: ModifyImageArgError) -> Self {
Self::PipelineArgumentError(value)
}
}
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -103,6 +112,8 @@ impl Display for Error {
Error::IoError(err) => write!(f, "IO Error: {}", err), Error::IoError(err) => write!(f, "IO Error: {}", err),
Error::VoiceError(err) => write!(f, "Voice error: {}", err), Error::VoiceError(err) => write!(f, "Voice error: {}", err),
Error::CTAError(err) => write!(f, "The CTA had an error, I'm shocked: {}", err), Error::CTAError(err) => write!(f, "The CTA had an error, I'm shocked: {}", err),
Error::NoImageFound => write!(f, "Image not found"),
Error::PipelineArgumentError(err) => write!(f, "{}", err),
} }
} }
} }

View File

@ -0,0 +1,387 @@
use crate::album_manager::{AlbumManager, ImageQuery, ImageSort};
use crate::error::Error;
use crate::models::motivation::{MotivationConfig};
use j_db::database::Database;
use j_db::error::JDbError;
use magick_rust::{CompositeOperator, DrawingWand, FilterType, MagickWand, PixelWand};
use rand::Rng;
use rand::distr::{Distribution, StandardUniform};
use reqwest::{Client, Url};
use serde::Deserialize;
use std::str::FromStr;
use thiserror::Error;
pub async fn create_motivation_image(
db: &Database,
picox: &AlbumManager,
color: &str,
album_name: Option<String>,
) -> Result<Vec<u8>, Error> {
image_pipeline(
db,
picox,
BaseImage::Motivation { album_name },
vec![ModifyImage::Motivation {
border_color: Some(color.to_string()),
}],
)
.await
}
pub async fn create_green_screen_image(
db: &Database,
picox: &AlbumManager,
foreground_album_name: Option<String>,
background_album_name: Option<String>,
) -> Result<Vec<u8>, Error> {
image_pipeline(
db,
picox,
BaseImage::ByAlbum {
album_name: foreground_album_name,
},
vec![ModifyImage::GreenScreen {
align: rand::random(),
background_album: background_album_name,
}],
)
.await
}
pub async fn create_overlay_image(
db: &Database,
picox: &AlbumManager,
overlay_album_name: Option<String>,
background_album_name: Option<String>,
) -> Result<Vec<u8>, Error> {
image_pipeline(
db,
picox,
BaseImage::ByAlbum {
album_name: background_album_name,
},
vec![ModifyImage::Overlay {
overlay_album: overlay_album_name,
}],
)
.await
}
pub async fn get_image_from_url(url: Url) -> Result<Vec<u8>, Error> {
let client = Client::new();
let image_blob = client.get(url).send().await?.bytes().await?.to_vec();
Ok(image_blob)
}
pub async fn get_album_image(
picox: &AlbumManager,
album: Option<String>,
) -> Result<Vec<u8>, Error> {
let img = picox
.query_image(ImageQuery {
album,
tags: vec![],
order: ImageSort::Random,
limit: 1,
})
.await?
.first()
.cloned()
.ok_or(Error::NoImageFound)?;
get_image_from_url(img.link).await
}
async fn apply_greenscreen(
picox: &AlbumManager,
img_blob: Vec<u8>,
background_album: Option<String>,
alignment: &Alignment,
) -> Result<Vec<u8>, Error> {
let background_image_blob = get_album_image(picox, background_album).await?;
let background_wand = MagickWand::new();
let foreground_wand = MagickWand::new();
background_wand.read_image_blob(background_image_blob)?;
foreground_wand.read_image_blob(img_blob)?;
let background_height = background_wand.get_image_height();
let background_width = background_wand.get_image_width();
let foreground_height = foreground_wand.get_image_height();
let foreground_width = foreground_wand.get_image_width();
let ratio = foreground_height as f64 / foreground_width as f64;
let (scale_height, scale_width) =
if foreground_height > background_height && foreground_width > background_width {
(
background_height as f64 / foreground_height as f64,
background_width as f64 / foreground_width as f64,
)
} else if foreground_height > background_height {
let scale = background_height as f64 / foreground_height as f64;
(scale, scale * ratio)
} else if foreground_width > background_width {
let scale = background_width as f64 / foreground_width as f64;
(scale * ratio, scale)
} else {
(1.0, 1.0)
};
foreground_wand.scale_image(scale_width * 0.6, scale_height * 0.6, FilterType::Lanczos2)?;
let pos_x = background_width as f64 * alignment.offset()
- foreground_wand.get_image_width() as f64 / 2.0;
let pos_y = background_height - foreground_wand.get_image_height();
background_wand.compose_images(
&foreground_wand,
CompositeOperator::Atop,
true,
pos_x as isize,
pos_y as isize,
)?;
Ok(background_wand.write_image_blob("png")?)
}
async fn apply_overlay(
picox: &AlbumManager,
img_blob: Vec<u8>,
overlay_album: Option<String>,
) -> Result<Vec<u8>, Error> {
let overlay_image_blob = get_album_image(picox, overlay_album).await?;
let background_wand = MagickWand::new();
let foreground_wand = MagickWand::new();
background_wand.read_image_blob(img_blob)?;
foreground_wand.read_image_blob(overlay_image_blob)?;
let overlay_height = background_wand.get_image_height();
let overlay_width = background_wand.get_image_width();
foreground_wand.resize_image(overlay_width, overlay_height, FilterType::Lanczos2)?;
background_wand.compose_images(&foreground_wand, CompositeOperator::Atop, true, 0, 0)?;
Ok(background_wand.write_image_blob("png")?)
}
async fn apply_motivation_layer(
db: &Database,
img_blob: Vec<u8>,
border_color: Option<String>,
) -> Result<Vec<u8>, Error> {
let motivation =
MotivationConfig::generate_motivation(db, &border_color.unwrap_or("white".to_string()))
.await?;
let text = format!("{} {}", motivation.action, motivation.goal);
let mut wand = MagickWand::new();
let mut border_wand = PixelWand::new();
wand.read_image_blob(img_blob)?;
border_wand.set_color(&motivation.border_color)?;
let width = wand.get_image_width();
let border = width / 100;
wand.border_image(
&border_wand,
border,
border,
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
)?;
border_wand.set_color("black")?;
let width = wand.get_image_width();
let border = width * 20 / 100;
wand.border_image(
&border_wand,
border,
border,
magick_rust::bindings::CompositeOperator_OverCompositeOp.into(),
)?;
let text_pos_x = wand.get_image_width() as f64 / 2.0;
let text_pos_y = wand.get_image_height() as f64 - (wand.get_image_height() as f64 * 0.05);
let mut text_wand = DrawingWand::new();
let mut text_color_wand = PixelWand::new();
text_color_wand.set_color("white")?;
text_wand.set_fill_color(&text_color_wand);
text_wand.set_font_size(0.07 * (wand.get_image_width() as f64));
text_wand.set_text_alignment(magick_rust::AlignType::Center);
wand.annotate_image(&text_wand, text_pos_x, text_pos_y, 0.0, &text)?;
Ok(wand.write_image_blob("jpg")?)
}
pub enum ModifyImage {
GreenScreen {
align: Alignment,
background_album: Option<String>,
},
Overlay {
overlay_album: Option<String>,
},
Motivation {
border_color: Option<String>,
},
}
impl ModifyImage {
pub async fn process_image_blob(
&self,
db: &Database,
picox: &AlbumManager,
img_blob: Vec<u8>,
) -> Result<Vec<u8>, Error> {
match self {
ModifyImage::GreenScreen {
align,
background_album,
} => apply_greenscreen(picox, img_blob, background_album.clone(), align).await,
ModifyImage::Overlay { overlay_album } => {
apply_overlay(picox, img_blob, overlay_album.clone()).await
}
ModifyImage::Motivation { border_color } => {
apply_motivation_layer(db, img_blob, border_color.clone()).await
}
}
}
}
#[derive(Debug)]
pub enum BaseImage {
Motivation { album_name: Option<String> },
ByAlbum { album_name: Option<String> },
}
pub async fn image_pipeline(
db: &Database,
picox: &AlbumManager,
base_image: BaseImage,
img_pipeline: Vec<ModifyImage>,
) -> Result<Vec<u8>, Error> {
let base_image = match base_image {
BaseImage::Motivation { album_name } => {
MotivationConfig::get_motivation_image(db, picox, album_name).await?
}
BaseImage::ByAlbum { album_name } => picox
.query_image(ImageQuery {
album: album_name,
tags: vec![],
order: ImageSort::Random,
limit: 1,
})
.await?
.first()
.ok_or(Error::from(JDbError::NotFound))?
.clone(),
};
let mut img_blob = get_image_from_url(base_image.link).await?;
for step in &img_pipeline {
img_blob = step.process_image_blob(db, picox, img_blob).await?;
}
Ok(img_blob)
}
#[derive(Error, Debug)]
pub enum ModifyImageArgError {
#[error("No type found matching")]
NoTypeFound,
#[error("Unknown modification type '{0}'")]
UnknownType(String),
#[error("Not all arguments provided")]
NotAllArgumentsSupplied,
#[error("{0}")]
InvalidAlignment(String),
}
impl FromStr for ModifyImage {
type Err = ModifyImageArgError;
/// type:arg1,arg2
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s_split = s.split(":");
let mod_type = s_split.next().ok_or(Self::Err::NoTypeFound)?.to_lowercase();
let mut args = s_split.next().unwrap_or("").split(",");
match mod_type.as_str() {
"greenscreen" => {
if args.clone().count() < 1 {
Err(Self::Err::NotAllArgumentsSupplied)
} else {
let alignment_str = args.next().unwrap();
let alignment: Alignment = Alignment::from_str(alignment_str)
.map_err(Self::Err::InvalidAlignment)?;
let background_album = args.next().map(|s| s.to_string());
Ok(Self::GreenScreen {
align: alignment,
background_album,
})
}
}
"overlay" => {
let overlay_album = args.next().map(|s| s.to_string());
Ok(Self::Overlay { overlay_album })
}
"motivation" => {
let border_color = args.next().map(|s| s.to_string());
Ok(Self::Motivation { border_color })
}
_ => Err(Self::Err::UnknownType(mod_type.to_string())),
}
}
}
#[derive(Debug, Deserialize)]
pub enum Alignment {
Left,
Center,
Right,
}
impl Alignment {
pub fn offset(&self) -> f64 {
match self {
Alignment::Left => 0.25,
Alignment::Center => 0.5,
Alignment::Right => 0.75,
}
}
}
impl Distribution<Alignment> for StandardUniform {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Alignment {
match rng.random_range(0..=2) {
0 => Alignment::Left,
1 => Alignment::Center,
_ => Alignment::Right,
}
}
}
impl FromStr for Alignment {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_lowercase();
match s.as_str() {
"left" => Ok(Alignment::Left),
"right" => Ok(Alignment::Right),
"center" => Ok(Alignment::Center),
_ => Err("Unknown alignment".to_string()),
}
}
}

View File

@ -2,6 +2,7 @@ mod album_manager;
mod config; mod config;
mod discord; mod discord;
mod error; mod error;
mod image_manipulation;
mod inventory; mod inventory;
mod migrations; mod migrations;
mod models; mod models;

View File

@ -58,13 +58,28 @@ impl MotivationConfig {
pub async fn generate_motivation( pub async fn generate_motivation(
db: &Database, db: &Database,
picox: &AlbumManager,
border_color: &str, border_color: &str,
album_name: Option<String>,
) -> Result<Motivation, Error> { ) -> Result<Motivation, Error> {
let motivation = Self::get_motivation_config(db)?; let actions = RandomConfig::get_random(db, ACTION_RANDOM_GROUP)?.unwrap_or_default();
let goals = RandomConfig::get_random(db, GOAL_RANDOM_GROUP)?.unwrap_or_default();
let action = actions.get_response(db)?.unwrap_or_default();
let goal = goals.get_response(db)?.unwrap_or_default();
Ok(Motivation {
action: action.to_string(),
goal: goal.to_string(),
border_color: border_color.to_string(),
})
}
pub async fn get_motivation_image(
db: &Database,
picox: &AlbumManager,
album_name: Option<String>,
) -> Result<Image, Error> {
let mut images = Vec::new(); let mut images = Vec::new();
let motivation = Self::get_motivation_config(db)?;
if let Some(album_name) = album_name { if let Some(album_name) = album_name {
let query = AlbumQuery { let query = AlbumQuery {
@ -92,26 +107,12 @@ impl MotivationConfig {
let image = images.choose(&mut rng()).ok_or(Error::NoAlbumFound)?; let image = images.choose(&mut rng()).ok_or(Error::NoAlbumFound)?;
let actions = RandomConfig::get_random(db, ACTION_RANDOM_GROUP)?.unwrap_or_default(); picox.get_image_by_id(*image).await
let goals = RandomConfig::get_random(db, GOAL_RANDOM_GROUP)?.unwrap_or_default();
let action = actions.get_response(db)?.unwrap_or_default();
let goal = goals.get_response(db)?.unwrap_or_default();
let image = picox.get_image_by_id(*image).await?;
Ok(Motivation {
image,
action: action.to_string(),
goal: goal.to_string(),
border_color: border_color.to_string(),
})
} }
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Motivation { pub struct Motivation {
pub image: Image,
pub action: String, pub action: String,
pub goal: String, pub goal: String,
pub border_color: String, pub border_color: String,