twitch_api/pubsub/
channel_bits.rs

1#![doc(alias = "bits")]
2#![doc(alias = "channel-bits-events-v2")]
3//! PubSub messages for bits
4use crate::{pubsub, types};
5use serde_derive::{Deserialize, Serialize};
6
7/// Anyone cheers in a specified channel.
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
9#[serde(into = "String", try_from = "String")]
10pub struct ChannelBitsEventsV2 {
11    /// The channel_id to watch. Can be fetched with the [Get Users](crate::helix::users::get_users) endpoint
12    pub channel_id: u32,
13}
14
15impl_de_ser!(ChannelBitsEventsV2, "channel-bits-events-v2", channel_id);
16
17impl pubsub::Topic for ChannelBitsEventsV2 {
18    #[cfg(feature = "twitch_oauth2")]
19    const SCOPE: twitch_oauth2::Validator =
20        twitch_oauth2::validator![twitch_oauth2::Scope::BitsRead];
21
22    fn into_topic(self) -> pubsub::Topics { super::Topics::ChannelBitsEventsV2(self) }
23}
24
25/// Reply from [ChannelBitsEventsV2]
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
28#[serde(tag = "message_type")]
29#[non_exhaustive]
30pub enum ChannelBitsEventsV2Reply {
31    /// Bits event
32    #[serde(rename = "bits_event")]
33    BitsEvent {
34        /// Data associated with reply
35        data: BitsEventData,
36        /// Message ID of message associated with this `bits_event`
37        message_id: String,
38        /// Version of `channel-bits-events-v2` reply
39        version: String,
40        #[doc(hidden)]
41        #[serde(default)] // FIXME: docs seems to be wrong here.
42        is_anonymous: bool,
43    },
44}
45
46/// Data for bits event
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
49#[non_exhaustive]
50pub struct BitsEventData {
51    /// If set, describes new unlocked badge for user
52    pub badge_entitlement: Option<BadgeEntitlement>,
53    /// The number of bits that were sent.
54    pub bits_used: i64,
55    /// ID of channel where message was sent
56    pub channel_id: types::UserId,
57    /// Username of channel where message was sent
58    pub channel_name: types::UserName,
59    /// The full message that was sent with the bits.
60    pub chat_message: String,
61    /// Context of `bits_event`, seems to only be [`cheer`](BitsContext::Cheer)
62    pub context: BitsContext,
63    #[serde(default)] // FIXME: docs don't have this field here, but actual responses do
64    /// Whether the cheer was anonymous.
65    pub is_anonymous: bool,
66    /// Time when pubsub message was sent
67    pub time: types::Timestamp,
68    /// The total number of bits that were ever sent by the user in the channel.
69    pub total_bits_used: i64,
70    /// ID of user that sent message
71    pub user_id: types::UserId,
72    /// Name of user that sent message
73    pub user_name: types::UserName,
74}
75
76/// [`ChannelBitsEventsV2Reply::BitsEvent`] event unlocked new badge for user.
77#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
78#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
79#[non_exhaustive]
80pub struct BadgeEntitlement {
81    /// New version of badge
82    new_version: u64,
83    /// Previous version of badge
84    previous_version: u64,
85}
86
87/// Context that triggered pubsub message
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89#[non_exhaustive]
90pub enum BitsContext {
91    /// Cheer
92    #[serde(rename = "cheer")]
93    Cheer,
94}
95
96#[cfg(test)]
97mod tests {
98    use super::super::{Response, TopicData};
99    use super::*;
100    #[test]
101    fn bits_event() {
102        let source = r#"
103{
104    "type": "MESSAGE",
105    "data": {
106        "topic": "channel-bits-events-v2.1234",
107        "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\"}"
108    }
109}"#;
110        let actual = dbg!(Response::parse(source).unwrap());
111        assert!(matches!(
112            actual,
113            Response::Message {
114                data: TopicData::ChannelBitsEventsV2 { .. },
115            }
116        ));
117    }
118
119    #[test]
120    fn bits_event_documented() {
121        let source = r#"
122{
123    "type": "MESSAGE",
124    "data": {
125       "topic": "channel-bits-events-v2.46024993",
126       "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}"
127    }
128}
129"#;
130
131        let actual = dbg!(Response::parse(source).unwrap());
132        assert!(matches!(
133            actual,
134            Response::Message {
135                data: TopicData::ChannelBitsEventsV2 { .. },
136            }
137        ));
138    }
139    #[test]
140    fn check_deser() {
141        use std::convert::TryInto as _;
142        let s = "channel-bits-events-v2.1234";
143        assert_eq!(
144            ChannelBitsEventsV2 { channel_id: 1234 },
145            s.to_string().try_into().unwrap()
146        );
147    }
148
149    #[test]
150    fn check_ser() {
151        let s = "channel-bits-events-v2.1234";
152        let right: String = ChannelBitsEventsV2 { channel_id: 1234 }.into();
153        assert_eq!(s.to_string(), right);
154    }
155}