first commit

This commit is contained in:
2026-04-04 01:33:50 +03:00
commit 05e2355615
33 changed files with 12383 additions and 0 deletions

View 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())
}
}
}
}

View 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
View 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
View 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("")
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}
}

View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

274
src/room/buffer.rs Normal file
View 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) = &current_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
View 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

File diff suppressed because it is too large Load Diff

226
src/room/verification.rs Normal file
View 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

File diff suppressed because it is too large Load Diff

122
src/utils.rs Normal file
View 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
View 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);
}
}
}