use std::borrow::Cow;
use crate::types;
use serde::de::DeserializeOwned;
use serde_derive::{Deserialize, Serialize};
use crate::parse_json;
pub mod automod;
pub mod channel;
pub mod conduit;
pub mod event;
pub mod stream;
pub mod user;
#[doc(inline)]
pub use event::{Event, EventType};
pub use event::websocket::*;
pub trait EventSubscription: DeserializeOwned + serde::Serialize + PartialEq + Clone {
type Payload: PartialEq + std::fmt::Debug + DeserializeOwned + serde::Serialize + Clone;
#[cfg(feature = "twitch_oauth2")]
const SCOPE: twitch_oauth2::Validator;
#[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, Eq, Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct VerificationRequest {
pub challenge: String,
}
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum Message<E: EventSubscription + Clone> {
VerificationRequest(VerificationRequest),
Revocation(),
#[serde(bound = "E: EventSubscription")]
Notification(<E as EventSubscription>::Payload),
}
impl<E: EventSubscription + Clone> Message<E> {
pub const fn is_verification_request(&self) -> bool {
matches!(self, Self::VerificationRequest(..))
}
pub const fn is_revocation(&self) -> bool { matches!(self, Self::Revocation(..)) }
pub const fn is_notification(&self) -> bool { matches!(self, Self::Notification(..)) }
}
impl<E: EventSubscription> Payload<E> {
pub fn parse_notification(source: &str) -> Result<Self, PayloadParseError> {
#[derive(Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
struct Notification<E: EventSubscription> {
#[serde(bound = "E: EventSubscription")]
pub subscription: EventSubscriptionInformation<E>,
#[serde(bound = "E: EventSubscription")]
pub event: <E as EventSubscription>::Payload,
}
let Notification {
subscription,
event,
} = parse_json::<Notification<E>>(source, true)?;
Ok(Self {
subscription,
message: Message::Notification(event),
})
}
pub fn parse_revocation(source: &str) -> Result<Self, PayloadParseError> {
#[derive(Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
struct Notification<E: EventSubscription> {
#[serde(bound = "E: EventSubscription")]
pub subscription: EventSubscriptionInformation<E>,
}
let Notification { subscription } = parse_json::<Notification<E>>(source, true)?;
Ok(Self {
subscription,
message: Message::Revocation(),
})
}
pub fn parse_verification_request(source: &str) -> Result<Self, PayloadParseError> {
#[derive(Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
struct Notification<E: EventSubscription> {
#[serde(bound = "E: EventSubscription")]
pub subscription: EventSubscriptionInformation<E>,
#[serde(bound = "E: EventSubscription")]
pub challenge: String,
}
let Notification {
subscription,
challenge,
} = parse_json::<Notification<E>>(source, true)?;
Ok(Self {
subscription,
message: Message::VerificationRequest(VerificationRequest { challenge }),
})
}
pub fn parse_http<B>(request: &http::Request<B>) -> Result<Self, PayloadParseError>
where B: AsRef<[u8]> {
let source = request.body().as_ref().into();
let ty = request
.headers()
.get("Twitch-Eventsub-Message-Type")
.map(|v| v.as_bytes())
.unwrap_or_else(|| b"notification")
.into();
Self::parse_request(ty, source)
}
#[doc(hidden)]
pub fn parse_request<'a>(
ty: Cow<'a, [u8]>,
source: Cow<'a, [u8]>,
) -> Result<Self, PayloadParseError> {
let source = std::str::from_utf8(&source)?;
Self::parse_request_str(ty.as_ref(), source)
}
#[doc(hidden)]
pub fn parse_request_str<'a>(ty: &'a [u8], source: &'a str) -> Result<Self, PayloadParseError> {
match ty {
b"notification" => Self::parse_notification(source),
b"webhook_callback_verification" => Self::parse_verification_request(source),
b"revocation" => Self::parse_revocation(source),
typ => Err(PayloadParseError::UnknownMessageType(
String::from_utf8_lossy(typ).into_owned(),
)),
}
}
}
#[derive(thiserror::Error, displaydoc::Display, Debug)]
#[non_exhaustive]
pub enum PayloadParseError {
Utf8Error(#[from] std::str::Utf8Error),
DeserializeError(#[from] crate::DeserError),
UnknownMessageType(String),
UnknownEventType(String),
MalformedEvent,
UnimplementedEvent {
version: String,
event_type: EventType,
},
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Payload<E: EventSubscription + Clone> {
#[serde(bound = "E: EventSubscription")]
pub subscription: EventSubscriptionInformation<E>,
#[serde(bound = "E: EventSubscription")]
pub message: Message<E>,
}
impl<E: EventSubscription + Clone> Payload<E> {
pub const fn get_event_type(&self) -> EventType { E::EVENT_TYPE }
pub const fn get_event_version(&self) -> &'static str { E::VERSION }
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct EventSubscriptionInformation<E: EventSubscription> {
pub id: types::EventSubId,
pub status: Status,
pub cost: usize,
#[serde(bound = "E: EventSubscription")]
pub condition: E,
pub created_at: types::Timestamp,
pub transport: TransportResponse,
#[serde(rename = "type")]
pub type_: EventType,
pub version: String,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct WebhookTransport {
pub callback: String,
pub secret: String,
}
impl std::fmt::Debug for WebhookTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebhookTransport")
.field("callback", &self.callback)
.field("secret", &"[redacted]")
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct WebsocketTransport {
pub session_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct ConduitTransport {
pub conduit_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "lowercase")]
#[non_exhaustive]
pub enum Transport {
Webhook(WebhookTransport),
Websocket(WebsocketTransport),
Conduit(ConduitTransport),
}
impl Transport {
pub fn webhook(callback: impl std::string::ToString, secret: String) -> Self {
Self::Webhook(WebhookTransport {
callback: callback.to_string(),
secret,
})
}
pub fn websocket(session_id: impl std::string::ToString) -> Self {
Self::Websocket(WebsocketTransport {
session_id: session_id.to_string(),
})
}
pub fn conduit(conduit_id: impl std::string::ToString) -> Self {
Self::Conduit(ConduitTransport {
conduit_id: conduit_id.to_string(),
})
}
#[must_use]
pub const fn is_webhook(&self) -> bool { matches!(self, Self::Webhook(..)) }
#[must_use]
pub const fn is_websocket(&self) -> bool { matches!(self, Self::Websocket(..)) }
#[must_use]
pub const fn is_conduit(&self) -> bool { matches!(self, Self::Conduit(..)) }
pub const fn as_webhook(&self) -> Option<&WebhookTransport> {
if let Self::Webhook(v) = self {
Some(v)
} else {
None
}
}
pub const fn as_websocket(&self) -> Option<&WebsocketTransport> {
if let Self::Websocket(v) = self {
Some(v)
} else {
None
}
}
pub const fn as_conduit(&self) -> Option<&ConduitTransport> {
if let Self::Conduit(v) = self {
Some(v)
} else {
None
}
}
pub fn try_into_webhook(self) -> Option<WebhookTransport> {
if let Self::Webhook(v) = self {
Some(v)
} else {
None
}
}
pub fn try_into_websocket(self) -> Option<WebsocketTransport> {
if let Self::Websocket(v) = self {
Some(v)
} else {
None
}
}
pub fn try_into_conduit(self) -> Option<ConduitTransport> {
if let Self::Conduit(v) = self {
Some(v)
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct WebsocketTransportResponse {
pub session_id: String,
pub connected_at: Option<types::Timestamp>,
pub disconnected_at: Option<types::Timestamp>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct WebhookTransportResponse {
pub callback: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct ConduitTransportResponse {
pub conduit_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "method", rename_all = "lowercase")]
#[non_exhaustive]
pub enum TransportResponse {
Webhook(WebhookTransportResponse),
Websocket(WebsocketTransportResponse),
Conduit(ConduitTransportResponse),
}
impl TransportResponse {
#[must_use]
pub const fn is_webhook(&self) -> bool { matches!(self, Self::Webhook(..)) }
#[must_use]
pub const fn is_websocket(&self) -> bool { matches!(self, Self::Websocket(..)) }
#[must_use]
pub const fn is_conduit(&self) -> bool { matches!(self, Self::Conduit(..)) }
pub const fn as_webhook(&self) -> Option<&WebhookTransportResponse> {
if let Self::Webhook(v) = self {
Some(v)
} else {
None
}
}
pub const fn as_websocket(&self) -> Option<&WebsocketTransportResponse> {
if let Self::Websocket(v) = self {
Some(v)
} else {
None
}
}
pub const fn as_conduit(&self) -> Option<&ConduitTransportResponse> {
if let Self::Conduit(v) = self {
Some(v)
} else {
None
}
}
pub fn try_into_webhook(self) -> Result<WebhookTransportResponse, Self> {
if let Self::Webhook(v) = self {
Ok(v)
} else {
Err(self)
}
}
pub fn try_into_websocket(self) -> Result<WebsocketTransportResponse, Self> {
if let Self::Websocket(v) = self {
Ok(v)
} else {
Err(self)
}
}
pub fn try_into_conduit(self) -> Result<ConduitTransportResponse, Self> {
if let Self::Conduit(v) = self {
Ok(v)
} else {
Err(self)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum TransportMethod {
Webhook,
Websocket,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Status {
#[serde(rename = "enabled")]
Enabled,
#[serde(rename = "webhook_callback_verification_pending")]
WebhookCallbackVerificationPending,
#[serde(rename = "webhook_callback_verification_failed")]
WebhookCallbackVerificationFailed,
#[serde(rename = "notification_failures_exceeded")]
NotificationFailuresExceeded,
#[serde(rename = "authorization_revoked")]
AuthorizationRevoked,
#[serde(rename = "moderator_removed")]
ModeratorRemoved,
#[serde(rename = "user_removed")]
UserRemoved,
#[serde(rename = "version_removed")]
VersionRemoved,
#[serde(rename = "beta_maintenance")]
BetaMaintenance,
#[serde(rename = "websocket_disconnected")]
WebsocketDisconnected,
#[serde(rename = "websocket_failed_ping_pong")]
WebsocketFailedPingPong,
#[serde(rename = "websocket_received_inbound_traffic")]
WebsocketReceivedInboundTraffic,
#[serde(rename = "websocket_connection_unused")]
WebsocketConnectionUnused,
#[serde(rename = "websocket_internal_error")]
WebsocketInternalError,
#[serde(rename = "websocket_network_timeout")]
WebsocketNetworkTimeout,
#[serde(rename = "websocket_network_error")]
WebsocketNetworkError,
#[serde(rename = "websocket_failed_to_reconnect")]
WebsocketFailedToReconnect,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive]
#[cfg(feature = "eventsub")]
#[cfg_attr(nightly, doc(cfg(feature = "eventsub")))]
pub struct EventSubSubscription {
pub cost: usize,
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_: EventType,
pub version: String,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive]
#[cfg(feature = "eventsub")]
#[cfg_attr(nightly, doc(cfg(feature = "eventsub")))]
pub struct Conduit {
pub id: types::ConduitId,
pub shard_count: usize,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
#[non_exhaustive]
#[cfg(feature = "eventsub")]
#[cfg_attr(nightly, doc(cfg(feature = "eventsub")))]
pub struct Shard {
pub id: types::ConduitShardId,
pub transport: Transport,
}
impl Shard {
pub fn new(id: impl Into<types::ConduitShardId>, transport: Transport) -> Self {
Self {
id: id.into(),
transport,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ShardStatus {
Enabled,
WebhookCallbackVerificationPending,
WebhookCallbackVerificationFailed,
NotificationFailuresExceeded,
WebsocketDisconnected,
WebsocketFailedPingPong,
WebsocketReceivedInboundTraffic,
WebsocketInternalError,
WebsocketNetworkTimeout,
WebsocketNetworkError,
WebsocketFailedToReconnect,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)]
#[non_exhaustive]
pub struct ShardError {
pub id: types::ConduitShardId,
pub message: String,
pub code: String,
}
#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)]
#[non_exhaustive]
pub struct ShardResponse {
pub id: types::ConduitShardId,
pub status: ShardStatus,
pub transport: TransportResponse,
}
pub(crate) trait NamedField {
const NAME: &'static str;
const OPT_PREFIX: Option<&'static str> = None;
}
mod enum_field_as_inner {
use serde::ser::SerializeMap;
use super::*;
pub fn deserialize<'de, D, S>(deserializer: D) -> Result<S, D::Error>
where
D: serde::Deserializer<'de>,
S: serde::Deserialize<'de> + NamedField, {
struct Inner<S>(std::marker::PhantomData<S>);
impl<'de, S> serde::de::Visitor<'de> for Inner<S>
where S: serde::Deserialize<'de> + NamedField
{
type Value = S;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("any object")
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where A: serde::de::MapAccess<'de> {
let mut map = map;
let mut value = None;
while let Some(key) = map.next_key::<String>()? {
if key == S::NAME {
value = Some(map.next_value()?);
} else {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
value.ok_or_else(|| serde::de::Error::missing_field(S::NAME))
}
}
deserializer.deserialize_any(Inner(std::marker::PhantomData))
}
pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: serde::Serialize + NamedField,
S: serde::Serializer, {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(T::NAME, value)?;
map.end()
}
}
mod enum_field_as_inner_prefixed {
use serde::ser::SerializeMap;
use super::*;
pub fn deserialize<'de, D, S>(deserializer: D) -> Result<S, D::Error>
where
D: serde::Deserializer<'de>,
S: serde::Deserialize<'de> + NamedField, {
struct Inner<S>(std::marker::PhantomData<S>);
impl<'de, S> serde::de::Visitor<'de> for Inner<S>
where S: serde::Deserialize<'de> + NamedField
{
type Value = S;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("any object")
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where A: serde::de::MapAccess<'de> {
let mut map = map;
let mut value = None;
let name = S::OPT_PREFIX.unwrap().to_string() + S::NAME;
while let Some(key) = map.next_key::<String>()? {
if key == name {
value = Some(map.next_value()?);
} else {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
value.ok_or_else(|| serde::de::Error::missing_field(S::NAME))
}
}
deserializer.deserialize_any(Inner(std::marker::PhantomData))
}
pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: serde::Serialize + NamedField,
S: serde::Serializer, {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(&(T::OPT_PREFIX.unwrap().to_string() + T::NAME), value)?;
map.end()
}
}
#[cfg(test)]
mod test {
#[test]
fn test_websocket_notification() {
let frame = r#"
{
"metadata": {
"message_id": "befa7b53-d79d-478f-86b9-120f112b044e",
"message_type": "notification",
"message_timestamp": "2019-11-16T10:11:12.123Z",
"subscription_type": "channel.follow",
"subscription_version": "1"
},
"payload": {
"subscription": {
"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
"status": "enabled",
"type": "channel.follow",
"version": "1",
"cost": 1,
"condition": {
"broadcaster_user_id": "12826"
},
"transport": {
"method": "websocket",
"session_id": "AQoQexAWVYKSTIu4ec_2VAxyuhAB"
},
"created_at": "2019-11-16T10:11:12.123Z"
},
"event": {
"user_id": "1337",
"user_login": "awesome_user",
"user_name": "Awesome_User",
"broadcaster_user_id": "12826",
"broadcaster_user_login": "twitch",
"broadcaster_user_name": "Twitch",
"followed_at": "2020-07-15T18:16:11.17106713Z"
}
}
}"#;
crate::eventsub::Event::parse_websocket(frame).unwrap();
}
#[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",
"cost": 1,
"condition": {
"broadcaster_user_id": "12826"
},
"transport": {
"method": "webhook",
"callback": "https://example.com/webhooks/callback"
},
"created_at": "2019-11-16T10:11:12.123Z"
}
}"#;
let mut request = http::Request::builder();
let _ = std::mem::replace(request.headers_mut().unwrap(), headers);
let request = request.body(body.as_bytes().to_vec()).unwrap();
let _payload = dbg!(crate::eventsub::Event::parse_http(&request).unwrap());
let payload = dbg!(crate::eventsub::Event::parse(
std::str::from_utf8(request.body()).unwrap()
)
.unwrap());
crate::tests::roundtrip(&payload)
}
#[test]
fn test_revoke() {
use http::header::{HeaderMap, HeaderName, HeaderValue};
#[rustfmt::skip]
let headers: HeaderMap = vec![
("Content-Length", "458"),
("Twitch-Eventsub-Message-Id", "84c1e79a-2a4b-4c13-ba0b-4312293e9308"),
("Twitch-Eventsub-Message-Retry", "0"),
("Twitch-Eventsub-Message-Type", "revocation"),
("Twitch-Eventsub-Message-Signature", "sha256=c1f92c51dab9888b0d6fb5f7e8e758"),
("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#"{"subscription":{"id":"f1c2a387-161a-49f9-a165-0f21d7a4e1c4","status":"authorization_revoked","type":"channel.follow","cost":1,"version":"1","condition":{"broadcaster_user_id":"12826"},"transport":{"method":"webhook","callback":"https://example.com/webhooks/callback"},"created_at":"2019-11-16T10:11:12.123Z"}}"#;
let mut request = http::Request::builder();
let _ = std::mem::replace(request.headers_mut().unwrap(), headers);
let request = request.body(body.as_bytes().to_vec()).unwrap();
let _payload = dbg!(crate::eventsub::Event::parse_http(&request).unwrap());
let payload = dbg!(crate::eventsub::Event::parse(
std::str::from_utf8(request.body()).unwrap()
)
.unwrap());
crate::tests::roundtrip(&payload)
}
#[test]
#[cfg(feature = "hmac")]
fn verify_request() {
use http::header::{HeaderMap, HeaderName, HeaderValue};
let secret = b"secretabcd";
#[rustfmt::skip]
let headers: HeaderMap = vec![
("Content-Length", "458"),
("Content-Type", "application/json"),
("Twitch-Eventsub-Message-Id", "ae2ff348-e102-16be-a3eb-6830c1bf38d2"),
("Twitch-Eventsub-Message-Retry", "0"),
("Twitch-Eventsub-Message-Signature", "sha256=d10f5bd9474b7ac7bd7105eb79c2d52768b4d0cd2a135982c3bf5a1d59a78823"),
("Twitch-Eventsub-Message-Timestamp", "2021-02-19T23:47:00.8091512Z"),
("Twitch-Eventsub-Message-Type", "notification"),
("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#"{"subscription":{"id":"ae2ff348-e102-16be-a3eb-6830c1bf38d2","status":"enabled","type":"channel.follow","version":"1","condition":{"broadcaster_user_id":"44429626"},"transport":{"method":"webhook","callback":"null"},"created_at":"2021-02-19T23:47:00.7621315Z"},"event":{"user_id":"28408015","user_login":"testFromUser","user_name":"testFromUser","broadcaster_user_id":"44429626","broadcaster_user_login":"44429626","broadcaster_user_name":"testBroadcaster"}}"#;
let mut request = http::Request::builder();
let _ = std::mem::replace(request.headers_mut().unwrap(), headers);
let request = request.body(body.as_bytes().to_vec()).unwrap();
dbg!(&body);
assert!(crate::eventsub::Event::verify_payload(&request, secret));
}
#[test]
#[cfg(feature = "hmac")]
fn verify_request_challenge() {
use http::header::{HeaderMap, HeaderName, HeaderValue};
let secret = b"HELLOabc2321";
#[rustfmt::skip]
let headers: HeaderMap = vec![
("Twitch-Eventsub-Message-Id", "8d8fa82b-9792-79da-4e11-a6fa58a7a582"),
("Twitch-Eventsub-Message-Retry", "0"),
("Twitch-Eventsub-Message-Signature", "sha256=091f6a5c74fba820f2d50e9d0c5e7650556ee009375af2cc662e610e670bc412"),
("Twitch-Eventsub-Message-Timestamp", "2022-02-06T04:03:24.2726598Z"),
("Twitch-Eventsub-Message-Type", "webhook_callback_verification"),
("Twitch-Eventsub-Subscription-Type", "channel.subscribe"),
("Twitch-Eventsub-Subscription-Version", "1"),
].into_iter()
.map(|(h, v)| {
(
h.parse::<HeaderName>().unwrap(),
v.parse::<HeaderValue>().unwrap(),
)
})
.collect();
let body = r#"{"challenge":"11535768-497e-14ec-8197-ba2cb5341a01","subscription":{"id":"8d8fa82b-9792-79da-4e11-a6fa58a7a582","status":"webhook_callback_verification_pending","type":"channel.subscribe","version":"1","condition":{"broadcaster_user_id":"88525095"},"transport":{"method":"webhook","callback":"http://localhost:80/twitch/eventsub"},"created_at":"2022-02-06T04:03:24.2706497Z","cost":0}}"#;
let mut request = http::Request::builder();
let _ = std::mem::replace(request.headers_mut().unwrap(), headers);
let request = request.body(body.as_bytes().to_vec()).unwrap();
let _payload = dbg!(crate::eventsub::Event::parse_http(&request).unwrap());
assert!(crate::eventsub::Event::verify_payload(&request, secret));
}
}