#[cfg(feature = "unsupported")]
pub mod websocket;
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use super::*;
macro_rules! fill_events {
($callback:ident( $($args:tt)* )) => {
$callback!($($args)*
channel::ChannelUpdateV1;
channel::ChannelFollowV1;
channel::ChannelSubscribeV1;
channel::ChannelCheerV1;
channel::ChannelBanV1;
channel::ChannelUnbanV1;
#[cfg(feature = "unsupported")]
channel::ChannelCharityCampaignDonateBeta;
channel::ChannelPointsCustomRewardAddV1;
channel::ChannelPointsCustomRewardUpdateV1;
channel::ChannelPointsCustomRewardRemoveV1;
channel::ChannelPointsCustomRewardRedemptionAddV1;
channel::ChannelPointsCustomRewardRedemptionUpdateV1;
channel::ChannelPollBeginV1;
channel::ChannelPollProgressV1;
channel::ChannelPollEndV1;
channel::ChannelPredictionBeginV1;
channel::ChannelPredictionProgressV1;
channel::ChannelPredictionLockV1;
channel::ChannelPredictionEndV1;
channel::ChannelRaidV1;
channel::ChannelSubscriptionEndV1;
channel::ChannelSubscriptionGiftV1;
channel::ChannelSubscriptionMessageV1;
channel::ChannelGoalBeginV1;
channel::ChannelGoalProgressV1;
channel::ChannelGoalEndV1;
channel::ChannelHypeTrainBeginV1;
channel::ChannelHypeTrainProgressV1;
channel::ChannelHypeTrainEndV1;
stream::StreamOnlineV1;
stream::StreamOfflineV1;
user::UserUpdateV1;
user::UserAuthorizationGrantV1;
user::UserAuthorizationRevokeV1;
)
};
}
macro_rules! is_thing {
(@inner $s:expr, $thing:ident; $( $(#[$meta:meta])* $module:ident::$event:ident);* $(;)?) => {
match $s {
$( $(#[$meta])* Event::$event(Payload { message : Message::$thing(..), ..}) => true,)*
_ => false,
}
};
($s:expr, $thing:ident) => {
fill_events!(is_thing(@inner $s, $thing;))
};
}
macro_rules! make_event_type {
($enum_docs:literal: pub enum $enum_name:ident {
$(
$event_docs:literal:
$variant_name:ident => $event_name:literal,
)*
},
to_str: $to_str_docs:literal,
from_str_error: $from_str_error:ident,
) => {
#[doc = $enum_docs]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub enum $enum_name {
$(
#[doc = concat!("`", $event_name, "`: ", $event_docs)]
#[serde(rename = $event_name)]
$variant_name,
)*
}
impl $enum_name {
#[doc = $to_str_docs]
pub const fn to_str(&self) -> &'static str {
use $enum_name::*;
match self {
$($variant_name => $event_name,)*
}
}
}
impl std::str::FromStr for $enum_name {
type Err = $from_str_error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use $enum_name::*;
match s {
$($event_name => Ok($variant_name),)*
_ => Err($from_str_error),
}
}
}
impl std::fmt::Display for $enum_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_str())
}
}
};
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("Unknown event type")]
pub struct EventTypeParseError;
make_event_type!("Event Types": pub enum EventType {
"a user donates to the broadcaster’s charity campaign.":
ChannelCharityCampaignDonate => "channel.charity_campaign.donate",
"subscription type sends notifications when a broadcaster updates the category, title, mature flag, or broadcast language for their channel.":
ChannelUpdate => "channel.update",
"a specified channel receives a follow.":
ChannelFollow => "channel.follow",
"a specified channel receives a subscriber. This does not include resubscribes.":
ChannelSubscribe => "channel.subscribe",
"a user cheers on the specified channel.":
ChannelCheer => "channel.cheer",
"a viewer is banned from the specified channel.":
ChannelBan => "channel.ban",
"a viewer is unbanned from the specified channel.":
ChannelUnban => "channel.unban",
"a custom channel points reward has been created for the specified channel.":
ChannelPointsCustomRewardAdd => "channel.channel_points_custom_reward.add",
"a custom channel points reward has been updated for the specified channel.":
ChannelPointsCustomRewardUpdate => "channel.channel_points_custom_reward.update",
"a custom channel points reward has been removed from the specified channel.":
ChannelPointsCustomRewardRemove => "channel.channel_points_custom_reward.remove",
"a viewer has redeemed a custom channel points reward on the specified channel.":
ChannelPointsCustomRewardRedemptionAdd => "channel.channel_points_custom_reward_redemption.add",
"a redemption of a channel points custom reward has been updated for the specified channel.":
ChannelPointsCustomRewardRedemptionUpdate => "channel.channel_points_custom_reward_redemption.update",
"a poll begins on the specified channel.":
ChannelPollBegin => "channel.poll.begin",
"a user responds to a poll on the specified channel.":
ChannelPollProgress => "channel.poll.progress",
"a poll ends on the specified channel.":
ChannelPollEnd => "channel.poll.end",
"a Prediction begins on the specified channel":
ChannelPredictionBegin => "channel.prediction.begin",
"a user participates in a Prediction on the specified channel.":
ChannelPredictionProgress => "channel.prediction.progress",
"a Prediction is locked on the specified channel.":
ChannelPredictionLock => "channel.prediction.lock",
"a Prediction ends on the specified channel.":
ChannelPredictionEnd => "channel.prediction.end",
"a broadcaster raids another broadcaster’s channel.":
ChannelRaid => "channel.raid",
"a subscription to the specified channel expires.":
ChannelSubscriptionEnd => "channel.subscription.end",
"a user gives one or more gifted subscriptions in a channel.":
ChannelSubscriptionGift => "channel.subscription.gift",
"a user sends a resubscription chat message in a specific channel":
ChannelSubscriptionMessage => "channel.subscription.message",
"a goal begins on the specified channel.":
ChannelGoalBegin => "channel.goal.begin",
"a goal makes progress on the specified channel.":
ChannelGoalProgress => "channel.goal.progress",
"a goal ends on the specified channel.":
ChannelGoalEnd => "channel.goal.end",
"a hype train begins on the specified channel.":
ChannelHypeTrainBegin => "channel.hype_train.begin",
"a hype train makes progress on the specified channel.":
ChannelHypeTrainProgress => "channel.hype_train.progress",
"a hype train ends on the specified channel.":
ChannelHypeTrainEnd => "channel.hype_train.end",
"the specified broadcaster starts a stream.":
StreamOnline => "stream.online",
"the specified broadcaster stops a stream.":
StreamOffline => "stream.offline",
"user updates their account.":
UserUpdate => "user.update",
"a user has revoked authorization for your client id. Use this webhook to meet government requirements for handling user data, such as GDPR, LGPD, or CCPA.":
UserAuthorizationRevoke => "user.authorization.revoke",
"a user’s authorization has been granted to your client id.":
UserAuthorizationGrant => "user.authorization.grant",
},
to_str: r#"Get the event string of this event.
```
# use twitch_api::eventsub::EventType;
fn main() {
assert_eq!(EventType::ChannelUpdate.to_str(), "channel.update");
assert_eq!(EventType::ChannelUnban.to_str(), "channel.unban");
}
```"#,
from_str_error: EventTypeParseError,
);
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Event {
#[cfg(feature = "unsupported")]
ChannelCharityCampaignDonateBeta(Payload<channel::ChannelCharityCampaignDonateBeta>),
ChannelUpdateV1(Payload<channel::ChannelUpdateV1>),
ChannelFollowV1(Payload<channel::ChannelFollowV1>),
ChannelSubscribeV1(Payload<channel::ChannelSubscribeV1>),
ChannelCheerV1(Payload<channel::ChannelCheerV1>),
ChannelBanV1(Payload<channel::ChannelBanV1>),
ChannelUnbanV1(Payload<channel::ChannelUnbanV1>),
ChannelPointsCustomRewardAddV1(Payload<channel::ChannelPointsCustomRewardAddV1>),
ChannelPointsCustomRewardUpdateV1(Payload<channel::ChannelPointsCustomRewardUpdateV1>),
ChannelPointsCustomRewardRemoveV1(Payload<channel::ChannelPointsCustomRewardRemoveV1>),
ChannelPointsCustomRewardRedemptionAddV1(
Payload<channel::ChannelPointsCustomRewardRedemptionAddV1>,
),
ChannelPointsCustomRewardRedemptionUpdateV1(
Payload<channel::ChannelPointsCustomRewardRedemptionUpdateV1>,
),
ChannelPollBeginV1(Payload<channel::ChannelPollBeginV1>),
ChannelPollProgressV1(Payload<channel::ChannelPollProgressV1>),
ChannelPollEndV1(Payload<channel::ChannelPollEndV1>),
ChannelPredictionBeginV1(Payload<channel::ChannelPredictionBeginV1>),
ChannelPredictionProgressV1(Payload<channel::ChannelPredictionProgressV1>),
ChannelPredictionLockV1(Payload<channel::ChannelPredictionLockV1>),
ChannelPredictionEndV1(Payload<channel::ChannelPredictionEndV1>),
ChannelGoalBeginV1(Payload<channel::ChannelGoalBeginV1>),
ChannelGoalProgressV1(Payload<channel::ChannelGoalProgressV1>),
ChannelGoalEndV1(Payload<channel::ChannelGoalEndV1>),
ChannelHypeTrainBeginV1(Payload<channel::ChannelHypeTrainBeginV1>),
ChannelHypeTrainProgressV1(Payload<channel::ChannelHypeTrainProgressV1>),
ChannelHypeTrainEndV1(Payload<channel::ChannelHypeTrainEndV1>),
StreamOnlineV1(Payload<stream::StreamOnlineV1>),
StreamOfflineV1(Payload<stream::StreamOfflineV1>),
UserUpdateV1(Payload<user::UserUpdateV1>),
UserAuthorizationGrantV1(Payload<user::UserAuthorizationGrantV1>),
UserAuthorizationRevokeV1(Payload<user::UserAuthorizationRevokeV1>),
ChannelRaidV1(Payload<channel::ChannelRaidV1>),
ChannelSubscriptionEndV1(Payload<channel::ChannelSubscriptionEndV1>),
ChannelSubscriptionGiftV1(Payload<channel::ChannelSubscriptionGiftV1>),
ChannelSubscriptionMessageV1(Payload<channel::ChannelSubscriptionMessageV1>),
}
impl Event {
pub fn parse(source: &str) -> Result<Event, PayloadParseError> {
let (version, ty, message_type) =
get_version_event_type_and_message_type_from_text(source)?;
Self::parse_request(version, &ty, message_type, source.as_bytes().into())
}
pub fn is_notification(&self) -> bool { is_thing!(self, Notification) }
pub fn is_revocation(&self) -> bool { is_thing!(self, Revocation) }
pub fn is_verification_request(&self) -> bool { is_thing!(self, VerificationRequest) }
#[rustfmt::skip]
pub fn get_verification_request(&self) -> Option<&VerificationRequest> {
macro_rules! match_event {
($($(#[$meta:meta])* $module:ident::$event:ident);* $(;)?) => {{
#[deny(unreachable_patterns)]
match &self {
$( $(#[$meta])* Event::$event(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),)*
_ => None,
}
}}
}
fill_events!(match_event())
}
pub fn subscription(&self) -> Result<EventSubSubscription, serde_json::Error> {
macro_rules! match_event {
($($(#[$meta:meta])* $module:ident::$event:ident);* $(;)?) => {{
match &self {
$(
$(#[$meta])*
Event::$event(notif) => Ok({
let self::Payload {subscription, ..} = notif; // FIXME: Use @ pattern-binding, currently stable
EventSubSubscription {
cost: subscription.cost,
condition: subscription.condition.condition()?,
created_at: subscription.created_at.clone(),
id: subscription.id.clone(),
status: subscription.status.clone(),
transport: subscription.transport.clone(),
type_: notif.get_event_type(),
version: notif.get_event_version().to_owned(),
}}),
)*
}
}}
}
fill_events!(match_event())
}
#[cfg(feature = "hmac")]
#[cfg_attr(nightly, doc(cfg(feature = "hmac")))]
#[must_use]
pub fn verify_payload<B>(request: &http::Request<B>, secret: &[u8]) -> bool
where B: AsRef<[u8]> {
use crypto_hmac::{Hmac, Mac};
fn message_and_signature<B>(request: &http::Request<B>) -> Option<(Vec<u8>, Vec<u8>)>
where B: AsRef<[u8]> {
static SHA_HEADER: &str = "sha256=";
let id = request
.headers()
.get("Twitch-Eventsub-Message-Id")?
.as_bytes();
let timestamp = request
.headers()
.get("Twitch-Eventsub-Message-Timestamp")?
.as_bytes();
let body = request.body().as_ref();
let mut message = Vec::with_capacity(id.len() + timestamp.len() + body.len());
message.extend_from_slice(id);
message.extend_from_slice(timestamp);
message.extend_from_slice(body);
let signature = request
.headers()
.get("Twitch-Eventsub-Message-Signature")?
.to_str()
.ok()?;
if !signature.starts_with(SHA_HEADER) {
return None;
}
let signature = signature.split_at(SHA_HEADER.len()).1;
if signature.len() % 2 == 0 {
let signature = ((0..signature.len())
.step_by(2)
.map(|i| u8::from_str_radix(&signature[i..i + 2], 16))
.collect::<Result<Vec<u8>, _>>())
.ok()?;
Some((message, signature))
} else {
None
}
}
if let Some((message, signature)) = message_and_signature(request) {
let mut mac = Hmac::<sha2::Sha256>::new_from_slice(secret).expect("");
mac.update(&message);
mac.verify(crypto_hmac::digest::generic_array::GenericArray::from_slice(&signature))
.is_ok()
} else {
false
}
}
}
#[allow(clippy::type_complexity)]
fn get_version_event_type_and_message_type_from_text(
source: &str,
) -> Result<(Cow<'_, str>, EventType, Cow<'_, [u8]>), PayloadParseError> {
#[derive(Deserialize)]
struct IEventSubscripionInformation {
#[serde(rename = "type")]
type_: EventType,
version: String,
}
#[derive(Deserialize)]
struct IEvent {
subscription: IEventSubscripionInformation,
challenge: Option<serde_json::Value>,
event: Option<serde_json::Value>,
}
let IEvent {
subscription,
challenge,
event,
} = parse_json(source, false)?;
if event.is_some() {
Ok((
subscription.version.into(),
subscription.type_,
Cow::Borrowed(b"notification"),
))
} else if challenge.is_some() {
Ok((
subscription.version.into(),
subscription.type_,
Cow::Borrowed(b"webhook_callback_verification"),
))
} else {
Ok((
subscription.version.into(),
subscription.type_,
Cow::Borrowed(b"revocation"),
))
}
}
#[allow(clippy::type_complexity)]
fn get_version_event_type_and_message_type_from_http<B>(
request: &http::Request<B>,
) -> Result<(Cow<'_, str>, EventType, Cow<'_, [u8]>), PayloadParseError>
where B: AsRef<[u8]> {
use serde::de::IntoDeserializer;
match (
request
.headers()
.get("Twitch-Eventsub-Subscription-Type")
.map(|v| v.as_bytes())
.map(std::str::from_utf8)
.transpose()?,
request
.headers()
.get("Twitch-Eventsub-Subscription-Version")
.map(|v| v.as_bytes())
.map(std::str::from_utf8)
.transpose()?,
request
.headers()
.get("Twitch-Eventsub-Message-Type")
.map(|v| v.as_bytes()),
) {
(Some(ty), Some(version), Some(message_type)) => Ok((
version.into(),
EventType::deserialize(ty.into_deserializer()).map_err(
|_: serde::de::value::Error| PayloadParseError::UnknownEventType(ty.to_owned()),
)?,
message_type.into(),
)),
(..) => Err(PayloadParseError::MalformedEvent),
}
}
impl Event {
pub fn parse_http<B>(request: &http::Request<B>) -> Result<Event, PayloadParseError>
where B: AsRef<[u8]> {
let (version, ty, message_type) =
get_version_event_type_and_message_type_from_http(request)?;
let source = request.body().as_ref().into();
Self::parse_request(version, &ty, message_type, source)
}
#[doc(hidden)]
pub fn parse_request<'a>(
version: Cow<'a, str>,
event_type: &'a EventType,
message_type: Cow<'a, [u8]>,
source: Cow<'a, [u8]>,
) -> Result<Event, PayloadParseError> {
macro_rules! match_event {
($($(#[$meta:meta])* $module:ident::$event:ident);* $(;)?) => {{
#[deny(unreachable_patterns)]
match (version.as_ref(), event_type) {
$( $(#[$meta])* (<$module::$event as EventSubscription>::VERSION, &<$module::$event as EventSubscription>::EVENT_TYPE) => {
Event::$event(Payload::parse_request(message_type, source)?)
} )*
(v, e) => return Err(PayloadParseError::UnimplementedEvent{version: v.to_owned(), event_type: e.clone()})
}
}}
}
Ok(fill_events!(match_event()))
}
#[cfg(feature = "unsupported")]
pub fn parse_websocket(frame: &str) -> Result<EventsubWebsocketData<'_>, PayloadParseError> {
#[derive(Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
struct EventsubWebsocketFrame<'a> {
metadata: EventsubWebsocketMetadata<'a>,
#[serde(borrow)]
payload: &'a serde_json::value::RawValue,
}
let frame: EventsubWebsocketFrame = crate::parse_json(frame, true)?;
macro_rules! match_event {
($metadata:expr, $message_type:literal, $($(#[$meta:meta])* $module:ident::$event:ident);* $(;)?) => {{
#[deny(unreachable_patterns)]
match ($metadata.subscription_version.as_ref(), &$metadata.subscription_type) {
$( $(#[$meta])* (<$module::$event as EventSubscription>::VERSION, &<$module::$event as EventSubscription>::EVENT_TYPE) => {
Event::$event(Payload::parse_request_str($message_type.as_ref(), frame.payload.get())?)
} )*
(v, e) => return Err(PayloadParseError::UnimplementedEvent{version: v.to_owned(), event_type: e.clone()})
}
}}
}
match frame.metadata {
EventsubWebsocketMetadata::Notification(notification) => {
let event = fill_events!(match_event(notification, "notification",));
Ok(EventsubWebsocketData::Notification {
metadata: notification,
payload: event,
})
}
EventsubWebsocketMetadata::Revocation(revocation) => {
let event = fill_events!(match_event(revocation, "revocation",));
Ok(EventsubWebsocketData::Revocation {
metadata: revocation,
payload: event,
})
}
EventsubWebsocketMetadata::Welcome(welcome) => Ok(EventsubWebsocketData::Welcome {
metadata: welcome,
payload: crate::parse_json(frame.payload.get(), true)?,
}),
EventsubWebsocketMetadata::Keepalive(keepalive) => {
Ok(EventsubWebsocketData::Keepalive {
metadata: keepalive,
payload: (),
})
}
EventsubWebsocketMetadata::Reconnect(reconnect) => {
Ok(EventsubWebsocketData::Reconnect {
metadata: reconnect,
payload: crate::parse_json(frame.payload.get(), true)?,
})
}
}
}
}