feat: embed building

This commit is contained in:
2024-02-23 01:26:40 -05:00
parent 84b2a8b659
commit 37b13643de
8 changed files with 359 additions and 1151 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
}

1286
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ecstasy = "0.3.4"
log = "0.4.20"
poise = "0.6.1"
reqwest = { version = "0.11.24", features = ["blocking", "json"] }
serenity = "0.12.0"
simple_logger = { version = "4.3.3", features = ["threads"] }
tokio = { version = "1.36.0", features = ["rt-multi-thread"] }

78
src/apis/api.rs Normal file
View File

@@ -0,0 +1,78 @@
use super::gelbooru::GelbooruApi;
use poise::serenity_prelude as serenity;
use ::serenity::all::Timestamp;
pub trait SauceApi {
fn name(&self) -> &str;
fn color(&self) -> (u8, u8, u8);
fn logo(&self) -> String;
fn into_sauce(&self) -> SauceData;
}
pub struct SauceData {
pub title: String, // Embed (title)
pub url: String, // Embed (Post link)
pub image_url: String, // Embed (image)
pub source_url: String, // Embed (Author link)
pub rating: String, // Embed (footer)
pub tags: Option<Vec<String>>, // Embed (Tags formatted in 3 columns)
pub characters: Option<Vec<String>>, // Embed
pub artists: Option<Vec<String>>, // Embed
pub parodies: Option<Vec<String>>, // Embed
pub color: (u8, u8, u8), // Embed (color)
pub timestamp: Timestamp, // Embed (timestamp)
}
pub struct Sauce {
pub api: Box<dyn SauceApi>,
pub data: SauceData,
}
impl Sauce {
pub fn new(url: &str) -> Self {
let api = Box::new(GelbooruApi::new());
let data = api.into_sauce();
Self { api, data }
} // TODO: this is most likely async
pub fn to_embed(&self) -> serenity::CreateEmbed {
let author = serenity::CreateEmbedAuthor::new("Author")
.name(self.data.artists.clone().unwrap().join(" & "))
.url(self.data.source_url.clone());
// divide tags into 3 columns
let tags = self.data.tags.clone().unwrap(); // TODO: filter out non-image descriptive tags
let column_1 = Self::tag_column(tags.clone(), 0);
let column_2 = Self::tag_column(tags.clone(), 1);
let column_3 = Self::tag_column(tags.clone(), 2);
let characters = self.data.characters.clone().unwrap().join("\n");
let footer = serenity::CreateEmbedFooter::new(self.data.rating.clone());
serenity::CreateEmbed::new()
.color(self.data.color.clone())
.author(author)
.title(self.data.title.clone()) // TODO: For posts without a title, use the first character tag in a short sentence that describes the image depending on the rating (e.g. "Link, Zelda and Riju" for safe posts, "Link and Zelda (explicit)" for explicit posts)
.url(self.data.url.clone())
.image(self.data.image_url.clone()) // TODO: We can't use that for videos, we need to make a button to link to the video "View video" and show the thumbnail
.fields(vec![
("Tags", column_1, true),
("", column_2, true),
("", column_3, true),
("Characters", characters, false)
])
.footer(footer)
.timestamp(self.data.timestamp.clone())
}
fn tag_column(tags: Vec<String>, column: i32) -> String {
tags.iter()
.enumerate()
.filter(|(i, _)| i % 3 == column as usize)
.map(|(_, tag)| tag.to_string())
.collect::<Vec<_>>()
.join("\n")
}
}

119
src/apis/gelbooru.rs Normal file
View File

@@ -0,0 +1,119 @@
use crate::apis::api::SauceApi;
use crate::apis::api::SauceData;
use serenity::all::Timestamp;
pub struct GelbooruApi;
impl GelbooruApi {
pub fn new() -> Self {
Self
}
pub fn boxed(self) -> Box<dyn SauceApi> {
Box::new(Self::new())
}
}
impl SauceApi for GelbooruApi {
fn name(&self) -> &str {
"Gelbooru"
}
fn color(&self) -> (u8, u8, u8) {
(37, 150, 190) // Gelbooru blue
}
fn logo(&self) -> String {
String::from("https://gelbooru.com/favicon.ico") // TODO: find high-res logo
}
fn into_sauce(&self) -> SauceData {
SauceData {
title: String::from("Gelbooru"),
url: String::from("https://gelbooru.com/"),
image_url: String::from("https://gelbooru.com/"),
source_url: String::from("https://gelbooru.com/"),
rating: String::from("Rating"),
tags: None,
characters: None,
artists: None,
parodies: None,
color: self.color(),
timestamp: Timestamp::from_millis(0 as i64).unwrap(),
}
}
}
pub enum GelbooruRating {
Safe,
Questionable,
Explicit,
}
pub enum GelbooruPostStatus {
Active,
Flagged,
Pending,
Deleted,
}
pub enum GelbooruTagType {
General,
Artist,
Faults,
Copyright,
Character,
Meta,
Style,
}
pub struct GelbooruTag {
pub id: i64,
pub name: String,
pub count: i64,
pub type_: GelbooruTagType,
pub ambiguous: bool,
}
impl GelbooruTag {
pub fn new(id: i64, name: String, count: i64, type_: GelbooruTagType, ambiguous: bool) -> Self {
Self {
id,
name,
count,
type_,
ambiguous,
}
}
}
pub struct GelbooruPost {
pub id: i64,
pub created_at: String,
pub score: i64,
pub width: i64,
pub height: i64,
pub md5: String,
//directory
pub file_name: String, //image field
pub rating: GelbooruRating,
pub source_url: String,
pub change: Option<i64>,
pub owner: String,
pub creator_id: i64,
//parent_id
//sample
pub preview_height: i64,
pub preview_width: i64,
pub tags: Vec<GelbooruTag>,
pub title: Option<String>,
pub has_notes: bool,
pub has_comments: bool,
pub file_url: String,
pub preview_url: String,
pub sample_url: Option<String>,
pub sample_height: Option<i64>,
pub sample_width: Option<i64>,
pub status: GelbooruPostStatus,
pub post_locked: bool,
//has_children
}

2
src/apis/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod api;
pub mod gelbooru;

View File

@@ -10,7 +10,7 @@ pub async fn sc_event_handler(
) -> Result<(), Error> {
match event {
serenity::FullEvent::Ready { .. }=> {
println!("Bot is ready!");
log::info!("Connected to Discord");
}
serenity::FullEvent::Message { new_message } => message_handler(ctx, new_message, data)?,
_ => {}
@@ -23,6 +23,6 @@ fn message_handler(
msg: &serenity::Message,
_data: &Data,
) -> Result<(), Error> {
println!("Received message: {:?}", msg.content);
log::info!("Received message: {:?}", msg.content);
Ok(())
}
}

View File

@@ -1,10 +1,13 @@
use log::LevelFilter;
use poise::serenity_prelude as serenity;
use simple_logger::SimpleLogger;
pub struct Data {} // User data, which is stored and accessible in all command invocations
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
pub mod event_handlers;
pub mod apis;
/// Ping command with latency measurement
#[poise::command(slash_command, prefix_command)]
@@ -18,6 +21,11 @@ async fn ping(ctx: Context<'_>) -> Result<(), Error> {
#[tokio::main]
async fn main() {
SimpleLogger::new()
.with_level(LevelFilter::Off)
.with_module_level("sauce_connoisseur", LevelFilter::Info)
.init()
.unwrap();
let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN");
let intents =
serenity::GatewayIntents::non_privileged().union(serenity::GatewayIntents::MESSAGE_CONTENT);
@@ -30,7 +38,9 @@ async fn main() {
},
commands: vec![ping()],
event_handler: |ctx, event, framework, data| {
Box::pin(event_handlers::sc_event_handler(ctx, event, framework, data))
Box::pin(event_handlers::sc_event_handler(
ctx, event, framework, data,
))
},
..Default::default()
})