pikadick/commands/
urban.rs

1use 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::{
26        prelude::*,
27        timestamp::Timestamp,
28    },
29    prelude::*,
30};
31use std::sync::Arc;
32use tracing::error;
33
34/// A Caching Urban Dictionary Client
35///
36#[derive(Clone, Default, Debug)]
37pub struct UrbanClient {
38    client: urban_dictionary::Client,
39    search_cache: TimedCache<String, urban_dictionary::DefinitionList>,
40}
41
42impl UrbanClient {
43    /// Make a new [`UrbanClient`].
44    ///
45    pub fn new() -> UrbanClient {
46        Default::default()
47    }
48
49    /// Get the top result for a query.
50    ///
51    pub async fn search(
52        &self,
53        query: &str,
54    ) -> Result<Arc<TimedCacheEntry<urban_dictionary::DefinitionList>>, urban_dictionary::Error>
55    {
56        if let Some(entry) = self.search_cache.get_if_fresh(query) {
57            return Ok(entry);
58        }
59
60        let results = self.client.lookup(query).await?;
61        self.search_cache.insert(String::from(query), results);
62
63        Ok(self
64            .search_cache
65            .get_if_fresh(query)
66            .expect("recently acquired entry expired"))
67    }
68}
69
70impl CacheStatsProvider for UrbanClient {
71    fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
72        cache_stats_builder.publish_stat("urban", "search_cache", self.search_cache.len() as f32);
73    }
74}
75
76#[command]
77#[description("Get the top definition from UrbanDictionary.com")]
78#[usage("\"<query>\"")]
79#[example("\"test\"")]
80#[min_args(1)]
81#[max_args(1)]
82#[checks(Enabled)]
83#[bucket("default")]
84pub async fn urban(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
85    let data_lock = ctx.data.read().await;
86    let client_data = data_lock.get::<ClientDataKey>().unwrap();
87    let client = client_data.urban_client.clone();
88    drop(data_lock);
89
90    let mut loading = LoadingReaction::new(ctx.http.clone(), msg);
91    let query = args.quoted().trimmed().current().expect("missing arg");
92
93    match client
94        .search(query)
95        .await
96        .context("failed to search urban dictionary")
97    {
98        Ok(entry) => {
99            if let Some(entry) = entry.data().list.first() {
100                let mut thumbs_down_buf = itoa::Buffer::new();
101
102                let mut embed_builder = CreateEmbed::new()
103                    .title(&entry.word)
104                    .url(entry.permalink.as_str())
105                    .field("Definition", entry.get_raw_definition(), false)
106                    .field("Example", entry.get_raw_example(), false)
107                    .field("👍", entry.thumbs_up.to_string(), true)
108                    .field("👎", thumbs_down_buf.format(entry.thumbs_down), true);
109
110                match Timestamp::parse(entry.written_on.as_str())
111                    .context("failed to parse timestamp")
112                {
113                    Ok(timestamp) => {
114                        embed_builder = embed_builder.timestamp(timestamp);
115                    }
116                    Err(error) => {
117                        error!("{error}");
118                    }
119                }
120
121                let message_builder = CreateMessage::new().embed(embed_builder);
122
123                msg.channel_id
124                    .send_message(&ctx.http, message_builder)
125                    .await?;
126
127                loading.send_ok();
128            } else {
129                msg.channel_id.say(&ctx.http, "No results").await?;
130            }
131        }
132
133        Err(error) => {
134            error!("{error:?}");
135            msg.channel_id.say(&ctx.http, format!("{error:?}")).await?;
136        }
137    }
138
139    client.search_cache.trim();
140
141    Ok(())
142}