diff --git a/Cargo.lock b/Cargo.lock index 5b33f52..28835d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,6 +576,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +[[package]] +name = "fuse-rust" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00df1351ccd6b34c2f67658bd4524b677e30f7269a6de44b038ec20211853e5e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "futf" version = "0.1.4" @@ -2544,6 +2553,7 @@ dependencies = [ "barrel", "combine", "dirs", + "fuse-rust", "futures", "html2text", "indoc", diff --git a/Cargo.toml b/Cargo.toml index 8346a73..840c280 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ refinery = { version = "0.5", features = ["rusqlite"]} barrel = { version = "0.6", features = ["sqlite3"] } tempfile = "3" substring = "1.4" +fuse-rust = "0.2" [dependencies.sqlx] version = "0.5" diff --git a/src/commands/parser.rs b/src/commands/parser.rs index 83b0882..c9c7cbe 100644 --- a/src/commands/parser.rs +++ b/src/commands/parser.rs @@ -9,7 +9,7 @@ use crate::commands::{ cthulhu::{CthAdvanceRoll, CthRoll}, management::{CheckCommand, LinkCommand, RegisterCommand, UnlinkCommand, UnregisterCommand}, misc::HelpCommand, - rooms::ListRoomsCommand, + rooms::{ListRoomsCommand, SetRoomCommand}, variables::{ DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand, }, @@ -91,6 +91,7 @@ pub fn parse_command(input: &str) -> Result, BotError> { "check" => convert_to!(CheckCommand, cmd_input), "unregister" => convert_to!(UnregisterCommand, cmd_input), "rooms" => convert_to!(ListRoomsCommand, cmd_input), + "room" => convert_to!(SetRoomCommand, cmd_input), _ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()), }, //All other errors passed up. diff --git a/src/commands/rooms.rs b/src/commands/rooms.rs index 3c78c20..5591b8b 100644 --- a/src/commands/rooms.rs +++ b/src/commands/rooms.rs @@ -1,11 +1,78 @@ use super::{Command, Execution, ExecutionResult}; use crate::context::Context; use crate::error::BotError; +use crate::matrix; use async_trait::async_trait; +use fuse_rust::{Fuse, FuseProperty, Fuseable}; use futures::stream::{self, StreamExt, TryStreamExt}; -use matrix_sdk::identifiers::UserId; +use matrix_sdk::{identifiers::UserId, Client}; use std::convert::TryFrom; +/// Holds matrix room ID and display name as strings, for use with +/// searching. See search_for_room. +#[derive(Clone, Debug)] +struct RoomNameAndId { + id: String, + name: String, +} + +/// Allows searching for a room name and ID struct, instead of just +/// searching room display names directly. +impl Fuseable for RoomNameAndId { + fn properties(&self) -> Vec { + return vec![FuseProperty { + value: String::from("name"), + weight: 1.0, + }]; + } + + fn lookup(&self, key: &str) -> Option<&str> { + return match key { + "name" => Some(&self.name), + _ => None, + }; + } +} + +/// Attempt to find a room by either name or Matrix Room ID query +/// string. It prefers the exact room ID first, and then falls back to +/// fuzzy searching based on room display name. The best match is +/// returned, or None if no matches were found. +fn search_for_room<'a>( + rooms_for_user: &'a [RoomNameAndId], + query: &str, +) -> Option<&'a RoomNameAndId> { + rooms_for_user + .iter() + .find(|room| room.id == query) + .or_else(|| { + Fuse::default() + .search_text_in_fuse_list(query, &rooms_for_user) + .into_iter() + .min_by(|r1, r2| r1.score.partial_cmp(&r2.score).unwrap()) + .and_then(|result| rooms_for_user.get(result.index)) + }) +} + +async fn get_rooms_for_user( + client: &Client, + user_id: &str, +) -> Result, BotError> { + let user_id = UserId::try_from(user_id)?; + let rooms_for_user = matrix::get_rooms_for_user(client, &user_id).await?; + let rooms_for_user: Vec = stream::iter(rooms_for_user) + .filter_map(|room| async move { + Some(room.display_name().await.map(|room_name| RoomNameAndId { + id: room.room_id().to_string(), + name: room_name, + })) + }) + .try_collect() + .await?; + + Ok(rooms_for_user) +} + pub struct ListRoomsCommand; impl From for Box { @@ -25,7 +92,7 @@ impl TryFrom for ListRoomsCommand { #[async_trait] impl Command for ListRoomsCommand { fn name(&self) -> &'static str { - "list rooms command" + "list rooms" } fn is_secure(&self) -> bool { @@ -33,22 +100,54 @@ impl Command for ListRoomsCommand { } async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult { - let user_id = UserId::try_from(ctx.username)?; - let rooms_for_user = crate::matrix::get_rooms_for_user(ctx.matrix_client, &user_id).await?; - - let rooms_for_user: Vec = stream::iter(rooms_for_user) - .filter_map(|room| async move { - Some( - room.display_name() - .await - .map(|room_name| (room.room_id().to_string(), room_name)), - ) - }) - .map_ok(|(room_id, room_name)| format!(" {} | {}", room_id, room_name)) - .try_collect() - .await?; + let rooms_for_user: Vec = get_rooms_for_user(ctx.matrix_client, ctx.username) + .await + .map(|rooms| { + rooms + .into_iter() + .map(|room| format!(" {} | {}", room.id, room.name)) + .collect() + })?; let html = format!("
{}
", rooms_for_user.join("\n")); Execution::success(html) } } + +pub struct SetRoomCommand(String); + +impl From for Box { + fn from(cmd: SetRoomCommand) -> Self { + Box::new(cmd) + } +} + +impl TryFrom for SetRoomCommand { + type Error = BotError; + + fn try_from(input: String) -> Result { + Ok(SetRoomCommand(input)) + } +} + +#[async_trait] +impl Command for SetRoomCommand { + fn name(&self) -> &'static str { + "set active room" + } + + fn is_secure(&self) -> bool { + true + } + + async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult { + let rooms_for_user = get_rooms_for_user(ctx.matrix_client, ctx.username).await?; + let room = search_for_room(&rooms_for_user, &self.0); + + if let Some(room) = room { + Execution::success(format!(r#"Active room set to "{}""#, room.name)) + } else { + Err(BotError::RoomDoesNotExist) + } + } +} diff --git a/src/error.rs b/src/error.rs index 4161608..8ad1914 100644 --- a/src/error.rs +++ b/src/error.rs @@ -90,6 +90,9 @@ pub enum BotError { #[error("user account already exists")] AccountAlreadyExists, + + #[error("room name or id does not exist")] + RoomDoesNotExist, } #[derive(Error, Debug)]