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

Implement channel rename #85

Merged
merged 2 commits into from
Sep 17, 2023
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
1 change: 1 addition & 0 deletions configs/network_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"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",
Expand Down
4 changes: 4 additions & 0 deletions configs/services.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions sable_ircd/src/capability/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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),
}
);

Expand Down
37 changes: 37 additions & 0 deletions sable_ircd/src/command/handlers/rename.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
1 change: 1 addition & 0 deletions sable_ircd/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mod handlers {
mod privmsg;
mod quit;
pub mod register;
mod rename;
mod topic;
mod user;
mod who;
Expand Down
1 change: 1 addition & 0 deletions sable_ircd/src/messages/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}" },
Expand Down
4 changes: 3 additions & 1 deletion sable_ircd/src/messages/send_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That issue seems to say it should be part of history, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's undecided for now so I'm mirroring existing implementations

Ok(())
}
}

Expand Down
56 changes: 56 additions & 0 deletions sable_ircd/src/messages/send_realtime.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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),
}
}
Expand Down Expand Up @@ -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)
}
}
}
7 changes: 7 additions & 0 deletions sable_network/src/network/event/details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ EventDetails => {
pub message: String,
}

#[target_type(ChannelId)]
struct ChannelRename {
pub source: UserId,
pub new_name: ChannelName,
pub message: Option<String>,
}

#[target_type(InviteId)]
struct ChannelInvite {
pub source: UserId,
Expand Down
64 changes: 52 additions & 12 deletions sable_network/src/network/network/channel_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions sable_network/src/network/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions sable_network/src/network/state/access_flag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub enum ChannelAccessFlag {
SetSimpleMode = 0x0000_0400,
SetKey = 0x0000_0800,

Rename = 0x0000_1000,

BanView = 0x0001_0000,
BanAdd = 0x0002_0000,
BanRemoveOwn = 0x0004_0000,
Expand Down
2 changes: 2 additions & 0 deletions sable_network/src/network/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions sable_network/src/policy/channel_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
10 changes: 10 additions & 0 deletions sable_network/src/policy/standard_channel_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading