Skip to content

g1t/crates/contracts/src/inbox.rs

287 lines9,067 bytesCodeBlame
1//! The inbox: what needs a person, or what they follow, as it happens.
2//!
3//! The events service keeps it, beside the event log: as events arrive it
4//! works out who should hear of each (see `services/events/src/inbox.rs`)
5//! and writes one item per person. Items are kept by username, which never
6//! changes. Methods, served at `POST /rpc/<method>` on the events service:
7//!
8//! - `inbox_list` takes `ListInboxArgs` and returns `InboxPage`. Items about
9//! a repository the viewer can no longer read are dropped as they are
10//! found.
11//! - `inbox_counts` takes `InboxCountsArgs` and returns `InboxCounts`: the
12//! unread items, by severity. One query, for every page's top bar.
13//! - `inbox_mark` takes `MarkInboxArgs` and returns how many items changed.
14//!
15//! What an event is about (the issue or pull request, its people, the
16//! comment) comes from the work service's `inbox_subject`, which takes
17//! `InboxSubjectArgs` and returns `Option<InboxSubject>`.
18
19use serde::{Deserialize, Serialize};
20
21use crate::Viewer;
22use crate::credentials::Principal;
23
24/// How much an item matters, and how it is shown: a failure, something a
25/// person must answer (an agent waiting on them), something that went
26/// well, or something to know.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "lowercase")]
29pub enum Severity {
30 Error,
31 Warning,
32 Success,
33 Info,
34}
35
36impl Severity {
37 pub const ALL: [Severity; 4] = [Severity::Error, Severity::Warning, Severity::Success, Severity::Info];
38
39 pub fn as_str(self) -> &'static str {
40 match self {
41 Severity::Error => "error",
42 Severity::Warning => "warning",
43 Severity::Success => "success",
44 Severity::Info => "info",
45 }
46 }
47
48 pub fn parse(value: &str) -> Option<Severity> {
49 Severity::ALL.into_iter().find(|severity| severity.as_str() == value)
50 }
51}
52
53/// What an item is about.
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum SubjectKind {
57 Issue,
58 Pull,
59 /// A workflow run.
60 Run,
61}
62
63impl SubjectKind {
64 pub fn as_str(self) -> &'static str {
65 match self {
66 SubjectKind::Issue => "issue",
67 SubjectKind::Pull => "pull",
68 SubjectKind::Run => "run",
69 }
70 }
71
72 pub fn parse(value: &str) -> Option<SubjectKind> {
73 [SubjectKind::Issue, SubjectKind::Pull, SubjectKind::Run]
74 .into_iter()
75 .find(|kind| kind.as_str() == value)
76 }
77}
78
79/// One thing a person was told.
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct InboxItem {
83 pub id: String,
84 /// Why they were told, such as `checks_failed` or `mentioned`.
85 pub reason: String,
86 pub severity: Severity,
87 /// One line: what happened, and where.
88 pub title: String,
89 /// One line: what it happened to, such as the pull request's title.
90 pub body: String,
91 /// `owner/name`.
92 pub repo: Option<String>,
93 /// The workspace it happened in.
94 pub workspace: Option<String>,
95 pub subject: Option<SubjectKind>,
96 /// The issue or pull request's number.
97 pub number: Option<u32>,
98 /// Where it is on g1t.sh: a path such as `/acme/rocket/pull/12`.
99 pub url: String,
100 /// Who did it: a username, or `g1t`. Absent when nobody did.
101 pub actor: Option<String>,
102 /// RFC 3339.
103 pub created_at: String,
104 pub read_at: Option<String>,
105 pub done_at: Option<String>,
106 pub saved: bool,
107 /// While this is in the future the item is out of the list.
108 pub snoozed_until: Option<String>,
109}
110
111/// Which items a list shows.
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "lowercase")]
114pub enum InboxView {
115 /// Everything not done and not snoozed: the inbox itself.
116 #[default]
117 Inbox,
118 /// Saved items, done or not.
119 Saved,
120 /// Items marked done.
121 Done,
122}
123
124/// `inbox_list`. Newest first, except that unread warnings (an agent
125/// waiting on the person) come before everything else in the inbox.
126#[derive(Clone, Debug, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct ListInboxArgs {
129 /// Whose inbox: the person signed in. Their memberships decide which
130 /// repositories they can still read.
131 pub viewer: Viewer,
132 #[serde(default)]
133 pub view: InboxView,
134 /// Only items of this severity.
135 #[serde(default)]
136 pub severity: Option<Severity>,
137 #[serde(default)]
138 pub unread: bool,
139 /// The `next` of the page before.
140 #[serde(default)]
141 pub before: Option<String>,
142 #[serde(default)]
143 pub limit: Option<u32>,
144}
145
146pub const DEFAULT_INBOX_PAGE: u32 = 30;
147pub const MAX_INBOX_PAGE: u32 = 100;
148
149#[derive(Clone, Debug, Default, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct InboxPage {
152 pub items: Vec<InboxItem>,
153 /// Pass as `before` for the next page; absent on the last.
154 pub next: Option<String>,
155}
156
157/// `inbox_counts`.
158#[derive(Clone, Debug, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct InboxCountsArgs {
161 pub username: String,
162}
163
164/// Unread items in the inbox view, by severity.
165#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct InboxCounts {
168 pub unread: u32,
169 pub error: u32,
170 pub warning: u32,
171 pub success: u32,
172 pub info: u32,
173}
174
175/// What `inbox_mark` does to the items it names.
176#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub enum InboxMark {
179 Read,
180 Unread,
181 /// Out of the inbox, into Done; read with it.
182 Done,
183 /// Back into the inbox.
184 Undone,
185 Save,
186 Unsave,
187 /// Out of the inbox until `until`.
188 Snooze,
189}
190
191/// `inbox_mark`: changes the person's own items, by id, or every item in
192/// their inbox when `ids` is empty and `all` is set (Mark all read).
193#[derive(Clone, Debug, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct MarkInboxArgs {
196 pub username: String,
197 pub mark: InboxMark,
198 #[serde(default)]
199 pub ids: Vec<String>,
200 #[serde(default)]
201 pub all: bool,
202 /// With `all`: only items of this severity.
203 #[serde(default)]
204 pub severity: Option<Severity>,
205 /// For `snooze`: RFC 3339.
206 #[serde(default)]
207 pub until: Option<String>,
208}
209
210/// The most ids one `inbox_mark` call changes.
211pub const MAX_MARK: usize = 100;
212
213/// `inbox_subject` on the work service: what an event names, for the
214/// inbox. A service-to-service read: it checks nobody's access, and what
215/// it returns is only ever shown to the people it names, or to those who
216/// can read the repository.
217#[derive(Clone, Debug, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct InboxSubjectArgs {
220 pub repo_id: String,
221 pub number: u32,
222 /// The comment the event is about, if any.
223 #[serde(default)]
224 pub comment_id: Option<String>,
225}
226
227#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct InboxSubject {
230 /// `issue` or `pull`.
231 pub kind: Option<SubjectKind>,
232 pub title: String,
233 pub author: Principal,
234 /// For work g1t did: the person it was done for.
235 #[serde(default)]
236 pub requested_by: Option<Principal>,
237 /// Usernames.
238 #[serde(default)]
239 pub assignees: Vec<String>,
240 /// Usernames, and `g1t`. Pull requests only.
241 #[serde(default)]
242 pub reviewers: Vec<String>,
243 /// For a pull request: the issue it is for, with that issue's people.
244 #[serde(default)]
245 pub issue: Option<Box<InboxSubject>>,
246 #[serde(default)]
247 pub comment: Option<InboxComment>,
248}
249
250impl InboxSubject {
251 /// Whose it is to answer for: whoever asked g1t for it, or its author.
252 pub fn owner(&self) -> &Principal {
253 self.requested_by.as_ref().unwrap_or(&self.author)
254 }
255}
256
257#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(rename_all = "camelCase")]
259pub struct InboxComment {
260 pub author: Principal,
261 /// The first line or so, as written.
262 pub excerpt: String,
263 /// The people it mentions by name, outside code and quotes. Never `g1t`.
264 #[serde(default)]
265 pub mentions: Vec<String>,
266 /// A review's verdict: `approve` or `request_changes`.
267 #[serde(default)]
268 pub verdict: Option<String>,
269 /// Something that happened (an assignment, a close), not something written.
270 #[serde(default)]
271 pub event: bool,
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn severities_and_subjects_read_back() {
280 for severity in Severity::ALL {
281 assert_eq!(Severity::parse(severity.as_str()), Some(severity));
282 }
283 assert_eq!(Severity::parse("fatal"), None);
284 assert_eq!(SubjectKind::parse("pull"), Some(SubjectKind::Pull));
285 assert_eq!(serde_json::to_value(InboxMark::Unsave).unwrap(), "unsave");
286 }
287}