main.rs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #![allow(clippy::new_without_default)]
  2. use anyhow::Context as _;
  3. use futures::{future::FutureExt, stream::StreamExt};
  4. use hyper::{header, Body, Request, Response, Server, StatusCode};
  5. use reqwest::Client;
  6. use std::{env, net::SocketAddr, sync::Arc};
  7. use triagebot::{db, github, handlers::Context, notification_listing, payload, EventName};
  8. use uuid::Uuid;
  9. mod logger;
  10. async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Body>, hyper::Error> {
  11. log::info!("request = {:?}", req);
  12. let (req, body_stream) = req.into_parts();
  13. if req.uri.path() == "/" {
  14. return Ok(Response::builder()
  15. .status(StatusCode::OK)
  16. .body(Body::from("Triagebot is awaiting triage."))
  17. .unwrap());
  18. }
  19. if req.uri.path() == "/notifications" {
  20. if let Some(query) = req.uri.query() {
  21. let user = url::form_urlencoded::parse(query.as_bytes()).find(|(k, _)| k == "user");
  22. if let Some((_, name)) = user {
  23. return Ok(Response::builder()
  24. .status(StatusCode::OK)
  25. .body(Body::from(
  26. notification_listing::render(&ctx.db, &*name).await,
  27. ))
  28. .unwrap());
  29. }
  30. }
  31. return Ok(Response::builder()
  32. .status(StatusCode::OK)
  33. .body(Body::from(String::from(
  34. "Please provide `?user=<username>` query param on URL.",
  35. )))
  36. .unwrap());
  37. }
  38. if req.uri.path() == "/zulip-hook" {
  39. let mut c = body_stream;
  40. let mut payload = Vec::new();
  41. while let Some(chunk) = c.next().await {
  42. let chunk = chunk?;
  43. payload.extend_from_slice(&chunk);
  44. }
  45. let req = match serde_json::from_slice(&payload) {
  46. Ok(r) => r,
  47. Err(e) => {
  48. return Ok(Response::builder()
  49. .status(StatusCode::BAD_REQUEST)
  50. .body(Body::from(format!(
  51. "Did not send valid JSON request: {}",
  52. e
  53. )))
  54. .unwrap());
  55. }
  56. };
  57. return Ok(Response::builder()
  58. .status(StatusCode::OK)
  59. .body(Body::from(triagebot::zulip::respond(&ctx, req).await))
  60. .unwrap());
  61. }
  62. if req.uri.path() != "/github-hook" {
  63. return Ok(Response::builder()
  64. .status(StatusCode::NOT_FOUND)
  65. .body(Body::empty())
  66. .unwrap());
  67. }
  68. if req.method != hyper::Method::POST {
  69. return Ok(Response::builder()
  70. .status(StatusCode::METHOD_NOT_ALLOWED)
  71. .header(header::ALLOW, "POST")
  72. .body(Body::empty())
  73. .unwrap());
  74. }
  75. let event = if let Some(ev) = req.headers.get("X-GitHub-Event") {
  76. let ev = match ev.to_str().ok() {
  77. Some(v) => v,
  78. None => {
  79. return Ok(Response::builder()
  80. .status(StatusCode::BAD_REQUEST)
  81. .body(Body::from("X-GitHub-Event header must be UTF-8 encoded"))
  82. .unwrap());
  83. }
  84. };
  85. match ev.parse::<EventName>() {
  86. Ok(v) => v,
  87. Err(_) => unreachable!(),
  88. }
  89. } else {
  90. return Ok(Response::builder()
  91. .status(StatusCode::BAD_REQUEST)
  92. .body(Body::from("X-GitHub-Event header must be set"))
  93. .unwrap());
  94. };
  95. log::debug!("event={}", event);
  96. let signature = if let Some(sig) = req.headers.get("X-Hub-Signature") {
  97. match sig.to_str().ok() {
  98. Some(v) => v,
  99. None => {
  100. return Ok(Response::builder()
  101. .status(StatusCode::BAD_REQUEST)
  102. .body(Body::from("X-Hub-Signature header must be UTF-8 encoded"))
  103. .unwrap());
  104. }
  105. }
  106. } else {
  107. return Ok(Response::builder()
  108. .status(StatusCode::BAD_REQUEST)
  109. .body(Body::from("X-Hub-Signature header must be set"))
  110. .unwrap());
  111. };
  112. log::debug!("signature={}", signature);
  113. let mut c = body_stream;
  114. let mut payload = Vec::new();
  115. while let Some(chunk) = c.next().await {
  116. let chunk = chunk?;
  117. payload.extend_from_slice(&chunk);
  118. }
  119. if let Err(_) = payload::assert_signed(signature, &payload) {
  120. return Ok(Response::builder()
  121. .status(StatusCode::FORBIDDEN)
  122. .body(Body::from("Wrong signature"))
  123. .unwrap());
  124. }
  125. let payload = match String::from_utf8(payload) {
  126. Ok(p) => p,
  127. Err(_) => {
  128. return Ok(Response::builder()
  129. .status(StatusCode::BAD_REQUEST)
  130. .body(Body::from("Payload must be UTF-8"))
  131. .unwrap());
  132. }
  133. };
  134. match triagebot::webhook(event, payload, &ctx).await {
  135. Ok(()) => {}
  136. Err(err) => {
  137. log::error!("request failed: {:?}", err);
  138. return Ok(Response::builder()
  139. .status(StatusCode::INTERNAL_SERVER_ERROR)
  140. .body(Body::from("request failed"))
  141. .unwrap());
  142. }
  143. }
  144. Ok(Response::new(Body::from("processed request")))
  145. }
  146. async fn run_server(addr: SocketAddr) -> anyhow::Result<()> {
  147. log::info!("Listening on http://{}", addr);
  148. let client = Client::new();
  149. let db_client = Context::make_db_client(&client)
  150. .await
  151. .context("open database connection")?;
  152. db::run_migrations(&db_client)
  153. .await
  154. .context("database migrations")?;
  155. let gh = github::GithubClient::new(
  156. client.clone(),
  157. env::var("GITHUB_API_TOKEN").expect("Missing GITHUB_API_TOKEN"),
  158. );
  159. let ctx = Arc::new(Context {
  160. username: github::User::current(&gh).await.unwrap().login,
  161. db: db_client,
  162. github: gh,
  163. });
  164. let svc = hyper::service::make_service_fn(move |_conn| {
  165. let ctx = ctx.clone();
  166. async move {
  167. let uuid = Uuid::new_v4();
  168. Ok::<_, hyper::Error>(hyper::service::service_fn(move |req| {
  169. logger::LogFuture::new(
  170. uuid,
  171. serve_req(req, ctx.clone()).map(move |mut resp| {
  172. if let Ok(resp) = &mut resp {
  173. resp.headers_mut()
  174. .insert("X-Request-Id", uuid.to_string().parse().unwrap());
  175. }
  176. log::info!("response = {:?}", resp);
  177. resp
  178. }),
  179. )
  180. }))
  181. }
  182. });
  183. let serve_future = Server::bind(&addr).serve(svc);
  184. serve_future.await?;
  185. Ok(())
  186. }
  187. #[tokio::main]
  188. async fn main() {
  189. dotenv::dotenv().ok();
  190. logger::init();
  191. let port = env::var("PORT")
  192. .ok()
  193. .map(|p| p.parse::<u16>().expect("parsed PORT"))
  194. .unwrap_or(8000);
  195. let addr = ([0, 0, 0, 0], port).into();
  196. if let Err(e) = run_server(addr).await {
  197. eprintln!("Failed to run server: {}", e);
  198. }
  199. }