Procházet zdrojové kódy

Merge pull request #9 from rust-lang/orgwide

Add per-repo config and prepare for org-wide webhook
Pietro Albini před 6 roky
rodič
revize
0f09c952a2
10 změnil soubory, kde provedl 274 přidání a 166 odebrání
  1. 17 0
      Cargo.lock
  2. 2 0
      Cargo.toml
  3. 6 6
      parser/src/command.rs
  4. 4 4
      parser/src/command/relabel.rs
  5. 61 0
      src/config.rs
  6. 59 1
      src/github.rs
  7. 48 17
      src/handlers.rs
  8. 52 56
      src/handlers/relabel.rs
  9. 25 37
      src/main.rs
  10. 0 45
      src/registry.rs

+ 17 - 0
Cargo.lock

@@ -387,6 +387,11 @@ dependencies = [
  "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "h2"
 version = "0.1.16"
@@ -1504,6 +1509,14 @@ dependencies = [
  "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "traitobject"
 version = "0.1.0"
@@ -1516,6 +1529,7 @@ dependencies = [
  "dotenv 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1528,6 +1542,7 @@ dependencies = [
  "rust_team_data 1.0.0 (git+https://github.com/rust-lang/team)",
  "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
+ "toml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -1771,6 +1786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
 "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4"
 "checksum getopts 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0a7292d30132fb5424b354f5dc02512a86e4c516fe544bb7a25e7f266951b797"
+"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 "checksum h2 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "ddb2b25a33e231484694267af28fec74ac63b5ccf51ee2065a5e313b834d836e"
 "checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77"
 "checksum http 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "1a10e5b573b9a0146545010f50772b9e8b1dd0a256564cc4307694c68832a2f5"
@@ -1887,6 +1903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum tokio-threadpool 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c3fd86cb15547d02daa2b21aadaf4e37dee3368df38a526178a5afa3c034d2fb"
 "checksum tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2910970404ba6fa78c5539126a9ae2045d62e3713041e447f695f41405a120c6"
 "checksum toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f"
+"checksum toml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "87c5890a989fa47ecdc7bcb4c63a77a82c18f306714104b1decfd722db17b39e"
 "checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079"
 "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"
 "checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887"

+ 2 - 0
Cargo.toml

@@ -21,6 +21,8 @@ hex = "0.3.2"
 env_logger = "0.6"
 parser = { path = "parser" }
 rust_team_data = { git = "https://github.com/rust-lang/team" }
+glob = "0.3.0"
+toml = "0.5.0"
 
 [dependencies.serde]
 version = "1"

+ 6 - 6
parser/src/command.rs

@@ -2,7 +2,7 @@ use crate::code_block::ColorCodeBlocks;
 use crate::error::Error;
 use crate::token::{Token, Tokenizer};
 
-pub mod label;
+pub mod relabel;
 
 pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> {
     input.find(&format!("@{}", bot))
@@ -10,7 +10,7 @@ pub fn find_commmand_start(input: &str, bot: &str) -> Option<usize> {
 
 #[derive(Debug)]
 pub enum Command<'a> {
-    Label(Result<label::LabelCommand, Error<'a>>),
+    Relabel(Result<relabel::RelabelCommand, Error<'a>>),
     None,
 }
 
@@ -50,14 +50,14 @@ impl<'a> Input<'a> {
 
         {
             let mut tok = original_tokenizer.clone();
-            let res = label::LabelCommand::parse(&mut tok);
+            let res = relabel::RelabelCommand::parse(&mut tok);
             match res {
                 Ok(None) => {}
                 Ok(Some(cmd)) => {
-                    success.push((tok, Command::Label(Ok(cmd))));
+                    success.push((tok, Command::Relabel(Ok(cmd))));
                 }
                 Err(err) => {
-                    success.push((tok, Command::Label(Err(err))));
+                    success.push((tok, Command::Relabel(Err(err))));
                 }
             }
         }
@@ -94,7 +94,7 @@ impl<'a> Input<'a> {
 impl<'a> Command<'a> {
     pub fn is_ok(&self) -> bool {
         match self {
-            Command::Label(r) => r.is_ok(),
+            Command::Relabel(r) => r.is_ok(),
             Command::None => true,
         }
     }

+ 4 - 4
parser/src/command/label.rs → parser/src/command/relabel.rs

@@ -30,7 +30,7 @@ use std::error::Error as _;
 use std::fmt;
 
 #[derive(Debug)]
-pub struct LabelCommand(pub Vec<LabelDelta>);
+pub struct RelabelCommand(pub Vec<LabelDelta>);
 
 #[derive(Debug, PartialEq, Eq)]
 pub enum LabelDelta {
@@ -124,7 +124,7 @@ fn delta_empty() {
     assert_eq!(err.position(), 1);
 }
 
-impl LabelCommand {
+impl RelabelCommand {
     pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
         let mut toks = input.clone();
         if let Some(Token::Word("modify")) = toks.next_token()? {
@@ -163,7 +163,7 @@ impl LabelCommand {
             if let Some(Token::Dot) | Some(Token::EndOfLine) = toks.peek_token()? {
                 toks.next_token()?;
                 *input = toks;
-                return Ok(Some(LabelCommand(deltas)));
+                return Ok(Some(RelabelCommand(deltas)));
             }
         }
     }
@@ -172,7 +172,7 @@ impl LabelCommand {
 #[cfg(test)]
 fn parse<'a>(input: &'a str) -> Result<Option<Vec<LabelDelta>>, Error<'a>> {
     let mut toks = Tokenizer::new(input);
-    Ok(LabelCommand::parse(&mut toks)?.map(|c| c.0))
+    Ok(RelabelCommand::parse(&mut toks)?.map(|c| c.0))
 }
 
 #[test]

+ 61 - 0
src/config.rs

@@ -0,0 +1,61 @@
+use crate::github::GithubClient;
+use failure::Error;
+use std::collections::HashMap;
+use std::sync::{Arc, RwLock};
+use std::time::{Duration, Instant};
+
+static CONFIG_FILE_NAME: &str = "triagebot.toml";
+const REFRESH_EVERY: Duration = Duration::from_secs(2 * 60); // Every two minutes
+
+lazy_static::lazy_static! {
+    static ref CONFIG_CACHE: RwLock<HashMap<String, (Arc<Config>, Instant)>> =
+        RwLock::new(HashMap::new());
+}
+
+#[derive(serde::Deserialize)]
+pub(crate) struct Config {
+    pub(crate) relabel: Option<RelabelConfig>,
+}
+
+#[derive(serde::Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub(crate) struct RelabelConfig {
+    #[serde(default)]
+    pub(crate) allow_unauthenticated: Vec<String>,
+}
+
+pub(crate) fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Error> {
+    if let Some(config) = get_cached_config(repo) {
+        Ok(config)
+    } else {
+        get_fresh_config(gh, repo)
+    }
+}
+
+fn get_cached_config(repo: &str) -> Option<Arc<Config>> {
+    let cache = CONFIG_CACHE.read().unwrap();
+    cache.get(repo).and_then(|(config, fetch_time)| {
+        if fetch_time.elapsed() < REFRESH_EVERY {
+            Some(config.clone())
+        } else {
+            None
+        }
+    })
+}
+
+fn get_fresh_config(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Error> {
+    let contents = gh
+        .raw_file(repo, "master", CONFIG_FILE_NAME)?
+        .ok_or_else(|| {
+            failure::err_msg(
+                "This repository is not enabled to use triagebot.\n\
+                 Add a `triagebot.toml` in the root of the master branch to enable it.",
+            )
+        })?;
+    let config = Arc::new(toml::from_slice::<Config>(&contents)?);
+    CONFIG_CACHE
+        .write()
+        .unwrap()
+        .insert(repo.to_string(), (config.clone(), Instant::now()));
+    Ok(config)
+}

+ 59 - 1
src/github.rs

@@ -1,6 +1,7 @@
 use failure::{Error, ResultExt};
 use reqwest::header::{AUTHORIZATION, USER_AGENT};
-use reqwest::{Client, Error as HttpError, RequestBuilder, Response};
+use reqwest::{Client, Error as HttpError, RequestBuilder, Response, StatusCode};
+use std::io::Read;
 
 #[derive(Debug, serde::Deserialize)]
 pub struct User {
@@ -149,6 +150,46 @@ impl Issue {
     }
 }
 
+#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum IssueCommentAction {
+    Created,
+    Edited,
+    Deleted,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct IssueCommentEvent {
+    pub action: IssueCommentAction,
+    pub issue: Issue,
+    pub comment: Comment,
+    pub repository: Repository,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct Repository {
+    pub full_name: String,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    IssueComment(IssueCommentEvent),
+}
+
+impl Event {
+    pub fn repo_name(&self) -> &str {
+        match self {
+            Event::IssueComment(event) => &event.repository.full_name,
+        }
+    }
+
+    pub fn issue(&self) -> Option<&Issue> {
+        match self {
+            Event::IssueComment(event) => Some(&event.issue),
+        }
+    }
+}
+
 trait RequestSend: Sized {
     fn configure(self, g: &GithubClient) -> Self;
     fn send_req(self) -> Result<Response, HttpError>;
@@ -183,6 +224,23 @@ impl GithubClient {
         &self.client
     }
 
+    pub fn raw_file(&self, repo: &str, branch: &str, path: &str) -> Result<Option<Vec<u8>>, Error> {
+        let url = format!(
+            "https://raw.githubusercontent.com/{}/{}/{}",
+            repo, branch, path
+        );
+        let mut resp = self.get(&url).send()?;
+        match resp.status() {
+            StatusCode::OK => {
+                let mut buf = Vec::with_capacity(resp.content_length().unwrap_or(4) as usize);
+                resp.read_to_end(&mut buf)?;
+                Ok(Some(buf))
+            }
+            StatusCode::NOT_FOUND => Ok(None),
+            status => failure::bail!("failed to GET {}: {}", url, status),
+        }
+    }
+
     fn get(&self, url: &str) -> RequestBuilder {
         log::trace!("get {:?}", url);
         self.client.get(url).configure(self)

+ 48 - 17
src/handlers.rs

@@ -1,20 +1,51 @@
-use crate::github::GithubClient;
-use crate::registry::HandleRegistry;
-use std::sync::Arc;
+use crate::github::{Event, GithubClient};
+use failure::Error;
 
-//mod assign;
-mod label;
-//mod tracking_issue;
+macro_rules! handlers {
+    ($($name:ident = $handler:expr,)*) => {
+        $(mod $name;)*
 
-pub fn register_all(registry: &mut HandleRegistry, client: GithubClient, username: Arc<String>) {
-    registry.register(label::LabelHandler {
-        client: client.clone(),
-        username: username.clone(),
-    });
-    //registry.register(assign::AssignmentHandler {
-    //    client: client.clone(),
-    //});
-    //registry.register(tracking_issue::TrackingIssueHandler {
-    //    client: client.clone(),
-    //});
+        pub fn handle(ctx: &Context, event: &Event) -> Result<(), Error> {
+            $(if let Some(input) = Handler::parse_input(&$handler, ctx, event)? {
+                let config = crate::config::get(&ctx.github, event.repo_name())?;
+                if let Some(config) = &config.$name {
+                    Handler::handle_input(&$handler, ctx, config, event, input)?;
+                } else {
+                    failure::bail!(
+                        "The feature `{}` is not enabled in this repository.\n\
+                         To enable it add its section in the `triagebot.toml` \
+                         in the root of the repository.",
+                        stringify!($name)
+                    );
+                }
+            })*
+            Ok(())
+        }
+    }
+}
+
+handlers! {
+    //assign = assign::AssignmentHandler,
+    relabel = relabel::RelabelHandler,
+    //tracking_issue = tracking_issue::TrackingIssueHandler,
+}
+
+pub struct Context {
+    pub github: GithubClient,
+    pub username: String,
+}
+
+pub trait Handler: Sync + Send {
+    type Input;
+    type Config;
+
+    fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, Error>;
+
+    fn handle_input(
+        &self,
+        ctx: &Context,
+        config: &Self::Config,
+        event: &Event,
+        input: Self::Input,
+    ) -> Result<(), Error>;
 }

+ 52 - 56
src/handlers/label.rs → src/handlers/relabel.rs

@@ -3,64 +3,70 @@
 //! Labels are checked against the labels in the project; the bot does not support creating new
 //! labels.
 //!
-//! Parsing is done in the `parser::command::label` module.
+//! Parsing is done in the `parser::command::relabel` module.
 //!
 //! If the command was successful, there will be no feedback beyond the label change to reduce
 //! notification noise.
 
 use crate::{
-    github::{self, GithubClient},
+    config::RelabelConfig,
+    github::{self, Event, GithubClient},
+    handlers::{Context, Handler},
     interactions::ErrorComment,
-    registry::{Event, Handler},
 };
 use failure::Error;
-use parser::command::label::{LabelCommand, LabelDelta};
+use parser::command::relabel::{RelabelCommand, LabelDelta};
 use parser::command::{Command, Input};
-use std::sync::Arc;
 
-pub struct LabelHandler {
-    pub client: GithubClient,
-    pub username: Arc<String>,
-}
+pub(super) struct RelabelHandler;
+
+impl Handler for RelabelHandler {
+    type Input = RelabelCommand;
+    type Config = RelabelConfig;
 
-impl Handler for LabelHandler {
-    fn handle_event(&self, event: &Event) -> Result<(), Error> {
+    fn parse_input(&self, ctx: &Context, event: &Event) -> Result<Option<Self::Input>, Error> {
         #[allow(irrefutable_let_patterns)]
         let event = if let Event::IssueComment(e) = event {
             e
         } else {
             // not interested in other events
-            return Ok(());
+            return Ok(None);
         };
 
-        let mut issue_labels = event.issue.labels().to_owned();
-
-        let mut input = Input::new(&event.comment.body, &self.username);
-        let deltas = match input.parse_command() {
-            Command::Label(Ok(LabelCommand(deltas))) => deltas,
-            Command::Label(Err(err)) => {
-                ErrorComment::new(
-                    &event.issue,
-                    format!(
-                        "Parsing label command in [comment]({}) failed: {}",
-                        event.comment.html_url, err
-                    ),
-                )
-                .post(&self.client)?;
+        let mut input = Input::new(&event.comment.body, &ctx.username);
+        match input.parse_command() {
+            Command::Relabel(Ok(command)) => Ok(Some(command)),
+            Command::Relabel(Err(err)) => {
                 failure::bail!(
-                    "label parsing failed for issue #{}, error: {:?}",
-                    event.issue.number,
-                    err
+                    "Parsing label command in [comment]({}) failed: {}",
+                    event.comment.html_url, err
                 );
             }
-            _ => return Ok(()),
+            _ => Ok(None),
+        }
+    }
+
+    fn handle_input(
+        &self,
+        ctx: &Context,
+        config: &RelabelConfig,
+        event: &Event,
+        input: RelabelCommand,
+    ) -> Result<(), Error> {
+        #[allow(irrefutable_let_patterns)]
+        let event = if let Event::IssueComment(e) = event {
+            e
+        } else {
+            // not interested in other events
+            return Ok(());
         };
 
+        let mut issue_labels = event.issue.labels().to_owned();
         let mut changed = false;
-        for delta in &deltas {
+        for delta in &input.0 {
             let name = delta.label().as_str();
-            if let Err(msg) = check_filter(name, &event.comment.user, &self.client) {
-                ErrorComment::new(&event.issue, msg).post(&self.client)?;
+            if let Err(msg) = check_filter(name, config, &event.comment.user, &ctx.github) {
+                ErrorComment::new(&event.issue, msg.to_string()).post(&ctx.github)?;
                 return Ok(());
             }
             match delta {
@@ -82,14 +88,19 @@ impl Handler for LabelHandler {
         }
 
         if changed {
-            event.issue.set_labels(&self.client, issue_labels)?;
+            event.issue.set_labels(&ctx.github, issue_labels)?;
         }
 
         Ok(())
     }
 }
 
-fn check_filter(label: &str, user: &github::User, client: &GithubClient) -> Result<(), String> {
+fn check_filter(
+    label: &str,
+    config: &RelabelConfig,
+    user: &github::User,
+    client: &GithubClient,
+) -> Result<(), Error> {
     let is_team_member;
     match user.is_team_member(client) {
         Ok(true) => return Ok(()),
@@ -102,34 +113,19 @@ fn check_filter(label: &str, user: &github::User, client: &GithubClient) -> Resu
             // continue on; if we failed to check their membership assume that they are not members.
         }
     }
-    if label.starts_with("C-") // categories
-    || label.starts_with("A-") // areas
-    || label.starts_with("E-") // easy, mentor, etc.
-    || label.starts_with("NLL-")
-    || label.starts_with("O-") // operating systems
-    || label.starts_with("S-") // status labels
-    || label.starts_with("T-")
-    || label.starts_with("WG-")
-    {
-        return Ok(());
-    }
-    match label {
-        "I-compilemem" | "I-compiletime" | "I-crash" | "I-hang" | "I-ICE" | "I-slow" => {
+    for pattern in &config.allow_unauthenticated {
+        let pattern = glob::Pattern::new(pattern)?;
+        if pattern.matches(label) {
             return Ok(());
         }
-        _ => {}
     }
-
     if is_team_member.is_ok() {
-        Err(format!(
-            "Label {} can only be set by Rust team members",
-            label
-        ))
+        failure::bail!("Label {} can only be set by Rust team members", label);
     } else {
-        Err(format!(
+        failure::bail!(
             "Label {} can only be set by Rust team members;\
              we were unable to check if you are a team member.",
             label
-        ))
+        );
     }
 }

+ 25 - 37
src/main.rs

@@ -10,41 +10,23 @@ use rocket::request;
 use rocket::State;
 use rocket::{http::Status, Outcome, Request};
 use std::env;
-use std::sync::Arc;
-
-mod handlers;
-mod registry;
 
+mod config;
 mod github;
+mod handlers;
 mod interactions;
 mod payload;
 mod team;
 
-use github::{Comment, GithubClient, Issue, User};
+use interactions::ErrorComment;
 use payload::SignedPayload;
-use registry::HandleRegistry;
-
-#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum IssueCommentAction {
-    Created,
-    Edited,
-    Deleted,
-}
-
-#[derive(Debug, serde::Deserialize)]
-pub struct IssueCommentEvent {
-    action: IssueCommentAction,
-    issue: Issue,
-    comment: Comment,
-}
 
-enum Event {
+enum EventName {
     IssueComment,
     Other,
 }
 
-impl<'a, 'r> request::FromRequest<'a, 'r> for Event {
+impl<'a, 'r> request::FromRequest<'a, 'r> for EventName {
     type Error = String;
     fn from_request(req: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
         let ev = if let Some(ev) = req.headers().get_one("X-GitHub-Event") {
@@ -53,8 +35,8 @@ impl<'a, 'r> request::FromRequest<'a, 'r> for Event {
             return Outcome::Failure((Status::BadRequest, "Needs a X-GitHub-Event".into()));
         };
         let ev = match ev {
-            "issue_comment" => Event::IssueComment,
-            _ => Event::Other,
+            "issue_comment" => EventName::IssueComment,
+            _ => EventName::Other,
         };
         Outcome::Success(ev)
     }
@@ -82,22 +64,27 @@ impl From<Error> for WebhookError {
 
 #[post("/github-hook", data = "<payload>")]
 fn webhook(
-    event: Event,
+    event: EventName,
     payload: SignedPayload,
-    reg: State<HandleRegistry>,
+    ctx: State<handlers::Context>,
 ) -> Result<(), WebhookError> {
     match event {
-        Event::IssueComment => {
+        EventName::IssueComment => {
             let payload = payload
-                .deserialize::<IssueCommentEvent>()
+                .deserialize::<github::IssueCommentEvent>()
                 .context("IssueCommentEvent failed to deserialize")
                 .map_err(Error::from)?;
 
-            let event = registry::Event::IssueComment(payload);
-            reg.handle(&event).map_err(Error::from)?;
+            let event = github::Event::IssueComment(payload);
+            if let Err(err) = handlers::handle(&ctx, &event) {
+                if let Some(issue) = event.issue() {
+                    ErrorComment::new(issue, err.to_string()).post(&ctx.github)?;
+                }
+                return Err(err.into());
+            }
         }
         // Other events need not be handled
-        Event::Other => {}
+        EventName::Other => {}
     }
     Ok(())
 }
@@ -110,13 +97,14 @@ fn not_found(_: &Request) -> &'static str {
 fn main() {
     dotenv::dotenv().ok();
     let client = Client::new();
-    let gh = GithubClient::new(
+    let gh = github::GithubClient::new(
         client.clone(),
         env::var("GITHUB_API_TOKEN").expect("Missing GITHUB_API_TOKEN"),
     );
-    let username = Arc::new(User::current(&gh).unwrap().login);
-    let mut registry = HandleRegistry::new();
-    handlers::register_all(&mut registry, gh.clone(), username);
+    let ctx = handlers::Context {
+        github: gh.clone(),
+        username: github::User::current(&gh).unwrap().login,
+    };
 
     let mut config = rocket::Config::active().unwrap();
     config.set_port(
@@ -126,7 +114,7 @@ fn main() {
     );
     rocket::custom(config)
         .manage(gh)
-        .manage(registry)
+        .manage(ctx)
         .mount("/", routes![webhook])
         .register(catchers![not_found])
         .launch();

+ 0 - 45
src/registry.rs

@@ -1,45 +0,0 @@
-use crate::IssueCommentEvent;
-use failure::Error;
-
-pub struct HandleRegistry {
-    handlers: Vec<Box<dyn Handler>>,
-}
-
-impl HandleRegistry {
-    pub fn new() -> HandleRegistry {
-        HandleRegistry {
-            handlers: Vec::new(),
-        }
-    }
-
-    pub fn register<H: Handler + 'static>(&mut self, h: H) {
-        self.handlers.push(Box::new(h));
-    }
-
-    pub fn handle(&self, event: &Event) -> Result<(), Error> {
-        let mut last_error = None;
-        for h in &self.handlers {
-            match h.handle_event(event) {
-                Ok(()) => {}
-                Err(e) => {
-                    eprintln!("event handling failed: {:?}", e);
-                    last_error = Some(e);
-                }
-            }
-        }
-        if let Some(err) = last_error {
-            Err(err)
-        } else {
-            Ok(())
-        }
-    }
-}
-
-#[derive(Debug)]
-pub enum Event {
-    IssueComment(IssueCommentEvent),
-}
-
-pub trait Handler: Sync + Send {
-    fn handle_event(&self, event: &Event) -> Result<(), Error>;
-}