pikadick/commands/
shift.rs

1use crate::{
2    checks::ENABLED_CHECK,
3    client_data::{
4        CacheStatsBuilder,
5        CacheStatsProvider,
6    },
7    util::TimedCache,
8    ClientDataKey,
9};
10use rand::seq::SliceRandom;
11use serenity::{
12    framework::standard::{
13        macros::command,
14        ArgError,
15        Args,
16        CommandResult,
17    },
18    model::prelude::*,
19    prelude::*,
20};
21use shift_orcz::{
22    Client as OrczClient,
23    Game,
24    ShiftCode,
25};
26use std::{
27    str::FromStr,
28    sync::Arc,
29};
30
31#[derive(Debug)]
32struct GameParseError(String);
33
34struct GameArg(Game);
35
36impl FromStr for GameArg {
37    type Err = GameParseError;
38
39    fn from_str(s: &str) -> Result<Self, Self::Err> {
40        match s {
41            "bl" => Ok(Self(Game::Borderlands)),
42            "bl2" => Ok(Self(Game::Borderlands2)),
43            "blps" => Ok(Self(Game::BorderlandsPreSequel)),
44            "bl3" => Ok(Self(Game::Borderlands3)),
45            _ => Err(GameParseError(s.into())),
46        }
47    }
48}
49
50#[derive(Default, Clone)]
51pub struct ShiftClient {
52    orcz_client: OrczClient,
53    cache: TimedCache<Game, Vec<Arc<ShiftCode>>>,
54}
55
56impl ShiftClient {
57    pub fn new() -> Self {
58        ShiftClient {
59            orcz_client: OrczClient::new(),
60            cache: TimedCache::new(),
61        }
62    }
63
64    /// Get a random shift code (PC only for now...)
65    pub async fn get_rand(
66        &self,
67        game: Game,
68    ) -> Result<Option<Arc<ShiftCode>>, shift_orcz::OrczError> {
69        if let Some(entry) = self.cache.get_if_fresh(&game) {
70            return Ok(entry.data().choose(&mut rand::thread_rng()).cloned());
71        }
72
73        let codes = self
74            .orcz_client
75            .get_shift_codes(game)
76            .await?
77            .into_iter()
78            .filter(|e| e.pc.is_valid())
79            .map(Arc::new)
80            .collect();
81
82        self.cache.insert(game, codes);
83
84        Ok(self
85            .cache
86            .get_if_fresh(&game)
87            .and_then(|entry| entry.data().choose(&mut rand::thread_rng()).cloned()))
88    }
89}
90
91impl CacheStatsProvider for ShiftClient {
92    fn publish_cache_stats(&self, cache_stats_builder: &mut CacheStatsBuilder) {
93        cache_stats_builder.publish_stat(
94            "shift",
95            "bl_cache",
96            self.cache
97                .get_if_fresh(&Game::Borderlands)
98                .map(|el| el.data().len())
99                .unwrap_or(0) as f32,
100        );
101
102        cache_stats_builder.publish_stat(
103            "shift",
104            "bl2_cache",
105            self.cache
106                .get_if_fresh(&Game::Borderlands2)
107                .map(|el| el.data().len())
108                .unwrap_or(0) as f32,
109        );
110
111        cache_stats_builder.publish_stat(
112            "shift",
113            "blps_cache",
114            self.cache
115                .get_if_fresh(&Game::BorderlandsPreSequel)
116                .map(|el| el.data().len())
117                .unwrap_or(0) as f32,
118        );
119
120        cache_stats_builder.publish_stat(
121            "shift",
122            "bl3_cache",
123            self.cache
124                .get_if_fresh(&Game::Borderlands3)
125                .map(|el| el.data().len())
126                .unwrap_or(0) as f32,
127        );
128    }
129}
130
131impl std::fmt::Debug for ShiftClient {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        // TODO: replace with derive impl if/when orcz_client becomes Debug-able
134        f.debug_struct("ShiftClient")
135            .field("cache", &self.cache)
136            .finish()
137    }
138}
139
140#[command]
141#[description("Get a random shift code for a Borderlands game")]
142#[min_args(1)]
143#[usage("<bl, bl2, blps, or bl3>")]
144#[example("blps")]
145#[checks(Enabled)]
146#[bucket("default")]
147async fn shift(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
148    let data_lock = ctx.data.read().await;
149    let client_data = data_lock.get::<ClientDataKey>().unwrap();
150    let client = client_data.shift_client.clone();
151    drop(data_lock);
152
153    let game = match args.single::<GameArg>().map(|el| el.0) {
154        Ok(g) => g,
155        Err(ArgError::Parse(e)) => {
156            msg.channel_id
157                .say(
158                    &ctx.http,
159                    format!("Invalid arg '{}'. Valid: bl, bl2, blps, bl3", e.0),
160                )
161                .await?;
162            return Ok(());
163        }
164        Err(_) => {
165            msg.channel_id
166                .say(&ctx.http, "Need arg. Valid: bl, bl2, blps, bl3")
167                .await?;
168            return Ok(());
169        }
170    };
171
172    match client.get_rand(game).await {
173        Ok(Some(code)) => {
174            msg.channel_id
175                .say(
176                    &ctx.http,
177                    format!(
178                        "Source: {}\nIssue Date: {}\nReward: {}\nCode: {}",
179                        code.source,
180                        code.issue_date
181                            .map(|d| d.to_string())
182                            .unwrap_or_else(|| "unknown".to_string()),
183                        code.rewards,
184                        code.pc
185                    ),
186                )
187                .await?;
188        }
189        Ok(None) => {
190            msg.channel_id
191                .say(&ctx.http, format!("No valid codes for {:?}", game))
192                .await?;
193        }
194        Err(e) => {
195            msg.channel_id
196                .say(&ctx.http, format!("Failed to get shift code: {:#?}", e))
197                .await?;
198        }
199    }
200
201    client.cache.trim();
202
203    Ok(())
204}