Skip to main content

botkit_core/
action.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6
7#[cfg(not(target_arch = "wasm32"))]
8use async_io::Timer;
9#[cfg(target_arch = "wasm32")]
10use gloo_timers::future::sleep;
11
12use crate::BotError;
13
14#[cfg(not(target_arch = "wasm32"))]
15pub type ChatActionFuture<'a> = Pin<Box<dyn Future<Output = Result<(), BotError>> + Send + 'a>>;
16#[cfg(target_arch = "wasm32")]
17pub type ChatActionFuture<'a> = Pin<Box<dyn Future<Output = Result<(), BotError>> + 'a>>;
18
19#[cfg(not(target_arch = "wasm32"))]
20pub trait ChatActionSenderBounds: Send + Sync {}
21#[cfg(not(target_arch = "wasm32"))]
22impl<T: Send + Sync + ?Sized> ChatActionSenderBounds for T {}
23
24#[cfg(target_arch = "wasm32")]
25pub trait ChatActionSenderBounds {}
26#[cfg(target_arch = "wasm32")]
27impl<T: ?Sized> ChatActionSenderBounds for T {}
28
29/// Chat action types for platform indicators
30///
31/// Internal enum - not exported publicly. Used by framework
32/// to show appropriate indicators (typing, uploading, etc.)
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ChatAction {
35    /// User is typing a message
36    Typing,
37    /// User is uploading a photo
38    UploadPhoto,
39    /// User is recording a video
40    RecordVideo,
41    /// User is uploading a video
42    UploadVideo,
43    /// User is recording audio/voice
44    RecordVoice,
45    /// User is uploading audio/voice
46    UploadVoice,
47    /// User is uploading a document
48    UploadDocument,
49    /// User is choosing a sticker
50    ChooseSticker,
51    /// User is finding a location
52    FindLocation,
53    /// User is recording a video note
54    RecordVideoNote,
55    /// User is uploading a video note
56    UploadVideoNote,
57}
58
59/// Trait for sending chat actions to a channel
60///
61/// Platform implementations define how to send actions and their expiration times.
62pub trait ChatActionSender: ChatActionSenderBounds + 'static {
63    /// Send a chat action to the specified channel
64    fn send_action(&self, action: ChatAction) -> ChatActionFuture<'_>;
65
66    /// Duration after which the action indicator expires
67    ///
68    /// Used for auto-renewal: renew at 80% of this duration.
69    fn action_expiry(&self) -> Duration;
70
71    /// Clone this sender into a boxed trait object
72    fn clone_boxed(&self) -> Box<dyn ChatActionSender>;
73}
74
75/// RAII guard that keeps a chat action active until dropped
76///
77/// When created, immediately sends the action and starts auto-renewal.
78/// When dropped, signals the background task to stop.
79///
80/// # Example
81/// ```ignore
82/// async fn slow_command(ctx: Context) -> String {
83///     let _typing = ctx.typing();  // Starts typing indicator
84///     expensive_work().await;
85///     "Done!"
86/// }  // Typing stops when _typing is dropped
87/// ```
88pub struct ChatActionGuard {
89    stop_flag: Arc<AtomicBool>,
90}
91
92impl ChatActionGuard {
93    /// Create and start a chat action indicator
94    ///
95    /// The action is sent immediately and renewed automatically until
96    /// the guard is dropped.
97    pub fn start(sender: Box<dyn ChatActionSender>, action: ChatAction) -> Self {
98        let stop_flag = Arc::new(AtomicBool::new(false));
99        let flag_clone = Arc::clone(&stop_flag);
100
101        // Calculate renewal interval (80% of expiry time)
102        let expiry = sender.action_expiry();
103        let renewal_interval = Duration::from_millis((expiry.as_millis() as u64 * 80) / 100);
104
105        spawn_renewal(async move {
106            // Send initial action
107            let _ = sender.send_action(action).await;
108
109            loop {
110                // Sleep for renewal interval
111                sleep_for(renewal_interval).await;
112
113                // Check if we should stop
114                if flag_clone.load(Ordering::Acquire) {
115                    break;
116                }
117
118                // Renew the action
119                if sender.send_action(action).await.is_err() {
120                    break;
121                }
122            }
123        });
124
125        Self { stop_flag }
126    }
127}
128
129#[cfg(not(target_arch = "wasm32"))]
130async fn sleep_for(duration: Duration) {
131    Timer::after(duration).await;
132}
133
134#[cfg(not(target_arch = "wasm32"))]
135fn spawn_renewal(task: impl Future<Output = ()> + Send + 'static) {
136    executor_core::spawn(task).detach();
137}
138
139#[cfg(target_arch = "wasm32")]
140async fn sleep_for(duration: Duration) {
141    sleep(duration).await;
142}
143
144#[cfg(target_arch = "wasm32")]
145fn spawn_renewal(task: impl Future<Output = ()> + 'static) {
146    executor_core::spawn_local(task).detach();
147}
148
149impl Drop for ChatActionGuard {
150    fn drop(&mut self) {
151        self.stop_flag.store(true, Ordering::Release);
152    }
153}