Skip to content

Commit 46ca96b

Browse files
committed
zulip: sync stream membership
1 parent f4a9c02 commit 46ca96b

File tree

7 files changed

+208
-1
lines changed

7 files changed

+208
-1
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: 95 additions & 0 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

@@ -421,6 +423,60 @@ impl Team {
421423
Ok(groups)
422424
}
423425

426+
pub(crate) fn raw_zulip_streams(&self) -> &[RawZulipStream] {
427+
&self.zulip_streams
428+
}
429+
430+
pub(crate) fn zulip_streams(&self, data: &Data) -> Result<Vec<ZulipStream>, Error> {
431+
let mut streams = Vec::new();
432+
let zulip_streams = self.raw_zulip_streams();
433+
434+
for raw_stream in zulip_streams {
435+
let mut stream = ZulipStream {
436+
name: raw_stream.name.clone(),
437+
members: Vec::new(),
438+
};
439+
440+
let mut members = if raw_stream.include_team_members {
441+
self.members(data)?
442+
} else {
443+
HashSet::new()
444+
};
445+
for person in &raw_stream.extra_people {
446+
members.insert(person.as_str());
447+
}
448+
for team in &raw_stream.extra_teams {
449+
let team = data
450+
.team(team)
451+
.ok_or_else(|| format_err!("team {} is missing", team))?;
452+
members.extend(team.members(data)?);
453+
}
454+
for excluded in &raw_stream.excluded_people {
455+
if !members.remove(excluded.as_str()) {
456+
bail!("'{excluded}' was specifically excluded from the Zulip stream '{}' but they were already not included", raw_stream.name);
457+
}
458+
}
459+
460+
for member in members.iter() {
461+
let member = data.person(member).ok_or_else(|| {
462+
format_err!("{} does not have a person configuration", member)
463+
})?;
464+
let member = match (member.github.clone(), member.zulip_id) {
465+
(github, Some(zulip_id)) => {
466+
ZulipStreamMember::MemberWithId { github, zulip_id }
467+
}
468+
(github, _) => ZulipStreamMember::MemberWithoutId { github },
469+
};
470+
stream.members.push(member);
471+
}
472+
for &extra in &raw_stream.extra_zulip_ids {
473+
stream.members.push(ZulipStreamMember::JustId(extra));
474+
}
475+
streams.push(stream);
476+
}
477+
Ok(streams)
478+
}
479+
424480
pub(crate) fn permissions(&self) -> &Permissions {
425481
&self.permissions
426482
}
@@ -691,6 +747,22 @@ pub(crate) struct RawZulipGroup {
691747
pub(crate) excluded_people: Vec<String>,
692748
}
693749

750+
#[derive(serde_derive::Deserialize, Debug)]
751+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
752+
pub(crate) struct RawZulipStream {
753+
pub(crate) name: String,
754+
#[serde(default = "default_true")]
755+
pub(crate) include_team_members: bool,
756+
#[serde(default)]
757+
pub(crate) extra_people: Vec<String>,
758+
#[serde(default)]
759+
pub(crate) extra_zulip_ids: Vec<u64>,
760+
#[serde(default)]
761+
pub(crate) extra_teams: Vec<String>,
762+
#[serde(default)]
763+
pub(crate) excluded_people: Vec<String>,
764+
}
765+
694766
#[derive(Debug)]
695767
pub(crate) struct List {
696768
address: String,
@@ -736,6 +808,29 @@ pub(crate) enum ZulipGroupMember {
736808
MemberWithoutId { github: String },
737809
}
738810

811+
#[derive(Debug)]
812+
pub(crate) struct ZulipStream {
813+
name: String,
814+
members: Vec<ZulipStreamMember>,
815+
}
816+
817+
impl ZulipStream {
818+
pub(crate) fn name(&self) -> &str {
819+
&self.name
820+
}
821+
822+
pub(crate) fn members(&self) -> &[ZulipStreamMember] {
823+
&self.members
824+
}
825+
}
826+
827+
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
828+
pub(crate) enum ZulipStreamMember {
829+
MemberWithId { github: String, zulip_id: u64 },
830+
JustId(u64),
831+
MemberWithoutId { github: String },
832+
}
833+
739834
fn default_true() -> bool {
740835
true
741836
}

src/static_api.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::data::Data;
22
use crate::schema::{
33
Bot, Email, MergeBot, Permissions, RepoPermission, TeamKind, ZulipGroupMember,
4+
ZulipStreamMember,
45
};
56
use anyhow::{ensure, Context as _, Error};
67
use indexmap::IndexMap;
@@ -30,6 +31,7 @@ impl<'a> Generator<'a> {
3031
self.generate_repos()?;
3132
self.generate_lists()?;
3233
self.generate_zulip_groups()?;
34+
self.generate_zulip_streams()?;
3335
self.generate_permissions()?;
3436
self.generate_rfcbot()?;
3537
self.generate_zulip_map()?;
@@ -303,6 +305,37 @@ impl<'a> Generator<'a> {
303305
Ok(())
304306
}
305307

308+
fn generate_zulip_streams(&self) -> Result<(), Error> {
309+
let mut streams = IndexMap::new();
310+
311+
for stream in self.data.zulip_streams()?.values() {
312+
let mut members = stream.members().to_vec();
313+
members.sort();
314+
streams.insert(
315+
stream.name().to_string(),
316+
v1::ZulipStream {
317+
name: stream.name().to_string(),
318+
members: members
319+
.into_iter()
320+
.filter_map(|m| match m {
321+
ZulipStreamMember::MemberWithId { zulip_id, .. } => {
322+
Some(v1::ZulipStreamMember::Id(zulip_id))
323+
}
324+
ZulipStreamMember::JustId(zulip_id) => {
325+
Some(v1::ZulipStreamMember::Id(zulip_id))
326+
}
327+
ZulipStreamMember::MemberWithoutId { .. } => None,
328+
})
329+
.collect(),
330+
},
331+
);
332+
}
333+
334+
streams.sort_keys();
335+
self.add("v1/zulip-streams.json", &v1::ZulipStreams { streams })?;
336+
Ok(())
337+
}
338+
306339
fn generate_permissions(&self) -> Result<(), Error> {
307340
for perm in &Permissions::available(self.data.config()) {
308341
let allowed = crate::permissions::allowed_people(self.data, perm)?;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"streams": {
3+
"t-foo/private": {
4+
"name": "t-foo/private",
5+
"members": [
6+
{
7+
"id": 1234
8+
},
9+
{
10+
"id": 4321
11+
}
12+
]
13+
}
14+
}
15+
}

tests/static-api/teams/foo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ extra-teams = ["wg-test"]
4949

5050
[[zulip-groups]]
5151
name = "T-foo"
52+
53+
[[zulip-streams]]
54+
name = "t-foo/private"

0 commit comments

Comments
 (0)