use std::borrow::Cow;
use crate::types;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::parse_json;
pub mod channel;
pub mod event;
pub mod stream;
pub mod user;
#[doc(inline)]
pub use event::{Event, EventType};
pub trait EventSubscription: DeserializeOwned + Serialize + PartialEq + Clone {
type Payload: PartialEq + std::fmt::Debug + DeserializeOwned + Serialize + Clone;
#[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(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 fn is_verification_request(&self) -> bool { matches!(self, Self::VerificationRequest(..)) }
pub fn is_revocation(&self) -> bool { matches!(self, Self::Revocation(..)) }
pub fn is_notification(&self) -> bool { matches!(self, Self::Notification(..)) }
}
impl<E: EventSubscription> Payload<E> {
pub fn parse(source: &str) -> Result<Payload<E>, PayloadParseError> {
Self::parse_notification(source)
}
pub fn parse_notification(source: &str) -> Result<Payload<E>, 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(Payload {
subscription,
message: Message::Notification(event),
})
}
pub fn parse_revocation(source: &str) -> Result<Payload<E>, 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(Payload {
subscription,
message: Message::Revocation(),
})
}
pub fn parse_verification_request(source: &str) -> Result<Payload<E>, 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(Payload {
subscription,
message: Message::VerificationRequest(VerificationRequest { challenge }),
})
}
pub fn parse_http<B>(request: &http::Request<B>) -> Result<Payload<E>, 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<Payload<E>, PayloadParseError> {
let source = std::str::from_utf8(&source)?;
match ty.as_ref() {
b"notification" => Payload::parse_notification(source),
b"webhook_callback_verification" => Payload::parse_verification_request(source),
b"revocation" => Payload::parse_revocation(source),
typ => Err(PayloadParseError::UnknownMessageType(
String::from_utf8_lossy(typ).into_owned(),
)),
}
}
}
#[derive(thiserror::Error, displaydoc::Display, Debug)]
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 fn get_event_type(&self) -> EventType { E::EVENT_TYPE }
pub fn get_event_version(&self) -> &'static str { E::VERSION }
}
#[derive(Clone, Debug, PartialEq, 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, Debug, typed_builder::TypedBuilder, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
#[non_exhaustive]
pub struct Transport {
pub method: TransportMethod,
pub callback: String,
pub secret: String,
}
impl Transport {
pub fn webhook(callback: impl std::string::ToString, secret: String) -> Transport {
Transport {
method: TransportMethod::Webhook,
callback: callback.to_string(),
secret,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "deny_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,
}
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 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,
}
#[cfg(test)]
mod test {
#[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));
}
}