Set Room Command #78

Manually merged
projectmoon merged 4 commits from set-room-command into master 2021-05-28 22:05:12 +00:00
6 changed files with 162 additions and 19 deletions

10
Cargo.lock generated
View File

@ -576,6 +576,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" 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]] [[package]]
name = "futf" name = "futf"
version = "0.1.4" version = "0.1.4"
@ -2544,6 +2553,7 @@ dependencies = [
"barrel", "barrel",
"combine", "combine",
"dirs", "dirs",
"fuse-rust",
"futures", "futures",
"html2text", "html2text",
"indoc", "indoc",

View File

@ -32,6 +32,7 @@ refinery = { version = "0.5", features = ["rusqlite"]}
barrel = { version = "0.6", features = ["sqlite3"] } barrel = { version = "0.6", features = ["sqlite3"] }
tempfile = "3" tempfile = "3"
substring = "1.4" substring = "1.4"
fuse-rust = "0.2"
[dependencies.sqlx] [dependencies.sqlx]
version = "0.5" version = "0.5"

View File

@ -132,14 +132,16 @@ fn log_command(cmd: &(impl Command + ?Sized), ctx: &Context, result: &ExecutionR
use substring::Substring; use substring::Substring;
let command = match cmd.is_secure() { let command = match cmd.is_secure() {
true => cmd.name(), true => cmd.name(),
false => ctx.message_body.substring(0, 30), false => ctx.message_body,
}; };
let dots = match ctx.message_body.len() { let dots = match command.len() {
_len if _len > 30 => "[...]", _len if _len > 30 => "[...]",
_ => "", _ => "",
}; };
let command = command.substring(0, 30);
match result { match result {
Ok(_) => { Ok(_) => {
info!( info!(

View File

@ -9,7 +9,7 @@ use crate::commands::{
cthulhu::{CthAdvanceRoll, CthRoll}, cthulhu::{CthAdvanceRoll, CthRoll},
management::{CheckCommand, LinkCommand, RegisterCommand, UnlinkCommand, UnregisterCommand}, management::{CheckCommand, LinkCommand, RegisterCommand, UnlinkCommand, UnregisterCommand},
misc::HelpCommand, misc::HelpCommand,
rooms::ListRoomsCommand, rooms::{ListRoomsCommand, SetRoomCommand},
variables::{ variables::{
DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand, DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand,
}, },
@ -91,6 +91,7 @@ pub fn parse_command(input: &str) -> Result<Box<dyn Command>, BotError> {
"check" => convert_to!(CheckCommand, cmd_input), "check" => convert_to!(CheckCommand, cmd_input),
"unregister" => convert_to!(UnregisterCommand, cmd_input), "unregister" => convert_to!(UnregisterCommand, cmd_input),
"rooms" => convert_to!(ListRoomsCommand, cmd_input), "rooms" => convert_to!(ListRoomsCommand, cmd_input),
"room" => convert_to!(SetRoomCommand, cmd_input),
_ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()), _ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()),
}, },
//All other errors passed up. //All other errors passed up.

View File

@ -1,11 +1,81 @@
use super::{Command, Execution, ExecutionResult}; use super::{Command, Execution, ExecutionResult};
use crate::context::Context; use crate::context::Context;
use crate::error::BotError; use crate::error::BotError;
use crate::matrix;
use async_trait::async_trait; use async_trait::async_trait;
use fuse_rust::{Fuse, FuseProperty, Fuseable};
use futures::stream::{self, StreamExt, TryStreamExt}; use futures::stream::{self, StreamExt, TryStreamExt};
use matrix_sdk::identifiers::UserId; use matrix_sdk::{identifiers::UserId, Client};
use std::convert::TryFrom; use std::convert::TryFrom;
/// Holds matrix room ID and display name as strings, for use with
/// searching. See search_for_room.
#[derive(Clone, Debug, Eq, PartialEq)]
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<FuseProperty> {
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],
search_for: &str,
) -> Option<&'a RoomNameAndId> {
//Lowest score is the best match.
let best_fuzzy_match = || -> Option<&RoomNameAndId> {
Fuse::default()
.search_text_in_fuse_list(search_for, &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))
};
rooms_for_user
.iter()
.find(|room| room.id == search_for)
.or_else(best_fuzzy_match)
}
async fn get_rooms_for_user(
client: &Client,
user_id: &str,
) -> Result<Vec<RoomNameAndId>, 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<RoomNameAndId> = 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; pub struct ListRoomsCommand;
impl From<ListRoomsCommand> for Box<dyn Command> { impl From<ListRoomsCommand> for Box<dyn Command> {
@ -25,7 +95,7 @@ impl TryFrom<String> for ListRoomsCommand {
#[async_trait] #[async_trait]
impl Command for ListRoomsCommand { impl Command for ListRoomsCommand {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"list rooms command" "list rooms"
} }
fn is_secure(&self) -> bool { fn is_secure(&self) -> bool {
@ -33,22 +103,78 @@ impl Command for ListRoomsCommand {
} }
async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult { async fn execute(&self, ctx: &Context<'_>) -> ExecutionResult {
let user_id = UserId::try_from(ctx.username)?; let rooms_for_user: Vec<String> = get_rooms_for_user(ctx.matrix_client, ctx.username)
let rooms_for_user = crate::matrix::get_rooms_for_user(ctx.matrix_client, &user_id).await?;
let rooms_for_user: Vec<String> = stream::iter(rooms_for_user)
.filter_map(|room| async move {
Some(
room.display_name()
.await .await
.map(|room_name| (room.room_id().to_string(), room_name)), .map(|rooms| {
) rooms
}) .into_iter()
.map_ok(|(room_id, room_name)| format!(" {} | {}", room_id, room_name)) .map(|room| format!(" {} | {}", room.id, room.name))
.try_collect() .collect()
.await?; })?;
let html = format!("<pre>{}</pre>", rooms_for_user.join("\n")); let html = format!("<pre>{}</pre>", rooms_for_user.join("\n"));
Execution::success(html) Execution::success(html)
} }
} }
pub struct SetRoomCommand(String);
impl From<SetRoomCommand> for Box<dyn Command> {
fn from(cmd: SetRoomCommand) -> Self {
Box::new(cmd)
}
}
impl TryFrom<String> for SetRoomCommand {
type Error = BotError;
fn try_from(input: String) -> Result<Self, Self::Error> {
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)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn set_room_prefers_room_id_over_name() {
let rooms = vec![
RoomNameAndId {
id: "roomid".to_string(),
name: "room_name".to_string(),
},
RoomNameAndId {
id: "anotherone".to_string(),
name: "roomid".to_string(),
},
];
let found_room = search_for_room(&rooms, "roomid");
assert!(found_room.is_some());
assert_eq!(found_room.unwrap(), &rooms[0]);
}
}

View File

@ -90,6 +90,9 @@ pub enum BotError {
#[error("user account already exists")] #[error("user account already exists")]
AccountAlreadyExists, AccountAlreadyExists,
#[error("room name or id does not exist")]
RoomDoesNotExist,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]