use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum GrazerError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("API error: {0}")]
Api(String),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, GrazerError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BottubeVideo {
pub id: String,
pub title: String,
#[serde(default)]
pub agent: String,
#[serde(default)]
pub category: String,
#[serde(default)]
pub views: u64,
#[serde(default)]
pub duration: f64,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub stream_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MoltbookPost {
pub id: u64,
#[serde(default)]
pub title: String,
#[serde(default)]
pub content: String,
#[serde(default)]
pub submolt: String,
#[serde(default)]
pub author: String,
#[serde(default)]
pub upvotes: i64,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FourclawThread {
pub id: String,
#[serde(default)]
pub title: String,
#[serde(default)]
pub content: String,
#[serde(default, rename = "agentName")]
pub agent_name: String,
#[serde(default)]
pub board: String,
#[serde(default, rename = "replyCount")]
pub reply_count: u64,
#[serde(default)]
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FourclawBoard {
pub slug: String,
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default, rename = "threadCount")]
pub thread_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColonyPost {
pub id: String,
#[serde(default)]
pub title: String,
#[serde(default)]
pub body: String,
#[serde(default)]
pub post_type: String,
#[serde(default)]
pub comment_count: u64,
#[serde(default)]
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MoltXPost {
pub id: String,
#[serde(default)]
pub content: String,
#[serde(default)]
pub author_display_name: String,
#[serde(default)]
pub like_count: u64,
#[serde(default)]
pub reply_count: u64,
#[serde(default)]
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MoltExchangeQuestion {
pub id: String,
#[serde(default)]
pub title: String,
#[serde(default)]
pub body: String,
#[serde(default)]
pub author: String,
#[serde(default)]
pub answer_count: u64,
#[serde(default)]
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClawCitiesSite {
pub name: String,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub url: String,
#[serde(default)]
pub guestbook_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClawstaPost {
pub id: String,
#[serde(default)]
pub content: String,
#[serde(default)]
pub author: String,
#[serde(default)]
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BottubeStats {
#[serde(default)]
pub total_videos: u64,
#[serde(default)]
pub total_agents: u64,
#[serde(default)]
pub total_views: u64,
}
pub struct GrazerClient {
http: Client,
}
impl GrazerClient {
pub fn new() -> Self {
Self {
http: Client::builder()
.user_agent("Grazer/1.9.0 (Rust; Elyan Labs)")
.timeout(std::time::Duration::from_secs(15))
.build()
.unwrap_or_default(),
}
}
pub fn discover_bottube(
&self,
category: Option<&str>,
agent: Option<&str>,
limit: Option<u32>,
) -> Result<Vec<BottubeVideo>> {
let mut url = "https://bottube.ai/api/videos?".to_string();
if let Some(cat) = category {
url.push_str(&format!("category={cat}&"));
}
if let Some(ag) = agent {
url.push_str(&format!("agent={ag}&"));
}
url.push_str(&format!("limit={}", limit.unwrap_or(20)));
let resp: Vec<BottubeVideo> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn search_bottube(&self, query: &str, limit: Option<u32>) -> Result<Vec<BottubeVideo>> {
let url = format!(
"https://bottube.ai/api/videos/search?q={}&limit={}",
query,
limit.unwrap_or(20)
);
let resp: Vec<BottubeVideo> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn bottube_stats(&self) -> Result<BottubeStats> {
let resp: BottubeStats = self.http.get("https://bottube.ai/api/stats").send()?.json()?;
Ok(resp)
}
pub fn discover_moltbook(
&self,
submolt: Option<&str>,
limit: Option<u32>,
) -> Result<Vec<MoltbookPost>> {
let mut url = "https://www.moltbook.com/api/v1/posts?".to_string();
if let Some(s) = submolt {
url.push_str(&format!("submolt={s}&"));
}
url.push_str(&format!("limit={}", limit.unwrap_or(20)));
let resp: Vec<MoltbookPost> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn fourclaw_boards(&self) -> Result<Vec<FourclawBoard>> {
let resp: Vec<FourclawBoard> = self
.http
.get("https://www.4claw.org/api/v1/boards")
.send()?
.json()?;
Ok(resp)
}
pub fn discover_fourclaw(
&self,
board: Option<&str>,
limit: Option<u32>,
) -> Result<Vec<FourclawThread>> {
let board = board.unwrap_or("b");
let url = format!(
"https://www.4claw.org/api/v1/boards/{board}/threads?limit={}",
limit.unwrap_or(20).min(20)
);
let resp: Vec<FourclawThread> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn fourclaw_thread(&self, thread_id: &str) -> Result<serde_json::Value> {
let url = format!("https://www.4claw.org/api/v1/threads/{thread_id}");
let resp: serde_json::Value = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn discover_colony(
&self,
colony: Option<&str>,
limit: Option<u32>,
) -> Result<Vec<ColonyPost>> {
let mut url = "https://thecolony.cc/api/v1/posts?".to_string();
if let Some(c) = colony {
url.push_str(&format!("colony={c}&"));
}
url.push_str(&format!("limit={}", limit.unwrap_or(20)));
let resp: Vec<ColonyPost> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn discover_moltx(&self, limit: Option<u32>) -> Result<Vec<MoltXPost>> {
let url = format!(
"https://moltx.io/v1/posts?limit={}",
limit.unwrap_or(20)
);
let resp: Vec<MoltXPost> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn discover_moltx_trending(&self, limit: Option<u32>) -> Result<Vec<MoltXPost>> {
let url = format!(
"https://moltx.io/v1/posts/trending?limit={}",
limit.unwrap_or(20)
);
let resp: Vec<MoltXPost> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn discover_moltexchange(
&self,
limit: Option<u32>,
) -> Result<Vec<MoltExchangeQuestion>> {
let url = format!(
"https://moltexchange.ai/v1/questions?limit={}",
limit.unwrap_or(20)
);
let resp: Vec<MoltExchangeQuestion> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn discover_clawcities(&self, limit: Option<u32>) -> Result<Vec<ClawCitiesSite>> {
let url = format!(
"https://clawcities.com/api/v1/sites?limit={}",
limit.unwrap_or(20)
);
let resp: Vec<ClawCitiesSite> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
pub fn discover_clawsta(&self, limit: Option<u32>) -> Result<Vec<ClawstaPost>> {
let url = format!(
"https://clawsta.io/v1/posts?limit={}",
limit.unwrap_or(20)
);
let resp: Vec<ClawstaPost> = self.http.get(&url).send()?.json()?;
Ok(resp)
}
}
impl Default for GrazerClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let _client = GrazerClient::new();
}
#[test]
fn test_default_impl() {
let _client = GrazerClient::default();
}
}