serenity/gateway/
mod.rs

1//! The gateway module contains the pieces - primarily the `Shard` - responsible for maintaining a
2//! WebSocket connection with Discord.
3//!
4//! A shard is an interface for the lower-level receiver and sender. It provides what can otherwise
5//! be thought of as "sugar methods". A shard represents a single connection to Discord. You can
6//! make use of a method named "sharding" to have multiple shards, potentially offloading some
7//! server load to another server(s).
8//!
9//! # Sharding
10//!
11//! Sharding is a method to split portions of bots into separate processes. This is an enforced
12//! strategy by Discord once a bot reaches a certain number of guilds (2500). Once this number is
13//! reached, a bot must be sharded in a way that only 2500 guilds maximum may be allocated per
14//! shard.
15//!
16//! The "recommended" number of guilds per shard is _around_ 1000. Sharding can be useful for
17//! splitting processes across separate servers. Often you may want some or all shards to be in the
18//! same process, allowing for a shared State. This is possible through this library.
19//!
20//! See [Discord's documentation][docs] for more information.
21//!
22//! If you do not require sharding - such as for a small bot - then use [`Client::start`].
23//!
24//! There are a few methods of sharding available:
25//! - [`Client::start_autosharded`]: retrieves the number of shards Discord recommends using from
26//!   the API, and then automatically starts that number of shards.
27//! - [`Client::start_shard`]: starts a single shard for use in the instance, handled by the
28//!   instance of the Client. Use this if you only want 1 shard handled by this instance.
29//! - [`Client::start_shards`]: starts all shards in this instance. This is best for when you want a
30//!   completely shared State.
31//! - [`Client::start_shard_range`]: start a range of shards within this instance. This should be
32//!   used when you, for example, want to split 10 shards across 3 instances.
33//!
34//! [`Client`]: crate::Client
35//! [`Client::start`]: crate::Client::start
36//! [`Client::start_autosharded`]: crate::Client::start_autosharded
37//! [`Client::start_shard`]: crate::Client::start_shard
38//! [`Client::start_shard_range`]: crate::Client::start_shard_range
39//! [`Client::start_shards`]: crate::Client::start_shards
40//! [docs]: https://discordapp.com/developers/docs/topics/gateway#sharding
41
42mod bridge;
43mod error;
44mod shard;
45mod ws;
46
47use std::fmt;
48
49#[cfg(feature = "http")]
50use reqwest::IntoUrl;
51use reqwest::Url;
52
53pub use self::bridge::*;
54pub use self::error::Error as GatewayError;
55pub use self::shard::Shard;
56pub use self::ws::WsClient;
57#[cfg(feature = "http")]
58use crate::internal::prelude::*;
59use crate::model::gateway::{Activity, ActivityType};
60use crate::model::id::UserId;
61use crate::model::user::OnlineStatus;
62
63/// Presence data of the current user.
64#[derive(Clone, Debug, Default)]
65pub struct PresenceData {
66    /// The current activity, if present
67    pub activity: Option<ActivityData>,
68    /// The current online status
69    pub status: OnlineStatus,
70}
71
72/// Activity data of the current user.
73#[derive(Clone, Debug, Serialize)]
74pub struct ActivityData {
75    /// The name of the activity
76    pub name: String,
77    /// The type of the activity
78    #[serde(rename = "type")]
79    pub kind: ActivityType,
80    /// The state of the activity, if the type is [`ActivityType::Custom`]
81    pub state: Option<String>,
82    /// The url of the activity, if the type is [`ActivityType::Streaming`]
83    pub url: Option<Url>,
84}
85
86impl ActivityData {
87    /// Creates an activity that appears as `Playing <name>`.
88    #[must_use]
89    pub fn playing(name: impl Into<String>) -> Self {
90        Self {
91            name: name.into(),
92            kind: ActivityType::Playing,
93            state: None,
94            url: None,
95        }
96    }
97
98    /// Creates an activity that appears as `Streaming <name>`.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the URL parsing fails.
103    #[cfg(feature = "http")]
104    pub fn streaming(name: impl Into<String>, url: impl IntoUrl) -> Result<Self> {
105        Ok(Self {
106            name: name.into(),
107            kind: ActivityType::Streaming,
108            state: None,
109            url: Some(url.into_url()?),
110        })
111    }
112
113    /// Creates an activity that appears as `Listening to <name>`.
114    #[must_use]
115    pub fn listening(name: impl Into<String>) -> Self {
116        Self {
117            name: name.into(),
118            kind: ActivityType::Listening,
119            state: None,
120            url: None,
121        }
122    }
123
124    /// Creates an activity that appears as `Watching <name>`.
125    #[must_use]
126    pub fn watching(name: impl Into<String>) -> Self {
127        Self {
128            name: name.into(),
129            kind: ActivityType::Watching,
130            state: None,
131            url: None,
132        }
133    }
134
135    /// Creates an activity that appears as `Competing in <name>`.
136    #[must_use]
137    pub fn competing(name: impl Into<String>) -> Self {
138        Self {
139            name: name.into(),
140            kind: ActivityType::Competing,
141            state: None,
142            url: None,
143        }
144    }
145
146    /// Creates an activity that appears as `<state>`.
147    #[must_use]
148    pub fn custom(state: impl Into<String>) -> Self {
149        Self {
150            // discord seems to require a name for custom activities
151            // even though it's not displayed
152            name: "~".to_string(),
153            kind: ActivityType::Custom,
154            state: Some(state.into()),
155            url: None,
156        }
157    }
158}
159
160impl From<Activity> for ActivityData {
161    fn from(activity: Activity) -> Self {
162        Self {
163            name: activity.name,
164            kind: activity.kind,
165            state: activity.state,
166            url: activity.url,
167        }
168    }
169}
170
171/// Indicates the current connection stage of a [`Shard`].
172///
173/// This can be useful for knowing which shards are currently "down"/"up".
174#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
175#[non_exhaustive]
176pub enum ConnectionStage {
177    /// Indicator that the [`Shard`] is normally connected and is not in, e.g., a resume phase.
178    Connected,
179    /// Indicator that the [`Shard`] is connecting and is in, e.g., a resume phase.
180    Connecting,
181    /// Indicator that the [`Shard`] is fully disconnected and is not in a reconnecting phase.
182    Disconnected,
183    /// Indicator that the [`Shard`] is currently initiating a handshake.
184    Handshake,
185    /// Indicator that the [`Shard`] has sent an IDENTIFY packet and is awaiting a READY packet.
186    Identifying,
187    /// Indicator that the [`Shard`] has sent a RESUME packet and is awaiting a RESUMED packet.
188    Resuming,
189}
190
191impl ConnectionStage {
192    /// Whether the stage is a form of connecting.
193    ///
194    /// This will return `true` on:
195    /// - [`Connecting`][`ConnectionStage::Connecting`]
196    /// - [`Handshake`][`ConnectionStage::Handshake`]
197    /// - [`Identifying`][`ConnectionStage::Identifying`]
198    /// - [`Resuming`][`ConnectionStage::Resuming`]
199    ///
200    /// All other variants will return `false`.
201    ///
202    /// # Examples
203    ///
204    /// Assert that [`ConnectionStage::Identifying`] is a connecting stage:
205    ///
206    /// ```rust
207    /// use serenity::gateway::ConnectionStage;
208    ///
209    /// assert!(ConnectionStage::Identifying.is_connecting());
210    /// ```
211    ///
212    /// Assert that [`ConnectionStage::Connected`] is _not_ a connecting stage:
213    ///
214    /// ```rust
215    /// use serenity::gateway::ConnectionStage;
216    ///
217    /// assert!(!ConnectionStage::Connected.is_connecting());
218    /// ```
219    #[must_use]
220    pub fn is_connecting(self) -> bool {
221        use self::ConnectionStage::{Connecting, Handshake, Identifying, Resuming};
222        matches!(self, Connecting | Handshake | Identifying | Resuming)
223    }
224}
225
226impl fmt::Display for ConnectionStage {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        f.write_str(match *self {
229            Self::Connected => "connected",
230            Self::Connecting => "connecting",
231            Self::Disconnected => "disconnected",
232            Self::Handshake => "handshaking",
233            Self::Identifying => "identifying",
234            Self::Resuming => "resuming",
235        })
236    }
237}
238
239#[derive(Debug)]
240#[non_exhaustive]
241pub enum ShardAction {
242    Heartbeat,
243    Identify,
244    Reconnect(ReconnectType),
245}
246
247/// The type of reconnection that should be performed.
248#[derive(Debug)]
249#[non_exhaustive]
250pub enum ReconnectType {
251    /// Indicator that a new connection should be made by sending an IDENTIFY.
252    Reidentify,
253    /// Indicator that a new connection should be made by sending a RESUME.
254    Resume,
255}
256
257/// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#request-guild-members).
258#[derive(Clone, Debug)]
259pub enum ChunkGuildFilter {
260    /// Returns all members of the guilds specified. Requires GUILD_MEMBERS intent.
261    None,
262    /// A common username prefix filter for the members returned.
263    ///
264    /// Will return a maximum of 100 members.
265    Query(String),
266    /// A set of exact user IDs to query for.
267    ///
268    /// Will return a maximum of 100 members.
269    UserIds(Vec<UserId>),
270}