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        self.require_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        self.require_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        self.require_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        self.require_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        self.require_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    fn require_token(&self) -> Result<()> {
213        if self.token.is_none() {
214            return Err(KickApiError::ApiError(
215                "OAuth token required for this endpoint".to_string(),
216            ));
217        }
218        Ok(())
219    }
220
221    async fn parse_response<T: serde::de::DeserializeOwned>(
222        &self,
223        response: reqwest::Response,
224    ) -> Result<Vec<T>> {
225        if response.status().is_success() {
226            let body = response.text().await?;
227
228            #[derive(serde::Deserialize)]
229            struct DataResponse<T> {
230                data: Vec<T>,
231            }
232
233            let resp: DataResponse<T> = serde_json::from_str(&body)
234                .map_err(|e| KickApiError::ApiError(format!("JSON parse error: {}", e)))?;
235
236            Ok(resp.data)
237        } else {
238            Err(KickApiError::ApiError(format!(
239                "Request failed: {}",
240                response.status()
241            )))
242        }
243    }
244
245    async fn parse_single_response<T: serde::de::DeserializeOwned>(
246        &self,
247        response: reqwest::Response,
248    ) -> Result<T> {
249        if response.status().is_success() {
250            let body = response.text().await?;
251
252            #[derive(serde::Deserialize)]
253            struct DataResponse<T> {
254                data: T,
255            }
256
257            let resp: DataResponse<T> = serde_json::from_str(&body)
258                .map_err(|e| KickApiError::ApiError(format!("JSON parse error: {}", e)))?;
259
260            Ok(resp.data)
261        } else {
262            Err(KickApiError::ApiError(format!(
263                "Request failed: {}",
264                response.status()
265            )))
266        }
267    }
268
269    async fn manage_redemptions(
270        &self,
271        action: &str,
272        redemption_ids: Vec<String>,
273    ) -> Result<ManageRedemptionsResponse> {
274        self.require_token()?;
275
276        let url = format!("{}/channels/rewards/redemptions/{}", self.base_url, action);
277        let request_body = ManageRedemptionsRequest { ids: redemption_ids };
278
279        let request = self
280            .client
281            .post(&url)
282            .header("Accept", "*/*")
283            .bearer_auth(self.token.as_ref().unwrap())
284            .json(&request_body);
285        let response = crate::http::send_with_retry(self.client, request).await?;
286
287        if response.status().is_success() {
288            let body = response.text().await?;
289            let resp: ManageRedemptionsResponse = serde_json::from_str(&body)
290                .map_err(|e| KickApiError::ApiError(format!("JSON parse error: {}", e)))?;
291            Ok(resp)
292        } else {
293            Err(KickApiError::ApiError(format!(
294                "Failed to {} redemptions: {}",
295                action,
296                response.status()
297            )))
298        }
299    }
300}