#![allow(deprecated)]
static ERROR_TRYFROM: &str = "no match";
macro_rules! impl_de_ser {
(@field $e:expr) => {".{}"};
($type:ident, $fmt:literal, $($field:ident),* $(,)? $(?$opt_field:ident),* $(,)?) => {
impl From<$type> for String {
fn from(t: $type) -> Self { format!(concat!($fmt, $(impl_de_ser!(@field $field),)+ $(impl_de_ser!(@field $opt_field),)*), $(t.$field,)*$(t.$opt_field.map(|f| f.to_string()).unwrap_or_default(),)*).trim_end_matches(".").to_owned() }
}
impl<'a> From<&'a $type> for String {
fn from(t: &'a $type) -> Self { format!(concat!($fmt, $(impl_de_ser!(@field $field),)+ $(impl_de_ser!(@field $opt_field),)*), $(t.$field,)*$(t.$opt_field.map(|f| f.to_string()).unwrap_or_default(),)*).trim_end_matches(".").to_owned() }
}
impl From<$type> for super::Topics {
fn from(t: $type) -> Self {
use super::Topic as _;
t.into_topic()
}
}
impl ::std::fmt::Display for $type {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let s: String = ::std::convert::TryInto::try_into(self).map_err(|_| ::std::fmt::Error)?;
f.write_str(&s)
}
}
impl ::std::convert::TryFrom<String> for $type {
type Error = &'static str;
fn try_from(s: String) -> ::std::result::Result<Self, Self::Error> {
if s.starts_with($fmt) {
let sub_s = s.strip_prefix($fmt).ok_or("could not strip str, this should never be hit")?;
match sub_s.split('.').collect::<Vec<_>>().as_slice() {
["", $($field,)* $($opt_field,)*] => {
Ok($type {
$(
$field: $field.parse()
.map_err(|_| concat!("could not parse field <", stringify!($field), ">"))?,
)*
$(
$opt_field: Some($opt_field.parse()
.map_err(|_| concat!("could not parse field <", stringify!($opt_field), ">"))?),
)*
} )
}
#[allow(unreachable_patterns)]
["", $($field,)*] => {
Ok($type {
$(
$field: $field.parse()
.map_err(|_| concat!("could not parse field <", stringify!($field), ">"))?,
)*
$(
$opt_field: None,
)*
} )
}
_ => Err(crate::pubsub::ERROR_TRYFROM)
}
} else {
Err(crate::pubsub::ERROR_TRYFROM)
}
}
}
};
}
use serde::Deserializer;
use serde_derive::{Deserialize, Serialize};
pub mod automod_queue;
pub mod channel_bits;
pub mod channel_bits_badge;
#[cfg(feature = "unsupported")]
pub mod channel_cheer;
pub mod channel_points;
#[cfg(feature = "unsupported")]
pub mod channel_sub_gifts;
pub mod channel_subscriptions;
#[cfg(feature = "unsupported")]
pub mod community_points;
#[cfg(feature = "unsupported")]
pub mod following;
#[cfg(feature = "unsupported")]
pub mod hypetrain;
pub mod moderation;
#[cfg(feature = "unsupported")]
pub mod raid;
pub mod user_moderation_notifications;
#[cfg(feature = "unsupported")]
pub mod video_playback;
use crate::parse_json;
pub trait Topic: serde::Serialize + Into<String> {
#[cfg(feature = "twitch_oauth2")]
#[cfg_attr(nightly, doc(cfg(feature = "twitch_oauth2")))]
const SCOPE: twitch_oauth2::Validator;
fn into_topic(self) -> Topics;
}
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Hash)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Topics {
AutoModQueue(automod_queue::AutoModQueue),
#[cfg(feature = "unsupported")]
CommunityPointsChannelV1(community_points::CommunityPointsChannelV1),
ChannelBitsEventsV2(channel_bits::ChannelBitsEventsV2),
ChannelBitsBadgeUnlocks(channel_bits_badge::ChannelBitsBadgeUnlocks),
#[cfg(feature = "unsupported")]
ChannelCheerEventsPublicV1(channel_cheer::ChannelCheerEventsPublicV1),
#[cfg(feature = "unsupported")]
ChannelSubGiftsV1(channel_sub_gifts::ChannelSubGiftsV1),
ChatModeratorActions(moderation::ChatModeratorActions),
ChannelPointsChannelV1(channel_points::ChannelPointsChannelV1),
ChannelSubscribeEventsV1(channel_subscriptions::ChannelSubscribeEventsV1),
#[cfg(feature = "unsupported")]
VideoPlayback(video_playback::VideoPlayback),
#[cfg(feature = "unsupported")]
VideoPlaybackById(video_playback::VideoPlaybackById),
#[cfg(feature = "unsupported")]
HypeTrainEventsV1(hypetrain::HypeTrainEventsV1),
#[cfg(feature = "unsupported")]
HypeTrainEventsV1Rewards(hypetrain::HypeTrainEventsV1Rewards),
#[cfg(feature = "unsupported")]
Following(following::Following),
#[cfg(feature = "unsupported")]
Raid(raid::Raid),
UserModerationNotifications(user_moderation_notifications::UserModerationNotifications),
}
impl std::fmt::Display for Topics {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use self::Topics::*;
let s = match self {
AutoModQueue(t) => t.to_string(),
#[cfg(feature = "unsupported")]
CommunityPointsChannelV1(t) => t.to_string(),
ChannelBitsEventsV2(t) => t.to_string(),
ChannelBitsBadgeUnlocks(t) => t.to_string(),
#[cfg(feature = "unsupported")]
ChannelCheerEventsPublicV1(t) => t.to_string(),
#[cfg(feature = "unsupported")]
ChannelSubGiftsV1(t) => t.to_string(),
ChatModeratorActions(t) => t.to_string(),
ChannelPointsChannelV1(t) => t.to_string(),
ChannelSubscribeEventsV1(t) => t.to_string(),
#[cfg(feature = "unsupported")]
VideoPlayback(t) => t.to_string(),
#[cfg(feature = "unsupported")]
VideoPlaybackById(t) => t.to_string(),
#[cfg(feature = "unsupported")]
HypeTrainEventsV1(t) => t.to_string(),
#[cfg(feature = "unsupported")]
HypeTrainEventsV1Rewards(t) => t.to_string(),
#[cfg(feature = "unsupported")]
Following(t) => t.to_string(),
#[cfg(feature = "unsupported")]
Raid(t) => t.to_string(),
UserModerationNotifications(t) => t.to_string(),
};
f.write_str(&s)
}
}
#[derive(Serialize)]
struct ITopicSubscribeData<'a> {
topics: &'a [String],
#[serde(skip_serializing_if = "Option::is_none")]
auth_token: Option<&'a str>,
}
#[derive(Serialize)]
struct ITopicSubscribe<'a> {
#[serde(rename = "type")]
_type: &'static str,
nonce: Option<&'a str>,
data: ITopicSubscribeData<'a>,
}
pub fn listen_command<'t, T, N>(
topics: &'t [Topics],
auth_token: T,
nonce: N,
) -> Result<String, serde_json::Error>
where
T: Into<Option<&'t str>>,
N: Into<Option<&'t str>>,
{
let topics = topics.iter().map(|t| t.to_string()).collect::<Vec<_>>();
serde_json::to_string(&ITopicSubscribe {
_type: "LISTEN",
nonce: nonce.into(),
data: ITopicSubscribeData {
topics: &topics,
auth_token: auth_token.into(),
},
})
}
pub fn unlisten_command<'t, O>(
topics: &'t [Topics],
nonce: O,
) -> Result<String, serde_json::Error>
where
O: Into<Option<&'t str>>,
{
let topics = topics.iter().map(|t| t.to_string()).collect::<Vec<_>>();
serde_json::to_string(&ITopicSubscribe {
_type: "UNLISTEN",
nonce: nonce.into(),
data: ITopicSubscribeData {
topics: &topics,
auth_token: None,
},
})
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct TwitchResponse {
pub nonce: Option<String>,
pub error: Option<String>,
}
impl TwitchResponse {
pub fn is_successful(&self) -> bool { self.error.as_ref().map_or(true, |s| s.is_empty()) }
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub enum TopicData {
AutoModQueue {
topic: automod_queue::AutoModQueue,
#[serde(rename = "message")]
reply: Box<automod_queue::AutoModQueueReply>,
},
ChannelBitsEventsV2 {
topic: channel_bits::ChannelBitsEventsV2,
#[serde(rename = "message")]
reply: Box<channel_bits::ChannelBitsEventsV2Reply>,
},
ChannelBitsBadgeUnlocks {
topic: channel_bits_badge::ChannelBitsBadgeUnlocks,
#[serde(rename = "message")]
reply: Box<channel_bits_badge::ChannelBitsBadgeUnlocksReply>,
},
ChatModeratorActions {
topic: moderation::ChatModeratorActions,
#[serde(rename = "message")]
reply: Box<moderation::ChatModeratorActionsReply>,
},
ChannelPointsChannelV1 {
topic: channel_points::ChannelPointsChannelV1,
#[serde(rename = "message")]
reply: Box<channel_points::ChannelPointsChannelV1Reply>,
},
ChannelSubscribeEventsV1 {
topic: channel_subscriptions::ChannelSubscribeEventsV1,
#[serde(rename = "message")]
reply: Box<channel_subscriptions::ChannelSubscribeEventsV1Reply>, },
#[cfg(feature = "unsupported")]
CommunityPointsChannelV1 {
topic: community_points::CommunityPointsChannelV1,
#[serde(rename = "message")]
reply: Box<channel_points::ChannelPointsChannelV1Reply>,
},
#[cfg(feature = "unsupported")]
ChannelCheerEventsPublicV1 {
topic: channel_cheer::ChannelCheerEventsPublicV1,
#[serde(rename = "message")]
reply: Box<channel_cheer::ChannelCheerEventsPublicV1Reply>,
},
#[cfg(feature = "unsupported")]
ChannelSubGiftsV1 {
topic: channel_sub_gifts::ChannelSubGiftsV1,
#[serde(rename = "message")]
reply: Box<channel_sub_gifts::ChannelSubGiftsV1Reply>,
},
#[cfg(feature = "unsupported")]
VideoPlayback {
topic: video_playback::VideoPlayback,
#[serde(rename = "message")]
reply: Box<video_playback::VideoPlaybackReply>,
},
#[cfg(feature = "unsupported")]
VideoPlaybackById {
topic: video_playback::VideoPlaybackById,
#[serde(rename = "message")]
reply: Box<video_playback::VideoPlaybackReply>,
},
#[cfg(feature = "unsupported")]
HypeTrainEventsV1 {
topic: hypetrain::HypeTrainEventsV1,
#[serde(rename = "message")]
reply: Box<hypetrain::HypeTrainEventsV1Reply>, },
#[cfg(feature = "unsupported")]
HypeTrainEventsV1Rewards {
topic: hypetrain::HypeTrainEventsV1Rewards,
#[serde(rename = "message")]
reply: Box<hypetrain::HypeTrainEventsV1Reply>,
},
#[cfg(feature = "unsupported")]
Following {
topic: following::Following,
#[serde(rename = "message")]
reply: Box<following::FollowingReply>,
},
#[cfg(feature = "unsupported")]
Raid {
topic: raid::Raid,
#[serde(rename = "message")]
reply: Box<raid::RaidReply>,
},
UserModerationNotifications {
topic: user_moderation_notifications::UserModerationNotifications,
#[serde(rename = "message")]
reply: Box<user_moderation_notifications::UserModerationNotificationsReply>,
},
}
impl<'de> serde::Deserialize<'de> for TopicData {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize, Debug)]
struct ITopicData {
topic: Topics,
message: String,
}
let reply = ITopicData::deserialize(deserializer).map_err(|e| {
serde::de::Error::custom(format!("could not deserialize topic reply: {e}"))
})?;
Ok(match reply.topic {
Topics::AutoModQueue(topic) => Self::AutoModQueue {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::CommunityPointsChannelV1(topic) => Self::CommunityPointsChannelV1 {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
Topics::ChannelBitsEventsV2(topic) => Self::ChannelBitsEventsV2 {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
Topics::ChannelBitsBadgeUnlocks(topic) => Self::ChannelBitsBadgeUnlocks {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::ChannelSubGiftsV1(topic) => Self::ChannelSubGiftsV1 {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::ChannelCheerEventsPublicV1(topic) => Self::ChannelCheerEventsPublicV1 {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
Topics::ChatModeratorActions(topic) => Self::ChatModeratorActions {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
Topics::ChannelPointsChannelV1(topic) => Self::ChannelPointsChannelV1 {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
Topics::ChannelSubscribeEventsV1(topic) => Self::ChannelSubscribeEventsV1 {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::VideoPlayback(topic) => Self::VideoPlayback {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::VideoPlaybackById(topic) => Self::VideoPlaybackById {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::HypeTrainEventsV1(topic) => Self::HypeTrainEventsV1 {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::HypeTrainEventsV1Rewards(topic) => Self::HypeTrainEventsV1Rewards {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::Following(topic) => Self::Following {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
#[cfg(feature = "unsupported")]
Topics::Raid(topic) => Self::Raid {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
Topics::UserModerationNotifications(topic) => Self::UserModerationNotifications {
topic,
reply: parse_json(&reply.message, true).map_err(serde::de::Error::custom)?,
},
})
}
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum Response {
#[serde(rename = "RESPONSE")]
Response(TwitchResponse),
#[serde(rename = "MESSAGE")]
Message {
data: TopicData,
},
#[serde(rename = "PONG")]
Pong,
#[serde(rename = "RECONNECT")]
Reconnect,
}
impl Response {
pub fn parse(source: &str) -> Result<Self, crate::DeserError> { parse_json(source, true) }
}
fn deserialize_default_from_null<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: serde::Deserialize<'de> + Default, {
use serde::Deserialize;
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
}
fn deserialize_none_from_empty_string<'de, D, S>(deserializer: D) -> Result<Option<S>, D::Error>
where
D: serde::Deserializer<'de>,
S: serde::Deserialize<'de>, {
use serde::de::IntoDeserializer;
struct Inner<S>(std::marker::PhantomData<S>);
impl<'de, S> serde::de::Visitor<'de> for Inner<S>
where S: serde::Deserialize<'de>
{
type Value = Option<S>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("any string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where E: serde::de::Error {
match value {
"" => Ok(None),
v => S::deserialize(v.into_deserializer()).map(Some),
}
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where E: serde::de::Error {
match &*value {
"" => Ok(None),
v => S::deserialize(v.into_deserializer()).map(Some),
}
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where E: serde::de::Error {
Ok(None)
}
}
deserializer.deserialize_any(Inner(std::marker::PhantomData))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error() {
let source = r#"
{
"type": "RESPONSE",
"nonce": "44h1k13746815ab1r2",
"error": ""
}
"#;
let expected = Response::Response(TwitchResponse {
nonce: Some(String::from("44h1k13746815ab1r2")),
error: Some(String::new()),
});
let actual = Response::parse(source).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn listen() {
let topic =
Topics::ChannelBitsEventsV2(channel_bits::ChannelBitsEventsV2 { channel_id: 12345 });
let expected = r#"{"type":"LISTEN","nonce":"my nonce","data":{"topics":["channel-bits-events-v2.12345"],"auth_token":"my token"}}"#;
let actual = listen_command(&[topic], "my token", "my nonce").expect("should serialize");
assert_eq!(expected, actual);
}
#[test]
fn unlisten() {
let topic =
Topics::ChannelBitsEventsV2(channel_bits::ChannelBitsEventsV2 { channel_id: 12345 });
let expected = r#"{"type":"UNLISTEN","nonce":"my nonce","data":{"topics":["channel-bits-events-v2.12345"]}}"#;
let actual = unlisten_command(&[topic], "my nonce").expect("should serialize");
assert_eq!(expected, actual);
}
}