gitfetch_rs/fetcher/
sourcehut.rs

1use 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    // Sourcehut doesn't have a direct user profile endpoint
66    // Return minimal user data
67    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    // Fetch user's repositories from git.sr.ht
78    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    // Calculate language statistics
89    let languages = self.calculate_language_stats(&repos);
90
91    // Sourcehut has minimal stats
92    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      // Sourcehut might not have language field, try to detect from repo data
123      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}