From 5b4c66f6859482c16b6e5de10bc2a36457d3fa30 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 15 Sep 2023 23:26:20 +0200 Subject: [PATCH 1/2] Implement channel rename For now, chathistory still matches based on channel name, so it's not carried over to the new channel. --- configs/network_config.json | 1 + sable_ircd/src/capability/mod.rs | 5 +- sable_ircd/src/command/handlers/rename.rs | 37 +++++++++++ sable_ircd/src/command/mod.rs | 1 + sable_ircd/src/messages/message.rs | 1 + sable_ircd/src/messages/send_history.rs | 4 +- sable_ircd/src/messages/send_realtime.rs | 56 ++++++++++++++++ sable_network/src/network/event/details.rs | 7 ++ .../src/network/network/channel_state.rs | 64 +++++++++++++++---- sable_network/src/network/network/mod.rs | 1 + .../src/network/state/access_flag.rs | 1 + sable_network/src/network/update.rs | 2 + sable_network/src/policy/channel_policy.rs | 9 +++ .../src/policy/standard_channel_policy.rs | 10 +++ 14 files changed, 184 insertions(+), 15 deletions(-) create mode 100644 sable_ircd/src/command/handlers/rename.rs diff --git a/configs/network_config.json b/configs/network_config.json index f076e9d2..2ba79783 100644 --- a/configs/network_config.json +++ b/configs/network_config.json @@ -34,6 +34,7 @@ "quiet_view", "quiet_add", "quiet_remove_any", "exempt_view", "exempt_add", "exempt_remove_any", "invite_self", "invite_other", + "rename", "invex_view", "invex_add", "invex_remove_any" ], "builtin:voice": [ diff --git a/sable_ircd/src/capability/mod.rs b/sable_ircd/src/capability/mod.rs index 88dae55b..05363031 100644 --- a/sable_ircd/src/capability/mod.rs +++ b/sable_ircd/src/capability/mod.rs @@ -17,7 +17,7 @@ macro_rules! define_capabilities { ( $typename:ident { - $( $cap:ident : $val:literal => ($name:literal, $def:literal) ),* + $( $cap:ident : $val:literal => ($name:literal, $def:literal) ),* $(,)? } ) => { #[derive(Clone,Copy,Debug,PartialEq,Eq,Serialize,Deserialize)] @@ -74,7 +74,8 @@ define_capabilities! ( ChatHistory: 0x100 => ("draft/chathistory", true), PersistentSession: 0x200 => ("sable.libera.chat/persistent-session", true), - AccountRegistration: 0x400 => ("sable.libera.chat/account-registration", true) + AccountRegistration: 0x400 => ("sable.libera.chat/account-registration", true), + ChannelRename: 0x800 => ("draft/channel-rename", true), } ); diff --git a/sable_ircd/src/command/handlers/rename.rs b/sable_ircd/src/command/handlers/rename.rs new file mode 100644 index 00000000..4135cfd4 --- /dev/null +++ b/sable_ircd/src/command/handlers/rename.rs @@ -0,0 +1,37 @@ +use super::*; + +#[command_handler("RENAME")] +async fn handle_rename( + server: &ClientServer, + net: &Network, + cmd: &dyn Command, + response: &dyn CommandResponse, + source: UserSource<'_>, + channel: wrapper::Channel<'_>, + new_name: &str, + message: Option<&str>, +) -> CommandResult { + let new_name = ChannelName::from_str(new_name)?; + server + .policy() + .can_rename(source.as_ref(), &channel, &new_name, message)?; + + if net.channel_by_name(&new_name).is_ok() { + response.send(message::Fail::new( + "RENAME", + "CHANNEL_NAME_IN_USE", + &format!("{} {}", channel.name(), new_name), // two context params + "The channel name is already taken", + )); + return Ok(()); + } + + let details = event::ChannelRename { + source: source.id(), + new_name, + message: message.map(|s| s.to_owned()), + }; + + cmd.new_event_with_response(channel.id(), details).await; + Ok(()) +} diff --git a/sable_ircd/src/command/mod.rs b/sable_ircd/src/command/mod.rs index 9d02f1ca..c424399c 100644 --- a/sable_ircd/src/command/mod.rs +++ b/sable_ircd/src/command/mod.rs @@ -58,6 +58,7 @@ mod handlers { mod privmsg; mod quit; pub mod register; + mod rename; mod topic; mod user; mod who; diff --git a/sable_ircd/src/messages/message.rs b/sable_ircd/src/messages/message.rs index a82fcb6e..7d35de8d 100644 --- a/sable_ircd/src/messages/message.rs +++ b/sable_ircd/src/messages/message.rs @@ -13,6 +13,7 @@ define_messages! { Part => { (source, chan: &ChannelName, msg: &str) => ":{source} PART {chan} :{msg}" }, Invite => { (source, target, chan: &ChannelName) => ":{source} INVITE {target} :{chan}" }, Quit => { (source, message: &str) => ":{source} QUIT :{message}" }, + Rename => { (source, old_name: &ChannelName, new_name: &ChannelName, reason: &str) => ":{source} RENAME {old_name} {new_name} :{reason}" }, Topic => { (source, chan: &ChannelName, text: &str) => ":{source} TOPIC {chan} :{text}" }, Mode => { (source, target, changes: &str) => ":{source} MODE {target} {changes}" }, diff --git a/sable_ircd/src/messages/send_history.rs b/sable_ircd/src/messages/send_history.rs index 7298f106..1cca911c 100644 --- a/sable_ircd/src/messages/send_history.rs +++ b/sable_ircd/src/messages/send_history.rs @@ -250,7 +250,9 @@ impl SendHistoryItem for update::ChannelInvite { impl SendHistoryItem for update::ChannelRename { fn send_to(&self, _conn: impl MessageSink, _from_entry: &HistoryLogEntry) -> HandleResult { - todo!(); + // Not part of history, so it is handled entirely in send_realtime.rs. + // See https://github.com/ircv3/ircv3-specifications/issues/532 + Ok(()) } } diff --git a/sable_ircd/src/messages/send_realtime.rs b/sable_ircd/src/messages/send_realtime.rs index ba1a872c..1c98ae11 100644 --- a/sable_ircd/src/messages/send_realtime.rs +++ b/sable_ircd/src/messages/send_realtime.rs @@ -1,6 +1,8 @@ use crate::errors::HandleResult; use crate::messages::MessageSink; use crate::ClientServer; +use sable_network::network::update::HistoricUser; +use sable_network::prelude::wrapper::ObjectWrapper; use sable_network::prelude::*; use super::send_history::SendHistoryItem; @@ -26,6 +28,7 @@ impl SendRealtimeItem for HistoryLogEntry { ) -> HandleResult { match &self.details { NetworkStateChange::ChannelJoin(detail) => detail.send_now(conn, self, server), + NetworkStateChange::ChannelRename(detail) => detail.send_now(conn, self, server), _ => self.send_to(conn, self), } } @@ -66,3 +69,56 @@ impl SendRealtimeItem for update::ChannelJoin { Ok(()) } } + +impl SendRealtimeItem for update::ChannelRename { + fn send_now( + &self, + conn: &impl MessageSink, + from_entry: &HistoryLogEntry, + server: &ClientServer, + ) -> HandleResult { + if conn.capabilities().has(ClientCapability::ChannelRename) { + conn.send( + message::Rename::new(&self.source, &self.old_name, &self.new_name, &self.message) + .with_tags_from(from_entry) + .with_required_capabilities(ClientCapability::ChannelRename), + ); + + Ok(()) + } else { + // For clients which don't support draft/channel-rename, emulate by making + // them PART + JOIN: + + let Some(user_id) = conn.user_id() else { + return Ok(()); + }; + + let network = server.network(); + let channel = network.channel(self.channel.id)?; + let Some(membership) = channel.has_member(user_id) else { + tracing::warn!("Cannot send ChannelRename to non-member {:?}", user_id); + return Ok(()); + }; + let user = network.user(user_id)?; + + conn.send( + message::Part::new( + &user, + &self.old_name, + &format!("Channel renamed to {}: {}", &self.new_name, &self.message), + ) + .except_capability(ClientCapability::ChannelRename), + ); + + update::ChannelJoin { + channel: self.channel.clone(), + membership: membership.raw().clone(), + user: HistoricUser { + user: user.raw().clone(), + nickname: user.nick(), + }, + } + .send_now(conn, from_entry, server) + } + } +} diff --git a/sable_network/src/network/event/details.rs b/sable_network/src/network/event/details.rs index 0ce392b0..27faef43 100644 --- a/sable_network/src/network/event/details.rs +++ b/sable_network/src/network/event/details.rs @@ -99,6 +99,13 @@ EventDetails => { pub message: String, } + #[target_type(ChannelId)] + struct ChannelRename { + pub source: UserId, + pub new_name: ChannelName, + pub message: Option, + } + #[target_type(InviteId)] struct ChannelInvite { pub source: UserId, diff --git a/sable_network/src/network/network/channel_state.rs b/sable_network/src/network/network/channel_state.rs index 77420df0..28c7f518 100644 --- a/sable_network/src/network/network/channel_state.rs +++ b/sable_network/src/network/network/channel_state.rs @@ -23,22 +23,34 @@ impl Network { fn do_rename_channel( &mut self, channel_id: ChannelId, + source: HistoricMessageSource, new_name: ChannelName, + message: String, event: &Event, updates: &dyn NetworkUpdateReceiver, ) { - if let Some(channel) = self.channels.get_mut(&channel_id) { - let old_name = channel.name; - channel.name = new_name; + match self.channels.get_mut(&channel_id) { + Some(channel) => { + let old_name = channel.name; + channel.name = new_name; - updates.notify( - update::ChannelRename { - channel: channel.clone(), - old_name, - new_name, - }, - event, - ); + updates.notify( + update::ChannelRename { + source, + channel: channel.clone(), + old_name, + new_name, + message, + }, + event, + ); + } + None => tracing::warn!( + "Attempted to rename unknown channel {:?} to {}: {}", + channel_id, + new_name, + message, + ), } } @@ -60,7 +72,18 @@ impl Network { // The new one wins. Rename the existing channel let newname = state_utils::hashed_channel_name_for(existing_id); - self.do_rename_channel(existing_id, newname, event, updates); + self.do_rename_channel( + existing_id, + HistoricMessageSource::Unknown, + newname, + format!( + "Name clash between {:?} and {:?}", + existing_id.server(), + target.server() + ), + event, + updates, + ); } else { // The old one wins. Change the name of this one details.name = state_utils::hashed_channel_name_for(target); @@ -341,6 +364,23 @@ impl Network { } } + pub(super) fn user_renamed_channel( + &mut self, + target: ChannelId, + event: &Event, + details: &details::ChannelRename, + updates: &dyn NetworkUpdateReceiver, + ) { + self.do_rename_channel( + target, + self.translate_state_change_source(details.source.into()), + details.new_name, + details.message.clone().unwrap_or("".to_string()), + event, + updates, + ); + } + pub(super) fn new_channel_invite( &mut self, target: InviteId, diff --git a/sable_network/src/network/network/mod.rs b/sable_network/src/network/network/mod.rs index c5a193e6..f8b305be 100644 --- a/sable_network/src/network/network/mod.rs +++ b/sable_network/src/network/network/mod.rs @@ -210,6 +210,7 @@ impl Network { ChannelJoin => self.user_joined_channel, ChannelKick => self.user_kicked_from_channel, ChannelPart => self.user_left_channel, + ChannelRename => self.user_renamed_channel, ChannelInvite => self.new_channel_invite, NewMessage => self.new_message, NewNetworkBan => self.new_ban, diff --git a/sable_network/src/network/state/access_flag.rs b/sable_network/src/network/state/access_flag.rs index 3a042cf3..b3df7992 100644 --- a/sable_network/src/network/state/access_flag.rs +++ b/sable_network/src/network/state/access_flag.rs @@ -32,6 +32,7 @@ pub enum ChannelAccessFlag { ReceiveOp = 0x0000_0010, ReceiveVoice = 0x0000_0020, ReceiveOpmod = 0x0000_0040, + Rename = 0x0000_0080, Topic = 0x0000_0100, Kick = 0x0000_0200, diff --git a/sable_network/src/network/update.rs b/sable_network/src/network/update.rs index 96275c78..fbd16063 100644 --- a/sable_network/src/network/update.rs +++ b/sable_network/src/network/update.rs @@ -171,9 +171,11 @@ NetworkStateChange => { /// A channel's name has changed struct ChannelRename { + pub source: HistoricMessageSource, pub channel: state::Channel, pub old_name: ChannelName, pub new_name: ChannelName, + pub message: String, } /// A message has been sent to a user or channel diff --git a/sable_network/src/policy/channel_policy.rs b/sable_network/src/policy/channel_policy.rs index 1614f311..b02795af 100644 --- a/sable_network/src/policy/channel_policy.rs +++ b/sable_network/src/policy/channel_policy.rs @@ -16,6 +16,15 @@ pub trait ChannelPolicyService { msg: &str, ) -> PermissionResult; + /// Determine whether the given user can change the name of the given channel + fn can_rename( + &self, + user: &User, + channel: &Channel, + new_name: &ChannelName, + msg: Option<&str>, + ) -> PermissionResult; + /// Determine whether the given user can send to the given channel fn can_send(&self, user: &User, channel: &Channel, msg: &str) -> PermissionResult; diff --git a/sable_network/src/policy/standard_channel_policy.rs b/sable_network/src/policy/standard_channel_policy.rs index 6027d35c..287241d2 100644 --- a/sable_network/src/policy/standard_channel_policy.rs +++ b/sable_network/src/policy/standard_channel_policy.rs @@ -114,6 +114,16 @@ impl ChannelPolicyService for StandardChannelPolicy { has_access(user, channel, ChannelAccessFlag::Kick) } + fn can_rename( + &self, + user: &User, + channel: &Channel, + _new_name: &ChannelName, + _msg: Option<&str>, + ) -> PermissionResult { + has_access(user, channel, ChannelAccessFlag::Rename) + } + fn can_send(&self, user: &User, channel: &Channel, _msg: &str) -> PermissionResult { if channel.mode().has_mode(ChannelModeFlag::NoExternal) && user.is_in_channel(channel.id()).is_none() From 671ca2430c0f9e72333431f697c4586c57c146dc Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 17 Sep 2023 19:23:43 +0200 Subject: [PATCH 2/2] Move/add permissions --- configs/network_config.json | 2 +- configs/services.conf | 4 ++++ sable_network/src/network/state/access_flag.rs | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/configs/network_config.json b/configs/network_config.json index 2ba79783..abb5b7f8 100644 --- a/configs/network_config.json +++ b/configs/network_config.json @@ -30,11 +30,11 @@ "op_self", "op_grant", "voice_self", "voice_grant", "receive_op", "receive_voice", "receive_opmod", "topic", "kick", "set_simple_mode", "set_key", + "rename", "ban_view", "ban_add", "ban_remove_any", "quiet_view", "quiet_add", "quiet_remove_any", "exempt_view", "exempt_add", "exempt_remove_any", "invite_self", "invite_other", - "rename", "invex_view", "invex_add", "invex_remove_any" ], "builtin:voice": [ diff --git a/configs/services.conf b/configs/services.conf index dfaade11..f54031bb 100644 --- a/configs/services.conf +++ b/configs/services.conf @@ -21,6 +21,7 @@ "invite_self", "invite_other", "receive_op", "receive_voice", "receive_opmod", "topic", "kick", "set_simple_mode", "set_key", + "rename", "ban_view", "ban_add", "ban_remove_any", "quiet_view", "quiet_add", "quiet_remove_any", "exempt_view", "exempt_add", "exempt_remove_any", @@ -30,6 +31,7 @@ "always_send", "receive_op", "receive_voice", "receive_opmod", "topic", "kick", "set_simple_mode", "set_key", + "rename", "ban_view", "ban_add", "ban_remove_any", "quiet_view", "quiet_add", "quiet_remove_any", "exempt_view", "exempt_add", "exempt_remove_any", @@ -44,6 +46,7 @@ "always_send", "receive_op", "receive_voice", "receive_opmod", "topic", "kick", "set_simple_mode", "set_key", + "rename", "ban_view", "ban_add", "ban_remove_any", "quiet_view", "quiet_add", "quiet_remove_any", "exempt_view", "exempt_add", "exempt_remove_any", @@ -57,6 +60,7 @@ "invite_self", "invite_other", "receive_op", "receive_voice", "receive_opmod", "topic", "kick", "set_simple_mode", "set_key", + "rename", "ban_view", "ban_add", "ban_remove_any", "quiet_view", "quiet_add", "quiet_remove_any", "exempt_view", "exempt_add", "exempt_remove_any", diff --git a/sable_network/src/network/state/access_flag.rs b/sable_network/src/network/state/access_flag.rs index b3df7992..f9cb4769 100644 --- a/sable_network/src/network/state/access_flag.rs +++ b/sable_network/src/network/state/access_flag.rs @@ -32,13 +32,14 @@ pub enum ChannelAccessFlag { ReceiveOp = 0x0000_0010, ReceiveVoice = 0x0000_0020, ReceiveOpmod = 0x0000_0040, - Rename = 0x0000_0080, Topic = 0x0000_0100, Kick = 0x0000_0200, SetSimpleMode = 0x0000_0400, SetKey = 0x0000_0800, + Rename = 0x0000_1000, + BanView = 0x0001_0000, BanAdd = 0x0002_0000, BanRemoveOwn = 0x0004_0000,