Skip to main content

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    #[must_use]
110    pub fn parse(source: &str) -> IRCPrefix {
111        if !source.contains('@') {
112            // just a hostname
113            IRCPrefix::HostOnly {
114                host: source.to_owned(),
115            }
116        } else {
117            // full prefix (nick[[!user]@host])
118            // valid forms:
119            // nick
120            // nick@host
121            // nick!user@host
122
123            // split on @ first, then on !
124            let mut at_split = source.splitn(2, '@');
125            let nick_and_user = at_split.next().unwrap();
126            let host = at_split.next();
127
128            // now nick_and_user is either "nick" or "nick!user"
129            let mut exc_split = nick_and_user.splitn(2, '!');
130            let nick = exc_split.next();
131            let user = exc_split.next();
132
133            IRCPrefix::Full {
134                nick: nick.unwrap().to_owned(),
135                user: user.map(|s| s.to_owned()),
136                host: host.map(|s| s.to_owned()),
137            }
138        }
139    }
140}
141
142impl AsRawIRC for IRCPrefix {
143    fn format_as_raw_irc(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
144        match self {
145            Self::HostOnly { host } => write!(f, "{host}")?,
146            Self::Full { nick, user, host } => {
147                write!(f, "{nick}")?;
148                if let Some(host) = host {
149                    if let Some(user) = user {
150                        write!(f, "!{user}")?;
151                    }
152                    write!(f, "@{host}")?;
153                }
154            }
155        }
156        Ok(())
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_display_host_only() {
166        let prefix = IRCPrefix::HostOnly {
167            host: "tmi.twitch.tv".to_owned(),
168        };
169        assert_eq!(prefix.as_raw_irc(), "tmi.twitch.tv");
170    }
171
172    #[test]
173    fn test_display_full_1() {
174        let prefix = IRCPrefix::Full {
175            nick: "justin".to_owned(),
176            user: Some("justin".to_owned()),
177            host: Some("justin.tmi.twitch.tv".to_owned()),
178        };
179        assert_eq!(prefix.as_raw_irc(), "justin!justin@justin.tmi.twitch.tv");
180    }
181
182    #[test]
183    fn test_display_full_2() {
184        let prefix = IRCPrefix::Full {
185            nick: "justin".to_owned(),
186            user: None,
187            host: Some("justin.tmi.twitch.tv".to_owned()),
188        };
189        assert_eq!(prefix.as_raw_irc(), "justin@justin.tmi.twitch.tv");
190    }
191
192    #[test]
193    fn test_display_full_3() {
194        let prefix = IRCPrefix::Full {
195            nick: "justin".to_owned(),
196            user: None,
197            host: None,
198        };
199        assert_eq!(prefix.as_raw_irc(), "justin");
200    }
201
202    #[test]
203    fn test_display_full_4_user_without_host_invalid_edge_case() {
204        let prefix = IRCPrefix::Full {
205            nick: "justin".to_owned(),
206            user: Some("justin".to_owned()),
207            host: None,
208        };
209        assert_eq!(prefix.as_raw_irc(), "justin");
210    }
211}