forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
3.3 KiB
1//! Reactions on an issue, a pull request, or one of their comments: `POST /{owner}/{repo}/issues/{n}/reactions`
2//! and `POST /{owner}/{repo}/pulls/{n}/reactions`. The form carries `csrf`, `target_kind` (`issue`
3//! or `comment`), `target_id` and `reaction` (a short name). The viewer must be signed in and able
4//! to read the repo, the repo must not be archived, and the target must belong to this item. The
5//! response toggles the viewer's reaction and is a 303 back to the item, anchored on the target.
6
7use forge_core::{Reaction, ReactionTarget};
8use serde::Deserialize;
9use topcoat::Result;
10use topcoat::context::{Cx, app_context};
11use topcoat::router::content::Form;
12use topcoat::router::error::{SeeOther, bad_request, forbidden, not_found, see_other};
13use topcoat::router::{path_param, route};
14
15use crate::auth::require_csrf;
16use crate::pages::issue_parts::{ItemKind, comment_for, item_for, may_converse, parse_number};
17use crate::pages::repo_ctx::load_repo;
18use crate::state::AppState;
19use crate::views::repo_href;
20
21path_param!(owner);
22path_param!(repo);
23path_param!(number);
24
25#[derive(Deserialize)]
26struct ReactionForm {
27 #[serde(default)]
28 csrf: String,
29 #[serde(default)]
30 target_kind: String,
31 #[serde(default)]
32 target_id: String,
33 #[serde(default)]
34 reaction: String,
35}
36
37#[route(POST "/{owner}/{repo}/issues/{number}/reactions")]
38async fn issue_reaction(cx: &Cx, Form(form): Form<ReactionForm>) -> Result<SeeOther> {
39 react(cx, ItemKind::Issue, form).await
40}
41
42#[route(POST "/{owner}/{repo}/pulls/{number}/reactions")]
43async fn pull_reaction(cx: &Cx, Form(form): Form<ReactionForm>) -> Result<SeeOther> {
44 react(cx, ItemKind::Pull, form).await
45}
46
47async fn react(cx: &Cx, kind: ItemKind, form: ReactionForm) -> Result<SeeOther> {
48 let number = parse_number(path_param::<Number>(cx))?;
49 let ctx = load_repo(cx, path_param::<Owner>(cx), path_param::<Repo>(cx)).await?;
50 let (user, _) = require_csrf(cx, &form.csrf).await?;
51 if ctx.repo.archived {
52 return Err(forbidden().into());
53 }
54 let state = app_context::<AppState>(cx);
55 let item = item_for(state, &ctx, kind, number).await?;
56 if !may_converse(&ctx, &item) {
57 return Err(forbidden().into());
58 }
59 let Some(reaction) = Reaction::parse(&form.reaction) else {
60 return Err(bad_request("unknown reaction").into());
61 };
62 let (target, anchor) = match form.target_kind.as_str() {
63 "issue" => {
64 if form.target_id != item.id {
65 return Err(not_found().into());
66 }
67 let prefix = match kind {
68 ItemKind::Issue => "issue",
69 ItemKind::Pull => "pull",
70 };
71 (ReactionTarget::Issue, format!("{prefix}-{number}"))
72 }
73 "comment" => {
74 let comment = comment_for(state, &item, &form.target_id).await?;
75 (ReactionTarget::Comment, format!("comment-{}", comment.id))
76 }
77 _ => return Err(bad_request("unknown reaction target").into()),
78 };
79 state
80 .store
81 .toggle_reaction(target, &form.target_id, &user.id, reaction)
82 .await?;
83 let href = repo_href(
84 &ctx.repo.owner_name,
85 &ctx.repo.name,
86 kind.segment(),
87 &number.to_string(),
88 );
89 Ok(see_other(format!("{href}#{anchor}")))
90}