gitfetch_rs/fetcher/
sourcehut.rs1use super::Fetcher;
2use anyhow::Result;
3use async_trait::async_trait;
4use serde_json::Value;
5
6pub struct SourcehutFetcher {
7 client: reqwest::Client,
8 base_url: String,
9 token: Option<String>,
10}
11
12impl SourcehutFetcher {
13 pub fn new(base_url: &str, token: Option<&str>) -> Result<Self> {
14 Ok(Self {
15 client: reqwest::Client::new(),
16 base_url: base_url.trim_end_matches('/').to_string(),
17 token: token.map(String::from),
18 })
19 }
20
21 fn api_request(&self, endpoint: &str) -> Result<Value> {
22 let url = format!("{}/api{}", self.base_url, endpoint);
23
24 let mut req = self.client.get(&url);
25
26 if let Some(token) = &self.token {
27 req = req.header("Authorization", format!("token {}", token));
28 }
29
30 let rt = tokio::runtime::Runtime::new()?;
31 let response =
32 rt.block_on(async { req.timeout(std::time::Duration::from_secs(30)).send().await })?;
33
34 if !response.status().is_success() {
35 return Err(anyhow::anyhow!(
36 "Sourcehut API request failed: {}",
37 response.status()
38 ));
39 }
40
41 let rt = tokio::runtime::Runtime::new()?;
42 let data = rt.block_on(async { response.json::<Value>().await })?;
43
44 Ok(data)
45 }
46}
47
48#[async_trait]
49impl Fetcher for SourcehutFetcher {
50 async fn get_authenticated_user(&self) -> Result<String> {
51 if self.token.is_none() {
52 return Err(anyhow::anyhow!(
53 "Token required for Sourcehut authentication"
54 ));
55 }
56
57 let data = self.api_request("/user/profile")?;
58 data["username"]
59 .as_str()
60 .map(String::from)
61 .ok_or_else(|| anyhow::anyhow!("Could not get authenticated user"))
62 }
63
64 async fn fetch_user_data(&self, username: &str) -> Result<Value> {
65 Ok(serde_json::json!({
68 "username": username,
69 "name": username,
70 "bio": "",
71 "website": "",
72 "company": "",
73 }))
74 }
75
76 async fn fetch_user_stats(&self, username: &str, _user_data: Option<&Value>) -> Result<Value> {
77 let repos_endpoint = format!("/repos?owner={}", username);
79 let repos_data = self
80 .api_request(&repos_endpoint)
81 .unwrap_or_else(|_| serde_json::json!({"results": []}));
82
83 let repos = repos_data["results"]
84 .as_array()
85 .cloned()
86 .unwrap_or_default();
87
88 let languages = self.calculate_language_stats(&repos);
90
91 Ok(serde_json::json!({
93 "total_stars": 0,
94 "total_forks": 0,
95 "total_repos": repos.len(),
96 "languages": languages,
97 "contribution_graph": [],
98 "current_streak": 0,
99 "longest_streak": 0,
100 "total_contributions": 0,
101 "pull_requests": {
102 "open": 0,
103 "awaiting_review": 0,
104 "mentions": 0
105 },
106 "issues": {
107 "assigned": 0,
108 "created": 0,
109 "mentions": 0
110 },
111 }))
112 }
113}
114
115impl SourcehutFetcher {
116 fn calculate_language_stats(&self, repos: &[Value]) -> Value {
117 use std::collections::HashMap;
118
119 let mut language_counts: HashMap<String, i32> = HashMap::new();
120
121 for repo in repos {
122 if let Some(language) = repo["language"].as_str() {
124 if !language.is_empty() {
125 let normalized = language.to_lowercase();
126 *language_counts.entry(normalized).or_insert(0) += 1;
127 }
128 }
129 }
130
131 let total: i32 = language_counts.values().sum();
132 if total == 0 {
133 return serde_json::json!({});
134 }
135
136 let mut language_percentages: HashMap<String, f64> = HashMap::new();
137 for (lang, count) in language_counts {
138 let percentage = (count as f64 / total as f64) * 100.0;
139 let display_name = lang
140 .chars()
141 .enumerate()
142 .map(|(i, c)| {
143 if i == 0 {
144 c.to_uppercase().to_string()
145 } else {
146 c.to_string()
147 }
148 })
149 .collect::<String>();
150 language_percentages.insert(display_name, percentage);
151 }
152
153 serde_json::to_value(language_percentages).unwrap_or_else(|_| serde_json::json!({}))
154 }
155}