nipaw_gitee/
lib.rs

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(&params).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(&params).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(&params).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}