kick_rust/fetch/
strategies.rs1use async_trait::async_trait;
7use crate::fetch::{
8 types::{ChannelInfo, FetchResult, FetchError, StrategyConfig},
9 parsers::JsonParser,
10 useragent::generate_browser_fingerprint,
11};
12use reqwest::Client;
13use std::time::Duration;
14use tokio::time::sleep;
15use tracing::{info, warn, debug};
16
17
18#[async_trait]
20pub trait FetchStrategy {
21 fn name(&self) -> &'static str;
23
24 async fn fetch_channel(&self, client: &Client, channel_name: &str, config: &StrategyConfig) -> FetchResult<ChannelInfo>;
26
27 fn is_enabled(&self, _config: &StrategyConfig) -> bool {
29 true
30 }
31}
32
33
34
35
36
37
38
39
40
41pub struct CurlStrategy {
43 _parser: JsonParser, }
45
46impl CurlStrategy {
47 pub fn new() -> Self {
48 Self {
49 _parser: JsonParser, }
51 }
52
53 async fn make_curl_request(&self, url: &str) -> Result<String, FetchError> {
54 let fingerprint = generate_browser_fingerprint();
56
57 let mut headers = curl::easy::List::new();
58 for header in fingerprint.get_curl_headers() {
59 headers.append(&header).unwrap();
60 }
61
62 let mut easy = curl::easy::Easy::new();
63 easy.url(url).unwrap();
64 easy.http_headers(headers).unwrap();
65 easy.follow_location(true).unwrap();
66 easy.timeout(Duration::from_secs(30)).unwrap();
67
68 let mut data = Vec::new();
69 {
70 let mut transfer = easy.transfer();
71
72 transfer.write_function(|new_data| {
73 data.extend_from_slice(new_data);
74 Ok(new_data.len())
75 }).unwrap();
76
77 match transfer.perform() {
78 Ok(_) => {
79 }
81 Err(e) => {
82 warn!("Curl request failed: {}", e);
83 return Err(FetchError::Network(format!("Curl request failed: {}", e)));
84 }
85 }
86 }
87
88 let response_code = easy.response_code().unwrap_or(0);
90 debug!("Curl response code: {}", response_code);
91
92 if response_code == 200 {
93 let decompressed_data = self.try_decompress(&data).await?;
95 let response_text = String::from_utf8(decompressed_data)
96 .map_err(|e| FetchError::Network(format!("Invalid UTF-8 response: {}", e)))?;
97 Ok(response_text)
98 } else if response_code == 404 {
99 Err(FetchError::ChannelNotFound("Channel not found".to_string()))
100 } else if response_code == 429 {
101 Err(FetchError::RateLimit)
102 } else {
103 let error_text = String::from_utf8(data).unwrap_or_default();
104 Err(FetchError::Http(format!("HTTP {}: {}", response_code, error_text)))
105 }
106 }
107}
108
109#[async_trait]
110impl FetchStrategy for CurlStrategy {
111 fn name(&self) -> &'static str {
112 "curl"
113 }
114
115 fn is_enabled(&self, _config: &StrategyConfig) -> bool {
116 true
117 }
118
119 async fn fetch_channel(&self, _client: &Client, channel_name: &str, _config: &StrategyConfig) -> FetchResult<ChannelInfo> {
120 let api_url = format!("https://kick.com/api/v2/channels/{}", channel_name);
121 debug!("Fetching channel {} using curl: {}", channel_name, api_url);
122
123 let response_text = self.make_curl_request(&api_url).await?;
124 JsonParser::parse_channel_response(&response_text)
125 }
126
127}
128
129impl CurlStrategy {
130 async fn try_decompress(&self, data: &[u8]) -> FetchResult<Vec<u8>> {
131 if data.len() >= 2 {
133 if data[0] == 0x1f && data[1] == 0x8b {
135 debug!("Detected gzip compression, decompressing...");
136 return self.decompress_gzip(data).await;
137 }
138 if data.len() >= 6 && &data[0..6] == b"\x8b\x02\x80\x00\x00\x00" {
140 debug!("Detected brotli compression, decompressing...");
141 return self.decompress_brotli(data).await;
142 }
143 }
144
145 if let Ok(decompressed) = self.decompress_gzip(data).await {
147 debug!("Successfully decompressed as gzip");
148 return Ok(decompressed);
149 }
150
151 if let Ok(decompressed) = self.decompress_brotli(data).await {
153 debug!("Successfully decompressed as brotli");
154 return Ok(decompressed);
155 }
156
157 debug!("No compression detected or decompression failed, using raw data");
159 Ok(data.to_vec())
160 }
161
162 async fn decompress_gzip(&self, data: &[u8]) -> FetchResult<Vec<u8>> {
163 use flate2::read::GzDecoder;
164 use std::io::Read;
165
166 let mut decoder = GzDecoder::new(data);
167 let mut decompressed = Vec::new();
168 decoder.read_to_end(&mut decompressed)
169 .map_err(|e| FetchError::Network(format!("Gzip decompression failed: {}", e)))?;
170 Ok(decompressed)
171 }
172
173 async fn decompress_brotli(&self, data: &[u8]) -> FetchResult<Vec<u8>> {
174 use brotli::Decompressor;
175 use std::io::Read;
176
177 let mut decompressed = Vec::new();
178 let mut decoder = Decompressor::new(data, 4096);
179 decoder.read_to_end(&mut decompressed)
180 .map_err(|e| FetchError::Network(format!("Brotli decompression failed: {}", e)))?;
181 Ok(decompressed)
182 }
183}
184
185pub struct StrategyManager {
187 strategies: Vec<Box<dyn FetchStrategy + Send + Sync>>,
188}
189
190impl StrategyManager {
191 pub fn new() -> Result<Self, FetchError> {
193 let strategies: Vec<Box<dyn FetchStrategy + Send + Sync>> = vec![
194 Box::new(CurlStrategy::new()), ];
196
197 Ok(Self { strategies })
198 }
199
200 pub fn with_config(_config: &StrategyConfig) -> Result<Self, FetchError> {
202 let strategies: Vec<Box<dyn FetchStrategy + Send + Sync>> = vec![
203 Box::new(CurlStrategy::new()), ];
205
206 Ok(Self { strategies })
207 }
208
209 pub async fn fetch_channel(
211 &self,
212 client: &Client,
213 channel_name: &str,
214 config: &StrategyConfig,
215 ) -> FetchResult<ChannelInfo> {
216 let mut last_error = None;
217
218 for strategy in &self.strategies {
219 if !strategy.is_enabled(config) {
220 debug!("Skipping disabled strategy: {}", strategy.name());
221 continue;
222 }
223
224 info!("Trying strategy: {}", strategy.name());
225
226 match strategy.fetch_channel(client, channel_name, config).await {
227 Ok(channel) => {
228 info!("Successfully fetched channel {} using strategy: {}", channel_name, strategy.name());
229 return Ok(channel);
230 }
231 Err(e) => {
232 warn!("Strategy {} failed for channel {}: {}", strategy.name(), channel_name, e);
233 last_error = Some(e);
234
235 sleep(Duration::from_millis(500)).await;
237 }
238 }
239 }
240
241 Err(last_error.unwrap_or_else(|| FetchError::ChannelNotFound(format!("No strategies succeeded for channel: {}", channel_name))))
242 }
243
244 pub fn strategy_names(&self) -> Vec<&'static str> {
246 self.strategies.iter().map(|s| s.name()).collect()
247 }
248}
249
250impl Default for StrategyManager {
251 fn default() -> Self {
252 Self::new().unwrap_or_else(|_| {
253 warn!("Failed to create StrategyManager, using curl strategy only");
254 Self {
255 strategies: vec![
256 Box::new(CurlStrategy::new()),
257 ],
258 }
259 })
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::fetch::types::StrategyConfig;
267
268 #[tokio::test]
269 async fn test_strategy_manager_creation() {
270 let manager = StrategyManager::new();
271 assert!(manager.is_ok());
272
273 if let Ok(manager) = manager {
274 let names = manager.strategy_names();
275 assert!(!names.is_empty());
276 assert!(names.contains(&"curl"));
277 }
278 }
279
280 #[tokio::test]
281 async fn test_strategy_manager_with_config() {
282 let config = StrategyConfig::default();
283
284 let manager = StrategyManager::with_config(&config);
285 assert!(manager.is_ok());
286
287 if let Ok(manager) = manager {
288 let names = manager.strategy_names();
289 assert_eq!(names.len(), 1);
290 assert_eq!(names[0], "curl");
291 }
292 }
293
294 #[test]
295 fn test_curl_strategy() {
296 let strategy = CurlStrategy::new();
297 assert_eq!(strategy.name(), "curl");
298
299 let config = StrategyConfig::default();
300 assert!(strategy.is_enabled(&config));
301 }
302}