tmi_parser/message.rs
1//! IRC-based TMI messages.
2
3use crate::{TagValue, Tags};
4use std::io::{Error, ErrorKind, Result};
5
6/// Possible types of TMI messages.
7/// Unrecognized messages are handled by the associated [`parse`] function.
8///
9/// Tags are always treated as Optional even on messages that require them.
10/// Actually, tags validation should be done by the user code.
11///
12/// Consider changing simple enum structs to enum tuples.
13#[derive(Debug, PartialEq)]
14pub enum Message<'a> {
15 /// Represents a ping request message.
16 /// `PING :<endpoint>`
17 Ping,
18 /// Represents a pong response message.
19 /// `PONG :<endpoint>`
20 Pong,
21 /// Represents a capability request message.
22 /// `CAP REQ :<capability>`
23 CapReq { req: &'a str },
24 /// Represents a capability acknowledgement message.
25 /// `:<endpoint> CAP * ACK :<capability>`
26 CapAck { req: &'a str },
27 /// Represents a password authentication message.
28 /// `PASS <password>`
29 /// `PASS oauth:<token>` (using Twitch OAuth tokens)
30 Pass { pass: &'a str },
31 /// Represents a nickname authentication message.
32 /// `NICK <user>`
33 Nick { nick: &'a str },
34 /// Represents a join command message.
35 /// `JOIN #<channel>`
36 Join { chan: &'a str },
37 /// Represents a part command message.
38 /// `PART #<channel>`
39 Part { chan: &'a str },
40 /// Represents a privmsg command message.
41 /// `[@<tags>] PRIVMSG #<channel> :<message>`
42 Privmsg {
43 tags: Option<Tags<'a>>,
44 chan: &'a str,
45 msg: &'a str,
46 },
47 /// Represents a clearchat command message.
48 /// `[@<tags>] :<endpoint> CLEARCHAT #<channel> [:<user>]`
49 Clearchat {
50 tags: Option<Tags<'a>>,
51 chan: &'a str,
52 usr: Option<&'a str>,
53 },
54 /// Represents a clearmsg command message.
55 /// `[@<tags>] :<endpoint> CLEARMSG #<channel> [:<message>]`
56 Clearmsg {
57 tags: Option<Tags<'a>>,
58 chan: &'a str,
59 msg: &'a str,
60 },
61 /// Represents a hosttarget start message.
62 /// `:<endpoint> HOSTTARGET #<host> :<channel> [<viewers>]`
63 HosttargetStart {
64 host: &'a str,
65 chan: &'a str,
66 view: Option<u32>,
67 },
68 /// Represents a hosttarget end message.
69 /// `:<endpoint> HOSTTARGET #<host> :- [<viewers>]`
70 HosttargetEnd { host: &'a str, view: Option<u32> },
71 /// Represents a notice message.
72 /// `[@<tags>] :<endpoint> NOTICE #<channel> :<message>`
73 Notice {
74 tags: Option<Tags<'a>>,
75 chan: &'a str,
76 msg: &'a str,
77 },
78 /// Represents a reconnect request message.
79 /// `RECONNECT`
80 Reconnect,
81 /// Represents a roomstate message.
82 /// `[@<tags>] :<endpoint> ROOMSTATE #<channel>`
83 Roomstate {
84 tags: Option<Tags<'a>>,
85 chan: &'a str,
86 },
87 /// Represents a usernotice message.
88 /// `[@<tags>] :<endpoint> USERNOTICE #<channel> :<message>`
89 Usernotice {
90 tags: Option<Tags<'a>>,
91 chan: &'a str,
92 msg: &'a str,
93 },
94 /// Represents a userstate message.
95 /// `[@<tags>] :<endpoint> USERSTATE #<channel>`
96 Userstate {
97 tags: Option<Tags<'a>>,
98 chan: &'a str,
99 },
100 /// Represents a global userstate message.
101 /// `[@<tags>] :<endpoint> GLOBALUSERSTATE`
102 GlobalUserstate { tags: Option<Tags<'a>> },
103}
104
105impl<'a> Message<'a> {
106 /// Parses a [`& str`] slice and returns a Message if successful, otherwise an [`io::Error`].
107 ///
108 /// # Examples
109 ///
110 /// ```
111 /// let s = ":tmi.twitch.tv CLEARCHAT #dallas :ronni";
112 /// let msg = tmi_parser::Message::parse(s);
113 /// ```
114 pub fn parse(msg: &'a str) -> Result<Message> {
115 if msg.len() < 5 {
116 return Err(Error::new(ErrorKind::Other, "Malformed message."));
117 }
118
119 let buf = msg.trim();
120 let (tags, off) = if let Some(buf) = buf.strip_prefix('@') {
121 Self::parse_tags(buf)?
122 } else {
123 (None, 0)
124 };
125
126 let mut rest = &buf[off..];
127 const ENDPOINT: &str = "tmi.twitch.tv ";
128
129 if let Some(off) = rest.find(ENDPOINT) {
130 rest = &rest[(off + ENDPOINT.len())..];
131 }
132
133 if let Some(off) = rest.find(' ') {
134 let cmd = &rest[..off];
135 let body = &rest[(off + 1)..];
136
137 Self::parse_command(cmd, body, tags)
138 } else {
139 Self::parse_command(rest, "", tags)
140 }
141 }
142
143 /// Helper function for parsing message tags.
144 fn parse_tags(msg: &'a str) -> Result<(Option<Tags<'a>>, usize)> {
145 let mut map = Tags::default();
146
147 if let Some(idx) = msg.find(" :") {
148 let tag = &msg[..idx];
149 let toks = tag.split(';');
150
151 for tok in toks {
152 let items = tok.split('=').collect::<Vec<_>>();
153 let key = items[0];
154 let val = items[1];
155
156 map.insert(key, TagValue::new(val));
157 }
158
159 Ok((Some(map), idx + 3))
160 } else {
161 Err(Error::new(ErrorKind::Other, "Parsing message tags failed."))
162 }
163 }
164
165 /// Helper function for parsing message body base on the command.
166 fn parse_command(cmd: &'a str, body: &'a str, tags: Option<Tags<'a>>) -> Result<Message<'a>> {
167 Ok(match cmd {
168 "PING" => Message::Ping,
169 "PONG" => Message::Pong,
170 "CAP" => {
171 let off = body
172 .find(" :")
173 .ok_or_else(|| Error::new(ErrorKind::Other, "Malformed CAP command."))?;
174
175 match &body[..off] {
176 "REQ" => Message::CapReq {
177 req: &body[(off + 2)..],
178 },
179 "* ACK" => Message::CapAck {
180 req: &body[(off + 2)..],
181 },
182 _ => return Err(Error::new(ErrorKind::Other, "Malformed CAP command.")),
183 }
184 }
185 "PASS" => Message::Pass { pass: body },
186 "NICK" => Message::Nick { nick: body },
187 "JOIN" => Message::Join { chan: &body[1..] },
188 "PART" => Message::Part { chan: &body[1..] },
189 "PRIVMSG" => {
190 let off = body
191 .find(" :")
192 .ok_or_else(|| Error::new(ErrorKind::Other, "Malformed PRIVMSG command."))?;
193
194 Message::Privmsg {
195 tags,
196 chan: &body[1..off],
197 msg: &body[(off + 2)..],
198 }
199 }
200 "CLEARCHAT" => {
201 if let Some(off) = body.find(" :") {
202 Message::Clearchat {
203 tags,
204 chan: &body[1..off],
205 usr: Some(&body[(off + 2)..]),
206 }
207 } else {
208 Message::Clearchat {
209 tags,
210 chan: &body[1..],
211 usr: None,
212 }
213 }
214 }
215 "CLEARMSG" => {
216 let off = body
217 .find(" :")
218 .ok_or_else(|| Error::new(ErrorKind::Other, "Malformed CLEARMSG command."))?;
219
220 Message::Clearmsg {
221 tags,
222 chan: &body[1..off],
223 msg: &body[(off + 2)..],
224 }
225 }
226 "HOSTTARGET" => {
227 if let Some(off) = body.find(" :-") {
228 if body.len() > off + 5 {
229 if let Ok(view) = body[(off + 4)..].parse::<u32>() {
230 Message::HosttargetEnd {
231 host: &body[1..off],
232 view: Some(view),
233 }
234 } else {
235 return Err(Error::new(
236 ErrorKind::Other,
237 "Malformed HOSTTARGET command.",
238 ));
239 }
240 } else {
241 Message::HosttargetEnd {
242 host: &body[1..off],
243 view: None,
244 }
245 }
246 } else {
247 let off = body.find(" :").ok_or_else(|| {
248 Error::new(ErrorKind::Other, "Malformed HOSTTARGET command.")
249 })?;
250
251 if body.len() < off + 3 {
252 return Err(Error::new(
253 ErrorKind::Other,
254 "Malformed HOSTTARGET command.",
255 ));
256 }
257
258 let host = &body[1..off];
259 let body = &body[(off + 2)..];
260
261 if let Some(off) = body.find(' ') {
262 if body.len() > off + 2 {
263 if let Ok(view) = body[(off + 1)..].parse::<u32>() {
264 return Ok(Message::HosttargetStart {
265 host,
266 chan: &body[..off],
267 view: Some(view),
268 });
269 } else {
270 return Err(Error::new(
271 ErrorKind::Other,
272 "Malformed HOSTTARGET command.",
273 ));
274 }
275 } else {
276 Message::HosttargetStart {
277 host,
278 chan: body,
279 view: None,
280 }
281 }
282 } else {
283 Message::HosttargetStart {
284 host,
285 chan: body,
286 view: None,
287 }
288 }
289 }
290 }
291 "NOTICE" => {
292 let off = body
293 .find(" :")
294 .ok_or_else(|| Error::new(ErrorKind::Other, "Malformed NOTICE command."))?;
295
296 Message::Notice {
297 tags,
298 chan: &body[1..off],
299 msg: &body[(off + 2)..],
300 }
301 }
302 "RECONNECT" => Message::Reconnect,
303 "ROOMSTATE" => Message::Roomstate {
304 tags,
305 chan: &body[1..],
306 },
307 "USERNOTICE" => {
308 let off = body
309 .find(" :")
310 .ok_or_else(|| Error::new(ErrorKind::Other, "Malformed USERNOTICE command."))?;
311
312 Message::Usernotice {
313 tags,
314 chan: &body[1..off],
315 msg: &body[(off + 2)..],
316 }
317 }
318 "USERSTATE" => Message::Userstate {
319 tags,
320 chan: &body[1..],
321 },
322 "GLOBALUSERSTATE" => Message::GlobalUserstate { tags },
323 _ => {
324 return Err(Error::new(
325 ErrorKind::Other,
326 "Parsing message command failed.",
327 ))
328 }
329 })
330 }
331}