first commit
This commit is contained in:
58
src/bar_items/buffer_name.rs
Normal file
58
src/bar_items/buffer_name.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{BarItem, BarItemCallback},
|
||||
Weechat,
|
||||
};
|
||||
|
||||
use crate::{BufferOwner, Servers};
|
||||
|
||||
pub(super) struct BufferName {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl BufferName {
|
||||
pub(super) fn create(servers: Servers) -> Result<BarItem, ()> {
|
||||
let status = BufferName { servers };
|
||||
BarItem::new("buffer_name", status)
|
||||
}
|
||||
}
|
||||
|
||||
impl BarItemCallback for BufferName {
|
||||
fn callback(&mut self, _: &Weechat, buffer: &Buffer) -> String {
|
||||
match self.servers.buffer_owner(buffer) {
|
||||
BufferOwner::Server(server) => {
|
||||
let color = if server.is_connection_secure() {
|
||||
"status_name_ssl"
|
||||
} else {
|
||||
"status_name"
|
||||
};
|
||||
|
||||
format!(
|
||||
"{color}server{del_color}[{color}{name}{del_color}]",
|
||||
color = Weechat::color(color),
|
||||
del_color = Weechat::color("bar_delim"),
|
||||
name = server.name()
|
||||
)
|
||||
}
|
||||
|
||||
BufferOwner::Room(server, _) => {
|
||||
let color = if server.is_connection_secure() {
|
||||
"status_name_ssl"
|
||||
} else {
|
||||
"status_name"
|
||||
};
|
||||
|
||||
format!("{}{}", Weechat::color(color), buffer.short_name())
|
||||
}
|
||||
|
||||
BufferOwner::Verification(_, _) => {
|
||||
// TODO special format this
|
||||
format!("{}{}", Weechat::color("status_name"), buffer.name())
|
||||
}
|
||||
|
||||
BufferOwner::None => {
|
||||
format!("{}{}", Weechat::color("status_name"), buffer.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/bar_items/buffer_plugin.rs
Normal file
38
src/bar_items/buffer_plugin.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{BarItem, BarItemCallback},
|
||||
Weechat,
|
||||
};
|
||||
|
||||
use crate::{BufferOwner, Servers, PLUGIN_NAME};
|
||||
|
||||
pub(super) struct BufferPlugin {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl BufferPlugin {
|
||||
pub(super) fn create(servers: Servers) -> Result<BarItem, ()> {
|
||||
let status = Self { servers };
|
||||
BarItem::new("buffer_plugin", status)
|
||||
}
|
||||
}
|
||||
|
||||
impl BarItemCallback for BufferPlugin {
|
||||
fn callback(&mut self, _: &Weechat, buffer: &Buffer) -> String {
|
||||
match self.servers.buffer_owner(buffer) {
|
||||
BufferOwner::Server(s)
|
||||
| BufferOwner::Room(s, _)
|
||||
| BufferOwner::Verification(s, _) => {
|
||||
format!(
|
||||
"{plugin_name}{del_color}/{color}{name}",
|
||||
plugin_name = PLUGIN_NAME,
|
||||
del_color = Weechat::color("bar_delim"),
|
||||
color = Weechat::color("bar_fg"),
|
||||
name = s.name()
|
||||
)
|
||||
}
|
||||
|
||||
BufferOwner::None => "".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/bar_items/mod.rs
Normal file
29
src/bar_items/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
mod buffer_name;
|
||||
mod buffer_plugin;
|
||||
mod status;
|
||||
|
||||
use weechat::hooks::BarItem;
|
||||
|
||||
use crate::Servers;
|
||||
use buffer_name::BufferName;
|
||||
use buffer_plugin::BufferPlugin;
|
||||
use status::Status;
|
||||
|
||||
pub struct BarItems {
|
||||
#[allow(dead_code)]
|
||||
status: BarItem,
|
||||
#[allow(dead_code)]
|
||||
buffer_name: BarItem,
|
||||
#[allow(dead_code)]
|
||||
buffer_plugin: BarItem,
|
||||
}
|
||||
|
||||
impl BarItems {
|
||||
pub fn hook_all(servers: Servers) -> Result<Self, ()> {
|
||||
Ok(Self {
|
||||
status: Status::create(servers.clone())?,
|
||||
buffer_name: BufferName::create(servers.clone())?,
|
||||
buffer_plugin: BufferPlugin::create(servers)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
54
src/bar_items/status.rs
Normal file
54
src/bar_items/status.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{BarItem, BarItemCallback},
|
||||
Weechat,
|
||||
};
|
||||
|
||||
use crate::{BufferOwner, Servers};
|
||||
|
||||
pub(super) struct Status {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub(super) fn create(servers: Servers) -> Result<BarItem, ()> {
|
||||
let status = Status { servers };
|
||||
BarItem::new("buffer_modes", status)
|
||||
}
|
||||
}
|
||||
|
||||
impl BarItemCallback for Status {
|
||||
fn callback(&mut self, _: &Weechat, buffer: &Buffer) -> String {
|
||||
let mut signs = Vec::new();
|
||||
|
||||
if let BufferOwner::Room(server, room) =
|
||||
self.servers.buffer_owner(buffer)
|
||||
{
|
||||
if room.is_encrypted() {
|
||||
signs.push(
|
||||
server.config().borrow().look().encrypted_room_sign(),
|
||||
);
|
||||
|
||||
if !room.contains_only_verified_devices() {
|
||||
signs.push(
|
||||
server
|
||||
.config()
|
||||
.borrow()
|
||||
.look()
|
||||
.encryption_warning_sign(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if room.is_public() {
|
||||
signs.push(server.config().borrow().look().public_room_sign());
|
||||
}
|
||||
|
||||
if room.is_busy() {
|
||||
signs.push(server.config().borrow().look().busy_sign());
|
||||
}
|
||||
}
|
||||
|
||||
signs.join("")
|
||||
}
|
||||
}
|
||||
39
src/commands/buffer_clear.rs
Normal file
39
src/commands/buffer_clear.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{CommandRun, CommandRunCallback},
|
||||
ReturnCode, Weechat,
|
||||
};
|
||||
|
||||
use crate::Servers;
|
||||
|
||||
pub struct BufferClearCommand {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl BufferClearCommand {
|
||||
pub fn create(servers: &Servers) -> Result<CommandRun, ()> {
|
||||
CommandRun::new(
|
||||
"/buffer clear",
|
||||
BufferClearCommand {
|
||||
servers: servers.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRunCallback for BufferClearCommand {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_: &Weechat,
|
||||
buffer: &Buffer,
|
||||
_: Cow<str>,
|
||||
) -> ReturnCode {
|
||||
if let Some(room) = self.servers.find_room(buffer) {
|
||||
room.reset_prev_batch();
|
||||
}
|
||||
|
||||
ReturnCode::Ok
|
||||
}
|
||||
}
|
||||
151
src/commands/devices.rs
Normal file
151
src/commands/devices.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use clap::{
|
||||
App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches,
|
||||
SubCommand,
|
||||
};
|
||||
use matrix_sdk::ruma::{OwnedDeviceId, OwnedUserId, UserId};
|
||||
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{Command, CommandCallback, CommandSettings},
|
||||
Args, Prefix, Weechat,
|
||||
};
|
||||
|
||||
use crate::Servers;
|
||||
|
||||
use super::parse_and_run;
|
||||
|
||||
pub struct DevicesCommand {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl DevicesCommand {
|
||||
pub const DESCRIPTION: &'static str =
|
||||
"List, delete or rename Matrix devices";
|
||||
|
||||
pub const SETTINGS: &'static [ArgParseSettings] = &[
|
||||
ArgParseSettings::DisableHelpFlags,
|
||||
ArgParseSettings::DisableVersion,
|
||||
ArgParseSettings::VersionlessSubcommands,
|
||||
ArgParseSettings::SubcommandRequiredElseHelp,
|
||||
];
|
||||
|
||||
pub fn create(servers: &Servers) -> Result<Command, ()> {
|
||||
let settings = CommandSettings::new("devices")
|
||||
.description(Self::DESCRIPTION)
|
||||
.add_argument("list")
|
||||
.add_argument("delete <device-id>")
|
||||
.add_argument("set-name <device-id> <name>")
|
||||
.arguments_description(
|
||||
"device-id: The unique id of the device that should be deleted.
|
||||
name: The name that the device name should be set to.",
|
||||
)
|
||||
.add_completion("list %(matrix-users)")
|
||||
.add_completion("delete %(matrix-own-devices)")
|
||||
.add_completion("set-name %(matrix-own-devices)")
|
||||
.add_completion("help list|delete|set-name");
|
||||
|
||||
Command::new(
|
||||
settings,
|
||||
DevicesCommand {
|
||||
servers: servers.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn delete(servers: &Servers, buffer: &Buffer, devices: Vec<OwnedDeviceId>) {
|
||||
let server = servers.find_server(buffer);
|
||||
|
||||
if let Some(s) = server {
|
||||
let devices = || async move {
|
||||
s.delete_devices(devices).await;
|
||||
};
|
||||
Weechat::spawn(devices()).detach();
|
||||
} else {
|
||||
Weechat::print("Must be executed on Matrix buffer")
|
||||
}
|
||||
}
|
||||
|
||||
fn list(servers: &Servers, buffer: &Buffer, user_id: Option<OwnedUserId>) {
|
||||
let server = servers.find_server(buffer);
|
||||
|
||||
if let Some(s) = server {
|
||||
let devices = || async move {
|
||||
s.devices(user_id).await;
|
||||
};
|
||||
Weechat::spawn(devices()).detach();
|
||||
} else {
|
||||
Weechat::print("Must be executed on Matrix buffer")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(buffer: &Buffer, servers: &Servers, args: &ArgMatches) {
|
||||
match args.subcommand() {
|
||||
("list", args) => {
|
||||
let user_id = args.and_then(|a| {
|
||||
a.args.get("user-id").and_then(|a| {
|
||||
a.vals.first().map(|u| {
|
||||
UserId::parse(u.to_string_lossy().as_ref())
|
||||
.expect("Argument wasn't a valid user id")
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
Self::list(servers, buffer, user_id);
|
||||
}
|
||||
("delete", args) => {
|
||||
let devices = args
|
||||
.and_then(|a| a.args.get("device-id"))
|
||||
.expect("Args didn't contain any device ids");
|
||||
let devices: Vec<OwnedDeviceId> = devices
|
||||
.vals
|
||||
.iter()
|
||||
.map(|d| d.clone().to_string_lossy().as_ref().into())
|
||||
.collect();
|
||||
Self::delete(servers, buffer, devices);
|
||||
}
|
||||
_ => Weechat::print(&format!(
|
||||
"{}Subcommand isn't implemented",
|
||||
Weechat::prefix(Prefix::Error)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subcommands() -> Vec<Argparse<'static, 'static>> {
|
||||
vec![
|
||||
SubCommand::with_name("list")
|
||||
.arg(Arg::with_name("user-id").required(false).validator(|u| {
|
||||
UserId::parse(u)
|
||||
.map_err(|_| {
|
||||
"The given user isn't a valid user ID".to_owned()
|
||||
})
|
||||
.map(|_| ())
|
||||
}))
|
||||
.about("List your own Matrix devices on the server."),
|
||||
SubCommand::with_name("delete")
|
||||
.about("Delete the given device")
|
||||
.arg(
|
||||
Arg::with_name("device-id")
|
||||
.require_delimiter(true)
|
||||
.multiple(true)
|
||||
.required(true),
|
||||
),
|
||||
SubCommand::with_name("set-name")
|
||||
.about("Set the human readable name of the given device")
|
||||
.arg(Arg::with_name("device-id").required(true))
|
||||
.arg(Arg::with_name("name").required(true)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandCallback for DevicesCommand {
|
||||
fn callback(&mut self, _: &Weechat, buffer: &Buffer, arguments: Args) {
|
||||
let argparse = Argparse::new("devices")
|
||||
.about(Self::DESCRIPTION)
|
||||
.settings(Self::SETTINGS)
|
||||
.subcommands(Self::subcommands());
|
||||
|
||||
parse_and_run(argparse, arguments, |matches| {
|
||||
Self::run(buffer, &self.servers, matches)
|
||||
});
|
||||
}
|
||||
}
|
||||
130
src/commands/keys.rs
Normal file
130
src/commands/keys.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{
|
||||
App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches,
|
||||
SubCommand,
|
||||
};
|
||||
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{Command, CommandCallback, CommandSettings},
|
||||
Args, Weechat,
|
||||
};
|
||||
|
||||
use super::parse_and_run;
|
||||
use crate::{MatrixServer, Servers};
|
||||
|
||||
pub struct KeysCommand {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl KeysCommand {
|
||||
pub const DESCRIPTION: &'static str = "Import or export E2EE keys.";
|
||||
pub const COMPLETION: &'static str = "import|export %(filename)";
|
||||
pub const SETTINGS: &'static [ArgParseSettings] = &[
|
||||
ArgParseSettings::DisableHelpFlags,
|
||||
ArgParseSettings::DisableVersion,
|
||||
ArgParseSettings::VersionlessSubcommands,
|
||||
ArgParseSettings::SubcommandRequiredElseHelp,
|
||||
];
|
||||
|
||||
pub fn create(servers: &Servers) -> Result<Command, ()> {
|
||||
let settings = CommandSettings::new("keys")
|
||||
.description(Self::DESCRIPTION)
|
||||
.add_argument("import <file> <passphrase>")
|
||||
.add_argument("export <file> <passphrase>")
|
||||
.arguments_description(
|
||||
"file: Path to a file that is or will contain the E2EE keys export",
|
||||
)
|
||||
.add_completion(Self::COMPLETION)
|
||||
.add_completion("help import|export");
|
||||
|
||||
Command::new(
|
||||
settings,
|
||||
Self {
|
||||
servers: servers.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn upcast_args(args: &ArgMatches) -> (PathBuf, String) {
|
||||
let passphrase = args
|
||||
.args
|
||||
.get("passphrase")
|
||||
.and_then(|p| p.vals.first().map(|p| p.clone().into_string().ok()))
|
||||
.flatten()
|
||||
.expect("No passphrase found");
|
||||
|
||||
let file = args
|
||||
.args
|
||||
.get("file")
|
||||
.and_then(|f| f.vals.first())
|
||||
.expect("No file found");
|
||||
let file = Weechat::expand_home(&file.to_string_lossy());
|
||||
let file = PathBuf::from(file);
|
||||
(file, passphrase)
|
||||
}
|
||||
|
||||
fn import(server: MatrixServer, file: PathBuf, passphrase: String) {
|
||||
let import = || async move {
|
||||
server.import_keys(file, passphrase).await;
|
||||
};
|
||||
Weechat::spawn(import()).detach();
|
||||
}
|
||||
|
||||
fn export(server: MatrixServer, file: PathBuf, passphrase: String) {
|
||||
let export = || async move {
|
||||
server.export_keys(file, passphrase).await;
|
||||
};
|
||||
Weechat::spawn(export()).detach();
|
||||
}
|
||||
|
||||
pub fn run(buffer: &Buffer, servers: &Servers, args: &ArgMatches) {
|
||||
if let Some(server) = servers.find_server(buffer) {
|
||||
match args.subcommand() {
|
||||
("import", args) => {
|
||||
let (file, passphrase) = Self::upcast_args(
|
||||
args.expect("No args were provided to the subcommand"),
|
||||
);
|
||||
Self::import(server, file, passphrase);
|
||||
}
|
||||
("export", args) => {
|
||||
let (file, passphrase) = Self::upcast_args(
|
||||
args.expect("No args were provided to the subcommand"),
|
||||
);
|
||||
Self::export(server, file, passphrase);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
Weechat::print("Must be executed on Matrix buffer")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subcommands() -> Vec<Argparse<'static, 'static>> {
|
||||
vec![
|
||||
SubCommand::with_name("import")
|
||||
.about("Import the E2EE keys from the given file.")
|
||||
.arg(Arg::with_name("file").required(true))
|
||||
.arg(Arg::with_name("passphrase").required(true)),
|
||||
SubCommand::with_name("export")
|
||||
// TODO add the ability to export keys only for a given room.
|
||||
.about("Export your E2EE keys to the given file.")
|
||||
.arg(Arg::with_name("file").required(true))
|
||||
.arg(Arg::with_name("passphrase").required(true)),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandCallback for KeysCommand {
|
||||
fn callback(&mut self, _: &Weechat, buffer: &Buffer, arguments: Args) {
|
||||
let argparse = Argparse::new("keys")
|
||||
.about(Self::DESCRIPTION)
|
||||
.settings(Self::SETTINGS)
|
||||
.subcommands(Self::subcommands());
|
||||
|
||||
parse_and_run(argparse, arguments, |matches| {
|
||||
Self::run(buffer, &self.servers, matches)
|
||||
});
|
||||
}
|
||||
}
|
||||
330
src/commands/matrix.rs
Normal file
330
src/commands/matrix.rs
Normal file
@@ -0,0 +1,330 @@
|
||||
use clap::{
|
||||
App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches,
|
||||
SubCommand,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{Command, CommandCallback, CommandSettings},
|
||||
Args, Prefix, Weechat,
|
||||
};
|
||||
|
||||
use super::{parse_and_run, verification::VerificationCommand};
|
||||
use crate::{
|
||||
commands::{DevicesCommand, KeysCommand},
|
||||
config::ConfigHandle,
|
||||
MatrixServer, Servers, PLUGIN_NAME,
|
||||
};
|
||||
|
||||
pub struct MatrixCommand {
|
||||
servers: Servers,
|
||||
config: ConfigHandle,
|
||||
}
|
||||
|
||||
impl MatrixCommand {
|
||||
pub fn create(
|
||||
servers: &Servers,
|
||||
config: &ConfigHandle,
|
||||
) -> Result<Command, ()> {
|
||||
let matrix_settings = CommandSettings::new("matrix")
|
||||
.description("Matrix chat protocol command.")
|
||||
.add_argument("server add <server-name> <hostname>[:<port>]")
|
||||
.add_argument("server delete|list|listfull <server-name>")
|
||||
.add_argument("connect <server-name>")
|
||||
.add_argument("devices delete|list|set-name")
|
||||
.add_argument("keys import|export <file> <passphrase>")
|
||||
.add_argument("disconnect <server-name>")
|
||||
.add_argument("reconnect <server-name>")
|
||||
.add_argument("help <matrix-command> [<matrix-subcommand>]")
|
||||
.arguments_description(format!(
|
||||
" server: List, add, or remove Matrix servers.
|
||||
connect: Connect to Matrix servers.
|
||||
disconnect: Disconnect from one or all Matrix servers.
|
||||
reconnect: Reconnect to server(s).
|
||||
devices: {}
|
||||
keys: {}
|
||||
verification: {}
|
||||
help: Show detailed command help.\n
|
||||
Use /matrix [command] help to find out more.\n",
|
||||
DevicesCommand::DESCRIPTION,
|
||||
KeysCommand::DESCRIPTION,
|
||||
VerificationCommand::DESCRIPTION,
|
||||
))
|
||||
.add_completion("server add|delete|list|listfull")
|
||||
.add_completion("devices list|delete|set-name %(matrix-users)")
|
||||
.add_completion(format!("keys {}", KeysCommand::COMPLETION))
|
||||
.add_completion(format!(
|
||||
"verification {}",
|
||||
VerificationCommand::COMPLETION
|
||||
))
|
||||
.add_completion("connect %(matrix_servers)")
|
||||
.add_completion("disconnect %(matrix_servers)")
|
||||
.add_completion("reconnect %(matrix_servers)")
|
||||
.add_completion(
|
||||
"help server|connect|disconnect|reconnect|keys|devices",
|
||||
);
|
||||
|
||||
Command::new(
|
||||
matrix_settings,
|
||||
MatrixCommand {
|
||||
servers: servers.clone(),
|
||||
config: config.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn add_server(&self, args: &ArgMatches) {
|
||||
let server_name = args
|
||||
.value_of("name")
|
||||
.expect("Server name not set but was required");
|
||||
let homeserver = args
|
||||
.value_of("homeserver")
|
||||
.expect("Homeserver not set but was required");
|
||||
let homeserver = Url::parse(homeserver)
|
||||
.expect("Can't parse Homeserver even if validation passed");
|
||||
|
||||
let mut config_borrow = self.config.borrow_mut();
|
||||
let mut section = config_borrow
|
||||
.search_section_mut("server")
|
||||
.expect("Can't get server section");
|
||||
|
||||
let server = MatrixServer::new(
|
||||
server_name,
|
||||
&self.config,
|
||||
&mut section,
|
||||
self.servers.clone(),
|
||||
);
|
||||
|
||||
self.servers.insert(server);
|
||||
|
||||
let homeserver_option = section
|
||||
.search_option(&format!("{}.homeserver", server_name))
|
||||
.expect("Homeserver option wasn't created");
|
||||
homeserver_option.set(homeserver.as_str(), true);
|
||||
|
||||
Weechat::print(&format!(
|
||||
"{}: Server {}{}{} has been added.",
|
||||
PLUGIN_NAME,
|
||||
Weechat::color("chat_server"),
|
||||
server_name,
|
||||
Weechat::color("reset")
|
||||
));
|
||||
}
|
||||
|
||||
fn delete_server(&self, args: &ArgMatches) {
|
||||
let server_name = args
|
||||
.value_of("name")
|
||||
.expect("Server name not set but was required");
|
||||
|
||||
let connected = {
|
||||
if let Some(s) = self.servers.get(server_name) {
|
||||
s.connected()
|
||||
} else {
|
||||
Weechat::print(&format!(
|
||||
"{}: No such server {}{}{} found.",
|
||||
PLUGIN_NAME,
|
||||
Weechat::color("chat_server"),
|
||||
server_name,
|
||||
Weechat::color("reset")
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if connected {
|
||||
Weechat::print(&format!(
|
||||
"{}: Server {}{}{} is still connected.",
|
||||
PLUGIN_NAME,
|
||||
Weechat::color("chat_server"),
|
||||
server_name,
|
||||
Weechat::color("reset")
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
let server = self.servers.remove(server_name).unwrap();
|
||||
|
||||
drop(server);
|
||||
|
||||
Weechat::print(&format!(
|
||||
"{}: Server {}{}{} has been deleted.",
|
||||
PLUGIN_NAME,
|
||||
Weechat::color("chat_server"),
|
||||
server_name,
|
||||
Weechat::color("reset")
|
||||
));
|
||||
}
|
||||
|
||||
fn list_servers(&self, details: bool) {
|
||||
if self.servers.borrow().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
Weechat::print("\nAll Matrix servers:");
|
||||
|
||||
// TODO print out some stats if the server is connected.
|
||||
for server in self.servers.borrow().values() {
|
||||
Weechat::print(&format!(" {}", server.get_info_str(details)));
|
||||
}
|
||||
}
|
||||
|
||||
fn server_command(&self, args: &ArgMatches) {
|
||||
match args.subcommand() {
|
||||
("add", Some(subargs)) => self.add_server(subargs),
|
||||
("delete", Some(subargs)) => self.delete_server(subargs),
|
||||
("list", _) => self.list_servers(false),
|
||||
("listfull", _) => self.list_servers(true),
|
||||
_ => self.list_servers(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn server_not_found(&self, server_name: &str) {
|
||||
Weechat::print(&format!(
|
||||
"{}{}: Server \"{}{}{}\" not found.",
|
||||
Weechat::prefix(Prefix::Error),
|
||||
PLUGIN_NAME,
|
||||
Weechat::color("chat_server"),
|
||||
server_name,
|
||||
Weechat::color("reset")
|
||||
));
|
||||
}
|
||||
|
||||
fn connect_command(&self, args: &ArgMatches) {
|
||||
let server_names = args
|
||||
.values_of("name")
|
||||
.expect("Server names not set but were required");
|
||||
|
||||
for server_name in server_names {
|
||||
if let Some(s) = self.servers.get(server_name) {
|
||||
match s.connect() {
|
||||
Ok(_) => (),
|
||||
Err(e) => Weechat::print(&format!("{:?}", e)),
|
||||
}
|
||||
} else {
|
||||
self.server_not_found(server_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disconnect_command(&self, args: &ArgMatches) {
|
||||
let server_name = args
|
||||
.value_of("name")
|
||||
.expect("Server name not set but was required");
|
||||
|
||||
if let Some(s) = self.servers.get(server_name) {
|
||||
s.disconnect();
|
||||
} else {
|
||||
self.server_not_found(server_name)
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&self, buffer: &Buffer, args: &ArgMatches) {
|
||||
match args.subcommand() {
|
||||
("connect", Some(subargs)) => self.connect_command(subargs),
|
||||
("disconnect", Some(subargs)) => self.disconnect_command(subargs),
|
||||
("server", Some(subargs)) => self.server_command(subargs),
|
||||
("devices", Some(subargs)) => {
|
||||
DevicesCommand::run(buffer, &self.servers, subargs)
|
||||
}
|
||||
("keys", Some(subargs)) => {
|
||||
KeysCommand::run(buffer, &self.servers, subargs)
|
||||
}
|
||||
("verification", Some(subargs)) => {
|
||||
VerificationCommand::run(buffer, &self.servers, subargs)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandCallback for MatrixCommand {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_weechat: &Weechat,
|
||||
buffer: &Buffer,
|
||||
arguments: Args,
|
||||
) {
|
||||
let server_command = SubCommand::with_name("server")
|
||||
.about("List, add or delete Matrix servers.")
|
||||
.subcommand(
|
||||
SubCommand::with_name("add")
|
||||
.about("Add a new Matrix server.")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.value_name("server-name")
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("homeserver")
|
||||
.required(true)
|
||||
.validator(MatrixServer::parse_url),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("delete")
|
||||
.about("Delete an existing Matrix server.")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.value_name("server-name")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("list")
|
||||
.about("List the configured Matrix servers."),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("listfull")
|
||||
.about("List detailed information about the configured Matrix servers."),
|
||||
);
|
||||
|
||||
let argparse = Argparse::new("matrix")
|
||||
.about("Matrix chat protocol command.")
|
||||
.global_settings(&[
|
||||
ArgParseSettings::DisableHelpFlags,
|
||||
ArgParseSettings::DisableVersion,
|
||||
ArgParseSettings::VersionlessSubcommands,
|
||||
])
|
||||
.setting(ArgParseSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(server_command)
|
||||
.subcommand(
|
||||
SubCommand::with_name("devices")
|
||||
.about(DevicesCommand::DESCRIPTION)
|
||||
.settings(DevicesCommand::SETTINGS)
|
||||
.subcommands(DevicesCommand::subcommands()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("keys")
|
||||
.about(KeysCommand::DESCRIPTION)
|
||||
.settings(KeysCommand::SETTINGS)
|
||||
.subcommands(KeysCommand::subcommands()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("verification")
|
||||
.about(VerificationCommand::DESCRIPTION)
|
||||
.subcommands(VerificationCommand::subcommands()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("connect")
|
||||
.about("Connect to Matrix servers.")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.value_name("server-name")
|
||||
.required(true)
|
||||
.multiple(true),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("disconnect")
|
||||
.about("Disconnect from one or all Matrix servers")
|
||||
.arg(
|
||||
Arg::with_name("name")
|
||||
.value_name("server-name")
|
||||
.required(true),
|
||||
),
|
||||
);
|
||||
|
||||
parse_and_run(argparse, arguments, |args| self.run(buffer, args));
|
||||
}
|
||||
}
|
||||
44
src/commands/me.rs
Normal file
44
src/commands/me.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{CommandRun, CommandRunCallback},
|
||||
ReturnCode, Weechat,
|
||||
};
|
||||
|
||||
use crate::Servers;
|
||||
|
||||
pub struct MeCommand {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl MeCommand {
|
||||
pub fn create(servers: &Servers) -> Result<CommandRun, ()> {
|
||||
CommandRun::new(
|
||||
"/me",
|
||||
MeCommand {
|
||||
servers: servers.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRunCallback for MeCommand {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_: &Weechat,
|
||||
buffer: &Buffer,
|
||||
cmd: Cow<str>,
|
||||
) -> ReturnCode {
|
||||
if let Some(room) = self.servers.find_room(buffer) {
|
||||
self.servers.runtime().block_on(room.send_message(
|
||||
RoomMessageEventContent::emote_plain(
|
||||
cmd.strip_prefix("/me ").map(|s| s.to_string()).unwrap(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
ReturnCode::Ok
|
||||
}
|
||||
}
|
||||
69
src/commands/mod.rs
Normal file
69
src/commands/mod.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use clap::{App, ArgMatches};
|
||||
use verification::VerificationCommand;
|
||||
use weechat::{
|
||||
hooks::{Command, CommandRun},
|
||||
Args, Weechat,
|
||||
};
|
||||
|
||||
use crate::{config::ConfigHandle, Servers};
|
||||
|
||||
mod buffer_clear;
|
||||
mod devices;
|
||||
mod keys;
|
||||
mod matrix;
|
||||
mod me;
|
||||
mod page_up;
|
||||
mod verification;
|
||||
|
||||
use buffer_clear::BufferClearCommand;
|
||||
use devices::DevicesCommand;
|
||||
use keys::KeysCommand;
|
||||
use matrix::MatrixCommand;
|
||||
use me::MeCommand;
|
||||
use page_up::PageUpCommand;
|
||||
|
||||
pub struct Commands {
|
||||
_matrix: Command,
|
||||
_keys: Command,
|
||||
_devices: Command,
|
||||
_page_up: CommandRun,
|
||||
_verification: Command,
|
||||
_buffer_clear: CommandRun,
|
||||
_me: CommandRun,
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub fn hook_all(
|
||||
servers: &Servers,
|
||||
config: &ConfigHandle,
|
||||
) -> Result<Commands, ()> {
|
||||
Ok(Commands {
|
||||
_matrix: MatrixCommand::create(servers, config)?,
|
||||
_devices: DevicesCommand::create(servers)?,
|
||||
_keys: KeysCommand::create(servers)?,
|
||||
_page_up: PageUpCommand::create(servers)?,
|
||||
_verification: VerificationCommand::create(servers)?,
|
||||
_buffer_clear: BufferClearCommand::create(servers)?,
|
||||
_me: MeCommand::create(servers)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_and_run(
|
||||
parser: App,
|
||||
arguments: Args,
|
||||
command: impl FnOnce(&ArgMatches),
|
||||
) {
|
||||
match parser.get_matches_from_safe(arguments) {
|
||||
Ok(m) => command(&m),
|
||||
Err(e) => {
|
||||
let error = Weechat::execute_modifier(
|
||||
"color_decode_ansi",
|
||||
"1",
|
||||
&e.to_string(),
|
||||
)
|
||||
.expect("Can't color decode ansi string");
|
||||
Weechat::print(&error);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/commands/page_up.rs
Normal file
44
src/commands/page_up.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{CommandRun, CommandRunCallback},
|
||||
ReturnCode, Weechat,
|
||||
};
|
||||
|
||||
use crate::Servers;
|
||||
|
||||
pub struct PageUpCommand {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl PageUpCommand {
|
||||
pub fn create(servers: &Servers) -> Result<CommandRun, ()> {
|
||||
CommandRun::new(
|
||||
"/window page_up",
|
||||
PageUpCommand {
|
||||
servers: servers.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandRunCallback for PageUpCommand {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_: &Weechat,
|
||||
buffer: &Buffer,
|
||||
_: Cow<str>,
|
||||
) -> ReturnCode {
|
||||
if let Some(room) = self.servers.find_room(buffer) {
|
||||
if let Some(window) = buffer.window() {
|
||||
if window.is_first_line_displayed() || buffer.num_lines() == 0 {
|
||||
Weechat::spawn(async move { room.get_messages().await })
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReturnCode::Ok
|
||||
}
|
||||
}
|
||||
117
src/commands/verification.rs
Normal file
117
src/commands/verification.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use clap::{
|
||||
App as Argparse, AppSettings as ArgParseSettings, ArgMatches, SubCommand,
|
||||
};
|
||||
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{Command, CommandCallback, CommandSettings},
|
||||
Args, Weechat,
|
||||
};
|
||||
|
||||
use super::parse_and_run;
|
||||
use crate::{BufferOwner, Servers};
|
||||
|
||||
pub struct VerificationCommand {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
enum CommandType {
|
||||
Accept,
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl VerificationCommand {
|
||||
pub const DESCRIPTION: &'static str =
|
||||
"Control interactive verification flows";
|
||||
|
||||
pub const COMPLETION: &'static str = "accept|confirm|cancel";
|
||||
|
||||
pub fn create(servers: &Servers) -> Result<Command, ()> {
|
||||
let settings = CommandSettings::new("verification")
|
||||
.description(Self::DESCRIPTION)
|
||||
.add_argument("verification accept|confirm|cancel")
|
||||
.arguments_description(
|
||||
"accept: accept the verification request
|
||||
confirm: confirm that the emojis match on both sides or \
|
||||
confirm that the other side has scanned our QR code
|
||||
cancel: cancel the verification flow or request",
|
||||
)
|
||||
.add_completion(Self::COMPLETION)
|
||||
.add_completion("help accept|confirm|cancel");
|
||||
|
||||
Command::new(
|
||||
settings,
|
||||
VerificationCommand {
|
||||
servers: servers.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn verification(servers: &Servers, buffer: &Buffer, command: CommandType) {
|
||||
let buffer_owner = servers.buffer_owner(buffer);
|
||||
|
||||
match buffer_owner {
|
||||
BufferOwner::Room(_, b) => match command {
|
||||
CommandType::Accept => b.accept_verification(),
|
||||
CommandType::Confirm => b.confirm_verification(),
|
||||
CommandType::Cancel => b.cancel_verification(),
|
||||
},
|
||||
BufferOwner::Verification(_, b) => match command {
|
||||
CommandType::Accept => b.accept(),
|
||||
CommandType::Confirm => b.confirm(),
|
||||
CommandType::Cancel => b.cancel(),
|
||||
},
|
||||
BufferOwner::Server(_) | BufferOwner::None => {
|
||||
Weechat::print(
|
||||
"The verification command needs to be executed in a room or \
|
||||
verification buffer",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(buffer: &Buffer, servers: &Servers, args: &ArgMatches) {
|
||||
match args.subcommand() {
|
||||
("accept", _) => {
|
||||
Self::verification(servers, buffer, CommandType::Accept)
|
||||
}
|
||||
("confirm", _) => {
|
||||
Self::verification(servers, buffer, CommandType::Confirm)
|
||||
}
|
||||
("cancel", _) => {
|
||||
Self::verification(servers, buffer, CommandType::Cancel)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subcommands() -> Vec<Argparse<'static, 'static>> {
|
||||
vec![
|
||||
SubCommand::with_name("accept")
|
||||
.about("Accept a verification request"),
|
||||
SubCommand::with_name("confirm").about(
|
||||
"Confirm that the emoji matches or that the other side has \
|
||||
scanned our QR code",
|
||||
),
|
||||
SubCommand::with_name("cancel")
|
||||
.about("Cancel the verification flow"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandCallback for VerificationCommand {
|
||||
fn callback(&mut self, _: &Weechat, buffer: &Buffer, arguments: Args) {
|
||||
let argparse = Argparse::new("verification")
|
||||
.about(Self::DESCRIPTION)
|
||||
.global_setting(ArgParseSettings::DisableHelpFlags)
|
||||
.global_setting(ArgParseSettings::DisableVersion)
|
||||
.global_setting(ArgParseSettings::VersionlessSubcommands)
|
||||
.setting(ArgParseSettings::SubcommandRequiredElseHelp)
|
||||
.subcommands(Self::subcommands());
|
||||
|
||||
parse_and_run(argparse, arguments, |matches| {
|
||||
Self::run(buffer, &self.servers, &matches)
|
||||
});
|
||||
}
|
||||
}
|
||||
110
src/completions.rs
Normal file
110
src/completions.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use weechat::{
|
||||
buffer::Buffer,
|
||||
hooks::{
|
||||
Completion, CompletionCallback, CompletionHook, CompletionPosition,
|
||||
},
|
||||
Weechat,
|
||||
};
|
||||
|
||||
use crate::Servers;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Completions {
|
||||
servers: CompletionHook,
|
||||
users: CompletionHook,
|
||||
}
|
||||
|
||||
impl Completions {
|
||||
pub fn hook_all(servers: Servers) -> Result<Self, ()> {
|
||||
Ok(Self {
|
||||
servers: ServersCompletion::create(servers.clone())?,
|
||||
users: UsersCompletion::create(servers)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ServersCompletion {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl ServersCompletion {
|
||||
fn create(servers: Servers) -> Result<CompletionHook, ()> {
|
||||
let comp = ServersCompletion { servers };
|
||||
|
||||
CompletionHook::new(
|
||||
"matrix_servers",
|
||||
"Completion for the list of added Matrix servers",
|
||||
comp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionCallback for ServersCompletion {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_weechat: &Weechat,
|
||||
_buffer: &Buffer,
|
||||
_completion_name: Cow<str>,
|
||||
completion: &Completion,
|
||||
) -> Result<(), ()> {
|
||||
for server_name in self.servers.borrow().keys() {
|
||||
completion.add_with_options(
|
||||
server_name,
|
||||
false,
|
||||
CompletionPosition::Sorted,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct UsersCompletion {
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl UsersCompletion {
|
||||
fn create(servers: Servers) -> Result<CompletionHook, ()> {
|
||||
let comp = UsersCompletion { servers };
|
||||
|
||||
CompletionHook::new(
|
||||
"matrix-users",
|
||||
"Completion for the list of Matrix users",
|
||||
comp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionCallback for UsersCompletion {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_: &Weechat,
|
||||
buffer: &Buffer,
|
||||
_: Cow<str>,
|
||||
completion: &Completion,
|
||||
) -> Result<(), ()> {
|
||||
if let Some(server) = self.servers.find_server(buffer) {
|
||||
if let Some(connection) = server.connection() {
|
||||
let tracked_users = self
|
||||
.servers
|
||||
.runtime()
|
||||
.block_on(connection.client().encryption().tracked_users())
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!("Error getting tracked users: {e}");
|
||||
Default::default()
|
||||
});
|
||||
|
||||
for user in tracked_users.into_iter() {
|
||||
completion.add_with_options(
|
||||
user.as_str(),
|
||||
true,
|
||||
CompletionPosition::Sorted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
274
src/config.rs
Normal file
274
src/config.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
//! Configuration module
|
||||
//!
|
||||
//! This is the global plugin configuration.
|
||||
//!
|
||||
//! Configuration options should be split out into different sections:
|
||||
//!
|
||||
//! * network
|
||||
//! * look
|
||||
//! * color
|
||||
//! * server
|
||||
//!
|
||||
//! The server config options are added in the server.rs file.
|
||||
//!
|
||||
//! The config options created here will be alive as long as the plugin is
|
||||
//! loaded so they don't need to be freed manually. The drop implementation of
|
||||
//! the section will do so.
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use strum::{EnumVariantNames, VariantNames};
|
||||
use weechat::{
|
||||
config,
|
||||
config::{
|
||||
Conf, ConfigOption, ConfigSection, ConfigSectionSettings,
|
||||
EnumOptionSettings, OptionChanged, SectionReadCallback,
|
||||
},
|
||||
Weechat,
|
||||
};
|
||||
|
||||
use crate::{MatrixServer, Servers};
|
||||
|
||||
#[derive(EnumVariantNames)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
#[derive(Default)]
|
||||
pub enum RedactionStyle {
|
||||
#[default]
|
||||
StrikeThrough,
|
||||
Delete,
|
||||
Notice,
|
||||
}
|
||||
|
||||
impl From<i32> for RedactionStyle {
|
||||
fn from(value: i32) -> Self {
|
||||
match value {
|
||||
0 => RedactionStyle::StrikeThrough,
|
||||
1 => RedactionStyle::Delete,
|
||||
2 => RedactionStyle::Notice,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(EnumVariantNames)]
|
||||
#[strum(serialize_all = "kebab_case")]
|
||||
#[derive(Default)]
|
||||
pub enum ServerBuffer {
|
||||
#[default]
|
||||
MergeWithCore,
|
||||
MergeWithoutCore,
|
||||
Independent,
|
||||
}
|
||||
|
||||
impl From<i32> for ServerBuffer {
|
||||
fn from(value: i32) -> Self {
|
||||
match value {
|
||||
0 => ServerBuffer::MergeWithCore,
|
||||
1 => ServerBuffer::MergeWithoutCore,
|
||||
2 => ServerBuffer::Independent,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config!(
|
||||
"matrix-rust",
|
||||
|
||||
Section look {
|
||||
encrypted_room_sign: String {
|
||||
// Description.
|
||||
"A sign that is used to show that the current room is encrypted",
|
||||
// Default value.
|
||||
"🔒",
|
||||
},
|
||||
|
||||
encryption_warning_sign: String {
|
||||
// Description.
|
||||
"A sign that is used to show that the current room contains unverified devices",
|
||||
// Default value.
|
||||
"❗",
|
||||
},
|
||||
|
||||
public_room_sign: String {
|
||||
// Description.
|
||||
"A sign indicating that the current room is public",
|
||||
// Default value.
|
||||
"🌍",
|
||||
},
|
||||
|
||||
busy_sign: String {
|
||||
// Description.
|
||||
"A sign that is used to show that the client is busy, \
|
||||
e.g. when room history is being fetched",
|
||||
// Default value.
|
||||
"⏳",
|
||||
},
|
||||
|
||||
local_echo: bool {
|
||||
// Description
|
||||
"Should the sending message be printed out before the server \
|
||||
confirms the reception of the message",
|
||||
// Default value
|
||||
true,
|
||||
},
|
||||
|
||||
redaction_style: Enum {
|
||||
// Description
|
||||
"The style that should be used when a message needs to be redacted",
|
||||
RedactionStyle,
|
||||
},
|
||||
},
|
||||
|
||||
Section network {
|
||||
debug_buffer: bool {
|
||||
// Description
|
||||
"Use a separate buffer for debug logs",
|
||||
// Default value.
|
||||
false,
|
||||
},
|
||||
},
|
||||
|
||||
Section input {
|
||||
markdown_input: bool {
|
||||
// Description
|
||||
"Should the input be parsed as markdown",
|
||||
// Default value.
|
||||
true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/// A wrapper for our config struct that can be cloned around.
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigHandle {
|
||||
pub inner: Rc<RefCell<Config>>,
|
||||
servers: Servers,
|
||||
}
|
||||
|
||||
impl ConfigHandle {
|
||||
/// Create a new config and wrap it in our config handle.
|
||||
pub fn new(servers: &Servers) -> ConfigHandle {
|
||||
let config = Config::new().expect("Can't create new config");
|
||||
|
||||
let config = ConfigHandle {
|
||||
inner: Rc::new(RefCell::new(config)),
|
||||
servers: servers.clone(),
|
||||
};
|
||||
|
||||
// The server section is special since it has a custom section read and
|
||||
// write implementations to support subsections for every configured
|
||||
// server.
|
||||
let server_section_options = ConfigSectionSettings::new("server")
|
||||
.set_write_callback(
|
||||
|_weechat: &Weechat,
|
||||
config: &Conf,
|
||||
section: &mut ConfigSection| {
|
||||
config.write_section(section.name());
|
||||
for option in section.options() {
|
||||
config.write_option(option);
|
||||
}
|
||||
},
|
||||
)
|
||||
.set_read_callback(config.clone());
|
||||
|
||||
{
|
||||
let mut config_borrow = config.borrow_mut();
|
||||
|
||||
config_borrow
|
||||
.new_section(server_section_options)
|
||||
.expect("Can't create server section");
|
||||
|
||||
let mut look_section = config_borrow.look_mut();
|
||||
|
||||
let servers = servers.clone();
|
||||
|
||||
let settings = EnumOptionSettings::new("server_buffer")
|
||||
.description("Should the server buffer be merged with other buffers or independent")
|
||||
.set_change_callback(move |_, _| {
|
||||
for server in servers.borrow().values() {
|
||||
server.merge_server_buffers();
|
||||
}
|
||||
})
|
||||
.default_value(ServerBuffer::default() as i32)
|
||||
.string_values(
|
||||
ServerBuffer::VARIANTS
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
|
||||
look_section
|
||||
.new_enum_option(settings)
|
||||
.expect("Can't create server buffers option");
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn borrow(&self) -> Ref<'_, Config> {
|
||||
self.inner.borrow()
|
||||
}
|
||||
|
||||
pub fn borrow_mut(&self) -> RefMut<'_, Config> {
|
||||
self.inner.borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl LookSection<'_> {
|
||||
pub fn server_buffer(&self) -> ServerBuffer {
|
||||
if let ConfigOption::Enum(o) =
|
||||
self.search_option("server_buffer").unwrap()
|
||||
{
|
||||
ServerBuffer::from(o.value())
|
||||
} else {
|
||||
panic!("Server buffer option has the wrong type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionReadCallback for ConfigHandle {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_: &Weechat,
|
||||
_: &Conf,
|
||||
section: &mut ConfigSection,
|
||||
option_name: &str,
|
||||
option_value: &str,
|
||||
) -> OptionChanged {
|
||||
if option_name.is_empty() {
|
||||
return OptionChanged::Error;
|
||||
}
|
||||
|
||||
let option_args: Vec<&str> = option_name.splitn(2, '.').collect();
|
||||
|
||||
if option_args.len() != 2 {
|
||||
return OptionChanged::Error;
|
||||
}
|
||||
|
||||
let server_name = option_args[0];
|
||||
|
||||
// We are reading the config, if the server doesn't yet exists
|
||||
// we need to create it before setting the option and running
|
||||
// the option change callback.
|
||||
if !self.servers.contains(server_name) {
|
||||
let server = MatrixServer::new(
|
||||
server_name,
|
||||
self,
|
||||
section,
|
||||
self.servers.clone(),
|
||||
);
|
||||
self.servers.insert(server);
|
||||
}
|
||||
|
||||
let option = section.search_option(option_name);
|
||||
|
||||
if let Some(o) = option {
|
||||
o.set(option_value, true)
|
||||
} else {
|
||||
OptionChanged::NotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
569
src/connection.rs
Normal file
569
src/connection.rs
Normal file
@@ -0,0 +1,569 @@
|
||||
use std::{
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
runtime::Runtime,
|
||||
sync::mpsc::{channel, Receiver, Sender},
|
||||
};
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use matrix_sdk::{
|
||||
self,
|
||||
config::SyncSettings,
|
||||
deserialized_responses::AmbiguityChange,
|
||||
room::{Messages, MessagesOptions, Room},
|
||||
ruma::{
|
||||
api::client::{
|
||||
device::{
|
||||
delete_devices::v3::Response as DeleteDevicesResponse,
|
||||
get_devices::v3::Response as DevicesResponse,
|
||||
},
|
||||
filter::{
|
||||
FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter,
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomSendResponse,
|
||||
session::login::v3::Response as LoginResponse,
|
||||
sync::sync_events::v3::Filter,
|
||||
uiaa::{AuthData, Password, UserIdentifier},
|
||||
},
|
||||
events::{
|
||||
room::member::RoomMemberEventContent, AnyMessageLikeEventContent,
|
||||
AnySyncStateEvent, AnySyncTimelineEvent, AnyToDeviceEvent,
|
||||
SyncStateEvent,
|
||||
},
|
||||
OwnedDeviceId, OwnedRoomId, OwnedTransactionId,
|
||||
},
|
||||
sync::State,
|
||||
Client, LoopCtrl, Result as MatrixResult, RoomMemberships,
|
||||
};
|
||||
|
||||
use weechat::{Task, Weechat};
|
||||
|
||||
use crate::{
|
||||
room::PrevBatch,
|
||||
server::{InnerServer, MatrixServer},
|
||||
};
|
||||
|
||||
const DEFAULT_SYNC_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub struct InteractiveAuthInfo {
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub session: Option<String>,
|
||||
}
|
||||
|
||||
impl InteractiveAuthInfo {
|
||||
pub fn as_auth_data(&self) -> AuthData {
|
||||
AuthData::Password(Password::new(
|
||||
UserIdentifier::UserIdOrLocalpart(self.user.clone()),
|
||||
self.password.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ClientMessage {
|
||||
LoginMessage(LoginResponse),
|
||||
SyncState(OwnedRoomId, AnySyncStateEvent),
|
||||
SyncEvent(OwnedRoomId, AnySyncTimelineEvent),
|
||||
ToDeviceEvent(AnyToDeviceEvent),
|
||||
MemberEvent(
|
||||
OwnedRoomId,
|
||||
SyncStateEvent<RoomMemberEventContent>,
|
||||
bool,
|
||||
Option<AmbiguityChange>,
|
||||
),
|
||||
RestoredRoom(Room),
|
||||
}
|
||||
|
||||
/// Struct representing an active connection to the homeserver.
|
||||
///
|
||||
/// Since the rust-sdk `Client` object uses reqwest for the HTTP client making
|
||||
/// requests requires the request to be made on a tokio runtime. The connection
|
||||
/// wraps the `Client` object and makes sure that requests are run on the
|
||||
/// runtime the `Connection` holds.
|
||||
///
|
||||
/// While this struct is alive a sync loop will be going on. To cancel the sync
|
||||
/// loop drop the object.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Connection {
|
||||
#[allow(dead_code)]
|
||||
receiver_task: Rc<Task<()>>,
|
||||
client: Client,
|
||||
pub runtime: Rc<Runtime>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn client(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
pub async fn spawn<F>(&self, future: F) -> F::Output
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
self.runtime
|
||||
.spawn(future)
|
||||
.await
|
||||
.expect("Tokio error while sending a message")
|
||||
}
|
||||
|
||||
pub fn new(server: &MatrixServer, client: &Client) -> Self {
|
||||
let (tx, rx) = channel(10_000);
|
||||
|
||||
let server_name = server.name();
|
||||
|
||||
let receiver_task = Weechat::spawn(Connection::response_receiver(
|
||||
rx,
|
||||
server.clone_weak(),
|
||||
));
|
||||
|
||||
let runtime = Runtime::new().unwrap();
|
||||
|
||||
runtime.spawn(Connection::sync_loop(
|
||||
client.clone(),
|
||||
tx,
|
||||
server.user_name(),
|
||||
server.password(),
|
||||
server_name.to_string(),
|
||||
server.get_server_path(),
|
||||
));
|
||||
|
||||
Self {
|
||||
client: client.clone(),
|
||||
runtime: runtime.into(),
|
||||
receiver_task: receiver_task.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message to the given room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room which the message should be sent to.
|
||||
///
|
||||
/// * `content` - The content of the message that will be sent to the
|
||||
/// server.
|
||||
///
|
||||
/// * `transaction_id` - Attach an unique id to this message, later on the
|
||||
/// event will contain the same id in the unsigned part of the event.
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
room: Room,
|
||||
content: AnyMessageLikeEventContent,
|
||||
transaction_id: Option<OwnedTransactionId>,
|
||||
) -> MatrixResult<RoomSendResponse> {
|
||||
self.spawn(async move {
|
||||
let mut msg = room.send(content);
|
||||
if let Some(txn_id) = transaction_id.as_deref() {
|
||||
msg = msg.with_transaction_id(txn_id.to_owned());
|
||||
}
|
||||
|
||||
msg.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_devices(
|
||||
&self,
|
||||
devices: Vec<OwnedDeviceId>,
|
||||
auth_info: Option<InteractiveAuthInfo>,
|
||||
) -> MatrixResult<DeleteDevicesResponse> {
|
||||
let client = self.client.clone();
|
||||
Ok(self
|
||||
.spawn(async move {
|
||||
if let Some(info) = auth_info {
|
||||
let auth = Some(info.as_auth_data());
|
||||
client.delete_devices(&devices, auth).await
|
||||
} else {
|
||||
client.delete_devices(&devices, None).await
|
||||
}
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Fetch historical messages for the given room.
|
||||
pub async fn room_messages(
|
||||
&self,
|
||||
room: Room,
|
||||
prev_batch: PrevBatch,
|
||||
) -> MatrixResult<Messages> {
|
||||
self.spawn(async move {
|
||||
let request = match &prev_batch {
|
||||
PrevBatch::Backwards(t) => {
|
||||
MessagesOptions::backward().from(Some(t.as_ref()))
|
||||
}
|
||||
PrevBatch::Forward(t) => {
|
||||
MessagesOptions::forward().from(Some(t.as_ref()))
|
||||
}
|
||||
};
|
||||
|
||||
room.messages(request).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get the list of our own devices.
|
||||
pub async fn devices(&self) -> MatrixResult<DevicesResponse> {
|
||||
let client = self.client.clone();
|
||||
Ok(self.spawn(async move { client.devices().await }).await?)
|
||||
}
|
||||
|
||||
/// Set or reset a typing notice.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room where the typing notice should be
|
||||
/// active.
|
||||
///
|
||||
/// * `typing` - Should we set or unset the typing notice.
|
||||
pub async fn send_typing_notice(
|
||||
&self,
|
||||
room: Room,
|
||||
typing: bool,
|
||||
) -> MatrixResult<()> {
|
||||
self.spawn(async move { room.typing_notice(typing).await })
|
||||
.await
|
||||
}
|
||||
|
||||
fn save_device_id(
|
||||
user_name: &str,
|
||||
mut server_path: PathBuf,
|
||||
response: &LoginResponse,
|
||||
) -> std::io::Result<()> {
|
||||
server_path.push(user_name);
|
||||
server_path.set_extension("device_id");
|
||||
std::fs::write(&server_path, &response.device_id)
|
||||
}
|
||||
|
||||
fn load_device_id(
|
||||
user_name: &str,
|
||||
mut server_path: PathBuf,
|
||||
) -> std::io::Result<Option<String>> {
|
||||
server_path.push(user_name);
|
||||
server_path.set_extension("device_id");
|
||||
|
||||
let device_id = std::fs::read_to_string(server_path);
|
||||
|
||||
if let Err(e) = device_id {
|
||||
// A file not found error is ok, report the rest.
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
return Err(e);
|
||||
}
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let device_id = device_id.unwrap_or_default();
|
||||
|
||||
if device_id.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(device_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Response receiver loop.
|
||||
/// This runs on the main Weechat thread and listens for responses coming
|
||||
/// from the client running in the tokio executor.
|
||||
pub async fn response_receiver(
|
||||
mut receiver: Receiver<Result<ClientMessage, String>>,
|
||||
server: Weak<InnerServer>,
|
||||
) {
|
||||
while let Some(message) = receiver.recv().await {
|
||||
let server = if let Some(s) = server.upgrade() {
|
||||
s
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
match message {
|
||||
Ok(message) => match message {
|
||||
ClientMessage::LoginMessage(r) => server.receive_login(r),
|
||||
ClientMessage::SyncEvent(r, e) => {
|
||||
server.receive_joined_timeline_event(&r, e).await
|
||||
}
|
||||
ClientMessage::SyncState(r, e) => {
|
||||
server.receive_joined_state_event(&r, e).await
|
||||
}
|
||||
ClientMessage::RestoredRoom(room) => {
|
||||
server.restore_room(room).await
|
||||
}
|
||||
ClientMessage::ToDeviceEvent(e) => {
|
||||
server.receive_to_device_event(e).await
|
||||
}
|
||||
ClientMessage::MemberEvent(
|
||||
room_id,
|
||||
e,
|
||||
is_state,
|
||||
change,
|
||||
) => {
|
||||
server
|
||||
.receive_member(room_id, e, is_state, change)
|
||||
.await
|
||||
}
|
||||
},
|
||||
Err(e) => server.print_error(&format!("Ruma error {}", e)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
fn sync_filter() -> FilterDefinition {
|
||||
let mut filter = FilterDefinition::default();
|
||||
let mut room_filter = RoomFilter::default();
|
||||
let mut event_filter = RoomEventFilter::default();
|
||||
|
||||
event_filter.lazy_load_options = LazyLoadOptions::Enabled {
|
||||
include_redundant_members: false,
|
||||
};
|
||||
event_filter.limit = Some(10u16.into());
|
||||
|
||||
room_filter.state = event_filter;
|
||||
filter.room = room_filter;
|
||||
|
||||
filter
|
||||
}
|
||||
|
||||
/// Main client sync loop.
|
||||
/// This runs on the per server tokio executor.
|
||||
/// It communicates with the main Weechat thread using a async channel.
|
||||
pub async fn sync_loop(
|
||||
client: Client,
|
||||
channel: Sender<Result<ClientMessage, String>>,
|
||||
username: String,
|
||||
password: String,
|
||||
server_name: String,
|
||||
server_path: PathBuf,
|
||||
) {
|
||||
if !client.matrix_auth().logged_in() {
|
||||
let device_id =
|
||||
Connection::load_device_id(&username, server_path.clone());
|
||||
|
||||
let device_id = match device_id {
|
||||
Err(e) => {
|
||||
// TODO: do we want to do something with channel.send()
|
||||
// errors?
|
||||
let _ = channel
|
||||
.send(Err(format!(
|
||||
"Error while reading the device id for server {}: {:?}",
|
||||
server_name, e
|
||||
)))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
Ok(d) => d,
|
||||
};
|
||||
|
||||
let first_login = device_id.is_none();
|
||||
|
||||
let mut builder = client
|
||||
.matrix_auth()
|
||||
.login_username(&username, &password)
|
||||
.initial_device_display_name("WeeChat-Matrix-rs");
|
||||
|
||||
if let Some(device_id) = device_id.as_ref() {
|
||||
builder = builder.device_id(device_id);
|
||||
};
|
||||
|
||||
match builder.send().await {
|
||||
Ok(response) => {
|
||||
if let Err(e) = Connection::save_device_id(
|
||||
&username,
|
||||
server_path.clone(),
|
||||
&response,
|
||||
) {
|
||||
let _ = channel
|
||||
.send(Err(format!(
|
||||
"Error while writing the device id for server {}: {:?}",
|
||||
server_name, e
|
||||
))).await;
|
||||
return;
|
||||
}
|
||||
|
||||
if channel
|
||||
.send(Ok(ClientMessage::LoginMessage(response)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = channel
|
||||
.send(Err(format!("Failed to log in: {:?}", e)))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !first_login {
|
||||
for room in client.joined_rooms() {
|
||||
if channel
|
||||
.send(Ok(ClientMessage::RestoredRoom(room)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filter = client
|
||||
.get_or_upload_filter("sync", Connection::sync_filter())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let sync_token: Option<String> = None;
|
||||
let sync_settings = SyncSettings::new()
|
||||
.timeout(DEFAULT_SYNC_TIMEOUT)
|
||||
.filter(Filter::FilterId(filter));
|
||||
|
||||
let sync_settings = if let Some(t) = sync_token {
|
||||
sync_settings.token(t)
|
||||
} else {
|
||||
sync_settings
|
||||
};
|
||||
|
||||
let sync_channel = &channel;
|
||||
|
||||
let client_ref = &client;
|
||||
|
||||
loop {
|
||||
let ret = client
|
||||
.sync_with_callback(sync_settings.clone(), |response| async move {
|
||||
for event in response.to_device.iter().filter_map(|e| e.to_raw().deserialize().ok()){
|
||||
if sync_channel
|
||||
.send(Ok(ClientMessage::ToDeviceEvent(event)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return LoopCtrl::Break;
|
||||
}
|
||||
}
|
||||
|
||||
for (room_id, room) in response.rooms.joined {
|
||||
if let State::Before(state) = room.state {
|
||||
for event in
|
||||
state.iter().filter_map(|e| e.deserialize().ok())
|
||||
{
|
||||
if let AnySyncStateEvent::RoomMember(m) = event {
|
||||
let change = room
|
||||
.ambiguity_changes
|
||||
.get(m.event_id())
|
||||
.cloned();
|
||||
|
||||
if sync_channel
|
||||
.send(Ok(ClientMessage::MemberEvent(
|
||||
room_id.clone(),
|
||||
m,
|
||||
true,
|
||||
change,
|
||||
)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return LoopCtrl::Break;
|
||||
}
|
||||
} else if sync_channel
|
||||
.send(Ok(ClientMessage::SyncState(
|
||||
room_id.clone(),
|
||||
event,
|
||||
)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return LoopCtrl::Break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for event in room
|
||||
.timeline
|
||||
.events
|
||||
.iter()
|
||||
.filter_map(|e| e.raw().deserialize().ok())
|
||||
{
|
||||
if let AnySyncTimelineEvent::State(
|
||||
AnySyncStateEvent::RoomMember(m),
|
||||
) = event
|
||||
{
|
||||
let change = room
|
||||
.ambiguity_changes
|
||||
.get(m.event_id())
|
||||
.cloned();
|
||||
|
||||
if sync_channel
|
||||
.send(Ok(ClientMessage::MemberEvent(
|
||||
room_id.clone(),
|
||||
m,
|
||||
false,
|
||||
change,
|
||||
)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return LoopCtrl::Break;
|
||||
}
|
||||
} else if sync_channel
|
||||
.send(Ok(ClientMessage::SyncEvent(
|
||||
room_id.clone(),
|
||||
event,
|
||||
)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return LoopCtrl::Break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(r) = client_ref.get_room(&room_id) {
|
||||
if !r.are_members_synced() {
|
||||
let room_id = room_id.clone();
|
||||
let channel = sync_channel.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Ok(members) =
|
||||
r.members(RoomMemberships::ACTIVE).await
|
||||
{
|
||||
for member in members {
|
||||
if let Err(e) = channel
|
||||
.send(Ok(
|
||||
ClientMessage::MemberEvent(
|
||||
room_id.clone(),
|
||||
member.event().as_sync().expect("Member event is not a StateSyncEvent").to_owned(),
|
||||
true,
|
||||
None,
|
||||
),
|
||||
))
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to send room member {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoopCtrl::Continue
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Err(err) = ret {
|
||||
error!("Matrix sync failed: {err}");
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/debug.rs
Normal file
60
src/debug.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::{cell::RefMut, io};
|
||||
|
||||
use weechat::{
|
||||
buffer::{BufferBuilder, BufferHandle},
|
||||
Weechat,
|
||||
};
|
||||
|
||||
use crate::Matrix;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Debug();
|
||||
|
||||
impl Debug {
|
||||
fn create_debug_buffer(debug_buffer: &mut RefMut<Option<BufferHandle>>) {
|
||||
let buffer = BufferBuilder::new("Matrix debug")
|
||||
.build()
|
||||
.expect("Can't create Matrix debug buffer");
|
||||
**debug_buffer = Some(buffer);
|
||||
}
|
||||
|
||||
async fn write_helper(message: Vec<u8>) {
|
||||
let matrix = Matrix::get();
|
||||
|
||||
let message = String::from_utf8(message).unwrap();
|
||||
let message =
|
||||
Weechat::execute_modifier("color_decode_ansi", "1", &message)
|
||||
.unwrap();
|
||||
|
||||
let mut debug_buffer = matrix.debug_buffer.borrow_mut();
|
||||
|
||||
if matrix.config.borrow().network().debug_buffer() {
|
||||
let buffer = if let Some(buffer) = debug_buffer.as_ref() {
|
||||
if let Ok(buffer) = buffer.upgrade() {
|
||||
buffer
|
||||
} else {
|
||||
Debug::create_debug_buffer(&mut debug_buffer);
|
||||
debug_buffer.as_ref().unwrap().upgrade().unwrap()
|
||||
}
|
||||
} else {
|
||||
Debug::create_debug_buffer(&mut debug_buffer);
|
||||
debug_buffer.as_ref().unwrap().upgrade().unwrap()
|
||||
};
|
||||
|
||||
buffer.print(&message);
|
||||
} else {
|
||||
Weechat::print(&message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Write for Debug {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
Weechat::spawn_from_thread(Debug::write_helper(buf.to_owned()));
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
315
src/lib.rs
Normal file
315
src/lib.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
mod bar_items;
|
||||
mod commands;
|
||||
mod completions;
|
||||
mod config;
|
||||
mod connection;
|
||||
mod debug;
|
||||
mod render;
|
||||
mod room;
|
||||
mod server;
|
||||
mod utils;
|
||||
mod verification_buffer;
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use tokio::runtime::{Handle, Runtime};
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
|
||||
use weechat::{
|
||||
buffer::{Buffer, BufferHandle},
|
||||
hooks::{SignalCallback, SignalData, SignalHook},
|
||||
plugin, Args, Plugin, ReturnCode, Weechat,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
bar_items::BarItems, commands::Commands, completions::Completions,
|
||||
config::ConfigHandle, room::RoomHandle, server::MatrixServer,
|
||||
verification_buffer::VerificationBuffer,
|
||||
};
|
||||
|
||||
const PLUGIN_NAME: &str = "matrix";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Servers {
|
||||
inner: Rc<RefCell<HashMap<String, MatrixServer>>>,
|
||||
runtime: Handle,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum BufferOwner {
|
||||
Server(MatrixServer),
|
||||
Room(MatrixServer, RoomHandle),
|
||||
Verification(MatrixServer, VerificationBuffer),
|
||||
None,
|
||||
}
|
||||
|
||||
impl BufferOwner {
|
||||
fn into_server(self) -> Option<MatrixServer> {
|
||||
match self {
|
||||
BufferOwner::Server(s) => Some(s),
|
||||
BufferOwner::Room(s, _) => Some(s),
|
||||
BufferOwner::Verification(s, _) => Some(s),
|
||||
BufferOwner::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_room(self) -> Option<RoomHandle> {
|
||||
if let BufferOwner::Room(_, r) = self {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Servers {
|
||||
fn new(handle: tokio::runtime::Handle) -> Self {
|
||||
Servers {
|
||||
inner: Rc::new(RefCell::new(HashMap::new())),
|
||||
runtime: handle,
|
||||
}
|
||||
}
|
||||
|
||||
fn borrow(&self) -> Ref<'_, HashMap<String, MatrixServer>> {
|
||||
self.inner.borrow()
|
||||
}
|
||||
|
||||
pub fn runtime(&self) -> &Handle {
|
||||
&self.runtime
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.borrow().is_empty()
|
||||
}
|
||||
|
||||
pub fn contains(&self, server_name: &str) -> bool {
|
||||
self.inner.borrow().contains_key(server_name)
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.inner.borrow_mut().clear();
|
||||
}
|
||||
|
||||
pub fn insert(&self, server: MatrixServer) {
|
||||
self.inner
|
||||
.borrow_mut()
|
||||
.insert(server.name().to_string(), server);
|
||||
}
|
||||
|
||||
pub fn get(&self, server_name: &str) -> Option<MatrixServer> {
|
||||
self.inner.borrow().get(server_name).cloned()
|
||||
}
|
||||
|
||||
pub fn remove(&self, server_name: &str) -> Option<MatrixServer> {
|
||||
self.inner.borrow_mut().remove(server_name)
|
||||
}
|
||||
|
||||
pub fn buffer_owner(&self, buffer: &Buffer) -> BufferOwner {
|
||||
let servers = self.borrow();
|
||||
|
||||
for server in servers.values() {
|
||||
if let Some(b) = &*server.server_buffer() {
|
||||
if b.upgrade().is_ok_and(|b| &b == buffer) {
|
||||
return BufferOwner::Server(server.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for room in server.rooms() {
|
||||
let buffer_handle = room.buffer_handle();
|
||||
|
||||
if let Ok(b) = buffer_handle.upgrade() {
|
||||
if buffer == &b {
|
||||
return BufferOwner::Room(server.clone(), room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for verification in server.verifications() {
|
||||
if let Ok(b) = verification.buffer().upgrade() {
|
||||
if buffer == &b {
|
||||
return BufferOwner::Verification(
|
||||
server.clone(),
|
||||
verification,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BufferOwner::None
|
||||
}
|
||||
|
||||
/// Find a `MatrixServer` that the given buffer belongs to.
|
||||
///
|
||||
/// Returns None if the buffer doesn't belong to any of our servers of
|
||||
/// rooms.
|
||||
pub fn find_server(&self, buffer: &Buffer) -> Option<MatrixServer> {
|
||||
self.buffer_owner(buffer).into_server()
|
||||
}
|
||||
|
||||
/// Find a `RoomHandle` that the given buffer belongs to.
|
||||
///
|
||||
/// Returns None if the buffer doesn't belong to any of our servers of
|
||||
/// rooms.
|
||||
pub fn find_room(&self, buffer: &Buffer) -> Option<RoomHandle> {
|
||||
self.buffer_owner(buffer).into_room()
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalCallback for Servers {
|
||||
fn callback(
|
||||
&mut self,
|
||||
_: &Weechat,
|
||||
_signal_name: &str,
|
||||
data: Option<SignalData>,
|
||||
) -> ReturnCode {
|
||||
if let Some(SignalData::Buffer(buffer)) = data {
|
||||
if let Some(room) = self.find_room(&buffer) {
|
||||
room.update_typing_notice();
|
||||
}
|
||||
}
|
||||
ReturnCode::Ok
|
||||
}
|
||||
}
|
||||
|
||||
struct Matrix {
|
||||
#[allow(dead_code)]
|
||||
global_runtime: Runtime,
|
||||
servers: Servers,
|
||||
#[allow(dead_code)]
|
||||
commands: Commands,
|
||||
config: ConfigHandle,
|
||||
#[allow(dead_code)]
|
||||
bar_items: BarItems,
|
||||
#[allow(dead_code)]
|
||||
typing_notice_signal: SignalHook,
|
||||
#[allow(dead_code)]
|
||||
completions: Completions,
|
||||
debug_buffer: RefCell<Option<BufferHandle>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Matrix {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut fmt = f.debug_struct("Matrix");
|
||||
fmt.field("servers", &self.servers).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn autoconnect(servers: &HashMap<String, MatrixServer>) {
|
||||
for server in servers.values() {
|
||||
if server.autoconnect() {
|
||||
match server.connect() {
|
||||
Ok(_) => (),
|
||||
Err(e) => Weechat::print(&format!("{:?}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_default_server(servers: Servers, config: &ConfigHandle) {
|
||||
// TODO change this to matrix.org.
|
||||
let server_name = "localhost";
|
||||
|
||||
let mut config_borrow = config.borrow_mut();
|
||||
let mut section = config_borrow
|
||||
.search_section_mut("server")
|
||||
.expect("Can't get server section");
|
||||
|
||||
let server = MatrixServer::new(
|
||||
server_name,
|
||||
config,
|
||||
&mut section,
|
||||
servers.clone(),
|
||||
);
|
||||
servers.insert(server);
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for Matrix {
|
||||
fn init(_: &Weechat, _args: Args) -> Result<Self, ()> {
|
||||
let global_runtime =
|
||||
Runtime::new().expect("Couldn't create a new global runtime");
|
||||
|
||||
let servers = Servers::new(global_runtime.handle().to_owned());
|
||||
let config = ConfigHandle::new(&servers);
|
||||
let commands = Commands::hook_all(&servers, &config)?;
|
||||
|
||||
let bar_items = BarItems::hook_all(servers.clone())?;
|
||||
let completions = Completions::hook_all(servers.clone())?;
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::filter::EnvFilter::from_default_env())
|
||||
.with(tracing_subscriber::fmt::layer().with_writer(debug::Debug));
|
||||
|
||||
let _ = tracing::subscriber::set_global_default(subscriber).map_err(
|
||||
|_err| Weechat::print("Unable to set global default subscriber"),
|
||||
);
|
||||
|
||||
{
|
||||
let config_borrow = config.borrow();
|
||||
if config_borrow.read().is_err() {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
if servers.is_empty() {
|
||||
Matrix::create_default_server(servers.clone(), &config)
|
||||
}
|
||||
|
||||
let typing = SignalHook::new("input_text_changed", servers.clone())
|
||||
.expect("Can't create signal hook for the typing notice cb");
|
||||
|
||||
let plugin = Matrix {
|
||||
global_runtime,
|
||||
servers: servers.clone(),
|
||||
commands,
|
||||
config,
|
||||
bar_items,
|
||||
completions,
|
||||
debug_buffer: RefCell::new(None),
|
||||
typing_notice_signal: typing,
|
||||
};
|
||||
|
||||
Weechat::spawn(async move {
|
||||
let servers = servers.borrow();
|
||||
Matrix::autoconnect(&servers);
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(plugin)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Matrix {
|
||||
fn drop(&mut self) {
|
||||
let servers = self.servers.borrow();
|
||||
|
||||
// Buffer close callbacks get called after this, so disconnect here so
|
||||
// we don't leave all our rooms.
|
||||
//
|
||||
// TODO set a flag on the server as well so we don't even try to leave
|
||||
// the rooms, once leaving the rooms is implemented when the buffer gets
|
||||
// closed.
|
||||
for server in servers.values() {
|
||||
server.disconnect();
|
||||
}
|
||||
|
||||
drop(servers);
|
||||
|
||||
self.servers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
plugin!(
|
||||
Matrix,
|
||||
name: "matrix",
|
||||
author: "Damir Jelić <poljar@termina.org.uk>",
|
||||
description: "Matrix protocol",
|
||||
version: "0.1.0",
|
||||
license: "ISC"
|
||||
);
|
||||
1025
src/render.rs
Normal file
1025
src/render.rs
Normal file
File diff suppressed because it is too large
Load Diff
274
src/room/buffer.rs
Normal file
274
src/room/buffer.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use std::{borrow::Cow, cell::RefCell, rc::Rc};
|
||||
|
||||
use matrix_sdk::{
|
||||
ruma::{EventId, OwnedRoomAliasId, TransactionId, UserId},
|
||||
Room, StoreError,
|
||||
};
|
||||
use tokio::runtime::Handle;
|
||||
use weechat::{
|
||||
buffer::{Buffer, BufferHandle, BufferLine, LineData},
|
||||
Prefix, Weechat,
|
||||
};
|
||||
|
||||
use crate::{render::RenderedEvent, utils::ToTag};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RoomBuffer {
|
||||
room: Room,
|
||||
runtime: Handle,
|
||||
pub(super) inner: Rc<RefCell<Option<BufferHandle>>>,
|
||||
}
|
||||
|
||||
impl RoomBuffer {
|
||||
pub fn new(room: Room, runtime: Handle) -> Self {
|
||||
Self {
|
||||
room,
|
||||
runtime,
|
||||
inner: Rc::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_handle(&self) -> BufferHandle {
|
||||
self.inner
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.expect("Room struct wasn't initialized properly")
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> String {
|
||||
self.inner
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.and_then(|b| b.upgrade().ok().map(|b| b.short_name().to_string()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Replace the local echo of an event with a fully rendered one.
|
||||
pub fn replace_local_echo(
|
||||
&self,
|
||||
transaction_id: &TransactionId,
|
||||
rendered: RenderedEvent,
|
||||
) {
|
||||
if let Ok(buffer) = self.buffer_handle().upgrade() {
|
||||
let uuid_tag = Cow::from(format!("matrix_echo_{}", transaction_id));
|
||||
let line_contains_uuid =
|
||||
|l: &BufferLine| l.tags().contains(&uuid_tag);
|
||||
|
||||
let mut lines = buffer.lines();
|
||||
let mut current_line = lines.rfind(line_contains_uuid);
|
||||
|
||||
// We go in reverse order here since we also use rfind(). We got from
|
||||
// the bottom of the buffer to the top since we're expecting these
|
||||
// lines to be freshly printed and thus at the bottom.
|
||||
let mut line_num = rendered.content.lines.len();
|
||||
|
||||
while let Some(line) = ¤t_line {
|
||||
line_num -= 1;
|
||||
let rendered_line = &rendered.content.lines[line_num];
|
||||
|
||||
line.set_message(&rendered_line.message);
|
||||
current_line = lines.next_back().filter(line_contains_uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_edit(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
sender: &UserId,
|
||||
event: RenderedEvent,
|
||||
) {
|
||||
if let Ok(buffer) = self.buffer_handle().upgrade() {
|
||||
let sender_tag = Cow::from(sender.to_tag());
|
||||
let event_id_tag = Cow::from(event_id.to_tag());
|
||||
|
||||
let lines: Vec<BufferLine> = buffer
|
||||
.lines()
|
||||
.filter(|l| l.tags().contains(&event_id_tag))
|
||||
.collect();
|
||||
|
||||
if lines
|
||||
.get(0)
|
||||
.map(|l| l.tags().contains(&sender_tag))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.replace_event_helper(&buffer, lines, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_event_helper(
|
||||
&self,
|
||||
buffer: &Buffer,
|
||||
lines: Vec<BufferLine<'_>>,
|
||||
event: RenderedEvent,
|
||||
) {
|
||||
use std::cmp::Ordering;
|
||||
let date = lines.get(0).map(|l| l.date()).unwrap_or_default();
|
||||
|
||||
for (line, new) in lines.iter().zip(event.content.lines.iter()) {
|
||||
let data = LineData {
|
||||
// Our prefixes always come with a \t character, but when we
|
||||
// replace stuff we're able to replace the prefix and the
|
||||
// message separately, so trim the whitespace in the prefix.
|
||||
prefix: Some(event.prefix.trim_end()),
|
||||
message: Some(&new.message),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
line.update(data);
|
||||
}
|
||||
|
||||
match lines.len().cmp(&event.content.lines.len()) {
|
||||
Ordering::Greater => {
|
||||
for line in &lines[event.content.lines.len()..] {
|
||||
line.set_message("");
|
||||
}
|
||||
}
|
||||
Ordering::Less => {
|
||||
for line in &event.content.lines[lines.len()..] {
|
||||
let message = format!("{}{}", &event.prefix, &line.message);
|
||||
let tags: Vec<&str> =
|
||||
line.tags.iter().map(|t| t.as_str()).collect();
|
||||
buffer.print_date_tags(date, &tags, &message)
|
||||
}
|
||||
|
||||
self.sort_messages()
|
||||
}
|
||||
Ordering::Equal => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_messages(&self) {
|
||||
struct LineCopy {
|
||||
date: isize,
|
||||
date_printed: isize,
|
||||
tags: Vec<String>,
|
||||
prefix: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl<'a> From<BufferLine<'a>> for LineCopy {
|
||||
fn from(line: BufferLine) -> Self {
|
||||
Self {
|
||||
date: line.date(),
|
||||
date_printed: line.date_printed(),
|
||||
message: line.message().to_string(),
|
||||
prefix: line.prefix().to_string(),
|
||||
tags: line.tags().iter().map(|t| t.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO update the highlight once Weechat starts supporting it.
|
||||
if let Ok(buffer) = self.buffer_handle().upgrade() {
|
||||
let mut lines: Vec<LineCopy> =
|
||||
buffer.lines().map(|l| l.into()).collect();
|
||||
lines.sort_by_key(|l| l.date);
|
||||
|
||||
for (line, new) in buffer.lines().zip(lines.drain(..)) {
|
||||
let tags =
|
||||
new.tags.iter().map(|t| t.as_str()).collect::<Vec<&str>>();
|
||||
let data = LineData {
|
||||
prefix: Some(&new.prefix),
|
||||
message: Some(&new.message),
|
||||
date: Some(new.date),
|
||||
date_printed: Some(new.date_printed),
|
||||
tags: Some(&tags),
|
||||
};
|
||||
line.update(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_topic(&self) {
|
||||
if let Ok(buffer) = self.buffer_handle().upgrade() {
|
||||
buffer.set_title(&self.room.topic().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_alias(&self) {
|
||||
if let Some(alias) = self.alias() {
|
||||
if let Ok(b) = self.buffer_handle().upgrade() {
|
||||
b.set_localvar("alias", alias.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn alias(&self) -> Option<OwnedRoomAliasId> {
|
||||
self.room.canonical_alias()
|
||||
}
|
||||
|
||||
pub fn calculate_buffer_name(&self) -> Result<String, StoreError> {
|
||||
let room = self.room.clone();
|
||||
let room_name = self.runtime.block_on(room.display_name())?.to_string();
|
||||
|
||||
let room_name = if room_name == "#" {
|
||||
"##".to_owned()
|
||||
} else if room_name.starts_with('#')
|
||||
|| self.runtime.block_on(room.is_direct()).unwrap_or(false)
|
||||
{
|
||||
room_name
|
||||
} else {
|
||||
format!("#{}", room_name)
|
||||
};
|
||||
|
||||
Ok(room_name.to_string())
|
||||
}
|
||||
|
||||
pub fn update_buffer_name(&self) {
|
||||
let buffer = self.buffer_handle();
|
||||
|
||||
let buffer = if let Ok(b) = buffer.upgrade() {
|
||||
b
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self.calculate_buffer_name() {
|
||||
Ok(name) => buffer.set_short_name(&name),
|
||||
Err(e) => {
|
||||
Weechat::print(&format!(
|
||||
"{}: Error fetching the room name from the store: {}",
|
||||
Weechat::prefix(Prefix::Error),
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_verification_event(
|
||||
&self,
|
||||
event_id: &EventId,
|
||||
event: RenderedEvent,
|
||||
) {
|
||||
if let Ok(buffer) = self.buffer_handle().upgrade() {
|
||||
let event_id_tag = Cow::from(event_id.to_tag());
|
||||
|
||||
let lines: Vec<BufferLine> = buffer
|
||||
.lines()
|
||||
.filter(|l| l.tags().contains(&event_id_tag))
|
||||
.collect();
|
||||
|
||||
self.replace_event_helper(&buffer, lines, event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_rendered_event(&self, rendered: RenderedEvent) {
|
||||
let buffer = self.buffer_handle();
|
||||
|
||||
if let Ok(buffer) = buffer.upgrade() {
|
||||
for line in rendered.content.lines {
|
||||
let message = format!("{}{}", &rendered.prefix, &line.message);
|
||||
let tags: Vec<&str> =
|
||||
line.tags.iter().map(|t| t.as_str()).collect();
|
||||
buffer.print_date_tags(
|
||||
rendered.message_timestamp,
|
||||
&tags,
|
||||
&message,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
422
src/room/members.rs
Normal file
422
src/room/members.rs
Normal file
@@ -0,0 +1,422 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::{error, info};
|
||||
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::AmbiguityChange,
|
||||
room::{Room, RoomMember},
|
||||
ruma::{
|
||||
events::{
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
power_levels::UserPowerLevel,
|
||||
},
|
||||
SyncStateEvent,
|
||||
},
|
||||
uint, Int, OwnedUserId, UserId,
|
||||
},
|
||||
};
|
||||
|
||||
use weechat::{
|
||||
buffer::{Buffer, NickSettings},
|
||||
Prefix, Weechat,
|
||||
};
|
||||
|
||||
use crate::{render::render_membership, room::buffer::RoomBuffer};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Members {
|
||||
room: Room,
|
||||
pub(super) runtime: Handle,
|
||||
ambiguity_map: Rc<DashMap<OwnedUserId, bool>>,
|
||||
nicks: Rc<DashMap<OwnedUserId, String>>,
|
||||
buffer: RoomBuffer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WeechatRoomMember {
|
||||
inner: RoomMember,
|
||||
color: Rc<String>,
|
||||
ambiguous_nick: Rc<bool>,
|
||||
}
|
||||
|
||||
impl PartialEq for WeechatRoomMember {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.user_id() == other.user_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Members {
|
||||
pub fn new(room: Room, runtime: Handle, buffer: RoomBuffer) -> Self {
|
||||
Self {
|
||||
room,
|
||||
runtime,
|
||||
nicks: DashMap::new().into(),
|
||||
ambiguity_map: DashMap::new().into(),
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_nick(&self, buffer: &Buffer, member: &WeechatRoomMember) {
|
||||
let nick = member.nick();
|
||||
|
||||
let group = buffer
|
||||
.search_nicklist_group(member.nicklist_group_name())
|
||||
.expect("No group found when adding member");
|
||||
|
||||
let nick_settings = NickSettings::new(&nick)
|
||||
.set_color(member.color())
|
||||
.set_prefix(member.nicklist_prefix())
|
||||
.set_prefix_color(member.prefix_color());
|
||||
|
||||
info!("Inserting nick {} for room {}", nick, buffer.short_name());
|
||||
|
||||
if group.add_nick(nick_settings).is_err() {
|
||||
error!(
|
||||
"Error adding nick {} ({}) to room {}, already added.",
|
||||
nick,
|
||||
member.user_id(),
|
||||
buffer.short_name()
|
||||
);
|
||||
};
|
||||
|
||||
self.nicks.insert(member.user_id().to_owned(), nick);
|
||||
}
|
||||
|
||||
pub async fn restore_member(&self, user_id: OwnedUserId) {
|
||||
let room = self.room.clone();
|
||||
let user = user_id.to_owned();
|
||||
|
||||
match self
|
||||
.runtime
|
||||
.spawn(async move { room.get_member_no_sync(&user).await })
|
||||
.await
|
||||
.expect("Fetching the room member from the store panicked")
|
||||
{
|
||||
Ok(Some(member)) => {
|
||||
self.ambiguity_map
|
||||
.insert(user_id.to_owned(), member.name_ambiguous());
|
||||
self.update_member(&user_id).await;
|
||||
}
|
||||
Ok(None) => {
|
||||
panic!(
|
||||
"Couldn't find member {} in {}",
|
||||
user_id,
|
||||
self.buffer.short_name()
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
Weechat::print(&format!(
|
||||
"{}: Error fetching a room member from the store: {}",
|
||||
Weechat::prefix(Prefix::Error),
|
||||
e,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_member(&self, user_id: &UserId) {
|
||||
let buffer = self.buffer.buffer_handle();
|
||||
|
||||
let buffer = if let Ok(b) = buffer.upgrade() {
|
||||
b
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(nick) = self.nicks.get(user_id) {
|
||||
buffer.remove_nick(&nick);
|
||||
}
|
||||
|
||||
let member = self.get(user_id).await.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Couldn't find member {} in {}",
|
||||
user_id,
|
||||
buffer.short_name()
|
||||
)
|
||||
});
|
||||
|
||||
self.add_nick(&buffer, &member);
|
||||
}
|
||||
|
||||
/// Add a new Weechat room member.
|
||||
pub async fn add_or_modify(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
ambiguity_change: Option<&AmbiguityChange>,
|
||||
) {
|
||||
if let Some(change) = ambiguity_change {
|
||||
self.ambiguity_map
|
||||
.insert(user_id.to_owned(), change.member_ambiguous);
|
||||
|
||||
if let Some(disambiguated) = &change.disambiguated_member {
|
||||
self.ambiguity_map.insert(disambiguated.clone(), false);
|
||||
self.update_member(disambiguated).await;
|
||||
}
|
||||
|
||||
if let Some(ambiguated) = &change.ambiguated_member {
|
||||
self.ambiguity_map.insert(ambiguated.clone(), true);
|
||||
self.update_member(ambiguated).await;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_member(user_id).await;
|
||||
}
|
||||
|
||||
/// Remove a Weechat room member by user ID.
|
||||
///
|
||||
/// Returns either the removed Weechat room member, or an error if the
|
||||
/// member does not exist.
|
||||
async fn remove(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
ambiguity_change: Option<&AmbiguityChange>,
|
||||
) {
|
||||
self.ambiguity_map.remove(user_id);
|
||||
|
||||
if let Some(change) = ambiguity_change {
|
||||
if let Some(disambiguated) = &change.disambiguated_member {
|
||||
self.ambiguity_map.insert(disambiguated.clone(), false);
|
||||
self.update_member(disambiguated).await;
|
||||
}
|
||||
|
||||
if let Some(ambiguated) = &change.ambiguated_member {
|
||||
self.ambiguity_map.insert(ambiguated.clone(), true);
|
||||
self.update_member(ambiguated).await;
|
||||
}
|
||||
}
|
||||
|
||||
let buffer = self.buffer.buffer_handle();
|
||||
|
||||
let buffer = if let Ok(b) = buffer.upgrade() {
|
||||
b
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some((_, nick)) = self.nicks.remove(user_id) {
|
||||
buffer.remove_nick(&nick);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a reference to a Weechat room member by user ID.
|
||||
pub async fn get(&self, user_id: &UserId) -> Option<WeechatRoomMember> {
|
||||
let color = if self.room.own_user_id() == user_id {
|
||||
"weechat.color.chat_nick_self".into()
|
||||
} else {
|
||||
Weechat::info_get("nick_color_name", user_id.as_str())
|
||||
.expect("Couldn't get the nick color name")
|
||||
};
|
||||
|
||||
let room = self.room.clone();
|
||||
let user = user_id.to_owned();
|
||||
|
||||
match self
|
||||
.runtime
|
||||
.spawn(async move { room.get_member_no_sync(&user).await })
|
||||
.await
|
||||
.expect("Fetching the room member from the store panicked")
|
||||
{
|
||||
Ok(m) => m.map(|m| WeechatRoomMember {
|
||||
color: Rc::new(color),
|
||||
ambiguous_nick: Rc::new(
|
||||
self.ambiguity_map
|
||||
.get(m.user_id())
|
||||
.map(|a| *a)
|
||||
.unwrap_or(false),
|
||||
),
|
||||
inner: m,
|
||||
}),
|
||||
Err(e) => {
|
||||
Weechat::print(&format!(
|
||||
"{}: Error fetching a room member from the store: {}",
|
||||
Weechat::prefix(Prefix::Error),
|
||||
e,
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn room(&self) -> &Room {
|
||||
&self.room
|
||||
}
|
||||
|
||||
pub async fn handle_membership_event(
|
||||
&self,
|
||||
event: &SyncStateEvent<RoomMemberEventContent>,
|
||||
state_event: bool,
|
||||
ambiguity_change: Option<&AmbiguityChange>,
|
||||
) {
|
||||
let buffer = self.buffer.buffer_handle();
|
||||
let buffer = if let Ok(b) = buffer.upgrade() {
|
||||
b
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let event = match event {
|
||||
SyncStateEvent::Original(e) => e,
|
||||
SyncStateEvent::Redacted(e) => {
|
||||
error!("Unhandled redacted event: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Handling membership event for room {} {} {:?}",
|
||||
buffer.short_name(),
|
||||
event.state_key,
|
||||
event.content.membership
|
||||
);
|
||||
|
||||
let sender_id = event.sender.clone();
|
||||
|
||||
let target_id = if let Ok(t) = UserId::parse(event.state_key.clone()) {
|
||||
t
|
||||
} else {
|
||||
error!(
|
||||
"Invalid state key in room {} from sender {}: {}",
|
||||
buffer.short_name(),
|
||||
event.sender,
|
||||
event.state_key,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
use MembershipState::*;
|
||||
|
||||
// For joins and invites, first we need to check whether a member
|
||||
// with some MXID exists. If he does, we have to update *that*
|
||||
// member with the new state. Only if they do not exist yet do we
|
||||
// create a new one.
|
||||
//
|
||||
// For leaves and bans we just need to remove the member.
|
||||
match event.content.membership {
|
||||
Invite | Join => {
|
||||
self.add_or_modify(&target_id, ambiguity_change).await
|
||||
}
|
||||
Leave | Ban => self.remove(&target_id, ambiguity_change).await,
|
||||
_ => (),
|
||||
};
|
||||
|
||||
// Names of rooms without display names can get affected by the
|
||||
// member list so we need to update them.
|
||||
self.buffer.update_buffer_name();
|
||||
|
||||
if !state_event {
|
||||
let sender = self.get(&sender_id).await;
|
||||
let target = self.get(&target_id).await;
|
||||
|
||||
// Display the event message
|
||||
let message = match (&sender, &target) {
|
||||
(Some(sender), Some(target)) => {
|
||||
render_membership(event, sender, target)
|
||||
}
|
||||
|
||||
_ => {
|
||||
if sender.is_none() {
|
||||
error!(
|
||||
"Cannot render event since event sender {} is not a room member",
|
||||
sender_id);
|
||||
}
|
||||
|
||||
if target.is_none() {
|
||||
error!(
|
||||
"Cannot render event since event target {} is not a room member",
|
||||
target_id);
|
||||
}
|
||||
|
||||
"ERROR: cannot render event since sender or target are not a room member".into()
|
||||
}
|
||||
};
|
||||
|
||||
let timestamp: i64 =
|
||||
(event.origin_server_ts.0 / uint!(1000)).into();
|
||||
buffer.print_date_tags(timestamp as isize, &[], &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WeechatRoomMember {
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
self.inner.user_id()
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Option<&str> {
|
||||
self.inner.display_name()
|
||||
}
|
||||
|
||||
pub fn color(&self) -> &str {
|
||||
&self.color
|
||||
}
|
||||
|
||||
fn nick_raw(&self) -> &str {
|
||||
self.inner.name()
|
||||
}
|
||||
|
||||
fn nicklist_group_name(&self) -> &str {
|
||||
match self.inner.normalized_power_level() {
|
||||
UserPowerLevel::Infinite => "000|o",
|
||||
p if p >= Int::from(100) => "000|o",
|
||||
p if p >= Int::from(50) => "001|h",
|
||||
p if p > Int::from(0) => "002|v",
|
||||
_ => "999|...",
|
||||
}
|
||||
}
|
||||
|
||||
fn nicklist_prefix(&self) -> &str {
|
||||
match self.inner.normalized_power_level() {
|
||||
UserPowerLevel::Infinite => "&",
|
||||
p if p >= Int::from(100) => "&",
|
||||
p if p >= Int::from(50) => "@",
|
||||
p if p > Int::from(0) => "+",
|
||||
_ => " ",
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix(&self) -> &str {
|
||||
self.nicklist_prefix().trim()
|
||||
}
|
||||
|
||||
fn prefix_color(&self) -> &str {
|
||||
match self.prefix() {
|
||||
"&" => "lightgreen",
|
||||
"@" => "lightmagenta",
|
||||
"+" => "yellow",
|
||||
_ => "default",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nick_colored(&self) -> String {
|
||||
if *self.ambiguous_nick {
|
||||
// TODO: this should color the parenthesis differently.
|
||||
format!(
|
||||
"{}{}{} ({})",
|
||||
Weechat::color(self.color()),
|
||||
self.nick_raw(),
|
||||
Weechat::color("reset"),
|
||||
self.user_id(),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}{}{}",
|
||||
Weechat::color(self.prefix_color()),
|
||||
self.prefix(),
|
||||
Weechat::color(self.color()),
|
||||
self.nick_raw(),
|
||||
Weechat::color("reset")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nick(&self) -> String {
|
||||
if *self.ambiguous_nick {
|
||||
format!("{} ({})", self.nick_raw(), self.user_id())
|
||||
} else {
|
||||
self.nick_raw().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
1039
src/room/mod.rs
Normal file
1039
src/room/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
226
src/room/verification.rs
Normal file
226
src/room/verification.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::verification::{SasVerification, VerificationRequest},
|
||||
ruma::{
|
||||
events::{
|
||||
key::verification::VerificationMethod, room::message::MessageType,
|
||||
AnySyncMessageLikeEvent,
|
||||
},
|
||||
UserId,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
connection::Connection,
|
||||
render::{Render, StartVerificationContext, VerificationContext},
|
||||
};
|
||||
|
||||
use super::{buffer::RoomBuffer, members::Members};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Verification {
|
||||
own_user_id: Rc<UserId>,
|
||||
connection: Rc<RefCell<Option<Connection>>>,
|
||||
members: Members,
|
||||
buffer: RoomBuffer,
|
||||
inner: Rc<RefCell<Option<ActiveVerification>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum ActiveVerification {
|
||||
Request(VerificationRequest),
|
||||
Sas(SasVerification),
|
||||
}
|
||||
|
||||
impl From<VerificationRequest> for ActiveVerification {
|
||||
fn from(v: VerificationRequest) -> Self {
|
||||
Self::Request(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SasVerification> for ActiveVerification {
|
||||
fn from(v: SasVerification) -> Self {
|
||||
Self::Sas(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl Verification {
|
||||
pub fn new(
|
||||
own_user_id: Rc<UserId>,
|
||||
connection: Rc<RefCell<Option<Connection>>>,
|
||||
members: Members,
|
||||
buffer: RoomBuffer,
|
||||
) -> Self {
|
||||
Self {
|
||||
own_user_id,
|
||||
connection,
|
||||
members,
|
||||
buffer,
|
||||
inner: Rc::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn confirm(&self) {
|
||||
let connection = self.connection.borrow().clone();
|
||||
|
||||
if let Some(c) = connection {
|
||||
if let Some(ActiveVerification::Sas(verification)) =
|
||||
self.inner.borrow().clone()
|
||||
{
|
||||
let ret =
|
||||
c.spawn(async move { verification.confirm().await }).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept(&self) {
|
||||
let connection = self.connection.borrow().clone();
|
||||
let verification = self.inner.borrow().clone();
|
||||
|
||||
if let Some(c) = connection {
|
||||
if let Some(ActiveVerification::Request(verification)) =
|
||||
verification
|
||||
{
|
||||
let verification_clone = verification.clone();
|
||||
|
||||
let ret = c
|
||||
.spawn(async move {
|
||||
verification
|
||||
.accept_with_methods(vec![
|
||||
VerificationMethod::SasV1,
|
||||
])
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
// We automatically start SAS verification here since it's the
|
||||
// only method we support.
|
||||
if let Some(sas) = c
|
||||
.spawn(async move { verification_clone.start_sas().await })
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
*self.inner.borrow_mut() = Some(sas.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_room_verification(
|
||||
&self,
|
||||
event: &AnySyncMessageLikeEvent,
|
||||
) {
|
||||
// TODO remove this expect.
|
||||
let sender =
|
||||
self.members.get(event.sender()).await.expect(
|
||||
"Rendering a message but the sender isn't in the nicklist",
|
||||
);
|
||||
let own_member = self
|
||||
.members
|
||||
.get(&self.own_user_id)
|
||||
.await
|
||||
.expect("Own member missing from the store");
|
||||
let send_time = event.origin_server_ts();
|
||||
let connection = self.connection.borrow().clone();
|
||||
|
||||
match event {
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_) => {}
|
||||
AnySyncMessageLikeEvent::KeyVerificationStart(e) => {
|
||||
if let Some(connection) = connection {
|
||||
let Some(e) = e.as_original() else {
|
||||
// Unhandled redacted event
|
||||
return;
|
||||
};
|
||||
let flow_id = &e.content.relates_to.event_id;
|
||||
|
||||
if let Some(sas) = connection
|
||||
.client()
|
||||
.encryption()
|
||||
.get_verification(&e.sender, flow_id.as_str())
|
||||
.await
|
||||
.map(|s| s.sas())
|
||||
.flatten()
|
||||
{
|
||||
let context = StartVerificationContext::Room(
|
||||
e.sender.to_owned(),
|
||||
sas.clone().into(),
|
||||
);
|
||||
let rendered = e.content.render_with_prefix(
|
||||
send_time,
|
||||
event.event_id(),
|
||||
&sender,
|
||||
&context,
|
||||
);
|
||||
self.buffer
|
||||
.replace_verification_event(flow_id, rendered);
|
||||
*self.inner.borrow_mut() = Some(sas.clone().into());
|
||||
|
||||
// We accept here automatically since the only method
|
||||
// we're supporting is SAS verification
|
||||
let ret = connection
|
||||
.spawn(async move { sas.accept().await })
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::KeyVerificationCancel(_) => {
|
||||
self.inner.borrow_mut().take();
|
||||
}
|
||||
AnySyncMessageLikeEvent::KeyVerificationAccept(_) => {}
|
||||
AnySyncMessageLikeEvent::KeyVerificationKey(e) => {
|
||||
let Some(e) = e.as_original() else {
|
||||
// Unhandled redacted event
|
||||
return;
|
||||
};
|
||||
let flow_id = &e.content.relates_to.event_id;
|
||||
if let Some(ActiveVerification::Sas(sas)) =
|
||||
self.inner.borrow().clone()
|
||||
{
|
||||
if sas.can_be_presented() {
|
||||
let rendered = e.content.render_with_prefix(
|
||||
send_time,
|
||||
event.event_id(),
|
||||
&sender,
|
||||
&sas,
|
||||
);
|
||||
self.buffer
|
||||
.replace_verification_event(flow_id, rendered);
|
||||
}
|
||||
}
|
||||
}
|
||||
AnySyncMessageLikeEvent::KeyVerificationMac(_) => {}
|
||||
AnySyncMessageLikeEvent::KeyVerificationDone(_) => {}
|
||||
AnySyncMessageLikeEvent::RoomMessage(e) => {
|
||||
let Some(e) = e.as_original() else {
|
||||
// Unhandled redacted event
|
||||
return;
|
||||
};
|
||||
if let MessageType::VerificationRequest(content) =
|
||||
&e.content.msgtype
|
||||
{
|
||||
let rendered = content.render_with_prefix(
|
||||
send_time,
|
||||
&e.event_id,
|
||||
&sender.clone(),
|
||||
&VerificationContext::Room(sender, own_member),
|
||||
);
|
||||
self.buffer.print_rendered_event(rendered);
|
||||
|
||||
if let Some(connection) = connection {
|
||||
if let Some(verification) = connection
|
||||
.client()
|
||||
.encryption()
|
||||
.get_verification_request(&e.sender, &e.event_id)
|
||||
.await
|
||||
{
|
||||
*self.inner.borrow_mut() =
|
||||
Some(verification.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
1368
src/server.rs
Normal file
1368
src/server.rs
Normal file
File diff suppressed because it is too large
Load Diff
122
src/utils.rs
Normal file
122
src/utils.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use matrix_sdk::ruma::{
|
||||
events::{
|
||||
room::message::{
|
||||
MessageType, Relation, RoomMessageEventContent,
|
||||
RoomMessageEventContentWithoutRelation,
|
||||
},
|
||||
AnyMessageLikeEvent, AnySyncMessageLikeEvent, SyncMessageLikeEvent,
|
||||
},
|
||||
EventId, UserId,
|
||||
};
|
||||
|
||||
pub trait ToTag {
|
||||
fn to_tag(&self) -> String;
|
||||
}
|
||||
|
||||
impl ToTag for EventId {
|
||||
fn to_tag(&self) -> String {
|
||||
format!("matrix_id_{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTag for UserId {
|
||||
fn to_tag(&self) -> String {
|
||||
format!("matrix_sender_{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Edit {
|
||||
fn is_edit(&self) -> bool;
|
||||
fn get_edit(
|
||||
&self,
|
||||
) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)>;
|
||||
}
|
||||
|
||||
pub trait VerificationEvent {
|
||||
fn is_verification(&self) -> bool;
|
||||
}
|
||||
|
||||
impl VerificationEvent for AnySyncMessageLikeEvent {
|
||||
fn is_verification(&self) -> bool {
|
||||
match self {
|
||||
AnySyncMessageLikeEvent::KeyVerificationReady(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationKey(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
|
||||
| AnySyncMessageLikeEvent::KeyVerificationDone(_) => true,
|
||||
AnySyncMessageLikeEvent::RoomMessage(m) => {
|
||||
if let SyncMessageLikeEvent::Original(m) = m {
|
||||
if let MessageType::VerificationRequest(_) =
|
||||
m.content.msgtype
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Edit for RoomMessageEventContent {
|
||||
fn is_edit(&self) -> bool {
|
||||
matches!(self.relates_to.as_ref(), Some(Relation::Replacement(_)))
|
||||
}
|
||||
|
||||
fn get_edit(
|
||||
&self,
|
||||
) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)> {
|
||||
if let Some(Relation::Replacement(r)) = self.relates_to.as_ref() {
|
||||
Some((&r.event_id, &r.new_content))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Edit for AnySyncMessageLikeEvent {
|
||||
fn is_edit(&self) -> bool {
|
||||
if let AnySyncMessageLikeEvent::RoomMessage(e) = self {
|
||||
e.as_original()
|
||||
.map(|e| e.content.is_edit())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn get_edit(
|
||||
&self,
|
||||
) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)> {
|
||||
if let AnySyncMessageLikeEvent::RoomMessage(e) = self {
|
||||
e.as_original().and_then(|e| e.content.get_edit())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Edit for AnyMessageLikeEvent {
|
||||
fn is_edit(&self) -> bool {
|
||||
if let AnyMessageLikeEvent::RoomMessage(c) = self {
|
||||
c.as_original()
|
||||
.map(|e| e.content.is_edit())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn get_edit(
|
||||
&self,
|
||||
) -> Option<(&EventId, &RoomMessageEventContentWithoutRelation)> {
|
||||
if let AnyMessageLikeEvent::RoomMessage(e) = self {
|
||||
e.as_original().and_then(|e| e.content.get_edit())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
370
src/verification_buffer.rs
Normal file
370
src/verification_buffer.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
use std::{cell::RefCell, convert::TryInto, rc::Rc};
|
||||
|
||||
use weechat::{
|
||||
buffer::{
|
||||
Buffer, BufferBuilderAsync, BufferCloseCallback, BufferHandle,
|
||||
BufferInputCallbackAsync,
|
||||
},
|
||||
Weechat,
|
||||
};
|
||||
|
||||
use qrcode::render::unicode::Dense1x2;
|
||||
|
||||
use matrix_sdk::{
|
||||
async_trait,
|
||||
encryption::verification::{
|
||||
QrVerification, SasVerification, Verification as SdkVerification,
|
||||
VerificationRequest,
|
||||
},
|
||||
ruma::{
|
||||
events::{
|
||||
key::verification::{
|
||||
key::ToDeviceKeyVerificationKeyEventContent, VerificationMethod,
|
||||
},
|
||||
AnyToDeviceEvent,
|
||||
},
|
||||
UserId,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
connection::Connection,
|
||||
render::{
|
||||
Render, RenderedContent, StartVerificationContext, VerificationContext,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VerificationBuffer {
|
||||
inner: InnerVerificationBuffer,
|
||||
buffer: BufferHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Verification {
|
||||
Request(VerificationRequest),
|
||||
Sas(SasVerification),
|
||||
Qr(QrVerification),
|
||||
}
|
||||
|
||||
impl TryInto<SdkVerification> for Verification {
|
||||
type Error = ();
|
||||
|
||||
fn try_into(self) -> Result<SdkVerification, Self::Error> {
|
||||
match self {
|
||||
Verification::Request(_) => Err(()),
|
||||
Verification::Sas(s) => Ok(s.into()),
|
||||
Verification::Qr(qr) => Ok(qr.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SasVerification> for Verification {
|
||||
fn from(s: SasVerification) -> Self {
|
||||
Self::Sas(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VerificationRequest> for Verification {
|
||||
fn from(v: VerificationRequest) -> Self {
|
||||
Self::Request(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<QrVerification> for Verification {
|
||||
fn from(v: QrVerification) -> Self {
|
||||
Self::Qr(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl Verification {
|
||||
async fn accept(&self) -> Result<(), Error> {
|
||||
match self {
|
||||
Verification::Request(r) => r.accept().await,
|
||||
Verification::Sas(s) => s.accept().await,
|
||||
Verification::Qr(qr) => qr.confirm().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_qr_code(&self) -> Option<QrVerification> {
|
||||
match self {
|
||||
Verification::Request(r) => r.generate_qr_code().await.unwrap(),
|
||||
Verification::Sas(_) => None,
|
||||
Verification::Qr(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn cancel(&self) -> Result<(), Error> {
|
||||
match self {
|
||||
Verification::Request(r) => r.cancel().await,
|
||||
Verification::Sas(s) => s.cancel().await,
|
||||
Verification::Qr(qr) => qr.cancel().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct InnerVerificationBuffer {
|
||||
verification: Rc<RefCell<Verification>>,
|
||||
connection: Rc<RefCell<Option<Connection>>>,
|
||||
}
|
||||
|
||||
impl InnerVerificationBuffer {
|
||||
fn print_done(&self, buffer: BufferHandle) {}
|
||||
|
||||
pub async fn accept(&self, buffer: BufferHandle) -> Result<(), Error> {
|
||||
if let Some(c) = self.connection.borrow().clone() {
|
||||
let verification = self.verification.borrow().clone();
|
||||
let verification_clone = verification.clone();
|
||||
|
||||
c.spawn(async move { verification_clone.accept().await })
|
||||
.await?;
|
||||
|
||||
if let Verification::Request(request) = verification {
|
||||
if request
|
||||
.their_supported_methods()
|
||||
.unwrap_or_default()
|
||||
.contains(&VerificationMethod::QrCodeShowV1)
|
||||
{
|
||||
if let Some(qr_code) = request.generate_qr_code().await? {
|
||||
if let Ok(code) = qr_code.to_qr_code() {
|
||||
if let Ok(b) = buffer.upgrade() {
|
||||
let string = code
|
||||
.render::<Dense1x2>()
|
||||
.light_color(Dense1x2::Dark)
|
||||
.dark_color(Dense1x2::Light)
|
||||
.build();
|
||||
b.print(&string);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(sas) =
|
||||
c.spawn(async move { request.start_sas().await }).await?
|
||||
{
|
||||
*self.verification.borrow_mut() = sas.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn confirm(&self, buffer: BufferHandle) -> Result<(), Error> {
|
||||
if let Some(c) = self.connection.borrow().clone() {
|
||||
if let Verification::Sas(s) = self.verification.borrow().clone() {
|
||||
let sas = s.clone();
|
||||
c.spawn(async move { s.confirm().await }).await?;
|
||||
|
||||
if sas.is_done() {
|
||||
self.print_done(buffer);
|
||||
}
|
||||
} else if let Verification::Qr(qr) =
|
||||
self.verification.borrow().clone()
|
||||
{
|
||||
c.spawn(async move { qr.confirm().await }).await?;
|
||||
|
||||
// if qr.is_done() {
|
||||
// self.print_done(buffer);
|
||||
// }
|
||||
} else if let Ok(b) = buffer.upgrade() {
|
||||
b.print("Error, can't confirm the verification yet");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cancel(&self) -> Result<(), Error> {
|
||||
if let Some(c) = self.connection.borrow().clone() {
|
||||
let verification = self.verification.borrow().clone();
|
||||
c.spawn(async move { verification.cancel().await }).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_input(
|
||||
&mut self,
|
||||
buffer: BufferHandle,
|
||||
input: &str,
|
||||
) -> Result<(), Error> {
|
||||
if input == "accept" {
|
||||
self.accept(buffer).await?;
|
||||
} else if input == "confirm" {
|
||||
self.confirm(buffer).await?;
|
||||
} else if input == "cancel" {
|
||||
self.cancel().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl BufferInputCallbackAsync for InnerVerificationBuffer {
|
||||
async fn callback(&mut self, buffer: BufferHandle, input: String) {
|
||||
if let Err(e) = self.handle_input(buffer.clone(), &input).await {
|
||||
if let Ok(buffer) = buffer.upgrade() {
|
||||
buffer.print(&format!(
|
||||
"Error with the verification flow {:?}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferCloseCallback for InnerVerificationBuffer {
|
||||
fn callback(&mut self, _: &Weechat, _: &Buffer) -> Result<(), ()> {
|
||||
let inner = self.clone();
|
||||
Weechat::spawn(async move { inner.cancel().await }).detach();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl VerificationBuffer {
|
||||
pub fn new(
|
||||
server_name: &str,
|
||||
sender: &UserId,
|
||||
verification: impl Into<Verification>,
|
||||
connection: Rc<RefCell<Option<Connection>>>,
|
||||
) -> Self {
|
||||
let inner = InnerVerificationBuffer {
|
||||
verification: Rc::new(RefCell::new(verification.into())),
|
||||
connection,
|
||||
};
|
||||
|
||||
let buffer_name = format!("{}.verification", server_name);
|
||||
|
||||
let buffer_handle = BufferBuilderAsync::new(&buffer_name)
|
||||
.input_callback(inner.clone())
|
||||
.close_callback(|_weechat: &Weechat, _buffer: &Buffer| {
|
||||
// TODO remove the roombuffer from the server here.
|
||||
// TODO leave the room if the plugin isn't unloading.
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
.expect("Can't create new room buffer");
|
||||
|
||||
let buffer = buffer_handle
|
||||
.upgrade()
|
||||
.expect("Can't upgrade newly created buffer");
|
||||
|
||||
buffer.disable_nicklist();
|
||||
buffer.disable_nicklist_groups();
|
||||
buffer.enable_multiline();
|
||||
|
||||
buffer.set_localvar("server", server_name);
|
||||
|
||||
Self {
|
||||
inner,
|
||||
buffer: buffer_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> BufferHandle {
|
||||
self.buffer.clone()
|
||||
}
|
||||
|
||||
pub fn accept(&self) {
|
||||
let buffer = self.buffer();
|
||||
let inner = self.inner.clone();
|
||||
Weechat::spawn(async move { inner.accept(buffer).await }).detach();
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
let inner = self.inner.clone();
|
||||
Weechat::spawn(async move { inner.cancel().await }).detach();
|
||||
}
|
||||
|
||||
pub fn confirm(&self) {
|
||||
let buffer = self.buffer();
|
||||
let inner = self.inner.clone();
|
||||
Weechat::spawn(async move { inner.confirm(buffer).await }).detach();
|
||||
}
|
||||
|
||||
pub async fn update_qr(&mut self, qr: QrVerification) {
|
||||
*self.inner.verification.borrow_mut() = qr.into();
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, sas: SasVerification) -> Result<(), Error> {
|
||||
*self.inner.verification.borrow_mut() = sas.into();
|
||||
let verification = self.inner.verification.borrow().clone();
|
||||
|
||||
if let Some(c) = self.inner.connection.borrow().clone() {
|
||||
c.spawn(async move { verification.accept().await }).await?;
|
||||
} else {
|
||||
// TODO print an error
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_event(&self, event: &AnyToDeviceEvent) {
|
||||
match event {
|
||||
AnyToDeviceEvent::KeyVerificationRequest(e) => {
|
||||
if let Verification::Request(request) =
|
||||
self.inner.verification.borrow().clone()
|
||||
{
|
||||
let content = e
|
||||
.content
|
||||
.render(&VerificationContext::ToDevice(request));
|
||||
|
||||
self.print(&content);
|
||||
}
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationStart(e) => {
|
||||
let verification = self.inner.verification.borrow().clone();
|
||||
|
||||
if let Ok(verification) = verification.try_into() {
|
||||
let content =
|
||||
e.content.render(&StartVerificationContext::ToDevice(
|
||||
e.sender.clone(),
|
||||
verification,
|
||||
));
|
||||
|
||||
self.print(&content);
|
||||
}
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationCancel(_) => {
|
||||
// let message =
|
||||
// format!("The verification flow has been canceled");
|
||||
// self.print(&message);
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationKey(e) => {
|
||||
self.print_sas(&e.content);
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationMac(_) => {
|
||||
if let Verification::Sas(sas) =
|
||||
self.inner.verification.borrow().clone()
|
||||
{
|
||||
if sas.is_done() {
|
||||
self.inner.print_done(self.buffer.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationDone(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn print(&self, message: &RenderedContent) {
|
||||
if let Ok(buffer) = self.buffer.upgrade() {
|
||||
for line in &message.lines {
|
||||
buffer.print_date_tags(0, &[], &line.message);
|
||||
}
|
||||
} else {
|
||||
Weechat::print("BUFFER CLOSED");
|
||||
}
|
||||
}
|
||||
|
||||
fn print_sas(&self, content: &ToDeviceKeyVerificationKeyEventContent) {
|
||||
if let Verification::Sas(sas) = self.inner.verification.borrow().clone()
|
||||
{
|
||||
let message = content.render(&sas);
|
||||
self.print(&message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user