Skip to content

g1t/crates/contracts/src/inbox.rs

724 lines24,749 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Inbox: the events service tells people what needs them as events arrive1//! 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`)
Inbox: threads, reasons, subscriptions and watching5//! and why. Each person has one item per **thread**, the thing it is about
6//! (an issue, a pull request, a workflow on a branch, a deployment): new
7//! activity on a thread brings its item back to the top, unread, and adds
8//! a line to its short history, rather than adding another item. Items are
9//! kept by username, which never changes.
10//!
11//! Who hears of a thread follows its **subscriptions**: whoever opened it,
12//! is assigned to it, was asked to review it, commented on it or was
13//! mentioned in it is subscribed without asking, and anyone can subscribe
14//! or unsubscribe by hand. A person can also **watch** a repository: only
15//! what they take part in (the default), all of its activity, only some
16//! kinds of it, or nothing at all.
17//!
18//! Methods, served at `POST /rpc/<method>` on the events service:
Inbox: the events service tells people what needs them as events arrive19//!
20//! - `inbox_list` takes `ListInboxArgs` and returns `InboxPage`. Items about
21//! a repository the viewer can no longer read are dropped as they are
22//! found.
23//! - `inbox_counts` takes `InboxCountsArgs` and returns `InboxCounts`: the
24//! unread items, by severity. One query, for every page's top bar.
25//! - `inbox_mark` takes `MarkInboxArgs` and returns how many items changed.
Inbox: threads, reasons, subscriptions and watching26//! - `inbox_thread` takes `ThreadArgs` and returns `Option<InboxThread>`:
27//! one item with its history and the person's subscription.
28//! - `inbox_subscription` takes `SubscriptionArgs` and returns
29//! `Option<ThreadSubscription>`; `inbox_subscribe` takes `SubscribeArgs`
30//! and returns the same.
31//! - `inbox_watching` takes `WatchingArgs` and returns `Watching`;
32//! `inbox_watch` takes `WatchArgs` and returns `Watching`;
33//! `inbox_watched` takes `InboxCountsArgs` and returns `Vec<Watching>`.
34//! - `inbox_settings` takes `InboxCountsArgs` and returns `InboxSettings`;
35//! `inbox_update_settings` takes `UpdateInboxSettingsArgs` and returns
36//! `InboxSettings`.
Inbox: the events service tells people what needs them as events arrive37//!
38//! What an event is about (the issue or pull request, its people, the
39//! comment) comes from the work service's `inbox_subject`, which takes
40//! `InboxSubjectArgs` and returns `Option<InboxSubject>`.
41
42use serde::{Deserialize, Serialize};
43
44use crate::Viewer;
45use crate::credentials::Principal;
46
47/// How much an item matters, and how it is shown: a failure, something a
Inbox: threads, reasons, subscriptions and watching48/// person must answer (an agent waiting on them, a review asked of them),
49/// something that went well, or something to know.
Inbox: the events service tells people what needs them as events arrive50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Severity {
53 Error,
54 Warning,
55 Success,
56 Info,
57}
58
59impl Severity {
60 pub const ALL: [Severity; 4] = [Severity::Error, Severity::Warning, Severity::Success, Severity::Info];
61
62 pub fn as_str(self) -> &'static str {
63 match self {
64 Severity::Error => "error",
65 Severity::Warning => "warning",
66 Severity::Success => "success",
67 Severity::Info => "info",
68 }
69 }
70
71 pub fn parse(value: &str) -> Option<Severity> {
72 Severity::ALL.into_iter().find(|severity| severity.as_str() == value)
73 }
Inbox: threads, reasons, subscriptions and watching74
75 /// Which of two is kept on an unread thread: what needs the person,
76 /// then a failure, then good news, then the rest. Lower comes first.
77 pub fn urgency(self) -> u8 {
78 match self {
79 Severity::Warning => 0,
80 Severity::Error => 1,
81 Severity::Success => 2,
82 Severity::Info => 3,
83 }
84 }
85}
86
87/// Why a person was told: what ties them to the thread, or what it asked
88/// of them.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum Reason {
92 /// An agent is waiting on them: it asked a question, or it stopped
93 /// until a person steps in.
94 Agent,
95 /// Someone asked them to review a pull request.
96 ReviewRequested,
97 /// They were assigned to it.
98 Assign,
99 /// Someone mentioned them by name.
100 Mention,
101 /// A check, workflow or deployment on their work finished.
102 CiActivity,
103 /// A security alert on a repository they look after.
104 SecurityAlert,
105 /// It was closed, reopened or merged.
106 StateChange,
107 /// They opened it, or asked g1t for it.
108 Author,
109 /// They commented on it.
110 Comment,
111 /// They subscribed to it by hand.
112 Manual,
113 /// They watch its repository.
114 Subscribed,
Inbox: the events service tells people what needs them as events arrive115}
116
Inbox: threads, reasons, subscriptions and watching117impl Reason {
118 /// Most specific first: when one person is told of something for more
119 /// than one reason, the first of these is the one shown.
120 pub const ALL: [Reason; 11] = [
121 Reason::Agent,
122 Reason::ReviewRequested,
123 Reason::Assign,
124 Reason::Mention,
125 Reason::CiActivity,
126 Reason::SecurityAlert,
127 Reason::StateChange,
128 Reason::Author,
129 Reason::Comment,
130 Reason::Manual,
131 Reason::Subscribed,
132 ];
133
134 pub fn as_str(self) -> &'static str {
135 match self {
136 Reason::Agent => "agent",
137 Reason::ReviewRequested => "review_requested",
138 Reason::Assign => "assign",
139 Reason::Mention => "mention",
140 Reason::CiActivity => "ci_activity",
141 Reason::SecurityAlert => "security_alert",
142 Reason::StateChange => "state_change",
143 Reason::Author => "author",
144 Reason::Comment => "comment",
145 Reason::Manual => "manual",
146 Reason::Subscribed => "subscribed",
147 }
148 }
149
150 pub fn parse(value: &str) -> Option<Reason> {
151 Reason::ALL.into_iter().find(|reason| reason.as_str() == value)
152 }
153
154 /// Lower is more specific.
155 pub fn rank(self) -> usize {
156 Reason::ALL.iter().position(|reason| *reason == self).unwrap_or(Reason::ALL.len())
157 }
158
159 /// Whether the person takes part in the thread themselves, rather than
160 /// following it: everything but a hand subscription and watching.
161 pub fn participating(self) -> bool {
162 !matches!(self, Reason::Manual | Reason::Subscribed)
163 }
164
165 /// What is asked of the person directly: told even when they
166 /// unsubscribed from the thread, though never when they ignore it or
167 /// its repository.
168 pub fn direct(self) -> bool {
169 matches!(
170 self,
171 Reason::Agent | Reason::ReviewRequested | Reason::Assign | Reason::Mention | Reason::CiActivity | Reason::SecurityAlert
172 )
173 }
174}
175
Inbox: the events service tells people what needs them as events arrive176/// What an item is about.
177#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "lowercase")]
179pub enum SubjectKind {
180 Issue,
181 Pull,
Inbox: threads, reasons, subscriptions and watching182 /// A workflow's runs on one branch.
Inbox: the events service tells people what needs them as events arrive183 Run,
Inbox: threads, reasons, subscriptions and watching184 /// A project's deployments: production, or one pull request's preview.
185 Deploy,
Inbox: the events service tells people what needs them as events arrive186}
187
188impl SubjectKind {
Inbox: threads, reasons, subscriptions and watching189 pub const ALL: [SubjectKind; 4] = [SubjectKind::Issue, SubjectKind::Pull, SubjectKind::Run, SubjectKind::Deploy];
190
Inbox: the events service tells people what needs them as events arrive191 pub fn as_str(self) -> &'static str {
192 match self {
193 SubjectKind::Issue => "issue",
194 SubjectKind::Pull => "pull",
195 SubjectKind::Run => "run",
Inbox: threads, reasons, subscriptions and watching196 SubjectKind::Deploy => "deploy",
Inbox: the events service tells people what needs them as events arrive197 }
198 }
199
200 pub fn parse(value: &str) -> Option<SubjectKind> {
Inbox: threads, reasons, subscriptions and watching201 SubjectKind::ALL.into_iter().find(|kind| kind.as_str() == value)
Inbox: the events service tells people what needs them as events arrive202 }
203}
204
Inbox: threads, reasons, subscriptions and watching205/// One thread in a person's inbox: what it is about, and its latest
206/// activity.
Inbox: the events service tells people what needs them as events arrive207#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct InboxItem {
Inbox: threads, reasons, subscriptions and watching210 /// The thread's id: the same for as long as the person has it.
Inbox: the events service tells people what needs them as events arrive211 pub id: String,
Inbox: threads, reasons, subscriptions and watching212 /// Why they were told of the latest activity.
213 pub reason: Reason,
214 /// While unread, the most urgent of what happened since it was last
215 /// read; once read, the latest's.
Inbox: the events service tells people what needs them as events arrive216 pub severity: Severity,
Inbox: threads, reasons, subscriptions and watching217 /// One line: what happened last, and where.
Inbox: the events service tells people what needs them as events arrive218 pub title: String,
219 /// One line: what it happened to, such as the pull request's title.
220 pub body: String,
Inbox: threads, reasons, subscriptions and watching221 /// The event behind the latest activity, such as `pull.merged`.
222 pub event: Option<String>,
Inbox: the events service tells people what needs them as events arrive223 /// `owner/name`.
224 pub repo: Option<String>,
225 /// The workspace it happened in.
226 pub workspace: Option<String>,
227 pub subject: Option<SubjectKind>,
228 /// The issue or pull request's number.
229 pub number: Option<u32>,
230 /// Where it is on g1t.sh: a path such as `/acme/rocket/pull/12`.
231 pub url: String,
Inbox: threads, reasons, subscriptions and watching232 /// Who did the latest: a username, or `g1t`. Absent when nobody did.
Inbox: the events service tells people what needs them as events arrive233 pub actor: Option<String>,
Inbox: threads, reasons, subscriptions and watching234 /// How many things have happened on the thread.
235 pub count: u32,
236 /// RFC 3339: when the person was first told of the thread.
Inbox: the events service tells people what needs them as events arrive237 pub created_at: String,
Inbox: threads, reasons, subscriptions and watching238 /// RFC 3339: its latest activity.
239 pub updated_at: String,
Inbox: the events service tells people what needs them as events arrive240 pub read_at: Option<String>,
241 pub done_at: Option<String>,
242 pub saved: bool,
243 /// While this is in the future the item is out of the list.
244 pub snoozed_until: Option<String>,
245}
246
Inbox: threads, reasons, subscriptions and watching247/// One thing that happened on a thread, as the person was told of it.
248#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct InboxActivity {
251 pub reason: Reason,
252 pub severity: Severity,
253 pub title: String,
254 pub body: String,
255 pub event: Option<String>,
256 pub actor: Option<String>,
257 pub created_at: String,
258}
259
260/// The most activity kept per thread, newest first.
261pub const MAX_ACTIVITY: u32 = 10;
262
Inbox: the events service tells people what needs them as events arrive263/// Which items a list shows.
264#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "lowercase")]
266pub enum InboxView {
267 /// Everything not done and not snoozed: the inbox itself.
268 #[default]
269 Inbox,
270 /// Saved items, done or not.
271 Saved,
272 /// Items marked done.
273 Done,
274}
275
Inbox: threads, reasons, subscriptions and watching276impl InboxView {
277 pub fn parse(value: &str) -> Option<InboxView> {
278 match value {
279 "inbox" => Some(InboxView::Inbox),
280 "saved" => Some(InboxView::Saved),
281 "done" => Some(InboxView::Done),
282 _ => None,
283 }
284 }
285}
286
287/// `inbox_list`. Latest activity first, except that in the inbox
288/// unfiltered, unread warnings (what is waiting on the person) come first.
289#[derive(Clone, Debug, Default, Serialize, Deserialize)]
Inbox: the events service tells people what needs them as events arrive290#[serde(rename_all = "camelCase")]
291pub struct ListInboxArgs {
292 /// Whose inbox: the person signed in. Their memberships decide which
293 /// repositories they can still read.
294 pub viewer: Viewer,
295 #[serde(default)]
296 pub view: InboxView,
297 /// Only items of this severity.
298 #[serde(default)]
299 pub severity: Option<Severity>,
Inbox: threads, reasons, subscriptions and watching300 /// Only items told for this reason.
301 #[serde(default)]
302 pub reason: Option<Reason>,
303 /// Only items the person takes part in (see [`Reason::participating`]).
Inbox: the events service tells people what needs them as events arrive304 #[serde(default)]
Inbox: threads, reasons, subscriptions and watching305 pub participating: bool,
306 /// Only items about this repository.
307 #[serde(default)]
308 pub repo_id: Option<String>,
309 #[serde(default)]
Inbox: the events service tells people what needs them as events arrive310 pub unread: bool,
Inbox: threads, reasons, subscriptions and watching311 /// RFC 3339: only items with activity at or after it.
312 #[serde(default)]
313 pub since: Option<String>,
314 /// RFC 3339: only items whose latest activity was before it.
315 #[serde(default)]
316 pub updated_before: Option<String>,
Inbox: the events service tells people what needs them as events arrive317 /// The `next` of the page before.
318 #[serde(default)]
319 pub before: Option<String>,
320 #[serde(default)]
321 pub limit: Option<u32>,
322}
323
324pub const DEFAULT_INBOX_PAGE: u32 = 30;
325pub const MAX_INBOX_PAGE: u32 = 100;
326
327#[derive(Clone, Debug, Default, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase")]
329pub struct InboxPage {
330 pub items: Vec<InboxItem>,
331 /// Pass as `before` for the next page; absent on the last.
332 pub next: Option<String>,
333}
334
Inbox: threads, reasons, subscriptions and watching335/// `inbox_counts`, and the other methods that need only whose.
Inbox: the events service tells people what needs them as events arrive336#[derive(Clone, Debug, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct InboxCountsArgs {
339 pub username: String,
340}
341
342/// Unread items in the inbox view, by severity.
343#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct InboxCounts {
346 pub unread: u32,
347 pub error: u32,
348 pub warning: u32,
349 pub success: u32,
350 pub info: u32,
351}
352
353/// What `inbox_mark` does to the items it names.
354#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
355#[serde(rename_all = "snake_case")]
356pub enum InboxMark {
357 Read,
358 Unread,
359 /// Out of the inbox, into Done; read with it.
360 Done,
361 /// Back into the inbox.
362 Undone,
363 Save,
364 Unsave,
365 /// Out of the inbox until `until`.
366 Snooze,
Inbox: threads, reasons, subscriptions and watching367 /// Back into the inbox now.
368 Unsnooze,
Inbox: the events service tells people what needs them as events arrive369}
370
371/// `inbox_mark`: changes the person's own items, by id, or every item in
372/// their inbox when `ids` is empty and `all` is set (Mark all read).
373#[derive(Clone, Debug, Serialize, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct MarkInboxArgs {
376 pub username: String,
377 pub mark: InboxMark,
378 #[serde(default)]
379 pub ids: Vec<String>,
380 #[serde(default)]
381 pub all: bool,
382 /// With `all`: only items of this severity.
383 #[serde(default)]
384 pub severity: Option<Severity>,
Inbox: threads, reasons, subscriptions and watching385 /// With `all`: only items about this repository.
386 #[serde(default)]
387 pub repo_id: Option<String>,
388 /// With `all`: only items whose latest activity was at or before this
389 /// (RFC 3339), so what arrived after the person looked stays unread.
390 #[serde(default)]
391 pub last_read_at: Option<String>,
Inbox: the events service tells people what needs them as events arrive392 /// For `snooze`: RFC 3339.
393 #[serde(default)]
394 pub until: Option<String>,
395}
396
397/// The most ids one `inbox_mark` call changes.
398pub const MAX_MARK: usize = 100;
399
Inbox: threads, reasons, subscriptions and watching400/// `inbox_thread`: one of the viewer's own threads, by id.
401#[derive(Clone, Debug, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct ThreadArgs {
404 pub viewer: Viewer,
405 pub id: String,
406}
407
408/// A thread with its history and the person's subscription to it.
409#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct InboxThread {
412 #[serde(flatten)]
413 pub item: InboxItem,
414 /// Newest first, at most [`MAX_ACTIVITY`].
415 pub activity: Vec<InboxActivity>,
416 /// For an issue or pull request; absent for a run or a deployment,
417 /// which nobody subscribes to.
418 pub subscription: Option<ThreadSubscription>,
419}
420
421/// A person's subscription to an issue or pull request.
422#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
423#[serde(rename_all = "camelCase")]
424pub struct ThreadSubscription {
425 /// Whether they hear of what happens on it.
426 pub subscribed: bool,
427 /// Whether they hear of nothing on it at all, not even a mention.
428 pub ignored: bool,
429 /// Why they are subscribed: they opened it (`author`), are assigned
430 /// (`assign`), were asked to review (`review_requested`), commented
431 /// (`comment`), were mentioned (`mention`) or subscribed by hand
432 /// (`manual`). Absent when they are not.
433 pub reason: Option<Reason>,
434 /// `owner/name`, and the issue or pull request's number.
435 pub repo: Option<String>,
436 pub number: Option<u32>,
437 /// RFC 3339: when they last chose, or null if they never did.
438 pub updated_at: Option<String>,
439}
440
441/// Which issue or pull request: by a thread's id, or by its repository and
442/// number.
443#[derive(Clone, Debug, Default, Serialize, Deserialize)]
444#[serde(rename_all = "camelCase")]
445pub struct SubscriptionArgs {
446 pub viewer: Viewer,
447 #[serde(default)]
448 pub id: Option<String>,
449 #[serde(default)]
450 pub repo_id: Option<String>,
451 #[serde(default)]
452 pub number: Option<u32>,
453}
454
455/// `inbox_subscribe`.
456#[derive(Clone, Debug, Default, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase")]
458pub struct SubscribeArgs {
459 #[serde(flatten)]
460 pub on: SubscriptionArgs,
461 /// True to subscribe, false to unsubscribe. Absent with `ignored`
462 /// false: back to the default, subscribed only while taking part.
463 #[serde(default)]
464 pub subscribed: Option<bool>,
465 /// True to hear of nothing on it, not even a mention.
466 #[serde(default)]
467 pub ignored: bool,
468}
469
470/// How closely a person follows a repository.
471#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(rename_all = "snake_case")]
473pub enum WatchLevel {
474 /// Only what they take part in or are mentioned in: the default.
475 #[default]
476 Participating,
477 /// Everything: every issue and pull request opened, commented on,
478 /// closed or merged, and every deployment.
479 All,
480 /// Nothing at all, not even a mention.
481 Ignore,
482 /// What they take part in, and the kinds of activity in `events`.
483 Custom,
484}
485
486impl WatchLevel {
487 pub const ALL: [WatchLevel; 4] = [WatchLevel::Participating, WatchLevel::All, WatchLevel::Ignore, WatchLevel::Custom];
488
489 pub fn as_str(self) -> &'static str {
490 match self {
491 WatchLevel::Participating => "participating",
492 WatchLevel::All => "all",
493 WatchLevel::Ignore => "ignore",
494 WatchLevel::Custom => "custom",
495 }
496 }
497
498 pub fn parse(value: &str) -> Option<WatchLevel> {
499 WatchLevel::ALL.into_iter().find(|level| level.as_str() == value)
500 }
501}
502
503/// The kinds of activity a custom watch can follow.
504pub const WATCH_EVENTS: [&str; 4] = ["issues", "pulls", "deployments", "security"];
505
506/// How a person watches one repository.
507#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct Watching {
510 pub repo_id: String,
511 /// `owner/name`, when known.
512 #[serde(default)]
513 pub repo: Option<String>,
514 pub level: WatchLevel,
515 /// With `custom`: some of [`WATCH_EVENTS`].
516 #[serde(default)]
517 pub events: Vec<String>,
518 /// RFC 3339: when they chose, or null if they never did.
519 #[serde(default)]
520 pub updated_at: Option<String>,
521}
522
523/// `inbox_watching`.
524#[derive(Clone, Debug, Serialize, Deserialize)]
525#[serde(rename_all = "camelCase")]
526pub struct WatchingArgs {
527 pub username: String,
528 pub repo_id: String,
529}
530
531/// `inbox_watch`. No `level`: back to the default.
532#[derive(Clone, Debug, Serialize, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct WatchArgs {
535 pub username: String,
536 pub repo_id: String,
537 /// `owner/name`, kept to list what the person watches.
538 #[serde(default)]
539 pub repo: Option<String>,
540 #[serde(default)]
541 pub level: Option<WatchLevel>,
542 #[serde(default)]
543 pub events: Vec<String>,
544}
545
546/// A person's choices about being told.
547#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
548#[serde(rename_all = "camelCase")]
549pub struct InboxSettings {
550 /// The reasons they are also emailed for.
551 pub email: Vec<Reason>,
552 /// How they watch a repository they create.
553 pub default_watch: WatchLevel,
554}
555
556impl Default for InboxSettings {
557 fn default() -> Self {
558 InboxSettings {
559 email: DEFAULT_EMAIL.to_vec(),
560 default_watch: WatchLevel::All,
561 }
562 }
563}
564
565/// What a person is emailed for until they choose: what is waiting on them.
566pub const DEFAULT_EMAIL: [Reason; 3] = [Reason::Agent, Reason::ReviewRequested, Reason::Mention];
567
568/// `inbox_update_settings`. What is left out is unchanged.
569#[derive(Clone, Debug, Serialize, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct UpdateInboxSettingsArgs {
572 pub username: String,
573 #[serde(default)]
574 pub email: Option<Vec<Reason>>,
575 #[serde(default)]
576 pub default_watch: Option<WatchLevel>,
577}
578
Inbox: the events service tells people what needs them as events arrive579/// `inbox_subject` on the work service: what an event names, for the
580/// inbox. A service-to-service read: it checks nobody's access, and what
581/// it returns is only ever shown to the people it names, or to those who
582/// can read the repository.
583#[derive(Clone, Debug, Serialize, Deserialize)]
584#[serde(rename_all = "camelCase")]
585pub struct InboxSubjectArgs {
586 pub repo_id: String,
587 pub number: u32,
588 /// The comment the event is about, if any.
589 #[serde(default)]
590 pub comment_id: Option<String>,
591}
592
593#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct InboxSubject {
596 /// `issue` or `pull`.
597 pub kind: Option<SubjectKind>,
598 pub title: String,
599 pub author: Principal,
600 /// For work g1t did: the person it was done for.
601 #[serde(default)]
602 pub requested_by: Option<Principal>,
603 /// Usernames.
604 #[serde(default)]
605 pub assignees: Vec<String>,
606 /// Usernames, and `g1t`. Pull requests only.
607 #[serde(default)]
608 pub reviewers: Vec<String>,
609 /// For a pull request: the issue it is for, with that issue's people.
610 #[serde(default)]
611 pub issue: Option<Box<InboxSubject>>,
612 #[serde(default)]
613 pub comment: Option<InboxComment>,
614}
615
616impl InboxSubject {
617 /// Whose it is to answer for: whoever asked g1t for it, or its author.
618 pub fn owner(&self) -> &Principal {
619 self.requested_by.as_ref().unwrap_or(&self.author)
620 }
621}
622
623#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
624#[serde(rename_all = "camelCase")]
625pub struct InboxComment {
626 pub author: Principal,
627 /// The first line or so, as written.
628 pub excerpt: String,
629 /// The people it mentions by name, outside code and quotes. Never `g1t`.
630 #[serde(default)]
631 pub mentions: Vec<String>,
632 /// A review's verdict: `approve` or `request_changes`.
633 #[serde(default)]
634 pub verdict: Option<String>,
635 /// Something that happened (an assignment, a close), not something written.
636 #[serde(default)]
637 pub event: bool,
638}
639
Inbox: threads, reasons, subscriptions and watching640/// `notify_by_email` on the identity service: one item, emailed to the
641/// person it is for, if they can still read its repository and their
642/// address is confirmed. Returns whether it was sent.
643#[derive(Clone, Debug, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct NotifyByEmailArgs {
646 pub username: String,
647 pub repo_id: String,
648 /// The subject line and the first paragraph.
649 pub subject: String,
650 pub intro: String,
651 /// What was said, and who said it, when it was written by someone.
652 #[serde(default)]
653 pub quote: Option<(String, String)>,
654 /// A path on the site, such as `/acme/rocket/pull/12`.
655 pub path: String,
656 pub reason: Reason,
657}
658
Inbox: the events service tells people what needs them as events arrive659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
Inbox: threads, reasons, subscriptions and watching664 fn severities_reasons_and_subjects_read_back() {
Inbox: the events service tells people what needs them as events arrive665 for severity in Severity::ALL {
666 assert_eq!(Severity::parse(severity.as_str()), Some(severity));
667 }
668 assert_eq!(Severity::parse("fatal"), None);
Inbox: threads, reasons, subscriptions and watching669 for reason in Reason::ALL {
670 assert_eq!(Reason::parse(reason.as_str()), Some(reason));
671 assert_eq!(serde_json::to_value(reason).unwrap(), reason.as_str());
672 }
673 for kind in SubjectKind::ALL {
674 assert_eq!(SubjectKind::parse(kind.as_str()), Some(kind));
675 }
676 for level in WatchLevel::ALL {
677 assert_eq!(WatchLevel::parse(level.as_str()), Some(level));
678 }
Inbox: the events service tells people what needs them as events arrive679 assert_eq!(serde_json::to_value(InboxMark::Unsave).unwrap(), "unsave");
680 }
Inbox: threads, reasons, subscriptions and watching681
682 #[test]
683 fn what_is_asked_of_a_person_outranks_what_they_follow() {
684 assert!(Reason::Agent.rank() < Reason::Mention.rank());
685 assert!(Reason::Mention.rank() < Reason::Author.rank());
686 assert!(Reason::Author.rank() < Reason::Subscribed.rank());
687 assert!(Reason::ReviewRequested.direct() && !Reason::Comment.direct());
688 assert!(!Reason::Subscribed.participating() && Reason::Author.participating());
689 assert!(Severity::Warning.urgency() < Severity::Error.urgency());
690 }
691
692 #[test]
693 fn a_thread_carries_its_item_flat() {
694 let thread = InboxThread {
695 item: InboxItem {
696 id: "ntf_1".into(),
697 reason: Reason::Mention,
698 severity: Severity::Info,
699 title: "t".into(),
700 body: "b".into(),
701 event: None,
702 repo: None,
703 workspace: None,
704 subject: None,
705 number: None,
706 url: "/inbox".into(),
707 actor: None,
708 count: 1,
709 created_at: "2026-10-07T12:00:00.000Z".into(),
710 updated_at: "2026-10-07T12:00:00.000Z".into(),
711 read_at: None,
712 done_at: None,
713 saved: false,
714 snoozed_until: None,
715 },
716 activity: Vec::new(),
717 subscription: None,
718 };
719 let value = serde_json::to_value(&thread).unwrap();
720 assert_eq!(value["id"], "ntf_1");
721 assert_eq!(value["reason"], "mention");
722 assert!(value["activity"].is_array());
723 }
Inbox: the events service tells people what needs them as events arrive724}