The inbox files a work item under its origin alone, and tells who only watches a linked repository only when they may read it
An item event's repository is the envelope's (its origin, or none): never a repoId in its data. Someone who hears of an item only by watching another repository it is linked to gets no row, push, email or count unless they are a member of its workspace or its origin is public.
- G1T-Session:
- ses_01m56w3nnce54skbep969amaqy
1 file+92−400/1 viewed
| 42 | 42 | //! hear of it; whoever ignores its origin hears of nothing on it. Its | |
| 43 | 43 | //! items are kept with its workspace and its origin, if it has one, and | |
| 44 | 44 | //! are shown to the workspace's members and to whoever can read the | |
| 45 | − | //! origin. Those who hear of it only by watching another repository it is | |
| 46 | − | //! linked to are not pushed it live: whether they may see it is known only | |
| 47 | − | //! when their inbox is read. | |
| 45 | + | //! origin (the event's own repository, never the first of `repoIds`). | |
| 46 | + | //! Those who hear of it only by watching another repository it is linked | |
| 47 | + | //! to are told only when they may read it: members of its workspace, or | |
| 48 | + | //! anyone when its origin is public. | |
| 48 | 49 | //! | |
| 49 | 50 | //! Nobody is told of what they did themselves, though an outcome they set | |
| 50 | 51 | //! off (checks, a workflow, a deployment, g1t's work) is theirs to hear of. | |
| ⋯ | |||
| 172 | 173 | let data = &event.data; | |
| 173 | 174 | let text = |key: &str| data[key].as_str().filter(|value| !value.is_empty()).map(str::to_owned); | |
| 174 | 175 | let number = |key: &str| data[key].as_u64().and_then(|n| u32::try_from(n).ok()); | |
| 175 | − | let repo_id = text("repoId").or_else(|| event.repo_id.clone()); | |
| 176 | 176 | // A work item's events, and comments on one: it may have no origin. | |
| 177 | + | // Its origin is the event's own repository and nothing else: never a | |
| 178 | + | // `repoId` in its data, nor the first of `repoIds`, which may be a | |
| 179 | + | // repository it is only linked to, whose readers may not see it. | |
| 177 | 180 | let on_item = event.kind == "comment.created" && data["itemId"].is_string(); | |
| 178 | 181 | if ITEM_TOLD.contains(&event.kind.as_str()) || on_item { | |
| 179 | 182 | return Some(Wanted { | |
| 180 | − | repo_id, | |
| 183 | + | repo_id: event.repo_id.clone().filter(|id| !id.is_empty()), | |
| 181 | 184 | number: Some(number("number")?), | |
| 182 | 185 | comment_id: if on_item { Some(text("commentId")?) } else { None }, | |
| 183 | 186 | item: Some(item_of(data)?), | |
| 184 | 187 | }); | |
| 185 | 188 | } | |
| 186 | − | let repo_id = repo_id?; | |
| 189 | + | let repo_id = text("repoId").or_else(|| event.repo_id.clone())?; | |
| 187 | 190 | let on = |number: Option<u32>, comment_id: Option<String>| { | |
| 188 | 191 | Some(Wanted { | |
| 189 | 192 | repo_id: Some(repo_id.clone()), | |
| ⋯ | |||
| 1392 | 1395 | event_id: String, | |
| 1393 | 1396 | /// Its event leaves nothing to do (`CLOSES`): settled once written. | |
| 1394 | 1397 | closing: bool, | |
| 1395 | − | /// Pushed live: not for someone who hears of a work item only by | |
| 1396 | − | /// watching a repository it is linked to other than its origin, unless | |
| 1397 | − | /// they are a member of its workspace, who may see it. | |
| 1398 | − | live: bool, | |
| 1399 | 1398 | /// The thread it is on, which notify keeps with what it replays. | |
| 1400 | 1399 | thread: String, | |
| 1401 | 1400 | } | |
| ⋯ | |||
| 1507 | 1506 | if let Some(notify) = sources.notify { | |
| 1508 | 1507 | let live = written | |
| 1509 | 1508 | .iter() | |
| 1510 | − | .filter(|item| !item.closing && item.live) | |
| 1509 | + | .filter(|item| !item.closing) | |
| 1511 | 1510 | .map(|item| { | |
| 1512 | 1511 | let live = live_notification(&item.event_id, &item.notice, &item.url, &rfc3339(now_ms()), Some(&item.thread)); | |
| 1513 | 1512 | (item.notice.username.clone(), live) | |
| ⋯ | |||
| 1741 | 1740 | None => repo.clone().unwrap_or_default(), | |
| 1742 | 1741 | }; | |
| 1743 | 1742 | let mut told = notices(event, &place, &actor, subject.as_ref(), &audience); | |
| 1743 | + | // Who hears of a work item only by watching another repository it is | |
| 1744 | + | // linked to is told only if they may read it: a member of its | |
| 1745 | + | // workspace, or anyone when its origin is public. Nothing of it (title, | |
| 1746 | + | // body, push, email, count) reaches anyone else. | |
| 1747 | + | if let Some(item) = &wanted.item { | |
| 1748 | + | let hearsay: Vec<String> = told | |
| 1749 | + | .iter() | |
| 1750 | + | .filter(|notice| only_elsewhere(notice, &elsewhere)) | |
| 1751 | + | .map(|notice| notice.username.clone()) | |
| 1752 | + | .collect(); | |
| 1753 | + | if !hearsay.is_empty() { | |
| 1754 | + | let asked = MembersAmongArgs { workspace: item.workspace.clone(), usernames: hearsay.clone() }; | |
| 1755 | + | let members: HashSet<String> = g1t_kit::call::<_, Vec<String>>(sources.identity, "members_among", &asked) | |
| 1756 | + | .await | |
| 1757 | + | .unwrap_or_else(|error| { | |
| 1758 | + | worker::console_error!("inbox: could not tell who is a member of {}: {error}", item.workspace); | |
| 1759 | + | Vec::new() | |
| 1760 | + | }) | |
| 1761 | + | .into_iter() | |
| 1762 | + | .map(|name| name.to_lowercase()) | |
| 1763 | + | .collect(); | |
| 1764 | + | let public = match &wanted.repo_id { | |
| 1765 | + | Some(origin) if hearsay.iter().any(|name| !members.contains(name)) => { | |
| 1766 | + | crate::live::public_of(sources.repos, vec![origin.clone()]).await.contains(origin) | |
| 1767 | + | } | |
| 1768 | + | _ => false, | |
| 1769 | + | }; | |
| 1770 | + | told.retain(|notice| may_hear(notice, &elsewhere, &members, public)); | |
| 1771 | + | } | |
| 1772 | + | } | |
| 1744 | 1773 | let mut statements = | |
| 1745 | 1774 | subscriptions::auto_subscribe(db, &thread.key, wanted.repo_id.as_deref(), &subscribes(event, subject.as_ref()), &event.time)?; | |
| 1746 | 1775 | // A scan's summary: one that opened nothing brings the thread up to date | |
| ⋯ | |||
| 1848 | 1877 | news.extend(rows.into_iter().map(|row| row.username)); | |
| 1849 | 1878 | } | |
| 1850 | 1879 | } | |
| 1851 | − | // Who hears of a work item only by watching another of its | |
| 1852 | − | // repositories is pushed it live when they are a member of its | |
| 1853 | − | // workspace, who may see it (all one filed with no repository has). | |
| 1854 | − | let members: HashSet<String> = match &wanted.item { | |
| 1855 | − | Some(item) => { | |
| 1856 | − | let usernames: Vec<String> = told | |
| 1857 | − | .iter() | |
| 1858 | − | .filter(|notice| notice.reason == Reason::Subscribed && elsewhere.contains(¬ice.username) && news.contains(¬ice.username)) | |
| 1859 | − | .map(|notice| notice.username.clone()) | |
| 1860 | − | .collect(); | |
| 1861 | − | if usernames.is_empty() { | |
| 1862 | − | HashSet::new() | |
| 1863 | − | } else { | |
| 1864 | − | let asked = MembersAmongArgs { workspace: item.workspace.clone(), usernames }; | |
| 1865 | − | g1t_kit::call::<_, Vec<String>>(sources.identity, "members_among", &asked) | |
| 1866 | − | .await | |
| 1867 | − | .unwrap_or_else(|error| { | |
| 1868 | − | worker::console_error!("inbox: could not tell who is a member of {}: {error}", item.workspace); | |
| 1869 | − | Vec::new() | |
| 1870 | − | }) | |
| 1871 | − | .into_iter() | |
| 1872 | − | .collect() | |
| 1873 | − | } | |
| 1874 | − | } | |
| 1875 | − | None => HashSet::new(), | |
| 1876 | − | }; | |
| 1877 | 1880 | Ok(told | |
| 1878 | 1881 | .into_iter() | |
| 1879 | 1882 | .filter(|notice| news.contains(¬ice.username)) | |
| 1880 | 1883 | .map(|notice| Written { | |
| 1881 | − | live: !(notice.reason == Reason::Subscribed | |
| 1882 | − | && elsewhere.contains(¬ice.username) | |
| 1883 | − | && !members.contains(¬ice.username)), | |
| 1884 | 1884 | notice, | |
| 1885 | 1885 | repo_id: wanted.repo_id.clone(), | |
| 1886 | 1886 | workspace: wanted.item.as_ref().map(|item| item.workspace.clone()), | |
| ⋯ | |||
| 1892 | 1892 | .collect()) | |
| 1893 | 1893 | } | |
| 1894 | 1894 | ||
| 1895 | + | /// Whether a notice is someone's only by watching a repository a work item | |
| 1896 | + | /// is linked to other than its origin: whether they may read it is not | |
| 1897 | + | /// known from that. | |
| 1898 | + | fn only_elsewhere(notice: &Notice, elsewhere: &HashSet<String>) -> bool { | |
| 1899 | + | notice.reason == Reason::Subscribed && elsewhere.contains(¬ice.username) | |
| 1900 | + | } | |
| 1901 | + | ||
| 1902 | + | /// Whether a work item's notice may be written: anyone told for a reason | |
| 1903 | + | /// of their own, or who watches its origin; one who only watches another | |
| 1904 | + | /// repository it is linked to (`elsewhere`) when they may read it, as a | |
| 1905 | + | /// member of its workspace (`members`) or because its origin is public. | |
| 1906 | + | /// An item with no origin is its workspace members' alone. | |
| 1907 | + | fn may_hear(notice: &Notice, elsewhere: &HashSet<String>, members: &HashSet<String>, origin_public: bool) -> bool { | |
| 1908 | + | !only_elsewhere(notice, elsewhere) || origin_public || members.contains(¬ice.username) | |
| 1909 | + | } | |
| 1910 | + | ||
| 1895 | 1911 | /// Brings a person's thread up with new activity, or starts it. `?1` id, | |
| 1896 | 1912 | /// `?2` username, `?3` thread, `?4` event id, `?5` event type, `?6` reason, | |
| 1897 | 1913 | /// `?7` severity, `?8` title, `?9` body, `?10` workspace, `?11` repo id, | |
| ⋯ | |||
| 2831 | 2847 | } | |
| 2832 | 2848 | ||
| 2833 | 2849 | #[test] | |
| 2850 | + | fn a_work_items_origin_is_its_events_repository_alone() { | |
| 2851 | + | // Filed in the workspace and linked to rep_2: no origin, whatever | |
| 2852 | + | // its data's `repoId` or the first of its `repoIds` say. | |
| 2853 | + | let data = json!({ "itemId": "wki_1", "workspace": "acme", "number": 40, "commentId": "cmt_1", "repoId": "rep_2", "repoIds": ["rep_2"] }); | |
| 2854 | + | for kind in ["work_item.opened", "comment.created"] { | |
| 2855 | + | let wanted = wants(&Event { repo_id: None, ..event(kind, None, data.clone()) }).unwrap_or_else(|| panic!("{kind}")); | |
| 2856 | + | assert_eq!(wanted.repo_id, None, "{kind}"); | |
| 2857 | + | } | |
| 2858 | + | // With an origin, the event's repository, not its data's. | |
| 2859 | + | let wanted = wants(&event("comment.created", None, data)).unwrap(); | |
| 2860 | + | assert_eq!(wanted.repo_id.as_deref(), Some("rep_1")); | |
| 2861 | + | } | |
| 2862 | + | ||
| 2863 | + | #[test] | |
| 2864 | + | fn who_only_watches_a_linked_repository_hears_of_a_work_item_they_may_read() { | |
| 2865 | + | let notice = |username: &str, reason: Reason| Notice { | |
| 2866 | + | username: username.into(), | |
| 2867 | + | reason, | |
| 2868 | + | severity: Severity::Info, | |
| 2869 | + | title: "acme#40 Secret plans".into(), | |
| 2870 | + | body: String::new(), | |
| 2871 | + | }; | |
| 2872 | + | let set = |names: &[&str]| names.iter().map(|name| (*name).to_owned()).collect::<HashSet<String>>(); | |
| 2873 | + | let elsewhere = set(&["cy", "dee"]); | |
| 2874 | + | let members = set(&["dee"]); | |
| 2875 | + | // Its origin's watchers and whoever is told for a reason of their own. | |
| 2876 | + | assert!(may_hear(¬ice("ana", Reason::Subscribed), &elsewhere, &members, false)); | |
| 2877 | + | assert!(may_hear(¬ice("cy", Reason::Mention), &elsewhere, &members, false)); | |
| 2878 | + | // A member of its workspace may read it. | |
| 2879 | + | assert!(may_hear(¬ice("dee", Reason::Subscribed), &elsewhere, &members, false)); | |
| 2880 | + | // Anyone else only when its origin is public. | |
| 2881 | + | assert!(!may_hear(¬ice("cy", Reason::Subscribed), &elsewhere, &members, false)); | |
| 2882 | + | assert!(may_hear(¬ice("cy", Reason::Subscribed), &elsewhere, &members, true)); | |
| 2883 | + | } | |
| 2884 | + | ||
| 2885 | + | #[test] | |
| 2834 | 2886 | fn a_work_items_watchers_are_its_origins_then_its_other_repositories() { | |
| 2835 | 2887 | let watcher = |name: &str, level: WatchLevel| Watcher { username: name.into(), level, events: Vec::new() }; | |
| 2836 | 2888 | let own = vec![watcher("ana", WatchLevel::All), watcher("bo", WatchLevel::Ignore)]; | |
Comments
Sign in to comment.