//! Reactions on an issue, a pull request, or one of their comments: `POST /{owner}/{repo}/issues/{n}/reactions` //! and `POST /{owner}/{repo}/pulls/{n}/reactions`. The form carries `csrf`, `target_kind` (`issue` //! or `comment`), `target_id` and `reaction` (a short name). The viewer must be signed in and able //! to read the repo, the repo must not be archived, and the target must belong to this item. The //! response toggles the viewer's reaction and is a 303 back to the item, anchored on the target. use forge_core::{Reaction, ReactionTarget}; use serde::Deserialize; use topcoat::Result; use topcoat::context::{Cx, app_context}; use topcoat::router::content::Form; use topcoat::router::error::{SeeOther, bad_request, forbidden, not_found, see_other}; use topcoat::router::{path_param, route}; use crate::auth::require_csrf; use crate::pages::issue_parts::{ItemKind, comment_for, item_for, may_converse, parse_number}; use crate::pages::repo_ctx::load_repo; use crate::state::AppState; use crate::views::repo_href; path_param!(owner); path_param!(repo); path_param!(number); #[derive(Deserialize)] struct ReactionForm { #[serde(default)] csrf: String, #[serde(default)] target_kind: String, #[serde(default)] target_id: String, #[serde(default)] reaction: String, } #[route(POST "/{owner}/{repo}/issues/{number}/reactions")] async fn issue_reaction(cx: &Cx, Form(form): Form) -> Result { react(cx, ItemKind::Issue, form).await } #[route(POST "/{owner}/{repo}/pulls/{number}/reactions")] async fn pull_reaction(cx: &Cx, Form(form): Form) -> Result { react(cx, ItemKind::Pull, form).await } async fn react(cx: &Cx, kind: ItemKind, form: ReactionForm) -> Result { let number = parse_number(path_param::(cx))?; let ctx = load_repo(cx, path_param::(cx), path_param::(cx)).await?; let (user, _) = require_csrf(cx, &form.csrf).await?; if ctx.repo.archived { return Err(forbidden().into()); } let state = app_context::(cx); let item = item_for(state, &ctx, kind, number).await?; if !may_converse(&ctx, &item) { return Err(forbidden().into()); } let Some(reaction) = Reaction::parse(&form.reaction) else { return Err(bad_request("unknown reaction").into()); }; let (target, anchor) = match form.target_kind.as_str() { "issue" => { if form.target_id != item.id { return Err(not_found().into()); } let prefix = match kind { ItemKind::Issue => "issue", ItemKind::Pull => "pull", }; (ReactionTarget::Issue, format!("{prefix}-{number}")) } "comment" => { let comment = comment_for(state, &item, &form.target_id).await?; (ReactionTarget::Comment, format!("comment-{}", comment.id)) } _ => return Err(bad_request("unknown reaction target").into()), }; state .store .toggle_reaction(target, &form.target_id, &user.id, reaction) .await?; let href = repo_href( &ctx.repo.owner_name, &ctx.repo.name, kind.segment(), &number.to_string(), ); Ok(see_other(format!("{href}#{anchor}"))) }