twitch_message/messages/
user_notice.rs

1#![allow(deprecated)]
2
3use std::borrow::Cow;
4
5use crate::{parse_badges, Badge, Color, Emote, Tags};
6
7use super::{Message, UserType};
8
9/// [`USERNOTICE`](https://dev.twitch.tv/docs/irc/commands/#usernotice). Sent when events like someone subscribing to the channel occurs.
10#[derive(Clone, Debug, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
12pub struct UserNotice<'a> {
13    /// The raw underlying string
14    pub raw: Cow<'a, str>,
15    /// Metadata attached to the message
16    pub tags: Tags<'a>,
17    /// The name of the channel that the event occurred in.
18    pub channel: Cow<'a, str>,
19    /// Data attached to the notice
20    pub data: Option<Cow<'a, str>>,
21}
22
23impl<'a> UserNotice<'a> {
24    /// Contains metadata related to the chat badges in the [`badges`](Self::badges) tag.
25    pub fn badge_info<'t: 'a>(&'t self) -> impl Iterator<Item = Badge<'a>> + 't {
26        self.tags
27            .get("badge-info")
28            .into_iter()
29            .flat_map(parse_badges)
30    }
31
32    /// Badges attached to a user in a channel.
33    pub fn badges<'t: 'a>(&'t self) -> impl Iterator<Item = Badge<'a>> + 't {
34        Badge::from_tags(&self.tags)
35    }
36
37    /// Emotes in the message.
38    pub fn emotes<'t: 'a>(&'t self) -> impl Iterator<Item = Emote<'a>> + 't {
39        self.data
40            .iter()
41            .flat_map(|data| Emote::from_tags(&self.tags, data))
42    }
43
44    /// The color of the user’s name in the chat room. This may be [`None`] if it is never set.
45    pub fn color(&self) -> Option<Color> {
46        self.tags.color()
47    }
48
49    /// The user’s display name
50    pub fn display_name(&self) -> Option<&str> {
51        self.tags.get("display-name")
52    }
53
54    /// The message Twitch shows in the chat room for this notice.
55    pub fn system_msg(&self) -> Option<&str> {
56        self.tags.get("system-msg")
57    }
58
59    /// The user is a moderator in the channel.
60    pub fn is_moderator(&self) -> bool {
61        self.tags.bool("mod")
62    }
63
64    /// The user is a subscriber of the channel.
65    pub fn is_subscriber(&self) -> bool {
66        self.tags.bool("subscriber")
67    }
68
69    /// The user is a turbo user.
70    pub fn is_turbo(&self) -> bool {
71        self.tags.bool("turbo")
72    }
73
74    /// The login name of the user whose action generated the message.
75    pub fn login(&self) -> Option<&str> {
76        self.tags.get("login")
77    }
78
79    /// An ID that uniquely identifies this message.
80    pub fn id(&self) -> Option<&str> {
81        self.tags.get("id")
82    }
83
84    /// The type of notice
85    pub fn msg_id(&self) -> Option<UserNoticeId> {
86        self.tags.get("msg-id").map(UserNoticeId::parse)
87    }
88
89    /// An ID that identifies the chat room (channel).
90    pub fn room_id(&self) -> Option<&str> {
91        self.tags.get("room_id")
92    }
93
94    /// The user’s ID.
95
96    pub fn user_id(&self) -> Option<&str> {
97        self.tags.get("user-id")
98    }
99
100    /// The UNIX timestamp.
101    pub fn tmi_sent_ts(&self) -> Option<&str> {
102        self.tags.get("tmi-sent-ts")
103    }
104
105    /// The user’s type.
106    pub fn user_type(&self) -> UserType {
107        self.tags
108            .get("user-type")
109            .map(UserType::parse)
110            .unwrap_or_default()
111    }
112
113    /// The total number of months the user has subscribed.
114    ///
115    /// Included only with [`sub`](UserNoticeId::Sub) and [`resub`](UserNoticeId::Resub) notices
116    pub fn msg_param_cumulative_months(&self) -> Option<&str> {
117        self.tags.get("msg-param-cumulative-months")
118    }
119
120    /// The display name of the broadcaster raiding this channel.
121    ///
122    /// Included only with [`raid`](UserNoticeId::Raid) notices
123    pub fn msg_param_display_name(&self) -> Option<&str> {
124        // XXX: docs have this in a weird casing, going to try the other as well.
125        self.tags
126            .get("msg-param-displayName")
127            .or_else(|| self.tags.get("msg-param-display-name"))
128    }
129
130    /// The display name of the broadcaster raiding this channel.
131    ///
132    /// Included only with [`raid`](UserNoticeId::Raid) notices
133    pub fn msg_param_login(&self) -> Option<&str> {
134        self.tags.get("msg-param-login")
135    }
136
137    /// The total number of months the user has subscribed.
138    ///
139    /// Included only with [`subgift`](UserNoticeId::Subgift) notices
140    ///
141    /// # See also
142    ///
143    /// [`msg-param-cumulative-months`](Self::msg_param_cumulative_months)
144    pub fn msg_param_months(&self) -> Option<&str> {
145        self.tags.get("msg-param-months")
146    }
147
148    /// The number of gifts the gifter has given during the promo indicated by [`msg-param-promo-name`](Self::msg_param_promo_name)
149    ///
150    /// Included only with [`anongiftpaidupgrade`](UserNoticeId::AnonGiftPaidUpgrade) and [`giftpaidupgrade`](UserNoticeId::GiftPaidUpgrade) notices
151    pub fn msg_param_promo_gift_total(&self) -> Option<&str> {
152        self.tags.get("msg-param-promo-gift-total")
153    }
154
155    /// The subscriptions promo, if any, that is ongoing (for example, Subtember 2018).
156    ///
157    /// Included only with [`anongiftpaidupgrade`](UserNoticeId::AnonGiftPaidUpgrade) and [`giftpaidupgrade`](UserNoticeId::GiftPaidUpgrade) notices
158    pub fn msg_param_promo_name(&self) -> Option<&str> {
159        self.tags.get("msg-param-promo-name")
160    }
161
162    /// The display name of the subscription gift recipient.
163    ///
164    /// Included only with [`subgift`](UserNoticeId::Subgift) notices
165    pub fn msg_param_recipient_display_name(&self) -> Option<&str> {
166        self.tags.get("msg-param-recipient-display-name")
167    }
168
169    /// The user ID of the subscription gift recipient.
170    ///
171    /// Included only with [`subgift`](UserNoticeId::Subgift) notices
172    pub fn msg_param_recipient_id(&self) -> Option<&str> {
173        self.tags.get("msg-param-recipient-id")
174    }
175
176    /// The user name of the subscription gift recipient.
177    ///
178    /// Included only with [`subgift`](UserNoticeId::Subgift) notices
179    pub fn msg_param_recipient_user_name(&self) -> Option<&str> {
180        self.tags.get("msg-param-recipient-user-name")
181    }
182
183    /// The login name of the user who gifted the subscription.
184    ///
185    /// Included only with [`giftpaidupgrade`](UserNoticeId::GiftPaidUpgrade) notices
186    pub fn msg_param_sender_login(&self) -> Option<&str> {
187        self.tags.get("msg-param-sender-login")
188    }
189
190    /// The display name of the user who gifted the subscription.
191    ///
192    /// Included only with [`giftpaidupgrade`](UserNoticeId::GiftPaidUpgrade) notices
193    pub fn msg_param_sender_name(&self) -> Option<&str> {
194        self.tags.get("msg-param-sender-name")
195    }
196
197    /// Indicates whether the user wants their streaks shared.
198    ///
199    /// Included only with [`sub`](UserNoticeId::Sub) and [`resub`](UserNoticeId::Resub) notices
200    pub fn msg_param_should_share_streak(&self) -> Option<&str> {
201        self.tags.get("msg-param-should-share-streak")
202    }
203
204    /// The number of consecutive months the user has subscribed.
205    ///
206    /// Included only with [`sub`](UserNoticeId::Sub) and [`resub`](UserNoticeId::Resub) notices
207    pub fn msg_param_streak_months(&self) -> Option<&str> {
208        self.tags.get("msg-param-streak-months")
209    }
210
211    /// The type of subscription plan being used.
212    ///
213    /// Included only with [`sub`](UserNoticeId::Sub), [`resub`](UserNoticeId::Resub) and [`subgift`](UserNoticeId::Subgift) notices
214    pub fn msg_param_sub_plan(&self) -> Option<&str> {
215        self.tags.get("msg-param-sub-plan")
216    }
217
218    /// The display name of the subscription plan.
219    ///
220    /// Included only with [`sub`](UserNoticeId::Sub), [`resub`](UserNoticeId::Resub) and [`subgift`](UserNoticeId::Subgift) notices
221    pub fn msg_param_sub_plan_name(&self) -> Option<&str> {
222        self.tags.get("msg-param-sub-plan-name")
223    }
224
225    /// The number of viewers raiding this channel from the broadcaster’s channel.
226    ///
227    /// Included only with [`raid`](UserNoticeId::Raid) notices
228    pub fn msg_param_viewer_count(&self) -> Option<&str> {
229        self.tags.get("msg-param-viewerCount")
230    }
231
232    #[deprecated]
233    #[allow(missing_docs)]
234    pub fn msg_param_ritual_name(&self) -> Option<&str> {
235        self.tags.get("msg-param-ritual-name")
236    }
237
238    /// The tier of the Bits badge the user just earned.
239    ///
240    /// Included only with [`bitsbadgetier`](UserNoticeId::BitsBadgeTier) notices
241    pub fn msg_param_threshold(&self) -> Option<&str> {
242        self.tags.get("msg-param-threshold")
243    }
244
245    /// The number of months gifted as part of a single, multi-month gift.
246    ///
247    /// Included only with [`subgift`](UserNoticeId::Subgift) notices
248    pub fn msg_param_gift_months(&self) -> Option<&str> {
249        self.tags.get("msg-param-gift-months")
250    }
251
252    /// The domain of the rewards being gifted (e.g. "pride_megacommerce_2020").
253    ///
254    /// Included only with [`rewardgift`](UserNoticeId::RewardGift) notices
255    pub fn msg_param_domain(&self) -> Option<&str> {
256        self.tags.get("msg-param-domain")
257    }
258
259    /// The type of monetary event that triggered the reward gift (e.g., "SUBGIFT", "CHEER").
260    ///
261    /// Included only with [`rewardgift`](UserNoticeId::RewardGift) notices
262    pub fn msg_param_trigger_type(&self) -> Option<&str> {
263        self.tags.get("msg-param-trigger-type")
264    }
265
266    /// The number of gifted rewards as part of the primary selection.
267    ///
268    /// Included only with [`rewardgift`](UserNoticeId::RewardGift) notices
269    pub fn msg_param_selected_count(&self) -> Option<&str> {
270        self.tags.get("msg-param-selected-count")
271    }
272    /// The total number of rewards being gifted (e.g. 5 emotes).
273    ///
274    /// Included only with [`rewardgift`](UserNoticeId::RewardGift) notices
275    pub fn msg_param_total_reward_count(&self) -> Option<&str> {
276        self.tags.get("msg-param-total-reward-count")
277    }
278    /// The number of instances of the trigger (e.g. 1 sub gift or 300 bits).
279    ///
280    /// Included only with [`rewardgift`](UserNoticeId::RewardGift) notices
281    pub fn msg_param_trigger_amount(&self) -> Option<&str> {
282        self.tags.get("msg-param-trigger-amount")
283    }
284}
285
286/// The type of notice
287#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
288#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
289pub enum UserNoticeId {
290    /// A subscription event
291    Sub,
292    /// A resubscription event
293    Resub,
294    /// A gift subscription event
295    Subgift,
296    /// A mass subscription event occurs.
297    SubMysteryGift,
298    /// A gifted subscription is continued
299    GiftPaidUpgrade,
300    /// Monetary event triggered emotes to be shared
301    RewardGift,
302    /// A gifted subscription from an anonymous user is continued
303    AnonGiftPaidUpgrade,
304    /// A raid happens
305    Raid,
306    /// A raid from the channel to another is cancelled.
307    Unraid,
308    #[allow(missing_docs)]
309    #[deprecated]
310    Ritual,
311    /// A user shares a new bits badge
312    BitsBadgeTier,
313    /// Unknown notice
314    #[default]
315    Unknown,
316}
317
318impl UserNoticeId {
319    fn parse(input: &str) -> Self {
320        match input {
321            "sub" => Self::Sub,
322            "resub" => Self::Resub,
323            "subgift" => Self::Subgift,
324            "submysterygift" => Self::SubMysteryGift,
325            "giftpaidupgrade" => Self::GiftPaidUpgrade,
326            "rewardgift" => Self::RewardGift,
327            "anongiftpaidupgrade" => Self::AnonGiftPaidUpgrade,
328            "raid" => Self::Raid,
329            "unraid" => Self::Unraid,
330            "ritual" => Self::Ritual,
331            "bitsbadgetier" => Self::BitsBadgeTier,
332            _ => Self::Unknown,
333        }
334    }
335}
336
337impl UserNotice<'_> {
338    fn validate(value: &Message<'_>) -> bool {
339        !value.args.is_empty()
340    }
341}
342
343impl<'a> TryFrom<Message<'a>> for UserNotice<'a> {
344    type Error = Message<'a>;
345
346    fn try_from(mut value: Message<'a>) -> Result<Self, Self::Error> {
347        if !Self::validate(&value) {
348            return Err(value);
349        }
350
351        Ok(Self {
352            raw: value.raw,
353            tags: value.tags,
354            channel: value.args.remove(0),
355            data: value.data,
356        })
357    }
358}
359
360impl<'a, 'b> TryFrom<&'b Message<'a>> for UserNotice<'a> {
361    type Error = &'b Message<'a>;
362
363    fn try_from(value: &'b Message<'a>) -> Result<Self, Self::Error> {
364        if !Self::validate(value) {
365            return Err(value);
366        }
367
368        Ok(Self {
369            raw: value.raw.clone(),
370            tags: value.tags.clone(),
371            channel: value.args[0].clone(),
372            data: value.data.clone(),
373        })
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use crate::test_util;
381
382    #[test]
383    fn user_notice() {
384        let inputs = [
385            "@badge-info=;badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-cumulative-months=6;msg-param-streak-months=2;msg-param-should-share-streak=1;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=12345678;subscriber=1;system-msg=ronni\\shas\\ssubscribed\\sfor\\s6\\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=87654321;user-type=staff :tmi.twitch.tv USERNOTICE #dallas :Great stream -- keep it up!\r\n",
386            "@badge-info=;badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=55554444;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\\sof\\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;system-msg=TWW2\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=87654321;user-type=staff :tmi.twitch.tv USERNOTICE #forstycup\r\n",
387            "@badge-info=;badges=turbo/1;color=#9ACD32;display-name=TestChannel;emotes=;id=3d830f12-795c-447d-af3c-ea05e40fbddb;login=testchannel;mod=0;msg-id=raid;msg-param-displayName=TestChannel;msg-param-login=testchannel;msg-param-viewerCount=15;room-id=33332222;subscriber=0;system-msg=15\\sraiders\\sfrom\\sTestChannel\\shave\\sjoined\\n!;tmi-sent-ts=1507246572675;turbo=1;user-id=123456;user-type= :tmi.twitch.tv USERNOTICE #othertestchannel\r\n",
388            "@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=87654321;subscriber=0;system-msg=Seventoes\\sis\\snew\\shere!;tmi-sent-ts=1508363903826;turbo=0;user-id=77776666;user-type= :tmi.twitch.tv USERNOTICE #seventoes :HeyGuys\r\n",
389        ];
390
391        for (input, (channel, data)) in inputs.into_iter().zip([
392            ("#dallas", Some("Great stream -- keep it up!")),
393            ("#forstycup", None),
394            ("#othertestchannel", None),
395            ("#seventoes", Some("HeyGuys")),
396        ]) {
397            let (raw, tags) = test_util::raw_and_tags(input);
398            assert_eq!(
399                crate::test_util::parse_as::<UserNotice>(input),
400                UserNotice {
401                    raw,
402                    tags,
403                    channel: Cow::from(channel),
404                    data: data.map(Cow::from)
405                }
406            );
407        }
408    }
409}