use crate::types;
use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize};
pub mod channel;
pub mod stream;
pub mod user;
pub trait EventSubscription: DeserializeOwned + Serialize + PartialEq {
type Payload: PartialEq + std::fmt::Debug + DeserializeOwned + Serialize;
#[cfg(feature = "twitch_oauth2")]
const SCOPE: &'static [twitch_oauth2::Scope];
#[cfg(feature = "twitch_oauth2")]
const OPT_SCOPE: &'static [twitch_oauth2::Scope] = &[];
const VERSION: &'static str;
const EVENT_TYPE: EventType;
fn condition(&self) -> Result<serde_json::Value, serde_json::Error> {
serde_json::to_value(self)
}
}
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct VerificationRequest {
pub challenge: String,
pub subscription: EventSubSubscription,
}
#[derive(PartialEq, Debug, Serialize, Deserialize)]
#[serde(remote = "Self")]
#[allow(clippy::large_enum_variant)]
pub enum Payload {
VerificationRequest(VerificationRequest),
ChannelUpdateV1(NotificationPayload<channel::ChannelUpdateV1>),
ChannelFollowV1(NotificationPayload<channel::ChannelFollowV1>),
ChannelSubscribeV1(NotificationPayload<channel::ChannelSubscribeV1>),
ChannelCheerV1(NotificationPayload<channel::ChannelCheerV1>),
ChannelBanV1(NotificationPayload<channel::ChannelBanV1>),
ChannelUnbanV1(NotificationPayload<channel::ChannelUnbanV1>),
ChannelPointsCustomRewardAddV1(NotificationPayload<channel::ChannelPointsCustomRewardAddV1>),
ChannelPointsCustomRewardUpdateV1(
NotificationPayload<channel::ChannelPointsCustomRewardUpdateV1>,
),
ChannelPointsCustomRewardRemoveV1(
NotificationPayload<channel::ChannelPointsCustomRewardRemoveV1>,
),
ChannelPointsCustomRewardRedemptionAddV1(
NotificationPayload<channel::ChannelPointsCustomRewardRedemptionAddV1>,
),
ChannelPointsCustomRewardRedemptionUpdateV1(
NotificationPayload<channel::ChannelPointsCustomRewardRedemptionUpdateV1>,
),
ChannelHypeTrainBeginV1(NotificationPayload<channel::ChannelHypeTrainBeginV1>),
ChannelHypeTrainProgressV1(NotificationPayload<channel::ChannelHypeTrainProgressV1>),
ChannelHypeTrainEndV1(NotificationPayload<channel::ChannelHypeTrainEndV1>),
StreamOnlineV1(NotificationPayload<stream::StreamOnlineV1>),
StreamOfflineV1(NotificationPayload<stream::StreamOfflineV1>),
UserUpdateV1(NotificationPayload<user::UserUpdateV1>),
UserAuthorizationRevokeV1(NotificationPayload<user::UserAuthorizationRevokeV1>),
}
impl Payload {
pub fn parse(source: &str) -> Result<Payload, PayloadParseError> {
serde_json::from_str(source).map_err(Into::into)
}
pub fn parse_response(source: &http::Response<Vec<u8>>) -> Result<Payload, PayloadParseError> {
Payload::parse(std::str::from_utf8(source.body())?)
}
}
#[derive(thiserror::Error, displaydoc::Display, Debug)]
pub enum PayloadParseError {
Utf8Error(#[from] std::str::Utf8Error),
DeserializeError(#[from] serde_json::Error),
}
impl<'de> Deserialize<'de> for Payload {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use std::convert::TryInto;
macro_rules! match_event {
($response:expr; $($module:ident::$event:ident);* $(;)?) => {
#[deny(unreachable_patterns)]
match (&*$response.s.version, &$response.s.type_) {
$( (<$module::$event as EventSubscription>::VERSION, &<$module::$event as EventSubscription>::EVENT_TYPE) => {
Payload::$event(NotificationPayload {
subscription: $response.s.try_into().map_err(serde::de::Error::custom)?,
event: serde_json::from_value($response.e).map_err(serde::de::Error::custom)?,
})
} )*
(v, e) => return Err(serde::de::Error::custom(format!("could not find a match for version `{}` on event type `{}`", v, e)))
}
}
}
#[derive(Deserialize, Clone)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
struct IEventSubscripionInformation {
condition: serde_json::Value,
created_at: types::Timestamp,
id: String,
transport: TransportResponse,
#[serde(rename = "type")]
type_: EventType,
version: String,
}
#[derive(Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
struct IResponse {
#[serde(rename = "subscription")]
s: IEventSubscripionInformation,
#[serde(rename = "event", alias = "challenge")]
e: serde_json::Value,
}
impl<E: EventSubscription> std::convert::TryFrom<IEventSubscripionInformation>
for EventSubscriptionInformation<E>
{
type Error = serde_json::Error;
fn try_from(info: IEventSubscripionInformation) -> Result<Self, Self::Error> {
debug_assert_eq!(info.version, E::VERSION);
debug_assert_eq!(info.type_, E::EVENT_TYPE);
Ok(EventSubscriptionInformation {
id: info.id,
condition: serde_json::from_value(info.condition)?,
created_at: info.created_at,
transport: info.transport,
})
}
}
#[derive(Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[serde(untagged)]
enum IIResponse {
VerificationRequest(VerificationRequest),
IResponse(IResponse),
}
let response = IIResponse::deserialize(deserializer).map_err(|e| {
serde::de::Error::custom(format!("could not deserialize response: {}", e))
})?;
match response {
IIResponse::VerificationRequest(verification) => {
Ok(Payload::VerificationRequest(verification))
}
IIResponse::IResponse(response) => Ok(match_event! { response;
channel::ChannelUpdateV1;
channel::ChannelFollowV1;
channel::ChannelSubscribeV1;
channel::ChannelCheerV1;
channel::ChannelBanV1;
channel::ChannelUnbanV1;
channel::ChannelPointsCustomRewardAddV1;
channel::ChannelPointsCustomRewardUpdateV1;
channel::ChannelPointsCustomRewardRemoveV1;
channel::ChannelPointsCustomRewardRedemptionAddV1;
channel::ChannelPointsCustomRewardRedemptionUpdateV1;
channel::ChannelHypeTrainBeginV1;
channel::ChannelHypeTrainProgressV1;
channel::ChannelHypeTrainEndV1;
stream::StreamOnlineV1;
stream::StreamOfflineV1;
user::UserUpdateV1;
user::UserAuthorizationRevokeV1;
}),
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct NotificationPayload<E: EventSubscription> {
#[serde(bound = "E: EventSubscription")]
pub subscription: EventSubscriptionInformation<E>,
#[serde(bound = "E: EventSubscription")]
pub event: <E as EventSubscription>::Payload,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct EventSubscriptionInformation<E: EventSubscription> {
pub id: String,
#[serde(bound = "E: EventSubscription")]
pub condition: E,
pub created_at: types::Timestamp,
pub transport: TransportResponse,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Transport {
pub method: TransportMethod,
pub callback: String,
pub secret: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct TransportResponse {
pub method: TransportMethod,
pub callback: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum TransportMethod {
Webhook,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(not(feature = "allow_unknown_fields"), serde(deny_unknown_fields))]
#[non_exhaustive]
pub enum EventType {
#[serde(rename = "channel.update")]
ChannelUpdate,
#[serde(rename = "channel.follow")]
ChannelFollow,
#[serde(rename = "channel.subscribe")]
ChannelSubscribe,
#[serde(rename = "channel.cheer")]
ChannelCheer,
#[serde(rename = "channel.ban")]
ChannelBan,
#[serde(rename = "channel.unban")]
ChannelUnban,
#[serde(rename = "channel.channel_points_custom_reward.add")]
ChannelPointsCustomRewardAdd,
#[serde(rename = "channel.channel_points_custom_reward.update")]
ChannelPointsCustomRewardUpdate,
#[serde(rename = "channel.channel_points_custom_reward.remove")]
ChannelPointsCustomRewardRemove,
#[serde(rename = "channel.channel_points_custom_reward_redemption.add")]
ChannelPointsCustomRewardRedemptionAdd,
#[serde(rename = "channel.channel_points_custom_reward_redemption.update")]
ChannelPointsCustomRewardRedemptionUpdate,
#[serde(rename = "channel.hype_train.begin")]
ChannelHypeTrainBegin,
#[serde(rename = "channel.hype_train.progress")]
ChannelHypeTrainProgress,
#[serde(rename = "channel.hype_train.end")]
ChannelHypeTrainEnd,
#[serde(rename = "stream.online")]
StreamOnline,
#[serde(rename = "stream.offline")]
StreamOffline,
#[serde(rename = "user.update")]
UserUpdate,
#[serde(rename = "user.authorization.revoke")]
UserAuthorizationRevoke,
}
impl std::fmt::Display for EventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.serialize(f) }
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum Status {
Enabled,
WebhookCallbackVerificationPending,
WebhookCallbackVerificationFailed,
NotificationFailuresExceeded,
AuthorizationRevoked,
UserRemoved,
}
#[derive(PartialEq, Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive]
#[cfg(feature = "eventsub")]
#[cfg_attr(nightly, doc(cfg(feature = "eventsub")))]
pub struct EventSubSubscription {
pub condition: serde_json::Value,
pub created_at: types::Timestamp,
pub id: types::EventSubId,
pub status: Status,
pub transport: TransportResponse,
#[serde(rename = "type")]
pub type_: String,
pub version: String,
}
#[test]
fn test_verification_response() {
use http::header::{HeaderMap, HeaderName, HeaderValue};
#[rustfmt::skip]
let _headers: HeaderMap = vec![
("Twitch-Eventsub-Message-Id","e76c6bd4-55c9-4987-8304-da1588d8988b"),
("Twitch-Eventsub-Message-Retry", "0"),
("Twitch-Eventsub-Message-Type", "webhook_callback_verification"),
("Twitch-Eventsub-Message-Signature","sha256=f56bf6ce06a1adf46fa27831d7d15d"),
("Twitch-Eventsub-Message-Timestamp","2019-11-16T10:11:12.123Z"),
("Twitch-Eventsub-Subscription-Type", "channel.follow"),
("Twitch-Eventsub-Subscription-Version", "1"),
].into_iter()
.map(|(h, v)| {
(
h.parse::<HeaderName>().unwrap(),
v.parse::<HeaderValue>().unwrap(),
)
})
.collect();
let body = r#"
{
"challenge": "pogchamp-kappa-360noscope-vohiyo",
"subscription": {
"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
"status": "webhook_callback_verification_pending",
"type": "channel.follow",
"version": "1",
"condition": {
"broadcaster_user_id": "12826"
},
"transport": {
"method": "webhook",
"callback": "https://example.com/webhooks/callback"
},
"created_at": "2019-11-16T10:11:12.123Z"
}
}"#;
dbg!(crate::eventsub::Payload::parse(&body).unwrap());
}