pikadick/commands/
nekos.rs1use crate::{
2 client_data::{
3 CacheStatsBuilder,
4 CacheStatsProvider,
5 },
6 ClientDataKey,
7};
8use anyhow::Context as _;
9use crossbeam::queue::ArrayQueue;
10use indexmap::set::IndexSet;
11use parking_lot::RwLock;
12use pikadick_slash_framework::FromOptions;
13use rand::Rng;
14use serenity::builder::{
15 CreateInteractionResponse,
16 CreateInteractionResponseMessage,
17};
18use std::sync::Arc;
19use tracing::error;
20use url::Url;
21
22const BUFFER_SIZE: u8 = 100;
24
25#[derive(Clone, Debug)]
27pub struct Cache(Arc<CacheInner>);
28
29impl Cache {
30 pub fn new() -> Self {
32 Self(Arc::new(CacheInner {
33 primary: ArrayQueue::new(BUFFER_SIZE.into()),
34 secondary: RwLock::new(IndexSet::new()),
35 }))
36 }
37
38 pub fn primary_len(&self) -> usize {
40 self.0.primary.len()
41 }
42
43 pub fn secondary_len(&self) -> usize {
45 self.0.secondary.read().len()
46 }
47
48 pub fn primary_is_empty(&self) -> bool {
50 self.0.primary.is_empty()
51 }
52
53 pub fn secondary_is_empty(&self) -> bool {
55 self.0.secondary.read().is_empty()
56 }
57
58 pub fn add(&self, uri: Url) {
60 let _ = self.0.primary.push(uri.clone()).is_ok();
61 self.0.secondary.write().insert(uri);
62 }
63
64 pub fn add_many<I>(&self, iter: I)
66 where
67 I: Iterator<Item = Url>,
68 {
69 let mut secondary = self.0.secondary.write();
70 for uri in iter {
71 let _ = self.0.primary.push(uri.clone()).is_ok();
72 secondary.insert(uri);
73 }
74 }
75
76 pub async fn get_rand(&self) -> Option<Url> {
78 if let Some(uri) = self.0.primary.pop() {
79 Some(uri)
80 } else {
81 let lock = self.0.secondary.read();
82
83 if lock.is_empty() {
84 return None;
85 }
86
87 let mut rng = rand::thread_rng();
88 let index = rng.gen_range(0..lock.len());
89
90 lock.get_index(index).cloned()
91 }
92 }
93}
94
95impl Default for Cache {
96 fn default() -> Self {
97 Cache::new()
98 }
99}
100
101#[derive(Debug)]
103struct CacheInner {
104 primary: ArrayQueue<Url>,
105 secondary: RwLock<IndexSet<Url>>,
106}
107
108#[derive(Clone, Debug)]
110pub struct NekosClient {
111 client: nekos::Client,
112
113 cache: Cache,
114 nsfw_cache: Cache,
115}
116
117impl NekosClient {
118 pub fn new() -> Self {
120 NekosClient {
121 client: Default::default(),
122 cache: Cache::new(),
123 nsfw_cache: Cache::new(),
124 }
125 }
126
127 fn get_cache(&self, nsfw: bool) -> &Cache {
129 if nsfw {
130 &self.nsfw_cache
131 } else {
132 &self.cache
133 }
134 }
135
136 pub async fn populate(&self, nsfw: bool) -> anyhow::Result<()> {
138 let cache = self.get_cache(nsfw);
139 let image_list = self
140 .client
141 .get_random(Some(nsfw), BUFFER_SIZE)
142 .await
143 .context("failed to get random nekos image list")?;
144
145 cache.add_many(
146 image_list
147 .images
148 .iter()
149 .filter_map(|img| img.get_url().ok()),
150 );
151
152 Ok(())
153 }
154
155 pub async fn get_rand(&self, nsfw: bool) -> anyhow::Result<Url> {
157 let cache = self.get_cache(nsfw);
158
159 if cache.primary_is_empty() {
160 let self_clone = self.clone();
161 tokio::spawn(async move {
162 if let Err(e) = self_clone
164 .populate(nsfw)
165 .await
166 .context("failed to get new nekos data")
167 {
168 error!("{:?}", e);
169 }
170 });
171 }
172
173 if cache.secondary_is_empty() {
174 self.populate(nsfw)
175 .await
176 .context("failed to populate caches")?;
177 }
178
179 cache
180 .get_rand()
181 .await
182 .context("both primary and secondary caches are empty")
183 }
184}
185
186impl CacheStatsProvider for NekosClient {
187 fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
188 let cache = self.get_cache(false);
189 let nsfw_cache = self.get_cache(true);
190
191 cache_stats_builder.publish_stat("nekos", "primary_cache", cache.primary_len() as f32);
192 cache_stats_builder.publish_stat(
193 "nekos",
194 "primary_nsfw_cache",
195 nsfw_cache.primary_len() as f32,
196 );
197 cache_stats_builder.publish_stat("nekos", "secondary_cache", cache.secondary_len() as f32);
198 cache_stats_builder.publish_stat(
199 "nekos",
200 "secondary_nsfw_cache",
201 nsfw_cache.secondary_len() as f32,
202 );
203 }
204}
205
206impl Default for NekosClient {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212#[derive(Debug, Copy, Clone, FromOptions)]
217pub struct NekosArguments {
218 pub nsfw: Option<bool>,
220}
221
222pub fn create_slash_command() -> anyhow::Result<pikadick_slash_framework::Command> {
224 pikadick_slash_framework::CommandBuilder::new()
225 .name("nekos")
226 .description("Get a random neko")
227 .argument(
228 pikadick_slash_framework::ArgumentParamBuilder::new()
229 .name("nsfw")
230 .kind(pikadick_slash_framework::ArgumentKind::Boolean)
231 .description("Whether this should use nsfw results")
232 .build()?,
233 )
234 .on_process(|ctx, interaction, args: NekosArguments| async move {
235 let data_lock = ctx.data.read().await;
236 let client_data = data_lock
237 .get::<ClientDataKey>()
238 .expect("failed to get client data");
239 let nekos_client = client_data.nekos_client.clone();
240 drop(data_lock);
241
242 let content = match nekos_client
243 .get_rand(args.nsfw.unwrap_or(false))
244 .await
245 .context("failed to repopulate nekos caches")
246 {
247 Ok(url) => url.into(),
248 Err(error) => {
249 error!("{error:?}");
250 format!("{error:?}")
251 }
252 };
253
254 let message_builder = CreateInteractionResponseMessage::new().content(content);
255 let response = CreateInteractionResponse::Message(message_builder);
256
257 interaction.create_response(&ctx.http, response).await?;
258
259 Ok(())
260 })
261 .build()
262 .context("failed to build command")
263}