pikadick/commands/
rule34.rs1use crate::{
2 client_data::{
3 CacheStatsBuilder,
4 CacheStatsProvider,
5 },
6 util::{
7 TimedCache,
8 TimedCacheEntry,
9 },
10 ClientDataKey,
11};
12use anyhow::Context as _;
13use rand::seq::SliceRandom;
14use serenity::builder::{
15 CreateInteractionResponse,
16 CreateInteractionResponseMessage,
17};
18use std::sync::Arc;
19use tracing::{
20 error,
21 info,
22};
23
24#[derive(Clone, Default, Debug)]
26pub struct Rule34Client {
27 client: rule34::Client,
28 list_cache: TimedCache<String, rule34::PostList>,
37}
38
39impl Rule34Client {
40 pub fn new() -> Rule34Client {
42 Rule34Client {
43 client: rule34::Client::new(),
44 list_cache: TimedCache::new(),
45 }
46 }
47
48 #[tracing::instrument(skip(self))]
50 pub async fn list(&self, tags: &str) -> anyhow::Result<Arc<TimedCacheEntry<rule34::PostList>>> {
51 if let Some(entry) = self.list_cache.get_if_fresh(tags) {
52 return Ok(entry);
53 }
54
55 let results = self
56 .client
57 .list_posts()
58 .tags(Some(tags))
59 .limit(Some(1_000))
60 .execute()
61 .await
62 .context("failed to search rule34")?;
63 Ok(self.list_cache.insert_and_get(String::from(tags), results))
64 }
65}
66
67impl CacheStatsProvider for Rule34Client {
68 fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
69 cache_stats_builder.publish_stat("rule34", "list_cache", self.list_cache.len() as f32);
70 }
71}
72
73#[derive(Debug, pikadick_slash_framework::FromOptions)]
75pub struct Rule34Options {
76 query: String,
78}
79
80pub fn create_slash_command() -> anyhow::Result<pikadick_slash_framework::Command> {
82 pikadick_slash_framework::CommandBuilder::new()
83 .name("rule34")
84 .description("Look up rule34 for almost anything")
85 .argument(
86 pikadick_slash_framework::ArgumentParamBuilder::new()
87 .name("query")
88 .description("The search query")
89 .kind(pikadick_slash_framework::ArgumentKind::String)
90 .required(true)
91 .build()?,
92 )
93 .on_process(|ctx, interaction, args: Rule34Options| async move {
94 let data_lock = ctx.data.read().await;
95 let client_data = data_lock
96 .get::<ClientDataKey>()
97 .expect("missing client data");
98 let client = client_data.rule34_client.clone();
99 drop(data_lock);
100
101 let query_str = rule34::SearchQueryBuilder::new()
102 .add_tag_iter(args.query.split(' '))
103 .take_query_string();
104
105 info!("searching rule34 for \"{query_str}\"");
106
107 let result = client
108 .list(&query_str)
109 .await
110 .context("failed to get search results");
111
112 let mut message_builder = CreateInteractionResponseMessage::new();
113 match result {
114 Ok(list_results) => {
115 let maybe_list_result: Option<String> = list_results
116 .data()
117 .posts
118 .choose(&mut rand::thread_rng())
119 .map(|list_result| list_result.file_url.to_string());
120
121 if let Some(file_url) = maybe_list_result {
122 info!("sending \"{file_url}\"");
123 message_builder = message_builder.content(file_url);
124 } else {
125 info!("no results");
126 message_builder =
127 message_builder.content(format!("No results for \"{query_str}\""));
128 }
129 }
130 Err(error) => {
131 error!("{error:?}");
132 message_builder = message_builder.content(format!("{error:?}"));
133 }
134 }
135 let response = CreateInteractionResponse::Message(message_builder);
136 interaction.create_response(&ctx.http, response).await?;
137
138 client.list_cache.trim();
139
140 Ok(())
141 })
142 .build()
143 .context("failed to build rule34 command")
144}