mirror of
https://github.com/kurbezz/discord-bot.git
synced 2025-12-06 15:15:37 +01:00
1076 lines
32 KiB
Rust
1076 lines
32 KiB
Rust
use auth::{Token, TokenStorage, TokenType};
|
|
|
|
use anyhow::bail;
|
|
use anyhow::Result;
|
|
|
|
use reqwest::Client as HttpClient;
|
|
use reqwest::{Method, Response};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use super::auth;
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct TwitchData<T> {
|
|
pub data: Vec<T>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Pagination {
|
|
pub cursor: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Client<T: TokenStorage> {
|
|
pub client_id: String,
|
|
pub client_secret: String,
|
|
pub token: Token,
|
|
pub http_client: HttpClient,
|
|
pub token_storage: T,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct User {
|
|
pub id: String,
|
|
pub login: String,
|
|
pub display_name: String,
|
|
pub r#type: String,
|
|
pub broadcaster_type: String,
|
|
pub description: String,
|
|
pub profile_image_url: String,
|
|
pub offline_image_url: String,
|
|
pub view_count: i64,
|
|
pub email: Option<String>,
|
|
pub created_at: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RewardImage {
|
|
pub url_1x: String,
|
|
pub url_2x: String,
|
|
pub url_4x: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RewardMaxPerStream {
|
|
pub is_enabled: bool,
|
|
pub max_per_stream: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RewardMaxPerUserPerStream {
|
|
pub is_enabled: bool,
|
|
pub max_per_user_per_stream: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RewardGlobalCooldown {
|
|
pub is_enabled: bool,
|
|
pub global_cooldown_seconds: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Reward {
|
|
pub broadcaster_id: String,
|
|
pub broadcaster_login: String,
|
|
pub broadcaster_name: String,
|
|
pub id: String,
|
|
pub title: String,
|
|
pub prompt: String,
|
|
pub cost: i64,
|
|
pub image: Option<RewardImage>,
|
|
pub default_image: RewardImage,
|
|
pub background_color: String,
|
|
pub is_enabled: bool,
|
|
pub is_user_input_required: bool,
|
|
pub max_per_stream_setting: RewardMaxPerStream,
|
|
pub max_per_user_per_stream_setting: RewardMaxPerUserPerStream,
|
|
pub global_cooldown_setting: RewardGlobalCooldown,
|
|
pub is_paused: bool,
|
|
pub is_in_stock: bool,
|
|
pub should_redemptions_skip_request_queue: bool,
|
|
pub redemptions_redeemed_current_stream: Option<i64>,
|
|
pub cooldown_expires_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct RewardCreate {
|
|
pub title: String,
|
|
pub cost: i64,
|
|
pub prompt: Option<String>,
|
|
pub is_enabled: Option<bool>,
|
|
pub background_color: Option<String>,
|
|
pub is_user_input_required: Option<bool>,
|
|
pub is_max_per_stream_enabled: Option<bool>,
|
|
pub max_per_stream: Option<i64>,
|
|
pub is_max_per_user_per_stream_enabled: Option<bool>,
|
|
pub max_per_user_per_stream: Option<i64>,
|
|
pub is_global_cooldown_enabled: Option<bool>,
|
|
pub global_cooldown_seconds: Option<i64>,
|
|
pub should_redemptions_skip_request_queue: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct RewardUpdate {
|
|
pub title: Option<String>,
|
|
pub cost: Option<i64>,
|
|
pub prompt: Option<String>,
|
|
pub is_enabled: Option<bool>,
|
|
pub background_color: Option<String>,
|
|
pub is_user_input_required: Option<bool>,
|
|
pub is_max_per_stream_enabled: Option<bool>,
|
|
pub max_per_stream: Option<i64>,
|
|
pub is_max_per_user_per_stream_enabled: Option<bool>,
|
|
pub max_per_user_per_stream: Option<i64>,
|
|
pub is_global_cooldown_enabled: Option<bool>,
|
|
pub global_cooldown_seconds: Option<i64>,
|
|
pub is_paused: Option<bool>,
|
|
pub should_redemptions_skip_request_queue: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct RedemptionStatus {
|
|
pub status: String,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct EventSubTransport {
|
|
pub method: String,
|
|
pub callback: Option<String>,
|
|
pub secret: Option<String>,
|
|
pub session_id: Option<String>,
|
|
pub connected_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct EventSubCondition {
|
|
pub broadcaster_id: Option<String>,
|
|
pub broadcaster_user_id: Option<String>,
|
|
pub moderator_user_id: Option<String>,
|
|
pub user_id: Option<String>,
|
|
pub from_broadcaster_user_id: Option<String>,
|
|
pub to_broadcaster_user_id: Option<String>,
|
|
pub reward_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct EventSub {
|
|
pub id: String,
|
|
pub status: String,
|
|
pub r#type: String,
|
|
pub version: String,
|
|
pub condition: EventSubCondition,
|
|
pub created_at: String,
|
|
pub transport: EventSubTransport,
|
|
pub cost: i64,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct EventSubCreate {
|
|
pub r#type: String,
|
|
pub version: String,
|
|
pub condition: EventSubCondition,
|
|
pub transport: EventSubTransport,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct BanUser {
|
|
pub user_id: String,
|
|
pub duration: i64,
|
|
pub reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct BanUserObj {
|
|
pub data: BanUser,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
pub struct BannedUser {
|
|
pub broadcaster_id: String,
|
|
pub moderator_id: String,
|
|
pub user_id: String,
|
|
pub created_at: String,
|
|
pub end_time: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ChannelInformation {
|
|
pub broadcaster_id: String,
|
|
pub broadcaster_login: String,
|
|
pub broadcaster_name: String,
|
|
pub broadcaster_language: String,
|
|
pub game_name: String,
|
|
pub game_id: String,
|
|
pub title: String,
|
|
pub delay: i64,
|
|
pub tags: Vec<String>,
|
|
pub content_classification_labels: Vec<String>,
|
|
pub is_branded_content: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PredictionTopPredictor {
|
|
pub user_id: String,
|
|
pub user_name: String,
|
|
pub user_login: String,
|
|
pub channel_points_used: i64,
|
|
pub channel_points_won: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PredictionOutcome {
|
|
pub id: String,
|
|
pub title: String,
|
|
pub users: i64,
|
|
pub channel_points: i64,
|
|
pub top_predictors: Option<Vec<PredictionTopPredictor>>,
|
|
pub color: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Prediction {
|
|
pub id: String,
|
|
pub broadcaster_id: String,
|
|
pub broadcaster_name: String,
|
|
pub broadcaster_login: String,
|
|
pub title: String,
|
|
pub winning_outcome_id: Option<String>,
|
|
pub outcomes: Vec<PredictionOutcome>,
|
|
pub prediction_window: i64,
|
|
pub status: String,
|
|
pub created_at: String,
|
|
pub ended_at: Option<String>,
|
|
pub locked_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PredictionOutcomeCreate {
|
|
pub title: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PredictionCreate {
|
|
pub broadcaster_id: String,
|
|
pub title: String,
|
|
pub outcomes: Vec<PredictionOutcomeCreate>,
|
|
pub prediction_window: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PredictionEnd {
|
|
pub broadcaster_id: String,
|
|
pub id: String,
|
|
pub status: String,
|
|
pub winning_outcome_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CommercialStart {
|
|
pub broadcaster_id: String,
|
|
pub length: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Commercial {
|
|
pub length: i64,
|
|
pub message: String,
|
|
pub retry_after: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Announcement {
|
|
pub message: String,
|
|
pub color: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Whisper {
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Stream {
|
|
pub id: String,
|
|
pub user_id: String,
|
|
pub user_login: String,
|
|
pub user_name: String,
|
|
pub game_id: String,
|
|
pub game_name: String,
|
|
pub r#type: String,
|
|
pub title: String,
|
|
pub tags: Vec<String>,
|
|
pub viewer_count: i64,
|
|
pub started_at: String,
|
|
pub language: String,
|
|
pub thumbnail_url: String,
|
|
pub is_mature: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ChannelFollowers {
|
|
pub followed_at: String,
|
|
pub user_id: String,
|
|
pub user_login: String,
|
|
pub user_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ChannelFollowersData {
|
|
pub data: Vec<ChannelFollowers>,
|
|
pub pagination: Pagination,
|
|
pub total: i64,
|
|
}
|
|
|
|
pub enum VideoId {
|
|
Id(String),
|
|
UserId(String),
|
|
GameId(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VideoMutedSegment {
|
|
pub duration: i64,
|
|
pub offset: i64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Video {
|
|
pub id: String,
|
|
pub stream_id: String,
|
|
pub user_id: String,
|
|
pub user_login: String,
|
|
pub user_name: String,
|
|
pub title: String,
|
|
pub description: String,
|
|
pub created_at: String,
|
|
pub published_at: String,
|
|
pub url: String,
|
|
pub thumbnail_url: String,
|
|
pub viewable: String,
|
|
pub view_count: i64,
|
|
pub language: String,
|
|
pub r#type: String,
|
|
pub duration: String,
|
|
pub muted_segments: Option<Vec<VideoMutedSegment>>,
|
|
}
|
|
|
|
impl<T: TokenStorage> Client<T> {
|
|
pub async fn http_request<T2: serde::Serialize>(
|
|
&mut self,
|
|
method: Method,
|
|
uri: String,
|
|
data_json: Option<T2>,
|
|
data_form: Option<String>,
|
|
) -> Result<Response> {
|
|
let mut req = self.http_client.request(method, uri);
|
|
|
|
req = match data_json {
|
|
Some(data_json) => req.json(&data_json),
|
|
None => match data_form {
|
|
Some(data_form) => req.body(data_form),
|
|
None => req,
|
|
},
|
|
};
|
|
|
|
let req = req
|
|
.timeout(core::time::Duration::from_secs(5))
|
|
.header(
|
|
"Authorization",
|
|
format!("Bearer {0}", self.token.access_token),
|
|
)
|
|
.header("Client-Id", self.client_id.clone());
|
|
|
|
Ok(req.send().await?)
|
|
}
|
|
|
|
pub async fn request<T1: serde::Serialize + std::clone::Clone>(
|
|
&mut self,
|
|
method: Method,
|
|
uri: String,
|
|
data_json: Option<T1>,
|
|
data_form: Option<String>,
|
|
) -> Result<Response> {
|
|
let mut res = self
|
|
.http_request(
|
|
method.clone(),
|
|
uri.clone(),
|
|
data_json.clone(),
|
|
data_form.clone(),
|
|
)
|
|
.await?;
|
|
|
|
if res.status() == reqwest::StatusCode::UNAUTHORIZED {
|
|
//Token invalid, get new? If fail, or fail again, return error.
|
|
self.refresh_token().await?;
|
|
res = self.http_request(method, uri, data_json, data_form).await?;
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
pub async fn request_result<
|
|
T1: for<'de> serde::Deserialize<'de>,
|
|
T2: serde::Serialize + std::clone::Clone,
|
|
>(
|
|
&mut self,
|
|
method: Method,
|
|
uri: String,
|
|
data_json: Option<T2>,
|
|
data_form: Option<String>,
|
|
) -> Result<T1> {
|
|
let res = self
|
|
.request::<T2>(method, uri, data_json, data_form)
|
|
.await?;
|
|
|
|
Ok(res.json::<T1>().await?)
|
|
}
|
|
|
|
pub async fn get<T1: for<'de> serde::Deserialize<'de>>(&mut self, uri: String) -> Result<T1> {
|
|
return self
|
|
.request_result::<T1, String>(Method::GET, uri, None, None)
|
|
.await;
|
|
}
|
|
|
|
pub async fn post_empty(&mut self, uri: String) -> Result<()> {
|
|
match self.request::<String>(Method::POST, uri, None, None).await {
|
|
Ok(..) => Ok(()),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
pub async fn post_form<T1: for<'de> serde::Deserialize<'de>>(
|
|
&mut self,
|
|
uri: String,
|
|
data: String,
|
|
) -> Result<T1> {
|
|
return self
|
|
.request_result::<T1, String>(Method::POST, uri, None, Some(data))
|
|
.await;
|
|
}
|
|
|
|
pub async fn post_json<
|
|
T1: for<'de> serde::Deserialize<'de>,
|
|
T2: serde::Serialize + std::clone::Clone,
|
|
>(
|
|
&mut self,
|
|
uri: String,
|
|
data: T2,
|
|
) -> Result<T1> {
|
|
return self
|
|
.request_result::<T1, T2>(Method::POST, uri, Some(data), None)
|
|
.await;
|
|
}
|
|
|
|
pub async fn post_json_empty<T1: serde::Serialize + std::clone::Clone>(
|
|
&mut self,
|
|
uri: String,
|
|
data: T1,
|
|
) -> Result<()> {
|
|
match self
|
|
.request::<T1>(Method::POST, uri, Some(data), None)
|
|
.await
|
|
{
|
|
Ok(..) => Ok(()),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
pub async fn patch_json<
|
|
T1: for<'de> serde::Deserialize<'de>,
|
|
T2: serde::Serialize + std::clone::Clone,
|
|
>(
|
|
&mut self,
|
|
uri: String,
|
|
data: T2,
|
|
) -> Result<T1> {
|
|
return self
|
|
.request_result::<T1, T2>(Method::PATCH, uri, Some(data), None)
|
|
.await;
|
|
}
|
|
|
|
pub async fn delete(&mut self, uri: String) -> Result<()> {
|
|
self.request::<String>(Method::DELETE, uri, None, None)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_token_user(&mut self) -> Result<User> {
|
|
match &self.token.user {
|
|
Some(v) => Ok(v.clone()),
|
|
None => {
|
|
if self.token.token_type == TokenType::UserAccessToken && self.token.user.is_none()
|
|
{
|
|
let user = self.get_user().await?;
|
|
self.token.user = Some(user.clone());
|
|
return Ok(user);
|
|
}
|
|
|
|
bail!("No User");
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_token_user_id(&mut self) -> Result<String> {
|
|
match &self.token.user {
|
|
Some(v) => Ok(v.id.clone()),
|
|
None => {
|
|
let user = self.get_token_user().await?;
|
|
return Ok(user.id.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_token_user_login(&mut self) -> Result<String> {
|
|
match &self.token.user {
|
|
Some(v) => Ok(v.id.clone()),
|
|
None => {
|
|
let user = self.get_token_user().await?;
|
|
return Ok(user.login.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_users_by_ids(&mut self, user_ids: Vec<String>) -> Result<Vec<User>> {
|
|
Ok(self
|
|
.get::<TwitchData<User>>(format!(
|
|
"https://api.twitch.tv/helix/users?id={0}",
|
|
user_ids.join("&id=")
|
|
))
|
|
.await?
|
|
.data)
|
|
}
|
|
|
|
pub async fn get_users_by_logins(&mut self, user_logins: Vec<String>) -> Result<Vec<User>> {
|
|
Ok(self
|
|
.get::<TwitchData<User>>(format!(
|
|
"https://api.twitch.tv/helix/users?login={0}",
|
|
user_logins.join("&login=")
|
|
))
|
|
.await?
|
|
.data)
|
|
}
|
|
|
|
pub async fn get_user_by_id(&mut self, user_id: String) -> Result<User> {
|
|
match self.get_users_by_ids(vec![user_id]).await?.first() {
|
|
Some(user) => Ok(user.clone()),
|
|
None => bail!("No User found"),
|
|
}
|
|
}
|
|
|
|
pub async fn get_user_by_login(&mut self, user_login: String) -> Result<User> {
|
|
match self.get_users_by_logins(vec![user_login]).await?.first() {
|
|
Some(user) => Ok(user.clone()),
|
|
None => bail!("No User found"),
|
|
}
|
|
}
|
|
|
|
pub async fn get_user(&mut self) -> Result<User> {
|
|
match self
|
|
.get::<TwitchData<User>>("https://api.twitch.tv/helix/users".to_string())
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(user) => Ok(user.clone()),
|
|
None => bail!("No User found"),
|
|
}
|
|
}
|
|
|
|
pub async fn create_custom_reward(&mut self, reward: &RewardCreate) -> Result<Reward> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
match self
|
|
.post_json::<TwitchData<Reward>, _>(format!("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}"), reward)
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(reward) => Ok(reward.clone()),
|
|
None => bail!("No User found"),
|
|
}
|
|
}
|
|
|
|
pub async fn update_custom_reward(
|
|
&mut self,
|
|
id: String,
|
|
reward: &RewardUpdate,
|
|
) -> Result<Reward> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
match self
|
|
.patch_json::<TwitchData<Reward>, _>(format!("https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}&id={id}"), reward)
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(reward) => Ok(reward.clone()),
|
|
None => bail!("No User found"),
|
|
}
|
|
}
|
|
|
|
pub async fn get_custom_rewards(&mut self, ids: Vec<String>) -> Result<Vec<Reward>> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.get::<TwitchData<Reward>>(format!(
|
|
"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}{0}",
|
|
if ids.len() > 0 { format!("&id={0}", ids.join("&id=") ) } else { "".to_string() }
|
|
))
|
|
.await?
|
|
.data)
|
|
}
|
|
|
|
pub async fn get_custom_reward(&mut self, id: String) -> Result<Reward> {
|
|
match self.get_custom_rewards(vec![id]).await?.first() {
|
|
Some(reward) => Ok(reward.clone()),
|
|
None => bail!("No Reward found"),
|
|
}
|
|
}
|
|
|
|
pub async fn delete_custom_reward(&mut self, id: String) -> Result<()> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.delete(format!(
|
|
"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={broadcaster_id}&id={id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn update_redemptions_status(
|
|
&mut self,
|
|
id: &String,
|
|
redemptions: Vec<String>,
|
|
status: &RedemptionStatus,
|
|
) -> Result<Vec<RedemptionStatus>> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.patch_json::<TwitchData<RedemptionStatus>, _>(format!(
|
|
"https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id={broadcaster_id}&reward_id={id}{0}",
|
|
format!("&id={0}", redemptions.join("&id=") )
|
|
), status)
|
|
.await?
|
|
.data)
|
|
}
|
|
|
|
pub async fn update_redemption_status(
|
|
&mut self,
|
|
id: &String,
|
|
redemption: &String,
|
|
status: &RedemptionStatus,
|
|
) -> Result<RedemptionStatus> {
|
|
match self
|
|
.update_redemptions_status(id, vec![redemption.clone()], status)
|
|
.await?
|
|
.first()
|
|
{
|
|
Some(status) => Ok(status.clone()),
|
|
None => bail!("No Redemption found"),
|
|
}
|
|
}
|
|
|
|
pub async fn create_eventsub_subscription(
|
|
&mut self,
|
|
eventsub: &EventSubCreate,
|
|
) -> Result<EventSub> {
|
|
match self
|
|
.post_json::<TwitchData<EventSub>, _>(
|
|
format!("https://api.twitch.tv/helix/eventsub/subscriptions"),
|
|
eventsub,
|
|
)
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(eventsub) => Ok(eventsub.clone()),
|
|
None => bail!("No EventSub found"),
|
|
}
|
|
}
|
|
|
|
pub async fn delete_eventsub_subscription(&mut self, id: String) -> Result<()> {
|
|
Ok(self
|
|
.delete(format!(
|
|
"https://api.twitch.tv/helix/eventsub/subscriptions?id={id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn add_channel_moderator(&mut self, id: String) -> Result<()> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.post_empty(format!(
|
|
"https://api.twitch.tv/helix/moderation/moderators?broadcaster_id={broadcaster_id}&user_id={id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn remove_channel_moderator(&mut self, id: String) -> Result<()> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.delete(format!(
|
|
"https://api.twitch.tv/helix/moderation/moderators?broadcaster_id={broadcaster_id}&user_id={id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn ban_user(
|
|
&mut self,
|
|
broadcaster_id: String,
|
|
banuser: &BanUser,
|
|
) -> Result<BannedUser> {
|
|
let moderator_id = self.get_token_user_id().await?;
|
|
match self
|
|
.post_json::<TwitchData<BannedUser>, _>(
|
|
format!("https://api.twitch.tv/helix/moderation/bans?moderator_id={moderator_id}&broadcaster_id={broadcaster_id}"),
|
|
BanUserObj {
|
|
data: banuser.clone()
|
|
},
|
|
)
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(banneduser) => Ok(banneduser.clone()),
|
|
None => bail!("Ban User failed"),
|
|
}
|
|
}
|
|
|
|
pub async fn unban_user(&mut self, broadcaster_id: String, user_id: String) -> Result<()> {
|
|
let moderator_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.delete(format!(
|
|
"https://api.twitch.tv/helix/moderation/bans?moderator_id={moderator_id}&broadcaster_id={broadcaster_id}&user_id={user_id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn shoutout(
|
|
&mut self,
|
|
from_broadcaster_id: String,
|
|
to_broadcaster_id: String,
|
|
) -> Result<()> {
|
|
let moderator_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.post_empty(format!(
|
|
"https://api.twitch.tv/helix/chat/shoutouts?from_broadcaster_id={from_broadcaster_id}&to_broadcaster_id={to_broadcaster_id}&moderator_id={moderator_id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn get_channel_information(
|
|
&mut self,
|
|
broadcaster_ids: Vec<String>,
|
|
) -> Result<Vec<ChannelInformation>> {
|
|
Ok(self
|
|
.get::<TwitchData<ChannelInformation>>(format!(
|
|
"https://api.twitch.tv/helix/channels?{0}",
|
|
if broadcaster_ids.len() > 0 {
|
|
format!(
|
|
"broadcaster_id={0}",
|
|
broadcaster_ids.join("&broadcaster_id=")
|
|
)
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
))
|
|
.await?
|
|
.data)
|
|
}
|
|
|
|
pub async fn whisper(&mut self, to_user_id: String, message: String) -> Result<()> {
|
|
let from_user_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.post_json_empty(
|
|
format!("https://api.twitch.tv/helix/whispers?from_user_id={from_user_id}&to_user_id={to_user_id}"),
|
|
Whisper {
|
|
message: message
|
|
},
|
|
)
|
|
.await?)
|
|
}
|
|
|
|
pub async fn get_predictions(
|
|
&mut self,
|
|
id: Option<String>,
|
|
first: Option<String>,
|
|
after: Option<String>,
|
|
) -> Result<Vec<Prediction>> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.get::<TwitchData<Prediction>>(format!(
|
|
"https://api.twitch.tv/helix/predictions?broadcaster_id={broadcaster_id}{0}{1}{2}",
|
|
if let Some(id) = id {
|
|
format!("&id={id}")
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(first) = first {
|
|
format!("&first={first}")
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(after) = after {
|
|
format!("&after={after}")
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
))
|
|
.await?
|
|
.data)
|
|
}
|
|
|
|
pub async fn create_prediction(
|
|
&mut self,
|
|
title: String,
|
|
outcomes: Vec<String>,
|
|
prediction_window: i64,
|
|
) -> Result<Prediction> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
match self
|
|
.post_json::<TwitchData<Prediction>, _>(
|
|
"https://api.twitch.tv/helix/predictions".to_string(),
|
|
PredictionCreate {
|
|
broadcaster_id: broadcaster_id,
|
|
title: title,
|
|
outcomes: outcomes
|
|
.into_iter()
|
|
.map(|o| PredictionOutcomeCreate { title: o })
|
|
.collect(),
|
|
prediction_window: prediction_window,
|
|
},
|
|
)
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(prediction) => Ok(prediction.clone()),
|
|
None => bail!("Create Prediction failed"),
|
|
}
|
|
}
|
|
|
|
pub async fn end_prediction(
|
|
&mut self,
|
|
id: String,
|
|
status: String,
|
|
winning_outcome_id: Option<String>,
|
|
) -> Result<Prediction> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
match self
|
|
.patch_json::<TwitchData<Prediction>, _>(
|
|
"https://api.twitch.tv/helix/predictions".to_string(),
|
|
PredictionEnd {
|
|
broadcaster_id: broadcaster_id,
|
|
id: id,
|
|
status: status,
|
|
winning_outcome_id: winning_outcome_id,
|
|
},
|
|
)
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(prediction) => Ok(prediction.clone()),
|
|
None => bail!("End Prediction failed"),
|
|
}
|
|
}
|
|
|
|
pub async fn send_chat_announcement(
|
|
&mut self,
|
|
broadcaster_id: String,
|
|
message: String,
|
|
color: Option<String>,
|
|
) -> Result<()> {
|
|
let moderator_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.post_json_empty(
|
|
format!("https://api.twitch.tv/helix/chat/announcements?broadcaster_id={broadcaster_id}&moderator_id={moderator_id}"),
|
|
Announcement {
|
|
message: message,
|
|
color: color,
|
|
}
|
|
)
|
|
.await?)
|
|
}
|
|
|
|
pub async fn start_commercial(&mut self, length: i64) -> Result<Commercial> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
match self
|
|
.post_json::<TwitchData<Commercial>, _>(
|
|
"https://api.twitch.tv/helix/channels/commercial".to_string(),
|
|
CommercialStart {
|
|
broadcaster_id: broadcaster_id,
|
|
length: length,
|
|
},
|
|
)
|
|
.await?
|
|
.data
|
|
.first()
|
|
{
|
|
Some(commercial) => Ok(commercial.clone()),
|
|
None => bail!("Start Commercial failed"),
|
|
}
|
|
}
|
|
|
|
pub async fn get_streams(
|
|
&mut self,
|
|
user_ids: Option<Vec<String>>,
|
|
user_logins: Option<Vec<String>>,
|
|
game_ids: Option<Vec<String>>,
|
|
r#type: Option<String>,
|
|
languages: Option<Vec<String>>,
|
|
first: Option<i64>,
|
|
before: Option<String>,
|
|
after: Option<String>,
|
|
) -> Result<Vec<Stream>> {
|
|
Ok(self
|
|
.get::<TwitchData<Stream>>(format!(
|
|
"https://api.twitch.tv/helix/streams?{0}{1}{2}{3}{4}{5}{6}{7}",
|
|
if let Some(user_ids) = user_ids {
|
|
format!("&user_id={}", user_ids.join("&user_id="))
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(user_logins) = user_logins {
|
|
format!("&user_login={}", user_logins.join("&user_login="))
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(game_ids) = game_ids {
|
|
format!("&game_id={}", game_ids.join("&game_id="))
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(type_) = r#type {
|
|
format!("&type={type_}")
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(languages) = languages {
|
|
format!("&language={}", languages.join("&language="))
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(first) = first {
|
|
format!("&first={first}")
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(before) = before {
|
|
format!("&before={before}")
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(after) = after {
|
|
format!("&after={after}")
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
))
|
|
.await?
|
|
.data)
|
|
}
|
|
|
|
pub async fn get_stream(&mut self, broadcaster_id: String) -> Result<Stream> {
|
|
match self
|
|
.get_streams(
|
|
vec![broadcaster_id].into(),
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
.await?
|
|
.first()
|
|
{
|
|
Some(stream) => Ok(stream.clone()),
|
|
None => bail!("No stream found"),
|
|
}
|
|
}
|
|
|
|
pub async fn add_channel_vip(&mut self, id: String) -> Result<()> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.post_empty(format!(
|
|
"https://api.twitch.tv/helix/channels/vips?broadcaster_id={broadcaster_id}&user_id={id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn remove_channel_vip(&mut self, id: String) -> Result<()> {
|
|
let broadcaster_id = self.get_token_user_id().await?;
|
|
Ok(self
|
|
.delete(format!(
|
|
"https://api.twitch.tv/helix/channels/vips?broadcaster_id={broadcaster_id}&user_id={id}"
|
|
))
|
|
.await?)
|
|
}
|
|
|
|
pub async fn get_channel_followers_total(&mut self, broadcaster_id: String) -> Result<i64> {
|
|
Ok(self
|
|
.get::<ChannelFollowersData>(format!(
|
|
"https://api.twitch.tv/helix/channels/followers?broadcaster_id={0}",
|
|
broadcaster_id
|
|
))
|
|
.await?
|
|
.total)
|
|
}
|
|
|
|
pub async fn get_videos(
|
|
&mut self,
|
|
id: VideoId,
|
|
language: Option<String>,
|
|
period: Option<String>,
|
|
sort: Option<String>,
|
|
r#type: Option<String>,
|
|
first: Option<String>,
|
|
after: Option<String>,
|
|
before: Option<String>,
|
|
) -> Result<Vec<Video>> {
|
|
Ok(self
|
|
.get::<TwitchData<Video>>(format!(
|
|
"https://api.twitch.tv/helix/videos?{}{}{}{}{}{}{}{}",
|
|
match id {
|
|
VideoId::Id(value) => format!("id={}", value),
|
|
VideoId::UserId(value) => format!("user_id={}", value),
|
|
VideoId::GameId(value) => format!("game_id={}", value),
|
|
},
|
|
if let Some(value) = language {
|
|
format!("&language={}", value)
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(value) = period {
|
|
format!("&period={}", value)
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(value) = sort {
|
|
format!("&sort={}", value)
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(value) = r#type {
|
|
format!("&type={}", value)
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(value) = first {
|
|
format!("&first={}", value)
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(value) = after {
|
|
format!("&after={}", value)
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if let Some(value) = before {
|
|
format!("&before={}", value)
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
))
|
|
.await?
|
|
.data)
|
|
}
|
|
}
|