rustacean_roulette/
lib.rs

1mod commands;
2mod defaults;
3
4pub use commands::Commands;
5use frankenstein::{client_reqwest::Bot, methods::{SetMyCommandsParams, SetMyDefaultAdministratorRightsParams}, types::ChatAdministratorRights, AsyncTelegramApi, Error};
6use rand::{Rng, seq::index::sample};
7use serde::Deserialize;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10/// Configuration for the bot.
11#[derive(Deserialize)]
12pub struct Config {
13    /// The token for the bot.
14    pub token: String,
15    /// The configuration for the Russian Roulette game.
16    #[serde(default)]
17    pub game: Roulette,
18    /// The override configuration for groups.
19    #[serde(default)]
20    pub groups: Vec<GroupConfig>,
21}
22
23/// A Russian Roulette game.
24#[derive(Clone, Debug, Deserialize)]
25pub struct Roulette {
26    /// Number of chambers in the revolver.
27    #[serde(default = "defaults::chambers")]
28    chambers: usize,
29    /// Number of bullets in the revolver.
30    #[serde(default = "defaults::bullets")]
31    bullets: usize,
32    /// Minimum time to mute in seconds.
33    #[serde(default = "defaults::min_mute_time")]
34    min_mute_time: u32,
35    /// Maximum time to mute in seconds.
36    #[serde(default = "defaults::max_mute_time")]
37    max_mute_time: u32,
38    /// An array of boolean values representing the contents of the chambers. `true` means the chamber is loaded with a bullet, `false` means it is empty.
39    #[serde(skip)]
40    contents: Vec<bool>,
41    /// The current chamber index.
42    #[serde(skip)]
43    position: usize,
44}
45
46impl Roulette {
47    /// (Re-)Start a new game of Russian Roulette.
48    pub fn restart(&mut self) {
49        // Sanity check
50        assert!(
51            self.chambers > 0,
52            "Number of chambers must be greater than 0"
53        );
54        assert!(self.bullets > 0, "Number of bullets must be greater than 0");
55        assert!(
56            self.bullets <= self.chambers,
57            "Number of bullets must be less than or equal to number of chambers"
58        );
59        assert!(
60            self.min_mute_time >= 30,
61            "Minimum mute time must be greater than or equal to 30 seconds"
62        );
63        assert!(
64            self.max_mute_time <= 3600,
65            "Maximum mute time must be less than or equal to 3600 seconds"
66        ); // FIXME: 365 days
67        assert!(
68            self.min_mute_time <= self.max_mute_time,
69            "Minimum mute time must be less than or equal to maximum mute time"
70        );
71
72        self.position = 0;
73        self.contents.resize(self.chambers, false);
74
75        // Randomly choose `bullets` chambers to be loaded with bullets.
76        let mut rng = rand::rng();
77        let selected = sample(&mut rng, self.chambers, self.bullets);
78        for i in selected {
79            self.contents[i] = true;
80        }
81    }
82
83    /// Get the number of bullets and chambers.
84    pub fn info(&self) -> (usize, usize) {
85        (self.bullets, self.chambers)
86    }
87
88    /// Generate a random mute time and the time until which the user will be muted.
89    pub fn random_mute_until(&self) -> (u64, u64) {
90        // Generate a random mute time between min and max
91        let mut rng = rand::rng();
92        let duration: u64 = rng
93            .random_range(self.min_mute_time..=self.max_mute_time)
94            .into();
95        // Convert to seconds and add to current time
96        let now = SystemTime::now()
97            .duration_since(UNIX_EPOCH)
98            .expect("Time went backwards")
99            .as_secs();
100        (duration, now + duration)
101    }
102
103    /// Try to fire the current chamber.
104    ///
105    /// - If the chamber is loaded with a bullet, return `Some(true)`
106    /// - If the chamber is empty, return `Some(false)`
107    /// - If we have fired all filled chambers, return `None`
108    pub fn fire(&mut self) -> Option<bool> {
109        if self.peek().0 == 0 {
110            // No filled chambers left
111            return None;
112        }
113
114        let result = self.contents[self.position];
115        self.position += 1;
116
117        Some(result)
118    }
119
120    /// Peek the left-over chambers, returning count of filled and left chambers.
121    pub fn peek(&self) -> (usize, usize) {
122        let filled = self
123            .contents
124            .iter()
125            .skip(self.position)
126            .filter(|&&x| x)
127            .count();
128        let left = self.contents.len() - self.position;
129        (filled, left)
130    }
131}
132
133impl Default for Roulette {
134    fn default() -> Self {
135        Roulette {
136            chambers: defaults::chambers(),
137            bullets: defaults::bullets(),
138            min_mute_time: defaults::min_mute_time(),
139            max_mute_time: defaults::max_mute_time(),
140            contents: vec![],
141            position: 0,
142        }
143    }
144}
145
146/// Configuration for a group.
147#[derive(Debug, Deserialize)]
148pub struct GroupConfig {
149    /// The ID of the group.
150    pub id: i64,
151    /// Override number of chambers in the revolver.
152    chambers: Option<usize>,
153    /// Override number of bullets in the revolver.
154    bullets: Option<usize>,
155    /// Override minimum time to mute in seconds.
156    min_mute_time: Option<u32>,
157    /// Override maximum time to mute in seconds.
158    max_mute_time: Option<u32>,
159}
160
161impl GroupConfig {
162    /// Resolves to a [`RouletteConfig`].
163    pub fn resolve(&self, default: &Roulette) -> Roulette {
164        let Self {
165            chambers,
166            bullets,
167            min_mute_time,
168            max_mute_time,
169            ..
170        } = self;
171        let (chambers, bullets, min_mute_time, max_mute_time) = (
172            chambers.unwrap_or(default.chambers),
173            bullets.unwrap_or(default.bullets),
174            min_mute_time.unwrap_or(default.min_mute_time),
175            max_mute_time.unwrap_or(default.max_mute_time),
176        );
177        Roulette {
178            chambers,
179            bullets,
180            min_mute_time,
181            max_mute_time,
182            ..Default::default()
183        }
184    }
185}
186
187/// Set commands and default admin rights for the bot.
188pub async fn init_commands_and_rights(bot: &Bot) -> Result<(), Error> {
189    let commands_param = SetMyCommandsParams::builder()
190        .commands(Commands::list())
191        .build();
192    bot.set_my_commands(&commands_param).await?;
193
194    let rights = ChatAdministratorRights::builder()
195        .is_anonymous(false)
196        .can_manage_chat(false)
197        .can_delete_messages(false)
198        .can_manage_video_chats(false)
199        .can_restrict_members(true) // Required
200        .can_promote_members(false)
201        .can_change_info(false)
202        .can_invite_users(false)
203        .build();
204    let rights_param = SetMyDefaultAdministratorRightsParams::builder()
205        .rights(rights)
206        .build();
207    bot.set_my_default_administrator_rights(&rights_param).await?;
208
209    Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_fire() {
218        let mut roulette = Roulette {
219            contents: vec![false, true, false],
220            position: 0,
221            chambers: 3,
222            bullets: 1,
223            ..Default::default()
224        };
225
226        assert_eq!(roulette.fire(), Some(false));
227        assert_eq!(roulette.fire(), Some(true));
228        // assert_eq!(roulette.fire(), Some(false)); // Early reload
229        assert_eq!(roulette.fire(), None);
230        assert_eq!(roulette.fire(), None);
231    }
232
233    #[test]
234    fn test_restart() {
235        let mut roulette = Roulette::default();
236
237        roulette.restart();
238        assert_eq!(roulette.contents.len(), 6);
239        assert_eq!(roulette.contents.iter().filter(|&&x| x).count(), 2);
240        assert_eq!(roulette.position, 0);
241    }
242}