Skip to main content

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