twitch_gql_rs/
lib.rs

1//! # TWITCH-GQL-RS
2//!
3//! [![crates.io](https://img.shields.io/crates/v/twitch-gql-rs.svg)](https://crates.io/crates/twitch-gql-rs)
4//! [![Documentation](https://docs.rs/twitch-gql-rs/badge.svg)](https://docs.rs/twitch-gql-rs/0.1.6/twitch_gql_rs)
5//! [![github.com](https://img.shields.io/github/stars/this-is-really/twitch-gql-rs.svg?style=social&label=Star)](https://github.com/this-is-really/twitch-gql-rs)
6//! 
7//! A small, lightweight implementation of a *GraphQL* client for interacting with **Twitch's GraphQL API**.
8//! Designed for simple queries, typed responses, and easy integration into async Rust applications.
9//!
10//! ## Example
11//!
12//! ```rust
13//! use std::{error::Error, path::Path};
14//! use twitch_gql_rs::{client_type::ClientType, TwitchClient};
15//!
16//! #[tokio::main]
17//! async fn main() -> Result<(), Box<dyn Error>> {
18//!     let path = Path::new("save.json");
19//!
20//!     if !path.exists() {
21//!         let client_type = ClientType::android_app();
22//!         let mut client = TwitchClient::new(&client_type).await?;
23//!         let get_auth = client.request_device_auth().await?;
24//!         println!("Please open the following link in your browser:\n{}\nThen enter this code: {}", get_auth.verification_uri, get_auth.user_code);
25//!         client.auth(get_auth).await?;
26//!         client.save_file(&path).await?;
27//!     }
28//!
29//!     let client = TwitchClient::load_from_file(&path).await?;
30//!     let inventory = client.get_inventory().await?;
31//!     for in_progress in inventory.inventory.dropCampaignsInProgress {
32//!         for time_based in in_progress.timeBasedDrops {
33//!             if let Some(id) = time_based.self_drop.dropInstanceID {
34//!                 println!("{id}");
35//!             }
36//!         }
37//!     }
38//!
39//!     Ok(())
40//! }
41//! ```
42
43
44use std::{error::Error, path::Path};
45
46use reqwest::{header::{HeaderMap, HeaderValue, ACCEPT, ACCEPT_LANGUAGE, CACHE_CONTROL, ORIGIN, PRAGMA, REFERER, USER_AGENT}, Client, ClientBuilder};
47use serde::{Deserialize, Serialize};
48use tokio::{fs};
49pub mod error;
50use error::*;
51mod gql;
52mod api;
53use gql::*;
54use api::*;
55
56use crate::{client_type::ClientType, structs::{AvailableDrops, CampaignDetails, ClaimDrop, CurrentDrop, Drops, GameDirectory, GetInventory, PlaybackAccessToken, StreamInfo}};
57/// All data structures used in the project
58pub mod structs;
59/// Client types
60pub mod client_type;
61
62/// Represents a Twitch GraphQL client used to interact with Twitch's API.
63#[derive(Deserialize, Serialize, Debug, Default, Clone)]
64pub struct TwitchClient {
65    #[serde(skip)]
66    client: Client,
67    client_id: String,
68    user_agent: String,
69    client_url: String,
70    pub user_id: Option<String>,
71    pub login: Option<String>,
72    pub access_token: Option<String>,
73}
74
75async fn get_headers(
76    client_id: &str,
77    user_agent: &str,
78    access_token: Option<&str>,
79) -> Result<HeaderMap, Box<dyn Error>> {
80    let device_id = uuid::Uuid::new_v4();
81    let mut headers = HeaderMap::new();
82
83    headers.insert(ACCEPT, HeaderValue::from_str("application/json")?);
84    headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_str("en-US")?);
85    headers.insert(CACHE_CONTROL, HeaderValue::from_str("no-cache")?);
86    headers.insert("Client-Id", HeaderValue::from_str(&format!("{}", client_id))?);
87    headers.insert(PRAGMA, HeaderValue::from_str("no-cache")?);
88    headers.insert(ORIGIN, HeaderValue::from_str("https://www.twitch.tv")?);
89    headers.insert(REFERER, HeaderValue::from_str("https://www.twitch.tv")?);
90    headers.insert(USER_AGENT, HeaderValue::from_str(&format!("{}", user_agent))?);
91    headers.insert("X-Device-Id", HeaderValue::from_str(&format!("{}", device_id))?);
92
93    if let Some(token) = access_token {
94        headers.insert("Authorization", HeaderValue::from_str(&format!("OAuth {}", token))?);
95    }
96
97    Ok(headers)
98}
99
100impl TwitchClient {
101    /// Saves the current state of the structure to a JSON file at the specified path.
102    /// Returns an error if the file already exists or if serialization fails.
103    pub async fn save_file(self, path: &Path) -> Result<Self, SystemError> {
104        if !path.exists() {
105            let info = match serde_json::to_string_pretty(&self) {
106                Ok(s) => s,
107                Err(e) => return Err(SystemError::SerializationProblem(e)),
108            };
109            fs::write(&path, info.as_bytes()).await.unwrap();
110            Ok(self)
111        } else {
112            Err(SystemError::FileAlredyExists)
113        }
114    }
115
116    /// Loads the structure from a JSON file at the specified path.
117    /// Returns an error if the file is not found or if deserialization fails.
118    pub async fn load_from_file(path: &Path) -> Result<Self, SystemError> {
119        if !path.exists() {
120            return Err(SystemError::FileNotFound);
121        }
122
123        let load = fs::read_to_string(&path).await.unwrap();
124        let mut load: TwitchClient = match serde_json::from_str(&load) {
125            Ok(s) => s,
126            Err(e) => return Err(SystemError::DeserializationProblem(e)),
127        };
128
129        let headers = get_headers(&load.client_id, &load.user_agent, load.access_token.as_deref()).await?;
130        let client = ClientBuilder::new().default_headers(headers).build()?;
131        load.client = client;
132
133        Ok(load)
134    }
135
136    /// Creates a new `TwitchClient` instance without an access token.
137    pub async fn new(client_type: &ClientType) -> Result<Self, SystemError> {
138        let headers = get_headers(&client_type.client_id, &client_type.user_agent, None).await?;
139        let client = ClientBuilder::new().default_headers(headers).build()?;
140
141        Ok(TwitchClient {
142            client,
143            client_id: client_type.client_id.to_string(),
144            user_agent: client_type.user_agent.to_string(),
145            client_url: client_type.client_url.to_string(),
146            user_id: None,
147            login: None,
148            access_token: None,
149        })
150    }
151
152    // API
153    /// Requests Device Flow from Twitch and returns a `DeviceAuth` structure.
154    pub async fn request_device_auth(&self) -> Result<DeviceAuth, TwitchError> {
155        let auth = request_device_auth(&self.client, &self.client_id).await?;
156        Ok(auth)
157    }
158
159    /// Authenticates the `TwitchClient`.
160    /// Starts a token polling cycle via Device Flow using the passed `DeviceAuth`.
161    pub async fn auth (&mut self, device_auth: DeviceAuth) -> Result<(), TwitchError> {
162        let auth = poll_device_auth(&self.client, &self.client_id, device_auth).await?;
163        self.access_token = Some(auth.0);
164        self.user_id = Some(auth.1);
165        self.login = Some(auth.2);
166        Ok(())
167    } 
168
169    /// Sends a "watch" event for a given channel.
170    pub async fn send_watch(&self, channel_login: &str, broadcast_id: &str, channel_id: &str) -> Result<(), TwitchError> {
171        if let Some(user_id) = &self.user_id {
172            send_watch(&self.client, &user_id, &self.client_url, channel_login, broadcast_id, channel_id).await?;
173        } else {
174            return Err(TwitchError::TwitchError("Not found user_id".into()));
175        }
176
177        Ok(())
178    }
179
180    //GQL
181
182    /// Retrieves the user's inventory from Twitch.
183    pub async fn get_inventory (&self) -> Result<GetInventory, TwitchError> {
184        let inv = inventory(&self.client).await?;
185        Ok(inv)
186    }
187
188    /// Returns current information about Twitch Drops campaigns.
189    pub async fn get_campaign (&self) -> Result<Drops, TwitchError> {
190        let drops = campaign(&self.client).await?;
191        Ok(drops)
192    }
193
194    /// Retrieves the slug for a given game name.
195    pub async fn get_slug (&self, game_name: &str) -> Result<String, TwitchError> {
196        let slug = slug_redirect(&self.client, game_name).await?;
197        Ok(slug)
198    }
199
200    /// Retrieves the playback access token for a given Twitch channel.
201    pub async fn get_playback_access_token (&self, channel_login: &str) -> Result<PlaybackAccessToken, TwitchError> {
202        let playback = playback_access_token(&self.client, channel_login).await?;
203        Ok(playback)
204    }
205
206    /// Retrieves a list of Twitch streams for a specific game, optionally filtering by drops-enabled streams
207    pub async fn get_game_directory(&self, game_slug: &str, limit: u64, drops_enabled: bool) -> Result<Vec<GameDirectory>, TwitchError> {
208        let streams = game_directory(&self.client, game_slug, limit, drops_enabled).await?;
209        Ok(streams)
210    }
211
212    /// Returns a list of available Twitch Drops and their progress for a given channel.
213    pub async fn get_available_drops_for_channel (&self, channel_id: &str) -> Result<AvailableDrops, TwitchError> {
214        let drops = available_drops(&self.client, channel_id).await?;
215        Ok(drops)
216    }
217
218    /// Retrieves detailed information about a specific Twitch Drops campaign for a user
219    pub async fn get_campaign_details (&self, drop_id: &str) -> Result<CampaignDetails, TwitchError> {
220        if let Some(login) = &self.login {
221            let details = campaign_details(&self.client, &login, drop_id).await?;
222            return Ok(details)
223        } else {
224            return Err(TwitchError::TwitchError("Not found login".into()));
225        }
226    }
227
228    /// Retrieves the current drop progress for a user on a specific Twitch channel.
229    pub async fn get_current_drop_progress_on_channel (&self, channel_login: &str, channel_id: &str) -> Result<CurrentDrop, TwitchError> {
230        let current = current_drop(&self.client, channel_login, channel_id).await?;
231        Ok(current)
232    }
233
234    /// Retrieves the current stream information for a given Twitch channel.
235    pub async fn get_stream_info (&self, channel_login: &str) -> Result<StreamInfo, TwitchError> {
236        let stream_info = stream_info(&self.client, channel_login).await?;
237        Ok(stream_info)
238    }
239
240    /// Claims a Twitch drop for the given drop instance ID
241    pub async fn claim_drop (&self, drop_instance_id: &str) -> Result<ClaimDrop, TwitchError> {
242        let claim = claim_drop(&self.client, drop_instance_id).await?;
243        Ok(claim)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[tokio::test]
252    async fn test() -> Result<(), Box<dyn Error>> {
253        Ok(())
254    }
255}