1use async_trait::async_trait;
2
3#[derive(Debug)]
4pub struct Repository {
5 pub owner: String,
6 pub name: String,
7}
8
9#[derive(Debug)]
10pub struct IssueCreatedEvent {
11 pub id: usize,
12 pub title: String,
13 pub body: String,
14 pub user: String,
15}
16
17#[derive(Debug)]
18pub enum UpdatedPart {
19 Title { from: String, to: String },
20 Body { from: String, to: String },
21}
22
23#[derive(Debug)]
24pub struct IssueUpdatedEvent {
25 pub id: usize,
26 pub updated_part: UpdatedPart,
27 pub user: String,
28}
29
30#[derive(Debug)]
31pub struct IssueReopenedEvent {
32 pub id: usize,
33 pub title: String,
34 pub body: String,
35 pub user: String,
36}
37
38#[derive(Debug)]
39pub struct PullRequestCreatedEvent {
40 pub id: usize,
41 pub title: String,
42 pub body: String,
43 pub user: String,
44 pub from_repo: Repository,
45 pub from_ref: String,
46 pub to_ref: String,
47}
48
49#[derive(Debug)]
50pub struct PullRequestUpdatedEvent {
51 pub id: usize,
52 pub updated_part: UpdatedPart,
53 pub user: String,
54}
55
56#[derive(Debug)]
57pub enum CommentTarget {
58 Issue(usize),
59 PullRequest(usize),
60}
61
62impl CommentTarget {
63 pub fn id(&self) -> usize {
64 match self {
65 CommentTarget::Issue(x) => *x,
66 CommentTarget::PullRequest(x) => *x,
67 }
68 }
69}
70
71#[derive(Debug)]
72pub struct CommentCreatedEvent {
73 pub id: usize,
74 pub user: String,
75 pub target: CommentTarget,
76 pub body: String,
77}
78
79#[derive(Debug)]
80pub struct CommentUpdatedEvent {
81 pub id: usize,
82 pub user: String,
83 pub target: CommentTarget,
84 pub from: String,
85 pub to: String,
86}
87
88#[derive(Debug)]
89pub struct RunningInfo {
90 pub run_id: usize,
91 pub run_number: usize,
92}
93
94#[async_trait]
95pub trait Bot: Send + Sync {
96 async fn on_issue_created(
97 &self,
98 _repo: Repository,
99 _running_info: RunningInfo,
100 _event: IssueCreatedEvent,
101 ) {
102 }
103
104 async fn on_issue_updated(
105 &self,
106 _repo: Repository,
107 _running_info: RunningInfo,
108 _event: IssueUpdatedEvent,
109 ) {
110 }
111
112 async fn on_issue_closed(
113 &self,
114 _repo: Repository,
115 _running_info: RunningInfo,
116 _issue_id: usize,
117 ) {
118 }
119
120 async fn on_issue_reopened(
121 &self,
122 _repo: Repository,
123 _running_info: RunningInfo,
124 _event: IssueReopenedEvent,
125 ) {
126 }
127
128 async fn on_pull_request_created(
129 &self,
130 _repo: Repository,
131 _running_info: RunningInfo,
132 _event: PullRequestCreatedEvent,
133 ) {
134 }
135
136 async fn on_pull_request_updated(
137 &self,
138 _repo: Repository,
139 _running_info: RunningInfo,
140 _event: PullRequestUpdatedEvent,
141 ) {
142 }
143
144 async fn on_pull_request_closed(
145 &self,
146 _repo: Repository,
147 _running_info: RunningInfo,
148 _pull_request_id: usize,
149 ) {
150 }
151
152 async fn on_comment_created(
153 &self,
154 _repo: Repository,
155 _running_info: RunningInfo,
156 _event: CommentCreatedEvent,
157 ) {
158 }
159
160 async fn on_comment_updated(
161 &self,
162 _repo: Repository,
163 _running_info: RunningInfo,
164 _event: CommentUpdatedEvent,
165 ) {
166 }
167
168 async fn on_comment_deleted(
169 &self,
170 _repo: Repository,
171 _running_info: RunningInfo,
172 _comment_id: usize,
173 ) {
174 }
175}
176
177pub struct Dispatcher<T: Bot> {
178 core: T,
179}
180
181impl<T: Bot> Dispatcher<T> {
182 pub fn new(core: T) -> Self {
183 Dispatcher { core }
184 }
185
186 pub async fn dispatch_event(&self, event: serde_json::Value) {
187 let event_name: &str = event["event_name"].as_str().unwrap();
188 match event_name {
189 "issues" if event["event"]["issue"].get("pull_request").is_some() => {
190 self.dispatch_pull_request_event(event).await
191 }
192 "pull_request" => self.dispatch_pull_request_event(event).await,
193 "issues" => self.dispatch_issues_event(event).await,
194 "issue_comment" => self.dispatch_issue_comment_event(event).await,
195 _ => unimplemented!(),
196 }
197 }
198
199 async fn dispatch_issues_event(&self, event: serde_json::Value) {
200 let event_action: &str = event["event"]["action"].as_str().unwrap();
201 let repo = Self::extract_repo_info(&event);
202 let running_info = Self::extract_running_info(&event);
203 match event_action {
204 "opened" => {
205 let inner_event = IssueCreatedEvent {
206 id: event["event"]["issue"]["number"].as_u64().unwrap() as _,
207 title: event["event"]["issue"]["title"]
208 .as_str()
209 .unwrap()
210 .to_string(),
211 body: event["event"]["issue"]["body"]
212 .as_str()
213 .unwrap()
214 .to_string(),
215 user: event["event"]["issue"]["user"]["login"]
216 .as_str()
217 .unwrap()
218 .to_string(),
219 };
220 self.core
221 .on_issue_created(repo, running_info, inner_event)
222 .await;
223 }
224 "closed" => {
225 let id = event["event"]["issue"]["number"].as_u64().unwrap() as usize;
226 self.core.on_issue_closed(repo, running_info, id).await;
227 }
228 "updated" => {
229 let updated_part = if event["event"]["changes"].get("body").is_some() {
230 UpdatedPart::Body {
231 from: event["event"]["changes"]["body"]["from"]
232 .as_str()
233 .unwrap()
234 .to_string(),
235 to: event["event"]["issue"]["body"]
236 .as_str()
237 .unwrap()
238 .to_string(),
239 }
240 } else {
241 UpdatedPart::Title {
242 from: event["event"]["changed"]["body"]["from"]
243 .as_str()
244 .unwrap()
245 .to_string(),
246 to: event["event"]["issue"]["title"]
247 .as_str()
248 .unwrap()
249 .to_string(),
250 }
251 };
252 let inner_event = IssueUpdatedEvent {
253 id: event["event"]["issue"]["number"].as_u64().unwrap() as usize,
254 updated_part,
255 user: event["event"]["issue"]["user"]["login"]
256 .as_str()
257 .unwrap()
258 .to_string(),
259 };
260 self.core
261 .on_issue_updated(repo, running_info, inner_event)
262 .await;
263 }
264 "reopened" => {
265 let inner_event = IssueReopenedEvent {
266 id: event["event"]["issue"]["number"].as_u64().unwrap() as _,
267 title: event["event"]["issue"]["title"]
268 .as_str()
269 .unwrap()
270 .to_string(),
271 body: event["event"]["issue"]["body"]
272 .as_str()
273 .unwrap()
274 .to_string(),
275 user: event["event"]["issue"]["user"]["login"]
276 .as_str()
277 .unwrap()
278 .to_string(),
279 };
280 self.core
281 .on_issue_reopened(repo, running_info, inner_event)
282 .await;
283 }
284 _ => unimplemented!(),
285 }
286 }
287
288 async fn dispatch_pull_request_event(&self, event: serde_json::Value) {
289 let repo = Self::extract_repo_info(&event);
290 let event_action: &str = event["event"]["action"].as_str().unwrap();
291 let running_info = Self::extract_running_info(&event);
292 match event_action {
293 "opened" => {
294 let inner_event = PullRequestCreatedEvent {
295 id: event["event"]["pull_request"]["number"].as_u64().unwrap() as _,
296 title: event["event"]["pull_request"]["title"]
297 .as_str()
298 .unwrap()
299 .to_string(),
300 body: event["event"]["pull_request"]["body"]
301 .as_str()
302 .unwrap()
303 .to_string(),
304 user: event["event"]["pull_request"]["user"]["login"]
305 .as_str()
306 .unwrap()
307 .to_string(),
308 from_repo: Repository {
309 owner: event["event"]["pull_request"]["head"]["user"]["login"]
310 .as_str()
311 .unwrap()
312 .to_string(),
313 name: event["event"]["pull_request"]["head"]["repo"]["name"]
314 .as_str()
315 .unwrap()
316 .to_string(),
317 },
318 from_ref: event["head_ref"].as_str().unwrap().to_string(),
319 to_ref: event["base_ref"].as_str().unwrap().to_string(),
320 };
321 self.core
322 .on_pull_request_created(repo, running_info, inner_event)
323 .await;
324 }
325 "closed" => {
326 let id = event["event"]["issue"]["number"].as_u64().unwrap() as usize;
327 self.core
328 .on_pull_request_closed(repo, running_info, id)
329 .await;
330 }
331 "edited" => {
332 let updated_part = if event["event"]["changes"].get("body").is_some() {
333 UpdatedPart::Body {
334 from: event["event"]["changes"]["body"]["from"]
335 .as_str()
336 .unwrap()
337 .to_string(),
338 to: event["event"]["pull_request"]["body"]
339 .as_str()
340 .unwrap()
341 .to_string(),
342 }
343 } else {
344 UpdatedPart::Title {
345 from: event["event"]["changed"]["body"]["from"]
346 .as_str()
347 .unwrap()
348 .to_string(),
349 to: event["event"]["pull_request"]["title"]
350 .as_str()
351 .unwrap()
352 .to_string(),
353 }
354 };
355 let inner_event = PullRequestUpdatedEvent {
356 id: event["event"]["issue"]["number"].as_u64().unwrap() as usize,
357 updated_part,
358 user: event["event"]["issue"]["user"]["login"]
359 .as_str()
360 .unwrap()
361 .to_string(),
362 };
363 self.core
364 .on_pull_request_updated(repo, running_info, inner_event)
365 .await;
366 }
367 _ => unimplemented!(),
368 }
369 }
370
371 async fn dispatch_issue_comment_event(&self, event: serde_json::Value) {
372 let repo = Self::extract_repo_info(&event);
373 let event_action: &str = event["event"]["action"].as_str().unwrap();
374 let running_info = Self::extract_running_info(&event);
375 let target = if event["event"]["issue"].get("pull_request").is_some() {
376 CommentTarget::PullRequest(event["event"]["issue"]["number"].as_u64().unwrap() as _)
377 } else {
378 CommentTarget::Issue(event["event"]["issue"]["number"].as_u64().unwrap() as _)
379 };
380 match event_action {
381 "created" => {
382 let inner_event = CommentCreatedEvent {
383 id: event["event"]["comment"]["id"].as_u64().unwrap() as usize,
384 user: event["event"]["comment"]["user"]["login"]
385 .as_str()
386 .unwrap()
387 .to_string(),
388 target,
389 body: event["event"]["comment"]["body"]
390 .as_str()
391 .unwrap()
392 .to_string(),
393 };
394 self.core
395 .on_comment_created(repo, running_info, inner_event)
396 .await;
397 }
398 "deleted" => {
399 let id = event["event"]["comment"]["id"].as_u64().unwrap() as usize;
400 self.core.on_comment_deleted(repo, running_info, id).await;
401 }
402 "edited" => {
403 let inner_event = CommentUpdatedEvent {
404 id: event["event"]["comment"]["id"].as_u64().unwrap() as usize,
405 user: event["event"]["comment"]["user"]["login"]
406 .as_str()
407 .unwrap()
408 .to_string(),
409 target,
410 from: event["event"]["changes"]["from"]
411 .as_str()
412 .unwrap()
413 .to_string(),
414 to: event["event"]["comment"]["body"]
415 .as_str()
416 .unwrap()
417 .to_string(),
418 };
419 self.core
420 .on_comment_updated(repo, running_info, inner_event)
421 .await;
422 }
423 _ => unimplemented!(),
424 }
425 }
426
427 fn extract_repo_info(event: &serde_json::Value) -> Repository {
428 Repository {
429 owner: event["event"]["repository"]["owner"]["login"]
430 .as_str()
431 .unwrap()
432 .to_string(),
433 name: event["repository"]
434 .as_str()
435 .unwrap()
436 .split('/')
437 .nth(1)
438 .unwrap()
439 .to_string(),
440 }
441 }
442
443 fn extract_running_info(event: &serde_json::Value) -> RunningInfo {
444 RunningInfo {
445 run_id: usize::from_str_radix(event["run_id"].as_str().unwrap(), 10).unwrap(),
446 run_number: usize::from_str_radix(event["run_number"].as_str().unwrap(), 10).unwrap(),
447 }
448 }
449}