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