diff --git a/Cargo.toml b/Cargo.toml index ca391e8546..27e5c421ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ pubsub = [ eventsub = [ "serde_json/raw_value", + "twitch_types/chat", "twitch_types/emote", "twitch_types/eventsub", "twitch_types/goal", diff --git a/src/eventsub/channel/mod.rs b/src/eventsub/channel/mod.rs index 5e6176433c..599654b9f8 100644 --- a/src/eventsub/channel/mod.rs +++ b/src/eventsub/channel/mod.rs @@ -18,6 +18,7 @@ pub mod hypetrain; pub mod poll; pub mod prediction; pub mod raid; +pub mod shared_chat; pub mod shield_mode; pub mod shoutout; pub mod subscribe; @@ -104,6 +105,12 @@ pub use prediction::{ChannelPredictionProgressV1, ChannelPredictionProgressV1Pay #[doc(inline)] pub use raid::{ChannelRaidV1, ChannelRaidV1Payload}; #[doc(inline)] +pub use shared_chat::{ChannelSharedChatBeginV1, ChannelSharedChatBeginV1Payload}; +#[doc(inline)] +pub use shared_chat::{ChannelSharedChatEndV1, ChannelSharedChatEndV1Payload}; +#[doc(inline)] +pub use shared_chat::{ChannelSharedChatUpdateV1, ChannelSharedChatUpdateV1Payload}; +#[doc(inline)] pub use shield_mode::{ChannelShieldModeBeginV1, ChannelShieldModeBeginV1Payload}; #[doc(inline)] pub use shield_mode::{ChannelShieldModeEndV1, ChannelShieldModeEndV1Payload}; diff --git a/src/eventsub/channel/shared_chat/begin.rs b/src/eventsub/channel/shared_chat/begin.rs new file mode 100644 index 0000000000..9902d407c9 --- /dev/null +++ b/src/eventsub/channel/shared_chat/begin.rs @@ -0,0 +1,134 @@ +#![doc(alias = "channel.shared_chat.begin")] +//! A channel becomes active in an active shared chat session. + +use super::*; + +/// [`channel.shared_chat.begin`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelshared_chatbegin): a channel becomes active in an active shared chat session. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ChannelSharedChatBeginV1 { + /// The User ID of the channel to receive shared chat session begin events for. + #[cfg_attr(feature = "typed-builder", builder(setter(into)))] + pub broadcaster_user_id: types::UserId, +} + +impl ChannelSharedChatBeginV1 { + /// The User ID of the channel to receive shared chat session begin events for. + pub fn broadcaster_user_id(broadcaster_user_id: impl Into) -> Self { + Self { + broadcaster_user_id: broadcaster_user_id.into(), + } + } +} + +impl EventSubscription for ChannelSharedChatBeginV1 { + type Payload = ChannelSharedChatBeginV1Payload; + + const EVENT_TYPE: EventType = EventType::ChannelSharedChatBegin; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = twitch_oauth2::validator![]; + const VERSION: &'static str = "1"; +} + +/// [`channel.shared_chat.begin`](ChannelSharedChatBeginV1) response payload. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ChannelSharedChatBeginV1Payload { + /// The unique identifier for the shared chat session. + pub session_id: types::SharedChatSessionId, + /// The User ID of the channel in the subscription condition which is now active in the shared chat session. + pub broadcaster_user_id: types::UserId, + /// The display name of the channel in the subscription condition which is now active in the shared chat session. + pub broadcaster_user_name: types::DisplayName, + /// The user login of the channel in the subscription condition which is now active in the shared chat session. + pub broadcaster_user_login: types::UserName, + /// The User ID of the host channel. + pub host_broadcaster_user_id: types::UserId, + /// The display name of the host channel. + pub host_broadcaster_user_name: types::DisplayName, + /// The user login of the host channel. + pub host_broadcaster_user_login: types::UserName, + /// The list of participants in the session. + pub participants: Vec, +} + +#[cfg(test)] +#[test] +fn parse_payload() { + use crate::eventsub::{Event, Message}; + + let payload = r##" + { + "subscription": { + "id": "bf0602d2-5b39-4ece-b1a4-44191d52df6b", + "status": "enabled", + "type": "channel.shared_chat.begin", + "version": "1", + "condition": { + "broadcaster_user_id": "1971641" + }, + "transport": { + "method": "websocket", + "session_id": "AgoQOtgGkFvXRlSkij343CndhIGY2VsbC1h" + }, + "created_at": "2023-10-06T18:04:38.807682738Z", + "cost": 0 + }, + "event": { + "session_id": "2b64a92a-dbb8-424e-b1c3-304423ba1b6f", + "broadcaster_user_id": "1971641", + "broadcaster_user_login": "streamer", + "broadcaster_user_name": "streamer", + "host_broadcaster_user_id": "1971641", + "host_broadcaster_user_login": "streamer", + "host_broadcaster_user_name": "streamer", + "participants": [ + { + "broadcaster_user_id": "1971641", + "broadcaster_user_name": "streamer", + "broadcaster_user_login": "streamer" + }, + { + "broadcaster_user_id": "112233", + "broadcaster_user_name": "streamer33", + "broadcaster_user_login": "streamer33" + } + ] + } + } + "##; + + let val = Event::parse(payload).unwrap(); + crate::tests::roundtrip(&val); + + let Event::ChannelSharedChatBeginV1(val) = val else { + panic!("invalid event type"); + }; + let Message::Notification(notif) = val.message else { + panic!("invalid message type"); + }; + + assert_eq!(notif.broadcaster_user_id.as_str(), "1971641"); + assert_eq!(notif.broadcaster_user_id, notif.host_broadcaster_user_id); + assert_eq!( + notif.broadcaster_user_login, + notif.host_broadcaster_user_login + ); + assert_eq!( + notif.broadcaster_user_name, + notif.host_broadcaster_user_name + ); + assert_eq!( + notif.session_id.as_str(), + "2b64a92a-dbb8-424e-b1c3-304423ba1b6f" + ); + assert_eq!(notif.participants.len(), 2); + assert_eq!( + notif.participants[0].broadcaster_user_id.as_str(), + "1971641" + ); + assert_eq!(notif.participants[1].broadcaster_user_id.as_str(), "112233"); +} diff --git a/src/eventsub/channel/shared_chat/end.rs b/src/eventsub/channel/shared_chat/end.rs new file mode 100644 index 0000000000..a551e7117b --- /dev/null +++ b/src/eventsub/channel/shared_chat/end.rs @@ -0,0 +1,114 @@ +#![doc(alias = "channel.shared_chat.end")] +//! A channel leaves a shared chat session or the session ends. + +use super::*; + +/// [`channel.shared_chat.end`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelshared_chatend): a channel leaves a shared chat session or the session ends. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ChannelSharedChatEndV1 { + /// The User ID of the channel to receive shared chat session end events for. + #[cfg_attr(feature = "typed-builder", builder(setter(into)))] + pub broadcaster_user_id: types::UserId, +} + +impl ChannelSharedChatEndV1 { + /// The User ID of the channel to receive shared chat session end events for. + pub fn broadcaster_user_id(broadcaster_user_id: impl Into) -> Self { + Self { + broadcaster_user_id: broadcaster_user_id.into(), + } + } +} + +impl EventSubscription for ChannelSharedChatEndV1 { + type Payload = ChannelSharedChatEndV1Payload; + + const EVENT_TYPE: EventType = EventType::ChannelSharedChatEnd; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = twitch_oauth2::validator![]; + const VERSION: &'static str = "1"; +} + +/// [`channel.shared_chat.end`](ChannelSharedChatEndV1) response payload. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ChannelSharedChatEndV1Payload { + /// The unique identifier for the shared chat session. + pub session_id: types::SharedChatSessionId, + /// The User ID of the channel in the subscription condition which is no longer active in the shared chat session. + pub broadcaster_user_id: types::UserId, + /// The display name of the channel in the subscription condition which is no longer active in the shared chat session. + pub broadcaster_user_name: types::DisplayName, + /// The user login of the channel in the subscription condition which is no longer active in the shared chat session. + pub broadcaster_user_login: types::UserName, + /// The User ID of the host channel. + pub host_broadcaster_user_id: types::UserId, + /// The display name of the host channel. + pub host_broadcaster_user_name: types::DisplayName, + /// The user login of the host channel. + pub host_broadcaster_user_login: types::UserName, +} + +#[cfg(test)] +#[test] +fn parse_payload() { + use crate::eventsub::{Event, Message}; + + let payload = r##" + { + "subscription": { + "id": "84a875f1-1dc0-43b2-8ed3-d7db4d650c37", + "status": "enabled", + "type": "channel.shared_chat.end", + "version": "1", + "condition": { + "broadcaster_user_id": "112233" + }, + "transport": { + "method": "websocket", + "session_id": "AgoQOtgGkFvXRlSkij343CndhIGY2VsbC1h" + }, + "created_at": "2023-10-06T18:04:38.807682738Z", + "cost": 0 + }, + "event": { + "session_id": "2b64a92a-dbb8-424e-b1c3-304423ba1b6f", + "broadcaster_user_id": "1971641", + "broadcaster_user_login": "streamer", + "broadcaster_user_name": "streamer", + "host_broadcaster_user_id": "1971641", + "host_broadcaster_user_login": "streamer", + "host_broadcaster_user_name": "streamer" + } + } + "##; + + let val = Event::parse(payload).unwrap(); + crate::tests::roundtrip(&val); + + let Event::ChannelSharedChatEndV1(val) = val else { + panic!("invalid event type"); + }; + let Message::Notification(notif) = val.message else { + panic!("invalid message type"); + }; + + assert_eq!(notif.broadcaster_user_id.as_str(), "1971641"); + assert_eq!(notif.broadcaster_user_id, notif.host_broadcaster_user_id); + assert_eq!( + notif.broadcaster_user_login, + notif.host_broadcaster_user_login + ); + assert_eq!( + notif.broadcaster_user_name, + notif.host_broadcaster_user_name + ); + assert_eq!( + notif.session_id.as_str(), + "2b64a92a-dbb8-424e-b1c3-304423ba1b6f" + ); +} diff --git a/src/eventsub/channel/shared_chat/mod.rs b/src/eventsub/channel/shared_chat/mod.rs new file mode 100644 index 0000000000..ba74ef8d19 --- /dev/null +++ b/src/eventsub/channel/shared_chat/mod.rs @@ -0,0 +1,29 @@ +#![doc(alias = "channel.shared_chat")] +//! Events related to shared chat +use super::{EventSubscription, EventType}; +use crate::types; +use serde_derive::{Deserialize, Serialize}; + +pub mod begin; +pub mod end; +pub mod update; + +#[doc(inline)] +pub use begin::{ChannelSharedChatBeginV1, ChannelSharedChatBeginV1Payload}; +#[doc(inline)] +pub use end::{ChannelSharedChatEndV1, ChannelSharedChatEndV1Payload}; +#[doc(inline)] +pub use update::{ChannelSharedChatUpdateV1, ChannelSharedChatUpdateV1Payload}; + +/// A participant in a shared chat session +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct Participant { + /// The User ID of the participant channel. + pub broadcaster_user_id: types::UserId, + /// The display name of the participant channel. + pub broadcaster_user_name: types::UserName, + /// The user login of the participant channel. + pub broadcaster_user_login: types::DisplayName, +} diff --git a/src/eventsub/channel/shared_chat/update.rs b/src/eventsub/channel/shared_chat/update.rs new file mode 100644 index 0000000000..00dc7837e9 --- /dev/null +++ b/src/eventsub/channel/shared_chat/update.rs @@ -0,0 +1,140 @@ +#![doc(alias = "channel.shared_chat.update")] +//! The active shared chat session the channel is in changed. + +use super::*; + +/// [`channel.shared_chat.update`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelshared_chatupdate): the active shared chat session the channel is in changed. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ChannelSharedChatUpdateV1 { + /// The User ID of the channel to receive shared chat session update events for. + #[cfg_attr(feature = "typed-builder", builder(setter(into)))] + pub broadcaster_user_id: types::UserId, +} + +impl ChannelSharedChatUpdateV1 { + /// The User ID of the channel to receive shared chat session update events for. + pub fn broadcaster_user_id(broadcaster_user_id: impl Into) -> Self { + Self { + broadcaster_user_id: broadcaster_user_id.into(), + } + } +} + +impl EventSubscription for ChannelSharedChatUpdateV1 { + type Payload = ChannelSharedChatUpdateV1Payload; + + const EVENT_TYPE: EventType = EventType::ChannelSharedChatUpdate; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = twitch_oauth2::validator![]; + const VERSION: &'static str = "1"; +} + +/// [`channel.shared_chat.update`](ChannelSharedChatUpdateV1) response payload. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ChannelSharedChatUpdateV1Payload { + /// The unique identifier for the shared chat session. + pub session_id: types::SharedChatSessionId, + /// The User ID of the channel in the subscription condition. + pub broadcaster_user_id: types::UserId, + /// The display name of the channel in the subscription condition. + pub broadcaster_user_name: types::DisplayName, + /// The user login of the channel in the subscription condition. + pub broadcaster_user_login: types::UserName, + /// The User ID of the host channel. + pub host_broadcaster_user_id: types::UserId, + /// The display name of the host channel. + pub host_broadcaster_user_name: types::DisplayName, + /// The user login of the host channel. + pub host_broadcaster_user_login: types::UserName, + /// The list of participants in the session. + pub participants: Vec, +} + +#[cfg(test)] +#[test] +fn parse_payload() { + use crate::eventsub::{Event, Message}; + + let payload = r##" + { + "subscription": { + "id": "5f65d0a7-0069-4f2b-944b-bc81b160ae49", + "status": "enabled", + "type": "channel.shared_chat.update", + "version": "1", + "condition": { + "broadcaster_user_id": "1971641" + }, + "transport": { + "method": "websocket", + "session_id": "AgoQOtgGkFvXRlSkij343CndhIGY2VsbC1h" + }, + "created_at": "2023-10-06T18:04:38.807682738Z", + "cost": 0 + }, + "event": { + "session_id": "2b64a92a-dbb8-424e-b1c3-304423ba1b6f", + "broadcaster_user_id": "1971641", + "broadcaster_user_login": "streamer", + "broadcaster_user_name": "streamer", + "host_broadcaster_user_id": "1971641", + "host_broadcaster_user_login": "streamer", + "host_broadcaster_user_name": "streamer", + "participants": [ + { + "broadcaster_user_id": "1971641", + "broadcaster_user_name": "streamer", + "broadcaster_user_login": "streamer" + }, + { + "broadcaster_user_id": "112233", + "broadcaster_user_name": "streamer33", + "broadcaster_user_login": "streamer33" + }, + { + "broadcaster_user_id": "332211", + "broadcaster_user_name": "streamer11", + "broadcaster_user_login": "streamer11" + } + ] + } + } + "##; + + let val = Event::parse(payload).unwrap(); + crate::tests::roundtrip(&val); + + let Event::ChannelSharedChatUpdateV1(val) = val else { + panic!("invalid event type"); + }; + let Message::Notification(notif) = val.message else { + panic!("invalid message type"); + }; + + assert_eq!(notif.broadcaster_user_id.as_str(), "1971641"); + assert_eq!(notif.broadcaster_user_id, notif.host_broadcaster_user_id); + assert_eq!( + notif.broadcaster_user_login, + notif.host_broadcaster_user_login + ); + assert_eq!( + notif.broadcaster_user_name, + notif.host_broadcaster_user_name + ); + assert_eq!( + notif.session_id.as_str(), + "2b64a92a-dbb8-424e-b1c3-304423ba1b6f" + ); + assert_eq!(notif.participants.len(), 3); + assert_eq!( + notif.participants[0].broadcaster_user_id.as_str(), + "1971641" + ); + assert_eq!(notif.participants[1].broadcaster_user_id.as_str(), "112233"); + assert_eq!(notif.participants[2].broadcaster_user_id.as_str(), "332211"); +} diff --git a/src/eventsub/event.rs b/src/eventsub/event.rs index 19c8043473..1947ea2d8e 100644 --- a/src/eventsub/event.rs +++ b/src/eventsub/event.rs @@ -48,6 +48,9 @@ macro_rules! fill_events { channel::ChannelPredictionLockV1; channel::ChannelPredictionProgressV1; channel::ChannelRaidV1; + channel::ChannelSharedChatBeginV1; + channel::ChannelSharedChatEndV1; + channel::ChannelSharedChatUpdateV1; channel::ChannelShieldModeBeginV1; channel::ChannelShieldModeEndV1; channel::ChannelShoutoutCreateV1; @@ -209,6 +212,12 @@ make_event_type!("Event Types": pub enum EventType { ChannelShoutoutReceive => "channel.shoutout.receive", "a broadcaster raids another broadcaster’s channel.": ChannelRaid => "channel.raid", + "a channel becomes active in an active shared chat session.": + ChannelSharedChatBegin => "channel.shared_chat.begin", + "a channel leaves a shared chat session or the session ends.": + ChannelSharedChatEnd => "channel.shared_chat.end", + "the active shared chat session the channel is in changed.": + ChannelSharedChatUpdate => "channel.shared_chat.update", "a subscription to the specified channel expires.": ChannelSubscriptionEnd => "channel.subscription.end", "a user gives one or more gifted subscriptions in a channel.": @@ -338,6 +347,12 @@ pub enum Event { ChannelPredictionEndV1(Payload), /// Channel Raid V1 Event ChannelRaidV1(Payload), + /// Channel SharedChat Begin V1 Event + ChannelSharedChatBeginV1(Payload), + /// Channel SharedChat End V1 Event + ChannelSharedChatEndV1(Payload), + /// Channel SharedChat Update V1 Event + ChannelSharedChatUpdateV1(Payload), /// Channel ShieldMode Begin V1 Event ChannelShieldModeBeginV1(Payload), /// Channel ShieldMode End V1 Event diff --git a/src/eventsub/mod.rs b/src/eventsub/mod.rs index 59f73b8533..f2fae25f32 100644 --- a/src/eventsub/mod.rs +++ b/src/eventsub/mod.rs @@ -90,7 +90,7 @@ //! //! //! -//!
channel.* 🟡 42/65 +//!
channel.* 🟡 45/65 //! //! | Name | Subscription
Payload | //! |---|:---| @@ -138,9 +138,9 @@ //! | [`channel.prediction.lock`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelpredictionlock) | [ChannelPredictionLockV1](channel::ChannelPredictionLockV1)
[ChannelPredictionLockV1Payload](channel::ChannelPredictionLockV1Payload) | //! | [`channel.prediction.progress`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelpredictionprogress) | [ChannelPredictionProgressV1](channel::ChannelPredictionProgressV1)
[ChannelPredictionProgressV1Payload](channel::ChannelPredictionProgressV1Payload) | //! | [`channel.raid`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelraid) | [ChannelRaidV1](channel::ChannelRaidV1)
[ChannelRaidV1Payload](channel::ChannelRaidV1Payload) | -//! | [`channel.shared_chat.begin`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshared_chatbegin) | -
- | -//! | [`channel.shared_chat.end`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshared_chatend) | -
- | -//! | [`channel.shared_chat.update`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshared_chatupdate) | -
- | +//! | [`channel.shared_chat.begin`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshared_chatbegin) | [ChannelSharedChatBeginV1](channel::ChannelSharedChatBeginV1)
[ChannelSharedChatBeginV1Payload](channel::ChannelSharedChatBeginV1Payload) | +//! | [`channel.shared_chat.end`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshared_chatend) | [ChannelSharedChatEndV1](channel::ChannelSharedChatEndV1)
[ChannelSharedChatEndV1Payload](channel::ChannelSharedChatEndV1Payload) | +//! | [`channel.shared_chat.update`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshared_chatupdate) | [ChannelSharedChatUpdateV1](channel::ChannelSharedChatUpdateV1)
[ChannelSharedChatUpdateV1Payload](channel::ChannelSharedChatUpdateV1Payload) | //! | [`channel.shield_mode.begin`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshield_modebegin) | [ChannelShieldModeBeginV1](channel::ChannelShieldModeBeginV1)
[ChannelShieldModeBeginV1Payload](channel::ChannelShieldModeBeginV1Payload) | //! | [`channel.shield_mode.end`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshield_modeend) | [ChannelShieldModeEndV1](channel::ChannelShieldModeEndV1)
[ChannelShieldModeEndV1Payload](channel::ChannelShieldModeEndV1Payload) | //! | [`channel.shoutout.create`](https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types#channelshoutoutcreate) | [ChannelShoutoutCreateV1](channel::ChannelShoutoutCreateV1)
[ChannelShoutoutCreateV1Payload](channel::ChannelShoutoutCreateV1Payload) |