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