1use crate::auth::{Token, TokenStorage, TokenType};
2use anyhow::bail;
3use anyhow::Result;
4
5use reqwest::Client as HttpClient;
6use reqwest::{Method, Response};
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize)]
10pub struct TwitchData<T> {
11 pub data: Vec<T>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Pagination {
16 pub cursor: Option<String>,
17}
18
19#[derive(Debug, Clone)]
20pub struct Client<T: TokenStorage> {
21 pub client_id: String,
22 pub client_secret: String,
23 pub token: Token,
24 pub http_client: HttpClient,
25 pub token_storage: T,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct User {
30 pub id: String,
31 pub login: String,
32 pub display_name: String,
33 pub r#type: String,
34 pub broadcaster_type: String,
35 pub description: String,
36 pub profile_image_url: String,
37 pub offline_image_url: String,
38 pub view_count: i64,
39 pub email: Option<String>,
40 pub created_at: String,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RewardImage {
45 pub url_1x: String,
46 pub url_2x: String,
47 pub url_4x: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct RewardMaxPerStream {
52 pub is_enabled: bool,
53 pub max_per_stream: i64,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RewardMaxPerUserPerStream {
58 pub is_enabled: bool,
59 pub max_per_user_per_stream: i64,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RewardGlobalCooldown {
64 pub is_enabled: bool,
65 pub global_cooldown_seconds: i64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Reward {
70 pub broadcaster_id: String,
71 pub broadcaster_login: String,
72 pub broadcaster_name: String,
73 pub id: String,
74 pub title: String,
75 pub prompt: String,
76 pub cost: i64,
77 pub image: Option<RewardImage>,
78 pub default_image: RewardImage,
79 pub background_color: String,
80 pub is_enabled: bool,
81 pub is_user_input_required: bool,
82 pub max_per_stream_setting: RewardMaxPerStream,
83 pub max_per_user_per_stream_setting: RewardMaxPerUserPerStream,
84 pub global_cooldown_setting: RewardGlobalCooldown,
85 pub is_paused: bool,
86 pub is_in_stock: bool,
87 pub should_redemptions_skip_request_queue: bool,
88 pub redemptions_redeemed_current_stream: Option<i64>,
89 pub cooldown_expires_at: Option<String>,
90}
91
92#[derive(Debug, Default, Clone, Serialize, Deserialize)]
93pub struct RewardCreate {
94 pub title: String,
95 pub cost: i64,
96 pub prompt: Option<String>,
97 pub is_enabled: Option<bool>,
98 pub background_color: Option<String>,
99 pub is_user_input_required: Option<bool>,
100 pub is_max_per_stream_enabled: Option<bool>,
101 pub max_per_stream: Option<i64>,
102 pub is_max_per_user_per_stream_enabled: Option<bool>,
103 pub max_per_user_per_stream: Option<i64>,
104 pub is_global_cooldown_enabled: Option<bool>,
105 pub global_cooldown_seconds: Option<i64>,
106 pub should_redemptions_skip_request_queue: Option<bool>,
107}
108
109#[derive(Debug, Default, Clone, Serialize, Deserialize)]
110pub struct RewardUpdate {
111 pub title: Option<String>,
112 pub cost: Option<i64>,
113 pub prompt: Option<String>,
114 pub is_enabled: Option<bool>,
115 pub background_color: Option<String>,
116 pub is_user_input_required: Option<bool>,
117 pub is_max_per_stream_enabled: Option<bool>,
118 pub max_per_stream: Option<i64>,
119 pub is_max_per_user_per_stream_enabled: Option<bool>,
120 pub max_per_user_per_stream: Option<i64>,
121 pub is_global_cooldown_enabled: Option<bool>,
122 pub global_cooldown_seconds: Option<i64>,
123 pub is_paused: Option<bool>,
124 pub should_redemptions_skip_request_queue: Option<bool>,
125}
126
127#[derive(Debug, Default, Clone, Serialize, Deserialize)]
128pub struct RedemptionStatus {
129 pub status: String,
130}
131
132#[derive(Debug, Default, Clone, Serialize, Deserialize)]
133pub struct EventSubTransport {
134 pub method: String,
135 pub callback: Option<String>,
136 pub secret: Option<String>,
137 pub session_id: Option<String>,
138 pub connected_at: Option<String>,
139}
140
141#[derive(Debug, Default, Clone, Serialize, Deserialize)]
142pub struct EventSubCondition {
143 pub broadcaster_id: Option<String>,
144 pub broadcaster_user_id: Option<String>,
145 pub moderator_user_id: Option<String>,
146 pub user_id: Option<String>,
147 pub from_broadcaster_user_id: Option<String>,
148 pub to_broadcaster_user_id: Option<String>,
149 pub reward_id: Option<String>,
150}
151
152#[derive(Debug, Default, Clone, Serialize, Deserialize)]
153pub struct EventSub {
154 pub id: String,
155 pub status: String,
156 pub r#type: String,
157 pub version: String,
158 pub condition: EventSubCondition,
159 pub created_at: String,
160 pub transport: EventSubTransport,
161 pub cost: i64,
162}
163
164#[derive(Debug, Default, Clone, Serialize, Deserialize)]
165pub struct EventSubCreate {
166 pub r#type: String,
167 pub version: String,
168 pub condition: EventSubCondition,
169 pub transport: EventSubTransport,
170}
171
172#[derive(Debug, Default, Clone, Serialize, Deserialize)]
173pub struct BanUser {
174 pub user_id: String,
175 pub duration: i64,
176 pub reason: Option<String>,
177}
178
179#[derive(Debug, Default, Clone, Serialize, Deserialize)]
180pub struct BanUserObj {
181 pub data: BanUser,
182}
183
184#[derive(Debug, Default, Clone, Serialize, Deserialize)]
185pub struct BannedUser {
186 pub broadcaster_id: String,
187 pub moderator_id: String,
188 pub user_id: String,
189 pub created_at: String,
190 pub end_time: String,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ChannelInformation {
195 pub broadcaster_id: String,
196 pub broadcaster_login: String,
197 pub broadcaster_name: String,
198 pub broadcaster_language: String,
199 pub game_name: String,
200 pub game_id: String,
201 pub title: String,
202 pub delay: i64,
203 pub tags: Vec<String>,
204 pub content_classification_labels: Vec<String>,
205 pub is_branded_content: bool,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct PredictionTopPredictor {
210 pub user_id: String,
211 pub user_name: String,
212 pub user_login: String,
213 pub channel_points_used: i64,
214 pub channel_points_won: i64,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct PredictionOutcome {
219 pub id: String,
220 pub title: String,
221 pub users: i64,
222 pub channel_points: i64,
223 pub top_predictors: Option<Vec<PredictionTopPredictor>>,
224 pub color: String,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct Prediction {
229 pub id: String,
230 pub broadcaster_id: String,
231 pub broadcaster_name: String,
232 pub broadcaster_login: String,
233 pub title: String,
234 pub winning_outcome_id: Option<String>,
235 pub outcomes: Vec<PredictionOutcome>,
236 pub prediction_window: i64,
237 pub status: String,
238 pub created_at: String,
239 pub ended_at: Option<String>,
240 pub locked_at: Option<String>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct PredictionOutcomeCreate {
245 pub title: String,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct PredictionCreate {
250 pub broadcaster_id: String,
251 pub title: String,
252 pub outcomes: Vec<PredictionOutcomeCreate>,
253 pub prediction_window: i64,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct PredictionEnd {
258 pub broadcaster_id: String,
259 pub id: String,
260 pub status: String,
261 pub winning_outcome_id: Option<String>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct CommercialStart {
266 pub broadcaster_id: String,
267 pub length: i64,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct Commercial {
272 pub length: i64,
273 pub message: String,
274 pub retry_after: i64,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct Announcement {
279 pub message: String,
280 pub color: Option<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct Whisper {
285 pub message: String,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct Stream {
290 pub id: String,
291 pub user_id: String,
292 pub user_login: String,
293 pub user_name: String,
294 pub game_id: String,
295 pub game_name: String,
296 pub r#type: String,
297 pub title: String,
298 pub tags: Vec<String>,
299 pub viewer_count: i64,
300 pub started_at: String,
301 pub language: String,
302 pub thumbnail_url: String,
303 pub is_mature: bool,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ChannelFollowers {
308 pub followed_at: String,
309 pub user_id: String,
310 pub user_login: String,
311 pub user_name: String,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct ChannelFollowersData {
316 pub data: Vec<ChannelFollowers>,
317 pub pagination: Pagination,
318 pub total: i64,
319}
320
321pub enum VideoId {
322 Id(String),
323 UserId(String),
324 GameId(String),
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct VideoMutedSegment {
329 pub duration: i64,
330 pub offset: i64,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct Video {
335 pub id: String,
336 pub stream_id: String,
337 pub user_id: String,
338 pub user_login: String,
339 pub user_name: String,
340 pub title: String,
341 pub description: String,
342 pub created_at: String,
343 pub published_at: String,
344 pub url: String,
345 pub thumbnail_url: String,
346 pub viewable: String,
347 pub view_count: i64,
348 pub language: String,
349 pub r#type: String,
350 pub duration: String,
351 pub muted_segments: Option<Vec<VideoMutedSegment>>,
352}
353
354impl<T: TokenStorage> Client<T> {
355 pub async fn http_request<T2: serde::Serialize>(
356 &mut self,
357 method: Method,
358 uri: String,
359 data_json: Option<T2>,
360 data_form: Option<String>,
361 ) -> Result<Response> {
362 let mut req = self.http_client.request(method, uri);
363
364 req = match data_json {
365 Some(data_json) => req.json(&data_json),
366 None => match data_form {
367 Some(data_form) => req.body(data_form),
368 None => req,
369 },
370 };
371
372 let req = req
373 .timeout(core::time::Duration::from_secs(5))
374 .header(
375 "Authorization",
376 format!("Bearer {0}", self.token.access_token),
377 )
378 .header("Client-Id", self.client_id.clone());
379
380 Ok(req.send().await?)
381 }
382
383 pub async fn request<T1: serde::Serialize + std::clone::Clone>(
384 &mut self,
385 method: Method,
386 uri: String,
387 data_json: Option<T1>,
388 data_form: Option<String>,
389 ) -> Result<Response> {
390 let mut res = self
391 .http_request(
392 method.clone(),
393 uri.clone(),
394 data_json.clone(),
395 data_form.clone(),
396 )
397 .await?;
398
399 if res.status() == reqwest::StatusCode::UNAUTHORIZED {
400 self.refresh_token().await?;
402 res = self.http_request(method, uri, data_json, data_form).await?;
403 }
404
405 Ok(res)
406 }
407
408 pub async fn request_result<
409 T1: for<'de> serde::Deserialize<'de>,
410 T2: serde::Serialize + std::clone::Clone,
411 >(
412 &mut self,
413 method: Method,
414 uri: String,
415 data_json: Option<T2>,
416 data_form: Option<String>,
417 ) -> Result<T1> {
418 let res = self
419 .request::<T2>(method, uri, data_json, data_form)
420 .await?;
421 Ok(res.json::<T1>().await?)
422 }
423
424 pub async fn get<T1: for<'de> serde::Deserialize<'de>>(&mut self, uri: String) -> Result<T1> {
425 return self
426 .request_result::<T1, String>(Method::GET, uri, None, None)
427 .await;
428 }
429
430 pub async fn post_empty(&mut self, uri: String) -> Result<()> {
431 match self.request::<String>(Method::POST, uri, None, None).await {
432 Ok(..) => Ok(()),
433 Err(e) => Err(e),
434 }
435 }
436
437 pub async fn post_form<T1: for<'de> serde::Deserialize<'de>>(
438 &mut self,
439 uri: String,
440 data: String,
441 ) -> Result<T1> {
442 return self
443 .request_result::<T1, String>(Method::POST, uri, None, Some(data))
444 .await;
445 }
446
447 pub async fn post_json<
448 T1: for<'de> serde::Deserialize<'de>,
449 T2: serde::Serialize + std::clone::Clone,
450 >(
451 &mut self,
452 uri: String,
453 data: T2,
454 ) -> Result<T1> {
455 return self
456 .request_result::<T1, T2>(Method::POST, uri, Some(data), None)
457 .await;
458 }
459
460 pub async fn post_json_empty<T1: serde::Serialize + std::clone::Clone>(
461 &mut self,
462 uri: String,
463 data: T1,
464 ) -> Result<()> {
465 match self
466 .request::<T1>(Method::POST, uri, Some(data), None)
467 .await
468 {
469 Ok(..) => Ok(()),
470 Err(e) => Err(e),
471 }
472 }
473
474 pub async fn patch_json<
475 T1: for<'de> serde::Deserialize<'de>,
476 T2: serde::Serialize + std::clone::Clone,
477 >(
478 &mut self,
479 uri: String,
480 data: T2,
481 ) -> Result<T1> {
482 return self
483 .request_result::<T1, T2>(Method::PATCH, uri, Some(data), None)
484 .await;
485 }
486
487 pub async fn delete(&mut self, uri: String) -> Result<()> {
488 self.request::<String>(Method::DELETE, uri, None, None)
489 .await?;
490 Ok(())
491 }
492
493 pub async fn get_token_user(&mut self) -> Result<User> {
494 match &self.token.user {
495 Some(v) => Ok(v.clone()),
496 None => {
497 if self.token.token_type == TokenType::UserAccessToken && self.token.user.is_none()
498 {
499 let user = self.get_user().await?;
500 self.token.user = Some(user.clone());
501 return Ok(user);
502 }
503
504 bail!("No User");
505 }
506 }
507 }
508
509 pub async fn get_token_user_id(&mut self) -> Result<String> {
510 match &self.token.user {
511 Some(v) => Ok(v.id.clone()),
512 None => {
513 let user = self.get_token_user().await?;
514 return Ok(user.id.clone());
515 }
516 }
517 }
518
519 pub async fn get_token_user_login(&mut self) -> Result<String> {
520 match &self.token.user {
521 Some(v) => Ok(v.id.clone()),
522 None => {
523 let user = self.get_token_user().await?;
524 return Ok(user.login.clone());
525 }
526 }
527 }
528
529 pub async fn get_users_by_ids(&mut self, user_ids: Vec<String>) -> Result<Vec<User>> {
530 Ok(self
531 .get::<TwitchData<User>>(format!(
532 "https://api.twitch.tv/helix/users?id={0}",
533 user_ids.join("&id=")
534 ))
535 .await?
536 .data)
537 }
538
539 pub async fn get_users_by_logins(&mut self, user_logins: Vec<String>) -> Result<Vec<User>> {
540 Ok(self
541 .get::<TwitchData<User>>(format!(
542 "https://api.twitch.tv/helix/users?login={0}",
543 user_logins.join("&login=")
544 ))
545 .await?
546 .data)
547 }
548
549 pub async fn get_user_by_id(&mut self, user_id: String) -> Result<User> {
550 match self.get_users_by_ids(vec![user_id]).await?.first() {
551 Some(user) => Ok(user.clone()),
552 None => bail!("No User found"),
553 }
554 }
555
556 pub async fn get_user_by_login(&mut self, user_login: String) -> Result<User> {
557 match self.get_users_by_logins(vec![user_login]).await?.first() {
558 Some(user) => Ok(user.clone()),
559 None => bail!("No User found"),
560 }
561 }
562
563 pub async fn get_user(&mut self) -> Result<User> {
564 match self
565 .get::<TwitchData<User>>("https://api.twitch.tv/helix/users".to_string())
566 .await?
567 .data
568 .first()
569 {
570 Some(user) => Ok(user.clone()),
571 None => bail!("No User found"),
572 }
573 }
574
575 pub async fn create_custom_reward(&mut self, reward: &RewardCreate) -> Result<Reward> {
576 let broadcaster_id = self.get_token_user_id().await?;
577 match self
578 .post_json::<TwitchData<Reward>, _>(format!("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}"), reward)
579 .await?
580 .data
581 .first()
582 {
583 Some(reward) => Ok(reward.clone()),
584 None => bail!("No User found"),
585 }
586 }
587
588 pub async fn update_custom_reward(
589 &mut self,
590 id: String,
591 reward: &RewardUpdate,
592 ) -> Result<Reward> {
593 let broadcaster_id = self.get_token_user_id().await?;
594 match self
595 .patch_json::<TwitchData<Reward>, _>(format!("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}&id={id}"), reward)
596 .await?
597 .data
598 .first()
599 {
600 Some(reward) => Ok(reward.clone()),
601 None => bail!("No User found"),
602 }
603 }
604
605 pub async fn get_custom_rewards(&mut self, ids: Vec<String>) -> Result<Vec<Reward>> {
606 let broadcaster_id = self.get_token_user_id().await?;
607 Ok(self
608 .get::<TwitchData<Reward>>(format!(
609 "https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}{0}",
610 if ids.len() > 0 { format!("&id={0}", ids.join("&id=") ) } else { "".to_string() }
611 ))
612 .await?
613 .data)
614 }
615
616 pub async fn get_custom_reward(&mut self, id: String) -> Result<Reward> {
617 match self.get_custom_rewards(vec![id]).await?.first() {
618 Some(reward) => Ok(reward.clone()),
619 None => bail!("No Reward found"),
620 }
621 }
622
623 pub async fn delete_custom_reward(&mut self, id: String) -> Result<()> {
624 let broadcaster_id = self.get_token_user_id().await?;
625 Ok(self
626 .delete(format!(
627 "https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}&id={id}"
628 ))
629 .await?)
630 }
631
632 pub async fn update_redemptions_status(
633 &mut self,
634 id: &String,
635 redemptions: Vec<String>,
636 status: &RedemptionStatus,
637 ) -> Result<Vec<RedemptionStatus>> {
638 let broadcaster_id = self.get_token_user_id().await?;
639 Ok(self
640 .patch_json::<TwitchData<RedemptionStatus>, _>(format!(
641 "https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id={broadcaster_id}&reward_id={id}{0}",
642 format!("&id={0}", redemptions.join("&id=") )
643 ), status)
644 .await?
645 .data)
646 }
647
648 pub async fn update_redemption_status(
649 &mut self,
650 id: &String,
651 redemption: &String,
652 status: &RedemptionStatus,
653 ) -> Result<RedemptionStatus> {
654 match self
655 .update_redemptions_status(id, vec![redemption.clone()], status)
656 .await?
657 .first()
658 {
659 Some(status) => Ok(status.clone()),
660 None => bail!("No Redemption found"),
661 }
662 }
663
664 pub async fn create_eventsub_subscription(
665 &mut self,
666 eventsub: &EventSubCreate,
667 ) -> Result<EventSub> {
668 match self
669 .post_json::<TwitchData<EventSub>, _>(
670 format!("https://api.twitch.tv/helix/eventsub/subscriptions"),
671 eventsub,
672 )
673 .await?
674 .data
675 .first()
676 {
677 Some(eventsub) => Ok(eventsub.clone()),
678 None => bail!("No EventSub found"),
679 }
680 }
681
682 pub async fn delete_eventsub_subscription(&mut self, id: String) -> Result<()> {
683 Ok(self
684 .delete(format!(
685 "https://api.twitch.tv/helix/eventsub/subscriptions?id={id}"
686 ))
687 .await?)
688 }
689
690 pub async fn add_channel_moderator(&mut self, id: String) -> Result<()> {
691 let broadcaster_id = self.get_token_user_id().await?;
692 Ok(self
693 .post_empty(format!(
694 "https://api.twitch.tv/helix/moderation/moderators?broadcaster_id={broadcaster_id}&user_id={id}"
695 ))
696 .await?)
697 }
698
699 pub async fn remove_channel_moderator(&mut self, id: String) -> Result<()> {
700 let broadcaster_id = self.get_token_user_id().await?;
701 Ok(self
702 .delete(format!(
703 "https://api.twitch.tv/helix/moderation/moderators?broadcaster_id={broadcaster_id}&user_id={id}"
704 ))
705 .await?)
706 }
707
708 pub async fn ban_user(
709 &mut self,
710 broadcaster_id: String,
711 banuser: &BanUser,
712 ) -> Result<BannedUser> {
713 let moderator_id = self.get_token_user_id().await?;
714 match self
715 .post_json::<TwitchData<BannedUser>, _>(
716 format!("https://api.twitch.tv/helix/moderation/bans?moderator_id={moderator_id}&broadcaster_id={broadcaster_id}"),
717 BanUserObj {
718 data: banuser.clone()
719 },
720 )
721 .await?
722 .data
723 .first()
724 {
725 Some(banneduser) => Ok(banneduser.clone()),
726 None => bail!("Ban User failed"),
727 }
728 }
729
730 pub async fn unban_user(&mut self, broadcaster_id: String, user_id: String) -> Result<()> {
731 let moderator_id = self.get_token_user_id().await?;
732 Ok(self
733 .delete(format!(
734 "https://api.twitch.tv/helix/moderation/bans?moderator_id={moderator_id}&broadcaster_id={broadcaster_id}&user_id={user_id}"
735 ))
736 .await?)
737 }
738
739 pub async fn shoutout(
740 &mut self,
741 from_broadcaster_id: String,
742 to_broadcaster_id: String,
743 ) -> Result<()> {
744 let moderator_id = self.get_token_user_id().await?;
745 Ok(self
746 .post_empty(format!(
747 "https://api.twitch.tv/helix/chat/shoutouts?from_broadcaster_id={from_broadcaster_id}&to_broadcaster_id={to_broadcaster_id}&moderator_id={moderator_id}"
748 ))
749 .await?)
750 }
751
752 pub async fn get_channel_information(
753 &mut self,
754 broadcaster_ids: Vec<String>,
755 ) -> Result<Vec<ChannelInformation>> {
756 Ok(self
757 .get::<TwitchData<ChannelInformation>>(format!(
758 "https://api.twitch.tv/helix/channels?{0}",
759 if broadcaster_ids.len() > 0 {
760 format!(
761 "broadcaster_id={0}",
762 broadcaster_ids.join("&broadcaster_id=")
763 )
764 } else {
765 "".to_string()
766 }
767 ))
768 .await?
769 .data)
770 }
771
772 pub async fn whisper(&mut self, to_user_id: String, message: String) -> Result<()> {
773 let from_user_id = self.get_token_user_id().await?;
774 Ok(self
775 .post_json_empty(
776 format!("https://api.twitch.tv/helix/whispers?from_user_id={from_user_id}&to_user_id={to_user_id}"),
777 Whisper {
778 message: message
779 },
780 )
781 .await?)
782 }
783
784 pub async fn get_predictions(
785 &mut self,
786 id: Option<String>,
787 first: Option<String>,
788 after: Option<String>,
789 ) -> Result<Vec<Prediction>> {
790 let broadcaster_id = self.get_token_user_id().await?;
791 Ok(self
792 .get::<TwitchData<Prediction>>(format!(
793 "https://api.twitch.tv/helix/predictions?broadcaster_id={broadcaster_id}{0}{1}{2}",
794 if let Some(id) = id {
795 format!("&id={id}")
796 } else {
797 "".to_string()
798 },
799 if let Some(first) = first {
800 format!("&first={first}")
801 } else {
802 "".to_string()
803 },
804 if let Some(after) = after {
805 format!("&after={after}")
806 } else {
807 "".to_string()
808 },
809 ))
810 .await?
811 .data)
812 }
813
814 pub async fn create_prediction(
815 &mut self,
816 title: String,
817 outcomes: Vec<String>,
818 prediction_window: i64,
819 ) -> Result<Prediction> {
820 let broadcaster_id = self.get_token_user_id().await?;
821 match self
822 .post_json::<TwitchData<Prediction>, _>(
823 "https://api.twitch.tv/helix/predictions".to_string(),
824 PredictionCreate {
825 broadcaster_id: broadcaster_id,
826 title: title,
827 outcomes: outcomes
828 .into_iter()
829 .map(|o| PredictionOutcomeCreate { title: o })
830 .collect(),
831 prediction_window: prediction_window,
832 },
833 )
834 .await?
835 .data
836 .first()
837 {
838 Some(prediction) => Ok(prediction.clone()),
839 None => bail!("Create Prediction failed"),
840 }
841 }
842
843 pub async fn end_prediction(
844 &mut self,
845 id: String,
846 status: String,
847 winning_outcome_id: Option<String>,
848 ) -> Result<Prediction> {
849 let broadcaster_id = self.get_token_user_id().await?;
850 match self
851 .patch_json::<TwitchData<Prediction>, _>(
852 "https://api.twitch.tv/helix/predictions".to_string(),
853 PredictionEnd {
854 broadcaster_id: broadcaster_id,
855 id: id,
856 status: status,
857 winning_outcome_id: winning_outcome_id,
858 },
859 )
860 .await?
861 .data
862 .first()
863 {
864 Some(prediction) => Ok(prediction.clone()),
865 None => bail!("End Prediction failed"),
866 }
867 }
868
869 pub async fn send_chat_announcement(
870 &mut self,
871 broadcaster_id: String,
872 message: String,
873 color: Option<String>,
874 ) -> Result<()> {
875 let moderator_id = self.get_token_user_id().await?;
876 Ok(self
877 .post_json_empty(
878 format!("https://api.twitch.tv/helix/chat/announcements?broadcaster_id={broadcaster_id}&moderator_id={moderator_id}"),
879 Announcement {
880 message: message,
881 color: color,
882 }
883 )
884 .await?)
885 }
886
887 pub async fn start_commercial(&mut self, length: i64) -> Result<Commercial> {
888 let broadcaster_id = self.get_token_user_id().await?;
889 match self
890 .post_json::<TwitchData<Commercial>, _>(
891 "https://api.twitch.tv/helix/channels/commercial".to_string(),
892 CommercialStart {
893 broadcaster_id: broadcaster_id,
894 length: length,
895 },
896 )
897 .await?
898 .data
899 .first()
900 {
901 Some(commercial) => Ok(commercial.clone()),
902 None => bail!("Start Commercial failed"),
903 }
904 }
905
906 pub async fn get_streams(
907 &mut self,
908 user_ids: Option<Vec<String>>,
909 user_logins: Option<Vec<String>>,
910 game_ids: Option<Vec<String>>,
911 r#type: Option<String>,
912 languages: Option<Vec<String>>,
913 first: Option<i64>,
914 before: Option<String>,
915 after: Option<String>,
916 ) -> Result<Vec<Stream>> {
917 Ok(self
918 .get::<TwitchData<Stream>>(format!(
919 "https://api.twitch.tv/helix/streams?{0}{1}{2}{3}{4}{5}{6}{7}",
920 if let Some(user_ids) = user_ids {
921 format!("&user_id={}", user_ids.join("&user_id="))
922 } else {
923 "".to_string()
924 },
925 if let Some(user_logins) = user_logins {
926 format!("&user_login={}", user_logins.join("&user_login="))
927 } else {
928 "".to_string()
929 },
930 if let Some(game_ids) = game_ids {
931 format!("&game_id={}", game_ids.join("&game_id="))
932 } else {
933 "".to_string()
934 },
935 if let Some(type_) = r#type {
936 format!("&type={type_}")
937 } else {
938 "".to_string()
939 },
940 if let Some(languages) = languages {
941 format!("&language={}", languages.join("&language="))
942 } else {
943 "".to_string()
944 },
945 if let Some(first) = first {
946 format!("&first={first}")
947 } else {
948 "".to_string()
949 },
950 if let Some(before) = before {
951 format!("&before={before}")
952 } else {
953 "".to_string()
954 },
955 if let Some(after) = after {
956 format!("&after={after}")
957 } else {
958 "".to_string()
959 },
960 ))
961 .await?
962 .data)
963 }
964
965 pub async fn get_stream(&mut self) -> Result<Stream> {
966 let broadcaster_id = self.get_token_user_id().await?;
967 match self
968 .get_streams(
969 vec![broadcaster_id].into(),
970 None,
971 None,
972 None,
973 None,
974 None,
975 None,
976 None,
977 )
978 .await?
979 .first()
980 {
981 Some(stream) => Ok(stream.clone()),
982 None => bail!("No stream found"),
983 }
984 }
985
986 pub async fn add_channel_vip(&mut self, id: String) -> Result<()> {
987 let broadcaster_id = self.get_token_user_id().await?;
988 Ok(self
989 .post_empty(format!(
990 "https://api.twitch.tv/helix/channels/vips?broadcaster_id={broadcaster_id}&user_id={id}"
991 ))
992 .await?)
993 }
994
995 pub async fn remove_channel_vip(&mut self, id: String) -> Result<()> {
996 let broadcaster_id = self.get_token_user_id().await?;
997 Ok(self
998 .delete(format!(
999 "https://api.twitch.tv/helix/channels/vips?broadcaster_id={broadcaster_id}&user_id={id}"
1000 ))
1001 .await?)
1002 }
1003
1004 pub async fn get_channel_followers_total(&mut self, broadcaster_id: String) -> Result<i64> {
1005 Ok(self
1006 .get::<ChannelFollowersData>(format!(
1007 "https://api.twitch.tv/helix/channels/followers?broadcaster_id={0}",
1008 broadcaster_id
1009 ))
1010 .await?
1011 .total)
1012 }
1013
1014 pub async fn get_videos(
1015 &mut self,
1016 id: VideoId,
1017 language: Option<String>,
1018 period: Option<String>,
1019 sort: Option<String>,
1020 r#type: Option<String>,
1021 first: Option<String>,
1022 after: Option<String>,
1023 before: Option<String>,
1024 ) -> Result<Vec<Video>> {
1025 Ok(self
1026 .get::<TwitchData<Video>>(format!(
1027 "https://api.twitch.tv/helix/videos?{}{}{}{}{}{}{}{}",
1028 match id {
1029 VideoId::Id(value) => format!("id={}", value),
1030 VideoId::UserId(value) => format!("user_id={}", value),
1031 VideoId::GameId(value) => format!("game_id={}", value),
1032 },
1033 if let Some(value) = language {
1034 format!("&language={}", value)
1035 } else {
1036 "".to_string()
1037 },
1038 if let Some(value) = period {
1039 format!("&period={}", value)
1040 } else {
1041 "".to_string()
1042 },
1043 if let Some(value) = sort {
1044 format!("&sort={}", value)
1045 } else {
1046 "".to_string()
1047 },
1048 if let Some(value) = r#type {
1049 format!("&type={}", value)
1050 } else {
1051 "".to_string()
1052 },
1053 if let Some(value) = first {
1054 format!("&first={}", value)
1055 } else {
1056 "".to_string()
1057 },
1058 if let Some(value) = after {
1059 format!("&after={}", value)
1060 } else {
1061 "".to_string()
1062 },
1063 if let Some(value) = before {
1064 format!("&before={}", value)
1065 } else {
1066 "".to_string()
1067 }
1068 ))
1069 .await?
1070 .data)
1071 }
1072}