twitch_api 0.7.2

Library for talking with the new Twitch API aka. "Helix", EventSub and more!
Documentation
//! PubSub messages for when AutoMod flags a message as potentially inappropriate, and when a moderator takes action on a message.
use crate::{pubsub, types};
use serde_derive::{Deserialize, Serialize};

/// A user follows the channel
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(into = "String", try_from = "String")]
pub struct AutoModQueue {
    /// The currently authenticated moderator
    pub moderator_id: u32,
    /// The channel_id to watch. Can be fetched with the [Get Users](crate::helix::users::get_users) endpoint
    pub channel_id: u32,
}

impl_de_ser!(
    AutoModQueue,
    "automod-queue",
    moderator_id,
    channel_id // FIXME: add trailing comma
);

impl pubsub::Topic for AutoModQueue {
    #[cfg(feature = "twitch_oauth2")]
    const SCOPE: twitch_oauth2::Validator =
        twitch_oauth2::validator![twitch_oauth2::Scope::ChannelModerate];

    fn into_topic(self) -> pubsub::Topics { super::Topics::AutoModQueue(self) }
}

/// Reply from [AutoModQueue]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[serde(tag = "type", content = "data")]
#[non_exhaustive]
pub enum AutoModQueueReply {
    /// Message held by automod
    #[serde(rename = "automod_caught_message")]
    AutoModCaughtMessage(AutoModCaughtMessage),
}

/// Message held by automod
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct AutoModCaughtMessage {
    /// Classification of caught message
    pub content_classification: ContentClassification,
    /// The message that was sent
    pub message: Message,
    // TODO: What is this?
    /// Code for reason
    #[serde(
        default,
        deserialize_with = "pubsub::deserialize_none_from_empty_string"
    )]
    pub reason_code: Option<String>,
    /// User ID of who resolved the message in the queue
    #[serde(
        default,
        deserialize_with = "pubsub::deserialize_none_from_empty_string"
    )]
    pub resolver_id: Option<types::UserId>,
    /// Username of who resolved the message in the queue
    #[serde(
        default,
        deserialize_with = "pubsub::deserialize_none_from_empty_string"
    )]
    pub resolver_login: Option<types::UserName>,
    /// Status of the message in the queue
    pub status: types::AutomodStatus,
}

/// Classification for content according to AutoMod
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct ContentClassification {
    // FIXME: Enum?
    /// Category for classification
    ///
    /// On twitch, these are the different categories available for AutoMod
    ///
    /// * Aggression
    ///   Threatening, inciting, or promoting violence or other harm
    /// * Bullying: namecalling
    ///   Name-calling, insults, or antagonization
    /// * Disability
    ///   Demonstrating hatred or prejudice based on perceived or actual mental or physical abilities
    /// * Sexuality, sex, or gender
    ///   Demonstrating hatred or prejudice based on sexual identity, sexual orientation, gender identity, or gender expression
    /// * Misogyny: misogyny
    ///   Demonstrating hatred or prejudice against women, including sexual objectification
    /// * Race, ethnicity, or religion: racism
    ///   Demonstrating hatred or prejudice based on race, ethnicity, or religion
    /// * Sex-based terms: sexwords
    ///   Sexual acts, anatomy
    /// * Swearing: swearing
    ///   Swear words, &*^!#@%
    pub category: String,
    /// Level of classification, eg. how strongly related the classification is related according to AutoMod
    pub level: i64,
}

/// Message that was caught by AutoMod
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Message {
    /// The content of the message
    pub content: Content,
    /// Chat ID of the message
    pub id: types::MsgId,
    /// User that sent the message
    pub sender: MessageUser,
    /// Time at which the message was sent
    pub sent_at: types::Timestamp,
    /// Language of the part of the message that was caught
    pub non_broadcaster_language: Option<String>,
}

/// A user according to Automod
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct MessageUser {
    /// ID of the user
    pub user_id: types::UserId,
    /// Login name of the user, not capitalized
    pub login: types::UserName,
    /// Display name of user
    pub display_name: types::DisplayName,
    /// Senders badges
    #[serde(default)]
    pub badges: Vec<MessageUserBadges>,
    /// Color of the user
    pub chat_color: Option<String>,
}

/// A users badges in the chat
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct MessageUserBadges {
    // FIXME: Enum?
    /// Id or type of the badge
    pub id: String,
    /// Version of the badge
    ///
    /// e.g `1000` for tier 1, `2000` for tier 2, etc.
    pub version: String,
}

/// The contents of a AutoMod message
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Content {
    /// The message split up in fragments.
    ///
    /// The message can be retrieved in full with [`text`](Self::text)
    pub fragments: Vec<Fragment>,
    /// The full message that was sent
    pub text: String,
}

/// A fragment of a AutoModded message
///
/// Can either be regular text, or classified as part of the reason for AutoMod
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[serde(untagged)]
#[non_exhaustive]
pub enum Fragment {
    /// Fragment that is classified under a AutoMod category which is being filtered out
    AutomodFragment {
        /// Text associated with this fragment
        text: String,
        /// AutoMod classification of the fragment
        automod: Automod,
    },
    /// Fragment that is not classified under a AutoMod category
    TextFragment {
        /// Text associated with this fragment
        text: String,
    },
    /// A text fragment that mentions another user
    UserMention {
        /// Text associated with this fragment
        text: String,
        /// User mentioned
        user_mention: FragmentUserMention,
    },
}

/// A mentioned user in a fragment
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct FragmentUserMention {
    /// User ID of the user
    #[serde(rename = "userID")]
    pub user_id: types::UserId,
    /// Username of the user
    pub login: types::UserName,
    /// Display name of the user
    pub display_name: types::DisplayName,
}

/// Specific AutoMod classification
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Automod {
    // FIXME: This should be a hash map of enum, i64
    /// The different topics and their level for the automod reason.
    ///
    /// # Examples
    ///
    /// ```text
    /// "topics": {
    ///     "vulgar": 6
    /// }
    /// ```
    pub topics: std::collections::HashMap<String, i64>,
}

#[cfg(test)]
mod tests {
    use super::super::{Response, TopicData};
    use super::*;
    #[test]
    fn automodcaught() {
        let source = r#"
        {"type":"MESSAGE","data":{"topic":"automod-queue.27620241.27620241","message":"{\"type\":\"automod_caught_message\",\"data\":{\"content_classification\":{\"category\":\"swearing\",\"level\":2},\"message\":{\"content\":{\"text\":\"fuck you xd\",\"fragments\":[{\"text\":\"fuck you\",\"automod\":{\"topics\":{\"vulgar\":6}}},{\"text\":\" xd\"}]},\"id\":\"a7e3f713-b220-444a-b54a-348b981b6bf0\",\"sender\":{\"user_id\":\"268131879\",\"login\":\"prettyb0i_swe\",\"display_name\":\"prettyb0i_swe\"},\"sent_at\":\"2021-05-17T19:28:31.062898778Z\"},\"reason_code\":\"\",\"resolver_id\":\"27620241\",\"resolver_login\":\"emilgardis\",\"status\":\"DENIED\"}}"}}
        "#;
        let actual = dbg!(Response::parse(source).unwrap());
        assert!(matches!(
            actual,
            Response::Message {
                data: TopicData::AutoModQueue { .. },
            }
        ));
    }

    #[test]
    fn automodcaught2() {
        let source = r#"
        {"type":"MESSAGE","data":{"topic":"automod-queue.27620241.27620241","message":"{\"type\":\"automod_caught_message\",\"data\":{\"content_classification\":{\"category\":\"aggression\",\"level\":4},\"message\":{\"content\":{\"text\":\"you suck balls\",\"fragments\":[{\"text\":\"you suck balls\",\"automod\":{\"topics\":{\"bullying\":3,\"dating_and_sexting\":7,\"vulgar\":5}}}]},\"id\":\"23b15313-ff6c-4e1c-8d0d-ea9c382a3806\",\"sender\":{\"user_id\":\"268131879\",\"login\":\"prettyb0i_swe\",\"display_name\":\"prettyb0i_swe\"},\"sent_at\":\"2021-05-29T13:12:41.237693525Z\"},\"reason_code\":\"\",\"resolver_id\":\"\",\"resolver_login\":\"\",\"status\":\"PENDING\"}}"}}
        "#;
        let actual = dbg!(Response::parse(source).unwrap());
        assert!(matches!(
            actual,
            Response::Message {
                data: TopicData::AutoModQueue { .. },
            }
        ));
    }

    #[test]
    fn automodcaught3() {
        let source = r##"
        {"type":"MESSAGE","data":{"topic":"automod-queue.27620241.27620241","message":"{\"type\":\"automod_caught_message\",\"data\":{\"content_classification\":{\"category\":\"aggression\",\"level\":1},\"message\":{\"content\":{\"text\":\"No I have been told that I can have an I;ll kill you face that scares the crap out of people when I am annoyed with ot angry at them. SO be it. It takes a lot to get me in that mood so you deserve it. @Emilgardis\",\"fragments\":[{\"text\":\"No I have been told that I can have an \"},{\"text\":\"I;ll kill you\",\"automod\":{\"topics\":{\"bullying\":7}}},{\"text\":\" face that scares the crap out of people when I am annoyed with ot angry at them. SO be it. It takes a lot to get me in that mood so you deserve it. \"},{\"text\":\"@Emilgardis\",\"user_mention\":{\"userID\":\"27620241\",\"login\":\"emilgardis\",\"display_name\":\"Emilgardis\"}}]},\"id\":\"87b2ae08-ac64-43e7-b2b7-28ae168e00ce\",\"sender\":{\"user_id\":\"1234\",\"login\":\"justintvfan\",\"display_name\":\"justintvfan\",\"chat_color\":\"#DAA520\",\"badges\":[{\"id\":\"subscriber\",\"version\":\"18\"},{\"id\":\"bits\",\"version\":\"1000\"}]},\"sent_at\":\"2021-06-27T19:28:48.747156458Z\"},\"reason_code\":\"\",\"resolver_id\":\"27620241\",\"resolver_login\":\"emilgardis\",\"status\":\"ALLOWED\"}}"}}
        "##;
        let actual = dbg!(Response::parse(source).unwrap());
        assert!(matches!(
            actual,
            Response::Message {
                data: TopicData::AutoModQueue { .. },
            }
        ));
    }

    #[test]
    fn automodcaught_foreign() {
        let source = r##"
{
    "type": "MESSAGE",
    "data": {
        "topic": "automod-queue.27620241.27620241",
        "message": "{\"type\":\"automod_caught_message\",\"data\":{\"content_classification\":{\"category\":\"homophobia\",\"level\":1},\"message\":{\"content\":{\"text\":\"Automod had an issues with the word deps?\",\"fragments\":[{\"text\":\"Automod had an issues with the word \"},{\"text\":\"deps?\",\"automod\":{\"topics\":{\"identity\":7}}}]},\"id\":\"933829c6-9db6-4b16-8f9d-4569cd4dd8d7\",\"sender\":{\"user_id\":\"1234\",\"login\":\"justinfan123\",\"display_name\":\"justinfan123\",\"chat_color\":\"#B382E8\",\"badges\":[{\"id\":\"partner\",\"version\":\"1\"}]},\"sent_at\":\"2021-10-18T19:12:01.860963699Z\",\"non_broadcaster_language\":\"fr\"},\"reason_code\":\"\",\"resolver_id\":\"\",\"resolver_login\":\"\",\"status\":\"PENDING\"}}"
    }
}"##;
        let actual = dbg!(Response::parse(source).unwrap());
        assert!(matches!(
            actual,
            Response::Message {
                data: TopicData::AutoModQueue { .. },
            }
        ));
    }

    #[test]
    fn check_deser() {
        use std::convert::TryInto as _;
        let s = "automod-queue.27620241.27620241";
        assert_eq!(
            AutoModQueue {
                channel_id: 27620241,
                moderator_id: 27620241
            },
            s.to_string().try_into().unwrap()
        );
    }

    #[test]
    fn check_ser() {
        let s = "automod-queue.27620241.27620241";
        let right: String = AutoModQueue {
            channel_id: 27620241,
            moderator_id: 27620241,
        }
        .into();
        assert_eq!(s.to_string(), right);
    }
}