Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sync zulip stream membership #1604

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/toml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,37 @@ excluded-people = [
"rylev",
]

# Define the Zulip streams used by the team
# It's optional, and there can be more than one.
#
# This will remove anyone who isn't in the team from the stream
# so it should only be used for private streams at the moment.
[[zulip-streams]]
# The name of the Zulip stream (required)
name = "t-overlords/private"
# This can be set to false to avoid including all the team members in the stream
# It's useful if you want to create the stream with a different set of members
# It's optional, and the default is `true`.
include-team-members = true
# Include the following extra people in the Zulip stream. Their email address
# or Zulip id will be fetched from their TOML in people/ (optional).
extra-people = [
"alexcrichton",
]
# Include the following Zulip ids in the Zulip stream (optional).
extra-zulip-ids = [
1234
]
# Include all the members of the following teams in the Zulip stream
# (optional).
extra-teams = [
"bots-nursery",
]
# Exclude the following people in the Zulip stream (optional).
excluded-people = [
"rylev",
]

# Roles to define in Discord.
[[discord-roles]]
# The name of the role.
Expand Down
20 changes: 20 additions & 0 deletions rust_team_data/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ pub struct ZulipGroups {
pub groups: IndexMap<String, ZulipGroup>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ZulipStream {
pub name: String,
pub members: Vec<ZulipStreamMember>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ZulipStreamMember {
// TODO(rylev): this variant can be removed once
// it is verified that no one is relying on it
Email(String),
Id(u64),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ZulipStreams {
pub streams: IndexMap<String, ZulipStream>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Permission {
pub people: Vec<PermissionPerson>,
Expand Down
12 changes: 11 additions & 1 deletion src/data.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::schema::{Config, List, Person, Repo, Team, ZulipGroup};
use crate::schema::{Config, List, Person, Repo, Team, ZulipGroup, ZulipStream};
use anyhow::{bail, Context as _, Error};
use serde::de::DeserializeOwned;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -140,6 +140,16 @@ impl Data {
Ok(groups)
}

pub(crate) fn zulip_streams(&self) -> Result<HashMap<String, ZulipStream>, Error> {
let mut streams = HashMap::new();
for team in self.teams() {
for stream in team.zulip_streams(self)? {
streams.insert(stream.name().to_string(), stream);
}
}
Ok(streams)
}

pub(crate) fn team(&self, name: &str) -> Option<&Team> {
self.teams.get(name)
}
Expand Down
168 changes: 122 additions & 46 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ pub(crate) struct Team {
lists: Vec<TeamList>,
#[serde(default)]
zulip_groups: Vec<RawZulipGroup>,
#[serde(default)]
zulip_streams: Vec<RawZulipStream>,
discord_roles: Option<Vec<DiscordRole>>,
}

Expand Down Expand Up @@ -368,6 +370,51 @@ impl Team {
Ok(lists)
}

/// `on_exclude_not_included` is a function that is returned when an excluded member
/// wasn't included.
fn expand_zulip_membership(
davidtwco marked this conversation as resolved.
Show resolved Hide resolved
&self,
data: &Data,
common: &RawZulipCommon,
on_exclude_not_included: impl Fn(&str) -> Error,
) -> Result<Vec<ZulipMember>, Error> {
let mut members = if common.include_team_members {
self.members(data)?
} else {
HashSet::new()
};
for person in &common.extra_people {
members.insert(person.as_str());
}
for team in &common.extra_teams {
let team = data
.team(team)
.ok_or_else(|| format_err!("team {} is missing", team))?;
members.extend(team.members(data)?);
}
for excluded in &common.excluded_people {
if !members.remove(excluded.as_str()) {
return Err(on_exclude_not_included(excluded));
}
}

let mut final_members = Vec::new();
for member in members.iter() {
let member = data
.person(member)
.ok_or_else(|| format_err!("{} does not have a person configuration", member))?;
let member = match (member.github.clone(), member.zulip_id) {
(github, Some(zulip_id)) => ZulipMember::MemberWithId { github, zulip_id },
(github, _) => ZulipMember::MemberWithoutId { github },
};
final_members.push(member);
}
for &extra in &common.extra_zulip_ids {
final_members.push(ZulipMember::JustId(extra));
}
Ok(final_members)
}

pub(crate) fn raw_zulip_groups(&self) -> &[RawZulipGroup] {
&self.zulip_groups
}
Expand All @@ -377,48 +424,43 @@ impl Team {
let zulip_groups = &self.zulip_groups;

for raw_group in zulip_groups {
let mut group = ZulipGroup {
name: raw_group.name.clone(),
includes_team_members: raw_group.include_team_members,
members: Vec::new(),
};
groups.push(ZulipGroup(ZulipCommon {
name: raw_group.common.name.clone(),
includes_team_members: raw_group.common.include_team_members,
members: self.expand_zulip_membership(
data,
&raw_group.common,
|excluded| {
format_err!("'{excluded}' was specifically excluded from the Zulip group '{}' but they were already not included", raw_group.common.name)
},
)?,
}));
}
Ok(groups)
}

let mut members = if raw_group.include_team_members {
self.members(data)?
} else {
HashSet::new()
};
for person in &raw_group.extra_people {
members.insert(person.as_str());
}
for team in &raw_group.extra_teams {
let team = data
.team(team)
.ok_or_else(|| format_err!("team {} is missing", team))?;
members.extend(team.members(data)?);
}
for excluded in &raw_group.excluded_people {
if !members.remove(excluded.as_str()) {
bail!("'{excluded}' was specifically excluded from the Zulip group '{}' but they were already not included", raw_group.name);
}
}
pub(crate) fn raw_zulip_streams(&self) -> &[RawZulipStream] {
&self.zulip_streams
}

for member in members.iter() {
let member = data.person(member).ok_or_else(|| {
format_err!("{} does not have a person configuration", member)
})?;
let member = match (member.github.clone(), member.zulip_id) {
(github, Some(zulip_id)) => ZulipGroupMember::MemberWithId { github, zulip_id },
(github, _) => ZulipGroupMember::MemberWithoutId { github },
};
group.members.push(member);
}
for &extra in &raw_group.extra_zulip_ids {
group.members.push(ZulipGroupMember::JustId(extra));
}
groups.push(group);
pub(crate) fn zulip_streams(&self, data: &Data) -> Result<Vec<ZulipStream>, Error> {
let mut streams = Vec::new();
let zulip_streams = self.raw_zulip_streams();

for raw_stream in zulip_streams {
streams.push(ZulipStream(ZulipCommon {
name: raw_stream.common.name.clone(),
includes_team_members: raw_stream.common.include_team_members,
members: self.expand_zulip_membership(
data,
&raw_stream.common,
|excluded| {
format_err!("'{excluded}' was specifically excluded from the Zulip stream '{}' but they were already not included", raw_stream.common.name)
},
)?,
}));
}
Ok(groups)
Ok(streams)
}

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

#[derive(serde_derive::Deserialize, Debug)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct RawZulipGroup {
pub(crate) struct RawZulipCommon {
pub(crate) name: String,
#[serde(default = "default_true")]
pub(crate) include_team_members: bool,
Expand All @@ -691,6 +733,20 @@ pub(crate) struct RawZulipGroup {
pub(crate) excluded_people: Vec<String>,
}

#[derive(serde_derive::Deserialize, Debug)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct RawZulipGroup {
#[serde(flatten)]
pub(crate) common: RawZulipCommon,
}

#[derive(serde_derive::Deserialize, Debug)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub(crate) struct RawZulipStream {
#[serde(flatten)]
pub(crate) common: RawZulipCommon,
}

#[derive(Debug)]
pub(crate) struct List {
address: String,
Expand All @@ -708,29 +764,49 @@ impl List {
}

#[derive(Debug)]
pub(crate) struct ZulipGroup {
pub(crate) struct ZulipCommon {
name: String,
includes_team_members: bool,
members: Vec<ZulipGroupMember>,
members: Vec<ZulipMember>,
}

impl ZulipGroup {
impl ZulipCommon {
pub(crate) fn name(&self) -> &str {
&self.name
}

/// Whether the group includes the members of the team its associated
/// Whether the group/stream includes the members of the associated team?
pub(crate) fn includes_team_members(&self) -> bool {
self.includes_team_members
}

pub(crate) fn members(&self) -> &[ZulipGroupMember] {
pub(crate) fn members(&self) -> &[ZulipMember] {
&self.members
}
}

#[derive(Debug)]
pub(crate) struct ZulipGroup(ZulipCommon);

impl std::ops::Deref for ZulipGroup {
type Target = ZulipCommon;
fn deref(&self) -> &Self::Target {
&self.0
}
}

#[derive(Debug)]
pub(crate) struct ZulipStream(ZulipCommon);

impl std::ops::Deref for ZulipStream {
type Target = ZulipCommon;
fn deref(&self) -> &Self::Target {
&self.0
}
}

#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub(crate) enum ZulipGroupMember {
pub(crate) enum ZulipMember {
MemberWithId { github: String, zulip_id: u64 },
JustId(u64),
MemberWithoutId { github: String },
Expand Down
42 changes: 36 additions & 6 deletions src/static_api.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use crate::data::Data;
use crate::schema::{
Bot, Email, MergeBot, Permissions, RepoPermission, TeamKind, ZulipGroupMember,
};
use crate::schema::{Bot, Email, MergeBot, Permissions, RepoPermission, TeamKind, ZulipMember};
use anyhow::{ensure, Context as _, Error};
use indexmap::IndexMap;
use log::info;
Expand Down Expand Up @@ -30,6 +28,7 @@ impl<'a> Generator<'a> {
self.generate_repos()?;
self.generate_lists()?;
self.generate_zulip_groups()?;
self.generate_zulip_streams()?;
self.generate_permissions()?;
self.generate_rfcbot()?;
self.generate_zulip_map()?;
Expand Down Expand Up @@ -285,13 +284,13 @@ impl<'a> Generator<'a> {
members: members
.into_iter()
.filter_map(|m| match m {
ZulipGroupMember::MemberWithId { zulip_id, .. } => {
ZulipMember::MemberWithId { zulip_id, .. } => {
Some(v1::ZulipGroupMember::Id(zulip_id))
}
ZulipGroupMember::JustId(zulip_id) => {
ZulipMember::JustId(zulip_id) => {
Some(v1::ZulipGroupMember::Id(zulip_id))
}
ZulipGroupMember::MemberWithoutId { .. } => None,
ZulipMember::MemberWithoutId { .. } => None,
})
.collect(),
},
Expand All @@ -303,6 +302,37 @@ impl<'a> Generator<'a> {
Ok(())
}

fn generate_zulip_streams(&self) -> Result<(), Error> {
let mut streams = IndexMap::new();

for stream in self.data.zulip_streams()?.values() {
let mut members = stream.members().to_vec();
members.sort();
streams.insert(
stream.name().to_string(),
v1::ZulipStream {
name: stream.name().to_string(),
members: members
.into_iter()
.filter_map(|m| match m {
ZulipMember::MemberWithId { zulip_id, .. } => {
Some(v1::ZulipStreamMember::Id(zulip_id))
}
ZulipMember::JustId(zulip_id) => {
Some(v1::ZulipStreamMember::Id(zulip_id))
}
ZulipMember::MemberWithoutId { .. } => None,
})
.collect(),
},
);
}

streams.sort_keys();
self.add("v1/zulip-streams.json", &v1::ZulipStreams { streams })?;
Ok(())
}

fn generate_permissions(&self) -> Result<(), Error> {
for perm in &Permissions::available(self.data.config()) {
let allowed = crate::permissions::allowed_people(self.data, perm)?;
Expand Down
Loading