1use reqwest::blocking::Client;
40use serde::{Deserialize, Serialize};
41
42#[derive(Debug, thiserror::Error)]
44pub enum GrazerError {
45 #[error("HTTP request failed: {0}")]
46 Http(#[from] reqwest::Error),
47 #[error("API error: {0}")]
48 Api(String),
49 #[error("JSON parse error: {0}")]
50 Json(#[from] serde_json::Error),
51}
52
53pub type Result<T> = std::result::Result<T, GrazerError>;
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct BottubeVideo {
60 pub id: String,
61 pub title: String,
62 #[serde(default)]
63 pub agent: String,
64 #[serde(default)]
65 pub category: String,
66 #[serde(default)]
67 pub views: u64,
68 #[serde(default)]
69 pub duration: f64,
70 #[serde(default)]
71 pub created_at: String,
72 #[serde(default)]
73 pub stream_url: String,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct MoltbookPost {
79 pub id: u64,
80 #[serde(default)]
81 pub title: String,
82 #[serde(default)]
83 pub content: String,
84 #[serde(default)]
85 pub submolt: String,
86 #[serde(default)]
87 pub author: String,
88 #[serde(default)]
89 pub upvotes: i64,
90 #[serde(default)]
91 pub created_at: String,
92 #[serde(default)]
93 pub url: String,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct FourclawThread {
99 pub id: String,
100 #[serde(default)]
101 pub title: String,
102 #[serde(default)]
103 pub content: String,
104 #[serde(default, rename = "agentName")]
105 pub agent_name: String,
106 #[serde(default)]
107 pub board: String,
108 #[serde(default, rename = "replyCount")]
109 pub reply_count: u64,
110 #[serde(default)]
111 pub created_at: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct FourclawBoard {
117 pub slug: String,
118 pub name: String,
119 #[serde(default)]
120 pub description: String,
121 #[serde(default, rename = "threadCount")]
122 pub thread_count: u64,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ColonyPost {
128 pub id: String,
129 #[serde(default)]
130 pub title: String,
131 #[serde(default)]
132 pub body: String,
133 #[serde(default)]
134 pub post_type: String,
135 #[serde(default)]
136 pub comment_count: u64,
137 #[serde(default)]
138 pub created_at: String,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct MoltXPost {
144 pub id: String,
145 #[serde(default)]
146 pub content: String,
147 #[serde(default)]
148 pub author_display_name: String,
149 #[serde(default)]
150 pub like_count: u64,
151 #[serde(default)]
152 pub reply_count: u64,
153 #[serde(default)]
154 pub created_at: String,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct MoltExchangeQuestion {
160 pub id: String,
161 #[serde(default)]
162 pub title: String,
163 #[serde(default)]
164 pub body: String,
165 #[serde(default)]
166 pub author: String,
167 #[serde(default)]
168 pub answer_count: u64,
169 #[serde(default)]
170 pub created_at: String,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ClawCitiesSite {
176 pub name: String,
177 #[serde(default)]
178 pub display_name: String,
179 #[serde(default)]
180 pub description: String,
181 #[serde(default)]
182 pub url: String,
183 #[serde(default)]
184 pub guestbook_count: u64,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct ClawstaPost {
190 pub id: String,
191 #[serde(default)]
192 pub content: String,
193 #[serde(default)]
194 pub author: String,
195 #[serde(default)]
196 pub created_at: String,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct BottubeStats {
202 #[serde(default)]
203 pub total_videos: u64,
204 #[serde(default)]
205 pub total_agents: u64,
206 #[serde(default)]
207 pub total_views: u64,
208}
209
210pub struct GrazerClient {
217 http: Client,
218}
219
220impl GrazerClient {
221 pub fn new() -> Self {
223 Self {
224 http: Client::builder()
225 .user_agent("Grazer/1.9.0 (Rust; Elyan Labs)")
226 .timeout(std::time::Duration::from_secs(15))
227 .build()
228 .unwrap_or_default(),
229 }
230 }
231
232 pub fn discover_bottube(
236 &self,
237 category: Option<&str>,
238 agent: Option<&str>,
239 limit: Option<u32>,
240 ) -> Result<Vec<BottubeVideo>> {
241 let mut url = "https://bottube.ai/api/videos?".to_string();
242 if let Some(cat) = category {
243 url.push_str(&format!("category={cat}&"));
244 }
245 if let Some(ag) = agent {
246 url.push_str(&format!("agent={ag}&"));
247 }
248 url.push_str(&format!("limit={}", limit.unwrap_or(20)));
249
250 let resp: Vec<BottubeVideo> = self.http.get(&url).send()?.json()?;
251 Ok(resp)
252 }
253
254 pub fn search_bottube(&self, query: &str, limit: Option<u32>) -> Result<Vec<BottubeVideo>> {
256 let url = format!(
257 "https://bottube.ai/api/videos/search?q={}&limit={}",
258 query,
259 limit.unwrap_or(20)
260 );
261 let resp: Vec<BottubeVideo> = self.http.get(&url).send()?.json()?;
262 Ok(resp)
263 }
264
265 pub fn bottube_stats(&self) -> Result<BottubeStats> {
267 let resp: BottubeStats = self.http.get("https://bottube.ai/api/stats").send()?.json()?;
268 Ok(resp)
269 }
270
271 pub fn discover_moltbook(
275 &self,
276 submolt: Option<&str>,
277 limit: Option<u32>,
278 ) -> Result<Vec<MoltbookPost>> {
279 let mut url = "https://www.moltbook.com/api/v1/posts?".to_string();
280 if let Some(s) = submolt {
281 url.push_str(&format!("submolt={s}&"));
282 }
283 url.push_str(&format!("limit={}", limit.unwrap_or(20)));
284
285 let resp: Vec<MoltbookPost> = self.http.get(&url).send()?.json()?;
286 Ok(resp)
287 }
288
289 pub fn fourclaw_boards(&self) -> Result<Vec<FourclawBoard>> {
293 let resp: Vec<FourclawBoard> = self
294 .http
295 .get("https://www.4claw.org/api/v1/boards")
296 .send()?
297 .json()?;
298 Ok(resp)
299 }
300
301 pub fn discover_fourclaw(
303 &self,
304 board: Option<&str>,
305 limit: Option<u32>,
306 ) -> Result<Vec<FourclawThread>> {
307 let board = board.unwrap_or("b");
308 let url = format!(
309 "https://www.4claw.org/api/v1/boards/{board}/threads?limit={}",
310 limit.unwrap_or(20).min(20)
311 );
312 let resp: Vec<FourclawThread> = self.http.get(&url).send()?.json()?;
313 Ok(resp)
314 }
315
316 pub fn fourclaw_thread(&self, thread_id: &str) -> Result<serde_json::Value> {
318 let url = format!("https://www.4claw.org/api/v1/threads/{thread_id}");
319 let resp: serde_json::Value = self.http.get(&url).send()?.json()?;
320 Ok(resp)
321 }
322
323 pub fn discover_colony(
327 &self,
328 colony: Option<&str>,
329 limit: Option<u32>,
330 ) -> Result<Vec<ColonyPost>> {
331 let mut url = "https://thecolony.cc/api/v1/posts?".to_string();
332 if let Some(c) = colony {
333 url.push_str(&format!("colony={c}&"));
334 }
335 url.push_str(&format!("limit={}", limit.unwrap_or(20)));
336
337 let resp: Vec<ColonyPost> = self.http.get(&url).send()?.json()?;
338 Ok(resp)
339 }
340
341 pub fn discover_moltx(&self, limit: Option<u32>) -> Result<Vec<MoltXPost>> {
345 let url = format!(
346 "https://moltx.io/v1/posts?limit={}",
347 limit.unwrap_or(20)
348 );
349 let resp: Vec<MoltXPost> = self.http.get(&url).send()?.json()?;
350 Ok(resp)
351 }
352
353 pub fn discover_moltx_trending(&self, limit: Option<u32>) -> Result<Vec<MoltXPost>> {
355 let url = format!(
356 "https://moltx.io/v1/posts/trending?limit={}",
357 limit.unwrap_or(20)
358 );
359 let resp: Vec<MoltXPost> = self.http.get(&url).send()?.json()?;
360 Ok(resp)
361 }
362
363 pub fn discover_moltexchange(
367 &self,
368 limit: Option<u32>,
369 ) -> Result<Vec<MoltExchangeQuestion>> {
370 let url = format!(
371 "https://moltexchange.ai/v1/questions?limit={}",
372 limit.unwrap_or(20)
373 );
374 let resp: Vec<MoltExchangeQuestion> = self.http.get(&url).send()?.json()?;
375 Ok(resp)
376 }
377
378 pub fn discover_clawcities(&self, limit: Option<u32>) -> Result<Vec<ClawCitiesSite>> {
382 let url = format!(
383 "https://clawcities.com/api/v1/sites?limit={}",
384 limit.unwrap_or(20)
385 );
386 let resp: Vec<ClawCitiesSite> = self.http.get(&url).send()?.json()?;
387 Ok(resp)
388 }
389
390 pub fn discover_clawsta(&self, limit: Option<u32>) -> Result<Vec<ClawstaPost>> {
394 let url = format!(
395 "https://clawsta.io/v1/posts?limit={}",
396 limit.unwrap_or(20)
397 );
398 let resp: Vec<ClawstaPost> = self.http.get(&url).send()?.json()?;
399 Ok(resp)
400 }
401}
402
403impl Default for GrazerClient {
404 fn default() -> Self {
405 Self::new()
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_client_creation() {
415 let _client = GrazerClient::new();
416 }
417
418 #[test]
419 fn test_default_impl() {
420 let _client = GrazerClient::default();
421 }
422}