Skip to main content

twitch_irc/message/commands/
usernotice.rs

1use crate::message::commands::IRCMessageParseExt;
2use crate::message::twitch::{Badge, Emote, RGBColor, TwitchUserBasics};
3use crate::message::{IRCMessage, ServerMessageParseError};
4use chrono::{DateTime, Utc};
5use std::convert::TryFrom;
6
7#[cfg(feature = "with-serde")]
8use {serde::Deserialize, serde::Serialize};
9
10/// A Twitch `USERNOTICE` message.
11///
12/// The `USERNOTICE` message represents a wide variety of "rich events" in chat,
13/// e.g. sub events, resubs, gifted subscriptions, incoming raids, etc.
14///
15/// See `UserNoticeEvent` for more details on all the different events.
16///
17/// Note that even though `UserNoticeMessage` has a `message_id`, you can NOT reply to these
18/// messages or delete them. For this reason,
19/// [`ReplyToMessage`](crate::message::ReplyToMessage) is not
20/// implemented for `UserNoticeMessage`.
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
23pub struct UserNoticeMessage {
24    /// Login name of the channel that this message was sent to.
25    pub channel_login: String,
26    /// ID of the channel that this message was sent to.
27    pub channel_id: String,
28
29    /// The user that sent/triggered this message. Depending on the `event` (see below),
30    /// this user may or may not have any actual meaning (for some type of events, this
31    /// user is a dummy user).
32    ///
33    /// Even if this user is not a dummy user, the meaning of what this user did depends on the
34    /// `event` that this `USERNOTICE` message represents. For example, in case of a raid,
35    /// this user is the user raiding the channel, in case of a `sub`, it's the user
36    /// subscribing, etc...)
37    pub sender: TwitchUserBasics,
38
39    /// If present, an optional message the user sent alongside the notification. Not all types
40    /// of events can have message text.
41    ///
42    /// Currently the only event that can a message is a `resub`, where this message text is the
43    /// message the user shared with the streamer alongside the resub message.
44    pub message_text: Option<String>,
45    /// A system message. If present, represents a user-presentable message
46    /// of what this event is, for example "FuchsGewand subscribed with Twitch Prime.
47    /// They've subscribed for 12 months, currently on a 9 month streak!".
48    ///
49    /// This is an empty string for some message types such as announcements.
50    pub system_message: String,
51
52    /// this holds the event-specific data, e.g. for sub, resub, subgift, etc...
53    pub event: UserNoticeEvent,
54
55    /// String identifying the type of event (`msg-id` tag). Can be used to manually parse
56    /// undocumented types of `USERNOTICE` messages.
57    pub event_id: String,
58
59    /// Metadata related to the chat badges in the `badges` tag.
60    ///
61    /// Currently this is used only for `subscriber`, to indicate the exact number of months
62    /// the user has been a subscriber. This number is finer grained than the version number in
63    /// badges. For example, a user who has been a subscriber for 45 months would have a
64    /// `badge_info` value of 45 but might have a `badges` `version` number for only 3 years.
65    pub badge_info: Vec<Badge>,
66    /// List of badges that should be displayed alongside the message.
67    pub badges: Vec<Badge>,
68    /// A list of emotes in this message. Each emote replaces a part of the `message_text`.
69    /// These emotes are sorted in the order that they appear in the message.
70    ///
71    /// If `message_text` is `None`, this is an empty list and carries no information (since
72    /// there is no message, and therefore no emotes to display)
73    pub emotes: Vec<Emote>,
74
75    /// If present, specifies the color that the user's name should be displayed in. A value
76    /// of `None` here signifies that the user has not picked any particular color.
77    /// Implementations differ on how they handle this, on the Twitch website users are assigned
78    /// a pseudorandom but consistent-per-user color if they have no color specified.
79    pub name_color: Option<RGBColor>,
80
81    /// A string uniquely identifying this message. Can be used with the Twitch API to
82    /// delete single messages. See also the `CLEARMSG` message type.
83    pub message_id: String,
84
85    /// Timestamp of when this message was sent.
86    pub server_timestamp: DateTime<Utc>,
87
88    /// The message that this `UserNoticeMessage` was parsed from.
89    pub source: IRCMessage,
90}
91
92/// Additionally present on `giftpaidupgrade` and `anongiftpaidupgrade` messages
93/// if the upgrade happens as part of a seasonal promotion on Twitch, e.g. Subtember
94/// or similar.
95#[derive(Debug, Clone, PartialEq, Eq)]
96#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
97pub struct SubGiftPromo {
98    /// Total number of subs gifted during this promotion
99    pub total_gifts: u64,
100    /// Friendly name of the promotion, e.g. `Subtember 2018`
101    pub promo_name: String,
102}
103
104impl SubGiftPromo {
105    fn parse_if_present(
106        source: &IRCMessage,
107    ) -> Result<Option<SubGiftPromo>, ServerMessageParseError> {
108        if let (Some(total_gifts), Some(promo_name)) = (
109            source.try_get_optional_number("msg-param-promo-gift-total")?,
110            source
111                .try_get_optional_nonempty_tag_value("msg-param-promo-name")?
112                .map(|s| s.to_owned()),
113        ) {
114            Ok(Some(SubGiftPromo {
115                total_gifts,
116                promo_name,
117            }))
118        } else {
119            Ok(None)
120        }
121    }
122}
123
124/// A type of event that a `UserNoticeMessage` represents.
125///
126/// The `USERNOTICE` command is used for a wide variety of different "rich events" on
127/// the Twitch platform. This enum provides parsed variants for a variety of documented
128/// type of events.
129///
130/// However Twitch has been known to often add new events without prior notice or even
131/// documenting them. For this reason, one should never expect this list to be exhaustive.
132/// All events that don't have a more concrete representation inside this enum get parsed
133/// as a `UserNoticeEvent::Unknown` (which is hidden from the documentation on purpose):
134/// You should always use the `_` rest-branch and `event_id` when manually parsing other events.
135///
136/// ```rust
137/// # use twitch_irc::message::{UserNoticeMessage, UserNoticeEvent, IRCMessage};
138/// # use std::convert::TryFrom;
139/// let message = UserNoticeMessage::try_from(IRCMessage::parse("@badge-info=subscriber/2;badges=subscriber/2,bits/1000;color=#FF4500;display-name=whoopiix;emotes=;flags=;id=d2b32a02-3071-4c52-b2ce-bc3716acdc44;login=whoopiix;mod=0;msg-id=bitsbadgetier;msg-param-threshold=1000;room-id=71092938;subscriber=1;system-msg=bits\\sbadge\\stier\\snotification;tmi-sent-ts=1594520403813;user-id=104252055;user-type= :tmi.twitch.tv USERNOTICE #xqcow").unwrap()).unwrap();
140/// match &message.event {
141///     UserNoticeEvent::BitsBadgeTier { threshold } => println!("{} just unlocked the {} bits badge!", message.sender.name, threshold),
142///     _ => println!("some other type of event: {}", message.event_id)
143/// }
144/// ```
145///
146/// This enum is also marked as `#[non_exhaustive]` to signify that more events may be
147/// added to it in the future, without the need for a breaking release.
148#[non_exhaustive]
149#[derive(Debug, Clone, PartialEq, Eq)]
150#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
151pub enum UserNoticeEvent {
152    /// Emitted when a user subscribes or resubscribes to a channel.
153    /// The user sending this `USERNOTICE` is the user subscribing/resubscribing.
154    ///
155    /// For brevity this event captures both `sub` and `resub` events because they both
156    /// carry the exact same parameters. You can differentiate between the two events using
157    /// `is_resub`, which is false for `sub` and true for `resub` events.
158    SubOrResub {
159        /// Indicates whether this is a first-time sub or a resub.
160        is_resub: bool,
161        /// Cumulative number of months the sending user has subscribed to this channel.
162        cumulative_months: u64,
163        /// Consecutive number of months the sending user has subscribed to this channel.
164        streak_months: Option<u64>,
165        /// `Prime`, `1000`, `2000` or `3000`, referring to Prime or tier 1, 2 or 3 subs respectively.
166        sub_plan: String,
167        /// A name the broadcaster configured for this sub plan, e.g. `The Ninjas` or
168        /// `Channel subscription (nymn_hs)`
169        sub_plan_name: String,
170    },
171
172    /// Incoming raid to a channel.
173    /// The user sending this `USERNOTICE` message is the user raiding this channel.
174    Raid {
175        /// How many viewers participated in the raid and just raided this channel.
176        viewer_count: u64,
177        /// A link to the profile image of the raiding user. This is not officially documented
178        /// Empirical evidence suggests this is always the 70x70 version of the full profile
179        /// picture.
180        ///
181        /// E.g. `https://static-cdn.jtvnw.net/jtv_user_pictures/cae3ca63-510d-4715-b4ce-059dcf938978-profile_image-70x70.png`
182        profile_image_url: String,
183    },
184
185    /// Indicates a gifted subscription.
186    ///
187    /// This event combines `subgift` and `anonsubgift`. In case of
188    /// `anonsubgift` the sending user of the `USERNOTICE` carries no useful information,
189    /// it can be e.g. the channel owner or a service user like `AnAnonymousGifter`. You should
190    /// always check for `is_sender_anonymous` before using the sender of the `USERNOTICE`.
191    SubGift {
192        /// Indicates whether the user sending this `USERNOTICE` is a dummy or a real gifter.
193        /// If this is `true` the gift comes from an anonymous user, and the user sending the
194        /// `USERNOTICE` carries no useful information and should be ignored.
195        is_sender_anonymous: bool,
196        /// Cumulative number of months the recipient has subscribed to this channel.
197        cumulative_months: u64,
198        /// The user that received this gifted subscription or resubscription.
199        recipient: TwitchUserBasics,
200        /// `1000`, `2000` or `3000`, referring to tier 1, 2 or 3 subs respectively.
201        sub_plan: String,
202        /// A name the broadcaster configured for this sub plan, e.g. `The Ninjas` or
203        /// `Channel subscription (nymn_hs)`
204        sub_plan_name: String,
205        /// number of months in a single multi-month gift.
206        num_gifted_months: u64,
207    },
208
209    /// This event precedes a wave of `subgift`/`anonsubgift` messages.
210    /// (`<User> is gifting <mass_gift_count> Tier 1 Subs to <Channel>'s community! They've gifted a total of <sender_total_gifts> in the channel!`)
211    SubMysteryGift {
212        /// Number of gifts the sender just gifted.
213        mass_gift_count: u64,
214        /// Total number of gifts the sender has gifted in this channel. This includes the
215        /// number of gifts in this `submysterygift` or `anonsubmysterygift`.
216        /// Present in most case, but notably missing when Twitch match gifted subs during SUBtember.
217        sender_total_gifts: Option<u64>,
218        /// The type of sub plan the recipients were gifted.
219        /// `1000`, `2000` or `3000`, referring to tier 1, 2 or 3 subs respectively.
220        sub_plan: String,
221    },
222
223    /// This event precedes a wave of `subgift`/`anonsubgift` messages.
224    /// (`An anonymous user is gifting <mass_gift_count> Tier 1 Subs to <Channel>'s community!`)
225    ///
226    /// This is a variant of `submysterygift` where the sending user is not known.
227    /// Not that even though every `USERNOTICE` carries a sending user, the sending user of this
228    /// type of `USERNOTICE` carries no useful information, it can be e.g. the channel owner
229    /// or a service user like `AnAnonymousGifter`.
230    ///
231    /// Compared to `submysterygift` this does not provide `sender_total_gifts`.
232    AnonSubMysteryGift {
233        /// Number of gifts the sender just gifted.
234        mass_gift_count: u64,
235        /// The type of sub plan the recipients were gifted.
236        /// `1000`, `2000` or `3000`, referring to tier 1, 2 or 3 subs respectively.
237        sub_plan: String,
238    },
239
240    /// Occurs when a user continues their gifted subscription they got from a non-anonymous
241    /// gifter.
242    ///
243    /// The sending user of this `USERNOTICE` is the user upgrading their sub.
244    /// The user that gifted the original gift sub is specified by these params.
245    GiftPaidUpgrade {
246        /// User that originally gifted the sub to this user.
247        /// This is the login name, see `TwitchUserBasics` for more info about the difference
248        /// between id, login and name.
249        gifter_login: String,
250        /// User that originally gifted the sub to this user.
251        /// This is the (display) name name, see `TwitchUserBasics` for more info about the
252        /// difference between id, login and name.
253        gifter_name: String,
254        /// Present if this gift/upgrade is part of a Twitch gift sub promotion, e.g.
255        /// Subtember or similar.
256        promotion: Option<SubGiftPromo>,
257    },
258
259    /// Occurs when a user continues their gifted subscription they got from an anonymous
260    /// gifter.
261    ///
262    /// The sending user of this `USERNOTICE` is the user upgrading their sub.
263    AnonGiftPaidUpgrade {
264        /// Present if this gift/upgrade is part of a Twitch gift sub promotion, e.g.
265        /// Subtember or similar.
266        promotion: Option<SubGiftPromo>,
267    },
268
269    /// A user is new in a channel and uses the rituals feature to send a message letting
270    /// the chat know they are new.
271    /// `<Sender> is new to <Channel>'s chat! Say hello!`
272    Ritual {
273        /// currently only valid value: `new_chatter`
274        ritual_name: String,
275    },
276
277    /// When a user cheers and earns himself a new bits badge with that cheer
278    /// (e.g. they just cheered more than/exactly 10000 bits in total,
279    /// and just earned themselves the 10k bits badge)
280    BitsBadgeTier {
281        /// tier of bits badge the user just earned themselves, e.g. `10000` if they just
282        /// earned the 10k bits badge.
283        threshold: u64,
284    },
285
286    /// A highlighted announcement created via /announcement.
287    Announcement {
288        /// If set, typically one of the values PRIMARY, BLUE, GREEN, ORANGE, PURPLE
289        color: Option<String>,
290    },
291
292    // this is hidden so users don't match on it. Instead they should match on _
293    // so their code still works the same when new variants are added here.
294    #[doc(hidden)]
295    Unknown,
296}
297
298impl TryFrom<IRCMessage> for UserNoticeMessage {
299    type Error = ServerMessageParseError;
300
301    fn try_from(source: IRCMessage) -> Result<UserNoticeMessage, ServerMessageParseError> {
302        if source.command != "USERNOTICE" {
303            return Err(ServerMessageParseError::MismatchedCommand(Box::new(source)));
304        }
305
306        // example message:
307        // @badge-info=subscriber/6;badges=subscriber/6,sub-gifter/1;color=#FF0000;display-name=9966Qtips;emotes=;flags=;id=916cdb58-87b6-407c-a54c-f79c54248aa7;login=9966qtips;mod=0;msg-id=resub;msg-param-cumulative-months=6;msg-param-months=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Channel\sSubscription\s(xqcow);msg-param-sub-plan=Prime;room-id=71092938;subscriber=1;system-msg=9966Qtips\ssubscribed\swith\sTwitch\sPrime.\sThey've\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1575162201680;user-id=46977320;user-type= :tmi.twitch.tv USERNOTICE #xqcow :xqcJAM xqcJAM xqcJAM xqcJAM
308
309        // note the message can also be missing:
310        // also note emotes= is still present
311        // @badge-info=subscriber/0;badges=subscriber/0,premium/1;color=#8A2BE2;display-name=PilotChup;emotes=;flags=;id=c7ae5c7a-3007-4f9d-9e64-35219a5c1134;login=pilotchup;mod=0;msg-id=sub;msg-param-cumulative-months=1;msg-param-months=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Channel\sSubscription\s(xqcow);msg-param-sub-plan=Prime;room-id=71092938;subscriber=1;system-msg=PilotChup\ssubscribed\swith\sTwitch\sPrime.;tmi-sent-ts=1575162111790;user-id=40745007;user-type= :tmi.twitch.tv USERNOTICE #xqcow
312
313        let sender = TwitchUserBasics {
314            id: source.try_get_nonempty_tag_value("user-id")?.to_owned(),
315            login: source.try_get_nonempty_tag_value("login")?.to_owned(),
316            name: source
317                .try_get_nonempty_tag_value("display-name")?
318                .to_owned(),
319        };
320
321        // the `msg-id` tag specifies the type of event this usernotice conveys. According to twitch,
322        // the value can be one of:
323        // sub, resub, raid, subgift, anonsubgift, anongiftpaidupgrade, giftpaidupgrade, ritual, bitsbadgetier
324        // more types are often added by Twitch ad-hoc without prior notice as part
325        // of seasonal events.
326        // TODO msg-id's that have been seen but are not documented:
327        //  rewardgift, primepaidupgrade, extendsub, standardpayforward, communitypayforward
328        //  (these can be added later)
329        // each event then has additional tags beginning with `msg-param-`, see below
330
331        let event_id = source.try_get_nonempty_tag_value("msg-id")?.to_owned();
332        let event = match event_id.as_str() {
333            // sub, resub:
334            // sender is the user subbing/resubbung
335            // msg-param-cumulative-months
336            // msg-param-should-share-streak
337            // msg-param-streak-months
338            // msg-param-sub-plan (1000, 2000 or 3000 for the three sub tiers, and Prime)
339            // msg-param-sub-plan-name (e.g. "The Ninjas")
340            "sub" | "resub" => UserNoticeEvent::SubOrResub {
341                is_resub: &event_id == "resub",
342                cumulative_months: source.try_get_number("msg-param-cumulative-months")?,
343                streak_months: if source.try_get_bool("msg-param-should-share-streak")? {
344                    Some(source.try_get_number("msg-param-streak-months")?)
345                } else {
346                    None
347                },
348                sub_plan: source
349                    .try_get_nonempty_tag_value("msg-param-sub-plan")?
350                    .to_owned(),
351                sub_plan_name: source
352                    .try_get_nonempty_tag_value("msg-param-sub-plan-name")?
353                    .to_owned(),
354            },
355            // raid:
356            // sender is the user raiding this channel
357            // msg-param-displayName (duplicates always-present display-name tag)
358            // msg-param-login (duplicates always-present login tag)
359            // msg-param-viewerCount
360            // msg-param-profileImageURL (link to 70x70 version of raider's pfp)
361            "raid" => UserNoticeEvent::Raid {
362                viewer_count: source.try_get_number::<u64>("msg-param-viewerCount")?,
363                profile_image_url: source
364                    .try_get_nonempty_tag_value("msg-param-profileImageURL")?
365                    .to_owned(),
366            },
367            // subgift, anonsubgift:
368            // sender of message is the gifter, or AnAnonymousGifter (ID 274598607)
369            // msg-param-months (same as msg-param-cumulative-months on sub/resub)
370            // msg-param-recipient-display-name
371            // msg-param-recipient-id
372            // msg-param-recipient-user-name (login name)
373            // msg-param-sub-plan (1000, 2000 or 3000 for the three sub tiers)
374            // msg-param-sub-plan-name (e.g. "The Ninjas")
375            // msg-param-gift-months (number of months in a single multi-month gift)
376            "subgift" | "anonsubgift" => UserNoticeEvent::SubGift {
377                // 274598607 is the user ID of "AnAnonymousGifter"
378                is_sender_anonymous: event_id == "anonsubgift" || sender.id == "274598607",
379                cumulative_months: source.try_get_number("msg-param-months")?,
380                recipient: TwitchUserBasics {
381                    id: source
382                        .try_get_nonempty_tag_value("msg-param-recipient-id")?
383                        .to_owned(),
384                    login: source
385                        .try_get_nonempty_tag_value("msg-param-recipient-user-name")?
386                        .to_owned(),
387                    name: source
388                        .try_get_nonempty_tag_value("msg-param-recipient-display-name")?
389                        .to_owned(),
390                },
391                sub_plan: source
392                    .try_get_nonempty_tag_value("msg-param-sub-plan")?
393                    .to_owned(),
394                sub_plan_name: source
395                    .try_get_nonempty_tag_value("msg-param-sub-plan-name")?
396                    .to_owned(),
397                num_gifted_months: source.try_get_number("msg-param-gift-months")?,
398            },
399            // submysterygift, anonsubmysterygift:
400            // this precedes a wave of subgift/anonsubgift messages.
401            // "AleMogul is gifting 100 Tier 1 Subs to NymN's community!
402            // They've gifted a total of 5688 in the channel!"
403            // msg-param-mass-gift-count - amount of gifts in this bulk, e.g. 100 above
404            // msg-param-sender-count - total amount gifted, e.g. 5688 above
405            //  - this seems to be missing if sender
406            // msg-param-sub-plan (1000, 2000 or 3000 for the three sub tiers)
407
408            // 274598607 is the user ID of "AnAnonymousGifter"
409            // the dorky syntax here instead of a normal match is to accomodate the special case
410            // for the submysterygift
411            _ if (sender.id == "274598607" && event_id == "submysterygift")
412                || event_id == "anonsubmysterygift" =>
413            {
414                UserNoticeEvent::AnonSubMysteryGift {
415                    mass_gift_count: source.try_get_number("msg-param-mass-gift-count")?,
416                    sub_plan: source
417                        .try_get_nonempty_tag_value("msg-param-sub-plan")?
418                        .to_owned(),
419                }
420            }
421            // this takes over all other cases of submysterygift.
422            "submysterygift" => UserNoticeEvent::SubMysteryGift {
423                mass_gift_count: source.try_get_number("msg-param-mass-gift-count")?,
424                sender_total_gifts: if sender.login != "twitch" {
425                    Some(source.try_get_number("msg-param-sender-count")?)
426                } else {
427                    //  - this seems to be missing if sender the sender is twitch (user-id=12826) on subtembers
428                    source.try_get_number("msg-param-sender-count").ok()
429                },
430                sub_plan: source
431                    .try_get_nonempty_tag_value("msg-param-sub-plan")?
432                    .to_owned(),
433            },
434            // giftpaidupgrade, anongiftpaidupgrade:
435            // When a user commits to continue the gift sub by another user (or an anonymous gifter).
436            // sender is the user continuing the gift sub.
437            // note anongiftpaidupgrade actually occurs, unlike anonsubgift
438            //
439            // these params are present when the upgrade is part of a promotion, e.g. Subtember 2018
440            // msg-param-promo-gift-total (number of gifts by the sending user in the specified promotion)
441            // msg-param-promo-name (name of the promo, e.g. Subtember 2018)
442            //
443            // only for giftpaidupgrade:
444            //   msg-param-sender-login - login name of user who gifted this user originally
445            //   msg-param-sender-name - display name of user who gifted this user originally
446            "giftpaidupgrade" => UserNoticeEvent::GiftPaidUpgrade {
447                gifter_login: source
448                    .try_get_nonempty_tag_value("msg-param-sender-login")?
449                    .to_owned(),
450                gifter_name: source
451                    .try_get_nonempty_tag_value("msg-param-sender-name")?
452                    .to_owned(),
453                promotion: SubGiftPromo::parse_if_present(&source)?,
454            },
455            "anongiftpaidupgrade" => UserNoticeEvent::AnonGiftPaidUpgrade {
456                promotion: SubGiftPromo::parse_if_present(&source)?,
457            },
458
459            // ritual
460            // A user is new in a channel and uses the rituals feature to send a message letting
461            // the chat know they are new.
462            // "<Sender> is new to <Channel>'s chat! Say hello!"
463            // msg-param-ritual-name - only valid value: "new_chatter"
464            "ritual" => UserNoticeEvent::Ritual {
465                ritual_name: source
466                    .try_get_nonempty_tag_value("msg-param-ritual-name")?
467                    .to_owned(),
468            },
469
470            // bitsbadgetier
471            // When a user cheers and earns himself a new bits badge with that cheer
472            // (e.g. they just cheered more than/exactly 10000 bits in total,
473            // and just earned themselves the 10k bits badge)
474            // msg-param-threshold - specifies the bits threshold, e.g. in the above example 10000
475            "bitsbadgetier" => UserNoticeEvent::BitsBadgeTier {
476                threshold: source
477                    .try_get_number::<u64>("msg-param-threshold")?
478                    .to_owned(),
479            },
480
481            // announcement
482            // created in chat using /announcement, just a highlighted message that remains sticky in web chat
483            // msg-param-color - If set, typically one of the values PRIMARY, BLUE, GREEN, ORANGE, PURPLE
484            "announcement" => UserNoticeEvent::Announcement {
485                color: source.tags.0.get("msg-param-color").cloned(),
486            },
487
488            // there are more events that are just not documented and not implemented yet. see above.
489            _ => UserNoticeEvent::Unknown,
490        };
491
492        let message_text = source.params.get(1).cloned(); // can also be None
493        let emotes = if let Some(message_text) = &message_text {
494            source.try_get_emotes("emotes", message_text)?
495        } else {
496            vec![]
497        };
498
499        Ok(UserNoticeMessage {
500            channel_login: source.try_get_channel_login()?.to_owned(),
501            channel_id: source.try_get_nonempty_tag_value("room-id")?.to_owned(),
502            sender,
503            message_text,
504            system_message: source.try_get_tag_value("system-msg")?.to_owned(),
505            event,
506            event_id,
507            badge_info: source.try_get_badges("badge-info")?,
508            badges: source.try_get_badges("badges")?,
509            emotes,
510            name_color: source.try_get_color("color")?,
511            message_id: source.try_get_nonempty_tag_value("id")?.to_owned(),
512            server_timestamp: source.try_get_timestamp("tmi-sent-ts")?,
513            source,
514        })
515    }
516}
517
518impl From<UserNoticeMessage> for IRCMessage {
519    fn from(msg: UserNoticeMessage) -> IRCMessage {
520        msg.source
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use crate::message::twitch::{Badge, Emote, RGBColor, TwitchUserBasics};
527    use crate::message::{IRCMessage, SubGiftPromo, UserNoticeEvent, UserNoticeMessage};
528    use chrono::{TimeZone, Utc};
529    use std::convert::TryFrom;
530    use std::ops::Range;
531
532    #[test]
533    pub fn test_sub() {
534        let src = "@badge-info=subscriber/0;badges=subscriber/0,premium/1;color=;display-name=fallenseraphhh;emotes=;flags=;id=2a9bea11-a80a-49a0-a498-1642d457f775;login=fallenseraphhh;mod=0;msg-id=sub;msg-param-cumulative-months=1;msg-param-months=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(xqcow);msg-param-sub-plan=Prime;room-id=71092938;subscriber=1;system-msg=fallenseraphhh\\ssubscribed\\swith\\sTwitch\\sPrime.;tmi-sent-ts=1582685713242;user-id=224005980;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
535        let irc_message = IRCMessage::parse(src).unwrap();
536        let msg = UserNoticeMessage::try_from(irc_message.clone()).unwrap();
537
538        assert_eq!(
539            msg,
540            UserNoticeMessage {
541                channel_login: "xqcow".to_owned(),
542                channel_id: "71092938".to_owned(),
543                sender: TwitchUserBasics {
544                    id: "224005980".to_owned(),
545                    login: "fallenseraphhh".to_owned(),
546                    name: "fallenseraphhh".to_owned(),
547                },
548                message_text: None,
549                system_message: "fallenseraphhh subscribed with Twitch Prime.".to_owned(),
550                event: UserNoticeEvent::SubOrResub {
551                    is_resub: false,
552                    cumulative_months: 1,
553                    streak_months: None,
554                    sub_plan: "Prime".to_owned(),
555                    sub_plan_name: "Channel Subscription (xqcow)".to_owned(),
556                },
557                event_id: "sub".to_owned(),
558                badge_info: vec![Badge {
559                    name: "subscriber".to_owned(),
560                    version: "0".to_owned(),
561                }],
562                badges: vec![
563                    Badge {
564                        name: "subscriber".to_owned(),
565                        version: "0".to_owned(),
566                    },
567                    Badge {
568                        name: "premium".to_owned(),
569                        version: "1".to_owned(),
570                    }
571                ],
572                emotes: vec![],
573                name_color: None,
574                message_id: "2a9bea11-a80a-49a0-a498-1642d457f775".to_owned(),
575                server_timestamp: Utc.timestamp_millis_opt(1_582_685_713_242).unwrap(),
576                source: irc_message,
577            }
578        );
579    }
580
581    #[test]
582    pub fn test_resub() {
583        let src = "@badge-info=subscriber/2;badges=subscriber/0,battlerite_1/1;color=#0000FF;display-name=Gutrin;emotes=1035663:0-3;flags=;id=e0975c76-054c-4954-8cb0-91b8867ec1ca;login=gutrin;mod=0;msg-id=resub;msg-param-cumulative-months=2;msg-param-months=0;msg-param-should-share-streak=1;msg-param-streak-months=2;msg-param-sub-plan-name=Channel\\sSubscription\\s(xqcow);msg-param-sub-plan=1000;room-id=71092938;subscriber=1;system-msg=Gutrin\\ssubscribed\\sat\\sTier\\s1.\\sThey've\\ssubscribed\\sfor\\s2\\smonths,\\scurrently\\son\\sa\\s2\\smonth\\sstreak!;tmi-sent-ts=1581713640019;user-id=21156217;user-type= :tmi.twitch.tv USERNOTICE #xqcow :xqcL";
584        let irc_message = IRCMessage::parse(src).unwrap();
585        let msg = UserNoticeMessage::try_from(irc_message.clone()).unwrap();
586
587        assert_eq!(
588            msg,
589            UserNoticeMessage {
590                channel_login: "xqcow".to_owned(),
591                channel_id: "71092938".to_owned(),
592                sender: TwitchUserBasics {
593                    id: "21156217".to_owned(),
594                    login: "gutrin".to_owned(),
595                    name: "Gutrin".to_owned(),
596                },
597                message_text: Some("xqcL".to_owned()),
598                system_message: "Gutrin subscribed at Tier 1. They've subscribed for 2 months, currently on a 2 month streak!".to_owned(),
599                event: UserNoticeEvent::SubOrResub {
600                    is_resub: true,
601                    cumulative_months: 2,
602                    streak_months: Some(2),
603                    sub_plan: "1000".to_owned(),
604                    sub_plan_name: "Channel Subscription (xqcow)".to_owned(),
605                },
606                event_id: "resub".to_owned(),
607                badge_info: vec![Badge {
608                    name: "subscriber".to_owned(),
609                    version: "2".to_owned(),
610                }],
611                badges: vec![
612                    Badge {
613                        name: "subscriber".to_owned(),
614                        version: "0".to_owned(),
615                    },
616                    Badge {
617                        name: "battlerite_1".to_owned(),
618                        version: "1".to_owned(),
619                    }
620                ],
621                emotes: vec![
622                    Emote {
623                        id: "1035663".to_owned(),
624                        char_range: Range { start: 0, end: 4 },
625                        code: "xqcL".to_owned(),
626                    }
627                ],
628                name_color: Some(RGBColor {
629                    r: 0x00,
630                    g: 0x00,
631                    b: 0xFF,
632                }),
633                message_id: "e0975c76-054c-4954-8cb0-91b8867ec1ca".to_owned(),
634                server_timestamp: Utc.timestamp_millis_opt(1_581_713_640_019).unwrap(),
635                source: irc_message,
636            }
637        );
638    }
639
640    #[test]
641    pub fn test_resub_no_share_streak() {
642        let src = "@badge-info=;badges=premium/1;color=#8A2BE2;display-name=rene_rs;emotes=;flags=;id=ca1f02fb-77ec-487d-a9b3-bc4bfef2fe8b;login=rene_rs;mod=0;msg-id=resub;msg-param-cumulative-months=11;msg-param-months=0;msg-param-should-share-streak=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(xqcow);msg-param-sub-plan=Prime;room-id=71092938;subscriber=0;system-msg=rene_rs\\ssubscribed\\swith\\sTwitch\\sPrime.\\sThey've\\ssubscribed\\sfor\\s11\\smonths!;tmi-sent-ts=1590628650446;user-id=171356987;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
643        let irc_message = IRCMessage::parse(src).unwrap();
644        let msg = UserNoticeMessage::try_from(irc_message.clone()).unwrap();
645
646        assert_eq!(
647            msg,
648            UserNoticeMessage {
649                channel_login: "xqcow".to_owned(),
650                channel_id: "71092938".to_owned(),
651                sender: TwitchUserBasics {
652                    id: "171356987".to_owned(),
653                    login: "rene_rs".to_owned(),
654                    name: "rene_rs".to_owned(),
655                },
656                message_text: None,
657                system_message:
658                    "rene_rs subscribed with Twitch Prime. They've subscribed for 11 months!"
659                        .to_owned(),
660                event: UserNoticeEvent::SubOrResub {
661                    is_resub: true,
662                    cumulative_months: 11,
663                    streak_months: None,
664                    sub_plan: "Prime".to_owned(),
665                    sub_plan_name: "Channel Subscription (xqcow)".to_owned(),
666                },
667                event_id: "resub".to_owned(),
668                badge_info: vec![],
669                badges: vec![Badge {
670                    name: "premium".to_owned(),
671                    version: "1".to_owned(),
672                },],
673                emotes: vec![],
674                name_color: Some(RGBColor {
675                    r: 0x8A,
676                    g: 0x2B,
677                    b: 0xE2,
678                }),
679                message_id: "ca1f02fb-77ec-487d-a9b3-bc4bfef2fe8b".to_owned(),
680                server_timestamp: Utc.timestamp_millis_opt(1_590_628_650_446).unwrap(),
681                source: irc_message,
682            }
683        );
684    }
685
686    #[test]
687    pub fn test_raid() {
688        let src = "@badge-info=;badges=glhf-pledge/1;color=#FF69B4;display-name=iamelisabete;emotes=;flags=;id=bb99dda7-3736-4583-9114-52aa11b23d17;login=iamelisabete;mod=0;msg-id=raid;msg-param-displayName=iamelisabete;msg-param-login=iamelisabete;msg-param-profileImageURL=https://static-cdn.jtvnw.net/jtv_user_pictures/cae3ca63-510d-4715-b4ce-059dcf938978-profile_image-70x70.png;msg-param-viewerCount=430;room-id=71092938;subscriber=0;system-msg=430\\sraiders\\sfrom\\siamelisabete\\shave\\sjoined!;tmi-sent-ts=1594517796120;user-id=155874595;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
689        let irc_message = IRCMessage::parse(src).unwrap();
690        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
691
692        assert_eq!(
693            msg.sender,
694            TwitchUserBasics {
695                id: "155874595".to_owned(),
696                login: "iamelisabete".to_owned(),
697                name: "iamelisabete".to_owned(),
698            }
699        );
700        assert_eq!(msg.event, UserNoticeEvent::Raid {
701            viewer_count: 430,
702            profile_image_url: "https://static-cdn.jtvnw.net/jtv_user_pictures/cae3ca63-510d-4715-b4ce-059dcf938978-profile_image-70x70.png".to_owned(),
703        });
704    }
705
706    #[test]
707    pub fn test_subgift() {
708        let src = "@badge-info=;badges=sub-gifter/50;color=;display-name=AdamAtReflectStudios;emotes=;flags=;id=e21409b1-d25d-4a1a-b5cf-ef27d8b7030e;login=adamatreflectstudios;mod=0;msg-id=subgift;msg-param-gift-months=1;msg-param-months=2;msg-param-origin-id=da\\s39\\sa3\\see\\s5e\\s6b\\s4b\\s0d\\s32\\s55\\sbf\\sef\\s95\\s60\\s18\\s90\\saf\\sd8\\s07\\s09;msg-param-recipient-display-name=qatarking24xd;msg-param-recipient-id=236653628;msg-param-recipient-user-name=qatarking24xd;msg-param-sender-count=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(xqcow);msg-param-sub-plan=1000;room-id=71092938;subscriber=0;system-msg=AdamAtReflectStudios\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sqatarking24xd!;tmi-sent-ts=1594583782376;user-id=211711554;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
709        let irc_message = IRCMessage::parse(src).unwrap();
710        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
711
712        assert_eq!(
713            msg.event,
714            UserNoticeEvent::SubGift {
715                is_sender_anonymous: false,
716                cumulative_months: 2,
717                recipient: TwitchUserBasics {
718                    id: "236653628".to_owned(),
719                    login: "qatarking24xd".to_owned(),
720                    name: "qatarking24xd".to_owned(),
721                },
722                sub_plan: "1000".to_owned(),
723                sub_plan_name: "Channel Subscription (xqcow)".to_owned(),
724                num_gifted_months: 1,
725            }
726        );
727    }
728
729    #[test]
730    pub fn test_subgift_ananonymousgifter() {
731        let src = "@badge-info=;badges=;color=;display-name=AnAnonymousGifter;emotes=;flags=;id=62c3fd39-84cc-452a-9096-628a5306633a;login=ananonymousgifter;mod=0;msg-id=subgift;msg-param-fun-string=FunStringThree;msg-param-gift-months=1;msg-param-months=13;msg-param-origin-id=da\\s39\\sa3\\see\\s5e\\s6b\\s4b\\s0d\\s32\\s55\\sbf\\sef\\s95\\s60\\s18\\s90\\saf\\sd8\\s07\\s09;msg-param-recipient-display-name=Dot0422;msg-param-recipient-id=151784015;msg-param-recipient-user-name=dot0422;msg-param-sub-plan-name=Channel\\sSubscription\\s(xqcow);msg-param-sub-plan=1000;room-id=71092938;subscriber=0;system-msg=An\\sanonymous\\suser\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sDot0422!\\s;tmi-sent-ts=1594495108936;user-id=274598607;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
732        let irc_message = IRCMessage::parse(src).unwrap();
733        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
734
735        assert_eq!(
736            msg.event,
737            UserNoticeEvent::SubGift {
738                is_sender_anonymous: true,
739                cumulative_months: 13,
740                recipient: TwitchUserBasics {
741                    id: "151784015".to_owned(),
742                    login: "dot0422".to_owned(),
743                    name: "Dot0422".to_owned(),
744                },
745                sub_plan: "1000".to_owned(),
746                sub_plan_name: "Channel Subscription (xqcow)".to_owned(),
747                num_gifted_months: 1,
748            }
749        );
750    }
751
752    #[test]
753    pub fn test_anonsubgift() {
754        // note there are no anonsubgift messages being sent on Twitch IRC as of the time of writing this.
755        // so I created a fake one that matches what the announcement said they would be like (in theory),
756        let src = "@badge-info=;badges=;color=;display-name=xQcOW;emotes=;flags=;id=e21409b1-d25d-4a1a-b5cf-ef27d8b7030e;login=xqcow;mod=0;msg-id=anonsubgift;msg-param-gift-months=1;msg-param-months=2;msg-param-origin-id=da\\s39\\sa3\\see\\s5e\\s6b\\s4b\\s0d\\s32\\s55\\sbf\\sef\\s95\\s60\\s18\\s90\\saf\\sd8\\s07\\s09;msg-param-recipient-display-name=qatarking24xd;msg-param-recipient-id=236653628;msg-param-recipient-user-name=qatarking24xd;msg-param-sender-count=0;msg-param-sub-plan-name=Channel\\sSubscription\\s(xqcow);msg-param-sub-plan=1000;room-id=71092938;subscriber=0;system-msg=An\\sanonymous\\sgifter\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sqatarking24xd!;tmi-sent-ts=1594583782376;user-id=71092938;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
757        let irc_message = IRCMessage::parse(src).unwrap();
758        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
759
760        assert_eq!(
761            msg.event,
762            UserNoticeEvent::SubGift {
763                is_sender_anonymous: true,
764                cumulative_months: 2,
765                recipient: TwitchUserBasics {
766                    id: "236653628".to_owned(),
767                    login: "qatarking24xd".to_owned(),
768                    name: "qatarking24xd".to_owned(),
769                },
770                sub_plan: "1000".to_owned(),
771                sub_plan_name: "Channel Subscription (xqcow)".to_owned(),
772                num_gifted_months: 1,
773            }
774        );
775    }
776
777    #[test]
778    pub fn test_submysterygift() {
779        let src = "@badge-info=;badges=sub-gifter/50;color=;display-name=AdamAtReflectStudios;emotes=;flags=;id=049e6371-7023-4fca-8605-7dec60e72e12;login=adamatreflectstudios;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=20;msg-param-origin-id=1f\\sbe\\sbb\\s4a\\s81\\s9a\\s65\\sd1\\s4b\\s77\\sf5\\s23\\s16\\s4a\\sd3\\s13\\s09\\se7\\sbe\\s55;msg-param-sender-count=100;msg-param-sub-plan=1000;room-id=71092938;subscriber=0;system-msg=AdamAtReflectStudios\\sis\\sgifting\\s20\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!\\sThey've\\sgifted\\sa\\stotal\\sof\\s100\\sin\\sthe\\schannel!;tmi-sent-ts=1594583777669;user-id=211711554;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
780        let irc_message = IRCMessage::parse(src).unwrap();
781        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
782
783        assert_eq!(
784            msg.event,
785            UserNoticeEvent::SubMysteryGift {
786                mass_gift_count: 20,
787                sender_total_gifts: Some(100),
788                sub_plan: "1000".to_owned(),
789            }
790        );
791    }
792
793    #[test]
794    pub fn test_submysterygift_twitch() {
795        let src = "@badge-info=;badges=sub-gifter/50;color=;display-name=twitch;emotes=;flags=;id=049e6371-7023-4fca-8605-7dec60e72e12;login=twitch;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=20;msg-param-sender-count=50;msg-param-origin-id=1f\\sbe\\sbb\\s4a\\s81\\s9a\\s65\\sd1\\s4b\\s77\\sf5\\s23\\s16\\s4a\\sd3\\s13\\s09\\se7\\sbe\\s55;msg-param-sub-plan=1000;room-id=71092938;subscriber=0;system-msg=AdamAtReflectStudios\\sis\\sgifting\\s20\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!\\sThey've\\sgifted\\sa\\stotal\\sof\\s100\\sin\\sthe\\schannel!;tmi-sent-ts=1594583777669;user-id=12826;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
796        let irc_message = IRCMessage::parse(src).unwrap();
797        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
798
799        assert_eq!(
800            msg.event,
801            UserNoticeEvent::SubMysteryGift {
802                mass_gift_count: 20,
803                sender_total_gifts: Some(50),
804                sub_plan: "1000".to_owned(),
805            }
806        );
807    }
808
809    #[test]
810    pub fn test_submysterygift_twitch_missing_count() {
811        let src = "@badge-info=;badges=sub-gifter/50;color=;display-name=twitch;emotes=;flags=;id=049e6371-7023-4fca-8605-7dec60e72e12;login=twitch;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=20;msg-param-origin-id=1f\\sbe\\sbb\\s4a\\s81\\s9a\\s65\\sd1\\s4b\\s77\\sf5\\s23\\s16\\s4a\\sd3\\s13\\s09\\se7\\sbe\\s55;msg-param-sub-plan=1000;room-id=71092938;subscriber=0;system-msg=AdamAtReflectStudios\\sis\\sgifting\\s20\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!\\sThey've\\sgifted\\sa\\stotal\\sof\\s100\\sin\\sthe\\schannel!;tmi-sent-ts=1594583777669;user-id=12826;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
812        let irc_message = IRCMessage::parse(src).unwrap();
813        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
814
815        assert_eq!(
816            msg.event,
817            UserNoticeEvent::SubMysteryGift {
818                mass_gift_count: 20,
819                sender_total_gifts: None,
820                sub_plan: "1000".to_owned(),
821            }
822        );
823    }
824
825    #[test]
826    pub fn test_submysterygift_ananonymousgifter() {
827        let src = "@badge-info=;badges=;color=;display-name=AnAnonymousGifter;emotes=;flags=;id=8db97752-3dee-460b-9001-e925d0e2ba5b;login=ananonymousgifter;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=10;msg-param-origin-id=13\\s33\\sed\\sc0\\sef\\sa0\\s7b\\s9b\\s48\\s59\\scb\\scc\\se4\\s39\\s7b\\s90\\sf9\\s54\\s75\\s66;msg-param-sub-plan=1000;room-id=71092938;subscriber=0;system-msg=An\\sanonymous\\suser\\sis\\sgifting\\s10\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!;tmi-sent-ts=1585447099603;user-id=274598607;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
828        let irc_message = IRCMessage::parse(src).unwrap();
829        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
830
831        assert_eq!(
832            msg.event,
833            UserNoticeEvent::AnonSubMysteryGift {
834                mass_gift_count: 10,
835                sub_plan: "1000".to_owned(),
836            }
837        );
838    }
839
840    #[test]
841    pub fn test_anonsubmysterygift() {
842        // again, this is never emitted on IRC currently. So this test case is a made-up
843        // modification of a subgift type message.
844        let src = "@badge-info=;badges=;color=;display-name=xQcOW;emotes=;flags=;id=8db97752-3dee-460b-9001-e925d0e2ba5b;login=xqcow;mod=0;msg-id=anonsubmysterygift;msg-param-mass-gift-count=15;msg-param-origin-id=13\\s33\\sed\\sc0\\sef\\sa0\\s7b\\s9b\\s48\\s59\\scb\\scc\\se4\\s39\\s7b\\s90\\sf9\\s54\\s75\\s66;msg-param-sub-plan=2000;room-id=71092938;subscriber=0;system-msg=An\\sanonymous\\suser\\sis\\sgifting\\s10\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!;tmi-sent-ts=1585447099603;user-id=71092938;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
845        let irc_message = IRCMessage::parse(src).unwrap();
846        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
847
848        assert_eq!(
849            msg.event,
850            UserNoticeEvent::AnonSubMysteryGift {
851                mass_gift_count: 15,
852                sub_plan: "2000".to_owned(),
853            }
854        );
855    }
856
857    #[test]
858    pub fn test_giftpaidupgrade_no_promo() {
859        let src = "@badge-info=subscriber/2;badges=subscriber/2;color=#00FFF5;display-name=CrazyCrackAnimal;emotes=;flags=;id=7006f242-a45c-4e07-83b3-11f9c6d1ee28;login=crazycrackanimal;mod=0;msg-id=giftpaidupgrade;msg-param-sender-login=stridezgum;msg-param-sender-name=Stridezgum;room-id=71092938;subscriber=1;system-msg=CrazyCrackAnimal\\sis\\scontinuing\\sthe\\sGift\\sSub\\sthey\\sgot\\sfrom\\sStridezgum!;tmi-sent-ts=1594518849459;user-id=86082877;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
860
861        let irc_message = IRCMessage::parse(src).unwrap();
862        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
863
864        assert_eq!(
865            msg.event,
866            UserNoticeEvent::GiftPaidUpgrade {
867                gifter_login: "stridezgum".to_owned(),
868                gifter_name: "Stridezgum".to_owned(),
869                promotion: None,
870            }
871        );
872    }
873
874    #[test]
875    pub fn test_giftpaidupgrade_with_promo() {
876        // I can't find any real examples for this type of message, so this is a made-up test case
877        // (the same one as above, but with two tags added)
878        let src = "@badge-info=subscriber/2;badges=subscriber/2;color=#00FFF5;display-name=CrazyCrackAnimal;emotes=;flags=;id=7006f242-a45c-4e07-83b3-11f9c6d1ee28;login=crazycrackanimal;mod=0;msg-id=giftpaidupgrade;msg-param-sender-login=stridezgum;msg-param-sender-name=Stridezgum;msg-param-promo-name=TestSubtember2020;msg-param-promo-gift-total=4003;room-id=71092938;subscriber=1;system-msg=CrazyCrackAnimal\\sis\\scontinuing\\sthe\\sGift\\sSub\\sthey\\sgot\\sfrom\\sStridezgum!\\sbla\\sbla\\bla\\sstuff\\sabout\\spromo\\shere;tmi-sent-ts=1594518849459;user-id=86082877;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
879
880        let irc_message = IRCMessage::parse(src).unwrap();
881        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
882
883        assert_eq!(
884            msg.event,
885            UserNoticeEvent::GiftPaidUpgrade {
886                gifter_login: "stridezgum".to_owned(),
887                gifter_name: "Stridezgum".to_owned(),
888                promotion: Some(SubGiftPromo {
889                    promo_name: "TestSubtember2020".to_owned(),
890                    total_gifts: 4003,
891                }),
892            }
893        );
894    }
895
896    #[test]
897    pub fn test_anongiftpaidupgrade_no_promo() {
898        let src = "@badge-info=subscriber/1;badges=subscriber/0,premium/1;color=#8A2BE2;display-name=samura1jack_ttv;emotes=;flags=;id=144ee636-0c1d-404e-8b29-35449a045a7e;login=samura1jack_ttv;mod=0;msg-id=anongiftpaidupgrade;room-id=71092938;subscriber=1;system-msg=samura1jack_ttv\\sis\\scontinuing\\sthe\\sGift\\sSub\\sthey\\sgot\\sfrom\\san\\sanonymous\\suser!;tmi-sent-ts=1594327421732;user-id=102707709;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
899
900        let irc_message = IRCMessage::parse(src).unwrap();
901        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
902
903        assert_eq!(
904            msg.event,
905            UserNoticeEvent::AnonGiftPaidUpgrade { promotion: None }
906        );
907    }
908
909    #[test]
910    pub fn test_anongiftpaidupgrade_with_promo() {
911        // I can't find any real examples for this type of message, so this is a made-up test case
912        // (the same one as above, but with two tags added)
913        let src = "@badge-info=subscriber/1;badges=subscriber/0,premium/1;color=#8A2BE2;display-name=samura1jack_ttv;emotes=;flags=;id=144ee636-0c1d-404e-8b29-35449a045a7e;msg-param-promo-name=TestSubtember2020;msg-param-promo-gift-total=4003;login=samura1jack_ttv;mod=0;msg-id=anongiftpaidupgrade;room-id=71092938;subscriber=1;system-msg=samura1jack_ttv\\sis\\scontinuing\\sthe\\sGift\\sSub\\sthey\\sgot\\sfrom\\san\\sanonymous\\suser!\\sbla\\sbla\\bla\\sstuff\\sabout\\spromo\\shere;tmi-sent-ts=1594327421732;user-id=102707709;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
914        let irc_message = IRCMessage::parse(src).unwrap();
915        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
916
917        assert_eq!(
918            msg.event,
919            UserNoticeEvent::AnonGiftPaidUpgrade {
920                promotion: Some(SubGiftPromo {
921                    promo_name: "TestSubtember2020".to_owned(),
922                    total_gifts: 4003,
923                })
924            }
925        );
926    }
927
928    #[test]
929    pub fn test_ritual() {
930        let src = "@badge-info=;badges=;color=;display-name=SevenTest1;emotes=30259:0-6;id=37feed0f-b9c7-4c3a-b475-21c6c6d21c3d;login=seventest1;mod=0;msg-id=ritual;msg-param-ritual-name=new_chatter;room-id=6316121;subscriber=0;system-msg=Seventoes\\sis\\snew\\shere!;tmi-sent-ts=1508363903826;turbo=0;user-id=131260580;user-type= :tmi.twitch.tv USERNOTICE #seventoes :HeyGuys";
931        let irc_message = IRCMessage::parse(src).unwrap();
932        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
933
934        assert_eq!(
935            msg.event,
936            UserNoticeEvent::Ritual {
937                ritual_name: "new_chatter".to_owned()
938            }
939        );
940    }
941
942    #[test]
943    pub fn test_bitsbadgetier() {
944        let src = "@badge-info=subscriber/2;badges=subscriber/2,bits/1000;color=#FF4500;display-name=whoopiix;emotes=;flags=;id=d2b32a02-3071-4c52-b2ce-bc3716acdc44;login=whoopiix;mod=0;msg-id=bitsbadgetier;msg-param-threshold=1000;room-id=71092938;subscriber=1;system-msg=bits\\sbadge\\stier\\snotification;tmi-sent-ts=1594520403813;user-id=104252055;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
945        let irc_message = IRCMessage::parse(src).unwrap();
946        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
947
948        assert_eq!(
949            msg.event,
950            UserNoticeEvent::BitsBadgeTier { threshold: 1000 }
951        );
952    }
953
954    #[test]
955    pub fn test_unknown() {
956        // just an example of an undocumented type of message that we don't parse currently.
957        let src = "@badge-info=;badges=sub-gifter/50;color=;display-name=AdamAtReflectStudios;emotes=;flags=;id=7f1336e4-f84a-4510-809d-e57bf50af0cc;login=adamatreflectstudios;mod=0;msg-id=rewardgift;msg-param-domain=pride_megacommerce_2020;msg-param-selected-count=100;msg-param-total-reward-count=100;msg-param-trigger-amount=20;msg-param-trigger-type=SUBGIFT;room-id=71092938;subscriber=0;system-msg=AdamAtReflectStudios's\\sGift\\sshared\\srewards\\sto\\s100\\sothers\\sin\\sChat!;tmi-sent-ts=1594583778756;user-id=211711554;user-type= :tmi.twitch.tv USERNOTICE #xqcow";
958        let irc_message = IRCMessage::parse(src).unwrap();
959        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
960
961        assert_eq!(msg.event, UserNoticeEvent::Unknown);
962    }
963
964    #[test]
965    pub fn test_sneaky_action_invalid_emote_tag() {
966        // See https://github.com/twitchdev/issues/issues/175
967        let src = r"@badge-info=subscriber/23;badges=moderator/1,subscriber/12;color=#19E6E6;display-name=randers;emotes=25:7-11,23-27/499:29-30;flags=;id=8c2918c2-adf4-4208-a554-8a72d016de70;login=randers;mod=1;msg-id=resub;msg-param-cumulative-months=23;msg-param-months=0;msg-param-should-share-streak=1;msg-param-streak-months=23;msg-param-sub-plan-name=look\sat\sthose\sshitty\semotes,\srip\s$5\sLUL;msg-param-sub-plan=1000;room-id=11148817;subscriber=1;system-msg=randers\ssubscribed\sat\sTier\s1.\sThey've\ssubscribed\sfor\s23\smonths,\scurrently\son\sa\s23\smonth\sstreak!;tmi-sent-ts=1595497450553;user-id=40286300;user-type=mod :tmi.twitch.tv USERNOTICE #pajlada :ACTION Kappa TEST TEST Kappa :)";
968        let irc_message = IRCMessage::parse(src).unwrap();
969        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
970
971        assert_eq!(
972            msg.message_text,
973            Some("ACTION Kappa TEST TEST Kappa :)".to_owned())
974        );
975        assert_eq!(
976            msg.emotes,
977            vec![
978                Emote {
979                    id: "25".to_owned(),
980                    char_range: Range { start: 7, end: 12 },
981                    code: " Kapp".to_owned(),
982                },
983                Emote {
984                    id: "25".to_owned(),
985                    char_range: Range { start: 23, end: 28 },
986                    code: " Kapp".to_owned(),
987                },
988                Emote {
989                    id: "499".to_owned(),
990                    char_range: Range { start: 29, end: 31 },
991                    code: " :".to_owned(),
992                },
993            ]
994        );
995    }
996
997    #[test]
998    pub fn test_announcement() {
999        let src = r"@badge-info=subscriber/88;badges=moderator/1,subscriber/72;color=#19E6E6;display-name=randers;emotes=185989:11-15/25:17-21/1902:23-27;flags=;id=9045a344-75b9-44fa-af49-c413bcb39559;login=randers;mod=1;msg-id=announcement;msg-param-color=PRIMARY;room-id=11148817;subscriber=1;system-msg=;tmi-sent-ts=1780872797175;user-id=40286300;user-type=mod;vip=0 :tmi.twitch.tv USERNOTICE #pajlada :hello pajs pajaH Kappa Keepo";
1000
1001        let irc_message = IRCMessage::parse(src).unwrap();
1002        let msg = UserNoticeMessage::try_from(irc_message).unwrap();
1003
1004        assert_eq!(
1005            msg.message_text,
1006            Some("hello pajs pajaH Kappa Keepo".to_owned())
1007        );
1008        assert_eq!(
1009            msg.event,
1010            UserNoticeEvent::Announcement {
1011                color: Some("PRIMARY".to_owned())
1012            }
1013        );
1014    }
1015}