feat: embed building
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.showUnlinkedFileNotification": false
|
||||
}
|
1286
Cargo.lock
generated
1286
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
78
src/apis/api.rs
Normal 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
119
src/apis/gelbooru.rs
Normal 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
2
src/apis/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod api;
|
||||
pub mod gelbooru;
|
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
12
src/main.rs
12
src/main.rs
@@ -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()
|
||||
})
|
||||
|
Reference in New Issue
Block a user