twitch_api/pubsub/
moderation.rs

1#![doc(alias = "mod")]
2#![doc(alias = "chat_moderator_actions")]
3#![allow(deprecated)]
4//! PubSub messages for moderator actions
5use crate::{pubsub, types};
6use serde_derive::{Deserialize, Serialize};
7
8/// A moderator performs an action in the channel.
9#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
10#[serde(into = "String", try_from = "String")]
11pub struct ChatModeratorActions {
12    /// The user_id to listen as. Can be fetched with the [Get Users](crate::helix::users::get_users) endpoint
13    pub user_id: u32,
14    /// The channel_id to listen to. Can be fetched with the [Get Users](crate::helix::users::get_users) endpoint
15    pub channel_id: u32,
16}
17
18impl_de_ser!(
19    ChatModeratorActions,
20    "chat_moderator_actions",
21    user_id,
22    channel_id
23);
24
25impl pubsub::Topic for ChatModeratorActions {
26    #[cfg(feature = "twitch_oauth2")]
27    const SCOPE: twitch_oauth2::Validator =
28        twitch_oauth2::validator![twitch_oauth2::Scope::ChannelModerate];
29
30    fn into_topic(self) -> pubsub::Topics { super::Topics::ChatModeratorActions(self) }
31}
32
33/// A moderation action. `moderation_action`
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
36#[non_exhaustive]
37pub struct ModerationAction {
38    /// Arguments for moderation_action
39    #[serde(deserialize_with = "pubsub::deserialize_default_from_null")]
40    pub args: Vec<String>,
41    /// User that did moderation action
42    #[serde(
43        default,
44        deserialize_with = "pubsub::deserialize_none_from_empty_string"
45    )]
46    pub created_by: Option<types::UserName>,
47    /// ID of user that did moderation action
48    #[serde(
49        default,
50        deserialize_with = "pubsub::deserialize_none_from_empty_string"
51    )]
52    pub created_by_user_id: Option<types::UserId>,
53    /// Moderation action is triggered from automod
54    #[serde(default)]
55    pub from_automod: bool,
56    /// Type of action
57    pub moderation_action: ModerationActionCommand,
58    /// ID of message associated with moderation action
59    #[serde(
60        default,
61        deserialize_with = "pubsub::deserialize_none_from_empty_string"
62    )]
63    pub msg_id: Option<types::MsgId>,
64    /// Target of moderation action
65    pub target_user_id: types::UserId,
66    /// Type of moderation
67    #[serde(rename = "type")]
68    pub type_: ModerationType,
69    // FIXME: Never filled
70    #[doc(hidden)]
71    #[serde(
72        default,
73        deserialize_with = "pubsub::deserialize_none_from_empty_string"
74    )]
75    pub target_user_login: Option<String>,
76    // FIXME: Not sure what this does
77    #[doc(hidden)]
78    #[serde(
79        default,
80        deserialize_with = "pubsub::deserialize_none_from_empty_string"
81    )]
82    pub created_at: Option<types::Timestamp>,
83}
84
85/// A moderator was added. `moderator_added`
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
87#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
88#[non_exhaustive]
89pub struct ModeratorAdded {
90    /// ID of channel where moderator was added
91    pub channel_id: types::UserId,
92    /// ID of added moderator
93    pub target_user_id: types::UserId,
94    /// Moderation action. Should be [`mod`](ModerationActionCommand::Mod)
95    pub moderation_action: ModerationActionCommand,
96    /// Username of added moderator
97    pub target_user_login: types::UserName,
98    /// ID of user that added moderator
99    pub created_by_user_id: types::UserId,
100    /// Username of user that added moderator
101    pub created_by: types::UserName,
102}
103
104/// A moderator was removed. `moderator_removed`
105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
106#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
107#[non_exhaustive]
108pub struct ModeratorRemoved {
109    /// ID of channel where moderator was added
110    pub channel_id: types::UserId,
111    /// ID of added moderator
112    pub target_user_id: types::UserId,
113    /// Moderation action. Should be [`unmod`](ModerationActionCommand::Unmod)
114    pub moderation_action: ModerationActionCommand,
115    /// Username of added moderator
116    pub target_user_login: types::UserName,
117    /// ID of user that added moderator
118    pub created_by_user_id: types::UserId,
119    /// Username of user that added moderator
120    pub created_by: types::UserName,
121}
122
123/// Channel Term actions
124#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
125#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
126#[non_exhaustive]
127pub struct ChannelTermsAction {
128    /// ID of channel where channel terms were changed
129    pub channel_id: types::UserId,
130    /// If the term added is temporary or not and if not, when it will expire.
131    #[serde(
132        default,
133        deserialize_with = "pubsub::deserialize_none_from_empty_string"
134    )]
135    pub expires_at: Option<types::Timestamp>,
136    /// If the term was permitted/denied because of a previous automod message
137    pub from_automod: bool,
138    /// Id of term
139    pub id: types::BlockedTermId,
140    /// User ID that caused the term
141    pub requester_id: types::UserId,
142    /// User name that caused the term
143    pub requester_login: types::UserName,
144    /// Term definition
145    pub text: String,
146    /// Type of action done
147    #[serde(rename = "type")]
148    pub type_: ChannelAction,
149    /// Defined if the term was updated, None if new.
150    #[serde(
151        default,
152        deserialize_with = "pubsub::deserialize_none_from_empty_string"
153    )]
154    pub updated_at: Option<types::Timestamp>,
155}
156
157/// Reply from [ChatModeratorActions]
158#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
159#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
160#[serde(tag = "type", content = "data")]
161#[non_exhaustive]
162pub enum ChatModeratorActionsReply {
163    /// A moderation action. `moderation_action`
164    #[serde(rename = "moderation_action")]
165    ModerationAction(ModerationAction),
166    /// A channel term was modified, added or removed
167    #[serde(rename = "channel_terms_action")]
168    ChannelTermsAction(ChannelTermsAction),
169    /// A moderator was added. `moderator_added`
170    #[serde(rename = "moderator_added")]
171    ModeratorAdded(ModeratorAdded),
172    /// A moderator was removed. `moderator_removed`
173    #[serde(rename = "moderator_removed")]
174    ModeratorRemoved(ModeratorRemoved),
175    /// Unban request denied
176    #[serde(rename = "deny_unban_request")]
177    DenyUnbanRequest(UnbanRequest),
178    /// Unban request approved
179    #[serde(rename = "approve_unban_request")]
180    ApproveUnbanRequest(UnbanRequest),
181    /// VIP Added
182    #[serde(rename = "vip_added")]
183    VipAdded(VipAdded),
184}
185
186/// User added as VIP
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
188#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
189pub struct VipAdded {
190    /// Id of channel where VIP was added
191    pub channel_id: types::UserId,
192    /// User who made target VIP (usually broadcaster)
193    pub created_by: types::UserName,
194    /// User ID of who made target VIP (usually broadcaster)
195    pub created_by_user_id: types::UserId,
196    /// User ID of who was made VIP
197    pub target_user_id: types::UserId,
198    /// User who was made VIP
199    pub target_user_login: types::UserName,
200}
201
202/// Unban request
203#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
205#[non_exhaustive]
206pub struct UnbanRequest {
207    /// Unban response created by user with id
208    pub created_by_id: types::UserId,
209    /// Unban response created by user with login
210    pub created_by_login: types::UserName,
211    /// Action taken, should be [ModerationActionCommand::ApproveUnbanRequest] or [ModerationActionCommand::DenyUnbanRequest]
212    pub moderation_action: ModerationActionCommand,
213    /// Message attached to unban request response
214    pub moderator_message: String,
215    /// Target user ID of unban request, e.g the user that was banned
216    pub target_user_id: types::UserId,
217    /// Target login of unban request, e.g the user that was banned
218    pub target_user_login: types::UserName,
219}
220
221/// A command
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224#[non_exhaustive]
225pub enum ModerationActionCommand {
226    /// Delete
227    ///
228    /// Given when a message is deleted with `/delete <msg-id>`
229    Delete,
230    /// Timeout
231    ///
232    /// Given when a user is timed-out with `/timeout <user> <time> <reason>`
233    Timeout,
234    /// Untimeout
235    ///
236    /// Given when a user is unbanned while under a timeout `/untimeout <user>` or `/unban <user>`
237    Untimeout,
238    /// Mod
239    ///
240    /// Given when a user is added as a moderator. `/mod <user>`.
241    ///
242    /// See [ChatModeratorActionsReply::ModeratorAdded] where this is given
243    Mod,
244    /// Unmod
245    ///
246    /// Given when a user is removed as a moderator, `/unmod <user>`
247    Unmod,
248    /// Modified automod properties
249    ///
250    /// Given when automod config is changed. I.e filtering changed etc
251    ModifiedAutomodProperties,
252    /// Ban
253    ///
254    /// Given when a user is banned with `/timeout <user> <reason>`
255    Ban,
256    /// Unban
257    ///
258    /// Given when a user is unbanned with `/unban <user>` or `/untimeout <user>`
259    Unban,
260    // TODO: (2021-11-06) is this still returned?
261    /// Automod message rejected
262    AutomodRejected,
263    /// Automod message approved
264    ApproveAutomodMessage,
265    /// Automod message denied
266    DeniedAutomodMessage,
267    /// Raid
268    ///
269    /// Given when editor/broadcaster does `/raid <channel>`
270    Raid,
271    /// Unraid
272    ///
273    /// Given when editor/broadcaster does `/unraid`
274    Unraid,
275    /// Slow-mode chat enabled
276    Slow,
277    #[serde(rename = "slowoff")]
278    /// Slow-mode chat disabled
279    SlowOff,
280    /// Followers-only chat enabled
281    Followers,
282    /// Followers-only chat disabled
283    #[serde(rename = "followersoff")]
284    FollowersOff,
285    /// Subscriber-only chat enabled
286    Subscribers,
287    /// Subscriber-only chat disabled
288    #[serde(rename = "subscribersoff")]
289    SubscribersOff,
290    /// Emote-only chat enabled
291    #[serde(rename = "emoteonly")]
292    EmoteOnly,
293    /// Emote-only chat disabled
294    #[serde(rename = "emoteonlyoff")]
295    EmoteOnlyOff,
296    /// Chat cleared for all viewers
297    Clear,
298    /// Unique chat enabled
299    #[serde(rename = "r9kbeta")]
300    R9KBeta,
301    /// Unique chat disabled
302    #[serde(rename = "r9kbetaoff")]
303    R9KBetaOff,
304    /// User added as VIP
305    ///
306    /// See also: [VipAdded], which **isn't** the same event.
307    Vip,
308    /// User removed as VIP
309    Unvip,
310    /// Channel host started
311    Host,
312    /// Channel host removed
313    Unhost,
314    /// Unban Request Approved
315    #[serde(rename = "APPROVE_UNBAN_REQUEST")]
316    ApproveUnbanRequest,
317    /// Unban Request Denied
318    #[serde(rename = "DENY_UNBAN_REQUEST")]
319    DenyUnbanRequest,
320    /// Users own message was deleted.
321    DeleteNotification,
322}
323
324/// A command
325#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
326#[serde(rename_all = "snake_case")]
327#[non_exhaustive]
328pub enum ChannelAction {
329    /// Automod permitted term added
330    AddPermittedTerm,
331    /// Automod permitted term removed
332    DeletePermittedTerm,
333    /// Automod blocked term added
334    AddBlockedTerm,
335    /// Automod blocked term removed
336    DeleteBlockedTerm,
337}
338
339impl std::fmt::Display for ModerationActionCommand {
340    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341        use serde::Serialize;
342        self.serialize(f)
343    }
344}
345
346/// Moderation type
347#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
348#[serde(rename_all = "snake_case")]
349#[non_exhaustive]
350pub enum ModerationType {
351    /// Chat moderated
352    ChatLoginModeration,
353    /// Channel moderated
354    ChatChannelModeration,
355    /// Chat targeted login moderation
356    ///
357    /// These events are sent when the [user](ChatModeratorActions::user_id) in the event is targeted by a moderation command.
358    ChatTargetedLoginModeration,
359}
360
361#[cfg(test)]
362mod tests {
363    #[allow(unused_imports)]
364    use super::super::{Response, TopicData};
365    use super::*;
366
367    #[test]
368    fn mod_action_delete() {
369        let source = r#"
370{
371    "type": "MESSAGE",
372    "data": {
373        "topic": "chat_moderator_actions.27620241.27620241",
374        "message": "{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_login_moderation\",\"moderation_action\":\"delete\",\"args\":[\"tmo\",\"bop\",\"e513c02d-dca5-4480-9af5-e6078d954e42\"],\"created_by\":\"emilgardis\",\"created_by_user_id\":\"27620241\",\"msg_id\":\"\",\"target_user_id\":\"1234\",\"target_user_login\":\"\",\"from_automod\":false}}"
375    }
376}"#;
377        let actual = dbg!(Response::parse(source).unwrap());
378        assert!(matches!(
379            actual,
380            Response::Message {
381                data: TopicData::ChatModeratorActions { .. },
382            }
383        ));
384    }
385    #[test]
386    fn check_deser() {
387        use std::convert::TryInto as _;
388        let s = "chat_moderator_actions.1337.1234";
389        assert_eq!(
390            ChatModeratorActions {
391                user_id: 1337,
392                channel_id: 1234,
393            },
394            s.to_string().try_into().unwrap()
395        );
396    }
397
398    #[test]
399    fn check_ser() {
400        let s = "chat_moderator_actions.1337.1234";
401        let right: String = ChatModeratorActions {
402            user_id: 1337,
403            channel_id: 1234,
404        }
405        .into();
406        assert_eq!(s.to_string(), right);
407    }
408
409    #[test]
410    fn mod_action_timeout() {
411        let source = r#"{"type":"MESSAGE","data":{"topic":"chat_moderator_actions.27620241.27620241","message":"{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_login_moderation\",\"moderation_action\":\"timeout\",\"args\":[\"tmo\",\"1\",\"\"],\"created_by\":\"emilgardis\",\"created_by_user_id\":\"27620241\",\"msg_id\":\"\",\"target_user_id\":\"1234\",\"target_user_login\":\"\",\"from_automod\":false}}"}}"#;
412        let actual = dbg!(Response::parse(source).unwrap());
413        assert!(matches!(
414            actual,
415            Response::Message {
416                data: TopicData::ChatModeratorActions { .. },
417            }
418        ));
419    }
420    #[test]
421    fn mod_add_moderator() {
422        let source = r#"{"type":"MESSAGE","data":{"topic":"chat_moderator_actions.27620241.27620241","message":"{\"type\":\"moderator_added\",  \"data\":{\"channel_id\":\"27620241\",\"target_user_id\":\"19264788\",\"moderation_action\":\"mod\",\"target_user_login\":\"nightbot\",\"created_by_user_id\":\"27620241\",\"created_by\":\"emilgardis\"}}"}}"#;
423        let actual = dbg!(Response::parse(source).unwrap());
424        assert!(matches!(
425            actual,
426            Response::Message {
427                data: TopicData::ChatModeratorActions { .. },
428            }
429        ));
430    }
431
432    #[test]
433    fn mod_add_moderator_no_user_id() {
434        let source = r#"{"type":"MESSAGE","data":{"topic":"chat_moderator_actions.27620241.27620241","message":"{\"type\":\"moderator_added\",  \"data\":{\"channel_id\":\"27620241\",\"target_user_id\":\"19264788\",\"moderation_action\":\"mod\",\"target_user_login\":\"nightbot\",\"created_by_user_id\":\"27620241\",\"created_by\":\"emilgardis\"}}"}}"#;
435        let actual = dbg!(Response::parse(source).unwrap());
436        assert!(matches!(
437            actual,
438            Response::Message {
439                data: TopicData::ChatModeratorActions { .. },
440            }
441        ));
442    }
443
444    #[test]
445    fn mod_remove_moderator() {
446        let source = r#"{"type":"MESSAGE","data":{"topic":"chat_moderator_actions.691109305.129546453","message":"{\"type\":\"moderator_removed\",\"data\":{\"channel_id\":\"129546453\",\"target_user_id\":\"691109305\",\"moderation_action\":\"unmod\",\"target_user_login\":\"rewardmore\",\"created_by_user_id\":\"129546453\",\"created_by\":\"nerixyz\"}}"}}"#;
447        let actual = dbg!(Response::parse(source).unwrap());
448        assert!(matches!(
449            actual,
450            Response::Message {
451                data: TopicData::ChatModeratorActions { .. },
452            }
453        ));
454    }
455    #[test]
456    fn mod_targeted_delete() {
457        let source = r#"{"type":"MESSAGE","data":{"topic":"chat_moderator_actions.27620241.80525799","message":"{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_targeted_login_moderation\",\"moderation_action\":\"delete_notification\",\"args\":[\"you have the moonpool no?\"],\"msg_id\":\"b7ffbf8a-ca9f-497e-bc6f-ae0e606e99dc\",\"target_user_id\":\"27620241\",\"target_user_login\":\"emilgardis\"}}"}}"#;
458        let actual = dbg!(Response::parse(source).unwrap());
459        assert!(matches!(
460            actual,
461            Response::Message {
462                data: TopicData::ChatModeratorActions { .. },
463            }
464        ));
465    }
466    #[test]
467    fn mod_automod() {
468        let source = r#"
469{
470    "type": "MESSAGE",
471    "data": {
472        "topic": "chat_moderator_actions.27620241.27620241",
473        "message": "{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_channel_moderation\",\"moderation_action\":\"modified_automod_properties\",\"args\":null,\"created_by\":\"emilgardis\",\"created_by_user_id\":\"27620241\",\"msg_id\":\"\",\"target_user_id\":\"\",\"target_user_login\":\"\",\"from_automod\":false}}"
474    }
475}"#;
476        let actual = dbg!(Response::parse(source).unwrap());
477        assert!(matches!(
478            actual,
479            Response::Message {
480                data: TopicData::ChatModeratorActions { .. },
481            }
482        ));
483    }
484
485    #[test]
486    fn mod_automod_delete_blocked_term() {
487        let source = r#"
488{
489    "type": "MESSAGE",
490    "data": {
491        "topic": "chat_moderator_actions.27620241.27620241",
492        "message": "{\"type\":\"channel_terms_action\",\"data\":{\"type\":\"delete_blocked_term\",\"id\":\"41a8f582-4c60-4ca1-aa10-91ec06161118\",\"text\":\"Hype\",\"requester_id\":\"27620241\",\"requester_login\":\"emilgardis\",\"channel_id\":\"27620241\",\"expires_at\":\"\",\"updated_at\":\"2021-05-10T21:35:28.745222679Z\",\"from_automod\":false}}"
493    }
494}"#;
495        let actual = dbg!(Response::parse(source).unwrap());
496        assert!(matches!(
497            actual,
498            Response::Message {
499                data: TopicData::ChatModeratorActions { .. },
500            }
501        ));
502    }
503
504    #[test]
505    fn mod_slowmode() {
506        let source = r#"
507{
508    "type": "MESSAGE",
509    "data": {
510        "topic": "chat_moderator_actions.27620241.27620241",
511        "message": "{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_channel_moderation\",\"moderation_action\":\"slow\",\"args\":[\"5\"],\"created_by\":\"tmo\",\"created_by_user_id\":\"1234\",\"msg_id\":\"\",\"target_user_id\":\"\",\"target_user_login\":\"\",\"from_automod\":false}}"
512    }
513}"#;
514        let actual = dbg!(Response::parse(source).unwrap());
515        assert!(matches!(
516            actual,
517            Response::Message {
518                data: TopicData::ChatModeratorActions { .. },
519            }
520        ));
521    }
522
523    #[test]
524    #[cfg(not(feature = "deny_unknown_fields"))]
525    fn allow_unknown() {
526        let source = r#"
527{
528    "type": "MESSAGE",
529    "data": {
530        "topic": "chat_moderator_actions.27620241.27620241",
531        "message": "{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_channel_moderation\",\"moderation_action\":\"slow\",\"unknownfield\": 1,\"args\":[\"5\"],\"created_by\":\"tmo\",\"created_by_user_id\":\"1234\",\"msg_id\":\"\",\"target_user_id\":\"\",\"target_user_login\":\"\",\"from_automod\":false}}"
532    }
533}"#;
534        let actual = dbg!(Response::parse(source).unwrap());
535        assert!(matches!(
536            actual,
537            Response::Message {
538                data: TopicData::ChatModeratorActions { .. },
539            }
540        ));
541    }
542
543    #[test]
544    fn deny_unban_request() {
545        let source = r#"
546{
547    "type": "MESSAGE",
548    "data": {
549        "topic": "chat_moderator_actions.80525799.80525799",
550        "message": "{\"type\":\"deny_unban_request\",\"data\":{\"moderation_action\":\"DENY_UNBAN_REQUEST\",\"created_by_id\":\"27620241\",\"created_by_login\":\"emilgardis\",\"moderator_message\":\"ok\",\"target_user_id\":\"465894629\",\"target_user_login\":\"emil_the_impostor\"}}"
551    }
552}"#;
553        let actual = dbg!(Response::parse(source).unwrap());
554        assert!(matches!(
555            actual,
556            Response::Message {
557                data: TopicData::ChatModeratorActions { .. },
558            }
559        ));
560    }
561
562    #[test]
563    fn approve_unban_request() {
564        let source = r#"
565{
566    "type": "MESSAGE",
567    "data": {
568        "topic": "chat_moderator_actions.80525799.80525799",
569        "message": "{\"type\":\"approve_unban_request\",\"data\":{\"moderation_action\":\"APPROVE_UNBAN_REQUEST\",\"created_by_id\":\"27620241\",\"created_by_login\":\"emilgardis\",\"moderator_message\":\"ok\",\"target_user_id\":\"465894629\",\"target_user_login\":\"emil_the_impostor\"}}"
570    }
571}"#;
572        let actual = dbg!(Response::parse(source).unwrap());
573        assert!(matches!(
574            actual,
575            Response::Message {
576                data: TopicData::ChatModeratorActions { .. },
577            }
578        ));
579    }
580
581    #[test]
582    fn vip_added() {
583        let source = r#"
584        {
585            "type": "MESSAGE",
586            "data": {
587                "topic": "chat_moderator_actions.80525799.80525799",
588                "message": "{\"type\":\"vip_added\",\"data\":{\"channel_id\":\"80525799\",\"target_user_id\":\"56345511\",\"target_user_login\":\"bossquest\",\"created_by_user_id\":\"80525799\",\"created_by\":\"sessis\"}}"
589            }
590        }"#;
591        let actual = dbg!(Response::parse(source).unwrap());
592        assert!(matches!(
593            actual,
594            Response::Message {
595                data: TopicData::ChatModeratorActions { .. },
596            }
597        ));
598    }
599
600    #[test]
601    fn vip_added_mod_action() {
602        let source = r#"
603        {
604            "type": "MESSAGE",
605            "data": {
606                "topic": "chat_moderator_actions.691109305.120183018",
607                "message": "{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_login_moderation\",\"moderation_action\":\"vip\",\"args\":[\"Floikka\"],\"created_by\":\"nam______________________\",\"created_by_user_id\":\"120183018\",\"created_at\":\"2022-12-20T16:41:26.168122804Z\",\"msg_id\":\"\",\"target_user_id\":\"85262774\",\"target_user_login\":\"\",\"from_automod\":false}}"
608            }
609        }"#;
610        let actual = dbg!(Response::parse(source).unwrap());
611        assert!(matches!(
612            actual,
613            Response::Message {
614                data: TopicData::ChatModeratorActions { .. },
615            }
616        ));
617    }
618
619    #[test]
620    fn vip_removed() {
621        let source = r#"
622        {
623            "type": "MESSAGE",
624            "data": {
625                "topic": "chat_moderator_actions.27620241.27620241",
626                "message": "{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_login_moderation\",\"moderation_action\":\"unvip\",\"args\":[\"emil_the_impostor\"],\"created_by\":\"emilgardis\",\"created_by_user_id\":\"27620241\",\"created_at\":\"2021-07-27T22:28:31.075027599Z\",\"msg_id\":\"\",\"target_user_id\":\"465894629\",\"target_user_login\":\"\",\"from_automod\":false}}"
627            }
628        }"#;
629        let actual = dbg!(Response::parse(source).unwrap());
630        assert!(matches!(
631            actual,
632            Response::Message {
633                data: TopicData::ChatModeratorActions { .. },
634            }
635        ));
636    }
637
638    #[test]
639    fn unraid() {
640        let source = r#"
641        {
642            "type": "MESSAGE",
643            "data": {
644                "topic": "chat_moderator_actions.27620241.27620241",
645                "message": "{\"type\":\"moderation_action\",\"data\":{\"type\":\"chat_channel_moderation\",\"moderation_action\":\"unraid\",\"args\":[\"emilgradis\"],\"created_by\":\"emilgardis\",\"created_by_user_id\":\"27620241\",\"created_at\":\"\",\"msg_id\":\"\",\"target_user_id\":\"\",\"target_user_login\":\"\",\"from_automod\":false}}"
646            }
647        }"#;
648        let actual = dbg!(Response::parse(source).unwrap());
649        assert!(matches!(
650            actual,
651            Response::Message {
652                data: TopicData::ChatModeratorActions { .. },
653            }
654        ));
655    }
656}