diff --git a/docs/iamb.1 b/docs/iamb.1 index cba3044..c1f3f45 100644 --- a/docs/iamb.1 +++ b/docs/iamb.1 @@ -130,6 +130,18 @@ Remove a tag from the currently focused room. Set the topic of the currently focused room. .It Sy ":room topic unset" Unset the topic of the currently focused room. +.It Sy ":room alias set [alias]" +Create and point the given alias to the room. +.It Sy ":room alias unset [alias]" +Delete the provided alias from the room's alternative alias list. +.It Sy ":room alias show" +Show alternative aliases to the room, if any are set. +.It Sy ":room canon set [alias]" +Set the room's canonical alias to the one provided, and make the previous one an alternative alias. +.It Sy ":room canon unset [alias]" +Delete the room's canonical alias. +.It Sy ":room canon show" +Show the room's canonical alias, if any is set. .El .Sh "WINDOW COMMANDS" diff --git a/src/base.rs b/src/base.rs index 8be9a4e..05f2ce6 100644 --- a/src/base.rs +++ b/src/base.rs @@ -369,6 +369,15 @@ pub enum RoomField { /// The room topic. Topic, + + /// The room's entire list of alternative aliases. + Aliases, + + /// A specific alternative alias to the room. + Alias(String), + + /// The room's canonical alias. + CanonicalAlias, } /// An action that operates on a focused room. @@ -397,6 +406,9 @@ pub enum RoomAction { /// Unset a room property. Unset(RoomField), + + /// List the values in a list room property. + Show(RoomField), } /// An action that sends a message to a room. @@ -596,6 +608,10 @@ pub enum IambError { #[error("Invalid user identifier: {0}")] InvalidUserId(String), + /// An invalid user identifier was specified. + #[error("Invalid room alias: {0}")] + InvalidRoomAlias(String), + /// An invalid verification identifier was specified. #[error("Invalid verification user/device pair: {0}")] InvalidVerificationId(String), @@ -659,6 +675,10 @@ pub enum IambError { #[error("Unknown room identifier: {0}")] UnknownRoom(OwnedRoomId), + /// An invalid room alias id was specified. + #[error("Invalid room alias id: {0}")] + InvalidRoomAliasId(#[from] matrix_sdk::ruma::IdParseError), + /// A failure occurred during verification. #[error("Verification request error: {0}")] VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError), diff --git a/src/commands.rs b/src/commands.rs index 81fe031..f8b3515 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -454,6 +454,42 @@ fn iamb_room(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { ("tag", "unset", Some(s)) => RoomAction::Unset(RoomField::Tag(tag_name(s)?)).into(), ("tag", "unset", None) => return Result::Err(CommandError::InvalidArgument), + // :room aliases show + ("alias", "show", None) => RoomAction::Show(RoomField::Aliases).into(), + ("alias", "show", Some(_)) => return Result::Err(CommandError::InvalidArgument), + + // :room aliases unset + ("alias", "unset", Some(s)) => RoomAction::Unset(RoomField::Alias(s)).into(), + ("alias", "unset", None) => return Result::Err(CommandError::InvalidArgument), + + // :room aliases set + ("alias", "set", Some(s)) => RoomAction::Set(RoomField::Alias(s), "".into()).into(), + ("alias", "set", None) => return Result::Err(CommandError::InvalidArgument), + + // :room canonicalalias show + ("canonicalalias" | "canon", "show", None) => { + RoomAction::Show(RoomField::CanonicalAlias).into() + }, + ("canonicalalias" | "canon", "show", Some(_)) => { + return Result::Err(CommandError::InvalidArgument) + }, + + // :room canonicalalias set + ("canonicalalias" | "canon", "set", Some(s)) => { + RoomAction::Set(RoomField::CanonicalAlias, s).into() + }, + ("canonicalalias" | "canon", "set", None) => { + return Result::Err(CommandError::InvalidArgument) + }, + + // :room canonicalalias unset + ("canonicalalias" | "canon", "unset", None) => { + RoomAction::Unset(RoomField::CanonicalAlias).into() + }, + ("canonicalalias" | "canon", "unset", Some(_)) => { + return Result::Err(CommandError::InvalidArgument) + }, + _ => return Result::Err(CommandError::InvalidArgument), }; diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs index e4a0abf..a251971 100644 --- a/src/windows/room/mod.rs +++ b/src/windows/room/mod.rs @@ -1,12 +1,26 @@ //! # Windows for Matrix rooms and spaces +use std::collections::HashSet; + use matrix_sdk::{ room::Room as MatrixRoom, ruma::{ + api::client::{ + alias::{ + create_alias::v3::Request as CreateAliasRequest, + delete_alias::v3::Request as DeleteAliasRequest, + }, + error::ErrorKind as ClientApiErrorKind, + }, events::{ - room::{name::RoomNameEventContent, topic::RoomTopicEventContent}, + room::{ + canonical_alias::RoomCanonicalAliasEventContent, + name::RoomNameEventContent, + topic::RoomTopicEventContent, + }, tag::{TagInfo, Tags}, }, OwnedEventId, + OwnedRoomAliasId, RoomId, }, DisplayName, @@ -53,6 +67,8 @@ use crate::base::{ use self::chat::ChatState; use self::space::{Space, SpaceState}; +use std::convert::TryFrom; + mod chat; mod scrollback; mod space; @@ -182,7 +198,7 @@ impl RoomState { pub async fn room_command( &mut self, act: RoomAction, - _: ProgramContext, + ctx: ProgramContext, store: &mut ProgramStore, ) -> IambResult, ProgramContext)>> { match act { @@ -280,6 +296,87 @@ impl RoomState { let ev = RoomTopicEventContent::new(value); let _ = room.send_state_event(ev).await.map_err(IambError::from)?; }, + RoomField::CanonicalAlias => { + let client = &mut store.application.worker.client; + + let Ok(orai) = OwnedRoomAliasId::try_from(value.as_str()) else { + let err = IambError::InvalidRoomAlias(value); + + return Err(err.into()); + }; + + let mut alt_aliases = + room.alt_aliases().into_iter().collect::>(); + let canonical_old = room.canonical_alias(); + + // If the room's alias is already that, ignore it + if canonical_old.as_ref() == Some(&orai) { + let msg = format!("The canonical room alias is already {orai}"); + + return Ok(vec![(Action::ShowInfoMessage(msg.into()), ctx)]); + } + + // Try creating the room alias on the server. + let alias_create_req = + CreateAliasRequest::new(orai.clone(), room.room_id().into()); + if let Err(e) = client.send(alias_create_req, None).await { + if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() { + // Ignore when it already exists. + } else { + return Err(IambError::from(e).into()); + } + } + + // Demote the previous one to an alt alias. + alt_aliases.extend(canonical_old); + + // At this point the room alias definitely exists, and we can update the + // state event. + let mut ev = RoomCanonicalAliasEventContent::new(); + ev.alias = Some(orai); + ev.alt_aliases = alt_aliases.into_iter().collect(); + let _ = room.send_state_event(ev).await.map_err(IambError::from)?; + }, + RoomField::Alias(alias) => { + let client = &mut store.application.worker.client; + + let Ok(orai) = OwnedRoomAliasId::try_from(alias.as_str()) else { + let err = IambError::InvalidRoomAlias(alias); + + return Err(err.into()); + }; + + let mut alt_aliases = + room.alt_aliases().into_iter().collect::>(); + let canonical = room.canonical_alias(); + + if alt_aliases.contains(&orai) || canonical.as_ref() == Some(&orai) { + let msg = format!("The alias {orai} already maps to this room"); + + return Ok(vec![(Action::ShowInfoMessage(msg.into()), ctx)]); + } else { + alt_aliases.insert(orai.clone()); + } + + // If the room alias does not exist on the server, create it + let alias_create_req = CreateAliasRequest::new(orai, room.room_id().into()); + if let Err(e) = client.send(alias_create_req, None).await { + if let Some(ClientApiErrorKind::Unknown) = e.client_api_error_kind() { + // Ignore when it already exists. + } else { + return Err(IambError::from(e).into()); + } + } + + // And add it to the aliases in the state event. + let mut ev = RoomCanonicalAliasEventContent::new(); + ev.alias = canonical; + ev.alt_aliases = alt_aliases.into_iter().collect(); + let _ = room.send_state_event(ev).await.map_err(IambError::from)?; + }, + RoomField::Aliases => { + // This never happens, aliases is only used for showing + }, } Ok(vec![]) @@ -302,10 +399,120 @@ impl RoomState { let ev = RoomTopicEventContent::new("".into()); let _ = room.send_state_event(ev).await.map_err(IambError::from)?; }, + RoomField::CanonicalAlias => { + let Some(alias_to_destroy) = room.canonical_alias() else { + let msg = format!("This room has no canonical alias to unset"); + + return Ok(vec![(Action::ShowInfoMessage(msg.into()), ctx)]); + }; + + // Remove the canonical alias from the state event. + let mut ev = RoomCanonicalAliasEventContent::new(); + ev.alias = None; + ev.alt_aliases = room.alt_aliases(); + let _ = room.send_state_event(ev).await.map_err(IambError::from)?; + + // And then unmap it on the server. + let del_req = DeleteAliasRequest::new(alias_to_destroy); + let _ = store + .application + .worker + .client + .send(del_req, None) + .await + .map_err(IambError::from)?; + }, + RoomField::Alias(alias) => { + let Ok(orai) = OwnedRoomAliasId::try_from(alias.as_str()) else { + let err = IambError::InvalidRoomAlias(alias); + + return Err(err.into()); + }; + + let alt_aliases = room.alt_aliases(); + let canonical = room.canonical_alias(); + + if !alt_aliases.contains(&orai) && canonical.as_ref() != Some(&orai) { + let msg = format!("The alias {orai:?} isn't mapped to this room"); + + return Ok(vec![(Action::ShowInfoMessage(msg.into()), ctx)]); + } + + // Remove the alias from the state event if it's in it. + let mut ev = RoomCanonicalAliasEventContent::new(); + ev.alias = canonical.filter(|canon| canon != &orai); + ev.alt_aliases = alt_aliases; + ev.alt_aliases.retain(|in_orai| in_orai != &orai); + let _ = room.send_state_event(ev).await.map_err(IambError::from)?; + + // And then unmap it on the server. + let del_req = DeleteAliasRequest::new(orai); + let _ = store + .application + .worker + .client + .send(del_req, None) + .await + .map_err(IambError::from)?; + }, + RoomField::Aliases => { + // This will not happen, you cannot unset all aliases + }, } Ok(vec![]) }, + RoomAction::Show(field) => { + let room = store + .application + .get_joined_room(self.id()) + .ok_or(UIError::Application(IambError::NotJoined))?; + + let msg = match field { + RoomField::Name => { + match room.name() { + None => "Room has no name".into(), + Some(name) => format!("Room name: {name:?}"), + } + }, + RoomField::Topic => { + match room.topic() { + None => "Room has no topic".into(), + Some(topic) => format!("Room topic: {topic:?}"), + } + }, + RoomField::Aliases => { + let aliases = room + .alt_aliases() + .iter() + .map(OwnedRoomAliasId::to_string) + .collect::>(); + + if aliases.is_empty() { + "No alternative aliases in room".into() + } else { + format!("Alternative aliases: {}.", aliases.join(", ")) + } + }, + RoomField::CanonicalAlias => { + match room.canonical_alias() { + None => "No canonical alias for room".into(), + Some(can) => format!("Canonical alias: {can}"), + } + }, + RoomField::Tag(_) => { + format!("Cannot currently show value for a tag") + }, + RoomField::Alias(_) => { + format!("Cannot show a single alias; use `:room aliases show` instead.") + }, + }; + + let msg = InfoMessage::Pager(msg); + let act = Action::ShowInfoMessage(msg); + + Ok(vec![(act, ctx)]) + }, } }