From 03703aa941c9261a79a62695fa2f82435fb7f9f3 Mon Sep 17 00:00:00 2001 From: skkeye Date: Thu, 29 Feb 2024 04:07:04 -0500 Subject: [PATCH] feat: sauce api --- .gitignore | 1 + Cargo.lock | 56 ++++- Cargo.toml | 4 + misc_data/api_gelbooru_post.json | 40 ++++ misc_data/api_gelbooru_tags.json | 261 +++++++++++++++++++++++ src/apis/api.rs | 78 ------- src/apis/gelbooru.rs | 341 +++++++++++++++++++++---------- src/apis/mod.rs | 3 +- src/apis/sauce.rs | 51 +++++ src/apis/utils.rs | 86 ++++++++ src/event_handlers.rs | 54 +++-- src/main.rs | 3 +- src/utils.rs | 22 ++ 13 files changed, 793 insertions(+), 207 deletions(-) create mode 100644 misc_data/api_gelbooru_post.json create mode 100644 misc_data/api_gelbooru_tags.json delete mode 100644 src/apis/api.rs create mode 100644 src/apis/sauce.rs create mode 100644 src/apis/utils.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..554fc10 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/old_apis \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3e0d4b2..ad29662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,21 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "chrono-wasi" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc966f9982ebc257d603e29e5e2ef7691826fc66b791a237b1e172a45a337143" +dependencies = [ + "libc", + "mach", + "num-integer", + "num-traits", + "redox_syscall 0.1.57", + "wasi", + "winapi", +] + [[package]] name = "colored" version = "2.1.0" @@ -783,6 +798,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.7.1" @@ -864,6 +888,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -969,7 +1002,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -1098,6 +1131,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1267,9 +1306,13 @@ dependencies = [ name = "sauce_connoisseur" version = "0.1.0" dependencies = [ + "chrono-wasi", "log", "poise", "reqwest", + "serde", + "serde_json", + "serde_repr", "serenity", "simple_logger", "tokio", @@ -1373,6 +1416,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.50", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 059ab6a..808aee4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono-wasi = "0.4.11" log = "0.4.20" poise = "0.6.1" reqwest = { version = "0.11.24", features = ["blocking", "json"] } +serde = "1.0.197" +serde_json = "1.0.114" +serde_repr = "0.1.18" serenity = "0.12.0" simple_logger = { version = "4.3.3", features = ["threads"] } tokio = { version = "1.36.0", features = ["rt-multi-thread"] } diff --git a/misc_data/api_gelbooru_post.json b/misc_data/api_gelbooru_post.json new file mode 100644 index 0000000..c42903c --- /dev/null +++ b/misc_data/api_gelbooru_post.json @@ -0,0 +1,40 @@ +{ + "@attributes": { + "limit": 1, + "offset": 0, + "count": 1 + }, + "post": [ + { + "id": 9659332, + "created_at": "Wed Feb 21 18:51:07 -0600 2024", + "score": 238, + "width": 720, + "height": 924, + "md5": "940876caab42e3d12fbc33ea11fa40a6", + "directory": "94\/08", + "image": "940876caab42e3d12fbc33ea11fa40a6.webm", + "rating": "explicit", + "source": "https:\/\/twitter.com\/Lx3_LCubed\/status\/1737880617520636134", + "change": 1708563067, + "owner": "only_kemonomimi", + "creator_id": 1198238, + "parent_id": 0, + "sample": 0, + "preview_height": 250, + "preview_width": 194, + "tags": "1boy 1girl animated arm_strap arms_between_legs artist_name asymmetrical_hair black_choker black_eyeshadow black_gloves black_hair blush bouncing_breasts breasts breath chest_belt choker cleavage closed_eyes closed_mouth covered_erect_nipples cowboy_shot cowgirl_position cum dress ejaculation elbow_gloves english_text evil_grin evil_smile eyebrows eyelashes eyeliner eyeshadow female_orgasm femdom fingerless_gloves frills from_above girl_on_top gloves glowing glowing_eyes goth_fashion grin hair_ornament hetero holding holding_scissors hololive hololive_english large_breasts leaning_forward live2d looking_at_viewer looking_down looking_up losloslos_lx3 makeup multicolored_hair myth1carts open_mouth orange_eyes orgasm pov pov_crotch purple_eyes rape riding runny_makeup scissors sex shiori_novella shiori_novella_(1st_costume) simple_background sitting sitting_on_person smile solo_focus sound spread_legs straddling strap_slip strapless strapless_dress striped_clothes striped_gloves tagme tears tongue tongue_out two-tone_hair vaginal video virtual_youtuber watermark white_background white_hair yandere", + "title": "", + "has_notes": "false", + "has_comments": "false", + "file_url": "https:\/\/video-cdn1.gelbooru.com\/images\/94\/08\/940876caab42e3d12fbc33ea11fa40a6.mp4", + "preview_url": "https:\/\/img3.gelbooru.com\/thumbnails\/94\/08\/thumbnail_940876caab42e3d12fbc33ea11fa40a6.jpg", + "sample_url": "", + "sample_height": 0, + "sample_width": 0, + "status": "active", + "post_locked": 0, + "has_children": "false" + } + ] +} \ No newline at end of file diff --git a/misc_data/api_gelbooru_tags.json b/misc_data/api_gelbooru_tags.json new file mode 100644 index 0000000..16fccea --- /dev/null +++ b/misc_data/api_gelbooru_tags.json @@ -0,0 +1,261 @@ +{ + "@attributes": { + "limit": 100, + "offset": 0, + "count": 36 + }, + "tag": [ + { + "id": 262, + "name": "highres", + "count": 5223646, + "type": 5, + "ambiguous": 0 + }, + { + "id": 796, + "name": "smile", + "count": 2697850, + "type": 0, + "ambiguous": 0 + }, + { + "id": 12336, + "name": "solo", + "count": 4866469, + "type": 0, + "ambiguous": 0 + }, + { + "id": 930, + "name": "thighs", + "count": 739228, + "type": 0, + "ambiguous": 0 + }, + { + "id": 33975, + "name": "looking_at_viewer", + "count": 3084580, + "type": 0, + "ambiguous": 0 + }, + { + "id": 152532, + "name": "1girl", + "count": 6298399, + "type": 0, + "ambiguous": 0 + }, + { + "id": 432, + "name": "nipples", + "count": 1282668, + "type": 0, + "ambiguous": 0 + }, + { + "id": 265, + "name": "long_hair", + "count": 4155835, + "type": 0, + "ambiguous": 1 + }, + { + "id": 14256, + "name": "hair_ribbon", + "count": 555093, + "type": 0, + "ambiguous": 0 + }, + { + "id": 13764, + "name": "ribbon", + "count": 994344, + "type": 0, + "ambiguous": 0 + }, + { + "id": 337, + "name": "brown_hair", + "count": 1597712, + "type": 0, + "ambiguous": 0 + }, + { + "id": 177, + "name": "yellow_eyes", + "count": 653600, + "type": 0, + "ambiguous": 0 + }, + { + "id": 25360, + "name": "collarbone", + "count": 731687, + "type": 0, + "ambiguous": 0 + }, + { + "id": 43250, + "name": "closed_mouth", + "count": 844280, + "type": 0, + "ambiguous": 0 + }, + { + "id": 747581, + "name": "feet_out_of_frame", + "count": 115123, + "type": 0, + "ambiguous": 0 + }, + { + "id": 337849, + "name": "hair_between_eyes", + "count": 963451, + "type": 0, + "ambiguous": 0 + }, + { + "id": 3477, + "name": "navel", + "count": 1198508, + "type": 0, + "ambiguous": 0 + }, + { + "id": 656, + "name": "toes", + "count": 206973, + "type": 0, + "ambiguous": 1 + }, + { + "id": 5498, + "name": "indoors", + "count": 365503, + "type": 0, + "ambiguous": 0 + }, + { + "id": 607371, + "name": "female_focus", + "count": 880105, + "type": 0, + "ambiguous": 0 + }, + { + "id": 171, + "name": "loli", + "count": 364398, + "type": 0, + "ambiguous": 1 + }, + { + "id": 884, + "name": "spread_legs", + "count": 402322, + "type": 0, + "ambiguous": 0 + }, + { + "id": 248, + "name": "pussy", + "count": 742082, + "type": 0, + "ambiguous": 0 + }, + { + "id": 50, + "name": "nude", + "count": 875073, + "type": 0, + "ambiguous": 0 + }, + { + "id": 15477, + "name": "red_ribbon", + "count": 140277, + "type": 0, + "ambiguous": 0 + }, + { + "id": 708618, + "name": "completely_nude", + "count": 163434, + "type": 0, + "ambiguous": 0 + }, + { + "id": 1331, + "name": "pillow", + "count": 155206, + "type": 0, + "ambiguous": 0 + }, + { + "id": 13319, + "name": "toenails", + "count": 38778, + "type": 0, + "ambiguous": 0 + }, + { + "id": 523, + "name": "flat_chest", + "count": 256879, + "type": 0, + "ambiguous": 0 + }, + { + "id": 151031, + "name": "bed_sheet", + "count": 99951, + "type": 0, + "ambiguous": 0 + }, + { + "id": 19606, + "name": "arm_support", + "count": 93891, + "type": 0, + "ambiguous": 0 + }, + { + "id": 7053, + "name": "ribs", + "count": 10331, + "type": 0, + "ambiguous": 0 + }, + { + "id": 306143, + "name": "to_love-ru", + "count": 11675, + "type": 3, + "ambiguous": 0 + }, + { + "id": 319567, + "name": "to_love-ru_darkness", + "count": 3709, + "type": 3, + "ambiguous": 0 + }, + { + "id": 1391655, + "name": "elementary", + "count": 102, + "type": 1, + "ambiguous": 0 + }, + { + "id": 402955, + "name": "master_nemesis", + "count": 304, + "type": 4, + "ambiguous": 0 + } + ] +} \ No newline at end of file diff --git a/src/apis/api.rs b/src/apis/api.rs deleted file mode 100644 index 2126343..0000000 --- a/src/apis/api.rs +++ /dev/null @@ -1,78 +0,0 @@ -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>, // Embed (Tags formatted in 3 columns) - pub characters: Option>, // Embed - pub artists: Option>, // Embed - pub parodies: Option>, // Embed - pub color: (u8, u8, u8), // Embed (color) - pub timestamp: Timestamp, // Embed (timestamp) -} - -pub struct Sauce { - pub api: Box, - 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, column: i32) -> String { - tags.iter() - .enumerate() - .filter(|(i, _)| i % 3 == column as usize) - .map(|(_, tag)| tag.to_string()) - .collect::>() - .join("\n") - } -} diff --git a/src/apis/gelbooru.rs b/src/apis/gelbooru.rs index 6c43050..f64a3d8 100644 --- a/src/apis/gelbooru.rs +++ b/src/apis/gelbooru.rs @@ -1,119 +1,244 @@ -use crate::apis::api::SauceApi; -use crate::apis::api::SauceData; -use serenity::all::Timestamp; +use super::sauce::Sauce; +use crate::apis::utils::{SauceApi, SauceType, Timestamp}; +use chrono::prelude::*; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; -pub struct GelbooruApi; -impl GelbooruApi { - pub fn new() -> Self { - Self - } +const API_POST_URL: &str = "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&id="; +const API_TAGS_URL: &str = "https://gelbooru.com/index.php?page=dapi&s=tag&q=index&json=1&names="; - pub fn boxed(self) -> Box { - Box::new(Self::new()) - } +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct GelbooruPosts { + pub post: Vec, } -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(), +impl From for Sauce { + fn from(val: GelbooruPosts) -> Self { + let is_video = &val.post[0].tags.tags.iter().any(|tag| tag.name == "video"); + let title_action: String = match &val.post[0].rating { + GelbooruRating::Safe => String::from(" lookin' cute"), + GelbooruRating::Questionable => String::from(" OWO what's this?"), + GelbooruRating::Explicit => String::from(" gettin' lewd"), + }; + let post = &val.post[0]; + let title = post + .tags + .tags + .iter() + .filter(|tag| tag.type_ == GelbooruTagType::Character) + .map(|tag| tag.name.clone()) + .collect::>() + .join(", ") + + " " + + &title_action; + let url = post.url.clone(); + let image_url = match is_video { + true => post.preview_url.clone(), // TODO: get video thumbnail/link + false => post.file_url.clone(), + }; + let source_url = post.source_url.clone(); + let rating = match post.rating { + GelbooruRating::Safe => String::from("Safe"), + GelbooruRating::Questionable => String::from("Questionable"), + GelbooruRating::Explicit => String::from("Explicit"), + }; + let tags = Some( + post.tags + .tags + .iter() + .map(|tag| tag.name.clone()) + .collect::>(), + ); + let characters = Some( + post.tags + .tags + .iter() + .filter(|tag| tag.type_ == GelbooruTagType::Character) + .map(|tag| tag.name.clone()) + .collect::>(), + ); + let artists = Some( + post.tags + .tags + .iter() + .filter(|tag| tag.type_ == GelbooruTagType::Artist) + .map(|tag| tag.name.clone()) + .collect::>(), + ); + let parodies = Some( + post.tags + .tags + .iter() + .filter(|tag| tag.type_ == GelbooruTagType::Faults) + .map(|tag| tag.name.clone()) + .collect::>(), + ); + let color = (37, 150, 190); + let timestamp = post.created_at.clone(); + Self { + title, + url, + image_url, + source_url, + rating, + tags, + characters, + artists, + parodies, + color, + timestamp, + api: SauceApi::Gelbooru, + } } - } } -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, +impl GelbooruPosts { + pub async fn from_saucetype(val: SauceType) -> Self { + let url = match val { + SauceType::Id(id) => format!("{}{}", API_POST_URL, id.id), + SauceType::Url(url) => url.url, + }; + let response = reqwest::get(url) + .await + .unwrap() + .text() + .await + .unwrap_or_default(); + let mut posts: GelbooruPosts = serde_json::from_str(response.as_str()).unwrap(); + posts.post[0].tags = GelbooruTags::from(posts.post[0].raw_tags.clone()).await; + posts.post[0].url = format!( + "https://gelbooru.com/index.php?page=post&s=view&id={}", + posts.post[0].id + ); + posts } - } } +#[derive(Default, Debug, Deserialize, Serialize)] 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, - pub owner: String, - pub creator_id: i64, - //parent_id - //sample - pub preview_height: i64, - pub preview_width: i64, - pub tags: Vec, - pub title: Option, - pub has_notes: bool, - pub has_comments: bool, - pub file_url: String, - pub preview_url: String, - pub sample_url: Option, - pub sample_height: Option, - pub sample_width: Option, - pub status: GelbooruPostStatus, - pub post_locked: bool, - //has_children -} \ No newline at end of file + pub id: i64, + #[serde(deserialize_with = "timestamp_from_js_date")] + pub created_at: Timestamp, + pub score: i64, + pub width: i64, + pub height: i64, + //pub md5: String, + pub image: String, //image field + pub rating: GelbooruRating, + #[serde(rename = "source")] + pub source_url: String, + pub owner: String, + pub creator_id: i64, + //#[serde(deserialize_with = "zero_is_none")] + //pub parent_id: Option, + //#[serde(deserialize_with = "zero_is_none")] + //pub sample: Option, + #[serde(rename = "tags", deserialize_with = "tags_from_string")] + raw_tags: GelbooruTagsRequest, + #[serde(skip_serializing, default)] + pub tags: GelbooruTags, + pub file_url: String, + pub preview_url: String, + pub status: GelbooruPostStatus, + //pub has_children: String, + #[serde(skip_serializing, default)] + pub url: String, +} + +#[derive(Debug, Deserialize, Serialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum GelbooruRating { + Safe, + Questionable, + #[default] + Explicit, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct GelbooruTagsRequest { + pub tags: String, +} + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct GelbooruTags { + #[serde(rename = "tag")] + pub tags: Vec, +} + +impl GelbooruTags { + pub async fn from(req: GelbooruTagsRequest) -> Self { + let url = format!("{}{}", API_TAGS_URL, req.tags.replace(' ', "%20")); + let response = reqwest::get(url) + .await + .unwrap() + .text() + .await + .unwrap_or_default(); + let tags: GelbooruTags = serde_json::from_str(response.as_str()).unwrap(); + tags + } +} + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct GelbooruTag { + pub id: i64, + pub name: String, + pub count: i64, + #[serde(rename = "type")] + pub type_: GelbooruTagType, + #[serde(deserialize_with = "bool_from_int")] + pub ambiguous: bool, +} + +#[derive(PartialEq, Debug, Deserialize_repr, Serialize_repr, Default)] +#[repr(u8)] +pub enum GelbooruTagType { + #[default] + General = 0, + Artist = 1, + Faults = 2, + Copyright = 3, + Character = 4, + Meta = 5, + Style = 6, +} + +#[derive(Debug, Deserialize, Serialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum GelbooruPostStatus { + #[default] + Active, + Flagged, + Pending, + Deleted, +} + +fn bool_from_int<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = u8::deserialize(deserializer)?; + if s == 0 { + Ok(false) + } else { + Ok(true) + } +} + +fn tags_from_string<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Ok(GelbooruTagsRequest { tags: s }) +} + +fn timestamp_from_js_date<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + let created_at = + DateTime::parse_from_str(&s, "%a %b %e %T %z %Y").unwrap_or(DateTime::from(Utc::now())); + Ok(Timestamp::from(created_at)) +} diff --git a/src/apis/mod.rs b/src/apis/mod.rs index bdfec27..d0ef893 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,2 +1,3 @@ -pub mod api; +pub mod sauce; pub mod gelbooru; +pub mod utils; \ No newline at end of file diff --git a/src/apis/sauce.rs b/src/apis/sauce.rs new file mode 100644 index 0000000..17b00b0 --- /dev/null +++ b/src/apis/sauce.rs @@ -0,0 +1,51 @@ +use crate::apis::gelbooru; +use crate::apis::utils::{SauceApi, Timestamp}; +use serde::{Deserialize, Serialize}; +use std::vec; + +use super::utils::SauceRequest; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Sauce { + pub title: String, + pub url: String, + pub image_url: String, + pub source_url: String, + pub rating: String, + pub tags: Option>, + pub characters: Option>, + pub artists: Option>, + pub parodies: Option>, + pub color: (u8, u8, u8), + pub timestamp: Timestamp, + pub api: SauceApi, +} + +impl Default for Sauce { + fn default() -> Self { + Self { + title: String::from("Gayniggers from Outer Space"), + url: String::from("https://www.imdb.com/title/tt0274518/"), + image_url: String::from("https://m.media-amazon.com/images/M/MV5BZjFkYTU2ODctNTQ4MS00YzRjLWE2M2YtM2U0NzUzMmIwMzRhXkEyXkFqcGdeQXVyNjExODE1MDc@._V1_FMjpg_UY500_.jpg"), + source_url: String::from("https://twitter.com/Lx3_LCubed/status/1737880617520636134"), + rating: String::from("Explicit"), + tags: Some(vec![String::from("Kasumi"), String::from("Dead or Alive"), String::from("Lx3") , String::from("Video")]), + characters: Some(vec![String::from("Kasumi")]), + artists: Some(vec![String::from("Lx3")]), + parodies: None, + color: (37, 150, 190), + timestamp: Timestamp::default(), + api: SauceApi::Gelbooru, + } + } +} + +impl Sauce { + pub async fn from_request(value: SauceRequest) -> Self { + match value.api { + SauceApi::Gelbooru => { + Sauce::from(gelbooru::GelbooruPosts::from_saucetype(value._ref).await) + } + } + } +} diff --git a/src/apis/utils.rs b/src/apis/utils.rs new file mode 100644 index 0000000..a74e609 --- /dev/null +++ b/src/apis/utils.rs @@ -0,0 +1,86 @@ +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub enum SauceApi { + Gelbooru, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Timestamp { + pub timestamp: i64, +} + +impl From> for Timestamp { + fn from(val: DateTime) -> Self { + Self { + timestamp: val.timestamp(), + } + } +} + +impl From> for Timestamp { + fn from(val: DateTime) -> Self { + Self { + timestamp: val.timestamp(), + } + } +} + +impl From> for Timestamp { + fn from(val: DateTime) -> Self { + Self { + timestamp: val.timestamp(), + } + } +} + +impl Default for Timestamp { + fn default() -> Self { + Self { + timestamp: Utc::now().timestamp(), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SauceRequest { + pub _ref: SauceType, + pub api: SauceApi, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum SauceType { + Id(SauceId), + Url(SauceUrl), +} + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct SauceId { + pub id: i64, +} + +impl From for SauceId { + fn from(val: i64) -> Self { + Self { id: val } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SauceUrl { + pub url: String, +} + +impl From for SauceUrl { + fn from(val: String) -> Self { + Self { url: val } + } +} + +impl From<&str> for SauceUrl { + fn from(val: &str) -> Self { + Self { + url: val.to_string(), + } + } +} \ No newline at end of file diff --git a/src/event_handlers.rs b/src/event_handlers.rs index 599e550..e72c9c9 100644 --- a/src/event_handlers.rs +++ b/src/event_handlers.rs @@ -1,28 +1,46 @@ -use poise::serenity_prelude as serenity; +use crate::apis::utils::{SauceRequest, SauceType}; +use crate::apis::{ + sauce::Sauce, + utils::{SauceApi, SauceId}, +}; use crate::Data; +use poise::serenity_prelude as serenity; type Error = Box; pub async fn sc_event_handler( - ctx: &serenity::Context, - event: &serenity::FullEvent, - _framework: poise::FrameworkContext<'_, Data, Error>, - data: &Data, + ctx: &serenity::Context, + event: &serenity::FullEvent, + _framework: poise::FrameworkContext<'_, Data, Error>, + data: &Data, ) -> Result<(), Error> { - match event { - serenity::FullEvent::Ready { .. }=> { - log::info!("Connected to Discord"); + match event { + serenity::FullEvent::Ready { .. } => { + log::info!("Connected to Discord"); + } + serenity::FullEvent::Message { new_message } => { + message_handler(ctx, new_message, data).await? + } + _ => {} } - serenity::FullEvent::Message { new_message } => message_handler(ctx, new_message, data)?, - _ => {} - } - Ok(()) + Ok(()) } -fn message_handler( - _ctx: &serenity::Context, - msg: &serenity::Message, - _data: &Data, +async fn message_handler( + _ctx: &serenity::Context, + msg: &serenity::Message, + _data: &Data, ) -> Result<(), Error> { - log::info!("Received message: {:?}", msg.content); - Ok(()) + log::info!("Received message: {:?}", msg.content); + + if msg.content == "?test" { + let req = SauceRequest { + _ref: SauceType::Id(SauceId::from(9526475)), + api: SauceApi::Gelbooru, + }; + let embed = serenity::CreateEmbed::from(Sauce::from_request(req).await); + let res = serenity::CreateMessage::new().embed(embed); + msg.channel_id.send_message(&_ctx.http, res).await?; + } + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index a207543..9fea263 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ use log::LevelFilter; use poise::serenity_prelude as serenity; use simple_logger::SimpleLogger; +mod utils; pub struct Data {} // User data, which is stored and accessible in all command invocations type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; -pub mod event_handlers; pub mod apis; +pub mod event_handlers; /// Ping command with latency measurement #[poise::command(slash_command, prefix_command)] diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..d013211 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,22 @@ +use crate::apis::sauce::Sauce; +use crate::apis::utils::Timestamp; +use poise::serenity_prelude as serenity; + +impl From for serenity::CreateEmbed { + fn from(val: Sauce) -> Self { + serenity::CreateEmbed::default() + .title(val.title) + .url(val.url) + .image(val.image_url) + .footer(serenity::CreateEmbedFooter::new("").text(val.rating)) + .timestamp(serenity::Timestamp::from(val.timestamp)) + .color(val.color) + } +} + +impl From for serenity::Timestamp { + fn from(val: Timestamp) -> Self { + serenity::Timestamp::from_unix_timestamp(val.timestamp) + .unwrap_or(serenity::Timestamp::now()) + } +}