tide_github/lib.rs
1#![deny(missing_docs)]
2//!
3//! Process Github webhooks in [tide](https://github.com/http-rs/tide).
4//!
5//! ## Example
6//!
7//! ```Rust
8//! #[async_std::main]
9//! async fn main() -> tide::Result<()> {
10//! let mut app = tide::new();
11//! let github = tide_github::new("My Github webhook s3cr#t")
12//! .on(Event::IssueComment, |payload| {
13//! println!("Received a payload for repository {}", payload.repository.name);
14//! })
15//! .build();
16//! app.at("/gh_webhooks").nest(github);
17//! app.listen("127.0.0.1:3000").await?;
18//! Ok(())
19//! }
20//!
21//! ```
22//!
23//! The API is still in development and may change in unexpected ways.
24use async_trait::async_trait;
25use std::collections::HashMap;
26use tide::{prelude::*, Request, StatusCode};
27use std::sync::Arc;
28
29mod middleware;
30/// The payload module contains the types and conversions for the webhook payloads.
31pub mod payload;
32use payload::Payload;
33
34/// Returns a [`ServerBuilder`] with the given webhook secret.
35///
36/// Call [`Self::on()`](on@ServerBuilder) to register closures to be run when the given event is
37/// received and [`Self::build()`](build@ServerBuilder) to retrieve the final [`tide::Server`].
38pub fn new<S: Into<String>>(webhook_secret: S) -> ServerBuilder {
39 ServerBuilder::new(webhook_secret.into())
40}
41
42type HandlerMap = HashMap<
43 Event,
44 // TODO: Create a nice type alias for the Event Handler
45 Arc<dyn Send + Sync + 'static + Fn(Payload)>,
46>;
47
48/// [`ServerBuilder`] is used to first register closures to events before finally building a
49/// [`tide::Server`] using those closures.
50pub struct ServerBuilder {
51 webhook_secret: String,
52 handlers: HandlerMap,
53}
54
55impl ServerBuilder {
56 fn new(webhook_secret: String) -> Self {
57 ServerBuilder {
58 webhook_secret,
59 handlers: HashMap::new(),
60 }
61 }
62
63 /// Registers the given event handler to be run when the given event is received.
64 ///
65 /// The event handler receives a [`Payload`] as the single argument. Since webhooks are
66 /// generally passively consumed (Github will not meaningfully (to us) process our response),
67 /// the handler returns only a `()`. As far as the event dispatcher is concerned, all the
68 /// meaningful work will be done as side-effects of the closures you register here.
69 ///
70 /// The types involved here are not stable yet due to ongoing API development.
71 ///
72 /// ## Example
73 ///
74 /// ```Rust
75 /// let github = tide_github::new("my webhook s3ct#t")
76 /// .on(Event::IssueComment, |payload| {
77 /// println!("Got payload for repository {}", payload.repository.name)
78 /// });
79 /// ```
80 pub fn on<E: Into<Event>>(
81 mut self,
82 event: E,
83 handler: impl Fn(Payload)
84 + Send
85 + Sync
86 + 'static,
87 ) -> Self {
88 let event: Event = event.into();
89 self.handlers.insert(event, Arc::new(handler));
90 self
91 }
92
93 /// Build a [`tide::Server`] using the registered events.
94 ///
95 /// Since the API is still in development, in the future we might instead (or additionally)
96 /// expose the `EventHandlerDispatcher` directly.
97 pub fn build(self) -> tide::Server<()> {
98 let mut server = tide::new();
99 let dispatcher = Box::new(EventHandlerDispatcher::new(self.handlers));
100 server.with(middleware::WebhookVerification::new(self.webhook_secret));
101 server
102 .at("/")
103 .post(dispatcher as Box<dyn tide::Endpoint<()>>);
104 server
105 }
106}
107
108/// This enum represents the event (and its variants the event type) for which we can receive a
109/// Github webhook.
110///
111/// Github sends the type of the event (and thus of the payload) as the `X-github-Event` header
112/// that we parse into an `Event` by implementing [`::std::str::FromStr`] for it.
113#[non_exhaustive]
114#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
115pub enum Event {
116 /// The Github
117 /// [`IssueCommentEvent`](https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types#issuecommentevent) event.
118 IssueComment,
119}
120
121use std::fmt;
122impl fmt::Display for Event {
123 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
124 match self {
125 Self::IssueComment => write!(f, "issue_comment"),
126 }
127 }
128}
129
130impl ::std::str::FromStr for Event {
131 type Err = EventDispatchError;
132
133 fn from_str(event: &str) -> Result<Event, Self::Err> {
134 use self::Event::*;
135
136 // TODO: Generate this from a derive macro on `Event`
137 match event {
138 "issue_comment" => Ok(IssueComment),
139 event => {
140 log::warn!("Unsupported event: {}", event);
141 Err(EventDispatchError::UnsupportedEvent)
142 },
143 }
144 }
145}
146
147/// The variants of [`EventDispatchError`] represent the errors that would prevent us from calling
148/// the handler to process the Github Webhook.
149#[derive(thiserror::Error, Clone, Debug)]
150pub enum EventDispatchError {
151 /// Github send us a webhook for an [`Event`] that we don't support.
152 #[error("Received an Event of an unsupported type")]
153 UnsupportedEvent,
154 /// We're processing something that does not seem to be a Github webhook.
155 #[error("No `X-Github-Event` header found")]
156 MissingEventHeader,
157 /// No handler was registered for the event we received.
158 #[error("No handler registered for Event '{0}'")]
159 MissingHandlerForEvent(Event),
160}
161
162struct EventHandlerDispatcher {
163 handlers: HandlerMap,
164}
165
166impl EventHandlerDispatcher {
167 fn new(handlers: HandlerMap) -> Self {
168 EventHandlerDispatcher { handlers }
169 }
170}
171
172#[async_trait]
173impl tide::Endpoint<()> for EventHandlerDispatcher
174where
175 EventHandlerDispatcher: Send + Sync,
176{
177 async fn call(&self, mut req: Request<()>) -> tide::Result {
178 use std::str::FromStr;
179 use async_std::task;
180
181 let event_header = req
182 .header("X-Github-Event")
183 .ok_or(EventDispatchError::MissingEventHeader)
184 .status(StatusCode::BadRequest)?.as_str();
185
186 let event = Event::from_str(event_header).status(StatusCode::NotImplemented)?;
187 let payload: payload::Payload = req.body_json().await?;
188 let handler = self
189 .handlers
190 .get(&event)
191 .ok_or_else(|| { println!("Missing Handler for Event {:?}", event); EventDispatchError::MissingHandlerForEvent(event)})
192 .status(StatusCode::NotImplemented)?;
193
194 let handler = handler.clone();
195
196 task::spawn_blocking(move || {handler(payload)});
197
198 Ok("".into())
199 }
200}