twilight_model/gateway/
close_code.rs

1use serde_repr::{Deserialize_repr, Serialize_repr};
2use std::{
3    error::Error,
4    fmt::{Display, Formatter, Result as FmtResult},
5};
6
7/// Gateway close event codes.
8///
9/// See [Discord Docs/Gateway Close Event Codes] for more information.
10///
11/// [Discord Docs/Gateway Close Event Codes]: https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes
12#[derive(Clone, Copy, Debug, Deserialize_repr, Eq, Hash, PartialEq, Serialize_repr)]
13#[non_exhaustive]
14#[repr(u16)]
15pub enum CloseCode {
16    /// An unknown error occurred.
17    UnknownError = 4000,
18    /// An invalid opcode or payload for an opcode was sent.
19    UnknownOpcode = 4001,
20    /// An invalid payload was sent.
21    DecodeError = 4002,
22    /// A payload was sent prior to identifying.
23    NotAuthenticated = 4003,
24    /// An invalid token was sent when identifying.
25    AuthenticationFailed = 4004,
26    /// Multiple identify payloads were sent.
27    AlreadyAuthenticated = 4005,
28    /// An invalid sequence was sent for resuming.
29    InvalidSequence = 4007,
30    /// Too many payloads were sent in a certain amount of time.
31    RateLimited = 4008,
32    /// The session timed out.
33    SessionTimedOut = 4009,
34    /// An invalid shard was sent when identifying.
35    InvalidShard = 4010,
36    /// Sharding is required because there are too many guilds.
37    ShardingRequired = 4011,
38    /// An invalid version for the gateway was sent.
39    InvalidApiVersion = 4012,
40    /// An invalid intent was sent.
41    InvalidIntents = 4013,
42    /// A disallowed intent was sent, may need allowlisting.
43    DisallowedIntents = 4014,
44}
45
46impl CloseCode {
47    /// Whether the close code is one that allows reconnection of a shard.
48    ///
49    /// Some close codes are considered *fatal*, meaning that using the same
50    /// gateway shard configuration would error. For example, the
51    /// [`AuthenticationFailed`] close code occurs when the provided Discord bot
52    /// token is invalid, and so attempting to reconnect with the same token
53    /// would fail. On the other hand, a close code such as [`RateLimited`]
54    /// occurs when too many gateway commands are sent in a short time, and so
55    /// creating a new connection would succeed.
56    ///
57    /// Refer to [Discord Docs/Gateway Close Event Codes][1] for more
58    /// information.
59    ///
60    /// # Reconnectable close codes
61    ///
62    /// - [`UnknownError`]
63    /// - [`DecodeError`]
64    /// - [`NotAuthenticated`]
65    /// - [`AlreadyAuthenticated`]
66    /// - [`InvalidSequence`]
67    /// - [`RateLimited`]
68    /// - [`SessionTimedOut`]
69    ///
70    /// # Fatal close codes
71    ///
72    /// - [`AuthenticationFailed`]
73    /// - [`InvalidShard`]
74    /// - [`ShardingRequired`]
75    /// - [`InvalidApiVersion`]
76    /// - [`InvalidIntents`]
77    /// - [`DisallowedIntents`]
78    ///
79    /// [`AlreadyAuthenticated`]: Self::AlreadyAuthenticated
80    /// [`AuthenticationFailed`]: Self::AuthenticationFailed
81    /// [`DecodeError`]: Self::DecodeError
82    /// [`DisallowedIntents`]: Self::DisallowedIntents
83    /// [`InvalidApiVersion`]: Self::InvalidApiVersion
84    /// [`InvalidIntents`]: Self::InvalidIntents
85    /// [`InvalidSequence`]: Self::InvalidSequence
86    /// [`InvalidShard`]: Self::InvalidShard
87    /// [`NotAuthenticated`]: Self::NotAuthenticated
88    /// [`RateLimited`]: Self::RateLimited
89    /// [`SessionTimedOut`]: Self::SessionTimedOut
90    /// [`ShardingRequired`]: Self::ShardingRequired
91    /// [`UnknownError`]: Self::UnknownError
92    /// [1]: https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes
93    pub const fn can_reconnect(self) -> bool {
94        matches!(
95            self,
96            Self::UnknownError
97                | Self::UnknownOpcode
98                | Self::DecodeError
99                | Self::NotAuthenticated
100                | Self::AlreadyAuthenticated
101                | Self::InvalidSequence
102                | Self::RateLimited
103                | Self::SessionTimedOut
104        )
105    }
106}
107
108impl Display for CloseCode {
109    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
110        f.write_str(match self {
111            CloseCode::UnknownError => "Unknown Error",
112            CloseCode::UnknownOpcode => "Unknown Opcode",
113            CloseCode::DecodeError => "Decode Error",
114            CloseCode::NotAuthenticated => "Not Authenticated",
115            CloseCode::AuthenticationFailed => "Authentication Failed",
116            CloseCode::AlreadyAuthenticated => "Already Authenticated",
117            CloseCode::InvalidSequence => "Invalid Sequence",
118            CloseCode::RateLimited => "Rate Limited",
119            CloseCode::SessionTimedOut => "Session Timed Out",
120            CloseCode::InvalidShard => "Invalid Shard",
121            CloseCode::ShardingRequired => "Sharding Required",
122            CloseCode::InvalidApiVersion => "Invalid Api Version",
123            CloseCode::InvalidIntents => "Invalid Intents",
124            CloseCode::DisallowedIntents => "Disallowed Intents",
125        })
126    }
127}
128
129#[derive(Debug, Eq, PartialEq)]
130pub struct CloseCodeConversionError {
131    code: u16,
132}
133
134impl CloseCodeConversionError {
135    const fn new(code: u16) -> Self {
136        Self { code }
137    }
138
139    pub const fn code(&self) -> u16 {
140        self.code
141    }
142}
143
144impl Display for CloseCodeConversionError {
145    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
146        Display::fmt(&self.code, f)?;
147
148        f.write_str(" isn't a valid close code")
149    }
150}
151
152impl Error for CloseCodeConversionError {}
153
154impl TryFrom<u16> for CloseCode {
155    type Error = CloseCodeConversionError;
156
157    fn try_from(value: u16) -> Result<Self, Self::Error> {
158        let close_code = match value {
159            4000 => CloseCode::UnknownError,
160            4001 => CloseCode::UnknownOpcode,
161            4002 => CloseCode::DecodeError,
162            4003 => CloseCode::NotAuthenticated,
163            4004 => CloseCode::AuthenticationFailed,
164            4005 => CloseCode::AlreadyAuthenticated,
165            4007 => CloseCode::InvalidSequence,
166            4008 => CloseCode::RateLimited,
167            4009 => CloseCode::SessionTimedOut,
168            4010 => CloseCode::InvalidShard,
169            4011 => CloseCode::ShardingRequired,
170            4012 => CloseCode::InvalidApiVersion,
171            4013 => CloseCode::InvalidIntents,
172            4014 => CloseCode::DisallowedIntents,
173            _ => return Err(CloseCodeConversionError::new(value)),
174        };
175
176        Ok(close_code)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::{CloseCode, CloseCodeConversionError};
183    use serde::{Deserialize, Serialize};
184    use serde_test::Token;
185    use static_assertions::assert_impl_all;
186    use std::{fmt::Debug, hash::Hash};
187
188    assert_impl_all!(
189        CloseCode: Clone,
190        Copy,
191        Debug,
192        Deserialize<'static>,
193        Eq,
194        Hash,
195        PartialEq,
196        Send,
197        Serialize,
198        Sync
199    );
200    assert_impl_all!(CloseCodeConversionError: Debug, Eq, PartialEq, Send, Sync);
201
202    const MAP: &[(CloseCode, u16, bool)] = &[
203        (CloseCode::UnknownError, 4000, true),
204        (CloseCode::UnknownOpcode, 4001, true),
205        (CloseCode::DecodeError, 4002, true),
206        (CloseCode::NotAuthenticated, 4003, true),
207        (CloseCode::AuthenticationFailed, 4004, false),
208        (CloseCode::AlreadyAuthenticated, 4005, true),
209        (CloseCode::InvalidSequence, 4007, true),
210        (CloseCode::RateLimited, 4008, true),
211        (CloseCode::SessionTimedOut, 4009, true),
212        (CloseCode::InvalidShard, 4010, false),
213        (CloseCode::ShardingRequired, 4011, false),
214        (CloseCode::InvalidApiVersion, 4012, false),
215        (CloseCode::InvalidIntents, 4013, false),
216        (CloseCode::DisallowedIntents, 4014, false),
217    ];
218
219    #[test]
220    fn variants() {
221        for (kind, num, can_reconnect) in MAP {
222            serde_test::assert_tokens(kind, &[Token::U16(*num)]);
223            assert_eq!(*kind, CloseCode::try_from(*num).unwrap());
224            assert_eq!(*num, *kind as u16);
225            assert!(kind.can_reconnect() == *can_reconnect)
226        }
227    }
228
229    #[test]
230    fn try_from() {
231        assert!(
232            matches!(CloseCode::try_from(5000), Err(CloseCodeConversionError { code }) if code == 5000)
233        );
234    }
235}