diff --git a/docs/iamb.1 b/docs/iamb.1 index 8eb8e45..dcc8abb 100644 --- a/docs/iamb.1 +++ b/docs/iamb.1 @@ -161,6 +161,12 @@ Set the room's canonical alias to the one provided, and make the previous one an Delete the room's canonical alias. .It Sy ":room canon show" Show the room's canonical alias, if any is set. +.It Sy ":room ban [user] [reason]" +Ban a user from this room with an optional reason. +.It Sy ":room unban [user] [reason]" +Unban a user from this room with an optional reason. +.It Sy ":room kick [user] [reason]" +Kick a user from this room with an optional reason. .El .Sh "WINDOW COMMANDS" diff --git a/src/base.rs b/src/base.rs index 2557729..e5f6a62 100644 --- a/src/base.rs +++ b/src/base.rs @@ -391,6 +391,24 @@ pub enum RoomField { CanonicalAlias, } +/// An action that operates on a room member. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum MemberUpdateAction { + Ban, + Kick, + Unban, +} + +impl Display for MemberUpdateAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MemberUpdateAction::Ban => write!(f, "ban"), + MemberUpdateAction::Kick => write!(f, "kick"), + MemberUpdateAction::Unban => write!(f, "unban"), + } + } +} + /// An action that operates on a focused room. #[derive(Clone, Debug, Eq, PartialEq)] pub enum RoomAction { @@ -406,6 +424,9 @@ pub enum RoomAction { /// Leave this room. Leave(bool), + /// Update a user's membership in this room. + MemberUpdate(MemberUpdateAction, String, Option, bool), + /// Open the members window. Members(Box), diff --git a/src/commands.rs b/src/commands.rs index 3a80a50..7c5aa5a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -20,6 +20,7 @@ use crate::base::{ IambAction, IambId, KeysAction, + MemberUpdateAction, MessageAction, ProgramCommand, ProgramCommands, @@ -411,6 +412,17 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { ("dm", "unset", None) => RoomAction::SetDirect(false).into(), ("dm", "unset", Some(_)) => return Result::Err(CommandError::InvalidArgument), + // :room [kick|ban|unban] + ("kick", u, r) => { + RoomAction::MemberUpdate(MemberUpdateAction::Kick, u.into(), r, desc.bang).into() + }, + ("ban", u, r) => { + RoomAction::MemberUpdate(MemberUpdateAction::Ban, u.into(), r, desc.bang).into() + }, + ("unban", u, r) => { + RoomAction::MemberUpdate(MemberUpdateAction::Unban, u.into(), r, desc.bang).into() + }, + // :room name set ("name", "set", Some(s)) => RoomAction::Set(RoomField::Name, s).into(), ("name", "set", None) => return Result::Err(CommandError::InvalidArgument), @@ -1047,6 +1059,69 @@ mod tests { assert_eq!(res, Err(CommandError::InvalidArgument)); } + #[test] + fn test_cmd_room_kick() { + let mut cmds = setup_commands(); + let ctx = EditContext::default(); + + let res = cmds.input_cmd("room kick @user:example.com", ctx.clone()).unwrap(); + let act = IambAction::Room(RoomAction::MemberUpdate( + MemberUpdateAction::Kick, + "@user:example.com".into(), + None, + false, + )); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("room! kick @user:example.com", ctx.clone()).unwrap(); + let act = IambAction::Room(RoomAction::MemberUpdate( + MemberUpdateAction::Kick, + "@user:example.com".into(), + None, + true, + )); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds + .input_cmd("room! kick @user:example.com \"reason here\"", ctx.clone()) + .unwrap(); + let act = IambAction::Room(RoomAction::MemberUpdate( + MemberUpdateAction::Kick, + "@user:example.com".into(), + Some("reason here".into()), + true, + )); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + } + + #[test] + fn test_cmd_room_ban_unban() { + let mut cmds = setup_commands(); + let ctx = EditContext::default(); + + let res = cmds + .input_cmd("room! ban @user:example.com \"spam\"", ctx.clone()) + .unwrap(); + let act = IambAction::Room(RoomAction::MemberUpdate( + MemberUpdateAction::Ban, + "@user:example.com".into(), + Some("spam".into()), + true, + )); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds + .input_cmd("room unban @user:example.com \"reconciled\"", ctx.clone()) + .unwrap(); + let act = IambAction::Room(RoomAction::MemberUpdate( + MemberUpdateAction::Unban, + "@user:example.com".into(), + Some("reconciled".into()), + false, + )); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + } + #[test] fn test_cmd_redact() { let mut cmds = setup_commands(); diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs index 5c25bfb..0700746 100644 --- a/src/windows/room/mod.rs +++ b/src/windows/room/mod.rs @@ -22,6 +22,7 @@ use matrix_sdk::{ }, OwnedEventId, OwnedRoomAliasId, + OwnedUserId, RoomId, }, DisplayName, @@ -56,6 +57,7 @@ use crate::base::{ IambId, IambInfo, IambResult, + MemberUpdateAction, MessageAction, ProgramAction, ProgramContext, @@ -269,6 +271,47 @@ impl RoomState { Err(IambError::NotJoined.into()) } }, + RoomAction::MemberUpdate(mua, user, reason, skip_confirm) => { + let Some(room) = store.application.worker.client.get_room(self.id()) else { + return Err(IambError::NotJoined.into()); + }; + + let Ok(user_id) = OwnedUserId::try_from(user.as_str()) else { + let err = IambError::InvalidUserId(user); + + return Err(err.into()); + }; + + if !skip_confirm { + let msg = format!("Do you really want to {mua} {user} from this room?"); + let act = RoomAction::MemberUpdate(mua, user, reason, true); + let act = IambAction::from(act); + let prompt = PromptYesNo::new(msg, vec![Action::from(act)]); + let prompt = Box::new(prompt); + + return Err(UIError::NeedConfirm(prompt)); + } + + match mua { + MemberUpdateAction::Ban => { + room.ban_user(&user_id, reason.as_deref()) + .await + .map_err(IambError::from)?; + }, + MemberUpdateAction::Unban => { + room.unban_user(&user_id, reason.as_deref()) + .await + .map_err(IambError::from)?; + }, + MemberUpdateAction::Kick => { + room.kick_user(&user_id, reason.as_deref()) + .await + .map_err(IambError::from)?; + }, + } + + Ok(vec![]) + }, RoomAction::Members(mut cmd) => { let width = Count::Exact(30); let act =