1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#![doc(alias = "bits")]
#![doc(alias = "channel-bits-events-v2")]
//! PubSub messages for bits
use crate::{pubsub, types};
use serde::{Deserialize, Serialize};

/// Anyone cheers in a specified channel.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub struct ChannelBitsEventsV2 {
    /// 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!(ChannelBitsEventsV2, "channel-bits-events-v2", channel_id);

impl pubsub::Topic for ChannelBitsEventsV2 {
    #[cfg(feature = "twitch_oauth2")]
    const SCOPE: &'static [twitch_oauth2::Scope] = &[twitch_oauth2::Scope::BitsRead];
}

/// Reply from [ChannelBitsEventsV2]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[serde(tag = "message_type")]
#[non_exhaustive]
pub enum ChannelBitsEventsV2Reply {
    /// Bits event
    #[serde(rename = "bits_event")]
    BitsEvent {
        /// Data associated with reply
        data: BitsEventData,
        /// Message ID of message associated with this `bits_event`
        message_id: String,
        /// Version of `channel-bits-events-v2` reply
        version: String,
        #[doc(hidden)]
        #[serde(default)] // FIXME: docs seems to be wrong here.
        is_anonymous: bool,
    },
}

/// Data for bits event
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct BitsEventData {
    /// If set, describes new unlocked badge for user
    pub badge_entitlement: Option<BadgeEntitlement>,
    /// The number of bits that were sent.
    pub bits_used: i64,
    /// ID of channel where message was sent
    pub channel_id: types::UserId,
    /// Username of channel where message was sent
    pub channel_name: types::UserName,
    /// The full message that was sent with the bits.
    pub chat_message: String,
    /// Context of `bits_event`, seems to only be [`cheer`](BitsContext::Cheer)
    pub context: BitsContext,
    #[serde(default)] // FIXME: docs don't have this field here, but actual responses do
    /// Whether the cheer was anonymous.
    pub is_anonymous: bool,
    /// Time when pubsub message was sent
    pub time: types::Timestamp,
    /// The total number of bits that were ever sent by the user in the channel.
    pub total_bits_used: i64,
    /// ID of user that sent message
    pub user_id: types::UserId,
    /// Name of user that sent message
    pub user_name: types::UserName,
}

/// [`ChannelBitsEventsV2Reply::BitsEvent`] event unlocked new badge for user.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct BadgeEntitlement {
    /// New version of badge
    new_version: u64,
    /// Previous version of badge
    previous_version: u64,
}

/// Context that triggered pubsub message
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BitsContext {
    /// Cheer
    #[serde(rename = "cheer")]
    Cheer,
}

#[cfg(test)]
mod tests {
    use super::super::{Response, TopicData};
    use super::*;
    #[test]
    fn bits_event() {
        let source = r#"
{
    "type": "MESSAGE",
    "data": {
        "topic": "channel-bits-events-v2.1234",
        "message": "{\"data\":{\"user_name\":\"justintv\",\"channel_name\":\"tmi\",\"user_id\":\"12345\",\"channel_id\":\"1234\",\"time\":\"2020-10-19T17:50:24.807841596Z\",\"chat_message\":\"Corgo1 Corgo1 Corgo1 Corgo1 Corgo1\",\"bits_used\":5,\"total_bits_used\":29,\"is_anonymous\":false,\"context\":\"cheer\",\"badge_entitlement\":null},\"version\":\"1.0\",\"message_type\":\"bits_event\",\"message_id\":\"d1831817-95f2-5dfa-8864-f36f16eeb5d8\"}"
    }
}"#;
        let actual = dbg!(Response::parse(source).unwrap());
        assert!(matches!(
            actual,
            Response::Message {
                data: TopicData::ChannelBitsEventsV2 { .. },
            }
        ));
    }

    #[test]
    fn bits_event_documented() {
        let source = r#"
{
    "type": "MESSAGE",
    "data": {
       "topic": "channel-bits-events-v2.46024993",
       "message": "{\"data\":{\"user_name\":\"jwp\",\"channel_name\":\"bontakun\",\"user_id\":\"95546976\",\"channel_id\":\"46024993\",\"time\":\"2017-02-09T13:23:58.168Z\",\"chat_message\":\"cheer10000 New badge hype!\",\"bits_used\":10000,\"total_bits_used\":25000,\"context\":\"cheer\",\"badge_entitlement\":{\"new_version\":25000,\"previous_version\":10000}},\"version\":\"1.0\",\"message_type\":\"bits_event\",\"message_id\":\"8145728a4-35f0-4cf7-9dc0-f2ef24de1eb6\",\"is_anonymous\":true}"
    }
}
"#;

        let actual = dbg!(Response::parse(source).unwrap());
        assert!(matches!(
            actual,
            Response::Message {
                data: TopicData::ChannelBitsEventsV2 { .. },
            }
        ));
    }
    #[test]
    fn check_deser() {
        use std::convert::TryInto as _;
        let s = "channel-bits-events-v2.1234";
        assert_eq!(
            ChannelBitsEventsV2 { channel_id: 1234 },
            s.to_string().try_into().unwrap()
        );
    }

    #[test]
    fn check_ser() {
        let s = "channel-bits-events-v2.1234";
        let right: String = ChannelBitsEventsV2 { channel_id: 1234 }.into();
        assert_eq!(s.to_string(), right);
    }
}