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}