feat: sauce api

This commit is contained in:
2024-02-29 04:07:04 -05:00
parent 37b13643de
commit 03703aa941
13 changed files with 793 additions and 207 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target /target
/old_apis

56
Cargo.lock generated
View File

@@ -192,6 +192,21 @@ dependencies = [
"windows-targets 0.52.0", "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]] [[package]]
name = "colored" name = "colored"
version = "2.1.0" version = "2.1.0"
@@ -783,6 +798,15 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "mach"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.1" version = "2.7.1"
@@ -864,6 +888,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.18" version = "0.2.18"
@@ -969,7 +1002,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall", "redox_syscall 0.4.1",
"smallvec", "smallvec",
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
@@ -1098,6 +1131,12 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@@ -1267,9 +1306,13 @@ dependencies = [
name = "sauce_connoisseur" name = "sauce_connoisseur"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono-wasi",
"log", "log",
"poise", "poise",
"reqwest", "reqwest",
"serde",
"serde_json",
"serde_repr",
"serenity", "serenity",
"simple_logger", "simple_logger",
"tokio", "tokio",
@@ -1373,6 +1416,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"

View File

@@ -6,9 +6,13 @@ edition = "2021"
# 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
[dependencies] [dependencies]
chrono-wasi = "0.4.11"
log = "0.4.20" log = "0.4.20"
poise = "0.6.1" poise = "0.6.1"
reqwest = { version = "0.11.24", features = ["blocking", "json"] } 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" serenity = "0.12.0"
simple_logger = { version = "4.3.3", features = ["threads"] } simple_logger = { version = "4.3.3", features = ["threads"] }
tokio = { version = "1.36.0", features = ["rt-multi-thread"] } tokio = { version = "1.36.0", features = ["rt-multi-thread"] }

View File

@@ -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"
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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<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")
}
}

View File

@@ -1,119 +1,244 @@
use crate::apis::api::SauceApi; use super::sauce::Sauce;
use crate::apis::api::SauceData; use crate::apis::utils::{SauceApi, SauceType, Timestamp};
use serenity::all::Timestamp; use chrono::prelude::*;
use serde::{Deserialize, Deserializer, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
pub struct GelbooruApi; const API_POST_URL: &str = "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&id=";
impl GelbooruApi { const API_TAGS_URL: &str = "https://gelbooru.com/index.php?page=dapi&s=tag&q=index&json=1&names=";
pub fn new() -> Self {
Self
}
pub fn boxed(self) -> Box<dyn SauceApi> { #[derive(Debug, Deserialize, Serialize, Default)]
Box::new(Self::new()) pub struct GelbooruPosts {
} pub post: Vec<GelbooruPost>,
} }
impl SauceApi for GelbooruApi { impl From<GelbooruPosts> for Sauce {
fn name(&self) -> &str { fn from(val: GelbooruPosts) -> Self {
"Gelbooru" 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"),
fn color(&self) -> (u8, u8, u8) { GelbooruRating::Questionable => String::from(" OWO what's this?"),
(37, 150, 190) // Gelbooru blue GelbooruRating::Explicit => String::from(" gettin' lewd"),
} };
let post = &val.post[0];
fn logo(&self) -> String { let title = post
String::from("https://gelbooru.com/favicon.ico") // TODO: find high-res logo .tags
} .tags
.iter()
fn into_sauce(&self) -> SauceData { .filter(|tag| tag.type_ == GelbooruTagType::Character)
SauceData { .map(|tag| tag.name.clone())
title: String::from("Gelbooru"), .collect::<Vec<String>>()
url: String::from("https://gelbooru.com/"), .join(", ")
image_url: String::from("https://gelbooru.com/"), + " "
source_url: String::from("https://gelbooru.com/"), + &title_action;
rating: String::from("Rating"), let url = post.url.clone();
tags: None, let image_url = match is_video {
characters: None, true => post.preview_url.clone(), // TODO: get video thumbnail/link
artists: None, false => post.file_url.clone(),
parodies: None, };
color: self.color(), let source_url = post.source_url.clone();
timestamp: Timestamp::from_millis(0 as i64).unwrap(), 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::<Vec<String>>(),
);
let characters = Some(
post.tags
.tags
.iter()
.filter(|tag| tag.type_ == GelbooruTagType::Character)
.map(|tag| tag.name.clone())
.collect::<Vec<String>>(),
);
let artists = Some(
post.tags
.tags
.iter()
.filter(|tag| tag.type_ == GelbooruTagType::Artist)
.map(|tag| tag.name.clone())
.collect::<Vec<String>>(),
);
let parodies = Some(
post.tags
.tags
.iter()
.filter(|tag| tag.type_ == GelbooruTagType::Faults)
.map(|tag| tag.name.clone())
.collect::<Vec<String>>(),
);
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 { impl GelbooruPosts {
Safe, pub async fn from_saucetype(val: SauceType) -> Self {
Questionable, let url = match val {
Explicit, SauceType::Id(id) => format!("{}{}", API_POST_URL, id.id),
} SauceType::Url(url) => url.url,
};
pub enum GelbooruPostStatus { let response = reqwest::get(url)
Active, .await
Flagged, .unwrap()
Pending, .text()
Deleted, .await
} .unwrap_or_default();
let mut posts: GelbooruPosts = serde_json::from_str(response.as_str()).unwrap();
pub enum GelbooruTagType { posts.post[0].tags = GelbooruTags::from(posts.post[0].raw_tags.clone()).await;
General, posts.post[0].url = format!(
Artist, "https://gelbooru.com/index.php?page=post&s=view&id={}",
Faults, posts.post[0].id
Copyright, );
Character, posts
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,
} }
}
} }
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct GelbooruPost { pub struct GelbooruPost {
pub id: i64, pub id: i64,
pub created_at: String, #[serde(deserialize_with = "timestamp_from_js_date")]
pub score: i64, pub created_at: Timestamp,
pub width: i64, pub score: i64,
pub height: i64, pub width: i64,
pub md5: String, pub height: i64,
//directory //pub md5: String,
pub file_name: String, //image field pub image: String, //image field
pub rating: GelbooruRating, pub rating: GelbooruRating,
pub source_url: String, #[serde(rename = "source")]
pub change: Option<i64>, pub source_url: String,
pub owner: String, pub owner: String,
pub creator_id: i64, pub creator_id: i64,
//parent_id //#[serde(deserialize_with = "zero_is_none")]
//sample //pub parent_id: Option<i64>,
pub preview_height: i64, //#[serde(deserialize_with = "zero_is_none")]
pub preview_width: i64, //pub sample: Option<i64>,
pub tags: Vec<GelbooruTag>, #[serde(rename = "tags", deserialize_with = "tags_from_string")]
pub title: Option<String>, raw_tags: GelbooruTagsRequest,
pub has_notes: bool, #[serde(skip_serializing, default)]
pub has_comments: bool, pub tags: GelbooruTags,
pub file_url: String, pub file_url: String,
pub preview_url: String, pub preview_url: String,
pub sample_url: Option<String>, pub status: GelbooruPostStatus,
pub sample_height: Option<i64>, //pub has_children: String,
pub sample_width: Option<i64>, #[serde(skip_serializing, default)]
pub status: GelbooruPostStatus, pub url: String,
pub post_locked: bool, }
//has_children
#[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<GelbooruTag>,
}
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<bool, D::Error>
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<GelbooruTagsRequest, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(GelbooruTagsRequest { tags: s })
}
fn timestamp_from_js_date<'de, D>(deserializer: D) -> Result<Timestamp, D::Error>
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))
} }

View File

@@ -1,2 +1,3 @@
pub mod api; pub mod sauce;
pub mod gelbooru; pub mod gelbooru;
pub mod utils;

51
src/apis/sauce.rs Normal file
View File

@@ -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<Vec<String>>,
pub characters: Option<Vec<String>>,
pub artists: Option<Vec<String>>,
pub parodies: Option<Vec<String>>,
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)
}
}
}
}

86
src/apis/utils.rs Normal file
View File

@@ -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<DateTime<FixedOffset>> for Timestamp {
fn from(val: DateTime<FixedOffset>) -> Self {
Self {
timestamp: val.timestamp(),
}
}
}
impl From<DateTime<Utc>> for Timestamp {
fn from(val: DateTime<Utc>) -> Self {
Self {
timestamp: val.timestamp(),
}
}
}
impl From<DateTime<Local>> for Timestamp {
fn from(val: DateTime<Local>) -> 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<i64> for SauceId {
fn from(val: i64) -> Self {
Self { id: val }
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SauceUrl {
pub url: String,
}
impl From<String> 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(),
}
}
}

View File

@@ -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 crate::Data;
use poise::serenity_prelude as serenity;
type Error = Box<dyn std::error::Error + Send + Sync>; type Error = Box<dyn std::error::Error + Send + Sync>;
pub async fn sc_event_handler( pub async fn sc_event_handler(
ctx: &serenity::Context, ctx: &serenity::Context,
event: &serenity::FullEvent, event: &serenity::FullEvent,
_framework: poise::FrameworkContext<'_, Data, Error>, _framework: poise::FrameworkContext<'_, Data, Error>,
data: &Data, data: &Data,
) -> Result<(), Error> { ) -> Result<(), Error> {
match event { match event {
serenity::FullEvent::Ready { .. }=> { serenity::FullEvent::Ready { .. } => {
log::info!("Connected to Discord"); 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( async fn message_handler(
_ctx: &serenity::Context, _ctx: &serenity::Context,
msg: &serenity::Message, msg: &serenity::Message,
_data: &Data, _data: &Data,
) -> Result<(), Error> { ) -> Result<(), Error> {
log::info!("Received message: {:?}", msg.content); log::info!("Received message: {:?}", msg.content);
Ok(())
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(())
} }

View File

@@ -1,13 +1,14 @@
use log::LevelFilter; use log::LevelFilter;
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use simple_logger::SimpleLogger; use simple_logger::SimpleLogger;
mod utils;
pub struct Data {} // User data, which is stored and accessible in all command invocations pub struct Data {} // User data, which is stored and accessible in all command invocations
type Error = Box<dyn std::error::Error + Send + Sync>; type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>; type Context<'a> = poise::Context<'a, Data, Error>;
pub mod event_handlers;
pub mod apis; pub mod apis;
pub mod event_handlers;
/// Ping command with latency measurement /// Ping command with latency measurement
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]

22
src/utils.rs Normal file
View File

@@ -0,0 +1,22 @@
use crate::apis::sauce::Sauce;
use crate::apis::utils::Timestamp;
use poise::serenity_prelude as serenity;
impl From<Sauce> 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<Timestamp> for serenity::Timestamp {
fn from(val: Timestamp) -> Self {
serenity::Timestamp::from_unix_timestamp(val.timestamp)
.unwrap_or(serenity::Timestamp::now())
}
}