use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use super::*;
macro_rules! is_thing {
($s:expr, $thing:ident) => {
is_thing!(@inner $s, $thing;
channel::ChannelUpdateV1;
channel::ChannelFollowV1;
channel::ChannelSubscribeV1;
channel::ChannelCheerV1;
channel::ChannelBanV1;
channel::ChannelUnbanV1;
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;
)
};
(@inner $s:expr, $thing:ident; $($module:ident::$event:ident);* $(;)?) => {
match $s {
$(Event::$event(Payload { message : Message::$thing(..), ..}) => true,)*
_ => false,
}
};
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_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.poll.begin")]
ChannelPollBegin,
#[serde(rename = "channel.poll.progress")]
ChannelPollProgress,
#[serde(rename = "channel.poll.end")]
ChannelPollEnd,
#[serde(rename = "channel.prediction.begin")]
ChannelPredictionBegin,
#[serde(rename = "channel.prediction.progress")]
ChannelPredictionProgress,
#[serde(rename = "channel.prediction.lock")]
ChannelPredictionLock,
#[serde(rename = "channel.prediction.end")]
ChannelPredictionEnd,
#[serde(rename = "channel.raid")]
ChannelRaid,
#[serde(rename = "channel.subscription.end")]
ChannelSubscriptionEnd,
#[serde(rename = "channel.subscription.gift")]
ChannelSubscriptionGift,
#[serde(rename = "channel.subscription.message")]
ChannelSubscriptionMessage,
#[serde(rename = "channel.goal.begin")]
ChannelGoalBegin,
#[serde(rename = "channel.goal.progress")]
ChannelGoalProgress,
#[serde(rename = "channel.goal.end")]
ChannelGoalEnd,
#[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,
#[serde(rename = "user.authorization.grant")]
UserAuthorizationGrant,
}
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Event {
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> {
match &self {
Event::ChannelUpdateV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelFollowV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelSubscribeV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelCheerV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelBanV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelUnbanV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPointsCustomRewardAddV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPointsCustomRewardUpdateV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPointsCustomRewardRemoveV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPointsCustomRewardRedemptionAddV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPointsCustomRewardRedemptionUpdateV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPollBeginV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPollProgressV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPollEndV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPredictionBeginV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPredictionProgressV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPredictionLockV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelPredictionEndV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelGoalBeginV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelGoalProgressV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelGoalEndV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelHypeTrainBeginV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelHypeTrainProgressV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelHypeTrainEndV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::StreamOnlineV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::StreamOfflineV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::UserUpdateV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::UserAuthorizationGrantV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::UserAuthorizationRevokeV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelRaidV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelSubscriptionEndV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelSubscriptionGiftV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
Event::ChannelSubscriptionMessageV1(Payload { message: Message::VerificationRequest(v), ..}) => Some(v),
_ => None,
}
}
pub fn subscription(&self) -> Result<EventSubSubscription, serde_json::Error> {
macro_rules! match_event {
($($module:ident::$event:ident);* $(;)?) => {{
match &self {
$(
Event::$event(notif) => Ok({
let self::Payload {subscription, ..} = notif;
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(),
}}),
)*
}
}}
}
match_event!(
channel::ChannelUpdateV1;
channel::ChannelFollowV1;
channel::ChannelSubscribeV1;
channel::ChannelCheerV1;
channel::ChannelBanV1;
channel::ChannelUnbanV1;
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;
)
}
#[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 {
($($module:ident::$event:ident);* $(;)?) => {{
#[deny(unreachable_patterns)]
match (version.as_ref(), event_type) {
$( (<$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(match_event! {
channel::ChannelUpdateV1;
channel::ChannelFollowV1;
channel::ChannelSubscribeV1;
channel::ChannelCheerV1;
channel::ChannelBanV1;
channel::ChannelUnbanV1;
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;
})
}
}