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