1use crate::{
2 error::{GithubError, GithubResult},
3 types::CollaboratorRole,
4};
5use octocrab::{models::CommentId, Octocrab};
6
7pub struct GithubApiClient {
9 client: Octocrab,
10}
11
12impl GithubApiClient {
13 pub fn new(token: String) -> GithubResult<Self> {
15 let client = Octocrab::builder()
16 .personal_token(token)
17 .build()
18 .map_err(|e| GithubError::ApiError(format!("Failed to create octocrab client: {}", e)))?;
19
20 Ok(Self { client })
21 }
22
23 pub fn from_octocrab(client: Octocrab) -> Self {
25 Self { client }
26 }
27
28 pub async fn close_pull_request(
35 &self,
36 owner: &str,
37 repo: &str,
38 pr_number: u64,
39 ) -> GithubResult<()> {
40 self.client
41 .pulls(owner, repo)
42 .update(pr_number)
43 .state(octocrab::params::pulls::State::Closed)
44 .send()
45 .await
46 .map_err(|e| {
47 GithubError::ApiError(format!("Failed to close PR #{}: {}", pr_number, e))
48 })?;
49
50 Ok(())
51 }
52
53 pub async fn add_comment(
61 &self,
62 owner: &str,
63 repo: &str,
64 issue_number: u64,
65 body: &str,
66 ) -> GithubResult<CommentId> {
67 let comment = self
68 .client
69 .issues(owner, repo)
70 .create_comment(issue_number, body)
71 .await
72 .map_err(|e| {
73 GithubError::ApiError(format!("Failed to add comment to #{}: {}", issue_number, e))
74 })?;
75
76 Ok(comment.id)
77 }
78
79 pub async fn check_collaborator_role(
89 &self,
90 owner: &str,
91 repo: &str,
92 username: &str,
93 ) -> GithubResult<CollaboratorRole> {
94 let result = self
97 .client
98 .repos(owner, repo)
99 .get_contributor_permission(username)
100 .send()
101 .await;
102
103 match result {
104 Ok(permission) => {
105 let perm_str = format!("{:?}", permission.permission).to_lowercase();
108 let role = match perm_str.as_str() {
109 "admin" => CollaboratorRole::Admin,
110 "maintain" => CollaboratorRole::Maintain,
111 "write" | "push" => CollaboratorRole::Write,
112 "triage" => CollaboratorRole::Triage,
113 "read" | "pull" => CollaboratorRole::Read,
114 _ => CollaboratorRole::None,
115 };
116 Ok(role)
117 }
118 Err(octocrab::Error::GitHub { source, .. })
119 if source.message.contains("404") || source.message.contains("Not Found") =>
120 {
121 Ok(CollaboratorRole::None)
123 }
124 Err(e) => Err(GithubError::ApiError(format!(
125 "Failed to check collaborator role for {}: {}",
126 username, e
127 ))),
128 }
129 }
130
131 pub async fn get_file_content(
141 &self,
142 owner: &str,
143 repo: &str,
144 path: &str,
145 ) -> GithubResult<String> {
146 let content = self
148 .client
149 .repos(owner, repo)
150 .get_content()
151 .path(path)
152 .send()
153 .await
154 .map_err(|e| {
155 GithubError::ApiError(format!("Failed to fetch file {} from {}/{}: {}", path, owner, repo, e))
156 })?;
157
158 if let Some(file) = content.items.first() {
161 if let Some(encoded_content) = &file.content {
162 let decoded = base64::Engine::decode(
164 &base64::engine::general_purpose::STANDARD,
165 encoded_content.replace('\n', "").as_bytes()
166 ).map_err(|e| {
167 GithubError::ApiError(format!("Failed to decode base64 content: {}", e))
168 })?;
169
170 let content_str = String::from_utf8(decoded).map_err(|e| {
172 GithubError::ApiError(format!("Failed to decode UTF-8 content: {}", e))
173 })?;
174
175 return Ok(content_str);
176 }
177 }
178
179 Err(GithubError::ApiError(format!(
180 "File {} not found in {}/{}",
181 path, owner, repo
182 )))
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[tokio::test]
194 async fn test_create_api_client() {
195 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
197
198 let result = GithubApiClient::new("test-token".to_string());
199 assert!(result.is_ok());
200 }
201
202 #[test]
203 fn test_collaborator_role_parsing() {
204 let admin_str = "admin";
206 let role = match admin_str {
207 "admin" => CollaboratorRole::Admin,
208 "maintain" => CollaboratorRole::Maintain,
209 "write" => CollaboratorRole::Write,
210 "triage" => CollaboratorRole::Triage,
211 "read" => CollaboratorRole::Read,
212 _ => CollaboratorRole::None,
213 };
214 assert_eq!(role, CollaboratorRole::Admin);
215 }
216
217 }