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

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
**/*.rs.bk

1
.rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
max_width = 80

29
.travis.yml Normal file
View File

@@ -0,0 +1,29 @@
language: rust
jobs:
allow_failures:
- os: osx
name: macOS 10.15
include:
- stage: Lint
os: linux
before_script:
- rustup component add rustfmt
- rustup component add clippy
script:
- cargo fmt --all -- --check
- cargo clippy -- -D warnings
- stage: Build
os: linux
- os: osx
- os: osx
name: macOS 10.15
osx_image: xcode12
script:
- cargo build

4916
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

44
Cargo.toml Normal file
View File

@@ -0,0 +1,44 @@
[package]
name = "weechat-matrix"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2018"
license = "ISC"
resolver = "2"
[lib]
name = "matrix"
crate-type = ["cdylib"]
[features]
default = []
[dependencies]
clap = "2.34.0"
chrono = "0.4.22"
dashmap = "6.1.0"
url = "2.3.1"
serde_json = "1.0.85"
strum = { version = "0.24.0", features = ["derive"] }
tokio = { version = "1.21.1", features = ["rt-multi-thread", "sync"] }
tracing = "0.1.36"
tracing-subscriber = { version = "0.3.15", features = ["env-filter"] }
uuid = { version = "1.1.2", features = ["v4"] }
unicode-segmentation = "1.10.0"
# Auto-select version from matrix-sdk
qrcode = { version = "*", default-features = false }
image = { version = "*", default-features = false, features = ["jpeg"] }
[dependencies.weechat]
git = "https://github.com/poljar/rust-weechat"
features = ["async", "config_macro"]
[dependencies.matrix-sdk]
version = "0.14.0"
# git = "https://github.com/matrix-org/matrix-rust-sdk.git"
# rev = "92192c549b3f53889c23954386743867a73b31b1"
features = ["markdown", "socks", "qrcode"]
[profile.dev.package]
sha2 = { opt-level = 2 }

14
LICENSE Normal file
View File

@@ -0,0 +1,14 @@
Weechat Matrix Protocol Plugin
Copyright © 2020 Damir Jelić <poljar@termina.org.uk>
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

29
Makefile Normal file
View File

@@ -0,0 +1,29 @@
# See https://weechat.org/files/doc/weechat/stable/weechat_user.en.html#xdg_directories
XDG_DATA_HOME ?= $(HOME)/.local/share
WEECHAT_DATA_DIR ?= $(XDG_DATA_HOME)/weechat
SOURCES := $(wildcard src/*.rs src/bar_items/*.rs src/commands/*.rs src/room/*.rs Cargo.lock)
PROFILE ?= release
.PHONY: install install-dir lint all help
all: help
help: ## Print this help message
@grep -E '^[a-zA-Z._-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
target/debug/libmatrix.so: $(SOURCES) ## Build plugin in dev profile
cargo build
target/release/libmatrix.so: $(SOURCES) ## Build plugin release profile
cargo build --release
install: install-dir target/$(PROFILE)/libmatrix.so ## Install plugin to weechat dir
install -m644 target/$(PROFILE)/libmatrix.so $(DESTDIR)$(WEECHAT_DATA_DIR)/plugins/matrix.so
install-dir: ## Create plugins directory
install -d $(DESTDIR)$(WEECHAT_DATA_DIR)/plugins
lint: ## Lint issues with clippy
cargo clippy

71
README.md Normal file
View File

@@ -0,0 +1,71 @@
[![build-test-release](https://github.com/poljar/weechat-matrix-rs/actions/workflows/release.yml/badge.svg?event=push)](https://github.com/poljar/weechat-matrix-rs/actions/workflows/release.yml)
[![#weechat-matrix](https://img.shields.io/badge/matrix-%23weechat--matrix:termina.org.uk-blue.svg?style=flat-square)](https://matrix.to/#/!twcBhHVdZlQWuuxBhN:termina.org.uk?via=termina.org.uk&via=matrix.org)
[![license](https://img.shields.io/badge/license-ISC-blue.svg?style=flat-square)](https://github.com/poljar/weechat-matrix-rs/blob/master/LICENSE)
# What is weechat-matrix?
[Weechat](https://weechat.org/) is an extensible chat client.
[Matrix](https://matrix.org/blog/home) is an open network for secure,
decentralized communication.
weechat-matrix-rs is a Rust plugin for Weechat that lets Weechat communicate
over the Matrix protocol. This is a Rust rewrite of the
[weechat-matrix](https://github.com/poljar/weechat-matrix) Python script.
# Project status
This project is a work in progress and doesn't do much yet. It can connect
to a Matrix server and send messages.
If you are interested in helping out take a look at the issue tracker.
# Build
After Rust is installed the plugin can be compiled with:
cargo build --release
If you are developing on weechat-matrix-rs, use debug builds which are faster at the expense of plugin performance:
cargo build
On Linux this creates a `libmatrix.so` file in the `target/release/` (`target/debug` for dev builds) folder, this
file needs to be renamed to `matrix.so` and copied to your Weechat plugin
directory. A plugin directory can be created in your `$WEECHAT_HOME` folder, by
default `.weechat/plugins/`.
Alternatively, `make install` (`make install PROFILE=debug` for dev build) will build and install the plugin in your
`$WEECHAT_HOME` as well.
# Configuration
Configuration is completed primarily through the Weechat interface. First start Weechat, and then issue the following commands _(replace the placeholders in brackets [] with your own details)_:
1. Add a server _(make sure the url includes the scheme e.g. 'https://matrix.org')_:
/matrix server add [server-name] [server-url]
2. Set your username and password:
/set matrix-rust.server.[server-name].username [username]
/set matrix-rust.server.[server-name].password [password]
3. Now try to connect:
/matrix connect [server-name]
4. Automatically connect to the server:
/set matrix-rust.server.[server-name].autoconnect on
5. If everything works, save the configuration:
/save
# Helpful Commands
`/help matrix` will print information about the `/matrix` command.
`/matrix help [command]` will print information for subcommands, such as `/matrix help server`

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);
}
}
}