Skip to main content

max / makenotwork

2.6 KB · 78 lines History Blame Raw
1 //! Mute or unmute one repository's issue notifications.
2 //!
3 //! Step 6 of the mailing-list plan (wiki `mnw-mailing-lists`). Issue mail used
4 //! to be gated by one account-wide bool, so leaving one noisy repository meant
5 //! leaving all of them. This is the per-repository control that replaces that
6 //! choice.
7 //!
8 //! It lives in the API tree rather than beside the git pages because
9 //! `git_routes` is a bare `Router` with no CSRF envelope, and a POST there
10 //! would silently skip it.
11
12 use axum::{Form, extract::State, response::IntoResponse};
13 use serde::Deserialize;
14 use sqlx::PgPool;
15
16 use crate::{
17 auth::AuthUser,
18 db::{self, GitRepoId},
19 error::{AppError, Result},
20 };
21
22 #[derive(Deserialize)]
23 pub(in crate::routes) struct MuteForm {
24 repo_id: String,
25 /// The owner's username, so the redirect lands back where the button was.
26 /// A collaborator is not the owner, so deriving it from the caller would
27 /// send them to a repository that is not theirs.
28 owner: String,
29 /// "true" mutes. Absent or anything else unmutes, so a stray value fails
30 /// toward receiving mail rather than silently dropping it.
31 muted: Option<String>,
32 }
33
34 /// `POST /api/repos/notifications`: set the caller's mute state for a repo.
35 ///
36 /// Only ever writes the caller's own subscription, so there is nothing to
37 /// authorise beyond being able to see the repository at all. That check is not
38 /// about the mute (muting a repo you cannot see would change nothing); it is so
39 /// this endpoint cannot be used to probe whether a private repository exists.
40 #[tracing::instrument(skip_all, name = "api::set_repo_notifications")]
41 pub(in crate::routes) async fn set_repo_notifications(
42 State(db): State<PgPool>,
43 AuthUser(user): AuthUser,
44 Form(form): Form<MuteForm>,
45 ) -> Result<impl IntoResponse> {
46 let repo_id: GitRepoId = form
47 .repo_id
48 .parse()
49 .map_err(|_| AppError::BadRequest("Invalid repository".to_string()))?;
50
51 let Some(repo) = db::git_repos::get_repo_by_id(&db, repo_id).await? else {
52 return Err(AppError::NotFound);
53 };
54
55 if repo.visibility == db::Visibility::Private
56 && repo.user_id != user.id
57 && !db::repo_collaborators::is_collaborator(&db, repo.id, user.id).await?
58 {
59 // Same answer a stranger gets for a repo that does not exist.
60 return Err(AppError::NotFound);
61 }
62
63 let muted = form.muted.as_deref() == Some("true");
64 db::lists::set_repo_muted(
65 &db,
66 *repo.id.as_uuid(),
67 user.id,
68 db::ListKind::Issues,
69 muted,
70 )
71 .await?;
72
73 Ok(axum::response::Redirect::to(&format!(
74 "/git/{}/{}",
75 form.owner, repo.name
76 )))
77 }
78