Skip to content
Merged
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
7 changes: 7 additions & 0 deletions docs/toml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ excluded-people = [
[[zulip-streams]]
# The name of the Zulip stream (required)
name = "t-overlords/private"
# Whether newly subscribed members can access messages sent before they joined
# the stream. This is optional. If omitted, the current setting is left
# unchanged.
public-history = false
# 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`.
Expand All @@ -266,6 +270,9 @@ excluded-people = [
]
```

The `public-history` option corresponds to Zulip's
[`history_public_to_subscribers` channel setting](https://zulip.com/api/update-stream#parameter-history_public_to_subscribers).

### Configuring Zulip streams

> [!TIP]
Expand Down
2 changes: 2 additions & 0 deletions rust_team_data/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ pub struct ZulipGroups {
pub struct ZulipStream {
pub name: String,
pub members: Vec<ZulipStreamMember>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_history: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
Expand Down
40 changes: 27 additions & 13 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,17 +497,20 @@ impl Team {
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)
},
)?,
}));
streams.push(ZulipStream {
common: 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)
},
)?,
},
public_history: raw_stream.public_history,
});
}
Ok(streams)
}
Expand Down Expand Up @@ -758,6 +761,8 @@ pub(crate) struct RawZulipGroup {
pub(crate) struct RawZulipStream {
#[serde(flatten)]
pub(crate) common: RawZulipCommon,
#[serde(default)]
pub(crate) public_history: Option<bool>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -809,12 +814,21 @@ impl std::ops::Deref for ZulipGroup {
}

#[derive(Debug)]
pub(crate) struct ZulipStream(ZulipCommon);
pub(crate) struct ZulipStream {
common: ZulipCommon,
public_history: Option<bool>,
}

impl ZulipStream {
pub(crate) fn public_history(&self) -> Option<bool> {
self.public_history
}
}

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

Expand Down
1 change: 1 addition & 0 deletions src/static_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ impl<'a> Generator<'a> {
ZulipMember::MemberWithoutId { .. } => None,
})
.collect(),
public_history: stream.public_history(),
},
);
}
Expand Down
33 changes: 33 additions & 0 deletions src/sync/zulip/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,38 @@ impl ZulipApi {
Ok(())
}

/// Set whether a Zulip stream's history is public to new subscribers.
///
/// See <https://zulip.com/api/update-stream#parameter-history_public_to_subscribers>.
pub(crate) async fn update_stream_public_history(
&self,
stream_id: u64,
public_history: bool,
) -> anyhow::Result<()> {
log::info!("updating stream {stream_id} to public_history={public_history}");

if self.dry_run {
return Ok(());
}

let history_public_to_subscribers = public_history.to_string();
Comment thread
ubiratansoares marked this conversation as resolved.
let mut form = HashMap::new();
form.insert(
"history_public_to_subscribers",
history_public_to_subscribers.as_str(),
);

self.req(
reqwest::Method::PATCH,
&format!("/streams/{stream_id}"),
Some(form),
)
.await?
.error_for_status()?;

Ok(())
}

/// Perform a request against the Zulip API
async fn req(
&self,
Expand Down Expand Up @@ -330,6 +362,7 @@ pub(crate) struct ZulipStream {
pub(crate) stream_id: u64,
pub(crate) name: String,
pub(crate) invite_only: bool,
pub(crate) history_public_to_subscribers: bool,
}

/// Membership of a Zulip stream
Expand Down
112 changes: 103 additions & 9 deletions src/sync/zulip/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ use std::collections::BTreeMap;

pub(crate) struct SyncZulip {
zulip_controller: ZulipController,
stream_definitions: BTreeMap<String, Vec<u64>>,
stream_definitions: BTreeMap<String, StreamDefinition>,
user_group_definitions: BTreeMap<String, Vec<u64>>,
}

struct StreamDefinition {
member_ids: Vec<u64>,
public_history: Option<bool>,
}

impl SyncZulip {
pub(crate) async fn new(
username: String,
Expand All @@ -41,15 +46,23 @@ impl SyncZulip {

pub(crate) async fn diff_all(&self) -> anyhow::Result<Diff> {
let stream_membership_diffs = futures_util::stream::iter(&self.stream_definitions)
.filter_map(|(stream_name, member_ids)| async move {
self.diff_stream_membership(stream_name, member_ids)
.filter_map(|(stream_name, definition)| async move {
self.diff_stream_membership(stream_name, &definition.member_ids)
.await
.transpose()
})
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<anyhow::Result<Vec<_>>>()?;
let stream_history_diffs = self
.stream_definitions
.iter()
.filter_map(|(stream_name, definition)| {
self.diff_stream_history(stream_name, definition.public_history)
.transpose()
})
.collect::<anyhow::Result<Vec<_>>>()?;
let user_group_diffs = self
.user_group_definitions
.iter()
Expand All @@ -60,10 +73,39 @@ impl SyncZulip {
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(Diff {
user_group_diffs,
stream_history_diffs,
stream_membership_diffs,
})
}

fn diff_stream_history(
&self,
stream_name: &str,
public_history: Option<bool>,
) -> anyhow::Result<Option<UpdateStreamHistoryDiff>> {
let Some(public_history) = public_history else {
return Ok(None);
};

let stream = self
.zulip_controller
.stream_from_name(stream_name)
.with_context(|| format!("no '{stream_name}' stream found on Zulip"))?;
if stream.history_public_to_subscribers == public_history {
log::debug!(
"'{stream_name}' stream ({}) already has public_history={public_history}",
stream.stream_id
);
Ok(None)
} else {
Ok(Some(UpdateStreamHistoryDiff {
stream_name: stream_name.to_owned(),
stream_id: stream.stream_id,
public_history,
}))
}
}

fn diff_user_group(
&self,
user_group_name: &str,
Expand Down Expand Up @@ -174,12 +216,12 @@ impl SyncZulip {
}

async fn add_rust_lang_owner_to_private_streams(
stream_definitions: &mut BTreeMap<String, Vec<u64>>,
stream_definitions: &mut BTreeMap<String, StreamDefinition>,
zulip_controller: &ZulipController,
) -> anyhow::Result<()> {
// Id of the `rust-lang-owner` Zulip user.
let rust_lang_owner_id = 494485;
for (stream_name, members) in stream_definitions {
for (stream_name, definition) in stream_definitions {
let stream_id = zulip_controller
.stream_id_from_name(stream_name)
.with_context(|| {
Expand All @@ -194,14 +236,15 @@ async fn add_rust_lang_owner_to_private_streams(
.is_stream_private(stream_id)
.await?;
if is_stream_private {
members.insert(0, rust_lang_owner_id);
definition.member_ids.insert(0, rust_lang_owner_id);
}
}
Ok(())
}

pub(crate) struct Diff {
user_group_diffs: Vec<UserGroupDiff>,
stream_history_diffs: Vec<UpdateStreamHistoryDiff>,
stream_membership_diffs: Vec<StreamMembershipDiff>,
}

Expand All @@ -210,14 +253,24 @@ impl Diff {
for user_group_diff in &self.user_group_diffs {
user_group_diff.apply(sync).await?;
}
for stream_history_diff in &self.stream_history_diffs {
stream_history_diff.apply(sync).await?;
}
for stream_membership_diff in &self.stream_membership_diffs {
stream_membership_diff.apply(sync).await?;
}
Ok(())
}

pub(crate) fn is_empty(&self) -> bool {
self.user_group_diffs.is_empty() && self.stream_membership_diffs.is_empty()
let Self {
user_group_diffs,
stream_history_diffs,
stream_membership_diffs,
} = self;
user_group_diffs.is_empty()
&& stream_history_diffs.is_empty()
&& stream_membership_diffs.is_empty()
}
}

Expand All @@ -230,6 +283,13 @@ impl std::fmt::Display for Diff {
}
}

if !&self.stream_history_diffs.is_empty() {
writeln!(f, "πŸ’» Stream History Diffs:")?;
for stream_history_diff in &self.stream_history_diffs {
write!(f, "{stream_history_diff}")?;
}
}

if !&self.stream_membership_diffs.is_empty() {
writeln!(f, "πŸ’» Stream Membership Diffs:")?;
for stream_membership_diff in &self.stream_membership_diffs {
Expand All @@ -241,6 +301,30 @@ impl std::fmt::Display for Diff {
}
}

struct UpdateStreamHistoryDiff {
stream_name: String,
stream_id: u64,
public_history: bool,
}

impl UpdateStreamHistoryDiff {
async fn apply(&self, sync: &SyncZulip) -> anyhow::Result<()> {
sync.zulip_controller
.zulip_api
.update_stream_public_history(self.stream_id, self.public_history)
.await
}
}

impl std::fmt::Display for UpdateStreamHistoryDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "πŸ“ Updating stream history:")?;
writeln!(f, " Name: {}", self.stream_name)?;
writeln!(f, " ID: {}", self.stream_id)?;
writeln!(f, " Public history: {}", self.public_history)
}
}

enum StreamMembershipDiff {
Update(UpdateStreamMembershipDiff),
}
Expand Down Expand Up @@ -424,7 +508,7 @@ async fn get_user_group_definitions(
async fn get_stream_definitions(
team_api: &TeamApi,
zulip_api: &ZulipApi,
) -> anyhow::Result<BTreeMap<String, Vec<u64>>> {
) -> anyhow::Result<BTreeMap<String, StreamDefinition>> {
let email_map = zulip_api
.get_users()
.await?
Expand All @@ -451,7 +535,13 @@ async fn get_stream_definitions(
ZulipStreamMember::Id(id) => Some(*id),
})
.collect::<Vec<_>>();
(name, member_ids)
(
name,
StreamDefinition {
member_ids,
public_history: stream.public_history,
},
)
})
.collect();
Ok(stream_definitions)
Expand Down Expand Up @@ -503,6 +593,10 @@ impl ZulipController {
self.stream_ids.get(stream_name).map(|st| st.stream_id)
}

fn stream_from_name(&self, stream_name: &str) -> Option<&ZulipStream> {
self.stream_ids.get(stream_name)
}

/// Create a user group with a certain name, description, and members
async fn create_user_group(
&self,
Expand Down
1 change: 1 addition & 0 deletions teams/rust-analyzer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ name = "T-rust-analyzer"

[[zulip-streams]]
name = "t-compiler/rust-analyzer-private"
public-history = false
3 changes: 2 additions & 1 deletion tests/static-api/_expected/v1/zulip-streams.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
{
"id": 4321
}
]
],
"public_history": false
}
}
}
1 change: 1 addition & 0 deletions tests/static-api/teams/foo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ name = "T-foo"

[[zulip-streams]]
name = "t-foo/private"
public-history = false