Skip to main content

botkit_telegram/
event.rs

1use std::any::Any;
2
3use botkit_core::action::ChatActionSender;
4use botkit_core::{ContextData, OptionValue};
5
6use crate::action::TelegramActionSender;
7use crate::client::TelegramClient;
8use crate::types::{EntityType, Update, UpdateKind};
9
10/// Telegram context data - implements ContextData for platform abstraction
11pub struct TelegramContextData {
12    pub update: Update,
13    // Client for API calls
14    client: TelegramClient,
15    // Cached values
16    channel_id: String,
17    chat_id: i64,
18    user_id: String,
19    user_name: String,
20    command_name: Option<String>,
21    command_args: Option<String>,
22    button_id: Option<String>,
23    message_content: Option<String>,
24}
25
26impl TelegramContextData {
27    pub fn new(update: Update, client: TelegramClient) -> Self {
28        let (chat_id, user_id, user_name, message_content) = match &update.kind {
29            UpdateKind::Message(m) | UpdateKind::EditedMessage(m) => {
30                let user = m.from.as_ref();
31                (
32                    m.chat.id,
33                    user.map(|u| u.id.to_string()).unwrap_or_default(),
34                    user.map(|u| u.first_name.clone()).unwrap_or_default(),
35                    m.text.clone(),
36                )
37            }
38            UpdateKind::CallbackQuery(cq) => {
39                let chat_id = cq.message.as_ref().map(|m| m.chat.id).unwrap_or(0);
40                (
41                    chat_id,
42                    cq.from.id.to_string(),
43                    cq.from.first_name.clone(),
44                    cq.message.as_ref().and_then(|m| m.text.clone()),
45                )
46            }
47            _ => (0, String::new(), String::new(), None),
48        };
49
50        let (command_name, command_args) = Self::extract_command(&update);
51
52        let button_id = match &update.kind {
53            UpdateKind::CallbackQuery(cq) => cq.data.clone(),
54            _ => None,
55        };
56
57        Self {
58            update,
59            client,
60            channel_id: chat_id.to_string(),
61            chat_id,
62            user_id,
63            user_name,
64            command_name,
65            command_args,
66            button_id,
67            message_content,
68        }
69    }
70
71    /// Get the client for making API calls
72    pub fn client(&self) -> &TelegramClient {
73        &self.client
74    }
75
76    /// Get the numeric chat ID
77    pub fn chat_id(&self) -> i64 {
78        self.chat_id
79    }
80
81    fn extract_command(update: &Update) -> (Option<String>, Option<String>) {
82        let message = match &update.kind {
83            UpdateKind::Message(m) => m,
84            _ => return (None, None),
85        };
86
87        let text = match &message.text {
88            Some(t) => t,
89            None => return (None, None),
90        };
91
92        let entities = match &message.entities {
93            Some(e) => e,
94            None => return (None, None),
95        };
96
97        // Find bot_command entity at offset 0
98        let cmd_entity = entities
99            .iter()
100            .find(|e| matches!(e.entity_type, EntityType::BotCommand) && e.offset == 0);
101
102        let cmd_entity = match cmd_entity {
103            Some(e) => e,
104            None => return (None, None),
105        };
106
107        let cmd_text = &text[..cmd_entity.length as usize];
108        // Remove leading '/' and any @bot_name suffix
109        let name = cmd_text
110            .trim_start_matches('/')
111            .split('@')
112            .next()
113            .unwrap_or("")
114            .to_string();
115
116        let args = text[cmd_entity.length as usize..].trim().to_string();
117        let args = if args.is_empty() { None } else { Some(args) };
118
119        (Some(name), args)
120    }
121}
122
123impl ContextData for TelegramContextData {
124    fn channel_id(&self) -> &str {
125        &self.channel_id
126    }
127
128    fn user_id(&self) -> &str {
129        &self.user_id
130    }
131
132    fn user_name(&self) -> &str {
133        &self.user_name
134    }
135
136    fn command_name(&self) -> Option<&str> {
137        self.command_name.as_deref()
138    }
139
140    fn command_args(&self) -> Option<&str> {
141        self.command_args.as_deref()
142    }
143
144    fn option(&self, _name: &str) -> Option<OptionValue> {
145        // Telegram doesn't have structured options like Discord
146        None
147    }
148
149    fn button_id(&self) -> Option<&str> {
150        self.button_id.as_deref()
151    }
152
153    fn message_content(&self) -> Option<&str> {
154        self.message_content.as_deref()
155    }
156
157    fn as_any(&self) -> &dyn Any {
158        self
159    }
160
161    fn action_sender(&self) -> Option<Box<dyn ChatActionSender>> {
162        if self.chat_id == 0 {
163            return None;
164        }
165        Some(Box::new(TelegramActionSender::new(
166            self.client.clone(),
167            self.chat_id,
168        )))
169    }
170}