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 arrive | 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`) | |
| Inbox: threads, reasons, subscriptions and watching | 5 | //! 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 arrive | 19 | //! |
| 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 watching | 26 | //! - `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 arrive | 37 | //! |
| 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 | ||
| 42 | use serde::{Deserialize, Serialize}; | |
| 43 | ||
| 44 | use crate::Viewer; | |
| 45 | use crate::credentials::Principal; | |
| 46 | ||
| 47 | /// How much an item matters, and how it is shown: a failure, something a | |
| Inbox: threads, reasons, subscriptions and watching | 48 | /// 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 arrive | 50 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 51 | #[serde(rename_all = "lowercase")] | |
| 52 | pub enum Severity { | |
| 53 | Error, | |
| 54 | Warning, | |
| 55 | Success, | |
| 56 | Info, | |
| 57 | } | |
| 58 | ||
| 59 | impl 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 watching | 74 | |
| 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")] | |
| 91 | pub 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, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 101 | /// Someone mentioned a team they are in (`@workspace/team`). |
| 102 | TeamMention, | |
| Inbox: threads, reasons, subscriptions and watching | 103 | /// A check, workflow or deployment on their work finished. |
| 104 | CiActivity, | |
| 105 | /// A security alert on a repository they look after. | |
| 106 | SecurityAlert, | |
| 107 | /// It was closed, reopened or merged. | |
| 108 | StateChange, | |
| 109 | /// They opened it, or asked g1t for it. | |
| 110 | Author, | |
| 111 | /// They commented on it. | |
| 112 | Comment, | |
| 113 | /// They subscribed to it by hand. | |
| 114 | Manual, | |
| 115 | /// They watch its repository. | |
| 116 | Subscribed, | |
| Inbox: the events service tells people what needs them as events arrive | 117 | } |
| 118 | ||
| Inbox: threads, reasons, subscriptions and watching | 119 | impl Reason { |
| 120 | /// Most specific first: when one person is told of something for more | |
| 121 | /// than one reason, the first of these is the one shown. | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 122 | pub const ALL: [Reason; 12] = [ |
| Inbox: threads, reasons, subscriptions and watching | 123 | Reason::Agent, |
| 124 | Reason::ReviewRequested, | |
| 125 | Reason::Assign, | |
| 126 | Reason::Mention, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 127 | Reason::TeamMention, |
| Inbox: threads, reasons, subscriptions and watching | 128 | Reason::CiActivity, |
| 129 | Reason::SecurityAlert, | |
| 130 | Reason::StateChange, | |
| 131 | Reason::Author, | |
| 132 | Reason::Comment, | |
| 133 | Reason::Manual, | |
| 134 | Reason::Subscribed, | |
| 135 | ]; | |
| 136 | ||
| 137 | pub fn as_str(self) -> &'static str { | |
| 138 | match self { | |
| 139 | Reason::Agent => "agent", | |
| 140 | Reason::ReviewRequested => "review_requested", | |
| 141 | Reason::Assign => "assign", | |
| 142 | Reason::Mention => "mention", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 143 | Reason::TeamMention => "team_mention", |
| Inbox: threads, reasons, subscriptions and watching | 144 | Reason::CiActivity => "ci_activity", |
| 145 | Reason::SecurityAlert => "security_alert", | |
| 146 | Reason::StateChange => "state_change", | |
| 147 | Reason::Author => "author", | |
| 148 | Reason::Comment => "comment", | |
| 149 | Reason::Manual => "manual", | |
| 150 | Reason::Subscribed => "subscribed", | |
| 151 | } | |
| 152 | } | |
| 153 | ||
| 154 | pub fn parse(value: &str) -> Option<Reason> { | |
| 155 | Reason::ALL.into_iter().find(|reason| reason.as_str() == value) | |
| 156 | } | |
| 157 | ||
| 158 | /// Lower is more specific. | |
| 159 | pub fn rank(self) -> usize { | |
| 160 | Reason::ALL.iter().position(|reason| *reason == self).unwrap_or(Reason::ALL.len()) | |
| 161 | } | |
| 162 | ||
| 163 | /// Whether the person takes part in the thread themselves, rather than | |
| 164 | /// following it: everything but a hand subscription and watching. | |
| 165 | pub fn participating(self) -> bool { | |
| 166 | !matches!(self, Reason::Manual | Reason::Subscribed) | |
| 167 | } | |
| 168 | ||
| 169 | /// What is asked of the person directly: told even when they | |
| 170 | /// unsubscribed from the thread, though never when they ignore it or | |
| 171 | /// its repository. | |
| 172 | pub fn direct(self) -> bool { | |
| 173 | matches!( | |
| 174 | self, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 175 | Reason::Agent |
| 176 | | Reason::ReviewRequested | |
| 177 | | Reason::Assign | |
| 178 | | Reason::Mention | |
| 179 | | Reason::TeamMention | |
| 180 | | Reason::CiActivity | |
| 181 | | Reason::SecurityAlert | |
| Inbox: threads, reasons, subscriptions and watching | 182 | ) |
| 183 | } | |
| 184 | } | |
| 185 | ||
| Inbox: the events service tells people what needs them as events arrive | 186 | /// What an item is about. |
| 187 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 188 | #[serde(rename_all = "lowercase")] | |
| 189 | pub enum SubjectKind { | |
| 190 | Issue, | |
| 191 | Pull, | |
| Inbox: threads, reasons, subscriptions and watching | 192 | /// A workflow's runs on one branch. |
| Inbox: the events service tells people what needs them as events arrive | 193 | Run, |
| Inbox: threads, reasons, subscriptions and watching | 194 | /// A project's deployments: production, or one pull request's preview. |
| 195 | Deploy, | |
| Inbox: the events service tells people what needs them as events arrive | 196 | } |
| 197 | ||
| 198 | impl SubjectKind { | |
| Inbox: threads, reasons, subscriptions and watching | 199 | pub const ALL: [SubjectKind; 4] = [SubjectKind::Issue, SubjectKind::Pull, SubjectKind::Run, SubjectKind::Deploy]; |
| 200 | ||
| Inbox: the events service tells people what needs them as events arrive | 201 | pub fn as_str(self) -> &'static str { |
| 202 | match self { | |
| 203 | SubjectKind::Issue => "issue", | |
| 204 | SubjectKind::Pull => "pull", | |
| 205 | SubjectKind::Run => "run", | |
| Inbox: threads, reasons, subscriptions and watching | 206 | SubjectKind::Deploy => "deploy", |
| Inbox: the events service tells people what needs them as events arrive | 207 | } |
| 208 | } | |
| 209 | ||
| 210 | pub fn parse(value: &str) -> Option<SubjectKind> { | |
| Inbox: threads, reasons, subscriptions and watching | 211 | SubjectKind::ALL.into_iter().find(|kind| kind.as_str() == value) |
| Inbox: the events service tells people what needs them as events arrive | 212 | } |
| 213 | } | |
| 214 | ||
| Inbox: threads, reasons, subscriptions and watching | 215 | /// One thread in a person's inbox: what it is about, and its latest |
| 216 | /// activity. | |
| Inbox: the events service tells people what needs them as events arrive | 217 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] |
| 218 | #[serde(rename_all = "camelCase")] | |
| 219 | pub struct InboxItem { | |
| Inbox: threads, reasons, subscriptions and watching | 220 | /// 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 arrive | 221 | pub id: String, |
| Inbox: threads, reasons, subscriptions and watching | 222 | /// Why they were told of the latest activity. |
| 223 | pub reason: Reason, | |
| 224 | /// While unread, the most urgent of what happened since it was last | |
| 225 | /// read; once read, the latest's. | |
| Inbox: the events service tells people what needs them as events arrive | 226 | pub severity: Severity, |
| Inbox: threads, reasons, subscriptions and watching | 227 | /// One line: what happened last, and where. |
| Inbox: the events service tells people what needs them as events arrive | 228 | pub title: String, |
| 229 | /// One line: what it happened to, such as the pull request's title. | |
| 230 | pub body: String, | |
| Inbox: threads, reasons, subscriptions and watching | 231 | /// The event behind the latest activity, such as `pull.merged`. |
| 232 | pub event: Option<String>, | |
| Inbox: the events service tells people what needs them as events arrive | 233 | /// `owner/name`. |
| 234 | pub repo: Option<String>, | |
| 235 | /// The workspace it happened in. | |
| 236 | pub workspace: Option<String>, | |
| 237 | pub subject: Option<SubjectKind>, | |
| 238 | /// The issue or pull request's number. | |
| 239 | pub number: Option<u32>, | |
| 240 | /// Where it is on g1t.sh: a path such as `/acme/rocket/pull/12`. | |
| 241 | pub url: String, | |
| Inbox: threads, reasons, subscriptions and watching | 242 | /// Who did the latest: a username, or `g1t`. Absent when nobody did. |
| Inbox: the events service tells people what needs them as events arrive | 243 | pub actor: Option<String>, |
| Inbox: threads, reasons, subscriptions and watching | 244 | /// How many things have happened on the thread. |
| 245 | pub count: u32, | |
| 246 | /// RFC 3339: when the person was first told of the thread. | |
| Inbox: the events service tells people what needs them as events arrive | 247 | pub created_at: String, |
| Inbox: threads, reasons, subscriptions and watching | 248 | /// RFC 3339: its latest activity. |
| 249 | pub updated_at: String, | |
| Inbox: the events service tells people what needs them as events arrive | 250 | pub read_at: Option<String>, |
| 251 | pub done_at: Option<String>, | |
| 252 | pub saved: bool, | |
| 253 | /// While this is in the future the item is out of the list. | |
| 254 | pub snoozed_until: Option<String>, | |
| 255 | } | |
| 256 | ||
| Inbox: threads, reasons, subscriptions and watching | 257 | /// One thing that happened on a thread, as the person was told of it. |
| 258 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 259 | #[serde(rename_all = "camelCase")] | |
| 260 | pub struct InboxActivity { | |
| 261 | pub reason: Reason, | |
| 262 | pub severity: Severity, | |
| 263 | pub title: String, | |
| 264 | pub body: String, | |
| 265 | pub event: Option<String>, | |
| 266 | pub actor: Option<String>, | |
| 267 | pub created_at: String, | |
| 268 | } | |
| 269 | ||
| 270 | /// The most activity kept per thread, newest first. | |
| 271 | pub const MAX_ACTIVITY: u32 = 10; | |
| 272 | ||
| Inbox: the events service tells people what needs them as events arrive | 273 | /// Which items a list shows. |
| 274 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 275 | #[serde(rename_all = "lowercase")] | |
| 276 | pub enum InboxView { | |
| 277 | /// Everything not done and not snoozed: the inbox itself. | |
| 278 | #[default] | |
| 279 | Inbox, | |
| 280 | /// Saved items, done or not. | |
| 281 | Saved, | |
| 282 | /// Items marked done. | |
| 283 | Done, | |
| 284 | } | |
| 285 | ||
| Inbox: threads, reasons, subscriptions and watching | 286 | impl InboxView { |
| 287 | pub fn parse(value: &str) -> Option<InboxView> { | |
| 288 | match value { | |
| 289 | "inbox" => Some(InboxView::Inbox), | |
| 290 | "saved" => Some(InboxView::Saved), | |
| 291 | "done" => Some(InboxView::Done), | |
| 292 | _ => None, | |
| 293 | } | |
| 294 | } | |
| 295 | } | |
| 296 | ||
| 297 | /// `inbox_list`. Latest activity first, except that in the inbox | |
| 298 | /// unfiltered, unread warnings (what is waiting on the person) come first. | |
| 299 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| Inbox: the events service tells people what needs them as events arrive | 300 | #[serde(rename_all = "camelCase")] |
| 301 | pub struct ListInboxArgs { | |
| 302 | /// Whose inbox: the person signed in. Their memberships decide which | |
| 303 | /// repositories they can still read. | |
| 304 | pub viewer: Viewer, | |
| 305 | #[serde(default)] | |
| 306 | pub view: InboxView, | |
| 307 | /// Only items of this severity. | |
| 308 | #[serde(default)] | |
| 309 | pub severity: Option<Severity>, | |
| Inbox: threads, reasons, subscriptions and watching | 310 | /// Only items told for this reason. |
| 311 | #[serde(default)] | |
| 312 | pub reason: Option<Reason>, | |
| 313 | /// Only items the person takes part in (see [`Reason::participating`]). | |
| Inbox: the events service tells people what needs them as events arrive | 314 | #[serde(default)] |
| Inbox: threads, reasons, subscriptions and watching | 315 | pub participating: bool, |
| 316 | /// Only items about this repository. | |
| 317 | #[serde(default)] | |
| 318 | pub repo_id: Option<String>, | |
| 319 | #[serde(default)] | |
| Inbox: the events service tells people what needs them as events arrive | 320 | pub unread: bool, |
| Inbox: threads, reasons, subscriptions and watching | 321 | /// RFC 3339: only items with activity at or after it. |
| 322 | #[serde(default)] | |
| 323 | pub since: Option<String>, | |
| 324 | /// RFC 3339: only items whose latest activity was before it. | |
| 325 | #[serde(default)] | |
| 326 | pub updated_before: Option<String>, | |
| Inbox: the events service tells people what needs them as events arrive | 327 | /// The `next` of the page before. |
| 328 | #[serde(default)] | |
| 329 | pub before: Option<String>, | |
| 330 | #[serde(default)] | |
| 331 | pub limit: Option<u32>, | |
| 332 | } | |
| 333 | ||
| 334 | pub const DEFAULT_INBOX_PAGE: u32 = 30; | |
| 335 | pub const MAX_INBOX_PAGE: u32 = 100; | |
| 336 | ||
| 337 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 338 | #[serde(rename_all = "camelCase")] | |
| 339 | pub struct InboxPage { | |
| 340 | pub items: Vec<InboxItem>, | |
| 341 | /// Pass as `before` for the next page; absent on the last. | |
| 342 | pub next: Option<String>, | |
| 343 | } | |
| 344 | ||
| Inbox: threads, reasons, subscriptions and watching | 345 | /// `inbox_counts`, and the other methods that need only whose. |
| Inbox: the events service tells people what needs them as events arrive | 346 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 347 | #[serde(rename_all = "camelCase")] | |
| 348 | pub struct InboxCountsArgs { | |
| 349 | pub username: String, | |
| 350 | } | |
| 351 | ||
| 352 | /// Unread items in the inbox view, by severity. | |
| 353 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 354 | #[serde(rename_all = "camelCase")] | |
| 355 | pub struct InboxCounts { | |
| 356 | pub unread: u32, | |
| 357 | pub error: u32, | |
| 358 | pub warning: u32, | |
| 359 | pub success: u32, | |
| 360 | pub info: u32, | |
| 361 | } | |
| 362 | ||
| 363 | /// What `inbox_mark` does to the items it names. | |
| 364 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 365 | #[serde(rename_all = "snake_case")] | |
| 366 | pub enum InboxMark { | |
| 367 | Read, | |
| 368 | Unread, | |
| 369 | /// Out of the inbox, into Done; read with it. | |
| 370 | Done, | |
| 371 | /// Back into the inbox. | |
| 372 | Undone, | |
| 373 | Save, | |
| 374 | Unsave, | |
| 375 | /// Out of the inbox until `until`. | |
| 376 | Snooze, | |
| Inbox: threads, reasons, subscriptions and watching | 377 | /// Back into the inbox now. |
| 378 | Unsnooze, | |
| Inbox: the events service tells people what needs them as events arrive | 379 | } |
| 380 | ||
| 381 | /// `inbox_mark`: changes the person's own items, by id, or every item in | |
| 382 | /// their inbox when `ids` is empty and `all` is set (Mark all read). | |
| 383 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 384 | #[serde(rename_all = "camelCase")] | |
| 385 | pub struct MarkInboxArgs { | |
| 386 | pub username: String, | |
| 387 | pub mark: InboxMark, | |
| 388 | #[serde(default)] | |
| 389 | pub ids: Vec<String>, | |
| 390 | #[serde(default)] | |
| 391 | pub all: bool, | |
| 392 | /// With `all`: only items of this severity. | |
| 393 | #[serde(default)] | |
| 394 | pub severity: Option<Severity>, | |
| Inbox: threads, reasons, subscriptions and watching | 395 | /// With `all`: only items about this repository. |
| 396 | #[serde(default)] | |
| 397 | pub repo_id: Option<String>, | |
| 398 | /// With `all`: only items whose latest activity was at or before this | |
| 399 | /// (RFC 3339), so what arrived after the person looked stays unread. | |
| 400 | #[serde(default)] | |
| 401 | pub last_read_at: Option<String>, | |
| Inbox: the events service tells people what needs them as events arrive | 402 | /// For `snooze`: RFC 3339. |
| 403 | #[serde(default)] | |
| 404 | pub until: Option<String>, | |
| 405 | } | |
| 406 | ||
| 407 | /// The most ids one `inbox_mark` call changes. | |
| 408 | pub const MAX_MARK: usize = 100; | |
| 409 | ||
| Inbox: threads, reasons, subscriptions and watching | 410 | /// `inbox_thread`: one of the viewer's own threads, by id. |
| 411 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 412 | #[serde(rename_all = "camelCase")] | |
| 413 | pub struct ThreadArgs { | |
| 414 | pub viewer: Viewer, | |
| 415 | pub id: String, | |
| 416 | } | |
| 417 | ||
| 418 | /// A thread with its history and the person's subscription to it. | |
| 419 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 420 | #[serde(rename_all = "camelCase")] | |
| 421 | pub struct InboxThread { | |
| 422 | #[serde(flatten)] | |
| 423 | pub item: InboxItem, | |
| 424 | /// Newest first, at most [`MAX_ACTIVITY`]. | |
| 425 | pub activity: Vec<InboxActivity>, | |
| 426 | /// For an issue or pull request; absent for a run or a deployment, | |
| 427 | /// which nobody subscribes to. | |
| 428 | pub subscription: Option<ThreadSubscription>, | |
| 429 | } | |
| 430 | ||
| 431 | /// A person's subscription to an issue or pull request. | |
| 432 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 433 | #[serde(rename_all = "camelCase")] | |
| 434 | pub struct ThreadSubscription { | |
| 435 | /// Whether they hear of what happens on it. | |
| 436 | pub subscribed: bool, | |
| 437 | /// Whether they hear of nothing on it at all, not even a mention. | |
| 438 | pub ignored: bool, | |
| 439 | /// Why they are subscribed: they opened it (`author`), are assigned | |
| 440 | /// (`assign`), were asked to review (`review_requested`), commented | |
| 441 | /// (`comment`), were mentioned (`mention`) or subscribed by hand | |
| 442 | /// (`manual`). Absent when they are not. | |
| 443 | pub reason: Option<Reason>, | |
| 444 | /// `owner/name`, and the issue or pull request's number. | |
| 445 | pub repo: Option<String>, | |
| 446 | pub number: Option<u32>, | |
| 447 | /// RFC 3339: when they last chose, or null if they never did. | |
| 448 | pub updated_at: Option<String>, | |
| 449 | } | |
| 450 | ||
| 451 | /// Which issue or pull request: by a thread's id, or by its repository and | |
| 452 | /// number. | |
| 453 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 454 | #[serde(rename_all = "camelCase")] | |
| 455 | pub struct SubscriptionArgs { | |
| 456 | pub viewer: Viewer, | |
| 457 | #[serde(default)] | |
| 458 | pub id: Option<String>, | |
| 459 | #[serde(default)] | |
| 460 | pub repo_id: Option<String>, | |
| 461 | #[serde(default)] | |
| 462 | pub number: Option<u32>, | |
| 463 | } | |
| 464 | ||
| 465 | /// `inbox_subscribe`. | |
| 466 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 467 | #[serde(rename_all = "camelCase")] | |
| 468 | pub struct SubscribeArgs { | |
| 469 | #[serde(flatten)] | |
| 470 | pub on: SubscriptionArgs, | |
| 471 | /// True to subscribe, false to unsubscribe. Absent with `ignored` | |
| 472 | /// false: back to the default, subscribed only while taking part. | |
| 473 | #[serde(default)] | |
| 474 | pub subscribed: Option<bool>, | |
| 475 | /// True to hear of nothing on it, not even a mention. | |
| 476 | #[serde(default)] | |
| 477 | pub ignored: bool, | |
| 478 | } | |
| 479 | ||
| 480 | /// How closely a person follows a repository. | |
| 481 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 482 | #[serde(rename_all = "snake_case")] | |
| 483 | pub enum WatchLevel { | |
| 484 | /// Only what they take part in or are mentioned in: the default. | |
| 485 | #[default] | |
| 486 | Participating, | |
| 487 | /// Everything: every issue and pull request opened, commented on, | |
| 488 | /// closed or merged, and every deployment. | |
| 489 | All, | |
| 490 | /// Nothing at all, not even a mention. | |
| 491 | Ignore, | |
| 492 | /// What they take part in, and the kinds of activity in `events`. | |
| 493 | Custom, | |
| 494 | } | |
| 495 | ||
| 496 | impl WatchLevel { | |
| 497 | pub const ALL: [WatchLevel; 4] = [WatchLevel::Participating, WatchLevel::All, WatchLevel::Ignore, WatchLevel::Custom]; | |
| 498 | ||
| 499 | pub fn as_str(self) -> &'static str { | |
| 500 | match self { | |
| 501 | WatchLevel::Participating => "participating", | |
| 502 | WatchLevel::All => "all", | |
| 503 | WatchLevel::Ignore => "ignore", | |
| 504 | WatchLevel::Custom => "custom", | |
| 505 | } | |
| 506 | } | |
| 507 | ||
| 508 | pub fn parse(value: &str) -> Option<WatchLevel> { | |
| 509 | WatchLevel::ALL.into_iter().find(|level| level.as_str() == value) | |
| 510 | } | |
| 511 | } | |
| 512 | ||
| 513 | /// The kinds of activity a custom watch can follow. | |
| 514 | pub const WATCH_EVENTS: [&str; 4] = ["issues", "pulls", "deployments", "security"]; | |
| 515 | ||
| 516 | /// How a person watches one repository. | |
| 517 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 518 | #[serde(rename_all = "camelCase")] | |
| 519 | pub struct Watching { | |
| 520 | pub repo_id: String, | |
| 521 | /// `owner/name`, when known. | |
| 522 | #[serde(default)] | |
| 523 | pub repo: Option<String>, | |
| 524 | pub level: WatchLevel, | |
| 525 | /// With `custom`: some of [`WATCH_EVENTS`]. | |
| 526 | #[serde(default)] | |
| 527 | pub events: Vec<String>, | |
| 528 | /// RFC 3339: when they chose, or null if they never did. | |
| 529 | #[serde(default)] | |
| 530 | pub updated_at: Option<String>, | |
| 531 | } | |
| 532 | ||
| 533 | /// `inbox_watching`. | |
| 534 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 535 | #[serde(rename_all = "camelCase")] | |
| 536 | pub struct WatchingArgs { | |
| 537 | pub username: String, | |
| 538 | pub repo_id: String, | |
| 539 | } | |
| 540 | ||
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 541 | /// `inbox_watchers`: how many watch a repository: all of its activity, |
| 542 | /// or some of it (custom). The default (participating) and ignoring are | |
| 543 | /// not counted. Returns `u64`. | |
| 544 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 545 | #[serde(rename_all = "camelCase")] | |
| 546 | pub struct WatchersArgs { | |
| 547 | pub repo_id: String, | |
| 548 | } | |
| 549 | ||
| Inbox: threads, reasons, subscriptions and watching | 550 | /// `inbox_watch`. No `level`: back to the default. |
| 551 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 552 | #[serde(rename_all = "camelCase")] | |
| 553 | pub struct WatchArgs { | |
| 554 | pub username: String, | |
| 555 | pub repo_id: String, | |
| 556 | /// `owner/name`, kept to list what the person watches. | |
| 557 | #[serde(default)] | |
| 558 | pub repo: Option<String>, | |
| 559 | #[serde(default)] | |
| 560 | pub level: Option<WatchLevel>, | |
| 561 | #[serde(default)] | |
| 562 | pub events: Vec<String>, | |
| 563 | } | |
| 564 | ||
| Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples. | 565 | /// When security alerts that are not urgent (medium, low, and what is only |
| 566 | /// to know) reach a person: in a digest once a day or once a week, at once | |
| 567 | /// like the urgent ones, or not at all. Critical and high alerts, a refused | |
| 568 | /// push and a vulnerability report never wait for the digest. | |
| 569 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 570 | #[serde(rename_all = "snake_case")] | |
| 571 | pub enum DigestCadence { | |
| 572 | #[default] | |
| 573 | Daily, | |
| 574 | Weekly, | |
| 575 | AtOnce, | |
| 576 | Off, | |
| 577 | } | |
| 578 | ||
| 579 | impl DigestCadence { | |
| 580 | pub const ALL: [DigestCadence; 4] = [DigestCadence::Daily, DigestCadence::Weekly, DigestCadence::AtOnce, DigestCadence::Off]; | |
| 581 | ||
| 582 | pub fn as_str(self) -> &'static str { | |
| 583 | match self { | |
| 584 | DigestCadence::Daily => "daily", | |
| 585 | DigestCadence::Weekly => "weekly", | |
| 586 | DigestCadence::AtOnce => "at_once", | |
| 587 | DigestCadence::Off => "off", | |
| 588 | } | |
| 589 | } | |
| 590 | ||
| 591 | pub fn parse(value: &str) -> Option<DigestCadence> { | |
| 592 | DigestCadence::ALL.into_iter().find(|cadence| cadence.as_str() == value) | |
| 593 | } | |
| 594 | } | |
| 595 | ||
| Inbox: threads, reasons, subscriptions and watching | 596 | /// A person's choices about being told. |
| 597 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 598 | #[serde(rename_all = "camelCase")] | |
| 599 | pub struct InboxSettings { | |
| 600 | /// The reasons they are also emailed for. | |
| 601 | pub email: Vec<Reason>, | |
| 602 | /// How they watch a repository they create. | |
| 603 | pub default_watch: WatchLevel, | |
| Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples. | 604 | /// Whether critical and high security alerts on repositories they watch |
| 605 | /// for security reach their inbox at once. A workspace's owners and | |
| 606 | /// security managers are told whatever this says: those alerts are | |
| 607 | /// theirs to act on. | |
| 608 | #[serde(default = "yes")] | |
| 609 | pub security_alerts: bool, | |
| 610 | /// When the rest of the security alerts reach them. | |
| 611 | #[serde(default)] | |
| 612 | pub security_digest: DigestCadence, | |
| 613 | } | |
| 614 | ||
| 615 | fn yes() -> bool { | |
| 616 | true | |
| Inbox: threads, reasons, subscriptions and watching | 617 | } |
| 618 | ||
| 619 | impl Default for InboxSettings { | |
| 620 | fn default() -> Self { | |
| 621 | InboxSettings { | |
| 622 | email: DEFAULT_EMAIL.to_vec(), | |
| 623 | default_watch: WatchLevel::All, | |
| Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples. | 624 | security_alerts: true, |
| 625 | security_digest: DigestCadence::Daily, | |
| Inbox: threads, reasons, subscriptions and watching | 626 | } |
| 627 | } | |
| 628 | } | |
| 629 | ||
| Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples. | 630 | /// What a person is emailed for until they choose: what is waiting on |
| 631 | /// them, and security alerts, which reach their inbox only when urgent or | |
| 632 | /// as their digest. | |
| 633 | pub const DEFAULT_EMAIL: [Reason; 4] = [Reason::Agent, Reason::ReviewRequested, Reason::Mention, Reason::SecurityAlert]; | |
| Inbox: threads, reasons, subscriptions and watching | 634 | |
| 635 | /// `inbox_update_settings`. What is left out is unchanged. | |
| 636 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 637 | #[serde(rename_all = "camelCase")] | |
| 638 | pub struct UpdateInboxSettingsArgs { | |
| 639 | pub username: String, | |
| 640 | #[serde(default)] | |
| 641 | pub email: Option<Vec<Reason>>, | |
| 642 | #[serde(default)] | |
| 643 | pub default_watch: Option<WatchLevel>, | |
| Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples. | 644 | #[serde(default)] |
| 645 | pub security_alerts: Option<bool>, | |
| 646 | #[serde(default)] | |
| 647 | pub security_digest: Option<DigestCadence>, | |
| Inbox: threads, reasons, subscriptions and watching | 648 | } |
| 649 | ||
| Inbox: the events service tells people what needs them as events arrive | 650 | /// `inbox_subject` on the work service: what an event names, for the |
| 651 | /// inbox. A service-to-service read: it checks nobody's access, and what | |
| 652 | /// it returns is only ever shown to the people it names, or to those who | |
| 653 | /// can read the repository. | |
| 654 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 655 | #[serde(rename_all = "camelCase")] | |
| 656 | pub struct InboxSubjectArgs { | |
| 657 | pub repo_id: String, | |
| 658 | pub number: u32, | |
| 659 | /// The comment the event is about, if any. | |
| 660 | #[serde(default)] | |
| 661 | pub comment_id: Option<String>, | |
| 662 | } | |
| 663 | ||
| 664 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 665 | #[serde(rename_all = "camelCase")] | |
| 666 | pub struct InboxSubject { | |
| 667 | /// `issue` or `pull`. | |
| 668 | pub kind: Option<SubjectKind>, | |
| 669 | pub title: String, | |
| 670 | pub author: Principal, | |
| 671 | /// For work g1t did: the person it was done for. | |
| 672 | #[serde(default)] | |
| 673 | pub requested_by: Option<Principal>, | |
| 674 | /// Usernames. | |
| 675 | #[serde(default)] | |
| 676 | pub assignees: Vec<String>, | |
| 677 | /// Usernames, and `g1t`. Pull requests only. | |
| 678 | #[serde(default)] | |
| 679 | pub reviewers: Vec<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 680 | /// The people its description mentions by name, outside code and |
| 681 | /// quotes, for `issue.opened` and `pull.opened`. Never `g1t`. | |
| 682 | #[serde(default)] | |
| 683 | pub mentions: Vec<String>, | |
| 684 | /// The teams its description mentions, with the people each tells. | |
| 685 | #[serde(default)] | |
| 686 | pub team_mentions: Vec<TeamMentioned>, | |
| Inbox: the events service tells people what needs them as events arrive | 687 | /// For a pull request: the issue it is for, with that issue's people. |
| 688 | #[serde(default)] | |
| 689 | pub issue: Option<Box<InboxSubject>>, | |
| 690 | #[serde(default)] | |
| 691 | pub comment: Option<InboxComment>, | |
| 692 | } | |
| 693 | ||
| 694 | impl InboxSubject { | |
| 695 | /// Whose it is to answer for: whoever asked g1t for it, or its author. | |
| 696 | pub fn owner(&self) -> &Principal { | |
| 697 | self.requested_by.as_ref().unwrap_or(&self.author) | |
| 698 | } | |
| 699 | } | |
| 700 | ||
| 701 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 702 | #[serde(rename_all = "camelCase")] | |
| 703 | pub struct InboxComment { | |
| 704 | pub author: Principal, | |
| 705 | /// The first line or so, as written. | |
| 706 | pub excerpt: String, | |
| 707 | /// The people it mentions by name, outside code and quotes. Never `g1t`. | |
| 708 | #[serde(default)] | |
| 709 | pub mentions: Vec<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 710 | /// The teams it mentions (`@workspace/team`), with the people each |
| 711 | /// tells: everyone in the team and its child teams, unless the team | |
| 712 | /// turned notifications off. A secret team tells nobody unless the | |
| 713 | /// writer is in it. | |
| 714 | #[serde(default)] | |
| 715 | pub team_mentions: Vec<TeamMentioned>, | |
| Inbox: the events service tells people what needs them as events arrive | 716 | /// A review's verdict: `approve` or `request_changes`. |
| 717 | #[serde(default)] | |
| 718 | pub verdict: Option<String>, | |
| 719 | /// Something that happened (an assignment, a close), not something written. | |
| 720 | #[serde(default)] | |
| 721 | pub event: bool, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 722 | /// Set when one of the workspace's agents wrote it, as itself: it is |
| 723 | /// shown by its name, marked as an agent, never as a person. | |
| 724 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 725 | pub agent: Option<crate::work::AgentRef>, | |
| 726 | /// Who the agent acted for, set with `agent`. | |
| 727 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 728 | pub acting_for: Option<Principal>, | |
| 729 | /// An agent's review: its verdict is advisory. | |
| 730 | #[serde(default)] | |
| 731 | pub advisory: bool, | |
| Inbox: the events service tells people what needs them as events arrive | 732 | } |
| 733 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 734 | /// A team a comment or description mentions, and who it tells. |
| 735 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 736 | #[serde(rename_all = "camelCase")] | |
| 737 | pub struct TeamMentioned { | |
| 738 | /// `workspace/slug`. | |
| 739 | pub team: String, | |
| 740 | /// Usernames. | |
| 741 | pub members: Vec<String>, | |
| 742 | } | |
| 743 | ||
| Inbox: threads, reasons, subscriptions and watching | 744 | /// `notify_by_email` on the identity service: one item, emailed to the |
| 745 | /// person it is for, if they can still read its repository and their | |
| 746 | /// address is confirmed. Returns whether it was sent. | |
| 747 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 748 | #[serde(rename_all = "camelCase")] | |
| 749 | pub struct NotifyByEmailArgs { | |
| 750 | pub username: String, | |
| 751 | pub repo_id: String, | |
| 752 | /// The subject line and the first paragraph. | |
| 753 | pub subject: String, | |
| 754 | pub intro: String, | |
| 755 | /// What was said, and who said it, when it was written by someone. | |
| 756 | #[serde(default)] | |
| 757 | pub quote: Option<(String, String)>, | |
| 758 | /// A path on the site, such as `/acme/rocket/pull/12`. | |
| 759 | pub path: String, | |
| 760 | pub reason: Reason, | |
| 761 | } | |
| 762 | ||
| Inbox: the events service tells people what needs them as events arrive | 763 | #[cfg(test)] |
| 764 | mod tests { | |
| 765 | use super::*; | |
| 766 | ||
| 767 | #[test] | |
| Inbox: threads, reasons, subscriptions and watching | 768 | fn severities_reasons_and_subjects_read_back() { |
| Inbox: the events service tells people what needs them as events arrive | 769 | for severity in Severity::ALL { |
| 770 | assert_eq!(Severity::parse(severity.as_str()), Some(severity)); | |
| 771 | } | |
| 772 | assert_eq!(Severity::parse("fatal"), None); | |
| Inbox: threads, reasons, subscriptions and watching | 773 | for reason in Reason::ALL { |
| 774 | assert_eq!(Reason::parse(reason.as_str()), Some(reason)); | |
| 775 | assert_eq!(serde_json::to_value(reason).unwrap(), reason.as_str()); | |
| 776 | } | |
| 777 | for kind in SubjectKind::ALL { | |
| 778 | assert_eq!(SubjectKind::parse(kind.as_str()), Some(kind)); | |
| 779 | } | |
| 780 | for level in WatchLevel::ALL { | |
| 781 | assert_eq!(WatchLevel::parse(level.as_str()), Some(level)); | |
| 782 | } | |
| Inbox: the events service tells people what needs them as events arrive | 783 | assert_eq!(serde_json::to_value(InboxMark::Unsave).unwrap(), "unsave"); |
| 784 | } | |
| Inbox: threads, reasons, subscriptions and watching | 785 | |
| 786 | #[test] | |
| 787 | fn what_is_asked_of_a_person_outranks_what_they_follow() { | |
| 788 | assert!(Reason::Agent.rank() < Reason::Mention.rank()); | |
| 789 | assert!(Reason::Mention.rank() < Reason::Author.rank()); | |
| 790 | assert!(Reason::Author.rank() < Reason::Subscribed.rank()); | |
| 791 | assert!(Reason::ReviewRequested.direct() && !Reason::Comment.direct()); | |
| 792 | assert!(!Reason::Subscribed.participating() && Reason::Author.participating()); | |
| 793 | assert!(Severity::Warning.urgency() < Severity::Error.urgency()); | |
| 794 | } | |
| 795 | ||
| 796 | #[test] | |
| 797 | fn a_thread_carries_its_item_flat() { | |
| 798 | let thread = InboxThread { | |
| 799 | item: InboxItem { | |
| 800 | id: "ntf_1".into(), | |
| 801 | reason: Reason::Mention, | |
| 802 | severity: Severity::Info, | |
| 803 | title: "t".into(), | |
| 804 | body: "b".into(), | |
| 805 | event: None, | |
| 806 | repo: None, | |
| 807 | workspace: None, | |
| 808 | subject: None, | |
| 809 | number: None, | |
| 810 | url: "/inbox".into(), | |
| 811 | actor: None, | |
| 812 | count: 1, | |
| 813 | created_at: "2026-10-07T12:00:00.000Z".into(), | |
| 814 | updated_at: "2026-10-07T12:00:00.000Z".into(), | |
| 815 | read_at: None, | |
| 816 | done_at: None, | |
| 817 | saved: false, | |
| 818 | snoozed_until: None, | |
| 819 | }, | |
| 820 | activity: Vec::new(), | |
| 821 | subscription: None, | |
| 822 | }; | |
| 823 | let value = serde_json::to_value(&thread).unwrap(); | |
| 824 | assert_eq!(value["id"], "ntf_1"); | |
| 825 | assert_eq!(value["reason"], "mention"); | |
| 826 | assert!(value["activity"].is_array()); | |
| 827 | } | |
| Inbox: the events service tells people what needs them as events arrive | 828 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.