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::*;
17use crate::message::commands::clearmsg::ClearMsgMessage;
18use crate::message::commands::join::JoinMessage;
19use crate::message::commands::part::PartMessage;
20use crate::message::commands::ping::PingMessage;
21use crate::message::commands::pong::PongMessage;
22use crate::message::commands::reconnect::ReconnectMessage;
23use crate::message::commands::userstate::UserStateMessage;
24use crate::message::prefix::IRCPrefix;
25use crate::message::twitch::{Badge, Emote, RGBColor};
26use crate::message::{
27 AsRawIRC, ClearChatMessage, GlobalUserStateMessage, IRCMessage, NoticeMessage, PrivmsgMessage,
28 RoomStateMessage, UserNoticeMessage, WhisperMessage,
29};
30use chrono::{DateTime, TimeZone, Utc};
31use std::collections::HashSet;
32use std::convert::TryFrom;
33use std::ops::Range;
34use std::str::FromStr;
35use thiserror::Error;
36
37#[cfg(feature = "with-serde")]
38use {serde::Deserialize, serde::Serialize};
39
40#[derive(Error, Debug, PartialEq, Eq)]
43pub enum ServerMessageParseError {
44 #[error("Could not parse IRC message {} as ServerMessage: That command's data is not parsed by this implementation", .0.as_raw_irc())]
50 MismatchedCommand(IRCMessage),
51 #[error("Could not parse IRC message {} as ServerMessage: No tag present under key `{1}`", .0.as_raw_irc())]
53 MissingTag(IRCMessage, &'static str),
54 #[error("Could not parse IRC message {} as ServerMessage: No tag value present under key `{1}`", .0.as_raw_irc())]
56 MissingTagValue(IRCMessage, &'static str),
57 #[error("Could not parse IRC message {} as ServerMessage: Malformed tag value for tag `{1}`, value was `{2}`", .0.as_raw_irc())]
59 MalformedTagValue(IRCMessage, &'static str, String),
60 #[error("Could not parse IRC message {} as ServerMessage: No parameter found at index {1}", .0.as_raw_irc())]
62 MissingParameter(IRCMessage, usize),
63 #[error("Could not parse IRC message {} as ServerMessage: Malformed channel parameter (# must be present + something after it)", .0.as_raw_irc())]
65 MalformedChannel(IRCMessage),
66 #[error("Could not parse IRC message {} as ServerMessage: Malformed parameter at index {1}", .0.as_raw_irc())]
68 MalformedParameter(IRCMessage, usize),
69 #[error("Could not parse IRC message {} as ServerMessage: Missing prefix altogether", .0.as_raw_irc())]
71 MissingPrefix(IRCMessage),
72 #[error("Could not parse IRC message {} as ServerMessage: No nickname found in prefix", .0.as_raw_irc())]
74 MissingNickname(IRCMessage),
75}
76
77impl From<ServerMessageParseError> for IRCMessage {
78 fn from(msg: ServerMessageParseError) -> IRCMessage {
79 match msg {
80 ServerMessageParseError::MismatchedCommand(m) => m,
81 ServerMessageParseError::MissingTag(m, _) => m,
82 ServerMessageParseError::MissingTagValue(m, _) => m,
83 ServerMessageParseError::MalformedTagValue(m, _, _) => m,
84 ServerMessageParseError::MissingParameter(m, _) => m,
85 ServerMessageParseError::MalformedChannel(m) => m,
86 ServerMessageParseError::MalformedParameter(m, _) => m,
87 ServerMessageParseError::MissingPrefix(m) => m,
88 ServerMessageParseError::MissingNickname(m) => m,
89 }
90 }
91}
92
93trait IRCMessageParseExt {
94 fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError>;
95 fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError>;
96 fn try_get_tag_value(&self, key: &'static str)
97 -> Result<Option<&str>, ServerMessageParseError>;
98 fn try_get_nonempty_tag_value(
99 &self,
100 key: &'static str,
101 ) -> Result<&str, ServerMessageParseError>;
102 fn try_get_optional_nonempty_tag_value(
103 &self,
104 key: &'static str,
105 ) -> Result<Option<&str>, ServerMessageParseError>;
106 fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError>;
107 fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError>;
108 fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError>;
109 fn try_get_emotes(
110 &self,
111 tag_key: &'static str,
112 message_text: &str,
113 ) -> Result<Vec<Emote>, ServerMessageParseError>;
114 fn try_get_emote_sets(
115 &self,
116 tag_key: &'static str,
117 ) -> Result<HashSet<String>, ServerMessageParseError>;
118 fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError>;
119 fn try_get_color(
120 &self,
121 tag_key: &'static str,
122 ) -> Result<Option<RGBColor>, ServerMessageParseError>;
123 fn try_get_number<N: FromStr>(
124 &self,
125 tag_key: &'static str,
126 ) -> Result<N, ServerMessageParseError>;
127 fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError>;
128 fn try_get_optional_number<N: FromStr>(
129 &self,
130 tag_key: &'static str,
131 ) -> Result<Option<N>, ServerMessageParseError>;
132 fn try_get_optional_bool(
133 &self,
134 tag_key: &'static str,
135 ) -> Result<Option<bool>, ServerMessageParseError>;
136 fn try_get_timestamp(
137 &self,
138 tag_key: &'static str,
139 ) -> Result<DateTime<Utc>, ServerMessageParseError>;
140}
141
142impl IRCMessageParseExt for IRCMessage {
143 fn try_get_param(&self, index: usize) -> Result<&str, ServerMessageParseError> {
144 Ok(self
145 .params
146 .get(index)
147 .ok_or_else(|| MissingParameter(self.to_owned(), index))?)
148 }
149
150 fn try_get_message_text(&self) -> Result<(&str, bool), ServerMessageParseError> {
151 let mut message_text = self.try_get_param(1)?;
152
153 let is_action =
154 message_text.starts_with("\u{0001}ACTION ") && message_text.ends_with('\u{0001}');
155 if is_action {
156 message_text = &message_text[8..message_text.len() - 1]
158 }
159
160 Ok((message_text, is_action))
161 }
162
163 fn try_get_tag_value(
164 &self,
165 key: &'static str,
166 ) -> Result<Option<&str>, ServerMessageParseError> {
167 match self.tags.0.get(key) {
168 Some(Some(value)) => Ok(Some(value)),
169 Some(None) => Ok(None),
170 None => Err(MissingTag(self.to_owned(), key)),
171 }
172 }
173
174 fn try_get_nonempty_tag_value(
175 &self,
176 key: &'static str,
177 ) -> Result<&str, ServerMessageParseError> {
178 match self.tags.0.get(key) {
179 Some(Some(value)) => Ok(value),
180 Some(None) => Err(MissingTagValue(self.to_owned(), key)),
181 None => Err(MissingTag(self.to_owned(), key)),
182 }
183 }
184
185 fn try_get_optional_nonempty_tag_value(
186 &self,
187 key: &'static str,
188 ) -> Result<Option<&str>, ServerMessageParseError> {
189 match self.tags.0.get(key) {
190 Some(Some(value)) => Ok(Some(value)),
191 Some(None) => Err(MissingTagValue(self.to_owned(), key)),
192 None => Ok(None),
193 }
194 }
195
196 fn try_get_channel_login(&self) -> Result<&str, ServerMessageParseError> {
197 let param = self.try_get_param(0)?;
198
199 if !param.starts_with('#') || param.len() < 2 {
200 return Err(MalformedChannel(self.to_owned()));
201 }
202
203 Ok(¶m[1..])
204 }
205
206 fn try_get_optional_channel_login(&self) -> Result<Option<&str>, ServerMessageParseError> {
207 let param = self.try_get_param(0)?;
208
209 if param == "*" {
210 return Ok(None);
211 }
212
213 if !param.starts_with('#') || param.len() < 2 {
214 return Err(MalformedChannel(self.to_owned()));
215 }
216
217 Ok(Some(¶m[1..]))
218 }
219
220 fn try_get_prefix_nickname(&self) -> Result<&str, ServerMessageParseError> {
222 match &self.prefix {
223 None => Err(MissingPrefix(self.to_owned())),
224 Some(IRCPrefix::HostOnly { host: _ }) => Err(MissingNickname(self.to_owned())),
225 Some(IRCPrefix::Full {
226 nick,
227 user: _,
228 host: _,
229 }) => Ok(nick),
230 }
231 }
232
233 fn try_get_emotes(
234 &self,
235 tag_key: &'static str,
236 message_text: &str,
237 ) -> Result<Vec<Emote>, ServerMessageParseError> {
238 let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
239
240 if tag_value.is_empty() {
241 return Ok(vec![]);
242 }
243
244 let mut emotes = Vec::new();
245
246 let make_error = || MalformedTagValue(self.to_owned(), tag_key, tag_value.to_owned());
247
248 for src in tag_value.split('/') {
251 let (emote_id, indices_src) = src.split_once(':').ok_or_else(make_error)?;
252
253 for range_src in indices_src.split(',') {
254 let (start, end) = range_src.split_once('-').ok_or_else(make_error)?;
255
256 let start = usize::from_str(start).map_err(|_| make_error())?;
257 let end = usize::from_str(end).map_err(|_| make_error())? + 1;
261
262 let code_length = end - start;
263
264 let code = message_text
265 .chars()
266 .skip(start)
267 .take(code_length)
268 .collect::<String>();
269
270 emotes.push(Emote {
275 id: emote_id.to_owned(),
276 char_range: Range { start, end },
277 code,
278 });
279 }
280 }
281
282 emotes.sort_unstable_by_key(|e| e.char_range.start);
283
284 Ok(emotes)
285 }
286
287 fn try_get_emote_sets(
288 &self,
289 tag_key: &'static str,
290 ) -> Result<HashSet<String>, ServerMessageParseError> {
291 let src = self.try_get_nonempty_tag_value(tag_key)?;
292
293 if src.is_empty() {
294 Ok(HashSet::new())
295 } else {
296 Ok(src.split(',').map(|s| s.to_owned()).collect())
297 }
298 }
299
300 fn try_get_badges(&self, tag_key: &'static str) -> Result<Vec<Badge>, ServerMessageParseError> {
301 let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
303
304 if tag_value.is_empty() {
305 return Ok(vec![]);
306 }
307
308 let mut badges = Vec::new();
309
310 let make_error = || MalformedTagValue(self.to_owned(), tag_key, tag_value.to_owned());
311
312 for src in tag_value.split(',') {
315 let (name, version) = src.split_once('/').ok_or_else(make_error)?;
316
317 badges.push(Badge {
318 name: name.to_owned(),
319 version: version.to_owned(),
320 });
321 }
322
323 Ok(badges)
324 }
325
326 fn try_get_color(
327 &self,
328 tag_key: &'static str,
329 ) -> Result<Option<RGBColor>, ServerMessageParseError> {
330 let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
331 let make_error = || MalformedTagValue(self.to_owned(), tag_key, tag_value.to_owned());
332
333 if tag_value.is_empty() {
334 return Ok(None);
335 }
336
337 if tag_value.len() != 7 {
339 return Err(make_error());
340 }
341
342 Ok(Some(RGBColor {
343 r: u8::from_str_radix(&tag_value[1..3], 16).map_err(|_| make_error())?,
344 g: u8::from_str_radix(&tag_value[3..5], 16).map_err(|_| make_error())?,
345 b: u8::from_str_radix(&tag_value[5..7], 16).map_err(|_| make_error())?,
346 }))
347 }
348
349 fn try_get_number<N: FromStr>(
350 &self,
351 tag_key: &'static str,
352 ) -> Result<N, ServerMessageParseError> {
353 let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
354 let number = N::from_str(tag_value)
355 .map_err(|_| MalformedTagValue(self.to_owned(), tag_key, tag_value.to_owned()))?;
356 Ok(number)
357 }
358
359 fn try_get_bool(&self, tag_key: &'static str) -> Result<bool, ServerMessageParseError> {
360 Ok(self.try_get_number::<u8>(tag_key)? > 0)
361 }
362
363 fn try_get_optional_number<N: FromStr>(
364 &self,
365 tag_key: &'static str,
366 ) -> Result<Option<N>, ServerMessageParseError> {
367 let tag_value = match self.tags.0.get(tag_key) {
368 Some(Some(value)) => value,
369 Some(None) => return Err(MissingTagValue(self.to_owned(), tag_key)),
370 None => return Ok(None),
371 };
372
373 let number = N::from_str(tag_value)
374 .map_err(|_| MalformedTagValue(self.to_owned(), tag_key, tag_value.to_owned()))?;
375 Ok(Some(number))
376 }
377
378 fn try_get_optional_bool(
379 &self,
380 tag_key: &'static str,
381 ) -> Result<Option<bool>, ServerMessageParseError> {
382 Ok(self.try_get_optional_number::<u8>(tag_key)?.map(|n| n > 0))
383 }
384
385 fn try_get_timestamp(
386 &self,
387 tag_key: &'static str,
388 ) -> Result<DateTime<Utc>, ServerMessageParseError> {
389 let tag_value = self.try_get_nonempty_tag_value(tag_key)?;
391 let milliseconds_since_epoch = i64::from_str(tag_value)
392 .map_err(|_| MalformedTagValue(self.to_owned(), tag_key, tag_value.to_owned()))?;
393 Utc.timestamp_millis_opt(milliseconds_since_epoch)
394 .single()
395 .ok_or_else(|| MalformedTagValue(self.to_owned(), tag_key, tag_value.to_owned()))
396 }
397}
398
399#[derive(Debug, PartialEq, Eq, Clone)]
406#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
407#[doc(hidden)]
408pub struct HiddenIRCMessage(pub(self) IRCMessage);
409
410#[derive(Debug, Clone)]
444#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
445#[non_exhaustive]
446pub enum ServerMessage {
447 ClearChat(ClearChatMessage),
449 ClearMsg(ClearMsgMessage),
451 GlobalUserState(GlobalUserStateMessage),
453 Join(JoinMessage),
455 Notice(NoticeMessage),
457 Part(PartMessage),
459 Ping(PingMessage),
461 Pong(PongMessage),
463 Privmsg(PrivmsgMessage),
465 Reconnect(ReconnectMessage),
467 RoomState(RoomStateMessage),
469 UserNotice(UserNoticeMessage),
471 UserState(UserStateMessage),
473 Whisper(WhisperMessage),
475 #[doc(hidden)]
476 Generic(HiddenIRCMessage),
477}
478
479impl TryFrom<IRCMessage> for ServerMessage {
480 type Error = ServerMessageParseError;
481
482 fn try_from(source: IRCMessage) -> Result<ServerMessage, ServerMessageParseError> {
483 use ServerMessage::*;
484
485 Ok(match source.command.as_str() {
486 "CLEARCHAT" => ClearChat(ClearChatMessage::try_from(source)?),
487 "CLEARMSG" => ClearMsg(ClearMsgMessage::try_from(source)?),
488 "GLOBALUSERSTATE" => GlobalUserState(GlobalUserStateMessage::try_from(source)?),
489 "JOIN" => Join(JoinMessage::try_from(source)?),
490 "NOTICE" => Notice(NoticeMessage::try_from(source)?),
491 "PART" => Part(PartMessage::try_from(source)?),
492 "PING" => Ping(PingMessage::try_from(source)?),
493 "PONG" => Pong(PongMessage::try_from(source)?),
494 "PRIVMSG" => Privmsg(PrivmsgMessage::try_from(source)?),
495 "RECONNECT" => Reconnect(ReconnectMessage::try_from(source)?),
496 "ROOMSTATE" => RoomState(RoomStateMessage::try_from(source)?),
497 "USERNOTICE" => UserNotice(UserNoticeMessage::try_from(source)?),
498 "USERSTATE" => UserState(UserStateMessage::try_from(source)?),
499 "WHISPER" => Whisper(WhisperMessage::try_from(source)?),
500 _ => Generic(HiddenIRCMessage(source)),
501 })
502 }
503}
504
505impl From<ServerMessage> for IRCMessage {
506 fn from(msg: ServerMessage) -> IRCMessage {
507 match msg {
508 ServerMessage::ClearChat(msg) => msg.source,
509 ServerMessage::ClearMsg(msg) => msg.source,
510 ServerMessage::GlobalUserState(msg) => msg.source,
511 ServerMessage::Join(msg) => msg.source,
512 ServerMessage::Notice(msg) => msg.source,
513 ServerMessage::Part(msg) => msg.source,
514 ServerMessage::Ping(msg) => msg.source,
515 ServerMessage::Pong(msg) => msg.source,
516 ServerMessage::Privmsg(msg) => msg.source,
517 ServerMessage::Reconnect(msg) => msg.source,
518 ServerMessage::RoomState(msg) => msg.source,
519 ServerMessage::UserNotice(msg) => msg.source,
520 ServerMessage::UserState(msg) => msg.source,
521 ServerMessage::Whisper(msg) => msg.source,
522 ServerMessage::Generic(msg) => msg.0,
523 }
524 }
525}
526
527impl ServerMessage {
529 pub fn source(&self) -> &IRCMessage {
531 match self {
532 ServerMessage::ClearChat(msg) => &msg.source,
533 ServerMessage::ClearMsg(msg) => &msg.source,
534 ServerMessage::GlobalUserState(msg) => &msg.source,
535 ServerMessage::Join(msg) => &msg.source,
536 ServerMessage::Notice(msg) => &msg.source,
537 ServerMessage::Part(msg) => &msg.source,
538 ServerMessage::Ping(msg) => &msg.source,
539 ServerMessage::Pong(msg) => &msg.source,
540 ServerMessage::Privmsg(msg) => &msg.source,
541 ServerMessage::Reconnect(msg) => &msg.source,
542 ServerMessage::RoomState(msg) => &msg.source,
543 ServerMessage::UserNotice(msg) => &msg.source,
544 ServerMessage::UserState(msg) => &msg.source,
545 ServerMessage::Whisper(msg) => &msg.source,
546 ServerMessage::Generic(msg) => &msg.0,
547 }
548 }
549
550 pub(crate) fn new_generic(message: IRCMessage) -> ServerMessage {
551 ServerMessage::Generic(HiddenIRCMessage(message))
552 }
553}
554
555impl AsRawIRC for ServerMessage {
556 fn format_as_raw_irc(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 self.source().format_as_raw_irc(f)
558 }
559}