1#![doc(alias = "mod")]
2#![doc(alias = "chat_moderator_actions")]
3#![allow(deprecated)]
4use crate::{pubsub, types};
6use serde_derive::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
10#[serde(into = "String", try_from = "String")]
11pub struct ChatModeratorActions {
12 pub user_id: u32,
14 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#[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 #[serde(deserialize_with = "pubsub::deserialize_default_from_null")]
40 pub args: Vec<String>,
41 #[serde(
43 default,
44 deserialize_with = "pubsub::deserialize_none_from_empty_string"
45 )]
46 pub created_by: Option<types::UserName>,
47 #[serde(
49 default,
50 deserialize_with = "pubsub::deserialize_none_from_empty_string"
51 )]
52 pub created_by_user_id: Option<types::UserId>,
53 #[serde(default)]
55 pub from_automod: bool,
56 pub moderation_action: ModerationActionCommand,
58 #[serde(
60 default,
61 deserialize_with = "pubsub::deserialize_none_from_empty_string"
62 )]
63 pub msg_id: Option<types::MsgId>,
64 pub target_user_id: types::UserId,
66 #[serde(rename = "type")]
68 pub type_: ModerationType,
69 #[doc(hidden)]
71 #[serde(
72 default,
73 deserialize_with = "pubsub::deserialize_none_from_empty_string"
74 )]
75 pub target_user_login: Option<String>,
76 #[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#[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 pub channel_id: types::UserId,
92 pub target_user_id: types::UserId,
94 pub moderation_action: ModerationActionCommand,
96 pub target_user_login: types::UserName,
98 pub created_by_user_id: types::UserId,
100 pub created_by: types::UserName,
102}
103
104#[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 pub channel_id: types::UserId,
111 pub target_user_id: types::UserId,
113 pub moderation_action: ModerationActionCommand,
115 pub target_user_login: types::UserName,
117 pub created_by_user_id: types::UserId,
119 pub created_by: types::UserName,
121}
122
123#[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 pub channel_id: types::UserId,
130 #[serde(
132 default,
133 deserialize_with = "pubsub::deserialize_none_from_empty_string"
134 )]
135 pub expires_at: Option<types::Timestamp>,
136 pub from_automod: bool,
138 pub id: types::BlockedTermId,
140 pub requester_id: types::UserId,
142 pub requester_login: types::UserName,
144 pub text: String,
146 #[serde(rename = "type")]
148 pub type_: ChannelAction,
149 #[serde(
151 default,
152 deserialize_with = "pubsub::deserialize_none_from_empty_string"
153 )]
154 pub updated_at: Option<types::Timestamp>,
155}
156
157#[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 #[serde(rename = "moderation_action")]
165 ModerationAction(ModerationAction),
166 #[serde(rename = "channel_terms_action")]
168 ChannelTermsAction(ChannelTermsAction),
169 #[serde(rename = "moderator_added")]
171 ModeratorAdded(ModeratorAdded),
172 #[serde(rename = "moderator_removed")]
174 ModeratorRemoved(ModeratorRemoved),
175 #[serde(rename = "deny_unban_request")]
177 DenyUnbanRequest(UnbanRequest),
178 #[serde(rename = "approve_unban_request")]
180 ApproveUnbanRequest(UnbanRequest),
181 #[serde(rename = "vip_added")]
183 VipAdded(VipAdded),
184}
185
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
188#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
189pub struct VipAdded {
190 pub channel_id: types::UserId,
192 pub created_by: types::UserName,
194 pub created_by_user_id: types::UserId,
196 pub target_user_id: types::UserId,
198 pub target_user_login: types::UserName,
200}
201
202#[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 pub created_by_id: types::UserId,
209 pub created_by_login: types::UserName,
211 pub moderation_action: ModerationActionCommand,
213 pub moderator_message: String,
215 pub target_user_id: types::UserId,
217 pub target_user_login: types::UserName,
219}
220
221#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224#[non_exhaustive]
225pub enum ModerationActionCommand {
226 Delete,
230 Timeout,
234 Untimeout,
238 Mod,
244 Unmod,
248 ModifiedAutomodProperties,
252 Ban,
256 Unban,
260 AutomodRejected,
263 ApproveAutomodMessage,
265 DeniedAutomodMessage,
267 Raid,
271 Unraid,
275 Slow,
277 #[serde(rename = "slowoff")]
278 SlowOff,
280 Followers,
282 #[serde(rename = "followersoff")]
284 FollowersOff,
285 Subscribers,
287 #[serde(rename = "subscribersoff")]
289 SubscribersOff,
290 #[serde(rename = "emoteonly")]
292 EmoteOnly,
293 #[serde(rename = "emoteonlyoff")]
295 EmoteOnlyOff,
296 Clear,
298 #[serde(rename = "r9kbeta")]
300 R9KBeta,
301 #[serde(rename = "r9kbetaoff")]
303 R9KBetaOff,
304 Vip,
308 Unvip,
310 Host,
312 Unhost,
314 #[serde(rename = "APPROVE_UNBAN_REQUEST")]
316 ApproveUnbanRequest,
317 #[serde(rename = "DENY_UNBAN_REQUEST")]
319 DenyUnbanRequest,
320 DeleteNotification,
322}
323
324#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
326#[serde(rename_all = "snake_case")]
327#[non_exhaustive]
328pub enum ChannelAction {
329 AddPermittedTerm,
331 DeletePermittedTerm,
333 AddBlockedTerm,
335 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
348#[serde(rename_all = "snake_case")]
349#[non_exhaustive]
350pub enum ModerationType {
351 ChatLoginModeration,
353 ChatChannelModeration,
355 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}