git_bot_feedback/client/github/
specific_api.rs1use super::{GithubApiClient, serde_structs::ThreadComment};
4use crate::{
5 CommentKind, CommentPolicy, RestApiClient, RestApiRateLimitHeaders, RestClientError,
6 ThreadCommentOptions,
7 client::{USER_AGENT, send_api_request},
8};
9use reqwest::{Client, Method, Url};
10use std::{collections::HashMap, env, fs};
11
12impl GithubApiClient {
13 pub fn new() -> Result<Self, RestClientError> {
15 let event_name = env::var("GITHUB_EVENT_NAME").unwrap_or(String::from("unknown"));
16 let pull_request = {
17 match event_name.as_str() {
18 "pull_request" => {
19 let event_payload_path = env::var("GITHUB_EVENT_PATH")?;
21 let file_buf = fs::read_to_string(event_payload_path.clone())?;
23 let payload = serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(
24 &file_buf,
25 )?;
26 payload["number"].as_i64().unwrap_or(-1)
27 }
28 _ => -1,
29 }
30 };
31 let gh_api_url = env::var("GITHUB_API_URL").unwrap_or("https://api.github.com".to_string());
33 let api_url = Url::parse(gh_api_url.as_str())?;
34
35 Ok(GithubApiClient {
36 client: Client::builder()
37 .default_headers(Self::make_headers()?)
38 .user_agent(USER_AGENT)
39 .build()?,
40 pull_request,
41 event_name,
42 api_url,
43 repo: env::var("GITHUB_REPOSITORY")?,
44 sha: env::var("GITHUB_SHA")?,
45 debug_enabled: env::var("ACTIONS_STEP_DEBUG").is_ok_and(|val| &val == "true"),
46 rate_limit_headers: RestApiRateLimitHeaders {
47 reset: "x-ratelimit-reset".to_string(),
48 remaining: "x-ratelimit-remaining".to_string(),
49 retry: "retry-after".to_string(),
50 },
51 })
52 }
53
54 pub async fn update_comment(
56 &self,
57 url: Url,
58 options: ThreadCommentOptions,
59 ) -> Result<(), RestClientError> {
60 let is_lgtm = options.kind == CommentKind::Lgtm;
61 let comment_url = self
62 .remove_bot_comments(
63 &url,
64 &options.marker,
65 (options.policy == CommentPolicy::Anew) || (is_lgtm && options.no_lgtm),
66 )
67 .await?;
68 let payload = HashMap::from([("body", options.mark_comment())]);
69
70 if !is_lgtm || !options.no_lgtm {
71 let req_meth = if comment_url.is_some() {
73 Method::PATCH
74 } else {
75 Method::POST
76 };
77 let request = Self::make_api_request(
78 &self.client,
79 comment_url.unwrap_or(url),
80 req_meth,
81 Some(serde_json::json!(&payload).to_string()),
82 None,
83 )?;
84 match send_api_request(&self.client, request, &self.rate_limit_headers).await {
85 Ok(response) => {
86 Self::log_response(response, "Failed to post thread comment").await;
87 }
88 Err(e) => {
89 log::error!("Failed to post thread comment: {e:?}");
90 }
91 }
92 }
93 Ok(())
94 }
95
96 async fn remove_bot_comments(
98 &self,
99 url: &Url,
100 comment_marker: &str,
101 delete: bool,
102 ) -> Result<Option<Url>, RestClientError> {
103 let mut comment_url = None;
104 let mut comments_url = Some(Url::parse_with_params(url.as_str(), &[("page", "1")])?);
105 let repo = format!(
106 "repos/{}{}/comments",
107 self.repo,
109 if self.is_pr_event() { "/issues" } else { "" },
110 );
111 let base_comment_url = self.api_url.join(&repo).unwrap();
112 while let Some(ref endpoint) = comments_url {
113 let request =
114 Self::make_api_request(&self.client, endpoint.as_str(), Method::GET, None, None)?;
115 let result = send_api_request(&self.client, request, &self.rate_limit_headers).await;
116 match result {
117 Err(e) => {
118 log::error!("Failed to get list of existing thread comments: {e:?}");
119 return Ok(comment_url);
120 }
121 Ok(response) => {
122 if !response.status().is_success() {
123 Self::log_response(
124 response,
125 "Failed to get list of existing thread comments",
126 )
127 .await;
128 return Ok(comment_url);
129 }
130 comments_url = Self::try_next_page(response.headers());
131 let payload =
132 serde_json::from_str::<Vec<ThreadComment>>(&response.text().await?);
133 match payload {
134 Err(e) => {
135 log::error!(
136 "Failed to deserialize list of existing thread comments: {e}"
137 );
138 continue;
139 }
140 Ok(payload) => {
141 for comment in payload {
142 if comment.body.starts_with(comment_marker) {
143 log::debug!(
144 "Found bot comment id {} from user {} ({})",
145 comment.id,
146 comment.user.login,
147 comment.user.id,
148 );
149 let this_comment_url = Url::parse(
150 format!("{base_comment_url}/{}", comment.id).as_str(),
151 )?;
152 if delete || comment_url.is_some() {
153 let del_url = if let Some(last_url) = &comment_url {
158 last_url
159 } else {
160 &this_comment_url
161 };
162 let req = Self::make_api_request(
163 &self.client,
164 del_url.as_str(),
165 Method::DELETE,
166 None,
167 None,
168 )?;
169 match send_api_request(
170 &self.client,
171 req,
172 &self.rate_limit_headers,
173 )
174 .await
175 {
176 Ok(result) => {
177 if !result.status().is_success() {
178 Self::log_response(
179 result,
180 "Failed to delete old thread comment",
181 )
182 .await;
183 }
184 }
185 Err(e) => {
186 log::error!(
187 "Failed to delete old thread comment: {e:?}"
188 )
189 }
190 }
191 }
192 if !delete {
193 comment_url = Some(this_comment_url)
194 }
195 }
196 }
197 }
198 }
199 }
200 }
201 }
202 Ok(comment_url)
203 }
204}