rustacean-roulette 0.1.2

A simple Russian Roulette Telegram bot implemented in Rust.
Documentation
use super::{Command, Roulette};
use crate::constants::RESTRICTED_PERM;
use frankenstein::{
    AsyncTelegramApi,
    client_reqwest::Bot,
    methods::{GetChatMemberParams, RestrictChatMemberParams},
    types::{ChatMember, Message},
};
use log::{error, info};
use tokio::sync::Mutex;
use crate::FireResult;

/// Joins the roulette game.
pub struct RouletteCommand;

impl Command for RouletteCommand {
    const TRIGGER: &'static str = "roulette";
    const HELP: &'static str = "Joins the roulette game.";
    async fn execute(bot: &Bot, msg: Message, roulette: &Mutex<Roulette>) -> Option<String> {
        // Get chat and sender
        let chat = &msg.chat;
        let Some(sender) = &msg.from else {
            error!("Cannot determine sender of message: {msg:?}");
            return None;
        };
        // Determine sender's role
        let get_chat_member_param = GetChatMemberParams::builder()
            .chat_id(chat.id)
            .user_id(sender.id)
            .build();
        let member = match bot.get_chat_member(&get_chat_member_param).await {
            Ok(res) => res.result,
            Err(err) => {
                error!(
                    "Failed to get chat member info for user ID {}: {err}",
                    sender.id
                );
                return None;
            }
        };
        let is_admin = matches!(
            member,
            ChatMember::Creator(_) | ChatMember::Administrator(_)
        );
        if is_admin {
            return Some("Cannot play roulette as an admin".to_string());
        }
        // Check the roulette status
        let mut roulette = roulette.lock().await;
        let result = roulette.fire();

        // Reload the gun if empty
        let reload_tip = if roulette.peek().0 == 0 {
            roulette.reload();
            let (bullets, chambers) = roulette.info();
            format!(" The gun has been reloaded, with {bullets} bullets in {chambers} chambers.")
        } else {
            String::new()
        };

        // Apply action and return the message
        let name = sender.username.as_deref();
        let name = name.unwrap_or(&sender.first_name);
        match result {
            FireResult::Bullet => {
                // Restrict the user for a certain period
                let (duration, until) = roulette.random_mute_until();
                let restrict_param = RestrictChatMemberParams::builder()
                    .chat_id(chat.id)
                    .user_id(sender.id)
                    .permissions(RESTRICTED_PERM)
                    .until_date(until)
                    .build();
                match bot.restrict_chat_member(&restrict_param).await {
                    Ok(_) => {
                        info!(
                            "Restricted user {name} for {duration}s in group <{}>",
                            chat.id
                        );
                    }
                    Err(err) => {
                        error!("Failed to restrict user {name}: {err}");
                        return None;
                    }
                };
                Some(format!("Bang! {name} was shot and muted for {duration}s.",) + &reload_tip)
            },
            FireResult::Empty => {
                Some(format!("Click! {name} is safe and sound.",) + &reload_tip)
            },
            FireResult::Jammed => {
                Some(format!("Click? You're lucky that the gun got jammed.",) + &reload_tip)
            },
            FireResult::NoBullets => {
                // This should not happen, but just in case
                Some(format!("Click? The gun is somehow empty...",) + &reload_tip)
            },
        }
    }
}