twilight_gateway/config.rs
1//! User configuration for shards.
2
3use crate::{queue::InMemoryQueue, Session};
4use std::{
5 fmt::{Debug, Formatter, Result as FmtResult},
6 sync::Arc,
7};
8use tokio_websockets::Connector;
9use twilight_model::gateway::{
10 payload::outgoing::{identify::IdentifyProperties, update_presence::UpdatePresencePayload},
11 Intents,
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) 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) 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![MinimalActivity {
282 /// kind: ActivityType::Playing,
283 /// name: "Not accepting commands".into(),
284 /// url: None,
285 /// }
286 /// .into()],
287 /// false,
288 /// None,
289 /// Status::Idle,
290 /// )?)
291 /// .build();
292 ///
293 /// let shard = Shard::with_config(ShardId::ONE, config);
294 /// # Ok(()) }
295 /// ```
296 ///
297 /// [successfully resumed]: twilight_model::gateway::event::Event::Resumed
298 #[allow(clippy::missing_const_for_fn)]
299 pub fn presence(mut self, presence: UpdatePresencePayload) -> Self {
300 self.inner.presence = Some(presence);
301
302 self
303 }
304
305 /// Set the proxy URL for connecting to the gateway.
306 ///
307 /// Resumes are always done to the URL specified in [`resume_gateway_url`].
308 ///
309 /// [`resume_gateway_url`]: twilight_model::gateway::payload::incoming::Ready::resume_gateway_url
310 #[allow(clippy::missing_const_for_fn)]
311 pub fn proxy_url(mut self, proxy_url: String) -> Self {
312 self.inner.proxy_url = Some(proxy_url.into_boxed_str());
313
314 self
315 }
316
317 /// Set the queue to use for queueing shard sessions.
318 ///
319 /// Defaults to [`InMemoryQueue`] with its default settings.
320 ///
321 /// Note that [`InMemoryQueue`] with a `max_concurrency` of `0` effectively
322 /// turns itself into a no-op.
323 pub fn queue<NewQ>(self, queue: NewQ) -> ConfigBuilder<NewQ> {
324 let Config {
325 identify_properties,
326 intents,
327 large_threshold,
328 presence,
329 proxy_url,
330 queue: _,
331 ratelimit_messages,
332 resume_url,
333 session,
334 tls,
335 token,
336 } = self.inner;
337
338 ConfigBuilder {
339 inner: Config {
340 identify_properties,
341 intents,
342 large_threshold,
343 presence,
344 proxy_url,
345 queue,
346 ratelimit_messages,
347 resume_url,
348 session,
349 tls,
350 token,
351 },
352 }
353 }
354
355 /// Set whether or not outgoing messages will be ratelimited.
356 ///
357 /// Useful when running behind a proxy gateway. Running without a
358 /// functional ratelimiter **will** get you ratelimited.
359 ///
360 /// Defaults to being enabled.
361 pub const fn ratelimit_messages(mut self, ratelimit_messages: bool) -> Self {
362 self.inner.ratelimit_messages = ratelimit_messages;
363
364 self
365 }
366
367 /// Set the resume URL to use when the initial shard connection resumes an old session.
368 ///
369 /// This is only used if the initial shard connection resumes instead of identifying and only affects the first session.
370 ///
371 /// This only has an effect if [`ConfigBuilder::session`] is also set.
372 #[allow(clippy::missing_const_for_fn)]
373 pub fn resume_url(mut self, resume_url: String) -> Self {
374 self.inner.resume_url = Some(resume_url.into_boxed_str());
375
376 self
377 }
378
379 /// Set the gateway session to use when connecting to the gateway.
380 ///
381 /// In practice this will result in the shard attempting to send a
382 /// [`Resume`] to the gateway instead of identifying and creating a new
383 /// session. Refer to the documentation for [`Session`] for more
384 /// information.
385 ///
386 /// [`Resume`]: twilight_model::gateway::payload::outgoing::Resume
387 #[allow(clippy::missing_const_for_fn)]
388 pub fn session(mut self, session: Session) -> Self {
389 self.inner.session = Some(session);
390
391 self
392 }
393}
394
395impl<Q> From<Config<Q>> for ConfigBuilder<Q> {
396 fn from(value: Config<Q>) -> Self {
397 Self { inner: value }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::{Config, ConfigBuilder};
404 use static_assertions::assert_impl_all;
405 use std::fmt::Debug;
406 use twilight_model::gateway::Intents;
407
408 assert_impl_all!(Config: Clone, Debug, Send, Sync);
409 assert_impl_all!(ConfigBuilder: Debug, Send, Sync);
410
411 fn builder() -> ConfigBuilder {
412 ConfigBuilder::new("test".to_owned(), Intents::empty())
413 }
414
415 #[tokio::test]
416 async fn large_threshold() {
417 const INPUTS: &[u64] = &[50, 100, 150, 200, 250];
418
419 for input in INPUTS {
420 assert_eq!(
421 builder().large_threshold(*input).build().large_threshold(),
422 *input,
423 );
424 }
425 }
426
427 #[should_panic(expected = "large threshold isn't in the accepted range")]
428 #[tokio::test]
429 async fn large_threshold_minimum() {
430 drop(builder().large_threshold(49));
431 }
432
433 #[should_panic(expected = "large threshold isn't in the accepted range")]
434 #[tokio::test]
435 async fn large_threshold_maximum() {
436 drop(builder().large_threshold(251));
437 }
438
439 #[tokio::test]
440 async fn config_prefixes_bot_to_token() {
441 const WITHOUT: &str = "test";
442 const WITH: &str = "Bot test";
443
444 assert_eq!(
445 ConfigBuilder::new(WITHOUT.to_owned(), Intents::empty())
446 .build()
447 .token
448 .inner
449 .as_ref(),
450 WITH
451 );
452 assert_eq!(
453 ConfigBuilder::new(WITH.to_owned(), Intents::empty())
454 .build()
455 .token
456 .inner
457 .as_ref(),
458 WITH
459 );
460 }
461
462 #[tokio::test]
463 async fn config_debug() {
464 let config = Config::new("Bot foo".to_owned(), Intents::empty());
465
466 assert!(format!("{config:?}").contains("token: <redacted>"));
467 }
468}