twitch_irc/message/
prefix.rs

1use super::AsRawIRC;
2use std::fmt;
3
4#[cfg(feature = "with-serde")]
5use {serde::Deserialize, serde::Serialize};
6
7/// A "prefix" part of an IRC message, as defined by RFC 2812:
8/// ```none
9/// <prefix>     ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
10/// <servername> ::= <host>
11/// <nick>       ::= <letter> { <letter> | <number> | <special> }
12/// <user>       ::= <nonwhite> { <nonwhite> }
13/// <host>       ::= see RFC 952 [DNS:4] for details on allowed hostnames
14/// <letter>     ::= 'a' ... 'z' | 'A' ... 'Z'
15/// <number>     ::= '0' ... '9'
16/// <special>    ::= '-' | '[' | ']' | '\' | '`' | '^' | '{' | '}'
17/// <nonwhite>   ::= <any 8bit code except SPACE (0x20), NUL (0x0), CR
18///                   (0xd), and LF (0xa)>
19/// ```
20///
21/// # Examples
22///
23/// ```
24/// use twitch_irc::message::IRCPrefix;
25/// use twitch_irc::message::AsRawIRC;
26///
27/// let prefix = IRCPrefix::Full {
28///     nick: "a_nick".to_owned(),
29///     user: Some("a_user".to_owned()),
30///     host: Some("a_host.com".to_owned())
31/// };
32///
33/// assert_eq!(prefix.as_raw_irc(), "a_nick!a_user@a_host.com");
34/// ```
35///
36/// ```
37/// use twitch_irc::message::IRCPrefix;
38/// use twitch_irc::message::AsRawIRC;
39///
40/// let prefix = IRCPrefix::Full {
41///     nick: "a_nick".to_owned(),
42///     user: None,
43///     host: Some("a_host.com".to_owned())
44/// };
45///
46/// assert_eq!(prefix.as_raw_irc(), "a_nick@a_host.com");
47/// ```
48///
49/// ```
50/// use twitch_irc::message::IRCPrefix;
51/// use twitch_irc::message::AsRawIRC;
52///
53/// let prefix = IRCPrefix::HostOnly {
54///     host: "a_host.com".to_owned()
55/// };
56///
57/// assert_eq!(prefix.as_raw_irc(), "a_host.com");
58/// ```
59#[derive(Debug, PartialEq, Eq, Clone, Hash)]
60#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
61pub enum IRCPrefix {
62    /// The prefix specifies only a sending server/hostname.
63    ///
64    /// Note that the spec also allows a very similar format where only a sending nickname is
65    /// specified. However that type of format plays no role on Twitch, and is practically impossible
66    /// to reliably tell apart from host-only prefix messages. For this reason, a prefix without
67    /// a `@` character is always assumed to be purely a host-only prefix, and not a nickname-only prefix.
68    HostOnly {
69        /// `host` part of the prefix
70        host: String,
71    },
72    /// The prefix variant specifies a nickname, and optionally also a username and optionally a
73    /// hostname. See above for the RFC definition.
74    Full {
75        /// `nick` part of the prefix
76        nick: String,
77        /// `user` part of the prefix
78        user: Option<String>,
79        /// `host` part of the prefix
80        host: Option<String>,
81    },
82}
83
84impl IRCPrefix {
85    /// Parse the `IRCPrefix` from the given string slice. `source` should be specified without
86    /// the leading `:` that precedes in full IRC messages.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use twitch_irc::message::IRCPrefix;
92    ///
93    /// let prefix = IRCPrefix::parse("a_nick!a_user@a_host.com");
94    /// assert_eq!(prefix, IRCPrefix::Full {
95    ///     nick: "a_nick".to_owned(),
96    ///     user: Some("a_user".to_owned()),
97    ///     host: Some("a_host.com".to_owned())
98    /// })
99    /// ```
100    ///
101    /// ```
102    /// use twitch_irc::message::IRCPrefix;
103    ///
104    /// let prefix = IRCPrefix::parse("a_host.com");
105    /// assert_eq!(prefix, IRCPrefix::HostOnly {
106    ///     host: "a_host.com".to_owned()
107    /// })
108    /// ```
109    pub fn parse(source: &str) -> IRCPrefix {
110        if !source.contains('@') {
111            // just a hostname
112            IRCPrefix::HostOnly {
113                host: source.to_owned(),
114            }
115        } else {
116            // full prefix (nick[[!user]@host])
117            // valid forms:
118            // nick
119            // nick@host
120            // nick!user@host
121
122            // split on @ first, then on !
123            let mut at_split = source.splitn(2, '@');
124            let nick_and_user = at_split.next().unwrap();
125            let host = at_split.next();
126
127            // now nick_and_user is either "nick" or "nick!user"
128            let mut exc_split = nick_and_user.splitn(2, '!');
129            let nick = exc_split.next();
130            let user = exc_split.next();
131
132            IRCPrefix::Full {
133                nick: nick.unwrap().to_owned(),
134                user: user.map(|s| s.to_owned()),
135                host: host.map(|s| s.to_owned()),
136            }
137        }
138    }
139}
140
141impl AsRawIRC for IRCPrefix {
142    fn format_as_raw_irc(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
143        match self {
144            Self::HostOnly { host } => write!(f, "{}", host)?,
145            Self::Full { nick, user, host } => {
146                write!(f, "{}", nick)?;
147                if let Some(host) = host {
148                    if let Some(user) = user {
149                        write!(f, "!{}", user)?
150                    }
151                    write!(f, "@{}", host)?;
152                }
153            }
154        }
155        Ok(())
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_display_host_only() {
165        let prefix = IRCPrefix::HostOnly {
166            host: "tmi.twitch.tv".to_owned(),
167        };
168        assert_eq!(prefix.as_raw_irc(), "tmi.twitch.tv");
169    }
170
171    #[test]
172    fn test_display_full_1() {
173        let prefix = IRCPrefix::Full {
174            nick: "justin".to_owned(),
175            user: Some("justin".to_owned()),
176            host: Some("justin.tmi.twitch.tv".to_owned()),
177        };
178        assert_eq!(prefix.as_raw_irc(), "justin!justin@justin.tmi.twitch.tv");
179    }
180
181    #[test]
182    fn test_display_full_2() {
183        let prefix = IRCPrefix::Full {
184            nick: "justin".to_owned(),
185            user: None,
186            host: Some("justin.tmi.twitch.tv".to_owned()),
187        };
188        assert_eq!(prefix.as_raw_irc(), "justin@justin.tmi.twitch.tv");
189    }
190
191    #[test]
192    fn test_display_full_3() {
193        let prefix = IRCPrefix::Full {
194            nick: "justin".to_owned(),
195            user: None,
196            host: None,
197        };
198        assert_eq!(prefix.as_raw_irc(), "justin");
199    }
200
201    #[test]
202    fn test_display_full_4_user_without_host_invalid_edge_case() {
203        let prefix = IRCPrefix::Full {
204            nick: "justin".to_owned(),
205            user: Some("justin".to_owned()),
206            host: None,
207        };
208        assert_eq!(prefix.as_raw_irc(), "justin");
209    }
210}