123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- use crate::db::notifications::add_metadata;
- use crate::db::notifications::{self, delete_ping, move_indices, record_ping, Identifier};
- use crate::github::GithubClient;
- use crate::handlers::Context;
- use anyhow::Context as _;
- #[derive(Debug, serde::Deserialize)]
- pub struct Request {
- /// Markdown body of the sent message.
- data: String,
- /// Metadata about this request.
- message: Message,
- /// Authentication token. The same for all Zulip messages.
- token: String,
- }
- #[derive(Debug, serde::Deserialize)]
- struct Message {
- sender_id: usize,
- sender_full_name: String,
- }
- #[derive(serde::Serialize)]
- struct Response<'a> {
- content: &'a str,
- }
- pub async fn to_github_id(client: &GithubClient, zulip_id: usize) -> anyhow::Result<Option<i64>> {
- let url = format!("{}/zulip-map.json", rust_team_data::v1::BASE_URL);
- let map: rust_team_data::v1::ZulipMapping = client
- .json(client.raw().get(&url))
- .await
- .context("could not get team data")?;
- Ok(map.users.get(&zulip_id).map(|v| *v as i64))
- }
- pub async fn respond(ctx: &Context, req: Request) -> String {
- let expected_token = std::env::var("ZULIP_TOKEN").expect("`ZULIP_TOKEN` set for authorization");
- if !openssl::memcmp::eq(req.token.as_bytes(), expected_token.as_bytes()) {
- return serde_json::to_string(&Response {
- content: "Invalid authorization.",
- })
- .unwrap();
- }
- log::trace!("zulip hook: {:?}", req);
- let gh_id = match to_github_id(&ctx.github, req.message.sender_id).await {
- Ok(Some(gh_id)) => gh_id,
- Ok(None) => {
- return serde_json::to_string(&Response {
- content: &format!(
- "Unknown Zulip user. Please add `zulip-id = {}` to your file in rust-lang/team.",
- req.message.sender_id),
- })
- .unwrap();
- }
- Err(e) => {
- return serde_json::to_string(&Response {
- content: &format!("Failed to query team API: {:?}", e),
- })
- .unwrap();
- }
- };
- let mut words = req.data.split_whitespace();
- match words.next() {
- Some("acknowledge") => match acknowledge(&ctx, gh_id, words).await {
- Ok(r) => r,
- Err(e) => serde_json::to_string(&Response {
- content: &format!(
- "Failed to parse acknowledgement, expected `acknowledge <identifier>`: {:?}.",
- e
- ),
- })
- .unwrap(),
- },
- Some("add") => match add_notification(&ctx, gh_id, words).await {
- Ok(r) => r,
- Err(e) => serde_json::to_string(&Response {
- content: &format!(
- "Failed to parse movement, expected `add <url> <description (multiple words)>`: {:?}.",
- e
- ),
- })
- .unwrap(),
- },
- Some("move") => match move_notification(&ctx, gh_id, words).await {
- Ok(r) => r,
- Err(e) => serde_json::to_string(&Response {
- content: &format!(
- "Failed to parse movement, expected `move <from> <to>`: {:?}.",
- e
- ),
- })
- .unwrap(),
- },
- Some("meta") => match add_meta_notification(&ctx, gh_id, words).await {
- Ok(r) => r,
- Err(e) => serde_json::to_string(&Response {
- content: &format!(
- "Failed to parse movement, expected `move <idx> <meta...>`: {:?}.",
- e
- ),
- })
- .unwrap(),
- },
- _ => serde_json::to_string(&Response {
- content: "Unknown command.",
- })
- .unwrap(),
- }
- }
- async fn acknowledge(
- ctx: &Context,
- gh_id: i64,
- mut words: impl Iterator<Item = &str>,
- ) -> anyhow::Result<String> {
- let url = match words.next() {
- Some(url) => {
- if words.next().is_some() {
- anyhow::bail!("too many words");
- }
- url
- }
- None => anyhow::bail!("not enough words"),
- };
- let ident = if let Ok(number) = url.parse::<usize>() {
- Identifier::Index(
- std::num::NonZeroUsize::new(number)
- .ok_or_else(|| anyhow::anyhow!("index must be at least 1"))?,
- )
- } else {
- Identifier::Url(url)
- };
- match delete_ping(
- &mut Context::make_db_client(&ctx.github.raw()).await?,
- gh_id,
- ident,
- )
- .await
- {
- Ok(()) => Ok(serde_json::to_string(&Response {
- content: &format!("Acknowledged {}.", url),
- })
- .unwrap()),
- Err(e) => Ok(serde_json::to_string(&Response {
- content: &format!("Failed to acknowledge {}: {:?}.", url, e),
- })
- .unwrap()),
- }
- }
- async fn add_notification(
- ctx: &Context,
- gh_id: i64,
- mut words: impl Iterator<Item = &str>,
- ) -> anyhow::Result<String> {
- let url = match words.next() {
- Some(idx) => idx,
- None => anyhow::bail!("url not present"),
- };
- let mut description = words.fold(String::new(), |mut acc, piece| {
- acc.push_str(piece);
- acc.push(' ');
- acc
- });
- let description = if description.is_empty() {
- None
- } else {
- assert_eq!(description.pop(), Some(' ')); // pop trailing space
- Some(description)
- };
- match record_ping(
- &ctx.db,
- ¬ifications::Notification {
- user_id: gh_id,
- origin_url: url.to_owned(),
- origin_html: String::new(),
- short_description: description,
- time: chrono::Utc::now().into(),
- team_name: None,
- },
- )
- .await
- {
- Ok(()) => Ok(serde_json::to_string(&Response {
- content: "Created!",
- })
- .unwrap()),
- Err(e) => Ok(serde_json::to_string(&Response {
- content: &format!("Failed to create: {:?}", e),
- })
- .unwrap()),
- }
- }
- async fn add_meta_notification(
- ctx: &Context,
- gh_id: i64,
- mut words: impl Iterator<Item = &str>,
- ) -> anyhow::Result<String> {
- let idx = match words.next() {
- Some(idx) => idx,
- None => anyhow::bail!("idx not present"),
- };
- let idx = idx
- .parse::<usize>()
- .context("index")?
- .checked_sub(1)
- .ok_or_else(|| anyhow::anyhow!("1-based indexes"))?;
- let mut description = words.fold(String::new(), |mut acc, piece| {
- acc.push_str(piece);
- acc.push(' ');
- acc
- });
- let description = if description.is_empty() {
- None
- } else {
- assert_eq!(description.pop(), Some(' ')); // pop trailing space
- Some(description)
- };
- match add_metadata(
- &mut Context::make_db_client(&ctx.github.raw()).await?,
- gh_id,
- idx,
- description.as_deref(),
- )
- .await
- {
- Ok(()) => Ok(serde_json::to_string(&Response {
- content: "Added metadata!",
- })
- .unwrap()),
- Err(e) => Ok(serde_json::to_string(&Response {
- content: &format!("Failed to add: {:?}", e),
- })
- .unwrap()),
- }
- }
- async fn move_notification(
- ctx: &Context,
- gh_id: i64,
- mut words: impl Iterator<Item = &str>,
- ) -> anyhow::Result<String> {
- let from = match words.next() {
- Some(idx) => idx,
- None => anyhow::bail!("from idx not present"),
- };
- let to = match words.next() {
- Some(idx) => idx,
- None => anyhow::bail!("from idx not present"),
- };
- let from = from
- .parse::<usize>()
- .context("from index")?
- .checked_sub(1)
- .ok_or_else(|| anyhow::anyhow!("1-based indexes"))?;
- let to = to
- .parse::<usize>()
- .context("to index")?
- .checked_sub(1)
- .ok_or_else(|| anyhow::anyhow!("1-based indexes"))?;
- match move_indices(
- &mut Context::make_db_client(&ctx.github.raw()).await?,
- gh_id,
- from,
- to,
- )
- .await
- {
- Ok(()) => Ok(serde_json::to_string(&Response {
- // to 1-base indices
- content: &format!("Moved {} to {}.", from + 1, to + 1),
- })
- .unwrap()),
- Err(e) => Ok(serde_json::to_string(&Response {
- content: &format!("Failed to move: {:?}.", e),
- })
- .unwrap()),
- }
- }
|