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}