pikadick/commands/
deviantart.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 Database,
14};
15use anyhow::Context as _;
16use deviantart::Deviation;
17use rand::seq::IteratorRandom;
18use serenity::{
19 framework::standard::{
20 macros::command,
21 Args,
22 CommandResult,
23 },
24 model::prelude::*,
25 prelude::*,
26};
27use std::{
28 sync::Arc,
29 time::Instant,
30};
31use tracing::{
32 error,
33 info,
34};
35
36const DATA_STORE_NAME: &str = "deviantart";
37const COOKIE_KEY: &str = "cookie-store";
38
39#[derive(Clone, Debug)]
41pub struct DeviantartClient {
42 client: deviantart::Client,
43 search_cache: TimedCache<String, Vec<Deviation>>,
44}
45
46impl DeviantartClient {
47 pub async fn new(db: &Database) -> anyhow::Result<Self> {
49 use std::io::BufReader;
50
51 let client = deviantart::Client::new();
52
53 let cookie_data: Option<Vec<u8>> = db
54 .store_get(DATA_STORE_NAME, COOKIE_KEY)
55 .await
56 .context("failed to get cookie data")?;
57
58 match cookie_data {
59 Some(cookie_data) => {
60 client
61 .load_json_cookies(BufReader::new(std::io::Cursor::new(cookie_data)))
62 .await?;
63 }
64 None => {
65 info!("could not load cookie data");
66 }
67 }
68
69 Ok(DeviantartClient {
70 client,
71 search_cache: TimedCache::new(),
72 })
73 }
74
75 pub async fn sign_in(
77 &self,
78 db: &Database,
79 username: &str,
80 password: &str,
81 ) -> anyhow::Result<()> {
82 if !self.client.is_logged_in_online().await? {
83 info!("re-signing in");
84 self.client.login(username, password).await?;
85
86 let cookie_store = self.client.cookie_store.clone();
88 let cookie_data = tokio::task::spawn_blocking(move || {
89 let mut cookie_data = Vec::with_capacity(1_000_000); cookie_store
91 .lock()
92 .expect("cookie store is poisoned")
93 .save_json(&mut cookie_data)
94 .map_err(deviantart::WrapBoxError)?;
95 anyhow::Result::<_>::Ok(cookie_data)
96 })
97 .await??;
98 db.store_put(DATA_STORE_NAME, COOKIE_KEY, cookie_data)
99 .await?;
100 }
101
102 Ok(())
103 }
104
105 pub async fn search(
107 &self,
108 db: &Database,
109 username: &str,
110 password: &str,
111 query: &str,
112 ) -> anyhow::Result<Arc<TimedCacheEntry<Vec<Deviation>>>> {
113 if let Some(entry) = self.search_cache.get_if_fresh(query) {
114 return Ok(entry);
115 }
116
117 let start = Instant::now();
118 self.sign_in(db, username, password)
119 .await
120 .context("failed to log in to deviantart")?;
121 let mut search_cursor = self.client.search(query, None);
122 search_cursor
123 .next_page()
124 .await
125 .context("failed to search")?;
126 let list = search_cursor
127 .take_current_deviations()
128 .expect("missing page")
129 .context("failed to process results")?;
130 let ret = self.search_cache.insert_and_get(String::from(query), list);
131
132 info!("searched deviantart in {:?}", start.elapsed());
133
134 Ok(ret)
135 }
136}
137
138impl CacheStatsProvider for DeviantartClient {
139 fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
140 cache_stats_builder.publish_stat(
141 "deviantart",
142 "search_cache",
143 self.search_cache.len() as f32,
144 );
145 }
146}
147
148#[command]
149#[description("Get art from deviantart")]
150#[usage("<query>")]
151#[example("sun")]
152#[min_args(1)]
153#[max_args(1)]
154#[checks(Enabled)]
155#[bucket("default")]
156async fn deviantart(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
157 let data_lock = ctx.data.read().await;
158 let client_data = data_lock
159 .get::<ClientDataKey>()
160 .expect("missing clientdata");
161 let client = client_data.deviantart_client.clone();
162 let db = client_data.db.clone();
163 let config = client_data.config.clone();
164 drop(data_lock);
165
166 let query = args.trimmed().quoted().current().expect("missing query");
167
168 info!("Searching for '{}' on deviantart", query);
169
170 let mut loading = LoadingReaction::new(ctx.http.clone(), msg);
171
172 match client
173 .search(
174 &db,
175 &config.deviantart.username,
176 &config.deviantart.password,
177 query,
178 )
179 .await
180 {
181 Ok(entry) => {
182 let data = entry.data();
183 let choice = data
184 .iter()
185 .filter_map(|deviation| {
186 if deviation.is_image() {
187 Some(
188 deviation
189 .get_image_download_url()
190 .or_else(|| deviation.get_fullview_url()),
191 )
192 } else if deviation.is_film() {
193 Some(deviation.get_best_video_url().cloned())
194 } else {
195 None
196 }
197 })
198 .choose(&mut rand::thread_rng());
199
200 if let Some(choice) = choice {
201 if let Some(url) = choice {
202 loading.send_ok();
203 msg.channel_id.say(&ctx.http, url).await?;
204 } else {
205 msg.channel_id
206 .say(&ctx.http, "Missing url. This is probably a bug.")
207 .await?;
208 error!("DeviantArt deviation missing asset url: {:?}", choice);
209 }
210 } else {
211 msg.channel_id.say(&ctx.http, "No Results").await?;
212 }
213 }
214 Err(e) => {
215 msg.channel_id
216 .say(&ctx.http, format!("Failed to search '{}': {:?}", query, e))
217 .await?;
218
219 error!("Failed to search for {} on deviantart: {:?}", query, e);
220 }
221 }
222
223 client.search_cache.trim();
224
225 Ok(())
226}