1use super::{HttpClient, HttpError};
14
15impl HttpClient {
16 pub async fn github_resolve_commit(
21 &self,
22 owner: &str,
23 repository: &str,
24 ) -> Result<Option<String>, HttpError> {
25 #[derive(serde::Deserialize)]
26 struct Commit {
27 sha: String,
28 }
29 let request = self.github_request_headers(
30 self.http_client
31 .get(format!(
32 "https://api.github.com/repos/{}/{}/commits",
33 owner, repository,
34 ))
35 .header("accept", "application/vnd.github+json"),
36 );
37 backoff::future::retry(backoff::ExponentialBackoff::default(), || async {
38 let response = request
39 .try_clone()
40 .unwrap()
41 .send()
42 .await
43 .map_err(HttpError::HttpError)?;
44 let code = response.status();
45 if code.is_success() {
46 let text =
47 response.text().await.map_err(HttpError::HttpError)?;
48 let mut de = serde_json::Deserializer::from_str(&text);
49 match serde_path_to_error::deserialize::<_, Vec<Commit>>(
50 &mut de,
51 ) {
52 Ok(commits) => Ok(commits.first().map(|c| c.sha.clone())),
53 Err(e) => Err(backoff::Error::transient(
54 HttpError::DeserializationError(e),
55 )),
56 }
57 } else if code == reqwest::StatusCode::NOT_FOUND {
58 Ok(None)
59 } else {
60 Err(backoff::Error::transient(
61 github_bad_status(response).await,
62 ))
63 }
64 })
65 .await
66 }
67
68 pub async fn github_get_agent(
73 &self,
74 owner: &str,
75 repository: &str,
76 commit: Option<&str>,
77 ) -> Result<Option<crate::agent::RemoteAgentBaseWithFallbacks>, HttpError>
78 {
79 self.github_get_entity(owner, repository, commit, "agent.json")
80 .await
81 }
82
83 pub async fn github_get_swarm(
88 &self,
89 owner: &str,
90 repository: &str,
91 commit: Option<&str>,
92 ) -> Result<Option<crate::swarm::RemoteSwarmBase>, HttpError> {
93 self.github_get_entity(owner, repository, commit, "swarm.json")
94 .await
95 }
96
97 pub async fn github_get_function(
102 &self,
103 owner: &str,
104 repository: &str,
105 commit: Option<&str>,
106 ) -> Result<Option<crate::functions::FullRemoteFunction>, HttpError> {
107 self.github_get_entity(owner, repository, commit, "function.json")
108 .await
109 }
110
111 pub async fn github_get_profile(
116 &self,
117 owner: &str,
118 repository: &str,
119 commit: Option<&str>,
120 ) -> Result<Option<crate::functions::RemoteProfile>, HttpError> {
121 self.github_get_entity(owner, repository, commit, "profile.json")
122 .await
123 }
124
125 async fn github_get_entity<T: serde::de::DeserializeOwned>(
128 &self,
129 owner: &str,
130 repository: &str,
131 commit: Option<&str>,
132 path: &str,
133 ) -> Result<Option<T>, HttpError> {
134 let resolved;
135 let commit = match commit {
136 Some(commit) => commit,
137 None => match self.github_resolve_commit(owner, repository).await? {
138 Some(commit) => {
139 resolved = commit;
140 resolved.as_str()
141 }
142 None => return Ok(None),
143 },
144 };
145 self.github_read_json(owner, repository, commit, path).await
146 }
147
148 async fn github_read_json<T: serde::de::DeserializeOwned>(
150 &self,
151 owner: &str,
152 repository: &str,
153 commit: &str,
154 path: &str,
155 ) -> Result<Option<T>, HttpError> {
156 match self.github_read_file(owner, repository, commit, path).await? {
157 Some(text) => {
158 let mut de = serde_json::Deserializer::from_str(&text);
159 match serde_path_to_error::deserialize::<_, T>(&mut de) {
160 Ok(value) => Ok(Some(value)),
161 Err(e) => Err(HttpError::DeserializationError(e)),
162 }
163 }
164 None => Ok(None),
165 }
166 }
167
168 async fn github_read_file(
173 &self,
174 owner: &str,
175 repository: &str,
176 commit: &str,
177 path: &str,
178 ) -> Result<Option<String>, HttpError> {
179 backoff::future::retry(backoff::ExponentialBackoff::default(), || async {
180 match self
181 .github_fetch_file_raw(owner, repository, commit, path)
182 .await
183 {
184 Ok(opt) => Ok(opt),
185 Err(e1) => match self
186 .github_fetch_file_api(owner, repository, commit, path)
187 .await
188 {
189 Ok(opt) => Ok(opt),
190 Err(e2) => Err(backoff::Error::transient(
191 HttpError::MultipleErrors(Box::new(e1), Box::new(e2)),
192 )),
193 },
194 }
195 })
196 .await
197 }
198
199 async fn github_fetch_file_raw(
201 &self,
202 owner: &str,
203 repository: &str,
204 commit: &str,
205 path: &str,
206 ) -> Result<Option<String>, HttpError> {
207 let response = self
208 .github_request_headers(self.http_client.get(format!(
209 "https://raw.githubusercontent.com/{}/{}/{}/{}",
210 owner, repository, commit, path,
211 )))
212 .send()
213 .await
214 .map_err(HttpError::HttpError)?;
215 let code = response.status();
216 if code.is_success() {
217 let text = response.text().await.map_err(HttpError::HttpError)?;
218 Ok(Some(text))
219 } else if code == reqwest::StatusCode::NOT_FOUND {
220 Ok(None)
221 } else {
222 Err(github_bad_status(response).await)
223 }
224 }
225
226 async fn github_fetch_file_api(
228 &self,
229 owner: &str,
230 repository: &str,
231 commit: &str,
232 path: &str,
233 ) -> Result<Option<String>, HttpError> {
234 let response = self
235 .github_request_headers(
236 self.http_client
237 .get(format!(
238 "https://api.github.com/repos/{}/{}/contents/{}?ref={}",
239 owner, repository, path, commit,
240 ))
241 .header("accept", "application/vnd.github.raw+json"),
242 )
243 .send()
244 .await
245 .map_err(HttpError::HttpError)?;
246 let code = response.status();
247 if code.is_success() {
248 let text = response.text().await.map_err(HttpError::HttpError)?;
249 Ok(Some(text))
250 } else if code == reqwest::StatusCode::NOT_FOUND {
251 Ok(None)
252 } else {
253 Err(github_bad_status(response).await)
254 }
255 }
256
257 fn github_request_headers(
260 &self,
261 mut request: reqwest::RequestBuilder,
262 ) -> reqwest::RequestBuilder {
263 if let Some(token) = &self.x_github_authorization {
264 let key = token.strip_prefix("Bearer ").unwrap_or(token.as_str());
265 request =
266 request.header("authorization", format!("Bearer {}", key));
267 }
268 request = request.header(
270 "user-agent",
271 self.user_agent.as_deref().unwrap_or("objectiveai-sdk"),
272 );
273 if let Some(x_title) = &self.x_title {
274 request = request.header("x-title", x_title);
275 }
276 if let Some(http_referer) = &self.http_referer {
277 request = request
278 .header("referer", http_referer)
279 .header("http-referer", http_referer);
280 }
281 request
282 }
283}
284
285async fn github_bad_status(response: reqwest::Response) -> HttpError {
287 let code = response.status();
288 match response.text().await {
289 Ok(text) => HttpError::BadStatus {
290 code,
291 body: serde_json::from_str::<serde_json::Value>(&text)
292 .unwrap_or(serde_json::Value::String(text)),
293 },
294 Err(_) => HttpError::BadStatus {
295 code,
296 body: serde_json::Value::Null,
297 },
298 }
299}