twilight_gateway/
config.rs

1//! User configuration for shards.
2
3use crate::{Session, queue::InMemoryQueue};
4use std::{
5    fmt::{Debug, Formatter, Result as FmtResult},
6    sync::Arc,
7};
8use tokio_websockets::Connector;
9use twilight_model::gateway::{
10    Intents,
11    payload::outgoing::{identify::IdentifyProperties, update_presence::UpdatePresencePayload},
12};
13
14/// Wrapper for an authorization token with a debug implementation that redacts
15/// the string.
16#[derive(Clone, Default)]
17struct Token {
18    /// Authorization token that is redacted in the Debug implementation.
19    inner: Box<str>,
20}
21
22impl Token {
23    /// Create a new authorization wrapper.
24    const fn new(token: Box<str>) -> Self {
25        Self { inner: token }
26    }
27}
28
29impl Debug for Token {
30    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
31        f.write_str("<redacted>")
32    }
33}
34
35/// Configuration used by the shard to identify with the gateway and operate.
36///
37/// May be reused by cloning, also reusing the hidden TLS context---reducing
38/// memory usage. The TLS context may still be reused with an otherwise
39/// different config by turning it into to a [`ConfigBuilder`] through the
40/// [`From<Config>`] implementation and then rebuilding it into a rew config.
41#[derive(Clone, Debug)]
42pub struct Config<Q = InMemoryQueue> {
43    /// Identification properties the shard will use.
44    identify_properties: Option<IdentifyProperties>,
45    /// Intents that the shard requests when identifying with the gateway.
46    intents: Intents,
47    /// When the gateway will stop sending a guild's member list in
48    /// Guild Create events.
49    large_threshold: u64,
50    /// Presence to set when identifying with the gateway.
51    presence: Option<UpdatePresencePayload>,
52    /// Gateway proxy URL.
53    proxy_url: Option<Box<str>>,
54    /// Queue in use by the shard.
55    queue: Q,
56    /// Whether [outgoing message] ratelimiting is enabled.
57    ///
58    /// [outgoing message]: crate::Shard::send
59    ratelimit_messages: bool,
60    /// URL to connect to if the shard resumes on initialization.
61    resume_url: Option<Box<str>>,
62    /// Session information to resume a shard on initialization.
63    session: Option<Session>,
64    /// TLS connector for Websocket connections.
65    // We need this to be public so [`stream`] can reuse TLS on multiple shards
66    // if unconfigured.
67    pub(crate) tls: Arc<Connector>,
68    /// Token used to authenticate when identifying with the gateway.
69    ///
70    /// The token is prefixed with "Bot ", which is required by Discord for
71    /// authentication.
72    token: Token,
73}
74
75impl Config {
76    /// Create a new default shard configuration.
77    ///
78    /// # Panics
79    ///
80    /// Panics if loading TLS certificates fails.
81    pub fn new(token: String, intents: Intents) -> Self {
82        ConfigBuilder::new(token, intents).build()
83    }
84}
85
86impl<Q> Config<Q> {
87    /// Immutable reference to the identification properties the shard will use.
88    pub const fn identify_properties(&self) -> Option<&IdentifyProperties> {
89        self.identify_properties.as_ref()
90    }
91
92    /// Intents that the shard requests when identifying with the gateway.
93    pub const fn intents(&self) -> Intents {
94        self.intents
95    }
96
97    /// Maximum threshold at which point the gateway will stop sending a guild's
98    /// member list in Guild Create events.
99    pub const fn large_threshold(&self) -> u64 {
100        self.large_threshold
101    }
102
103    /// Immutable reference to the presence to set when identifying
104    /// with the gateway.
105    ///
106    /// This will be the bot's presence. For example, setting the online status
107    /// to Do Not Disturb will show the status in the bot's presence.
108    pub const fn presence(&self) -> Option<&UpdatePresencePayload> {
109        self.presence.as_ref()
110    }
111
112    /// Immutable reference to the gateway proxy URL.
113    pub fn proxy_url(&self) -> Option<&str> {
114        self.proxy_url.as_deref()
115    }
116
117    /// Immutable reference to the queue in use by the shard.
118    pub const fn queue(&self) -> &Q {
119        &self.queue
120    }
121
122    /// Whether [outgoing message] ratelimiting is enabled.
123    ///
124    /// [outgoing message]: crate::Shard::send
125    pub const fn ratelimit_messages(&self) -> bool {
126        self.ratelimit_messages
127    }
128
129    /// Immutable reference to the token used to authenticate when identifying
130    /// with the gateway.
131    pub const fn token(&self) -> &str {
132        &self.token.inner
133    }
134
135    /// Url to connect to if the shard resumes on initialization.
136    pub(crate) const fn take_resume_url(&mut self) -> Option<Box<str>> {
137        self.resume_url.take()
138    }
139
140    /// Session information to resume a shard on initialization.
141    pub(crate) const fn take_session(&mut self) -> Option<Session> {
142        self.session.take()
143    }
144}
145
146/// Builder to customize the operation of a shard.
147#[derive(Debug)]
148#[must_use = "builder must be completed to be used"]
149pub struct ConfigBuilder<Q = InMemoryQueue> {
150    /// Inner configuration being modified.
151    inner: Config<Q>,
152}
153
154impl ConfigBuilder {
155    /// Create a new builder to configure and construct a shard.
156    ///
157    /// Refer to each method to learn their default values.
158    ///
159    /// # Panics
160    ///
161    /// Panics if loading TLS certificates fails.
162    pub fn new(mut token: String, intents: Intents) -> Self {
163        if !token.starts_with("Bot ") {
164            token.insert_str(0, "Bot ");
165        }
166
167        Self {
168            inner: Config {
169                identify_properties: None,
170                intents,
171                large_threshold: 50,
172                presence: None,
173                proxy_url: None,
174                queue: InMemoryQueue::default(),
175                ratelimit_messages: true,
176                resume_url: None,
177                session: None,
178                tls: Arc::new(Connector::new().unwrap()),
179                token: Token::new(token.into_boxed_str()),
180            },
181        }
182    }
183}
184
185impl<Q> ConfigBuilder<Q> {
186    /// Consume the builder, constructing a shard.
187    #[allow(clippy::missing_const_for_fn)]
188    pub fn build(self) -> Config<Q> {
189        self.inner
190    }
191
192    /// Set the properties to identify with.
193    ///
194    /// This may be used if you want to set a different operating system, for
195    /// example.
196    ///
197    /// # Examples
198    ///
199    /// Set the identify properties for a shard:
200    ///
201    /// ```no_run
202    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
203    /// use std::env::{self, consts::OS};
204    /// use twilight_gateway::{ConfigBuilder, Intents, Shard};
205    /// use twilight_model::gateway::payload::outgoing::identify::IdentifyProperties;
206    ///
207    /// let token = env::var("DISCORD_TOKEN")?;
208    /// let properties = IdentifyProperties::new("twilight.rs", "twilight.rs", OS);
209    ///
210    /// let config = ConfigBuilder::new(token, Intents::empty())
211    ///     .identify_properties(properties)
212    ///     .build();
213    /// # Ok(()) }
214    /// ```
215    #[allow(clippy::missing_const_for_fn)]
216    pub fn identify_properties(mut self, identify_properties: IdentifyProperties) -> Self {
217        self.inner.identify_properties = Some(identify_properties);
218
219        self
220    }
221
222    /// Set the maximum number of members in a guild to load the member list.
223    ///
224    /// Default value is `50`. The minimum value is `50` and the maximum is
225    /// `250`.
226    ///
227    /// # Examples
228    ///
229    /// If you pass `200`, then if there are 250 members in a guild the member
230    /// list won't be sent. If there are 150 members, then the list *will* be
231    /// sent.
232    ///
233    /// # Panics
234    ///
235    /// Panics if the provided value is below 50 or above 250.
236    #[track_caller]
237    pub const fn large_threshold(mut self, large_threshold: u64) -> Self {
238        /// Maximum acceptable large threshold.
239        const MAXIMUM: u64 = 250;
240
241        /// Minimum acceptable large threshold.
242        const MINIMUM: u64 = 50;
243
244        assert!(
245            large_threshold >= MINIMUM && large_threshold <= MAXIMUM,
246            "large threshold isn't in the accepted range"
247        );
248
249        self.inner.large_threshold = large_threshold;
250
251        self
252    }
253
254    /// Set the presence to use automatically when starting a new session.
255    ///
256    /// The active presence of a session is maintained across re-connections
257    /// when a session can be [successfully resumed], and when a new session has
258    /// to be made shards will send the configured presence. Manually updating
259    /// the presence after a disconnection isn't necessary.
260    ///
261    /// Default is no presence, which defaults to strictly being "online"
262    /// with no special qualities.
263    ///
264    /// # Examples
265    ///
266    /// Set the bot user's presence to idle with the status "Not accepting
267    /// commands":
268    ///
269    /// ```no_run
270    /// use std::env;
271    /// use twilight_gateway::{ConfigBuilder, Intents, Shard, ShardId};
272    /// use twilight_model::gateway::{
273    ///     payload::outgoing::update_presence::UpdatePresencePayload,
274    ///     presence::{ActivityType, MinimalActivity, Status},
275    /// };
276    ///
277    /// # #[tokio::main]
278    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
279    /// let config = ConfigBuilder::new(env::var("DISCORD_TOKEN")?, Intents::empty())
280    ///     .presence(UpdatePresencePayload::new(
281    ///         vec![
282    ///             MinimalActivity {
283    ///                 kind: ActivityType::Playing,
284    ///                 name: "Not accepting commands".into(),
285    ///                 url: None,
286    ///             }
287    ///             .into(),
288    ///         ],
289    ///         false,
290    ///         None,
291    ///         Status::Idle,
292    ///     )?)
293    ///     .build();
294    ///
295    /// let shard = Shard::with_config(ShardId::ONE, config);
296    /// # Ok(()) }
297    /// ```
298    ///
299    /// [successfully resumed]: twilight_model::gateway::event::Event::Resumed
300    #[allow(clippy::missing_const_for_fn)]
301    pub fn presence(mut self, presence: UpdatePresencePayload) -> Self {
302        self.inner.presence = Some(presence);
303
304        self
305    }
306
307    /// Set the proxy URL for connecting to the gateway.
308    ///
309    /// Resumes are always done to the URL specified in [`resume_gateway_url`].
310    ///
311    /// [`resume_gateway_url`]: twilight_model::gateway::payload::incoming::Ready::resume_gateway_url
312    #[allow(clippy::missing_const_for_fn)]
313    pub fn proxy_url(mut self, proxy_url: String) -> Self {
314        self.inner.proxy_url = Some(proxy_url.into_boxed_str());
315
316        self
317    }
318
319    /// Set the queue to use for queueing shard sessions.
320    ///
321    /// Defaults to [`InMemoryQueue`] with its default settings.
322    ///
323    /// Note that [`InMemoryQueue`] with a `max_concurrency` of `0` effectively
324    /// turns itself into a no-op.
325    pub fn queue<NewQ>(self, queue: NewQ) -> ConfigBuilder<NewQ> {
326        let Config {
327            identify_properties,
328            intents,
329            large_threshold,
330            presence,
331            proxy_url,
332            queue: _,
333            ratelimit_messages,
334            resume_url,
335            session,
336            tls,
337            token,
338        } = self.inner;
339
340        ConfigBuilder {
341            inner: Config {
342                identify_properties,
343                intents,
344                large_threshold,
345                presence,
346                proxy_url,
347                queue,
348                ratelimit_messages,
349                resume_url,
350                session,
351                tls,
352                token,
353            },
354        }
355    }
356
357    /// Set whether or not outgoing messages will be ratelimited.
358    ///
359    /// Useful when running behind a proxy gateway. Running without a
360    /// functional ratelimiter **will** get you ratelimited.
361    ///
362    /// Defaults to being enabled.
363    pub const fn ratelimit_messages(mut self, ratelimit_messages: bool) -> Self {
364        self.inner.ratelimit_messages = ratelimit_messages;
365
366        self
367    }
368
369    /// Set the resume URL to use when the initial shard connection resumes an old session.
370    ///
371    /// This is only used if the initial shard connection resumes instead of identifying and only affects the first session.
372    ///
373    /// This only has an effect if [`ConfigBuilder::session`] is also set.
374    #[allow(clippy::missing_const_for_fn)]
375    pub fn resume_url(mut self, resume_url: String) -> Self {
376        self.inner.resume_url = Some(resume_url.into_boxed_str());
377
378        self
379    }
380
381    /// Set the gateway session to use when connecting to the gateway.
382    ///
383    /// In practice this will result in the shard attempting to send a
384    /// [`Resume`] to the gateway instead of identifying and creating a new
385    /// session. Refer to the documentation for [`Session`] for more
386    /// information.
387    ///
388    /// [`Resume`]: twilight_model::gateway::payload::outgoing::Resume
389    #[allow(clippy::missing_const_for_fn)]
390    pub fn session(mut self, session: Session) -> Self {
391        self.inner.session = Some(session);
392
393        self
394    }
395}
396
397impl<Q> From<Config<Q>> for ConfigBuilder<Q> {
398    fn from(value: Config<Q>) -> Self {
399        Self { inner: value }
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::{Config, ConfigBuilder};
406    use static_assertions::assert_impl_all;
407    use std::fmt::Debug;
408    use twilight_model::gateway::Intents;
409
410    assert_impl_all!(Config: Clone, Debug, Send, Sync);
411    assert_impl_all!(ConfigBuilder: Debug, Send, Sync);
412
413    fn builder() -> ConfigBuilder {
414        ConfigBuilder::new("test".to_owned(), Intents::empty())
415    }
416
417    #[tokio::test]
418    async fn large_threshold() {
419        const INPUTS: &[u64] = &[50, 100, 150, 200, 250];
420
421        for input in INPUTS {
422            assert_eq!(
423                builder().large_threshold(*input).build().large_threshold(),
424                *input,
425            );
426        }
427    }
428
429    #[should_panic(expected = "large threshold isn't in the accepted range")]
430    #[tokio::test]
431    async fn large_threshold_minimum() {
432        drop(builder().large_threshold(49));
433    }
434
435    #[should_panic(expected = "large threshold isn't in the accepted range")]
436    #[tokio::test]
437    async fn large_threshold_maximum() {
438        drop(builder().large_threshold(251));
439    }
440
441    #[tokio::test]
442    async fn config_prefixes_bot_to_token() {
443        const WITHOUT: &str = "test";
444        const WITH: &str = "Bot test";
445
446        assert_eq!(
447            ConfigBuilder::new(WITHOUT.to_owned(), Intents::empty())
448                .build()
449                .token
450                .inner
451                .as_ref(),
452            WITH
453        );
454        assert_eq!(
455            ConfigBuilder::new(WITH.to_owned(), Intents::empty())
456                .build()
457                .token
458                .inner
459                .as_ref(),
460            WITH
461        );
462    }
463
464    #[tokio::test]
465    async fn config_debug() {
466        let config = Config::new("Bot foo".to_owned(), Intents::empty());
467
468        assert!(format!("{config:?}").contains("token: <redacted>"));
469    }
470}