1mod client;
2mod common;
3mod middleware;
4
5pub use nipaw_core::Client;
6
7use crate::{
8 client::{HTTP_CLIENT, PROXY_URL},
9 common::{Html, JsonValue},
10};
11use async_trait::async_trait;
12use nipaw_core::{
13 Result,
14 error::Error,
15 option::{CommitListOptions, OrgRepoListOptions, ReposListOptions},
16 types::{
17 collaborator::{CollaboratorPermission, CollaboratorResult},
18 commit::CommitInfo,
19 org::OrgInfo,
20 repo::RepoInfo,
21 user::{ContributionResult, UserInfo},
22 },
23};
24use reqwest::header;
25use serde_json::Value;
26use std::collections::HashMap;
27
28const API_URL: &str = "https://gitee.com/api/v5";
29const BASE_URL: &str = "https://gitee.com";
30
31#[derive(Debug, Default)]
32pub struct GiteeClient {
33 pub token: Option<String>,
34}
35
36impl GiteeClient {
37 pub fn new() -> Self {
38 Self::default()
39 }
40}
41
42#[async_trait]
43impl Client for GiteeClient {
44 fn set_token(&mut self, token: &str) -> Result<()> {
45 if token.is_empty() {
46 return Err(Error::TokenEmpty);
47 }
48 self.token = Some(token.to_string());
49 Ok(())
50 }
51
52 fn set_proxy(&mut self, proxy: &str) -> Result<()> {
53 PROXY_URL.set(proxy.to_string()).unwrap();
54 Ok(())
55 }
56
57 async fn get_user_info(&self) -> Result<UserInfo> {
58 if self.token.is_none() {
59 return Err(Error::TokenEmpty);
60 }
61 let url = format!("{}/user", API_URL);
62 let request =
63 HTTP_CLIENT.get(url).query(&[("access_token", self.token.as_ref().unwrap().as_str())]);
64
65 let resp = request.send().await?;
66 let user_info: JsonValue = resp.json().await?;
67 Ok(user_info.into())
68 }
69
70 async fn get_user_info_with_name(&self, user_name: &str) -> Result<UserInfo> {
71 let url = format!("{}/users/{}", API_URL, user_name);
72 let mut request = HTTP_CLIENT.get(url);
73 if let Some(token) = &self.token {
74 request = request.query(&[("access_token", token.as_str())]);
75 }
76 let resp = request.send().await?;
77 let user_info: JsonValue = resp.json().await?;
78 Ok(user_info.into())
79 }
80
81 async fn get_user_avatar_url(&self, user_name: &str) -> Result<String> {
82 let url = format!("{}/users/{}/detail", BASE_URL, user_name);
83 let request = HTTP_CLIENT.get(url).header("Referer", BASE_URL);
84 let resp = request.send().await?;
85 let user_info: JsonValue = resp.json().await?;
86 let avatar_url = user_info
87 .0
88 .get("data")
89 .and_then(|data| data.get("avatar_url"))
90 .and_then(|v| v.as_str())
91 .unwrap()
92 .to_string();
93 Ok(avatar_url)
94 }
95
96 async fn get_user_contribution(&self, user_name: &str) -> Result<ContributionResult> {
97 let url = format!("{}/{}", BASE_URL, user_name);
98 let request = HTTP_CLIENT
99 .get(url)
100 .header("X-Requested-With", "XMLHttpRequest")
101 .header("Accept", "application/javascript");
102 let resp = request.send().await?;
103 let html: Html = resp.text().await?.into();
104 Ok(html.into())
105 }
106
107 async fn get_org_info(&self, org_name: &str) -> Result<OrgInfo> {
108 let url = format!("{}/orgs/{}", BASE_URL, org_name);
109 let mut request = HTTP_CLIENT.get(url);
110 if let Some(token) = &self.token {
111 request = request.query(&[("access_token", token.as_str())]);
112 }
113 let resp = request.send().await?;
114 let org_info: JsonValue = resp.json().await?;
115 Ok(org_info.into())
116 }
117
118 async fn get_org_repos(
119 &self,
120 org_name: &str,
121 options: Option<OrgRepoListOptions>,
122 ) -> Result<Vec<RepoInfo>> {
123 let url = format!("{}/orgs/{}/repos", API_URL, org_name);
124 let mut request = HTTP_CLIENT.get(url);
125 let mut params = HashMap::new();
126 if let Some(token) = &self.token {
127 request = request.query(&[("access_token", token.as_str())]);
128 }
129 if let Some(option) = options {
130 let per_page = option.per_page.unwrap_or_default().min(100);
131 params.insert("per_page", per_page.to_string());
132 let page = option.page.unwrap_or_default();
133 params.insert("page", page.to_string());
134 }
135 let resp = request.send().await?;
136 let repo_list: Vec<JsonValue> = resp.json().await?;
137 Ok(repo_list.into_iter().map(|v| v.into()).collect())
138 }
139
140 async fn get_org_avatar_url(&self, org_name: &str) -> Result<String> {
141 let url = format!("{}/{}", BASE_URL, org_name);
142 let request = HTTP_CLIENT.get(url);
143 let resp = request.send().await?;
144 let org_html: String = resp.text().await?;
145
146 let document = scraper::Html::parse_document(&org_html);
147 let selector = scraper::Selector::parse("img.avatar.current-group-avatar").unwrap();
148
149 let element = document.select(&selector).next().unwrap();
150 let src = element.value().attr("src").unwrap();
151 let avatar_url = src.split('!').next().unwrap_or(src).to_string();
152 Ok(avatar_url)
153 }
154
155 async fn get_repo_info(&self, repo_path: (&str, &str)) -> Result<RepoInfo> {
156 let url = format!("{}/repos/{}/{}", API_URL, repo_path.0, repo_path.1);
157 let mut request = HTTP_CLIENT.get(url);
158 if let Some(token) = &self.token {
159 request = request.query(&[("access_token", token.as_str())]);
160 }
161 let resp = request.send().await?;
162 let repo_info: JsonValue = resp.json().await?;
163 Ok(repo_info.into())
164 }
165
166 async fn get_repo_default_branch(
167 &self,
168 repo_path: (&str, &str),
169 use_web_api: Option<bool>,
170 ) -> Result<String> {
171 match use_web_api {
172 Some(true) => {
173 let url =
174 format!("{}/{}/{}/branches/names.json", BASE_URL, repo_path.0, repo_path.1);
175 let request = HTTP_CLIENT.get(url).header("Referer", BASE_URL);
176 let resp = request.send().await?;
177 let repo_info: JsonValue = resp.json().await?;
178 let default_branch = repo_info
179 .0
180 .get("branches")
181 .and_then(|branches| branches.as_array())
182 .and_then(|branches| {
183 branches.iter().find(|branch| {
184 branch.get("is_default").and_then(|v| v.as_bool()).unwrap_or(false)
185 })
186 })
187 .and_then(|branch| branch.get("name").and_then(|v| v.as_str()))
188 .map(|s| s.to_string())
189 .unwrap();
190 Ok(default_branch)
191 }
192 Some(false) | None => {
193 let url = format!("{}/repos/{}/{}", API_URL, repo_path.0, repo_path.1);
194 let mut request = HTTP_CLIENT.get(url);
195 if let Some(token) = &self.token {
196 request = request.query(&[("access_token", token.as_str())]);
197 }
198 let resp = request.send().await?;
199 let repo_info: JsonValue = resp.json().await?;
200 let default_branch =
201 repo_info.0.get("default_branch").and_then(|v| v.as_str()).unwrap().to_string();
202 Ok(default_branch)
203 }
204 }
205 }
206
207 async fn get_user_repos(&self, option: Option<ReposListOptions>) -> Result<Vec<RepoInfo>> {
208 let url = format!("{}/user/repos", API_URL);
209 let request = HTTP_CLIENT.get(url);
210 let mut params: HashMap<&str, String> = HashMap::new();
211 if let Some(token) = &self.token {
212 params.insert("access_token", token.to_owned());
213 }
214
215 params.insert("sort", "updated".to_string());
216
217 if let Some(option) = option {
218 let per_page = option.per_page.unwrap_or_default().min(100);
219 params.insert("per_page", per_page.to_string());
220 let page = option.page.unwrap_or_default();
221 params.insert("page", page.to_string());
222 }
223 let resp = request.query(¶ms).send().await?;
224 let repo_infos: Vec<JsonValue> = resp.json().await?;
225 Ok(repo_infos.into_iter().map(|v| v.into()).collect())
226 }
227
228 async fn get_user_repos_with_name(
229 &self,
230 user_name: &str,
231 option: Option<ReposListOptions>,
232 ) -> Result<Vec<RepoInfo>> {
233 let url = format!("{}/users/{}/repos", API_URL, user_name);
234 let request = HTTP_CLIENT.get(url);
235 let mut params: HashMap<&str, String> = HashMap::new();
236 if let Some(token) = &self.token {
237 params.insert("access_token", token.to_owned());
238 };
239 params.insert("sort", "pushed".to_string());
240
241 if let Some(option) = option {
242 let per_page = option.per_page.unwrap_or_default().min(100);
243 params.insert("per_page", per_page.to_string());
244 let page = option.page.unwrap_or_default();
245 params.insert("page", page.to_string());
246 }
247 let resp = request.query(¶ms).send().await?;
248 let repo_infos: Vec<JsonValue> = resp.json().await?;
249 Ok(repo_infos.into_iter().map(|v| v.into()).collect())
250 }
251
252 async fn get_commit_info(
253 &self,
254 repo_path: (&str, &str),
255 sha: Option<&str>,
256 ) -> Result<CommitInfo> {
257 let url = format!(
258 "{}/repos/{}/{}/commits/{}",
259 API_URL,
260 repo_path.0,
261 repo_path.1,
262 sha.unwrap_or("HEAD")
263 );
264 let mut request = HTTP_CLIENT.get(url);
265 if let Some(token) = &self.token {
266 request = request.query(&[("access_token", token.as_str())]);
267 }
268 let resp = request.send().await?;
269 let mut commit_info: JsonValue = resp.json().await?;
270 let author_avatar_url = commit_info
271 .0
272 .get("author")
273 .and_then(|v| v.get("avatar_url"))
274 .and_then(|v| v.as_str())
275 .unwrap()
276 .to_string();
277 let committer_avatar_url = commit_info
278 .0
279 .get("committer")
280 .and_then(|v| v.get("avatar_url"))
281 .and_then(|v| v.as_str())
282 .unwrap()
283 .to_string();
284 if let Some(author_obj) = commit_info
285 .0
286 .get_mut("commit")
287 .and_then(|commit| commit.as_object_mut())
288 .and_then(|commit_obj| commit_obj.get_mut("author"))
289 .and_then(|author| author.as_object_mut())
290 {
291 author_obj.insert("avatar_url".to_string(), Value::String(author_avatar_url));
292 }
293
294 if let Some(committer_obj) = commit_info
295 .0
296 .get_mut("commit")
297 .and_then(|commit| commit.as_object_mut())
298 .and_then(|commit_obj| commit_obj.get_mut("committer"))
299 .and_then(|committer| committer.as_object_mut())
300 {
301 committer_obj.insert("avatar_url".to_string(), Value::String(committer_avatar_url));
302 }
303 Ok(commit_info.into())
304 }
305
306 async fn get_commit_infos(
307 &self,
308 repo_path: (&str, &str),
309 option: Option<CommitListOptions>,
310 ) -> Result<Vec<CommitInfo>> {
311 let url = format!("{}/repos/{}/{}/commits", API_URL, repo_path.0, repo_path.1);
312 let request = HTTP_CLIENT.get(url);
313 let mut params: HashMap<&str, String> = HashMap::new();
314 if let Some(token) = &self.token {
315 params.insert("access_token", token.to_owned());
316 }
317
318 if let Some(option) = option {
319 let per_page = option.per_page.unwrap_or_default().min(100);
320 params.insert("per_page", per_page.to_string());
321 let page = option.page.unwrap_or_default();
322 params.insert("page", page.to_string());
323 if let Some(sha) = option.sha {
324 params.insert("sha", sha.to_string());
325 }
326 if let Some(author) = option.author {
327 params.insert("author", author.to_string());
328 }
329 if let Some(since) = option.since {
330 params.insert("since", since.to_rfc3339());
331 }
332 if let Some(until) = option.until {
333 params.insert("until", until.to_rfc3339());
334 }
335 }
336 let resp = request.query(¶ms).send().await?;
337 let commit_infos: Vec<JsonValue> = resp.json().await?;
338 Ok(commit_infos.into_iter().map(|v| v.into()).collect())
339 }
340
341 async fn add_repo_collaborator(
342 &self,
343 repo_path: (&str, &str),
344 user_name: &str,
345 permission: Option<CollaboratorPermission>,
346 ) -> Result<CollaboratorResult> {
347 let url = format!(
348 "{}/repos/{}/{}/collaborators/{}",
349 API_URL, repo_path.0, repo_path.1, user_name
350 );
351 let request = HTTP_CLIENT.put(url);
352
353 let permission = match permission {
354 Some(permission) => match permission {
355 CollaboratorPermission::Admin => "admin".to_string(),
356 CollaboratorPermission::Push => "push".to_string(),
357 CollaboratorPermission::Pull => "pull".to_string(),
358 },
359 None => "pull".to_string(),
360 };
361
362 let body = if let Some(token) = &self.token {
363 serde_json::json!({
364 "access_token": token.to_string(),
365 "permission": permission,
366 })
367 } else {
368 serde_json::json!({
369 "permission": permission,
370 })
371 };
372
373 let resp = request
374 .header(header::CONTENT_TYPE, "application/json")
375 .body(body.to_string())
376 .send()
377 .await?;
378 let collaborator: JsonValue = resp.json().await?;
379 Ok(collaborator.into())
380 }
381}