Skip to main content

botkit_cli/
context.rs

1//! `ContextData` for the CLI platform: the unified `Context` view plus the
2//! full inbound payload for adapters that want it.
3
4use std::any::Any;
5
6use botkit_core::BotError;
7use botkit_core::action::{
8    AnyChatActionSender, ChatAction, ChatActionFutureBounds, ChatActionSender,
9};
10use botkit_core::{ContextData, OptionValue};
11
12use crate::hub::CliHub;
13use crate::wire::{Inbound, Outbound, OutboundAction};
14
15/// Per-event context for the CLI platform.
16///
17/// `event` carries the full wire payload — media, stickers, reactions,
18/// threads — for consumers that downcast via `Context::platform`.
19pub struct CliContextData {
20    /// The inbound event being dispatched.
21    pub event: Inbound,
22    hub: CliHub,
23}
24
25impl CliContextData {
26    /// Build the context for one inbound event.
27    pub(crate) fn new(event: Inbound, hub: CliHub) -> Self {
28        Self { event, hub }
29    }
30}
31
32impl ContextData for CliContextData {
33    fn channel_id(&self) -> &str {
34        self.event.chat()
35    }
36
37    fn user_id(&self) -> &str {
38        self.event.user().map_or("", |u| u.id.as_str())
39    }
40
41    fn user_name(&self) -> &str {
42        self.event.user().map_or("", |u| u.name.as_str())
43    }
44
45    fn command_name(&self) -> Option<&str> {
46        match &self.event {
47            Inbound::Command(command) => Some(command.name.as_str()),
48            _ => None,
49        }
50    }
51
52    fn command_args(&self) -> Option<&str> {
53        match &self.event {
54            Inbound::Command(command) => Some(command.args.as_str()),
55            _ => None,
56        }
57    }
58
59    fn option(&self, _name: &str) -> Option<OptionValue> {
60        None
61    }
62
63    fn button_id(&self) -> Option<&str> {
64        match &self.event {
65            Inbound::Button(button) => Some(button.data.as_str()),
66            _ => None,
67        }
68    }
69
70    fn message_content(&self) -> Option<&str> {
71        match &self.event {
72            Inbound::Message(message) => message.text.as_deref().or(message.caption.as_deref()),
73            Inbound::Button(button) => button.message_text.as_deref(),
74            Inbound::Edited(edited) => edited.text.as_deref(),
75            _ => None,
76        }
77    }
78
79    fn as_any(&self) -> &dyn Any {
80        self
81    }
82
83    fn action_sender(&self) -> Option<AnyChatActionSender> {
84        Some(AnyChatActionSender::new(CliActionSender {
85            hub: self.hub.clone(),
86            chat: self.event.chat().to_string(),
87            thread_id: self.event.thread_id(),
88        }))
89    }
90}
91
92/// Chat action sender that reports indicators as outbound `action` lines.
93#[derive(Clone)]
94pub struct CliActionSender {
95    hub: CliHub,
96    chat: String,
97    thread_id: Option<i64>,
98}
99
100impl CliActionSender {
101    /// A sender emitting action lines for `chat`, optionally in `thread_id`.
102    pub fn new(hub: CliHub, chat: String, thread_id: Option<i64>) -> Self {
103        Self {
104            hub,
105            chat,
106            thread_id,
107        }
108    }
109
110    fn emit(&self, action: &str, clear: bool) {
111        self.hub.emit(Outbound::Action(OutboundAction {
112            chat: self.chat.clone(),
113            action: action.to_string(),
114            clear,
115            thread_id: self.thread_id,
116        }));
117    }
118}
119
120impl ChatActionSender for CliActionSender {
121    fn send_action(
122        &self,
123        action: ChatAction,
124    ) -> impl ChatActionFutureBounds<Output = Result<(), BotError>> + '_ {
125        let name = match action {
126            ChatAction::Typing => "typing",
127            ChatAction::UploadPhoto => "upload_photo",
128            ChatAction::RecordVideo => "record_video",
129            ChatAction::UploadVideo => "upload_video",
130            ChatAction::RecordVoice => "record_voice",
131            ChatAction::UploadVoice => "upload_voice",
132            ChatAction::UploadDocument => "upload_document",
133            ChatAction::ChooseSticker => "choose_sticker",
134            ChatAction::FindLocation => "find_location",
135            ChatAction::RecordVideoNote => "record_video_note",
136            ChatAction::UploadVideoNote => "upload_video_note",
137        };
138        self.emit(name, false);
139        async { Ok(()) }
140    }
141
142    fn action_expiry(&self) -> std::time::Duration {
143        // The driver renders action lines as they arrive; there is nothing
144        // to renew.
145        std::time::Duration::ZERO
146    }
147
148    fn clear_action(&self) -> impl ChatActionFutureBounds<Output = Result<(), BotError>> + '_ {
149        self.emit("typing", true);
150        async { Ok(()) }
151    }
152}