titanium_rs/
context.rs

1use std::sync::Arc;
2use titanium_http::HttpClient;
3use titanium_model::{
4    Embed, Interaction, InteractionCallbackData, InteractionCallbackType, InteractionResponse,
5    Message, Snowflake, User,
6};
7use tokio::sync::RwLock;
8
9/// Context for a Slash Command execution.
10use titanium_gateway::Shard;
11
12/// Context for a Slash Command execution.
13#[derive(Clone)]
14pub struct Context {
15    pub http: Arc<HttpClient>,
16    pub cache: Arc<titanium_cache::InMemoryCache>,
17    pub shard: Arc<Shard>,
18    pub interaction: Option<Arc<Interaction<'static>>>,
19    /// Whether the interaction has been deferred or replied to.
20    pub has_responded: Arc<RwLock<bool>>,
21}
22
23impl Context {
24    pub fn new(
25        http: Arc<HttpClient>,
26        cache: Arc<titanium_cache::InMemoryCache>,
27        shard: Arc<Shard>,
28        interaction: Option<Interaction<'static>>,
29    ) -> Self {
30        Self {
31            http,
32            cache,
33            shard,
34            interaction: interaction.map(Arc::new),
35            has_responded: Arc::new(RwLock::new(false)),
36        }
37    }
38
39    /// Defer the interaction.
40    ///
41    /// This sends a "Thinking..." state (Opcode 5) to discord.
42    /// You must call this within 3 seconds if your task takes long.
43    pub async fn defer(
44        &self,
45        ephemeral: bool,
46    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
47        let mut responded = self.has_responded.write().await;
48        if *responded {
49            return Ok(()); // Already accepted
50        }
51
52        let interaction = self
53            .interaction
54            .as_ref()
55            .ok_or("No interaction in context")?;
56
57        let response = InteractionResponse {
58            response_type: InteractionCallbackType::DeferredChannelMessageWithSource,
59            data: Some(InteractionCallbackData {
60                flags: if ephemeral { Some(64) } else { None }, // 64 = Ephemeral
61                content: None,
62                tts: false,
63                embeds: vec![],
64                allowed_mentions: None,
65                components: vec![],
66                attachments: vec![],
67                choices: None,
68                custom_id: None,
69                title: None,
70            }),
71        };
72
73        self.http
74            .create_interaction_response(interaction.id, &interaction.token, &response)
75            .await?;
76
77        *responded = true;
78        Ok(())
79    }
80
81    /// Reply to the command.
82    ///
83    /// This smarter method checks if we have deferred.
84    /// If NOT deferred -> calls `create_interaction_response`
85    /// If DEFERRED -> calls `edit_original_interaction_response`
86    ///
87    /// This solves the "3 second rule" complexity for the user!
88    pub async fn reply(
89        &self,
90        content: impl Into<String>,
91    ) -> Result<Message<'static>, Box<dyn std::error::Error + Send + Sync>> {
92        let content = content.into();
93        let mut responded = self.has_responded.write().await;
94        let interaction = self
95            .interaction
96            .as_ref()
97            .ok_or("No interaction in context")?;
98
99        if *responded {
100            // We already deferred (or replied), so we must EDIT the original
101            // Note: technically if we replied, we should create followup.
102            // But for simple defer -> reply flow, this is Edit.
103            let app_id = interaction.application_id;
104            let token = &interaction.token;
105
106            // Simple struct for body
107            #[derive(serde::Serialize)]
108            struct EditBody {
109                content: String,
110            }
111
112            let message = self
113                .http
114                .edit_original_interaction_response(app_id, token, EditBody { content })
115                .await?;
116
117            Ok(message)
118        } else {
119            // Initial response
120            let response = InteractionResponse {
121                response_type: InteractionCallbackType::ChannelMessageWithSource,
122                data: Some(InteractionCallbackData {
123                    content: Some(content.clone().into()),
124                    tts: false,
125                    embeds: vec![],
126                    allowed_mentions: None,
127                    flags: None,
128                    components: vec![],
129                    attachments: vec![],
130                    choices: None,
131                    custom_id: None,
132                    title: None,
133                }),
134            };
135
136            self.http
137                .create_interaction_response(interaction.id, &interaction.token, &response)
138                .await?;
139
140            *responded = true;
141
142            // Fetch the interaction response to return a full Message object.
143            // This is required because create_interaction_response returns 204 No Content.
144            let msg = self
145                .http
146                .get_original_interaction_response(interaction.application_id, &interaction.token)
147                .await?;
148            Ok(msg)
149        }
150    }
151
152    /// Reply with an embed (discord.js-style).
153    pub async fn reply_embed(
154        &self,
155        embed: impl Into<Embed<'static>>,
156    ) -> Result<Message<'static>, Box<dyn std::error::Error + Send + Sync>> {
157        let embed = embed.into();
158        let mut responded = self.has_responded.write().await;
159        let interaction = self
160            .interaction
161            .as_ref()
162            .ok_or("No interaction in context")?;
163
164        if *responded {
165            // Edit original response
166            #[derive(serde::Serialize)]
167            struct EditBody {
168                embeds: Vec<Embed<'static>>,
169            }
170
171            let message = self
172                .http
173                .edit_original_interaction_response(
174                    interaction.application_id,
175                    &interaction.token,
176                    EditBody {
177                        embeds: vec![embed],
178                    },
179                )
180                .await?;
181            Ok(message)
182        } else {
183            let response = InteractionResponse {
184                response_type: InteractionCallbackType::ChannelMessageWithSource,
185                data: Some(InteractionCallbackData {
186                    embeds: vec![embed],
187                    content: None,
188                    tts: false,
189                    allowed_mentions: None,
190                    flags: None,
191                    components: vec![],
192                    attachments: vec![],
193                    choices: None,
194                    custom_id: None,
195                    title: None,
196                }),
197            };
198
199            self.http
200                .create_interaction_response(interaction.id, &interaction.token, &response)
201                .await?;
202
203            *responded = true;
204
205            // Fetch the interaction response
206            let msg = self
207                .http
208                .get_original_interaction_response(interaction.application_id, &interaction.token)
209                .await?;
210            Ok(msg)
211        }
212    }
213
214    /// Reply with an ephemeral message (only visible to user).
215    pub async fn reply_ephemeral(
216        &self,
217        content: impl Into<String>,
218    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
219        let content = content.into();
220        let mut responded = self.has_responded.write().await;
221        let interaction = self
222            .interaction
223            .as_ref()
224            .ok_or("No interaction in context")?;
225
226        if *responded {
227            // Can't make an existing response ephemeral if it wasn't already.
228            return Err("Cannot reply ephemeral after already responding".into());
229        }
230
231        let response = InteractionResponse {
232            response_type: InteractionCallbackType::ChannelMessageWithSource,
233            data: Some(InteractionCallbackData {
234                content: Some(content.into()),
235                flags: Some(64), // EPHEMERAL
236                tts: false,
237                embeds: vec![],
238                allowed_mentions: None,
239                components: vec![],
240                attachments: vec![],
241                choices: None,
242                custom_id: None,
243                title: None,
244            }),
245        };
246
247        self.http
248            .create_interaction_response(interaction.id, &interaction.token, &response)
249            .await?;
250
251        *responded = true;
252        Ok(())
253    }
254
255    /// Edit the original interaction response.
256    pub async fn edit_reply(
257        &self,
258        content: impl Into<String>,
259    ) -> Result<Message<'static>, Box<dyn std::error::Error + Send + Sync>> {
260        let interaction = self
261            .interaction
262            .as_ref()
263            .ok_or("No interaction in context")?;
264        #[derive(serde::Serialize)]
265        struct EditBody {
266            content: String,
267        }
268
269        let message = self
270            .http
271            .edit_original_interaction_response(
272                interaction.application_id,
273                &interaction.token,
274                EditBody {
275                    content: content.into(),
276                },
277            )
278            .await?;
279
280        Ok(message)
281    }
282
283    /// Send a follow-up message.
284    pub async fn followup(
285        &self,
286        content: impl Into<String>,
287    ) -> Result<Message<'static>, Box<dyn std::error::Error + Send + Sync>> {
288        use titanium_model::builder::ExecuteWebhook;
289        let interaction = self
290            .interaction
291            .as_ref()
292            .ok_or("No interaction in context")?;
293
294        let params = ExecuteWebhook {
295            content: Some(content.into()),
296            ..Default::default()
297        };
298
299        // Interaction followups use the webhook endpoint with application_id as webhook_id
300        let msg = self
301            .http
302            .execute_webhook(interaction.application_id, &interaction.token, &params)
303            .await?;
304
305        // execute_webhook returns Option<Message>, but with wait=true it should return Some
306        Ok(msg.ok_or("Failed to get followup message")?)
307    }
308
309    /// Get the user who triggered the interaction.
310    #[inline]
311    pub fn user(&self) -> Option<&User<'static>> {
312        self.interaction.as_ref().and_then(|i| {
313            i.member
314                .as_ref()
315                .and_then(|m| m.user.as_ref())
316                .or(i.user.as_ref())
317        })
318    }
319
320    /// Get the guild ID if in a guild.
321    #[inline]
322    #[must_use]
323    pub fn guild_id(&self) -> Option<Snowflake> {
324        self.interaction.as_ref().and_then(|i| i.guild_id)
325    }
326
327    /// Get the channel ID.
328    #[inline]
329    #[must_use]
330    pub fn channel_id(&self) -> Option<Snowflake> {
331        self.interaction.as_ref().and_then(|i| i.channel_id)
332    }
333}