pikadick/commands/
r6stats.rs

1use crate::{
2    client_data::{
3        CacheStatsBuilder,
4        CacheStatsProvider,
5    },
6    util::{
7        TimedCache,
8        TimedCacheEntry,
9    },
10    ClientDataKey,
11};
12use anyhow::Context as _;
13use r6stats::UserData;
14use serenity::builder::{
15    CreateEmbed,
16    CreateInteractionResponse,
17    CreateInteractionResponseMessage,
18};
19use std::sync::Arc;
20use tracing::{
21    error,
22    info,
23};
24
25#[derive(Clone, Default, Debug)]
26pub struct R6StatsClient {
27    client: r6stats::Client,
28    search_cache: TimedCache<String, UserData>,
29}
30
31impl R6StatsClient {
32    pub fn new() -> Self {
33        Self {
34            client: r6stats::Client::new(),
35            search_cache: TimedCache::new(),
36        }
37    }
38
39    /// Get stats
40    pub async fn get_stats(
41        &self,
42        query: &str,
43    ) -> Result<Option<Arc<TimedCacheEntry<UserData>>>, r6stats::Error> {
44        if let Some(entry) = self.search_cache.get_if_fresh(query) {
45            return Ok(Some(entry));
46        }
47
48        let mut user_list = self.client.search(query).await?;
49
50        if user_list.is_empty() {
51            return Ok(None);
52        }
53
54        let user = user_list.swap_remove(0);
55
56        self.search_cache.insert(String::from(query), user);
57
58        Ok(self.search_cache.get_if_fresh(query))
59    }
60}
61
62impl CacheStatsProvider for R6StatsClient {
63    fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
64        cache_stats_builder.publish_stat("r6stats", "search_cache", self.search_cache.len() as f32);
65    }
66}
67
68/// Options for r6stats
69#[derive(Debug, pikadick_slash_framework::FromOptions)]
70struct R6StatsOptions {
71    /// The user name
72    name: String,
73}
74
75/// Create a slash command
76pub fn create_slash_command() -> anyhow::Result<pikadick_slash_framework::Command> {
77    pikadick_slash_framework::CommandBuilder::new()
78        .name("r6stats")
79        .description("Get r6 stats for a user from r6stats")
80        .argument(
81            pikadick_slash_framework::ArgumentParamBuilder::new()
82                .name("name")
83                .description("The name of the user")
84                .kind(pikadick_slash_framework::ArgumentKind::String)
85                .required(true)
86                .build()?,
87        )
88        .on_process(|ctx, interaction, args: R6StatsOptions| async move {
89            let data_lock = ctx.data.read().await;
90            let client_data = data_lock
91                .get::<ClientDataKey>()
92                .expect("missing client data");
93            let client = client_data.r6stats_client.clone();
94            drop(data_lock);
95
96            let name = args.name.as_str();
97
98            info!("getting r6 stats for \"{name}\" using r6stats");
99
100            let result = client
101                .get_stats(name)
102                .await
103                .with_context(|| format!("failed to get stats for \"{name}\" using r6stats"));
104
105            let mut message_builder = CreateInteractionResponseMessage::new();
106            match result {
107                Ok(Some(entry)) => {
108                    let data = entry.data();
109
110                    let mut embed_builder = CreateEmbed::new();
111                    embed_builder = embed_builder
112                        .title(&data.username)
113                        .image(data.avatar_url_256.as_str());
114
115                    if let Some(stats) = data.seasonal_stats.as_ref() {
116                        embed_builder =
117                            embed_builder.field("MMR", ryu::Buffer::new().format(stats.mmr), true);
118                        embed_builder = embed_builder.field(
119                            "Max MMR",
120                            ryu::Buffer::new().format(stats.max_mmr),
121                            true,
122                        );
123                        embed_builder = embed_builder.field(
124                            "Mean Skill",
125                            ryu::Buffer::new().format(stats.skill_mean),
126                            true,
127                        );
128                    }
129
130                    if let Some(kd) = data.kd() {
131                        embed_builder = embed_builder.field(
132                            "Overall Kill / Death",
133                            ryu::Buffer::new().format(kd),
134                            true,
135                        );
136                    }
137
138                    if let Some(wl) = data.wl() {
139                        embed_builder = embed_builder.field(
140                            "Overall Win / Loss",
141                            ryu::Buffer::new().format(wl),
142                            true,
143                        );
144                    }
145
146                    message_builder = message_builder.embed(embed_builder);
147                }
148                Ok(None) => message_builder = message_builder.content("No results"),
149                Err(error) => {
150                    error!("{error:?}");
151                    message_builder = message_builder.content(format!("{error:?}"));
152                }
153            }
154            let response_builder = CreateInteractionResponse::Message(message_builder);
155
156            interaction
157                .create_response(&ctx.http, response_builder)
158                .await?;
159
160            client.search_cache.trim();
161
162            Ok(())
163        })
164        .build()
165        .context("failed to build r6stats command")
166}