1use graphql_client::GraphQLQuery;
2use octocrab::Octocrab;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::{
7 config,
8 types::github::{
9 IssueContext, IssueId, Notification, NotificationContext, NotificationId,
10 PullRequestContext, PullRequestId, RepositoryKey, ThreadId,
11 },
12};
13
14#[derive(Debug, Error)]
15pub enum GithubError {
16 #[error("invalid credential. please make sure a valid PAT is set")]
17 BadCredential,
18 #[error("secondary rate limits exceeded")]
20 SecondaryRateLimit,
21 #[error("github api error: {0}")]
22 Api(Box<octocrab::Error>),
23}
24
25impl From<octocrab::Error> for GithubError {
26 fn from(err: octocrab::Error) -> Self {
27 match &err {
28 octocrab::Error::GitHub { source, .. } => match source.status_code.as_u16() {
29 401 => GithubError::BadCredential,
30 403 if source.message.contains("secondary rate limit") => {
31 GithubError::SecondaryRateLimit
32 }
33 _ => GithubError::Api(Box::new(err)),
34 },
35 _ => GithubError::Api(Box::new(err)),
36 }
37 }
38}
39
40#[derive(Clone)]
41pub struct GithubClient {
42 client: Octocrab,
43}
44
45impl GithubClient {
46 pub fn new(pat: impl Into<String>) -> Result<Self, GithubError> {
47 let pat = pat.into();
48 if pat.is_empty() {
49 return Err(GithubError::BadCredential);
50 }
51 let timeout = Some(config::github::CLIENT_TIMEOUT);
52 let octo = Octocrab::builder()
53 .personal_token(pat)
54 .set_connect_timeout(timeout)
55 .set_read_timeout(timeout)
56 .set_write_timeout(timeout)
57 .build()
58 .unwrap();
59 Ok(Self::with(octo))
60 }
61
62 #[must_use]
63 pub fn with(client: Octocrab) -> Self {
64 Self { client }
65 }
66
67 pub(crate) async fn mark_thread_as_done(&self, id: NotificationId) -> Result<(), GithubError> {
68 self.client
69 .activity()
70 .notifications()
71 .mark_as_read(id)
72 .await
73 .map_err(GithubError::from)
74 }
75
76 pub(crate) async fn unsubscribe_thread(&self, id: ThreadId) -> Result<(), GithubError> {
77 #[derive(serde::Serialize)]
83 struct Inner {
84 ignored: bool,
85 }
86 #[derive(serde::Deserialize)]
87 struct Response {}
88
89 let thread = id;
90 let ignored = true;
91
92 let route = format!("/notifications/threads/{thread}/subscription");
93 let body = Inner { ignored };
94
95 self.client
96 .put::<Response, _, _>(route, Some(&body))
97 .await?;
98 Ok(())
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
103pub(crate) enum FetchNotificationInclude {
104 OnlyUnread,
106 All,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
110pub(crate) enum FetchNotificationParticipating {
111 OnlyParticipating,
113 All,
114}
115
116#[derive(Debug, Clone)]
117pub(crate) struct FetchNotificationsParams {
118 pub(crate) page: u8,
119 pub(crate) include: FetchNotificationInclude,
120 pub(crate) participating: FetchNotificationParticipating,
121}
122
123impl GithubClient {
124 #[tracing::instrument(skip(self))]
125 pub(crate) async fn fetch_notifications(
126 &self,
127 FetchNotificationsParams {
128 page,
129 include,
130 participating,
131 }: FetchNotificationsParams,
132 ) -> Result<Vec<Notification>, GithubError> {
133 let mut page = self
134 .client
135 .activity()
136 .notifications()
137 .list()
138 .participating(participating == FetchNotificationParticipating::OnlyParticipating)
139 .all(include == FetchNotificationInclude::All)
140 .page(page) .per_page(config::github::NOTIFICATION_PER_PAGE)
142 .send()
143 .await?;
144 let notifications: Vec<_> = page
145 .take_items()
146 .into_iter()
147 .map(Notification::from)
148 .collect();
149
150 tracing::debug!(
151 "Fetch {} github notifications: {page:?}",
152 notifications.len()
153 );
154
155 Ok(notifications)
156 }
157}
158
159#[derive(GraphQLQuery)]
160#[graphql(
161 schema_path = "src/client/github/schema.json",
162 query_path = "src/client/github/issue_query.gql",
163 variables_derives = "Clone, Debug",
164 response_derives = "Clone, Debug"
165)]
166pub(crate) struct IssueQuery;
167
168impl GithubClient {
169 pub(crate) async fn fetch_issue(
170 &self,
171 NotificationContext {
172 id,
173 repository_key: RepositoryKey { name, owner },
174 ..
175 }: NotificationContext<IssueId>,
176 ) -> Result<IssueContext, GithubError> {
177 let response: octocrab::Result<graphql_client::Response<issue_query::ResponseData>> = self
178 .client
179 .graphql(&IssueQuery::build_query(issue_query::Variables {
180 repository_owner: owner,
181 repository_name: name,
182 issue_number: id.into_inner(),
183 }))
184 .await;
185
186 match response {
187 Ok(response) => match (response.data, response.errors) {
188 (_, Some(errors)) => {
189 tracing::error!("{errors:?}");
190 todo!()
191 }
192 (Some(data), _) => Ok(IssueContext::from(data)),
193 _ => unreachable!(),
194 },
195 Err(error) => Err(GithubError::from(error)),
196 }
197 }
198}
199
200#[derive(GraphQLQuery)]
201#[graphql(
202 schema_path = "src/client/github/schema.json",
203 query_path = "src/client/github/pull_request_query.gql",
204 variables_derives = "Clone, Debug",
205 response_derives = "Clone, Debug"
206)]
207pub(crate) struct PullRequestQuery;
208
209impl GithubClient {
210 pub(crate) async fn fetch_pull_request(
211 &self,
212 NotificationContext {
213 id,
214 repository_key: RepositoryKey { name, owner },
215 ..
216 }: NotificationContext<PullRequestId>,
217 ) -> Result<PullRequestContext, GithubError> {
218 let response: octocrab::Result<graphql_client::Response<pull_request_query::ResponseData>> =
219 self.client
220 .graphql(&PullRequestQuery::build_query(
221 pull_request_query::Variables {
222 repository_owner: owner,
223 repository_name: name,
224 pull_request_number: id.into_inner(),
225 },
226 ))
227 .await;
228
229 match response {
230 Ok(response) => match (response.data, response.errors) {
231 (_, Some(errors)) => {
232 tracing::error!("{errors:?}");
233 todo!()
234 }
235 (Some(data), _) => Ok(PullRequestContext::from(data)),
236 _ => unreachable!(),
237 },
238 Err(error) => Err(GithubError::from(error)),
239 }
240 }
241}