titanium_rs/
client.rs

1//! Discord bot client implementation.
2//!
3//! This module provides the main [`Client`] struct and [`ClientBuilder`] for
4//! creating and running a Discord bot.
5//!
6//! # Architecture
7//!
8//! The client manages:
9//! - A **Gateway Cluster** that handles WebSocket connections to Discord
10//! - An **HTTP Client** for REST API requests
11//! - An **In-Memory Cache** for frequently accessed entities
12//! - **Event Handlers** for responding to Discord events
13//!
14//! # Example
15//!
16//! ```no_run
17//! use titanium_rs::prelude::*;
18//! use async_trait::async_trait;
19//!
20//! struct MyHandler;
21//!
22//! #[async_trait]
23//! impl EventHandler for MyHandler {
24//!     async fn message_create(&self, ctx: Context, msg: Message<'_>) {
25//!         if &*msg.content == "!ping" {
26//!             let _ = ctx.send(msg.channel_id, "Pong!").await;
27//!         }
28//!     }
29//! }
30//!
31//! #[tokio::main]
32//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
33//!     let token = std::env::var("DISCORD_TOKEN")?;
34//!     
35//!     Client::builder(token)
36//!         .intents(Intents::GUILD_MESSAGES | Intents::MESSAGE_CONTENT)
37//!         .event_handler(MyHandler)
38//!         .build()
39//!         .await?
40//!         .start()
41//!         .await?;
42//!     
43//!     Ok(())
44//! }
45//! ```
46
47use crate::error::TitaniumError;
48use crate::framework::Framework;
49use std::sync::Arc;
50use titanium_cache::{Cache, InMemoryCache};
51use titanium_gateway::{Cluster, ClusterConfig, Event};
52use titanium_http::HttpClient;
53use titanium_model::Intents;
54
55/// The main Titanium Discord Client.
56///
57/// This struct is the central hub of your bot. It manages the connection to Discord,
58/// caches entities, and dispatches events to your handlers.
59///
60/// # Creating a Client
61///
62/// Use [`Client::builder`] to create a new client:
63///
64/// ```no_run
65/// # use titanium_rs::Client;
66/// # use titanium_model::Intents;
67/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
68/// let client = Client::builder("your-token")
69///     .intents(Intents::GUILD_MESSAGES)
70///     .build()
71///     .await?;
72/// # Ok(())
73/// # }
74/// ```
75///
76/// # Thread Safety
77///
78/// `Client` is `Clone` and all internal state is wrapped in `Arc`, making it
79/// safe to share across tasks.
80#[derive(Clone)]
81pub struct Client {
82    /// The gateway cluster managing WebSocket connections.
83    pub cluster: Arc<Cluster>,
84    /// HTTP client for REST API requests.
85    pub http: Arc<HttpClient>,
86    /// In-memory cache for guilds, channels, users, etc.
87    pub cache: Arc<InMemoryCache>,
88    /// Optional command framework.
89    pub framework: Option<Arc<Framework>>,
90    /// Event handler for processing Discord events.
91    pub event_handler: Option<Arc<dyn EventHandler>>,
92    /// Channel receiving events from all shards.
93    pub event_rx: flume::Receiver<(u16, Event<'static>)>,
94    /// Bot token (stored for potential reconnection).
95    #[allow(dead_code)]
96    token: String,
97}
98
99impl Client {
100    /// Create a new Client Builder.
101    #[inline]
102    pub fn builder(token: impl Into<String>) -> ClientBuilder {
103        ClientBuilder::new(token)
104    }
105
106    /// Start the client and run the event loop.
107    pub async fn start(&self) -> Result<(), TitaniumError> {
108        // Start the cluster (spawns shards)
109        self.cluster.start()?;
110
111        // Event Processing Loop
112        while let Ok((shard_id, event)) = self.event_rx.recv_async().await {
113            // Automatic Cache Updates
114            match &event {
115                Event::Ready(ready) => {
116                    self.cache.insert_user(Arc::new(ready.user.clone()));
117                    // We don't cache guilds here as they come in GuildCreate
118                }
119                Event::GuildCreate(guild) => {
120                    self.cache.insert_guild(Arc::clone(guild));
121                }
122                Event::GuildUpdate(guild) => {
123                    self.cache.insert_guild(Arc::clone(guild));
124                }
125                Event::GuildDelete(unavailable) => {
126                    if !unavailable.unavailable {
127                        self.cache.remove_guild(unavailable.id);
128                    }
129                }
130                Event::ChannelCreate(channel) => {
131                    self.cache.insert_channel(Arc::clone(channel));
132                }
133                Event::ChannelUpdate(channel) => {
134                    self.cache.insert_channel(Arc::clone(channel));
135                }
136                Event::ChannelDelete(channel) => {
137                    self.cache.remove_channel(channel.id);
138                }
139                Event::GuildMemberAdd(member) => {
140                    // Convert GuildMemberAddEvent to GuildMember for cache
141                    let guild_member = titanium_model::GuildMember {
142                        user: member.user.clone(),
143                        nick: member.nick.clone().map(Into::into),
144                        avatar: member.avatar.clone().map(Into::into),
145                        roles: member.roles.clone().into(),
146                        joined_at: member.joined_at.clone().into(),
147                        premium_since: None, // Not present in Add event in current model
148                        deaf: member.deaf,
149                        mute: member.mute,
150                        flags: member.flags,
151                        pending: member.pending,
152                        permissions: None,
153                        communication_disabled_until: None,
154                    };
155                    self.cache
156                        .insert_member(member.guild_id, Arc::new(guild_member));
157                }
158                Event::GuildMemberUpdate(member) => {
159                    // Update member in cache if exists
160                    if let Some(cached_member) = self.cache.member(member.guild_id, member.user.id)
161                    {
162                        // cached_member is Arc<GuildMember>.
163                        let mut new_member = (*cached_member).clone();
164
165                        new_member.roles = member.roles.clone().into();
166                        new_member.nick = member.nick.clone().map(Into::into);
167                        new_member.avatar = member.avatar.clone().map(Into::into);
168                        if let Some(joined) = &member.joined_at {
169                            new_member.joined_at = joined.clone().into();
170                        }
171                        new_member.deaf = member.deaf.unwrap_or(new_member.deaf);
172                        new_member.mute = member.mute.unwrap_or(new_member.mute);
173                        new_member.pending = member.pending;
174                        new_member.communication_disabled_until =
175                            member.communication_disabled_until.clone().map(Into::into);
176
177                        self.cache
178                            .insert_member(member.guild_id, Arc::new(new_member));
179                    }
180                }
181                Event::GuildMemberRemove(event) => {
182                    self.cache.remove_member(event.guild_id, event.user.id);
183                }
184                Event::GuildRoleCreate(event) => {
185                    self.cache
186                        .insert_role(event.role.id, Arc::new(event.role.clone()));
187                }
188                Event::GuildRoleUpdate(event) => {
189                    self.cache
190                        .insert_role(event.role.id, Arc::new(event.role.clone()));
191                }
192                Event::GuildRoleDelete(event) => {
193                    self.cache.remove_role(event.role_id);
194                }
195                Event::UserUpdate(user) => {
196                    self.cache.insert_user(user.clone());
197                }
198                _ => {}
199            }
200
201            // Dispatch to Event Handler
202            if let Some(handler) = &self.event_handler {
203                let http = self.http.clone();
204                let cache = self.cache.clone();
205                // Retrieve specific shard for context - skip event if shard not found
206                let Some(shard) = self.cluster.shard(shard_id) else {
207                    tracing::warn!(shard_id, "Shard not found for event, skipping dispatch");
208                    continue;
209                };
210
211                // Prepare variables for the spawned task
212                let handler = handler.clone();
213                let event = event.clone(); // Cheap clone (enum of Arcs)
214
215                // Spawn a new task for the handler to ensure the event loop remains unblocked
216                tokio::spawn(async move {
217                    // Helper to create Context
218                    let make_ctx = || {
219                        super::context::Context::new(
220                            http.clone(),
221                            cache.clone(),
222                            shard.clone(),
223                            None,
224                        )
225                    };
226
227                    match event {
228                        Event::Ready(ready) => {
229                            handler.ready(make_ctx(), (*ready).clone()).await;
230                        }
231                        Event::MessageCreate(msg) => {
232                            handler.message_create(make_ctx(), (*msg).clone()).await;
233                        }
234                        Event::InteractionCreate(interaction) => {
235                            // Interaction is Arc<Interaction>
236                            let interaction_val = (*interaction).clone();
237                            let ctx = super::context::Context::new(
238                                http.clone(),
239                                cache.clone(),
240                                shard.clone(),
241                                Some(interaction_val.clone()),
242                            );
243                            handler.interaction_create(ctx, interaction_val).await;
244                        }
245                        Event::MessageReactionAdd(ev) => {
246                            handler.reaction_add(make_ctx(), (*ev).clone()).await;
247                        }
248                        Event::ThreadCreate(thread) => {
249                            handler.thread_create(make_ctx(), (*thread).clone()).await;
250                        }
251                        Event::GuildRoleCreate(ev) => {
252                            handler.role_create(make_ctx(), (*ev).clone()).await;
253                        }
254                        _ => {}
255                    }
256                });
257            }
258        }
259
260        Ok(())
261    }
262}
263
264use crate::prelude::*;
265use async_trait::async_trait;
266
267#[async_trait]
268pub trait EventHandler: Send + Sync {
269    async fn ready(&self, _ctx: Context, _ready: ReadyEventData<'_>) {}
270    async fn message_create(&self, _ctx: Context, _msg: Message<'_>) {}
271    async fn interaction_create(&self, _ctx: Context, _interaction: Interaction<'_>) {}
272    async fn reaction_add(
273        &self,
274        _ctx: Context,
275        _add: titanium_model::MessageReactionAddEvent<'async_trait>,
276    ) {
277    }
278    async fn thread_create(&self, _ctx: Context, _thread: Channel<'async_trait>) {}
279    async fn role_create(&self, _ctx: Context, _role: GuildRoleEvent<'async_trait>) {}
280}
281
282pub struct ClientBuilder {
283    token: String,
284    intents: Intents,
285    framework: Option<Framework>,
286    event_handler: Option<Arc<dyn EventHandler>>,
287}
288
289impl ClientBuilder {
290    #[inline]
291    pub fn new(token: impl Into<String>) -> Self {
292        Self {
293            token: token.into(),
294            intents: Intents::default(),
295            framework: None,
296            event_handler: None,
297        }
298    }
299
300    /// Set the initial intents.
301    #[must_use]
302    pub const fn intents(mut self, intents: Intents) -> Self {
303        self.intents = intents;
304        self
305    }
306
307    /// Set the command framework.
308    #[must_use]
309    pub fn framework(mut self, framework: Framework) -> Self {
310        self.framework = Some(framework);
311        self
312    }
313
314    pub fn event_handler<H: EventHandler + 'static>(mut self, handler: H) -> Self {
315        self.event_handler = Some(Arc::new(handler));
316        self
317    }
318
319    pub async fn build(self) -> Result<Client, TitaniumError> {
320        let http = Arc::new(HttpClient::new(self.token.clone())?);
321        let cache = Arc::new(titanium_cache::InMemoryCache::new());
322
323        // Use auto-scaling cluster configuration
324        // This fetches the recommended shard count from Discord
325        let config = ClusterConfig::autoscaled(self.token.clone(), self.intents).await?;
326
327        // Initialize Cluster
328        let (cluster, rx) = Cluster::new(config);
329        let cluster = Arc::new(cluster);
330
331        Ok(Client {
332            cluster,
333            http,
334            cache,
335            framework: self.framework.map(Arc::new),
336            event_handler: self.event_handler,
337            event_rx: rx,
338            token: self.token,
339        })
340    }
341}