twitch_api/helix/endpoints/eventsub/
create_eventsub_subscription.rs

1//! Creates an EventSub subscription.
2//!
3//! See also [`HelixClient::create_eventsub_subscription`](crate::helix::HelixClient::create_eventsub_subscription)
4use super::*;
5use crate::eventsub::{EventSubscription, EventType, Status, Transport, TransportResponse};
6
7/// Query Parameters for [Create EventSub Subscription](super::create_eventsub_subscription)
8///
9/// [`create-eventsub-subscription`](https://dev.twitch.tv/docs/api/reference#create-eventsub-subscription)
10#[derive(PartialEq, Eq, Serialize, Clone, Debug)]
11#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))]
12#[non_exhaustive]
13#[must_use]
14pub struct CreateEventSubSubscriptionRequest<E: EventSubscription> {
15    #[cfg_attr(feature = "typed-builder", builder(setter(skip), default))]
16    #[serde(skip)]
17    phantom: std::marker::PhantomData<E>,
18}
19
20impl<E: EventSubscription> CreateEventSubSubscriptionRequest<E> {
21    /// Create a new eventsub subscription
22    pub fn new() -> Self { Self::default() }
23}
24
25impl<E: EventSubscription> Default for CreateEventSubSubscriptionRequest<E> {
26    fn default() -> Self {
27        Self {
28            phantom: Default::default(),
29        }
30    }
31}
32
33impl<E: EventSubscription> helix::Request for CreateEventSubSubscriptionRequest<E> {
34    type Response = CreateEventSubSubscription<E>;
35
36    #[cfg(feature = "twitch_oauth2")]
37    const OPT_SCOPE: &'static [twitch_oauth2::Scope] = E::OPT_SCOPE;
38    const PATH: &'static str = "eventsub/subscriptions";
39    #[cfg(feature = "twitch_oauth2")]
40    const SCOPE: twitch_oauth2::Validator = E::SCOPE;
41}
42
43/// Body Parameters for [Create EventSub Subscription](super::create_eventsub_subscription)
44///
45/// [`create-eventsub-subscription`](https://dev.twitch.tv/docs/api/reference#create-eventsub-subscription)
46///
47/// # Notes
48///
49/// This body is quite different from the official body. If you want the true representation in text, see [`helix::HelixRequestBody::try_to_body`] on [`CreateEventSubSubscriptionRequest<E: EventSubscription>`](CreateEventSubSubscriptionRequest)
50#[derive(PartialEq, Eq, Deserialize, Clone, Debug)]
51#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))]
52#[non_exhaustive]
53pub struct CreateEventSubSubscriptionBody<E: EventSubscription> {
54    /// Subscription that will be created
55    #[serde(bound(deserialize = "E: EventSubscription"))]
56    pub subscription: E,
57    /// The notification delivery specific information
58    pub transport: Transport,
59}
60
61impl<E: EventSubscription> helix::HelixRequestBody for CreateEventSubSubscriptionBody<E> {
62    fn try_to_body(&self) -> Result<hyper::body::Bytes, helix::BodyError> {
63        #[derive(PartialEq, Serialize, Debug)]
64        struct IEventSubRequestBody<'a> {
65            r#type: EventType,
66            version: &'static str,
67            condition: serde_json::Value,
68            transport: &'a Transport,
69        }
70
71        let b = IEventSubRequestBody {
72            r#type: E::EVENT_TYPE,
73            version: E::VERSION,
74            condition: self.subscription.condition()?,
75            transport: &self.transport,
76        };
77        serde_json::to_vec(&b).map_err(Into::into).map(Into::into)
78    }
79}
80
81// FIXME: Builder?
82impl<E: EventSubscription> CreateEventSubSubscriptionBody<E> {
83    /// Create a new [`CreateEventSubSubscriptionBody`]
84    pub const fn new(subscription: E, transport: Transport) -> Self {
85        Self {
86            subscription,
87            transport,
88        }
89    }
90}
91
92/// Return Values for [Create EventSub Subscription](super::create_eventsub_subscription)
93///
94/// [`create-eventsub-subscription`](https://dev.twitch.tv/docs/api/reference#create-eventsub-subscription)
95#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)]
96#[non_exhaustive]
97pub struct CreateEventSubSubscription<E: EventSubscription> {
98    /// ID of the subscription created.
99    pub id: types::EventSubId,
100    /// Status of the subscription.
101    pub status: Status,
102    /// The category of the subscription that was created.
103    #[serde(rename = "type")]
104    pub type_: EventType,
105    /// The version of the subscription type that was created.
106    pub version: String,
107    /// JSON object specifying custom parameters for the subscription.
108    #[serde(bound(deserialize = "E: EventSubscription"))]
109    pub condition: E,
110    /// RFC3339 timestamp indicating when the subscription was created.
111    pub created_at: types::Timestamp,
112    /// JSON object indicating the notification delivery specific information. Includes the transport method and callback URL.
113    pub transport: TransportResponse,
114    /// Total number of subscriptions for the client ID that made the subscription creation request.
115    pub total: usize,
116    /// Total cost of all the subscriptions for the client ID that made the subscription creation request.
117    pub total_cost: usize,
118    /// The maximum total cost allowed for all of the subscriptions for the client ID that made the subscription creation request.
119    pub max_total_cost: usize,
120    /// How much the subscription counts against your limit.
121    pub cost: usize,
122}
123
124impl<E: EventSubscription> helix::RequestPost for CreateEventSubSubscriptionRequest<E> {
125    type Body = CreateEventSubSubscriptionBody<E>;
126
127    fn parse_inner_response(
128        request: Option<Self>,
129        uri: &http::Uri,
130        text: &str,
131        status: http::StatusCode,
132    ) -> Result<helix::Response<Self, Self::Response>, helix::HelixRequestPostError>
133    where
134        Self: Sized,
135    {
136        #[derive(PartialEq, Eq, Deserialize, Debug)]
137        #[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
138        pub struct InnerResponseData<E: EventSubscription> {
139            cost: usize,
140            #[serde(bound(deserialize = "E: EventSubscription"))]
141            condition: E,
142            created_at: types::Timestamp,
143            id: types::EventSubId,
144            status: Status,
145            transport: TransportResponse,
146            #[serde(rename = "type")]
147            type_: EventType,
148            version: String,
149        }
150        #[derive(PartialEq, Deserialize, Debug)]
151        #[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))]
152        struct InnerResponse<E: EventSubscription> {
153            #[serde(bound(deserialize = "E: EventSubscription"))]
154            data: Vec<InnerResponseData<E>>,
155            limit: Option<usize>,
156            total: usize,
157            total_cost: usize,
158            max_total_cost: usize,
159        }
160        let response: InnerResponse<E> = helix::parse_json(text, true).map_err(|e| {
161            helix::HelixRequestPostError::DeserializeError(text.to_string(), e, uri.clone(), status)
162        })?;
163        let data = response.data.into_iter().next().ok_or_else(|| {
164            helix::HelixRequestPostError::InvalidResponse {
165                reason: "missing response data",
166                response: text.to_string(),
167                status,
168                uri: uri.clone(),
169            }
170        })?;
171        Ok(helix::Response::with_data(
172            CreateEventSubSubscription {
173                // helix::Response total is generally the total number of results, not what the total for this endpoint means. Thus, we set it to None.))
174                total: response.total,
175                total_cost: response.total_cost,
176                max_total_cost: response.max_total_cost,
177                cost: data.cost,
178                id: data.id,
179                status: data.status,
180                type_: data.type_,
181                version: data.version,
182                condition: data.condition,
183                created_at: data.created_at,
184                transport: data.transport,
185            },
186            request,
187        ))
188    }
189}
190
191#[cfg(test)]
192#[test]
193fn test_request() {
194    use crate::eventsub::{self, user::UserUpdateV1};
195    use helix::*;
196    let req: CreateEventSubSubscriptionRequest<UserUpdateV1> =
197        CreateEventSubSubscriptionRequest::default();
198
199    let sub = UserUpdateV1::new("1234");
200    let transport =
201        eventsub::Transport::webhook("https://this-is-a-callback.com", "s3cre7".to_string());
202
203    let body = CreateEventSubSubscriptionBody::new(sub, transport);
204
205    assert_eq!(
206        std::str::from_utf8(&body.try_to_body().unwrap()).unwrap(),
207        r#"{"type":"user.update","version":"1","condition":{"user_id":"1234"},"transport":{"method":"webhook","callback":"https://this-is-a-callback.com","secret":"s3cre7"}}"#
208    );
209
210    dbg!(req.create_request(body, "token", "clientid").unwrap());
211
212    // From twitch docs, FIXME: docs say `users.update` in example for Create EventSub Subscription, they also use kebab-case for status
213    // "{"type":"users.update","version":"1","condition":{"user_id":"1234"},"transport":{"method":"webhook","callback":"https://this-is-a-callback.com","secret":"s3cre7"}}"
214    let data = br#"{
215    "data": [
216        {
217            "id": "26b1c993-bfcf-44d9-b876-379dacafe75a",
218            "status": "webhook_callback_verification_pending",
219            "type": "user.update",
220            "version": "1",
221            "condition": {
222                "user_id": "1234"
223            },
224            "created_at": "2020-11-10T14:32:18.730260295Z",
225            "transport": {
226                "method": "webhook",
227                "callback": "https://this-is-a-callback.com"
228            },
229            "cost": 1
230        }
231    ],
232    "total": 1,
233    "total_cost": 1,
234    "max_total_cost": 10000
235}
236    "#
237    .to_vec();
238    let http_response = http::Response::builder().status(202).body(data).unwrap();
239
240    let uri = req.get_uri().unwrap();
241    assert_eq!(
242        uri.to_string(),
243        "https://api.twitch.tv/helix/eventsub/subscriptions?"
244    );
245
246    dbg!(
247        "{:#?}",
248        CreateEventSubSubscriptionRequest::parse_response(Some(req), &uri, http_response).unwrap()
249    );
250}