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}