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