Skip to main content

twitch_irc/message/commands/
mod.rs

1pub mod clearchat;
2pub mod clearmsg;
3pub mod globaluserstate;
4pub mod join;
5pub mod notice;
6pub mod part;
7pub mod ping;
8pub mod pong;
9pub mod privmsg;
10pub mod reconnect;
11pub mod roomstate;
12pub mod usernotice;
13pub mod userstate;
14pub mod whisper;
15
16use self::ServerMessageParseError::{
17    MalformedChannel, MalformedTagValue, MissingNickname, MissingParameter, MissingPrefix,
18    MissingTag, MissingTagValue,
19};
20use crate::message::commands::clearmsg::ClearMsgMessage;
21use crate::message::commands::join::JoinMessage;
22use crate::message::commands::part::PartMessage;
23use crate::message::commands::ping::PingMessage;
24use crate::message::commands::pong::PongMessage;
25use crate::message::commands::reconnect::ReconnectMessage;
26use crate::message::commands::userstate::UserStateMessage;
27use crate::message::prefix::IRCPrefix;
28use crate::message::twitch::{Badge, Emote, RGBColor};
29use crate::message::{
30    AsRawIRC, ClearChatMessage, GlobalUserStateMessage, IRCMessage, NoticeMessage, PrivmsgMessage,
31    ReplyParent, RoomStateMessage, TwitchUserBasics, UserNoticeMessage, WhisperMessage,
32};
33use chrono::{DateTime, TimeZone, Utc};
34use std::collections::HashSet;
35use std::convert::TryFrom;
36use std::ops::Range;
37use std::str::FromStr;
38use thiserror::Error;
39
40#[cfg(feature = "with-serde")]
41use {serde::Deserialize, serde::Serialize};
42
43/// Errors encountered while trying to parse an IRC message as a more specialized "server message",
44/// based on its IRC command.
45#[derive(Error, Debug, PartialEq, Eq)]
46pub enum ServerMessageParseError {
47    /// That command's data is not parsed by this implementation
48    ///
49    /// This type of error is only returned if you use `try_from` directly on a special
50    /// server message implementation, instead of the general `ServerMessage::try_from`
51    /// which covers all implementations and does not emit this type of error.
52    #[error("Could not parse IRC message {} as ServerMessage: That command's data is not parsed by this implementation", .0.as_raw_irc())]
53    MismatchedCommand(Box<IRCMessage>),
54    /// No tag present under key `key`
55    #[error(
56        "Could not parse IRC message {msg} as ServerMessage: No tag present under key `{key}`",
57        msg = .0.as_raw_irc(),
58        key = .1,
59    )]
60    MissingTag(Box<IRCMessage>, &'static str),
61    /// No tag value present under key `key`
62    #[error(
63        "Could not parse IRC message {msg} as ServerMessage: No tag value present under key `{key}`", 
64        msg = .0.as_raw_irc(),
65        key = .1,
66    )]
67    MissingTagValue(Box<IRCMessage>, &'static str),
68    /// Malformed tag value for tag `key`, value was `value`
69    #[error(
70        "Could not parse IRC message {msg} as ServerMessage: Malformed tag value for tag `{key}`, value was `{value}`",
71        msg = .0.as_raw_irc(),
72        key = .1,
73        value = .2,
74    )]
75    MalformedTagValue(Box<IRCMessage>, &'static str, String),
76    /// No parameter found at index `n`
77    #[error(
78        "Could not parse IRC message {msg} as ServerMessage: No parameter found at index {key}",
79        msg = .0.as_raw_irc(),
80        key = .1,
81    )]
82    MissingParameter(Box<IRCMessage>, usize),
83    /// Malformed channel parameter (`#` must be present + something after it)
84    #[error(
85        "Could not parse IRC message {msg} as ServerMessage: Malformed channel parameter (# must be present + something after it)",
86        msg = .0.as_raw_irc(),
87    )]
88    MalformedChannel(Box<IRCMessage>),
89    /// Malformed parameter at index `n`
90    #[error(
91        "Could not parse IRC message {msg} as ServerMessage: Malformed parameter at index {idx}",
92        msg = .0.as_raw_irc(),
93        idx = .1,
94    )]
95    MalformedParameter(Box<IRCMessage>, usize),
96    /// Missing prefix altogether
97    #[error(
98        "Could not parse IRC message {msg} as ServerMessage: Missing prefix altogether",
99        msg = .0.as_raw_irc(),
100    )]
101    MissingPrefix(Box<IRCMessage>),
102    /// No nickname found in prefix
103    #[error(
104        "Could not parse IRC message {msg} as ServerMessage: No nickname found in prefix",
105        msg = .0.as_raw_irc(),
106    )]
107    MissingNickname(Box<IRCMessage>),
108}
109
110impl From<ServerMessageParseError> for IRCMessage {
111    fn from(msg: ServerMessageParseError) -> IRCMessage {
112        match msg {
113            ServerMessageParseError::MismatchedCommand(m)
114            | ServerMessageParseError::MissingTag(m, _)
115            | ServerMessageParseError::MissingTagValue(m, _)
116            | ServerMessageParseError::MalformedTagValue(m, _, _)
117            | ServerMessageParseError::MissingParameter(m, _)
118            | ServerMessageParseError::MalformedChannel(m)
119            | ServerMessageParseError::MalformedParameter(m, _)
120            | ServerMessageParseError::MissingPrefix(m)
121            | ServerMessageParseError::MissingNickname(m) => *m,
122        }
123    }
124}
125
126trait IRCMessageParseExt {
127    fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError>;
128    fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError>;
129    fn try_get_tag_value(&self, key: &'static str) -> Result<&str, ServerMessageParseError>;
130    fn try_get_nonempty_tag_value(
131        &self,
132        key: &'static str,
133    ) -> Result<&str, ServerMessageParseError>;
134    fn try_get_optional_nonempty_tag_value(
135        &self,
136        key: &'static str,
137    ) -> Result<Option<&str>, ServerMessageParseError>;
138    fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError>;
139    fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError>;
140    fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError>;
141    fn try_get_emotes(
142        &self,
143        tag_key: &'static str,
144        message_text: &str,
145    ) -> Result<Vec<Emote>, ServerMessageParseError>;
146    fn try_get_emote_sets(
147        &self,
148        tag_key: &'static str,
149    ) -> Result<HashSet<String>, ServerMessageParseError>;
150    fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError>;
151    fn try_get_color(
152        &self,
153        tag_key: &'static str,
154    ) -> Result<Option<RGBColor>, ServerMessageParseError>;
155    fn try_get_number<N: FromStr>(
156        &self,
157        tag_key: &'static str,
158    ) -> Result<N, ServerMessageParseError>;
159    fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError>;
160    fn try_get_optional_number<N: FromStr>(
161        &self,
162        tag_key: &'static str,
163    ) -> Result<Option<N>, ServerMessageParseError>;
164    fn try_get_optional_bool(
165        &self,
166        tag_key: &'static str,
167    ) -> Result<Option<bool>, ServerMessageParseError>;
168    fn try_get_timestamp(
169        &self,
170        tag_key: &'static str,
171    ) -> Result<DateTime<Utc>, ServerMessageParseError>;
172    fn try_get_optional_reply_parent(&self)
173    -> Result<Option<ReplyParent>, ServerMessageParseError>;
174}
175
176impl IRCMessageParseExt for IRCMessage {
177    fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError> {
178        Ok(self
179            .params
180            .get(index)
181            .ok_or_else(|| MissingParameter(Box::new(self.to_owned()), index))?)
182    }
183
184    fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError> {
185        let mut message_text = self.try_get_param(1)?;
186
187        let is_action =
188            message_text.starts_with("\u{0001}ACTION ") && message_text.ends_with('\u{0001}');
189        if is_action {
190            // remove the prefix and suffix
191            message_text = &message_text[8..message_text.len() - 1];
192        }
193
194        Ok((message_text, is_action))
195    }
196
197    fn try_get_tag_value(&self, key: &'static str) -> Result<&str, ServerMessageParseError> {
198        match self.tags.0.get(key) {
199            Some(value) => Ok(value),
200            None => Err(MissingTag(Box::new(self.to_owned()), key)),
201        }
202    }
203
204    fn try_get_nonempty_tag_value(
205        &self,
206        key: &'static str,
207    ) -> Result<&str, ServerMessageParseError> {
208        match self.tags.0.get(key) {
209            Some(value) => match value.as_str() {
210                "" => Err(MissingTagValue(Box::new(self.to_owned()), key)),
211                otherwise => Ok(otherwise),
212            },
213            None => Err(MissingTag(Box::new(self.to_owned()), key)),
214        }
215    }
216
217    fn try_get_optional_nonempty_tag_value(
218        &self,
219        key: &'static str,
220    ) -> Result<Option<&str>, ServerMessageParseError> {
221        match self.tags.0.get(key) {
222            Some(value) => match value.as_str() {
223                "" => Err(MissingTagValue(Box::new(self.to_owned()), key)),
224                otherwise => Ok(Some(otherwise)),
225            },
226            None => Ok(None),
227        }
228    }
229
230    fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError> {
231        let param = self.try_get_param(0)?;
232
233        if !param.starts_with('#') || param.len() < 2 {
234            return Err(MalformedChannel(Box::new(self.to_owned())));
235        }
236
237        Ok(&param[1..])
238    }
239
240    fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError> {
241        let param = self.try_get_param(0)?;
242
243        if param == "*" {
244            return Ok(None);
245        }
246
247        if !param.starts_with('#') || param.len() < 2 {
248            return Err(MalformedChannel(Box::new(self.to_owned())));
249        }
250
251        Ok(Some(&param[1..]))
252    }
253
254    /// Get the sending user's login name from the IRC prefix.
255    fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError> {
256        match &self.prefix {
257            None => Err(MissingPrefix(Box::new(self.to_owned()))),
258            Some(IRCPrefix::HostOnly { host: _ }) => {
259                Err(MissingNickname(Box::new(self.to_owned())))
260            }
261            Some(IRCPrefix::Full {
262                nick,
263                user: _,
264                host: _,
265            }) => Ok(nick),
266        }
267    }
268
269    fn try_get_emotes(
270        &self,
271        tag_key: &'static str,
272        message_text: &str,
273    ) -> Result<Vec<Emote>, ServerMessageParseError> {
274        let tag_value = self.try_get_tag_value(tag_key)?;
275
276        if tag_value.is_empty() {
277            return Ok(vec![]);
278        }
279
280        let mut emotes = Vec::new();
281
282        let make_error =
283            || MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
284
285        // emotes tag format:
286        // emote_id:from-to,from-to,from-to/emote_id:from-to,from-to/emote_id:from-to
287        for src in tag_value.split('/') {
288            let (emote_id, indices_src) = src.split_once(':').ok_or_else(make_error)?;
289
290            for range_src in indices_src.split(',') {
291                let (start, end) = range_src.split_once('-').ok_or_else(make_error)?;
292
293                let start = usize::from_str(start).map_err(|_| make_error())?;
294                // twitch specifies the end index as inclusive, but in Rust (and most programming
295                // languages for that matter) it's very common to specify end indices as exclusive,
296                // so we add 1 here to make it exclusive.
297                let end = usize::from_str(end).map_err(|_| make_error())? + 1;
298
299                let code_length = end - start;
300
301                let code = message_text
302                    .chars()
303                    .skip(start)
304                    .take(code_length)
305                    .collect::<String>();
306
307                // we intentionally gracefully handle indices that are out of bounds for the
308                // given string by taking as much as possible until the end of the string.
309                // This is to work around a Twitch bug: https://github.com/twitchdev/issues/issues/104
310
311                emotes.push(Emote {
312                    id: emote_id.to_owned(),
313                    char_range: Range { start, end },
314                    code,
315                });
316            }
317        }
318
319        emotes.sort_unstable_by_key(|e| e.char_range.start);
320
321        Ok(emotes)
322    }
323
324    fn try_get_emote_sets(
325        &self,
326        tag_key: &'static str,
327    ) -> Result<HashSet<String>, ServerMessageParseError> {
328        let src = self.try_get_tag_value(tag_key)?;
329
330        if src.is_empty() {
331            Ok(HashSet::new())
332        } else {
333            Ok(src.split(',').map(|s| s.to_owned()).collect())
334        }
335    }
336
337    fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError> {
338        // TODO same thing as above, could be optimized to not clone the tag value as well
339        let tag_value = self.try_get_tag_value(tag_key)?;
340
341        if tag_value.is_empty() {
342            return Ok(vec![]);
343        }
344
345        let mut badges = Vec::new();
346
347        let make_error =
348            || MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
349
350        // badges tag format:
351        // admin/1,moderator/1,subscriber/12
352        for src in tag_value.split(',') {
353            let (name, version) = src.split_once('/').ok_or_else(make_error)?;
354
355            badges.push(Badge {
356                name: name.to_owned(),
357                version: version.to_owned(),
358            });
359        }
360
361        Ok(badges)
362    }
363
364    fn try_get_color(
365        &self,
366        tag_key: &'static str,
367    ) -> Result<Option<RGBColor>, ServerMessageParseError> {
368        let tag_value = self.try_get_tag_value(tag_key)?;
369        let make_error =
370            || MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
371
372        if tag_value.is_empty() {
373            return Ok(None);
374        }
375
376        // color is expected to be in format #RRGGBB
377        if tag_value.len() != 7 {
378            return Err(make_error());
379        }
380
381        Ok(Some(RGBColor {
382            r: u8::from_str_radix(&tag_value[1..3], 16).map_err(|_| make_error())?,
383            g: u8::from_str_radix(&tag_value[3..5], 16).map_err(|_| make_error())?,
384            b: u8::from_str_radix(&tag_value[5..7], 16).map_err(|_| make_error())?,
385        }))
386    }
387
388    fn try_get_number<N: FromStr>(
389        &self,
390        tag_key: &'static str,
391    ) -> Result<N, ServerMessageParseError> {
392        let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
393        let number = N::from_str(tag_value).map_err(|_| {
394            MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
395        })?;
396        Ok(number)
397    }
398
399    fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError> {
400        Ok(self.try_get_number::<u8>(tag_key)? > 0)
401    }
402
403    fn try_get_optional_number<N: FromStr>(
404        &self,
405        tag_key: &'static str,
406    ) -> Result<Option<N>, ServerMessageParseError> {
407        let tag_value = match self.tags.0.get(tag_key) {
408            Some(value) => match value.as_str() {
409                "" => return Err(MissingTagValue(Box::new(self.to_owned()), tag_key)),
410                otherwise => otherwise,
411            },
412            None => return Ok(None),
413        };
414
415        let number = N::from_str(tag_value).map_err(|_| {
416            MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
417        })?;
418        Ok(Some(number))
419    }
420
421    fn try_get_optional_bool(
422        &self,
423        tag_key: &'static str,
424    ) -> Result<Option<bool>, ServerMessageParseError> {
425        Ok(self.try_get_optional_number::<u8>(tag_key)?.map(|n| n > 0))
426    }
427
428    fn try_get_timestamp(
429        &self,
430        tag_key: &'static str,
431    ) -> Result<DateTime<Utc>, ServerMessageParseError> {
432        // e.g. tmi-sent-ts.
433        let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
434        let milliseconds_since_epoch = i64::from_str(tag_value).map_err(|_| {
435            MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
436        })?;
437        Utc.timestamp_millis_opt(milliseconds_since_epoch)
438            .single()
439            .ok_or_else(|| {
440                MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
441            })
442    }
443
444    fn try_get_optional_reply_parent(
445        &self,
446    ) -> Result<Option<ReplyParent>, ServerMessageParseError> {
447        // If at least one of the reply-parent tags is present, the other four will be guaranteed.
448        if !self.tags.0.contains_key("reply-parent-msg-id") {
449            return Ok(None);
450        }
451
452        Ok(Some(ReplyParent {
453            message_id: self.try_get_tag_value("reply-parent-msg-id")?.to_owned(),
454            reply_parent_user: TwitchUserBasics {
455                id: self
456                    .try_get_nonempty_tag_value("reply-parent-user-id")?
457                    .to_owned(),
458                login: self
459                    .try_get_nonempty_tag_value("reply-parent-user-login")?
460                    .to_owned(),
461                name: self
462                    .try_get_nonempty_tag_value("reply-parent-display-name")?
463                    .to_owned(),
464            },
465            message_text: self.try_get_tag_value("reply-parent-msg-body")?.to_owned(),
466        }))
467    }
468}
469
470// makes it so users cannot match against Generic and get the underlying IRCMessage
471// that way (which would break their implementations if there is an enum variant added and they
472// expect certain commands to be emitted under Generic)
473// that means the only way to get the IRCMessage is via IRCMessage::from()/.into()
474// which combined with #[non_exhaustive] allows us to add enum variants
475// without making a major release
476#[derive(Debug, PartialEq, Eq, Clone)]
477#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
478#[doc(hidden)]
479pub struct HiddenIRCMessage(pub(self) IRCMessage);
480
481/// An [`IRCMessage`] that has been parsed into a more concrete type based on its command.
482///
483/// This type is non-exhausive, because more types of commands exist and can be added.
484///
485/// If you wish to (manually) parse a type of command that is not already parsed by this library,
486/// use `IRCMessage::from` to convert the `ServerMessage` back to an `IRCMessage`, then
487/// check the message's `command` and perform your parsing.
488///
489/// There is intentionally no generic `Unparsed` variant here. If there was, and the library
490/// added parsing for the command you were trying to catch by matching against the `Unparsed`
491/// variant, your code would be broken without any compiler error.
492///
493/// # Examples
494///
495/// ```
496/// use twitch_irc::message::{IRCMessage, ServerMessage};
497/// use std::convert::TryFrom;
498///
499/// let irc_message = IRCMessage::parse(":tmi.twitch.tv PING").unwrap();
500/// let server_message = ServerMessage::try_from(irc_message).unwrap();
501///
502/// match server_message {
503///     // match against known types first
504///     ServerMessage::Ping { .. } => println!("Got pinged!"),
505///     rest => {
506///         // can do manual parsing here
507///         let irc_message = IRCMessage::from(rest);
508///         if irc_message.command == "CUSTOMCMD" {
509///              // ...
510///         }
511///     }
512/// }
513/// ```
514#[derive(Debug, Clone)]
515#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
516#[non_exhaustive]
517pub enum ServerMessage {
518    /// `CLEARCHAT` message
519    ClearChat(ClearChatMessage),
520    /// `CLEARMSG` message
521    ClearMsg(ClearMsgMessage),
522    /// `GLOBALUSERSTATE` message
523    GlobalUserState(GlobalUserStateMessage),
524    /// `JOIN` message
525    Join(JoinMessage),
526    /// `NOTICE` message
527    Notice(NoticeMessage),
528    /// `PART` message
529    Part(PartMessage),
530    /// `PING` message
531    Ping(PingMessage),
532    /// `PONG` message
533    Pong(PongMessage),
534    /// `PRIVMSG` message
535    Privmsg(PrivmsgMessage),
536    /// `RECONNECT` message
537    Reconnect(ReconnectMessage),
538    /// `ROOMSTATE` message
539    RoomState(RoomStateMessage),
540    /// `USERNOTICE` message
541    UserNotice(UserNoticeMessage),
542    /// `USERSTATE` message
543    UserState(UserStateMessage),
544    /// `WHISPER` message
545    Whisper(WhisperMessage),
546    #[doc(hidden)]
547    Generic(HiddenIRCMessage),
548}
549
550impl TryFrom<IRCMessage> for ServerMessage {
551    type Error = ServerMessageParseError;
552
553    fn try_from(source: IRCMessage) -> Result<ServerMessage, ServerMessageParseError> {
554        use ServerMessage::{
555            ClearChat, ClearMsg, Generic, GlobalUserState, Join, Notice, Part, Ping, Pong, Privmsg,
556            Reconnect, RoomState, UserNotice, UserState, Whisper,
557        };
558
559        Ok(match source.command.as_str() {
560            "CLEARCHAT" => ClearChat(ClearChatMessage::try_from(source)?),
561            "CLEARMSG" => ClearMsg(ClearMsgMessage::try_from(source)?),
562            "GLOBALUSERSTATE" => GlobalUserState(GlobalUserStateMessage::try_from(source)?),
563            "JOIN" => Join(JoinMessage::try_from(source)?),
564            "NOTICE" => Notice(NoticeMessage::try_from(source)?),
565            "PART" => Part(PartMessage::try_from(source)?),
566            "PING" => Ping(PingMessage::try_from(source)?),
567            "PONG" => Pong(PongMessage::try_from(source)?),
568            "PRIVMSG" => Privmsg(PrivmsgMessage::try_from(source)?),
569            "RECONNECT" => Reconnect(ReconnectMessage::try_from(source)?),
570            "ROOMSTATE" => RoomState(RoomStateMessage::try_from(source)?),
571            "USERNOTICE" => UserNotice(UserNoticeMessage::try_from(source)?),
572            "USERSTATE" => UserState(UserStateMessage::try_from(source)?),
573            "WHISPER" => Whisper(WhisperMessage::try_from(source)?),
574            _ => Generic(HiddenIRCMessage(source)),
575        })
576    }
577}
578
579impl From<ServerMessage> for IRCMessage {
580    fn from(msg: ServerMessage) -> IRCMessage {
581        match msg {
582            ServerMessage::ClearChat(msg) => msg.source,
583            ServerMessage::ClearMsg(msg) => msg.source,
584            ServerMessage::GlobalUserState(msg) => msg.source,
585            ServerMessage::Join(msg) => msg.source,
586            ServerMessage::Notice(msg) => msg.source,
587            ServerMessage::Part(msg) => msg.source,
588            ServerMessage::Ping(msg) => msg.source,
589            ServerMessage::Pong(msg) => msg.source,
590            ServerMessage::Privmsg(msg) => msg.source,
591            ServerMessage::Reconnect(msg) => msg.source,
592            ServerMessage::RoomState(msg) => msg.source,
593            ServerMessage::UserNotice(msg) => msg.source,
594            ServerMessage::UserState(msg) => msg.source,
595            ServerMessage::Whisper(msg) => msg.source,
596            ServerMessage::Generic(msg) => msg.0,
597        }
598    }
599}
600
601// borrowed variant of the above
602impl ServerMessage {
603    /// Get a reference to the `IRCMessage` this `ServerMessage` was parsed from.
604    #[must_use]
605    pub fn source(&self) -> &IRCMessage {
606        match self {
607            ServerMessage::ClearChat(msg) => &msg.source,
608            ServerMessage::ClearMsg(msg) => &msg.source,
609            ServerMessage::GlobalUserState(msg) => &msg.source,
610            ServerMessage::Join(msg) => &msg.source,
611            ServerMessage::Notice(msg) => &msg.source,
612            ServerMessage::Part(msg) => &msg.source,
613            ServerMessage::Ping(msg) => &msg.source,
614            ServerMessage::Pong(msg) => &msg.source,
615            ServerMessage::Privmsg(msg) => &msg.source,
616            ServerMessage::Reconnect(msg) => &msg.source,
617            ServerMessage::RoomState(msg) => &msg.source,
618            ServerMessage::UserNotice(msg) => &msg.source,
619            ServerMessage::UserState(msg) => &msg.source,
620            ServerMessage::Whisper(msg) => &msg.source,
621            ServerMessage::Generic(msg) => &msg.0,
622        }
623    }
624
625    pub(crate) fn new_generic(message: IRCMessage) -> ServerMessage {
626        ServerMessage::Generic(HiddenIRCMessage(message))
627    }
628}
629
630impl AsRawIRC for ServerMessage {
631    fn format_as_raw_irc(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
632        self.source().format_as_raw_irc(f)
633    }
634}