twitch_api/pubsub/
channel_subscriptions.rs

1#![doc(alias = "subscription")]
2#![doc(alias = "subscriptions")]
3#![doc(alias = "channel-subscribe-events-v1")]
4//! PubSub messages for subscriptions
5use crate::{pubsub, types};
6use serde_derive::{Deserialize, Serialize};
7
8/// A subscription event happens in channel
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
10#[serde(into = "String", try_from = "String")]
11pub struct ChannelSubscribeEventsV1 {
12    /// The channel_id to watch. Can be fetched with the [Get Users](crate::helix::users::get_users) endpoint
13    pub channel_id: u32,
14}
15
16impl_de_ser!(
17    ChannelSubscribeEventsV1,
18    "channel-subscribe-events-v1",
19    channel_id // FIXME: add trailing comma
20);
21
22impl pubsub::Topic for ChannelSubscribeEventsV1 {
23    #[cfg(feature = "twitch_oauth2")]
24    const SCOPE: twitch_oauth2::Validator = twitch_oauth2::validator![];
25
26    fn into_topic(self) -> pubsub::Topics { super::Topics::ChannelSubscribeEventsV1(self) }
27}
28
29/// A subscription
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
32#[non_exhaustive]
33pub struct Sub {
34    // FIXME: Could be for month that subscription ends
35    /// Unknown
36    pub benefit_end_month: i64,
37    /// ID of the channel that has been subscribed or subgifted
38    pub channel_id: types::UserId,
39    /// Name of the channel that has been subscribed or subgifted
40    pub channel_name: types::UserName,
41    /// Cumulative months that user has been subscribed
42    pub cumulative_months: i64,
43    /// Resubscription is a gift
44    pub is_gift: bool,
45    #[doc(hidden)]
46    pub months: i64,
47    // FIXME: should be a enum
48    /// Duration of subscription, e.g 1, 3 or 6
49    pub multi_month_duration: i64,
50    /// Message sent with this subscription
51    pub sub_message: SubMessage,
52    /// Subscription plan
53    pub sub_plan: types::SubscriptionTier,
54    /// Name of subscription plan
55    pub sub_plan_name: String,
56    /// Time when pubsub message was sent
57    pub time: types::Timestamp,
58    /// ID of user that subscribed
59    pub user_id: types::UserId,
60    /// Username of user that subscribed
61    pub user_name: types::UserName,
62    /// Display name of user that subscribed
63    pub display_name: types::DisplayName,
64}
65
66/// A resubscription
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
69#[non_exhaustive]
70pub struct ReSub {
71    // missing in documented example
72    // FIXME: Could be for month that subscription ends
73    /// Unknown
74    pub benefit_end_month: Option<i64>,
75    /// ID of the channel that has been subscribed or subgifted
76    pub channel_id: types::UserId,
77    /// Name of the channel that has been subscribed or subgifted
78    pub channel_name: types::UserName,
79    /// Cumulative months that user has been subscribed
80    pub cumulative_months: i64,
81    /// Resubscription is a gift
82    pub is_gift: bool,
83    /// Months the user has been subscribed for in a row.
84    pub streak_months: Option<i64>,
85    #[doc(hidden)]
86    pub months: i64,
87    /// Duration of subscription, e.g 1, 3 or 6
88    pub multi_month_duration: Option<i64>,
89    /// Message sent with this subscription
90    pub sub_message: SubMessage,
91    /// Subscription plan
92    pub sub_plan: types::SubscriptionTier,
93    /// Name of subscription plan
94    pub sub_plan_name: String,
95    /// Time when pubsub message was sent
96    pub time: types::Timestamp,
97    /// ID of user that subscribed
98    pub user_id: types::UserId,
99    /// Username of user that subscribed
100    pub user_name: types::UserName,
101    /// Display name of user that subscribed
102    pub display_name: types::DisplayName,
103}
104
105/// A gifted subscription happened
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
108#[non_exhaustive]
109pub struct SubGift {
110    // missing in documented example
111    // FIXME: Could be for month that subscription ends
112    /// Unknown
113    pub benefit_end_month: Option<i64>,
114    /// ID of the channel that has been subscribed or subgifted
115    pub channel_id: types::UserId,
116    /// Name of the channel that has been subscribed or subgifted
117    pub channel_name: types::UserName,
118    #[doc(hidden)]
119    pub is_gift: bool,
120    /// Months
121    pub months: i64,
122    // FIXME: should be a enum
123    /// Duration of subscription, e.g 1, 3 or 6
124    pub multi_month_duration: Option<i64>,
125    /// Display name of user that received gifted subscription
126    pub recipient_display_name: types::DisplayName,
127    /// Username of user that received gifted subscription
128    pub recipient_id: types::UserId,
129    /// Username of user that received gifted subscription
130    pub recipient_user_name: types::UserName,
131    /// Message sent with this subscription
132    #[doc(hidden)]
133    pub sub_message: SubMessage,
134    /// Subscription plan
135    pub sub_plan: types::SubscriptionTier,
136    /// Name of subscription plan
137    pub sub_plan_name: String,
138    /// Time when pubsub message was sent
139    pub time: types::Timestamp,
140    /// ID of user that purchased gifted subscription
141    pub user_id: types::UserId,
142    /// Username of user that purchased gifted subscription
143    pub user_name: types::UserName,
144    /// Display name of user that purchased gifted subscription
145    pub display_name: types::DisplayName,
146}
147
148/// Gifted resubscription with optional message
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
151#[non_exhaustive]
152pub struct ResubGift {
153    // missing in documented example
154    // FIXME: Could be for month that subscription ends
155    /// Unknown
156    pub benefit_end_month: Option<i64>,
157    /// ID of the channel that has been subscribed or subgifted
158    pub channel_id: types::UserId,
159    /// Name of the channel that has been subscribed or subgifted
160    pub channel_name: types::UserName,
161    /// Cumulative months that user has been subscribed
162    pub cumulative_months: i64,
163    #[doc(hidden)]
164    pub is_gift: bool,
165    /// Months
166    pub months: i64,
167    // FIXME: should be a enum
168    // FIXME: Seems to always be zero on resubgift
169    /// Duration of subscription, e.g 1, 3 or 6
170    pub multi_month_duration: Option<i64>,
171    /// Display name of user that received gifted subscription
172    pub recipient_display_name: types::DisplayName,
173    /// Username of user that received gifted subscription
174    pub recipient_user_name: types::UserName,
175    // FIXME: Is this ever shared in a resubgift?
176    /// Months the recipient has been subscribed for in a row.
177    pub streak_months: Option<i64>,
178    /// Message sent with this subscription
179    pub sub_message: SubMessage,
180    /// Subscription plan
181    pub sub_plan: types::SubscriptionTier,
182    /// Name of subscription plan
183    pub sub_plan_name: String,
184    /// Time when pubsub message was sent
185    pub time: types::Timestamp,
186    /// ID of user that purchased gifted subscription
187    pub user_id: types::UserId,
188    /// Username of user that purchased gifted subscription
189    pub user_name: types::UserName,
190    /// Display name of user that purchased gifted subscription
191    pub display_name: types::DisplayName,
192}
193
194/// User extends a (gifted) sub
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
196#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
197#[non_exhaustive]
198pub struct ExtendSub {
199    /// Unknown
200    pub benefit_end_month: Option<i64>,
201    /// ID of the channel that has been subscribed or subgifted
202    pub channel_id: types::UserId,
203    /// Name of the channel that has been subscribed or subgifted
204    pub channel_name: types::UserName,
205    /// Cumulative months that user has been subscribed
206    pub cumulative_months: i64,
207    #[doc(hidden)]
208    pub is_gift: bool,
209    /// Months
210    pub months: i64,
211    // FIXME: should be a enum
212    /// Duration of subscription, e.g 1, 3 or 6
213    pub multi_month_duration: Option<i64>,
214    /// Message sent with this subscription
215    pub sub_message: SubMessage,
216    /// Subscription plan
217    pub sub_plan: types::SubscriptionTier,
218    /// Name of subscription plan
219    pub sub_plan_name: String,
220    /// Time when pubsub message was sent
221    pub time: types::Timestamp,
222    /// ID of user that purchased gifted subscription
223    pub user_id: types::UserId,
224    /// Username of user that purchased gifted subscription
225    pub user_name: types::UserName,
226    /// Display name of user that purchased gifted subscription
227    pub display_name: types::DisplayName,
228}
229
230// FIXME: Missing anonsubgift and anonresubgift
231// Should probably share fields.
232/// Reply from [ChannelSubscribeEventsV1]
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234#[serde(tag = "context")]
235#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
236#[non_exhaustive]
237pub enum ChannelSubscribeEventsV1Reply {
238    /// A subscription
239    #[serde(rename = "sub")]
240    Sub(Sub),
241    /// A resubscription
242    #[serde(rename = "resub")]
243    ReSub(ReSub),
244    /// A gifted subscription happened
245    #[serde(rename = "subgift")]
246    SubGift(SubGift),
247    /// Gifted resubscription with optional message
248    #[serde(rename = "resubgift")]
249    ResubGift(ResubGift),
250    /// User extends sub through the month.
251    ///
252    /// Message emited in web chat is something like: `User Extended their Tier 1 subscription through June!`
253    #[serde(rename = "extendsub")]
254    ExtendSub(ExtendSub),
255}
256
257/// Described where in a message an emote is
258#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize)]
259#[non_exhaustive]
260pub struct Emote {
261    // FIXME: Mention how to get the emote picture
262    /// ID of emote
263    pub id: String,
264    /// Start index of emote in message
265    pub start: i64,
266    /// End index of emote in message
267    pub end: i64,
268}
269
270/// Message sent with subscription
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
272#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
273#[non_exhaustive]
274pub struct SubMessage {
275    /// Emotes in subscription message
276    #[serde(deserialize_with = "pubsub::deserialize_default_from_null")]
277    pub emotes: Vec<Emote>,
278    /// Message in subscription
279    pub message: String,
280}
281
282#[cfg(test)]
283mod tests {
284    use super::super::{Response, TopicData};
285    use super::*;
286
287    #[test]
288    fn subscription_doc_example_resub() {
289        // twitch docs broken as usual. /emotes/id  is a string and /months is missing
290        let message = r##"
291{
292    "user_name": "tww2",
293    "display_name": "TWW2",
294    "channel_name": "mr_woodchuck",
295    "user_id": "13405587",
296    "channel_id": "89614178",
297    "time": "2015-12-19T16:39:57-08:00",
298    "sub_plan": "1000",
299    "sub_plan_name": "Channel Subscription (mr_woodchuck)",
300    "cumulative_months": 9,
301    "streak_months": 3,
302    "months": 0,
303    "context": "resub",
304    "is_gift": false,
305    "sub_message": {
306        "message": "A Twitch baby is born! KappaHD",
307        "emotes": [
308            {
309                "start": 23,
310                "end": 7,
311                "id": "2867"
312            }
313        ]
314    }
315}
316"##;
317
318        let source = format!(
319            r#"{{"type": "MESSAGE", "data": {{ "topic": "channel-subscribe-events-v1.27620241", "message": {message:?} }}}}"#
320        );
321        let actual = dbg!(Response::parse(&source).unwrap());
322        assert!(matches!(
323            actual,
324            Response::Message {
325                data: TopicData::ChannelSubscribeEventsV1 { .. },
326            }
327        ));
328    }
329
330    #[test]
331    fn subscription_doc_example_subgift() {
332        let message = r##"
333{
334    "user_name": "tww2",
335    "display_name": "TWW2",
336    "channel_name": "mr_woodchuck",
337    "user_id": "13405587",
338    "channel_id": "89614178",
339    "time": "2015-12-19T16:39:57-08:00",
340    "sub_plan": "1000",
341    "sub_plan_name": "Channel Subscription (mr_woodchuck)",
342    "months": 9,
343    "context": "subgift",
344    "is_gift": true,
345    "sub_message": {
346        "message": "",
347        "emotes": null
348    },
349    "recipient_id": "19571752",
350    "recipient_user_name": "forstycup",
351    "recipient_display_name": "forstycup"
352}
353"##;
354
355        let source = format!(
356            r#"{{"type": "MESSAGE", "data": {{ "topic": "channel-subscribe-events-v1.27620241", "message": {message:?} }}}}"#
357        );
358        let actual = dbg!(Response::parse(&source).unwrap());
359        assert!(matches!(
360            actual,
361            Response::Message {
362                data: TopicData::ChannelSubscribeEventsV1 { .. },
363            }
364        ));
365    }
366
367    #[test]
368    fn new_sub() {
369        let message = r##"
370{
371    "benefit_end_month": 11,
372    "user_name": "tmi",
373    "display_name": "tmi",
374    "channel_name": "emilgardis",
375    "user_id": "1234",
376    "channel_id": "27620241",
377    "time": "2020-10-20T22:17:43.242793831Z",
378    "sub_message": {
379        "message": "",
380        "emotes": null
381    },
382    "sub_plan": "1000",
383    "sub_plan_name": "Channel Subscription (emilgardis)",
384    "months": 0,
385    "cumulative_months": 1,
386    "context": "sub",
387    "is_gift": false,
388    "multi_month_duration": 0
389}
390"##;
391
392        let source = format!(
393            r#"{{"type": "MESSAGE", "data": {{ "topic": "channel-subscribe-events-v1.27620241", "message": {message:?} }}}}"#
394        );
395        let actual = dbg!(Response::parse(&source).unwrap());
396        assert!(matches!(
397            actual,
398            Response::Message {
399                data: TopicData::ChannelSubscribeEventsV1 { .. },
400            }
401        ));
402    }
403
404    #[test]
405    fn gifted_sub() {
406        let message = r##"
407{
408    "benefit_end_month": 0,
409    "user_name": "tmi",
410    "display_name": "tmi",
411    "channel_name": "emilgardis",
412    "user_id": "1234",
413    "channel_id": "27620241",
414    "recipient_id": "1337",
415    "recipient_user_name": "justintv",
416    "recipient_display_name": "justintv",
417    "time": "2020-10-20T22:18:17.542444893Z",
418    "sub_message": {
419        "message": "",
420        "emotes": null
421    },
422    "sub_plan": "1000",
423    "sub_plan_name": "Channel Subscription (emilgardis)",
424    "months": 1,
425    "context": "subgift",
426    "is_gift": true,
427    "multi_month_duration": 1
428}
429"##;
430
431        let source = format!(
432            r#"{{"type": "MESSAGE", "data": {{ "topic": "channel-subscribe-events-v1.27620241", "message": {message:?} }}}}"#
433        );
434        let actual = dbg!(Response::parse(&source).unwrap());
435        assert!(matches!(
436            actual,
437            Response::Message {
438                data: TopicData::ChannelSubscribeEventsV1 { .. },
439            }
440        ));
441    }
442
443    #[test]
444    fn resub() {
445        let message = r##"
446{
447    "benefit_end_month": 0,
448    "user_name": "tmi",
449    "display_name": "tmi",
450    "channel_name": "emilgardis",
451    "user_id": "1234",
452    "channel_id": "80525799",
453    "time": "2020-10-25T17:15:36.541972298Z",
454    "sub_message": {
455        "message": "message here",
456        "emotes": [
457            {
458                "start": 191,
459                "end": 197,
460                "id": "12342378"
461            }
462        ]
463    },
464    "sub_plan": "2000",
465    "sub_plan_name": "Channel Subscription (emilgardis): $9.99 Sub",
466    "months": 0,
467    "cumulative_months": 12,
468    "streak_months": 12,
469    "context": "resub",
470    "is_gift": false,
471    "multi_month_duration": 0
472}
473"##;
474
475        let source = format!(
476            r#"{{"type": "MESSAGE", "data": {{ "topic": "channel-subscribe-events-v1.27620241", "message": {message:?} }}}}"#
477        );
478        let actual = dbg!(Response::parse(&source).unwrap());
479        assert!(matches!(
480            actual,
481            Response::Message {
482                data: TopicData::ChannelSubscribeEventsV1 { .. },
483            }
484        ));
485    }
486
487    #[test]
488    fn resub_gift() {
489        let message = r##"
490{
491    "benefit_end_month": 0,
492    "user_name": "emilgardis",
493    "display_name": "emilgardis",
494    "channel_name": "sessis",
495    "user_id": "158640756",
496    "channel_id": "80525799",
497    "recipient_user_name": "champi70",
498    "recipient_display_name": "Champi70",
499    "time": "2020-12-06T18:54:52.804481633Z",
500    "sub_message": {
501        "message": "¡Gracias, @emilgardis, por regalarme una suscripción! thank you so mych sessis for the streams you brighten my day each time you are in stream you are awesome sessHug",
502        "emotes": [
503            {
504                "start": 161,
505                "end": 167,
506                "id": "300741652"
507            }
508        ]
509    },
510    "sub_plan": "1000",
511    "sub_plan_name": "Channel Subscription (sessis)",
512    "months": 0,
513    "cumulative_months": 24,
514    "context": "resubgift",
515    "is_gift": true,
516    "multi_month_duration": 0
517}
518"##;
519
520        let source = format!(
521            r#"{{"type": "MESSAGE", "data": {{ "topic": "channel-subscribe-events-v1.27620241", "message": {message:?} }}}}"#
522        );
523        let actual = dbg!(Response::parse(&source).unwrap());
524        assert!(matches!(
525            actual,
526            Response::Message {
527                data: TopicData::ChannelSubscribeEventsV1 { .. },
528            }
529        ));
530    }
531
532    #[test]
533    fn extendsub() {
534        let message = r##"
535{
536    "benefit_end_month": 6,
537    "user_name": "user",
538    "display_name": "User!",
539    "channel_name": "twitch",
540    "user_id": "1234",
541    "channel_id": "123",
542    "time": "2021-05-14T20:54:06.805273338Z",
543    "sub_message": {
544        "message": "",
545        "emotes": null
546    },
547    "sub_plan": "1000",
548    "sub_plan_name": "Channel Subscription (twitch)",
549    "months": 0,
550    "cumulative_months": 5,
551    "context": "extendsub",
552    "is_gift": false,
553    "multi_month_duration": 0
554}
555"##;
556
557        let source = format!(
558            r#"{{"type": "MESSAGE", "data": {{ "topic": "channel-subscribe-events-v1.27620241", "message": {message:?} }}}}"#
559        );
560        let actual = dbg!(Response::parse(&source).unwrap());
561        assert!(matches!(
562            actual,
563            Response::Message {
564                data: TopicData::ChannelSubscribeEventsV1 { .. },
565            }
566        ));
567    }
568
569    #[test]
570    fn check_deser() {
571        use std::convert::TryInto as _;
572        let s = "channel-subscribe-events-v1.1234";
573        assert_eq!(
574            ChannelSubscribeEventsV1 { channel_id: 1234 },
575            s.to_string().try_into().unwrap()
576        );
577    }
578
579    #[test]
580    fn check_ser() {
581        let s = "channel-subscribe-events-v1.1234";
582        let right: String = ChannelSubscribeEventsV1 { channel_id: 1234 }.into();
583        assert_eq!(s.to_string(), right);
584    }
585}