Skip to content

Commit d7b6ef1

Browse files
authored
Merge pull request #1604 from davidtwco/stream-membership
sync zulip stream membership
2 parents 2a5701d + 87644dc commit d7b6ef1

File tree

9 files changed

+321
-67
lines changed

9 files changed

+321
-67
lines changed

docs/toml-schema.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,37 @@ excluded-people = [
209209
"rylev",
210210
]
211211

212+
# Define the Zulip streams used by the team
213+
# It's optional, and there can be more than one.
214+
#
215+
# This will remove anyone who isn't in the team from the stream
216+
# so it should only be used for private streams at the moment.
217+
[[zulip-streams]]
218+
# The name of the Zulip stream (required)
219+
name = "t-overlords/private"
220+
# This can be set to false to avoid including all the team members in the stream
221+
# It's useful if you want to create the stream with a different set of members
222+
# It's optional, and the default is `true`.
223+
include-team-members = true
224+
# Include the following extra people in the Zulip stream. Their email address
225+
# or Zulip id will be fetched from their TOML in people/ (optional).
226+
extra-people = [
227+
"alexcrichton",
228+
]
229+
# Include the following Zulip ids in the Zulip stream (optional).
230+
extra-zulip-ids = [
231+
1234
232+
]
233+
# Include all the members of the following teams in the Zulip stream
234+
# (optional).
235+
extra-teams = [
236+
"bots-nursery",
237+
]
238+
# Exclude the following people in the Zulip stream (optional).
239+
excluded-people = [
240+
"rylev",
241+
]
242+
212243
# Roles to define in Discord.
213244
[[discord-roles]]
214245
# The name of the role.

rust_team_data/src/v1.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,26 @@ pub struct ZulipGroups {
126126
pub groups: IndexMap<String, ZulipGroup>,
127127
}
128128

129+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130+
pub struct ZulipStream {
131+
pub name: String,
132+
pub members: Vec<ZulipStreamMember>,
133+
}
134+
135+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
136+
#[serde(rename_all = "snake_case")]
137+
pub enum ZulipStreamMember {
138+
// TODO(rylev): this variant can be removed once
139+
// it is verified that no one is relying on it
140+
Email(String),
141+
Id(u64),
142+
}
143+
144+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
145+
pub struct ZulipStreams {
146+
pub streams: IndexMap<String, ZulipStream>,
147+
}
148+
129149
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130150
pub struct Permission {
131151
pub people: Vec<PermissionPerson>,

src/data.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::schema::{Config, List, Person, Repo, Team, ZulipGroup};
1+
use crate::schema::{Config, List, Person, Repo, Team, ZulipGroup, ZulipStream};
22
use anyhow::{bail, Context as _, Error};
33
use serde::de::DeserializeOwned;
44
use std::collections::{HashMap, HashSet};
@@ -140,6 +140,16 @@ impl Data {
140140
Ok(groups)
141141
}
142142

143+
pub(crate) fn zulip_streams(&self) -> Result<HashMap<String, ZulipStream>, Error> {
144+
let mut streams = HashMap::new();
145+
for team in self.teams() {
146+
for stream in team.zulip_streams(self)? {
147+
streams.insert(stream.name().to_string(), stream);
148+
}
149+
}
150+
Ok(streams)
151+
}
152+
143153
pub(crate) fn team(&self, name: &str) -> Option<&Team> {
144154
self.teams.get(name)
145155
}

src/schema.rs

Lines changed: 122 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ pub(crate) struct Team {
179179
lists: Vec<TeamList>,
180180
#[serde(default)]
181181
zulip_groups: Vec<RawZulipGroup>,
182+
#[serde(default)]
183+
zulip_streams: Vec<RawZulipStream>,
182184
discord_roles: Option<Vec<DiscordRole>>,
183185
}
184186

@@ -364,6 +366,51 @@ impl Team {
364366
Ok(lists)
365367
}
366368

369+
/// `on_exclude_not_included` is a function that is returned when an excluded member
370+
/// wasn't included.
371+
fn expand_zulip_membership(
372+
&self,
373+
data: &Data,
374+
common: &RawZulipCommon,
375+
on_exclude_not_included: impl Fn(&str) -> Error,
376+
) -> Result<Vec<ZulipMember>, Error> {
377+
let mut members = if common.include_team_members {
378+
self.members(data)?
379+
} else {
380+
HashSet::new()
381+
};
382+
for person in &common.extra_people {
383+
members.insert(person.as_str());
384+
}
385+
for team in &common.extra_teams {
386+
let team = data
387+
.team(team)
388+
.ok_or_else(|| format_err!("team {} is missing", team))?;
389+
members.extend(team.members(data)?);
390+
}
391+
for excluded in &common.excluded_people {
392+
if !members.remove(excluded.as_str()) {
393+
return Err(on_exclude_not_included(excluded));
394+
}
395+
}
396+
397+
let mut final_members = Vec::new();
398+
for member in members.iter() {
399+
let member = data
400+
.person(member)
401+
.ok_or_else(|| format_err!("{} does not have a person configuration", member))?;
402+
let member = match (member.github.clone(), member.zulip_id) {
403+
(github, Some(zulip_id)) => ZulipMember::MemberWithId { github, zulip_id },
404+
(github, _) => ZulipMember::MemberWithoutId { github },
405+
};
406+
final_members.push(member);
407+
}
408+
for &extra in &common.extra_zulip_ids {
409+
final_members.push(ZulipMember::JustId(extra));
410+
}
411+
Ok(final_members)
412+
}
413+
367414
pub(crate) fn raw_zulip_groups(&self) -> &[RawZulipGroup] {
368415
&self.zulip_groups
369416
}
@@ -373,48 +420,43 @@ impl Team {
373420
let zulip_groups = &self.zulip_groups;
374421

375422
for raw_group in zulip_groups {
376-
let mut group = ZulipGroup {
377-
name: raw_group.name.clone(),
378-
includes_team_members: raw_group.include_team_members,
379-
members: Vec::new(),
380-
};
423+
groups.push(ZulipGroup(ZulipCommon {
424+
name: raw_group.common.name.clone(),
425+
includes_team_members: raw_group.common.include_team_members,
426+
members: self.expand_zulip_membership(
427+
data,
428+
&raw_group.common,
429+
|excluded| {
430+
format_err!("'{excluded}' was specifically excluded from the Zulip group '{}' but they were already not included", raw_group.common.name)
431+
},
432+
)?,
433+
}));
434+
}
435+
Ok(groups)
436+
}
381437

382-
let mut members = if raw_group.include_team_members {
383-
self.members(data)?
384-
} else {
385-
HashSet::new()
386-
};
387-
for person in &raw_group.extra_people {
388-
members.insert(person.as_str());
389-
}
390-
for team in &raw_group.extra_teams {
391-
let team = data
392-
.team(team)
393-
.ok_or_else(|| format_err!("team {} is missing", team))?;
394-
members.extend(team.members(data)?);
395-
}
396-
for excluded in &raw_group.excluded_people {
397-
if !members.remove(excluded.as_str()) {
398-
bail!("'{excluded}' was specifically excluded from the Zulip group '{}' but they were already not included", raw_group.name);
399-
}
400-
}
438+
pub(crate) fn raw_zulip_streams(&self) -> &[RawZulipStream] {
439+
&self.zulip_streams
440+
}
401441

402-
for member in members.iter() {
403-
let member = data.person(member).ok_or_else(|| {
404-
format_err!("{} does not have a person configuration", member)
405-
})?;
406-
let member = match (member.github.clone(), member.zulip_id) {
407-
(github, Some(zulip_id)) => ZulipGroupMember::MemberWithId { github, zulip_id },
408-
(github, _) => ZulipGroupMember::MemberWithoutId { github },
409-
};
410-
group.members.push(member);
411-
}
412-
for &extra in &raw_group.extra_zulip_ids {
413-
group.members.push(ZulipGroupMember::JustId(extra));
414-
}
415-
groups.push(group);
442+
pub(crate) fn zulip_streams(&self, data: &Data) -> Result<Vec<ZulipStream>, Error> {
443+
let mut streams = Vec::new();
444+
let zulip_streams = self.raw_zulip_streams();
445+
446+
for raw_stream in zulip_streams {
447+
streams.push(ZulipStream(ZulipCommon {
448+
name: raw_stream.common.name.clone(),
449+
includes_team_members: raw_stream.common.include_team_members,
450+
members: self.expand_zulip_membership(
451+
data,
452+
&raw_stream.common,
453+
|excluded| {
454+
format_err!("'{excluded}' was specifically excluded from the Zulip stream '{}' but they were already not included", raw_stream.common.name)
455+
},
456+
)?,
457+
}));
416458
}
417-
Ok(groups)
459+
Ok(streams)
418460
}
419461

420462
pub(crate) fn permissions(&self) -> &Permissions {
@@ -677,7 +719,7 @@ pub(crate) struct TeamList {
677719

678720
#[derive(serde_derive::Deserialize, Debug)]
679721
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
680-
pub(crate) struct RawZulipGroup {
722+
pub(crate) struct RawZulipCommon {
681723
pub(crate) name: String,
682724
#[serde(default = "default_true")]
683725
pub(crate) include_team_members: bool,
@@ -691,6 +733,20 @@ pub(crate) struct RawZulipGroup {
691733
pub(crate) excluded_people: Vec<String>,
692734
}
693735

736+
#[derive(serde_derive::Deserialize, Debug)]
737+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
738+
pub(crate) struct RawZulipGroup {
739+
#[serde(flatten)]
740+
pub(crate) common: RawZulipCommon,
741+
}
742+
743+
#[derive(serde_derive::Deserialize, Debug)]
744+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
745+
pub(crate) struct RawZulipStream {
746+
#[serde(flatten)]
747+
pub(crate) common: RawZulipCommon,
748+
}
749+
694750
#[derive(Debug)]
695751
pub(crate) struct List {
696752
address: String,
@@ -708,29 +764,49 @@ impl List {
708764
}
709765

710766
#[derive(Debug)]
711-
pub(crate) struct ZulipGroup {
767+
pub(crate) struct ZulipCommon {
712768
name: String,
713769
includes_team_members: bool,
714-
members: Vec<ZulipGroupMember>,
770+
members: Vec<ZulipMember>,
715771
}
716772

717-
impl ZulipGroup {
773+
impl ZulipCommon {
718774
pub(crate) fn name(&self) -> &str {
719775
&self.name
720776
}
721777

722-
/// Whether the group includes the members of the team its associated
778+
/// Whether the group/stream includes the members of the associated team?
723779
pub(crate) fn includes_team_members(&self) -> bool {
724780
self.includes_team_members
725781
}
726782

727-
pub(crate) fn members(&self) -> &[ZulipGroupMember] {
783+
pub(crate) fn members(&self) -> &[ZulipMember] {
728784
&self.members
729785
}
730786
}
731787

788+
#[derive(Debug)]
789+
pub(crate) struct ZulipGroup(ZulipCommon);
790+
791+
impl std::ops::Deref for ZulipGroup {
792+
type Target = ZulipCommon;
793+
fn deref(&self) -> &Self::Target {
794+
&self.0
795+
}
796+
}
797+
798+
#[derive(Debug)]
799+
pub(crate) struct ZulipStream(ZulipCommon);
800+
801+
impl std::ops::Deref for ZulipStream {
802+
type Target = ZulipCommon;
803+
fn deref(&self) -> &Self::Target {
804+
&self.0
805+
}
806+
}
807+
732808
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
733-
pub(crate) enum ZulipGroupMember {
809+
pub(crate) enum ZulipMember {
734810
MemberWithId { github: String, zulip_id: u64 },
735811
JustId(u64),
736812
MemberWithoutId { github: String },

src/static_api.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use crate::data::Data;
2-
use crate::schema::{
3-
Bot, Email, MergeBot, Permissions, RepoPermission, TeamKind, ZulipGroupMember,
4-
};
2+
use crate::schema::{Bot, Email, MergeBot, Permissions, RepoPermission, TeamKind, ZulipMember};
53
use anyhow::{ensure, Context as _, Error};
64
use indexmap::IndexMap;
75
use log::info;
@@ -30,6 +28,7 @@ impl<'a> Generator<'a> {
3028
self.generate_repos()?;
3129
self.generate_lists()?;
3230
self.generate_zulip_groups()?;
31+
self.generate_zulip_streams()?;
3332
self.generate_permissions()?;
3433
self.generate_rfcbot()?;
3534
self.generate_zulip_map()?;
@@ -292,13 +291,13 @@ impl<'a> Generator<'a> {
292291
members: members
293292
.into_iter()
294293
.filter_map(|m| match m {
295-
ZulipGroupMember::MemberWithId { zulip_id, .. } => {
294+
ZulipMember::MemberWithId { zulip_id, .. } => {
296295
Some(v1::ZulipGroupMember::Id(zulip_id))
297296
}
298-
ZulipGroupMember::JustId(zulip_id) => {
297+
ZulipMember::JustId(zulip_id) => {
299298
Some(v1::ZulipGroupMember::Id(zulip_id))
300299
}
301-
ZulipGroupMember::MemberWithoutId { .. } => None,
300+
ZulipMember::MemberWithoutId { .. } => None,
302301
})
303302
.collect(),
304303
},
@@ -310,6 +309,37 @@ impl<'a> Generator<'a> {
310309
Ok(())
311310
}
312311

312+
fn generate_zulip_streams(&self) -> Result<(), Error> {
313+
let mut streams = IndexMap::new();
314+
315+
for stream in self.data.zulip_streams()?.values() {
316+
let mut members = stream.members().to_vec();
317+
members.sort();
318+
streams.insert(
319+
stream.name().to_string(),
320+
v1::ZulipStream {
321+
name: stream.name().to_string(),
322+
members: members
323+
.into_iter()
324+
.filter_map(|m| match m {
325+
ZulipMember::MemberWithId { zulip_id, .. } => {
326+
Some(v1::ZulipStreamMember::Id(zulip_id))
327+
}
328+
ZulipMember::JustId(zulip_id) => {
329+
Some(v1::ZulipStreamMember::Id(zulip_id))
330+
}
331+
ZulipMember::MemberWithoutId { .. } => None,
332+
})
333+
.collect(),
334+
},
335+
);
336+
}
337+
338+
streams.sort_keys();
339+
self.add("v1/zulip-streams.json", &v1::ZulipStreams { streams })?;
340+
Ok(())
341+
}
342+
313343
fn generate_permissions(&self) -> Result<(), Error> {
314344
for perm in &Permissions::available(self.data.config()) {
315345
let allowed = crate::permissions::allowed_people(self.data, perm)?;

0 commit comments

Comments
 (0)