1pub mod clearchat;
2pub mod clearmsg;
3pub mod globaluserstate;
4pub mod join;
5pub mod notice;
6pub mod part;
7pub mod ping;
8pub mod pong;
9pub mod privmsg;
10pub mod reconnect;
11pub mod roomstate;
12pub mod usernotice;
13pub mod userstate;
14pub mod whisper;
15
16use self::ServerMessageParseError::{
17 MalformedChannel, MalformedTagValue, MissingNickname, MissingParameter, MissingPrefix,
18 MissingTag, MissingTagValue,
19};
20use crate::message::commands::clearmsg::ClearMsgMessage;
21use crate::message::commands::join::JoinMessage;
22use crate::message::commands::part::PartMessage;
23use crate::message::commands::ping::PingMessage;
24use crate::message::commands::pong::PongMessage;
25use crate::message::commands::reconnect::ReconnectMessage;
26use crate::message::commands::userstate::UserStateMessage;
27use crate::message::prefix::IRCPrefix;
28use crate::message::twitch::{Badge, Emote, RGBColor};
29use crate::message::{
30 AsRawIRC, ClearChatMessage, GlobalUserStateMessage, IRCMessage, NoticeMessage, PrivmsgMessage,
31 ReplyParent, RoomStateMessage, TwitchUserBasics, UserNoticeMessage, WhisperMessage,
32};
33use chrono::{DateTime, TimeZone, Utc};
34use std::collections::HashSet;
35use std::convert::TryFrom;
36use std::ops::Range;
37use std::str::FromStr;
38use thiserror::Error;
39
40#[cfg(feature = "with-serde")]
41use {serde::Deserialize, serde::Serialize};
42
43#[derive(Error, Debug, PartialEq, Eq)]
46pub enum ServerMessageParseError {
47 #[error("Could not parse IRC message {} as ServerMessage: That command's data is not parsed by this implementation", .0.as_raw_irc())]
53 MismatchedCommand(Box<IRCMessage>),
54 #[error(
56 "Could not parse IRC message {msg} as ServerMessage: No tag present under key `{key}`",
57 msg = .0.as_raw_irc(),
58 key = .1,
59 )]
60 MissingTag(Box<IRCMessage>, &'static str),
61 #[error(
63 "Could not parse IRC message {msg} as ServerMessage: No tag value present under key `{key}`",
64 msg = .0.as_raw_irc(),
65 key = .1,
66 )]
67 MissingTagValue(Box<IRCMessage>, &'static str),
68 #[error(
70 "Could not parse IRC message {msg} as ServerMessage: Malformed tag value for tag `{key}`, value was `{value}`",
71 msg = .0.as_raw_irc(),
72 key = .1,
73 value = .2,
74 )]
75 MalformedTagValue(Box<IRCMessage>, &'static str, String),
76 #[error(
78 "Could not parse IRC message {msg} as ServerMessage: No parameter found at index {key}",
79 msg = .0.as_raw_irc(),
80 key = .1,
81 )]
82 MissingParameter(Box<IRCMessage>, usize),
83 #[error(
85 "Could not parse IRC message {msg} as ServerMessage: Malformed channel parameter (# must be present + something after it)",
86 msg = .0.as_raw_irc(),
87 )]
88 MalformedChannel(Box<IRCMessage>),
89 #[error(
91 "Could not parse IRC message {msg} as ServerMessage: Malformed parameter at index {idx}",
92 msg = .0.as_raw_irc(),
93 idx = .1,
94 )]
95 MalformedParameter(Box<IRCMessage>, usize),
96 #[error(
98 "Could not parse IRC message {msg} as ServerMessage: Missing prefix altogether",
99 msg = .0.as_raw_irc(),
100 )]
101 MissingPrefix(Box<IRCMessage>),
102 #[error(
104 "Could not parse IRC message {msg} as ServerMessage: No nickname found in prefix",
105 msg = .0.as_raw_irc(),
106 )]
107 MissingNickname(Box<IRCMessage>),
108}
109
110impl From<ServerMessageParseError> for IRCMessage {
111 fn from(msg: ServerMessageParseError) -> IRCMessage {
112 match msg {
113 ServerMessageParseError::MismatchedCommand(m)
114 | ServerMessageParseError::MissingTag(m, _)
115 | ServerMessageParseError::MissingTagValue(m, _)
116 | ServerMessageParseError::MalformedTagValue(m, _, _)
117 | ServerMessageParseError::MissingParameter(m, _)
118 | ServerMessageParseError::MalformedChannel(m)
119 | ServerMessageParseError::MalformedParameter(m, _)
120 | ServerMessageParseError::MissingPrefix(m)
121 | ServerMessageParseError::MissingNickname(m) => *m,
122 }
123 }
124}
125
126trait IRCMessageParseExt {
127 fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError>;
128 fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError>;
129 fn try_get_tag_value(&self, key: &'static str) -> Result<&str, ServerMessageParseError>;
130 fn try_get_nonempty_tag_value(
131 &self,
132 key: &'static str,
133 ) -> Result<&str, ServerMessageParseError>;
134 fn try_get_optional_nonempty_tag_value(
135 &self,
136 key: &'static str,
137 ) -> Result<Option<&str>, ServerMessageParseError>;
138 fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError>;
139 fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError>;
140 fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError>;
141 fn try_get_emotes(
142 &self,
143 tag_key: &'static str,
144 message_text: &str,
145 ) -> Result<Vec<Emote>, ServerMessageParseError>;
146 fn try_get_emote_sets(
147 &self,
148 tag_key: &'static str,
149 ) -> Result<HashSet<String>, ServerMessageParseError>;
150 fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError>;
151 fn try_get_color(
152 &self,
153 tag_key: &'static str,
154 ) -> Result<Option<RGBColor>, ServerMessageParseError>;
155 fn try_get_number<N: FromStr>(
156 &self,
157 tag_key: &'static str,
158 ) -> Result<N, ServerMessageParseError>;
159 fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError>;
160 fn try_get_optional_number<N: FromStr>(
161 &self,
162 tag_key: &'static str,
163 ) -> Result<Option<N>, ServerMessageParseError>;
164 fn try_get_optional_bool(
165 &self,
166 tag_key: &'static str,
167 ) -> Result<Option<bool>, ServerMessageParseError>;
168 fn try_get_timestamp(
169 &self,
170 tag_key: &'static str,
171 ) -> Result<DateTime<Utc>, ServerMessageParseError>;
172 fn try_get_optional_reply_parent(&self)
173 -> Result<Option<ReplyParent>, ServerMessageParseError>;
174}
175
176impl IRCMessageParseExt for IRCMessage {
177 fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError> {
178 Ok(self
179 .params
180 .get(index)
181 .ok_or_else(|| MissingParameter(Box::new(self.to_owned()), index))?)
182 }
183
184 fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError> {
185 let mut message_text = self.try_get_param(1)?;
186
187 let is_action =
188 message_text.starts_with("\u{0001}ACTION ") && message_text.ends_with('\u{0001}');
189 if is_action {
190 message_text = &message_text[8..message_text.len() - 1];
192 }
193
194 Ok((message_text, is_action))
195 }
196
197 fn try_get_tag_value(&self, key: &'static str) -> Result<&str, ServerMessageParseError> {
198 match self.tags.0.get(key) {
199 Some(value) => Ok(value),
200 None => Err(MissingTag(Box::new(self.to_owned()), key)),
201 }
202 }
203
204 fn try_get_nonempty_tag_value(
205 &self,
206 key: &'static str,
207 ) -> Result<&str, ServerMessageParseError> {
208 match self.tags.0.get(key) {
209 Some(value) => match value.as_str() {
210 "" => Err(MissingTagValue(Box::new(self.to_owned()), key)),
211 otherwise => Ok(otherwise),
212 },
213 None => Err(MissingTag(Box::new(self.to_owned()), key)),
214 }
215 }
216
217 fn try_get_optional_nonempty_tag_value(
218 &self,
219 key: &'static str,
220 ) -> Result<Option<&str>, ServerMessageParseError> {
221 match self.tags.0.get(key) {
222 Some(value) => match value.as_str() {
223 "" => Err(MissingTagValue(Box::new(self.to_owned()), key)),
224 otherwise => Ok(Some(otherwise)),
225 },
226 None => Ok(None),
227 }
228 }
229
230 fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError> {
231 let param = self.try_get_param(0)?;
232
233 if !param.starts_with('#') || param.len() < 2 {
234 return Err(MalformedChannel(Box::new(self.to_owned())));
235 }
236
237 Ok(¶m[1..])
238 }
239
240 fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError> {
241 let param = self.try_get_param(0)?;
242
243 if param == "*" {
244 return Ok(None);
245 }
246
247 if !param.starts_with('#') || param.len() < 2 {
248 return Err(MalformedChannel(Box::new(self.to_owned())));
249 }
250
251 Ok(Some(¶m[1..]))
252 }
253
254 fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError> {
256 match &self.prefix {
257 None => Err(MissingPrefix(Box::new(self.to_owned()))),
258 Some(IRCPrefix::HostOnly { host: _ }) => {
259 Err(MissingNickname(Box::new(self.to_owned())))
260 }
261 Some(IRCPrefix::Full {
262 nick,
263 user: _,
264 host: _,
265 }) => Ok(nick),
266 }
267 }
268
269 fn try_get_emotes(
270 &self,
271 tag_key: &'static str,
272 message_text: &str,
273 ) -> Result<Vec<Emote>, ServerMessageParseError> {
274 let tag_value = self.try_get_tag_value(tag_key)?;
275
276 if tag_value.is_empty() {
277 return Ok(vec![]);
278 }
279
280 let mut emotes = Vec::new();
281
282 let make_error =
283 || MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
284
285 for src in tag_value.split('/') {
288 let (emote_id, indices_src) = src.split_once(':').ok_or_else(make_error)?;
289
290 for range_src in indices_src.split(',') {
291 let (start, end) = range_src.split_once('-').ok_or_else(make_error)?;
292
293 let start = usize::from_str(start).map_err(|_| make_error())?;
294 let end = usize::from_str(end).map_err(|_| make_error())? + 1;
298
299 let code_length = end - start;
300
301 let code = message_text
302 .chars()
303 .skip(start)
304 .take(code_length)
305 .collect::<String>();
306
307 emotes.push(Emote {
312 id: emote_id.to_owned(),
313 char_range: Range { start, end },
314 code,
315 });
316 }
317 }
318
319 emotes.sort_unstable_by_key(|e| e.char_range.start);
320
321 Ok(emotes)
322 }
323
324 fn try_get_emote_sets(
325 &self,
326 tag_key: &'static str,
327 ) -> Result<HashSet<String>, ServerMessageParseError> {
328 let src = self.try_get_tag_value(tag_key)?;
329
330 if src.is_empty() {
331 Ok(HashSet::new())
332 } else {
333 Ok(src.split(',').map(|s| s.to_owned()).collect())
334 }
335 }
336
337 fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError> {
338 let tag_value = self.try_get_tag_value(tag_key)?;
340
341 if tag_value.is_empty() {
342 return Ok(vec![]);
343 }
344
345 let mut badges = Vec::new();
346
347 let make_error =
348 || MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
349
350 for src in tag_value.split(',') {
353 let (name, version) = src.split_once('/').ok_or_else(make_error)?;
354
355 badges.push(Badge {
356 name: name.to_owned(),
357 version: version.to_owned(),
358 });
359 }
360
361 Ok(badges)
362 }
363
364 fn try_get_color(
365 &self,
366 tag_key: &'static str,
367 ) -> Result<Option<RGBColor>, ServerMessageParseError> {
368 let tag_value = self.try_get_tag_value(tag_key)?;
369 let make_error =
370 || MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned());
371
372 if tag_value.is_empty() {
373 return Ok(None);
374 }
375
376 if tag_value.len() != 7 {
378 return Err(make_error());
379 }
380
381 Ok(Some(RGBColor {
382 r: u8::from_str_radix(&tag_value[1..3], 16).map_err(|_| make_error())?,
383 g: u8::from_str_radix(&tag_value[3..5], 16).map_err(|_| make_error())?,
384 b: u8::from_str_radix(&tag_value[5..7], 16).map_err(|_| make_error())?,
385 }))
386 }
387
388 fn try_get_number<N: FromStr>(
389 &self,
390 tag_key: &'static str,
391 ) -> Result<N, ServerMessageParseError> {
392 let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
393 let number = N::from_str(tag_value).map_err(|_| {
394 MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
395 })?;
396 Ok(number)
397 }
398
399 fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError> {
400 Ok(self.try_get_number::<u8>(tag_key)? > 0)
401 }
402
403 fn try_get_optional_number<N: FromStr>(
404 &self,
405 tag_key: &'static str,
406 ) -> Result<Option<N>, ServerMessageParseError> {
407 let tag_value = match self.tags.0.get(tag_key) {
408 Some(value) => match value.as_str() {
409 "" => return Err(MissingTagValue(Box::new(self.to_owned()), tag_key)),
410 otherwise => otherwise,
411 },
412 None => return Ok(None),
413 };
414
415 let number = N::from_str(tag_value).map_err(|_| {
416 MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
417 })?;
418 Ok(Some(number))
419 }
420
421 fn try_get_optional_bool(
422 &self,
423 tag_key: &'static str,
424 ) -> Result<Option<bool>, ServerMessageParseError> {
425 Ok(self.try_get_optional_number::<u8>(tag_key)?.map(|n| n > 0))
426 }
427
428 fn try_get_timestamp(
429 &self,
430 tag_key: &'static str,
431 ) -> Result<DateTime<Utc>, ServerMessageParseError> {
432 let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
434 let milliseconds_since_epoch = i64::from_str(tag_value).map_err(|_| {
435 MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
436 })?;
437 Utc.timestamp_millis_opt(milliseconds_since_epoch)
438 .single()
439 .ok_or_else(|| {
440 MalformedTagValue(Box::new(self.to_owned()), tag_key, tag_value.to_owned())
441 })
442 }
443
444 fn try_get_optional_reply_parent(
445 &self,
446 ) -> Result<Option<ReplyParent>, ServerMessageParseError> {
447 if !self.tags.0.contains_key("reply-parent-msg-id") {
449 return Ok(None);
450 }
451
452 Ok(Some(ReplyParent {
453 message_id: self.try_get_tag_value("reply-parent-msg-id")?.to_owned(),
454 reply_parent_user: TwitchUserBasics {
455 id: self
456 .try_get_nonempty_tag_value("reply-parent-user-id")?
457 .to_owned(),
458 login: self
459 .try_get_nonempty_tag_value("reply-parent-user-login")?
460 .to_owned(),
461 name: self
462 .try_get_nonempty_tag_value("reply-parent-display-name")?
463 .to_owned(),
464 },
465 message_text: self.try_get_tag_value("reply-parent-msg-body")?.to_owned(),
466 }))
467 }
468}
469
470#[derive(Debug, PartialEq, Eq, Clone)]
477#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
478#[doc(hidden)]
479pub struct HiddenIRCMessage(pub(self) IRCMessage);
480
481#[derive(Debug, Clone)]
515#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
516#[non_exhaustive]
517pub enum ServerMessage {
518 ClearChat(ClearChatMessage),
520 ClearMsg(ClearMsgMessage),
522 GlobalUserState(GlobalUserStateMessage),
524 Join(JoinMessage),
526 Notice(NoticeMessage),
528 Part(PartMessage),
530 Ping(PingMessage),
532 Pong(PongMessage),
534 Privmsg(PrivmsgMessage),
536 Reconnect(ReconnectMessage),
538 RoomState(RoomStateMessage),
540 UserNotice(UserNoticeMessage),
542 UserState(UserStateMessage),
544 Whisper(WhisperMessage),
546 #[doc(hidden)]
547 Generic(HiddenIRCMessage),
548}
549
550impl TryFrom<IRCMessage> for ServerMessage {
551 type Error = ServerMessageParseError;
552
553 fn try_from(source: IRCMessage) -> Result<ServerMessage, ServerMessageParseError> {
554 use ServerMessage::{
555 ClearChat, ClearMsg, Generic, GlobalUserState, Join, Notice, Part, Ping, Pong, Privmsg,
556 Reconnect, RoomState, UserNotice, UserState, Whisper,
557 };
558
559 Ok(match source.command.as_str() {
560 "CLEARCHAT" => ClearChat(ClearChatMessage::try_from(source)?),
561 "CLEARMSG" => ClearMsg(ClearMsgMessage::try_from(source)?),
562 "GLOBALUSERSTATE" => GlobalUserState(GlobalUserStateMessage::try_from(source)?),
563 "JOIN" => Join(JoinMessage::try_from(source)?),
564 "NOTICE" => Notice(NoticeMessage::try_from(source)?),
565 "PART" => Part(PartMessage::try_from(source)?),
566 "PING" => Ping(PingMessage::try_from(source)?),
567 "PONG" => Pong(PongMessage::try_from(source)?),
568 "PRIVMSG" => Privmsg(PrivmsgMessage::try_from(source)?),
569 "RECONNECT" => Reconnect(ReconnectMessage::try_from(source)?),
570 "ROOMSTATE" => RoomState(RoomStateMessage::try_from(source)?),
571 "USERNOTICE" => UserNotice(UserNoticeMessage::try_from(source)?),
572 "USERSTATE" => UserState(UserStateMessage::try_from(source)?),
573 "WHISPER" => Whisper(WhisperMessage::try_from(source)?),
574 _ => Generic(HiddenIRCMessage(source)),
575 })
576 }
577}
578
579impl From<ServerMessage> for IRCMessage {
580 fn from(msg: ServerMessage) -> IRCMessage {
581 match msg {
582 ServerMessage::ClearChat(msg) => msg.source,
583 ServerMessage::ClearMsg(msg) => msg.source,
584 ServerMessage::GlobalUserState(msg) => msg.source,
585 ServerMessage::Join(msg) => msg.source,
586 ServerMessage::Notice(msg) => msg.source,
587 ServerMessage::Part(msg) => msg.source,
588 ServerMessage::Ping(msg) => msg.source,
589 ServerMessage::Pong(msg) => msg.source,
590 ServerMessage::Privmsg(msg) => msg.source,
591 ServerMessage::Reconnect(msg) => msg.source,
592 ServerMessage::RoomState(msg) => msg.source,
593 ServerMessage::UserNotice(msg) => msg.source,
594 ServerMessage::UserState(msg) => msg.source,
595 ServerMessage::Whisper(msg) => msg.source,
596 ServerMessage::Generic(msg) => msg.0,
597 }
598 }
599}
600
601impl ServerMessage {
603 #[must_use]
605 pub fn source(&self) -> &IRCMessage {
606 match self {
607 ServerMessage::ClearChat(msg) => &msg.source,
608 ServerMessage::ClearMsg(msg) => &msg.source,
609 ServerMessage::GlobalUserState(msg) => &msg.source,
610 ServerMessage::Join(msg) => &msg.source,
611 ServerMessage::Notice(msg) => &msg.source,
612 ServerMessage::Part(msg) => &msg.source,
613 ServerMessage::Ping(msg) => &msg.source,
614 ServerMessage::Pong(msg) => &msg.source,
615 ServerMessage::Privmsg(msg) => &msg.source,
616 ServerMessage::Reconnect(msg) => &msg.source,
617 ServerMessage::RoomState(msg) => &msg.source,
618 ServerMessage::UserNotice(msg) => &msg.source,
619 ServerMessage::UserState(msg) => &msg.source,
620 ServerMessage::Whisper(msg) => &msg.source,
621 ServerMessage::Generic(msg) => &msg.0,
622 }
623 }
624
625 pub(crate) fn new_generic(message: IRCMessage) -> ServerMessage {
626 ServerMessage::Generic(HiddenIRCMessage(message))
627 }
628}
629
630impl AsRawIRC for ServerMessage {
631 fn format_as_raw_irc(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
632 self.source().format_as_raw_irc(f)
633 }
634}