pikadick/commands/
sauce_nao.rs1use crate::{
2 checks::ENABLED_CHECK,
3 client_data::{
4 CacheStatsBuilder,
5 CacheStatsProvider,
6 },
7 util::{
8 LoadingReaction,
9 TimedCache,
10 TimedCacheEntry,
11 },
12 ClientDataKey,
13};
14use anyhow::Context as _;
15use serenity::{
16 builder::{
17 CreateEmbed,
18 CreateMessage,
19 },
20 framework::standard::{
21 macros::command,
22 Args,
23 CommandResult,
24 },
25 model::prelude::*,
26 prelude::*,
27};
28use std::sync::Arc;
29use tracing::error;
30
31#[derive(Clone, Debug)]
32pub struct SauceNaoClient {
33 client: sauce_nao::Client,
34 search_cache: TimedCache<String, sauce_nao::OkResponse>,
35}
36
37impl SauceNaoClient {
38 pub fn new(api_key: &str) -> Self {
39 Self {
40 client: sauce_nao::Client::new(api_key),
41 search_cache: TimedCache::new(),
42 }
43 }
44
45 pub async fn search(
47 &self,
48 query: &str,
49 ) -> anyhow::Result<Arc<TimedCacheEntry<sauce_nao::OkResponse>>> {
50 if let Some(entry) = self.search_cache.get_if_fresh(query) {
51 return Ok(entry);
52 }
53
54 let search_results = self
55 .client
56 .search(query)
57 .await
58 .context("failed to search for image")?;
59
60 self.search_cache
61 .insert(String::from(query), search_results);
62
63 self.search_cache
64 .get_if_fresh(query)
65 .context("cache data expired")
66 }
67}
68
69impl CacheStatsProvider for SauceNaoClient {
70 fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
71 cache_stats_builder.publish_stat(
72 "sauce-nao",
73 "search_cache",
74 self.search_cache.len() as f32,
75 );
76 }
77}
78
79#[command("sauce-nao")]
80#[description("Search SauceNao for an image at a url")]
81#[usage("<img_url>")]
82#[example("https://konachan.com/image/5982d8946ae503351e960f097f84cd90/Konachan.com%20-%20330136%20animal%20nobody%20original%20signed%20yutaka_kana.jpg")]
83#[checks(Enabled)]
84#[min_args(1)]
85#[max_args(1)]
86async fn sauce_nao(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
87 let data_lock = ctx.data.read().await;
88 let client_data = data_lock
89 .get::<ClientDataKey>()
90 .expect("missing client data");
91 let client = client_data.sauce_nao_client.clone();
92 drop(data_lock);
93
94 let query = args.trimmed().current().expect("missing query");
95
96 let mut loading = LoadingReaction::new(ctx.http.clone(), msg);
97
98 let result = client
99 .search(query)
100 .await
101 .context("failed to search for image");
102
103 match result {
104 Ok(data) => {
105 let data = data.data();
106
107 match data.results.first() {
108 Some(data) => {
109 let mut embed_builder = CreateEmbed::new()
110 .title("SauceNao Best Match")
111 .image(data.header.thumbnail.as_str());
112 if let Some(ext_url) = data.data.ext_urls.first() {
113 embed_builder = embed_builder
114 .description(ext_url.as_str())
115 .url(ext_url.as_str());
116 }
117
118 if let Some(source) = data.data.source.as_deref() {
119 embed_builder = embed_builder.field("Source", source, true);
120 }
121
122 if let Some(eng_name) = data.data.eng_name.as_deref() {
123 embed_builder = embed_builder.field("English Name", eng_name, true);
124 }
125
126 if let Some(jp_name) = data.data.jp_name.as_deref() {
127 embed_builder = embed_builder.field("Jap Name", jp_name, true);
128 }
129
130 let message_builder = CreateMessage::new().embed(embed_builder);
131
132 msg.channel_id
133 .send_message(&ctx.http, message_builder)
134 .await?;
135
136 loading.send_ok();
137 }
138 None => {
139 msg.channel_id
140 .say(&ctx.http, format!("No results on SauceNao for \"{query}\""))
141 .await?;
142 }
143 }
144 }
145 Err(error) => {
146 error!("{error:?}");
147 msg.channel_id.say(&ctx.http, format!("{error:?}")).await?;
148 }
149 }
150
151 Ok(())
152}