pikadick/commands/
r6stats.rs1use 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 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#[derive(Debug, pikadick_slash_framework::FromOptions)]
70struct R6StatsOptions {
71 name: String,
73}
74
75pub 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}