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}