Skip to main content

kick_api/api/
rewards.rs

1use crate::error::{KickApiError, Result};
2use crate::models::{
3    ChannelReward, ChannelRewardRedemption, CreateRewardRequest, ManageRedemptionsRequest,
4    ManageRedemptionsResponse, RedemptionStatus, UpdateRewardRequest,
5};
6use reqwest;
7
8/// Rewards API - handles all channel reward endpoints
9pub struct RewardsApi<'a> {
10    client: &'a reqwest::Client,
11    token: &'a Option<String>,
12    base_url: &'a str,
13}
14
15impl<'a> RewardsApi<'a> {
16    /// Create a new RewardsApi instance
17    pub(crate) fn new(
18        client: &'a reqwest::Client,
19        token: &'a Option<String>,
20        base_url: &'a str,
21    ) -> Self {
22        Self {
23            client,
24            token,
25            base_url,
26        }
27    }
28
29    /// Get all channel rewards
30    ///
31    /// Requires OAuth token with `channel:rewards:read` scope
32    ///
33    /// # Example
34    /// ```no_run
35    /// let rewards = client.rewards().get_all().await?;
36    /// for reward in rewards {
37    ///     println!("Reward: {} - {} points", reward.title, reward.cost);
38    /// }
39    /// ```
40    pub async fn get_all(&self) -> Result<Vec<ChannelReward>> {
41        super::require_token(self.token)?;
42
43        let url = format!("{}/channels/rewards", self.base_url);
44        let request = self
45            .client
46            .get(&url)
47            .header("Accept", "*/*")
48            .bearer_auth(self.token.as_ref().unwrap());
49        let response = crate::http::send_with_retry(self.client, request).await?;
50
51        self.parse_response(response).await
52    }
53
54    /// Create a new channel reward
55    ///
56    /// Requires OAuth token with `channel:rewards:write` scope
57    ///
58    /// # Example
59    /// ```no_run
60    /// use kick_api::CreateRewardRequest;
61    ///
62    /// let request = CreateRewardRequest {
63    ///     title: "Song Request".to_string(),
64    ///     cost: 500,
65    ///     description: Some("Request a song!".to_string()),
66    ///     is_user_input_required: Some(true),
67    ///     ..Default::default()
68    /// };
69    ///
70    /// let reward = client.rewards().create(request).await?;
71    /// ```
72    pub async fn create(&self, request: CreateRewardRequest) -> Result<ChannelReward> {
73        super::require_token(self.token)?;
74
75        let url = format!("{}/channels/rewards", self.base_url);
76        let request = self
77            .client
78            .post(&url)
79            .header("Accept", "*/*")
80            .bearer_auth(self.token.as_ref().unwrap())
81            .json(&request);
82        let response = crate::http::send_with_retry(self.client, request).await?;
83
84        self.parse_single_response(response).await
85    }
86
87    /// Update an existing reward
88    ///
89    /// Requires OAuth token with `channel:rewards:write` scope
90    ///
91    /// # Example
92    /// ```no_run
93    /// use kick_api::UpdateRewardRequest;
94    ///
95    /// let update = UpdateRewardRequest {
96    ///     cost: Some(1000),
97    ///     is_paused: Some(true),
98    ///     ..Default::default()
99    /// };
100    ///
101    /// let reward = client.rewards().update("reward_id", update).await?;
102    /// ```
103    pub async fn update(
104        &self,
105        reward_id: &str,
106        request: UpdateRewardRequest,
107    ) -> Result<ChannelReward> {
108        super::require_token(self.token)?;
109
110        let url = format!("{}/channels/rewards/{}", self.base_url, reward_id);
111        let request = self
112            .client
113            .patch(&url)
114            .header("Accept", "*/*")
115            .bearer_auth(self.token.as_ref().unwrap())
116            .json(&request);
117        let response = crate::http::send_with_retry(self.client, request).await?;
118
119        self.parse_single_response(response).await
120    }
121
122    /// Delete a reward
123    ///
124    /// Requires OAuth token with `channel:rewards:write` scope
125    pub async fn delete(&self, reward_id: &str) -> Result<()> {
126        super::require_token(self.token)?;
127
128        let url = format!("{}/channels/rewards/{}", self.base_url, reward_id);
129        let request = self
130            .client
131            .delete(&url)
132            .header("Accept", "*/*")
133            .bearer_auth(self.token.as_ref().unwrap());
134        let response = crate::http::send_with_retry(self.client, request).await?;
135
136        if response.status().is_success() {
137            Ok(())
138        } else {
139            Err(KickApiError::ApiError(format!(
140                "Failed to delete reward: {}",
141                response.status()
142            )))
143        }
144    }
145
146    /// Get reward redemptions
147    ///
148    /// Requires OAuth token with `channel:rewards:read` scope
149    ///
150    /// # Parameters
151    /// - `reward_id`: Optional - filter by specific reward
152    /// - `status`: Optional - filter by status (defaults to pending)
153    pub async fn get_redemptions(
154        &self,
155        reward_id: Option<&str>,
156        status: Option<RedemptionStatus>,
157    ) -> Result<Vec<ChannelRewardRedemption>> {
158        super::require_token(self.token)?;
159
160        let url = format!("{}/channels/rewards/redemptions", self.base_url);
161        let mut request = self
162            .client
163            .get(&url)
164            .header("Accept", "*/*")
165            .bearer_auth(self.token.as_ref().unwrap());
166
167        if let Some(id) = reward_id {
168            request = request.query(&[("reward_id", id)]);
169        }
170
171        if let Some(s) = status {
172            let status_str = match s {
173                RedemptionStatus::Pending => "pending",
174                RedemptionStatus::Accepted => "accepted",
175                RedemptionStatus::Rejected => "rejected",
176            };
177            request = request.query(&[("status", status_str)]);
178        }
179
180        let response = crate::http::send_with_retry(self.client, request).await?;
181        self.parse_response(response).await
182    }
183
184    /// Accept pending redemptions
185    ///
186    /// Requires OAuth token with `channel:rewards:write` scope
187    ///
188    /// # Parameters
189    /// - `redemption_ids`: List of redemption IDs to accept (1-25)
190    pub async fn accept_redemptions(
191        &self,
192        redemption_ids: Vec<String>,
193    ) -> Result<ManageRedemptionsResponse> {
194        self.manage_redemptions("accept", redemption_ids).await
195    }
196
197    /// Reject pending redemptions
198    ///
199    /// Requires OAuth token with `channel:rewards:write` scope
200    ///
201    /// # Parameters
202    /// - `redemption_ids`: List of redemption IDs to reject (1-25)
203    pub async fn reject_redemptions(
204        &self,
205        redemption_ids: Vec<String>,
206    ) -> Result<ManageRedemptionsResponse> {
207        self.manage_redemptions("reject", redemption_ids).await
208    }
209
210    // Helper methods
211
212    async fn parse_response<T: serde::de::DeserializeOwned>(
213        &self,
214        response: reqwest::Response,
215    ) -> Result<Vec<T>> {
216        if response.status().is_success() {
217            let body = response.text().await?;
218
219            #[derive(serde::Deserialize)]
220            struct DataResponse<T> {
221                data: Vec<T>,
222            }
223
224            let resp: DataResponse<T> = serde_json::from_str(&body)
225                .map_err(|e| KickApiError::ApiError(format!("JSON parse error: {}", e)))?;
226
227            Ok(resp.data)
228        } else {
229            Err(KickApiError::ApiError(format!(
230                "Request failed: {}",
231                response.status()
232            )))
233        }
234    }
235
236    async fn parse_single_response<T: serde::de::DeserializeOwned>(
237        &self,
238        response: reqwest::Response,
239    ) -> Result<T> {
240        if response.status().is_success() {
241            let body = response.text().await?;
242
243            #[derive(serde::Deserialize)]
244            struct DataResponse<T> {
245                data: T,
246            }
247
248            let resp: DataResponse<T> = serde_json::from_str(&body)
249                .map_err(|e| KickApiError::ApiError(format!("JSON parse error: {}", e)))?;
250
251            Ok(resp.data)
252        } else {
253            Err(KickApiError::ApiError(format!(
254                "Request failed: {}",
255                response.status()
256            )))
257        }
258    }
259
260    async fn manage_redemptions(
261        &self,
262        action: &str,
263        redemption_ids: Vec<String>,
264    ) -> Result<ManageRedemptionsResponse> {
265        super::require_token(self.token)?;
266
267        let url = format!("{}/channels/rewards/redemptions/{}", self.base_url, action);
268        let request_body = ManageRedemptionsRequest { ids: redemption_ids };
269
270        let request = self
271            .client
272            .post(&url)
273            .header("Accept", "*/*")
274            .bearer_auth(self.token.as_ref().unwrap())
275            .json(&request_body);
276        let response = crate::http::send_with_retry(self.client, request).await?;
277
278        if response.status().is_success() {
279            let body = response.text().await?;
280            let resp: ManageRedemptionsResponse = serde_json::from_str(&body)
281                .map_err(|e| KickApiError::ApiError(format!("JSON parse error: {}", e)))?;
282            Ok(resp)
283        } else {
284            Err(KickApiError::ApiError(format!(
285                "Failed to {} redemptions: {}",
286                action,
287                response.status()
288            )))
289        }
290    }
291}