pikadick/commands/
fml.rs

1use crate::{
2    checks::ENABLED_CHECK,
3    client_data::{
4        CacheStatsBuilder,
5        CacheStatsProvider,
6    },
7    util::LoadingReaction,
8    ClientDataKey,
9};
10use crossbeam::queue::SegQueue;
11use fml::{
12    types::Article,
13    FmlResult,
14};
15use serenity::{
16    builder::{
17        CreateEmbed,
18        CreateMessage,
19    },
20    client::Context,
21    framework::standard::{
22        macros::command,
23        Args,
24        CommandResult,
25    },
26    model::channel::Message,
27};
28use std::sync::Arc;
29use tracing::error;
30
31/// A caching fml client
32#[derive(Clone, Debug)]
33pub struct FmlClient {
34    client: fml::Client,
35    cache: Arc<SegQueue<Article>>,
36}
37
38impl FmlClient {
39    /// Make a new FmlClient
40    pub fn new(key: String) -> Self {
41        Self {
42            client: fml::Client::new(key),
43            cache: Arc::new(SegQueue::new()),
44        }
45    }
46
47    /// Repopulate the cache
48    async fn repopulate(&self) -> FmlResult<()> {
49        let articles = self.client.list_random(100).await?;
50        for article in articles.into_iter() {
51            self.cache.push(article);
52        }
53
54        Ok(())
55    }
56
57    fn should_repopulate(&self) -> bool {
58        self.cache.len() < 50
59    }
60
61    fn get_entry(&self) -> Option<Article> {
62        self.cache.pop()
63    }
64}
65
66impl CacheStatsProvider for FmlClient {
67    fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
68        cache_stats_builder.publish_stat("fml", "cache", self.cache.len() as f32);
69    }
70}
71
72// TODO: Format command output better
73#[command]
74#[description("Get a random story from fmylife.com")]
75#[checks(Enabled)]
76#[bucket("default")]
77async fn fml(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
78    let data_lock = ctx.data.read().await;
79    let client_data = data_lock.get::<ClientDataKey>().unwrap();
80    let client = client_data.fml_client.clone();
81    drop(data_lock);
82
83    let mut loading = LoadingReaction::new(ctx.http.clone(), msg);
84
85    if client.should_repopulate() {
86        if let Err(error) = client.repopulate().await {
87            error!("Failed to repopulate fml cache: {error}");
88
89            msg.channel_id
90                .say(&ctx.http, format!("Failed to get fml entry: {error}"))
91                .await?;
92
93            return Ok(());
94        }
95    }
96
97    if let Some(entry) = client.get_entry() {
98        let mut votes_up_buf = itoa::Buffer::new();
99        let mut votes_down_buf = itoa::Buffer::new();
100
101        let embed_builder = CreateEmbed::new()
102            .title("FML Story")
103            .description(entry.content_hidden)
104            .field(
105                "I agree, your life sucks",
106                votes_up_buf.format(entry.metrics.votes_up),
107                true,
108            )
109            .field(
110                "You deserved it",
111                votes_down_buf.format(entry.metrics.votes_down),
112                true,
113            )
114            .field("\u{200B}", "\u{200B}", false)
115            .field(
116                "Reactions",
117                format!(
118                    "😐 {}\n\n😃 {}\n\n😲 {}\n\nšŸ˜‚ {}",
119                    entry.metrics.smiley_amusing,
120                    entry.metrics.smiley_funny,
121                    entry.metrics.smiley_weird,
122                    entry.metrics.smiley_hilarious
123                ),
124                true,
125            );
126        let message_builder = CreateMessage::new().embed(embed_builder);
127
128        msg.channel_id
129            .send_message(&ctx.http, message_builder)
130            .await?;
131
132        loading.send_ok();
133    } else {
134        // TODO: Maybe get a lock so this can't fail?
135        msg.channel_id
136            .say(&ctx.http, "Failed to get fml entry: Cache Empty")
137            .await?;
138    };
139
140    Ok(())
141}