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}