Go · 6191 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package notif
4
5 // Reason is the short string the inbox row + email footer surface
6 // to the user ("you were mentioned", "you were assigned", etc.).
7 // Kept short because it's also the
8 // `notification_email_log.notification_id`-joined column on the
9 // inbox row.
10 type Reason string
11
12 const (
13 ReasonMention Reason = "mention"
14 ReasonAssignment Reason = "assignment"
15 ReasonReviewRequested Reason = "review_requested"
16 ReasonAuthor Reason = "author" // you opened the thread
17 ReasonCommenter Reason = "commenter" // you commented earlier
18 ReasonSubscribed Reason = "subscribed" // explicit thread sub
19 ReasonWatching Reason = "watching" // repo-level watch
20 ReasonRepoAdminAction Reason = "repo_admin_action"
21 )
22
23 // Relation is how a recipient relates to an event. The routing
24 // matrix is keyed on (event-kind, relation). Typically more than
25 // one relation applies — the fan-out worker picks the highest-
26 // priority one (the order matches the priority).
27 type Relation int
28
29 const (
30 RelMention Relation = iota
31 RelAssignee
32 RelReviewer
33 RelAuthor
34 RelCommenter
35 RelSubscribedThread
36 RelWatchingAll
37 RelWatchingParticipating
38 RelRepoOwner // for repo-admin lifecycle events
39 )
40
41 // Action is the routing-matrix output. NotifyInbox controls the
42 // in-app inbox row; EmailDefault is whether to email by default
43 // (subject to the user's per-kind email pref). OverrideIgnore
44 // means the event notifies even when the user has set
45 // `watches.level = 'ignore'` for the repo (matches GitHub's
46 // "@mentions always notify" semantics).
47 type Action struct {
48 NotifyInbox bool
49 EmailDefault bool
50 OverrideIgnore bool
51 Reason Reason
52 }
53
54 // Skip is the zero-action used when the matrix decides the
55 // event-relation pair shouldn't notify.
56 var Skip = Action{}
57
58 // Routing returns the action for an (event-kind, recipient
59 // relation) pair. The matrix is small enough today to be a
60 // switch; once we have ~30 event kinds it'll graduate to a real
61 // table-driven shape.
62 //
63 // Unknown kinds → Skip (deny by default — better to miss a
64 // notification than to spam users when a new kind is added but
65 // not routed).
66 func Routing(kind string, rel Relation) Action {
67 switch kind {
68 case "issue_created", "pr_opened":
69 // New issue / PR. Watching=all surfaces; @mention slot
70 // fires too if the body mentions someone.
71 switch rel {
72 case RelMention:
73 return Action{NotifyInbox: true, EmailDefault: true, OverrideIgnore: true, Reason: ReasonMention}
74 case RelWatchingAll:
75 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonWatching}
76 }
77 case "issue_comment_created", "pr_comment_created":
78 switch rel {
79 case RelMention:
80 return Action{NotifyInbox: true, EmailDefault: true, OverrideIgnore: true, Reason: ReasonMention}
81 case RelAssignee:
82 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonAssignment}
83 case RelAuthor:
84 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonAuthor}
85 case RelCommenter:
86 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonCommenter}
87 case RelSubscribedThread:
88 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonSubscribed}
89 case RelWatchingAll:
90 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonWatching}
91 }
92 case "issue_assigned", "pr_assigned":
93 switch rel {
94 case RelAssignee:
95 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonAssignment}
96 case RelMention:
97 return Action{NotifyInbox: true, EmailDefault: true, OverrideIgnore: true, Reason: ReasonMention}
98 }
99 case "issue_closed", "issue_reopened", "pr_closed", "pr_reopened", "pr_merged":
100 switch rel {
101 case RelAuthor:
102 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonAuthor}
103 case RelAssignee:
104 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonAssignment}
105 case RelSubscribedThread:
106 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonSubscribed}
107 case RelWatchingAll:
108 return Action{NotifyInbox: true, EmailDefault: false, Reason: ReasonWatching}
109 }
110 case "review_requested":
111 switch rel {
112 case RelReviewer:
113 return Action{NotifyInbox: true, EmailDefault: true, OverrideIgnore: true, Reason: ReasonReviewRequested}
114 }
115 case "review_submitted":
116 switch rel {
117 case RelAuthor:
118 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonAuthor}
119 case RelSubscribedThread:
120 return Action{NotifyInbox: true, EmailDefault: false, Reason: ReasonSubscribed}
121 }
122 case "mentioned":
123 // Standalone mention event (no inline thread context).
124 // Always inbox + email; overrides ignore.
125 switch rel {
126 case RelMention:
127 return Action{NotifyInbox: true, EmailDefault: true, OverrideIgnore: true, Reason: ReasonMention}
128 }
129 case "check_failed", "check_fixed":
130 // PR-author gets pinged when their PR's checks flip.
131 switch rel {
132 case RelAuthor:
133 return Action{NotifyInbox: true, EmailDefault: false, Reason: ReasonAuthor}
134 }
135 case "repo_archived", "repo_unarchived",
136 "repo_visibility_changed", "repo_soft_deleted",
137 "repo_restored", "repo_transfer_requested",
138 "repo_transfer_accepted", "repo_transfer_declined",
139 "repo_transfer_canceled", "repo_transfer_expired":
140 // S16-deferred lifecycle email kinds. Repo owner is the
141 // canonical recipient; transfer flows additionally notify
142 // the prospective recipient (computed by the caller).
143 switch rel {
144 case RelRepoOwner:
145 return Action{NotifyInbox: true, EmailDefault: true, Reason: ReasonRepoAdminAction}
146 }
147 }
148 return Skip
149 }
150
151 // EmailPrefKey returns the user_notification_prefs key for the
152 // per-kind email toggle. Empty means "no toggle exists for this
153 // reason — fall back to global default true."
154 func EmailPrefKey(reason Reason) string {
155 switch reason {
156 case ReasonMention:
157 return "mentions_email"
158 case ReasonAssignment:
159 return "assignments_email"
160 case ReasonReviewRequested:
161 return "pr_review_requests_email"
162 case ReasonAuthor, ReasonCommenter, ReasonSubscribed, ReasonWatching:
163 return "issues_email"
164 case ReasonRepoAdminAction:
165 return "repo_admin_action_email"
166 }
167 return ""
168 }
169