Update to matrix-sdk@0.7.1 (#200)

This commit is contained in:
Ulyssa 2024-03-02 15:00:29 -08:00 committed by GitHub
parent 1948d80ec8
commit 9732971fc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1579 additions and 754 deletions

View file

@ -22,8 +22,8 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust (1.67 w/ clippy) - name: Install Rust (1.70 w/ clippy)
uses: dtolnay/rust-toolchain@1.67 uses: dtolnay/rust-toolchain@1.70
with: with:
components: clippy components: clippy
- name: Install Rust (nightly w/ rustfmt) - name: Install Rust (nightly w/ rustfmt)

1562
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ license = "Apache-2.0"
exclude = [".github", "CONTRIBUTING.md"] exclude = [".github", "CONTRIBUTING.md"]
keywords = ["matrix", "chat", "tui", "vim"] keywords = ["matrix", "chat", "tui", "vim"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
rust-version = "1.67" rust-version = "1.70"
build = "build.rs" build = "build.rs"
[build-dependencies] [build-dependencies]
@ -40,12 +40,15 @@ markup5ever_rcdom = "0.2.0"
mime = "^0.3.16" mime = "^0.3.16"
mime_guess = "^2.0.4" mime_guess = "^2.0.4"
open = "3.2.0" open = "3.2.0"
rand = "0.8.5"
ratatui = "0.23" ratatui = "0.23"
ratatui-image = { version = "0.4.3", features = ["serde"] } ratatui-image = { version = "0.4.3", features = ["serde"] }
regex = "^1.5" regex = "^1.5"
rpassword = "^7.2" rpassword = "^7.2"
serde = "^1.0" serde = "^1.0"
serde_json = "^1.0" serde_json = "^1.0"
sled = "0.34.7"
temp-dir = "0.1.12"
thiserror = "^1.0.37" thiserror = "^1.0.37"
tracing = "~0.1.36" tracing = "~0.1.36"
tracing-appender = "~0.2.2" tracing-appender = "~0.2.2"
@ -66,9 +69,9 @@ git = "https://github.com/ulyssa/modalkit"
rev = "cb8c8aeb9a499b9b16615ce144f9014d78036e01" rev = "cb8c8aeb9a499b9b16615ce144f9014d78036e01"
[dependencies.matrix-sdk] [dependencies.matrix-sdk]
version = "^0.6.2" version = "0.7.1"
default-features = false default-features = false
features = ["e2e-encryption", "sled", "rustls-tls", "sso-login"] features = ["e2e-encryption", "rustls-tls", "bundled-sqlite", "sso-login"]
[dependencies.tokio] [dependencies.tokio]
version = "1.24.1" version = "1.24.1"

View file

@ -22,7 +22,7 @@ website, [iamb.chat].
## Installation ## Installation
Install Rust (1.67.0 or above) and Cargo, and then run: Install Rust (1.70.0 or above) and Cargo, and then run:
``` ```
cargo install --locked iamb cargo install --locked iamb

View file

@ -32,17 +32,18 @@ use url::Url;
use matrix_sdk::{ use matrix_sdk::{
encryption::verification::SasVerification, encryption::verification::SasVerification,
room::{Joined, Room as MatrixRoom}, room::Room as MatrixRoom,
ruma::{ ruma::{
events::{ events::{
reaction::ReactionEvent, reaction::ReactionEvent,
relation::Replacement,
room::encrypted::RoomEncryptedEvent, room::encrypted::RoomEncryptedEvent,
room::message::{ room::message::{
OriginalRoomMessageEvent, OriginalRoomMessageEvent,
Relation, Relation,
Replacement,
RoomMessageEvent, RoomMessageEvent,
RoomMessageEventContent, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
}, },
tag::{TagName, Tags}, tag::{TagName, Tags},
MessageLikeEvent, MessageLikeEvent,
@ -55,6 +56,7 @@ use matrix_sdk::{
RoomId, RoomId,
UserId, UserId,
}, },
RoomState as MatrixRoomState,
}; };
use modalkit::{ use modalkit::{
@ -581,6 +583,10 @@ pub enum IambError {
#[error("Cryptographic storage error: {0}")] #[error("Cryptographic storage error: {0}")]
CryptoStore(#[from] matrix_sdk::encryption::CryptoStoreError), CryptoStore(#[from] matrix_sdk::encryption::CryptoStoreError),
/// A failure related to the cryptographic store.
#[error("Cannot export keys from sled: {0}")]
UpgradeSled(#[from] crate::sled_export::SledMigrationError),
/// An HTTP error. /// An HTTP error.
#[error("HTTP client error: {0}")] #[error("HTTP client error: {0}")]
Http(#[from] matrix_sdk::HttpError), Http(#[from] matrix_sdk::HttpError),
@ -809,9 +815,9 @@ impl RoomInfo {
} }
/// Insert an edit. /// Insert an edit.
pub fn insert_edit(&mut self, msg: Replacement) { pub fn insert_edit(&mut self, msg: Replacement<RoomMessageEventContentWithoutRelation>) {
let event_id = msg.event_id; let event_id = msg.event_id;
let new_content = msg.new_content; let new_msgtype = msg.new_content;
let key = if let Some(EventLocation::Message(k)) = self.keys.get(&event_id) { let key = if let Some(EventLocation::Message(k)) = self.keys.get(&event_id) {
k k
@ -827,10 +833,10 @@ impl RoomInfo {
match &mut msg.event { match &mut msg.event {
MessageEvent::Original(orig) => { MessageEvent::Original(orig) => {
orig.content.msgtype = new_content.msgtype; orig.content.apply_replacement(new_msgtype);
}, },
MessageEvent::Local(_, content) => { MessageEvent::Local(_, content) => {
content.msgtype = new_content.msgtype; content.apply_replacement(new_msgtype);
}, },
MessageEvent::Redacted(_) | MessageEvent::Redacted(_) |
MessageEvent::EncryptedOriginal(_) | MessageEvent::EncryptedOriginal(_) |
@ -1182,8 +1188,16 @@ impl ChatStore {
} }
/// Get a joined room. /// Get a joined room.
pub fn get_joined_room(&self, room_id: &RoomId) -> Option<Joined> { pub fn get_joined_room(&self, room_id: &RoomId) -> Option<MatrixRoom> {
self.worker.client.get_joined_room(room_id) let Some(room) = self.worker.client.get_room(room_id) else {
return None;
};
if room.state() == MatrixRoomState::Joined {
Some(room)
} else {
None
}
} }
/// Get the title for a room. /// Get the title for a room.

View file

@ -5,20 +5,29 @@ use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::fs::File; use std::fs::File;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::io::BufReader; use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use clap::Parser; use clap::Parser;
use matrix_sdk::ruma::{OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId}; use matrix_sdk::matrix_auth::MatrixSession;
use matrix_sdk::ruma::{OwnedDeviceId, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId};
use ratatui::style::{Color, Modifier as StyleModifier, Style}; use ratatui::style::{Color, Modifier as StyleModifier, Style};
use ratatui::text::Span; use ratatui::text::Span;
use ratatui_image::picker::ProtocolType; use ratatui_image::picker::ProtocolType;
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer}; use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer, Serialize};
use tracing::Level; use tracing::Level;
use url::Url; use url::Url;
use super::base::{IambId, RoomInfo, SortColumn, SortFieldRoom, SortFieldUser, SortOrder}; use super::base::{
IambError,
IambId,
RoomInfo,
SortColumn,
SortFieldRoom,
SortFieldUser,
SortOrder,
};
macro_rules! usage { macro_rules! usage {
( $($args: tt)* ) => { ( $($args: tt)* ) => {
@ -215,6 +224,40 @@ impl<'de> Deserialize<'de> for UserColor {
} }
} }
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Session {
access_token: String,
refresh_token: Option<String>,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
}
impl From<Session> for MatrixSession {
fn from(session: Session) -> Self {
MatrixSession {
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
access_token: session.access_token,
refresh_token: session.refresh_token,
},
meta: matrix_sdk::SessionMeta {
user_id: session.user_id,
device_id: session.device_id,
},
}
}
}
impl From<MatrixSession> for Session {
fn from(session: MatrixSession) -> Self {
Session {
access_token: session.tokens.access_token,
refresh_token: session.tokens.refresh_token,
user_id: session.meta.user_id,
device_id: session.meta.device_id,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
pub struct UserDisplayTunables { pub struct UserDisplayTunables {
pub color: Option<UserColor>, pub color: Option<UserColor>,
@ -421,14 +464,35 @@ impl Tunables {
#[derive(Clone)] #[derive(Clone)]
pub struct DirectoryValues { pub struct DirectoryValues {
pub cache: PathBuf, pub cache: PathBuf,
pub data: PathBuf,
pub logs: PathBuf, pub logs: PathBuf,
pub downloads: Option<PathBuf>, pub downloads: Option<PathBuf>,
pub image_previews: PathBuf, pub image_previews: PathBuf,
} }
impl DirectoryValues {
fn create_dir_all(&self) -> std::io::Result<()> {
use std::fs::create_dir_all;
let Self { cache, data, logs, downloads, image_previews } = self;
create_dir_all(cache)?;
create_dir_all(data)?;
create_dir_all(logs)?;
create_dir_all(image_previews)?;
if let Some(downloads) = downloads {
create_dir_all(downloads)?;
}
Ok(())
}
}
#[derive(Clone, Default, Deserialize)] #[derive(Clone, Default, Deserialize)]
pub struct Directories { pub struct Directories {
pub cache: Option<PathBuf>, pub cache: Option<PathBuf>,
pub data: Option<PathBuf>,
pub logs: Option<PathBuf>, pub logs: Option<PathBuf>,
pub downloads: Option<PathBuf>, pub downloads: Option<PathBuf>,
pub image_previews: Option<PathBuf>, pub image_previews: Option<PathBuf>,
@ -438,6 +502,7 @@ impl Directories {
fn merge(self, other: Self) -> Self { fn merge(self, other: Self) -> Self {
Directories { Directories {
cache: self.cache.or(other.cache), cache: self.cache.or(other.cache),
data: self.data.or(other.data),
logs: self.logs.or(other.logs), logs: self.logs.or(other.logs),
downloads: self.downloads.or(other.downloads), downloads: self.downloads.or(other.downloads),
image_previews: self.image_previews.or(other.image_previews), image_previews: self.image_previews.or(other.image_previews),
@ -454,6 +519,15 @@ impl Directories {
}) })
.expect("no dirs.cache value configured!"); .expect("no dirs.cache value configured!");
let data = self
.data
.or_else(|| {
let mut dir = dirs::data_dir()?;
dir.push("iamb");
dir.into()
})
.expect("no dirs.data value configured!");
let logs = self.logs.unwrap_or_else(|| { let logs = self.logs.unwrap_or_else(|| {
let mut dir = cache.clone(); let mut dir = cache.clone();
dir.push("logs"); dir.push("logs");
@ -468,7 +542,7 @@ impl Directories {
dir dir
}); });
DirectoryValues { cache, logs, downloads, image_previews } DirectoryValues { cache, data, logs, downloads, image_previews }
} }
} }
@ -540,9 +614,11 @@ impl IambConfig {
#[derive(Clone)] #[derive(Clone)]
pub struct ApplicationSettings { pub struct ApplicationSettings {
pub matrix_dir: PathBuf,
pub layout_json: PathBuf, pub layout_json: PathBuf,
pub session_json: PathBuf, pub session_json: PathBuf,
pub session_json_old: PathBuf,
pub sled_dir: PathBuf,
pub sqlite_dir: PathBuf,
pub profile_name: String, pub profile_name: String,
pub profile: ProfileConfig, pub profile: ProfileConfig,
pub tunables: TunableValues, pub tunables: TunableValues,
@ -602,17 +678,30 @@ impl ApplicationSettings {
let dirs = profile.dirs.take().unwrap_or_default().merge(dirs); let dirs = profile.dirs.take().unwrap_or_default().merge(dirs);
let dirs = dirs.values(); let dirs = dirs.values();
// Create directories
dirs.create_dir_all()?;
// Set up paths that live inside the profile's data directory. // Set up paths that live inside the profile's data directory.
let mut profile_dir = config_dir.clone(); let mut profile_dir = config_dir.clone();
profile_dir.push("profiles"); profile_dir.push("profiles");
profile_dir.push(profile_name.as_str()); profile_dir.push(profile_name.as_str());
let mut matrix_dir = profile_dir.clone(); let mut profile_data_dir = dirs.data.clone();
matrix_dir.push("matrix"); profile_data_dir.push("profiles");
profile_data_dir.push(profile_name.as_str());
let mut session_json = profile_dir; let mut sled_dir = profile_dir.clone();
sled_dir.push("matrix");
let mut sqlite_dir = profile_data_dir.clone();
sqlite_dir.push("sqlite");
let mut session_json = profile_data_dir.clone();
session_json.push("session.json"); session_json.push("session.json");
let mut session_json_old = profile_dir;
session_json_old.push("session.json");
// Set up paths that live inside the profile's cache directory. // Set up paths that live inside the profile's cache directory.
let mut cache_dir = dirs.cache.clone(); let mut cache_dir = dirs.cache.clone();
cache_dir.push("profiles"); cache_dir.push("profiles");
@ -622,9 +711,11 @@ impl ApplicationSettings {
layout_json.push("layout.json"); layout_json.push("layout.json");
let settings = ApplicationSettings { let settings = ApplicationSettings {
matrix_dir, sled_dir,
layout_json, layout_json,
session_json, session_json,
session_json_old,
sqlite_dir,
profile_name, profile_name,
profile, profile,
tunables, tunables,
@ -635,6 +726,21 @@ impl ApplicationSettings {
Ok(settings) Ok(settings)
} }
pub fn read_session(&self, path: impl AsRef<Path>) -> Result<Session, IambError> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let session = serde_json::from_reader(reader).map_err(IambError::from)?;
Ok(session)
}
pub fn write_session(&self, session: MatrixSession) -> Result<(), IambError> {
let file = File::create(self.session_json.as_path())?;
let writer = BufWriter::new(file);
let session = Session::from(session);
serde_json::to_writer(writer, &session).map_err(IambError::from)?;
Ok(())
}
pub fn get_user_char_span<'a>(&self, user_id: &'a UserId) -> Span<'a> { pub fn get_user_char_span<'a>(&self, user_id: &'a UserId) -> Span<'a> {
let (color, c) = self let (color, c) = self
.tunables .tunables

View file

@ -19,7 +19,7 @@ use std::collections::VecDeque;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::Display; use std::fmt::Display;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::io::{stdout, BufReader, BufWriter, Stdout}; use std::io::{stdout, BufWriter, Stdout};
use std::ops::DerefMut; use std::ops::DerefMut;
use std::process; use std::process;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
@ -27,7 +27,10 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use clap::Parser; use clap::Parser;
use matrix_sdk::crypto::encrypt_room_key_export;
use matrix_sdk::ruma::OwnedUserId; use matrix_sdk::ruma::OwnedUserId;
use rand::{distributions::Alphanumeric, Rng};
use temp_dir::TempDir;
use tokio::sync::Mutex as AsyncMutex; use tokio::sync::Mutex as AsyncMutex;
use tracing_subscriber::FmtSubscriber; use tracing_subscriber::FmtSubscriber;
@ -62,6 +65,7 @@ mod config;
mod keybindings; mod keybindings;
mod message; mod message;
mod preview; mod preview;
mod sled_export;
mod util; mod util;
mod windows; mod windows;
mod worker; mod worker;
@ -558,7 +562,7 @@ impl Application {
match action { match action {
HomeserverAction::CreateRoom(alias, vis, flags) => { HomeserverAction::CreateRoom(alias, vis, flags) => {
let client = &store.application.worker.client; let client = &store.application.worker.client;
let room_id = create_room(client, alias.as_deref(), vis, flags).await?; let room_id = create_room(client, alias, vis, flags).await?;
let room = IambId::Room(room_id); let room = IambId::Room(room_id);
let target = OpenTarget::Application(room); let target = OpenTarget::Application(room);
let action = WindowAction::Switch(target); let action = WindowAction::Switch(target);
@ -659,26 +663,47 @@ impl Application {
} }
} }
async fn login(worker: Requester, settings: &ApplicationSettings) -> IambResult<()> { fn gen_passphrase() -> String {
println!("Logging in for {}...", settings.profile.user_id); rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(20)
.map(char::from)
.collect()
}
fn read_response(question: &str) -> String {
println!("{question}");
let mut input = String::new();
let _ = std::io::stdin().read_line(&mut input);
input
}
fn read_yesno(question: &str) -> Option<char> {
read_response(question).chars().next().map(|c| c.to_ascii_lowercase())
}
async fn login(worker: &Requester, settings: &ApplicationSettings) -> IambResult<()> {
if settings.session_json.is_file() { if settings.session_json.is_file() {
let file = File::open(settings.session_json.as_path())?; let session = settings.read_session(&settings.session_json)?;
let reader = BufReader::new(file); worker.login(LoginStyle::SessionRestore(session.into()))?;
let session = serde_json::from_reader(reader).map_err(IambError::from)?;
worker.login(LoginStyle::SessionRestore(session))?; return Ok(());
}
if settings.session_json_old.is_file() && !settings.sled_dir.is_dir() {
let session = settings.read_session(&settings.session_json_old)?;
worker.login(LoginStyle::SessionRestore(session.into()))?;
return Ok(()); return Ok(());
} }
loop { loop {
println!("Please select login type: [p]assword / [s]ingle sign on"); let login_style =
match read_response("Please select login type: [p]assword / [s]ingle sign on")
let mut input = String::new(); .chars()
std::io::stdin().read_line(&mut input).unwrap(); .next()
.map(|c| c.to_ascii_lowercase())
let login_style = match input.chars().next().map(|c| c.to_ascii_lowercase()) { {
None | Some('p') => { None | Some('p') => {
let password = rpassword::prompt_password("Password: ")?; let password = rpassword::prompt_password("Password: ")?;
LoginStyle::Password(password) LoginStyle::Password(password)
@ -713,17 +738,142 @@ fn print_exit<T: Display, N>(v: T) -> N {
process::exit(2); process::exit(2);
} }
// We can't access the OlmMachine directly, so write the keys to a temporary
// file first, and then import them later.
async fn check_import_keys(
settings: &ApplicationSettings,
) -> IambResult<Option<(temp_dir::TempDir, String)>> {
let do_import = settings.sled_dir.is_dir() && !settings.sqlite_dir.is_dir();
if !do_import {
return Ok(None);
}
let question = format!(
"Found old sled store in {}. Would you like to export room keys from it? [y]es/[n]o",
settings.sled_dir.display()
);
loop {
match read_yesno(&question) {
Some('y') => {
break;
},
Some('n') => {
return Ok(None);
},
Some(_) | None => {
continue;
},
}
}
let keys = sled_export::export_room_keys(&settings.sled_dir).await?;
let passphrase = gen_passphrase();
println!("* Encrypting {} room keys with the passphrase {passphrase:?}...", keys.len());
let encrypted = match encrypt_room_key_export(&keys, &passphrase, 500000) {
Ok(encrypted) => encrypted,
Err(e) => {
format!("* Failed to encrypt room keys during export: {e}");
process::exit(2);
},
};
let tmpdir = TempDir::new()?;
let exported = tmpdir.child("keys");
println!("* Writing encrypted room keys to {}...", exported.display());
tokio::fs::write(&exported, &encrypted).await?;
Ok(Some((tmpdir, passphrase)))
}
async fn login_upgrade(
keydir: TempDir,
passphrase: String,
worker: &Requester,
settings: &ApplicationSettings,
store: &AsyncProgramStore,
) -> IambResult<()> {
println!(
"Please log in for {} to import the room keys into a new session",
settings.profile.user_id
);
login(worker, settings).await?;
println!("* Importing room keys...");
let exported = keydir.child("keys");
let imported = worker.client.encryption().import_room_keys(exported, &passphrase).await;
match imported {
Ok(res) => {
println!(
"* Successfully imported {} out of {} keys",
res.imported_count, res.total_count
);
let _ = keydir.cleanup();
},
Err(e) => {
println!(
"Failed to import room keys from {}/keys: {e}\n\n\
They have been encrypted with the passphrase {passphrase:?}.\
Please save them and try importing them manually instead\n",
keydir.path().display()
);
loop {
match read_yesno("Would you like to continue logging in? [y]es/[n]o") {
Some('y') => break,
Some('n') => print_exit("* Exiting..."),
Some(_) | None => continue,
}
}
},
}
println!("* Syncing...");
worker::do_first_sync(&worker.client, store).await;
Ok(())
}
async fn login_normal(
worker: &Requester,
settings: &ApplicationSettings,
store: &AsyncProgramStore,
) -> IambResult<()> {
println!("* Logging in for {}...", settings.profile.user_id);
login(worker, settings).await?;
worker::do_first_sync(&worker.client, store).await;
Ok(())
}
async fn run(settings: ApplicationSettings) -> IambResult<()> { async fn run(settings: ApplicationSettings) -> IambResult<()> {
// Get old keys the first time we run w/ the upgraded SDK.
let import_keys = check_import_keys(&settings).await?;
// Set up client state.
create_dir_all(settings.sqlite_dir.as_path())?;
let client = worker::create_client(&settings).await;
// Set up the async worker thread and global store. // Set up the async worker thread and global store.
let worker = ClientWorker::spawn(settings.clone()).await; let worker = ClientWorker::spawn(client.clone(), settings.clone()).await;
let client = worker.client.clone();
let store = ChatStore::new(worker.clone(), settings.clone()); let store = ChatStore::new(worker.clone(), settings.clone());
let store = Store::new(store); let store = Store::new(store);
let store = Arc::new(AsyncMutex::new(store)); let store = Arc::new(AsyncMutex::new(store));
worker.init(store.clone()); worker.init(store.clone());
login(worker, &settings).await.unwrap_or_else(print_exit); if let Some((keydir, pass)) = import_keys {
worker::do_first_sync(client, &store).await; login_upgrade(keydir, pass, &worker, &settings, &store)
.await
.unwrap_or_else(print_exit);
} else {
login_normal(&worker, &settings, &store).await.unwrap_or_else(print_exit);
}
fn restore_tty() { fn restore_tty() {
let _ = crossterm::terminal::disable_raw_mode(); let _ = crossterm::terminal::disable_raw_mode();
@ -767,10 +917,6 @@ fn main() -> IambResult<()> {
let log_prefix = format!("iamb-log-{}", settings.profile_name); let log_prefix = format!("iamb-log-{}", settings.profile_name);
let log_dir = settings.dirs.logs.as_path(); let log_dir = settings.dirs.logs.as_path();
create_dir_all(settings.matrix_dir.as_path())?;
create_dir_all(settings.dirs.image_previews.as_path())?;
create_dir_all(log_dir)?;
let appender = tracing_appender::rolling::daily(log_dir, log_prefix); let appender = tracing_appender::rolling::daily(log_dir, log_prefix);
let (appender, guard) = tracing_appender::non_blocking(appender); let (appender, guard) = tracing_appender::non_blocking(appender);

View file

@ -9,6 +9,7 @@ use std::hash::{Hash, Hasher};
use chrono::{DateTime, Local as LocalTz, NaiveDateTime, TimeZone}; use chrono::{DateTime, Local as LocalTz, NaiveDateTime, TimeZone};
use comrak::{markdown_to_html, ComrakOptions}; use comrak::{markdown_to_html, ComrakOptions};
use serde_json::json;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use matrix_sdk::ruma::{ use matrix_sdk::ruma::{
@ -33,7 +34,7 @@ use matrix_sdk::ruma::{
redaction::SyncRoomRedactionEvent, redaction::SyncRoomRedactionEvent,
}, },
AnyMessageLikeEvent, AnyMessageLikeEvent,
Redact, RedactContent,
RedactedUnsigned, RedactedUnsigned,
}, },
EventId, EventId,
@ -345,6 +346,28 @@ impl PartialOrd for MessageCursor {
} }
} }
fn redaction_reason(ev: &SyncRoomRedactionEvent) -> Option<&str> {
let SyncRoomRedactionEvent::Original(ev) = ev else {
return None;
};
return ev.content.reason.as_deref();
}
fn redaction_unsigned(ev: SyncRoomRedactionEvent) -> RedactedUnsigned {
let reason = redaction_reason(&ev);
let redacted_because = json!({
"content": {
"reason": reason
},
"event_id": ev.event_id(),
"sender": ev.sender(),
"origin_server_ts": ev.origin_server_ts(),
"unsigned": {},
});
RedactedUnsigned::new(serde_json::from_value(redacted_because).unwrap())
}
#[derive(Clone)] #[derive(Clone)]
pub enum MessageEvent { pub enum MessageEvent {
EncryptedOriginal(Box<OriginalRoomEncryptedEvent>), EncryptedOriginal(Box<OriginalRoomEncryptedEvent>),
@ -419,7 +442,14 @@ impl MessageEvent {
MessageEvent::Redacted(_) => return, MessageEvent::Redacted(_) => return,
MessageEvent::Local(_, _) => return, MessageEvent::Local(_, _) => return,
MessageEvent::Original(ev) => { MessageEvent::Original(ev) => {
let redacted = ev.clone().redact(redaction, version); let redacted = RedactedRoomMessageEvent {
content: ev.content.clone().redact(version),
event_id: ev.event_id.clone(),
sender: ev.sender.clone(),
origin_server_ts: ev.origin_server_ts,
room_id: ev.room_id.clone(),
unsigned: redaction_unsigned(redaction),
};
*self = MessageEvent::Redacted(Box::new(redacted)); *self = MessageEvent::Redacted(Box::new(redacted));
}, },
} }
@ -455,11 +485,7 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
} }
fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> { fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> {
let reason = unsigned let reason = unsigned.redacted_because.content.reason.as_ref();
.redacted_because
.as_ref()
.and_then(|e| e.as_original())
.and_then(|r| r.content.reason.as_ref());
if let Some(r) = reason { if let Some(r) = reason {
Cow::Owned(format!("[Redacted: {r:?}]")) Cow::Owned(format!("[Redacted: {r:?}]"))

58
src/sled_export.rs Normal file
View file

@ -0,0 +1,58 @@
//! # sled -> sqlite migration code
//!
//! Before the 0.0.9 release, iamb used matrix-sdk@0.6.2, which used [sled]
//! for storing information, including room keys. In matrix-sdk@0.7.0,
//! the SDK switched to using SQLite. This module takes care of opening
//! sled, exporting the inbound group sessions used for decryption,
//! and importing them into SQLite.
//!
//! This code will eventually be removed once people have been given enough
//! time to upgrade off of pre-0.0.9 versions.
//!
//! [sled]: https://docs.rs/sled/0.34.7/sled/index.html
use sled::{Config, IVec};
use std::path::Path;
use crate::base::IambError;
use matrix_sdk::crypto::olm::{ExportedRoomKey, InboundGroupSession, PickledInboundGroupSession};
#[derive(Debug, thiserror::Error)]
pub enum SledMigrationError {
#[error("sled failure: {0}")]
Sled(#[from] sled::Error),
#[error("deserialization failure: {0}")]
Deserialize(#[from] serde_json::Error),
}
fn group_session_from_slice(
(_, bytes): (IVec, IVec),
) -> Result<PickledInboundGroupSession, SledMigrationError> {
serde_json::from_slice(&bytes).map_err(SledMigrationError::from)
}
async fn export_room_keys_priv(
sled_dir: &Path,
) -> Result<Vec<ExportedRoomKey>, SledMigrationError> {
let path = sled_dir.join("matrix-sdk-state");
let store = Config::new().temporary(false).path(&path).open()?;
let inbound_groups = store.open_tree("inbound_group_sessions")?;
let mut exported = vec![];
let sessions = inbound_groups
.iter()
.map(|p| p.map_err(SledMigrationError::from).and_then(group_session_from_slice))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter_map(|p| InboundGroupSession::from_pickle(p).ok());
for session in sessions {
exported.push(session.export().await);
}
Ok(exported)
}
pub async fn export_room_keys(sled_dir: &Path) -> Result<Vec<ExportedRoomKey>, IambError> {
export_room_keys_priv(sled_dir).await.map_err(IambError::from)
}

View file

@ -169,6 +169,7 @@ pub fn mock_room() -> RoomInfo {
pub fn mock_dirs() -> DirectoryValues { pub fn mock_dirs() -> DirectoryValues {
DirectoryValues { DirectoryValues {
cache: PathBuf::new(), cache: PathBuf::new(),
data: PathBuf::new(),
logs: PathBuf::new(), logs: PathBuf::new(),
downloads: None, downloads: None,
image_previews: PathBuf::new(), image_previews: PathBuf::new(),
@ -202,9 +203,11 @@ pub fn mock_tunables() -> TunableValues {
pub fn mock_settings() -> ApplicationSettings { pub fn mock_settings() -> ApplicationSettings {
ApplicationSettings { ApplicationSettings {
matrix_dir: PathBuf::new(),
layout_json: PathBuf::new(), layout_json: PathBuf::new(),
session_json: PathBuf::new(), session_json: PathBuf::new(),
session_json_old: PathBuf::new(),
sled_dir: PathBuf::new(),
sqlite_dir: PathBuf::new(),
profile_name: "test".into(), profile_name: "test".into(),
profile: ProfileConfig { profile: ProfileConfig {

View file

@ -14,14 +14,16 @@ use url::Url;
use matrix_sdk::{ use matrix_sdk::{
attachment::AttachmentConfig, attachment::AttachmentConfig,
media::{MediaFormat, MediaRequest}, media::{MediaFormat, MediaRequest},
room::{Joined, Room as MatrixRoom}, room::Room as MatrixRoom,
ruma::{ ruma::{
events::reaction::{ReactionEventContent, Relation as Reaction}, events::reaction::ReactionEventContent,
events::relation::{Annotation, Replacement},
events::room::message::{ events::room::message::{
AddMentions,
ForwardThread,
MessageType, MessageType,
OriginalRoomMessageEvent, OriginalRoomMessageEvent,
Relation, Relation,
Replacement,
RoomMessageEventContent, RoomMessageEventContent,
TextMessageEventContent, TextMessageEventContent,
}, },
@ -29,6 +31,7 @@ use matrix_sdk::{
OwnedRoomId, OwnedRoomId,
RoomId, RoomId,
}, },
RoomState,
}; };
use ratatui::{ use ratatui::{
@ -126,8 +129,16 @@ impl ChatState {
} }
} }
fn get_joined(&self, worker: &Requester) -> Result<Joined, IambError> { fn get_joined(&self, worker: &Requester) -> Result<MatrixRoom, IambError> {
worker.client.get_joined_room(self.id()).ok_or(IambError::NotJoined) let Some(room) = worker.client.get_room(self.id()) else {
return Err(IambError::NotJoined);
};
if room.state() == RoomState::Joined {
Ok(room)
} else {
Err(IambError::NotJoined)
}
} }
fn get_reply_to<'a>(&self, info: &'a RoomInfo) -> Option<&'a OriginalRoomMessageEvent> { fn get_reply_to<'a>(&self, info: &'a RoomInfo) -> Option<&'a OriginalRoomMessageEvent> {
@ -356,9 +367,9 @@ impl ChatState {
}, },
}; };
let reaction = Reaction::new(event_id, emoji); let reaction = Annotation::new(event_id, emoji);
let msg = ReactionEventContent::new(reaction); let msg = ReactionEventContent::new(reaction);
let _ = room.send(msg, None).await.map_err(IambError::from)?; let _ = room.send(msg).await.map_err(IambError::from)?;
Ok(None) Ok(None)
}, },
@ -449,12 +460,7 @@ impl ChatState {
_: ProgramContext, _: ProgramContext,
store: &mut ProgramStore, store: &mut ProgramStore,
) -> IambResult<EditInfo> { ) -> IambResult<EditInfo> {
let room = store let room = self.get_joined(&store.application.worker)?;
.application
.worker
.client
.get_joined_room(self.id())
.ok_or(IambError::NotJoined)?;
let info = store.application.rooms.get_or_default(self.id().to_owned()); let info = store.application.rooms.get_or_default(self.id().to_owned());
let mut show_echo = true; let mut show_echo = true;
@ -475,18 +481,18 @@ impl ChatState {
if let Some((_, event_id)) = &self.editing { if let Some((_, event_id)) = &self.editing {
msg.relates_to = Some(Relation::Replacement(Replacement::new( msg.relates_to = Some(Relation::Replacement(Replacement::new(
event_id.clone(), event_id.clone(),
Box::new(msg.clone()), msg.msgtype.clone().into(),
))); )));
show_echo = false; show_echo = false;
} else if let Some(m) = self.get_reply_to(info) { } else if let Some(m) = self.get_reply_to(info) {
// XXX: Switch to RoomMessageEventContent::reply() once it's stable? // XXX: Switch to RoomMessageEventContent::reply() once it's stable?
msg = msg.make_reply_to(m); msg = msg.make_reply_to(m, ForwardThread::Yes, AddMentions::No);
} }
// XXX: second parameter can be a locally unique transaction id. // XXX: second parameter can be a locally unique transaction id.
// Useful for doing retries. // Useful for doing retries.
let resp = room.send(msg.clone(), None).await.map_err(IambError::from)?; let resp = room.send(msg.clone()).await.map_err(IambError::from)?;
let event_id = resp.event_id; let event_id = resp.event_id;
// Reset message bar state now that it's been sent. // Reset message bar state now that it's been sent.
@ -506,7 +512,7 @@ impl ChatState {
let config = AttachmentConfig::new(); let config = AttachmentConfig::new();
let resp = room let resp = room
.send_attachment(name.as_ref(), &mime, bytes.as_ref(), config) .send_attachment(name.as_ref(), &mime, bytes, config)
.await .await
.map_err(IambError::from)?; .map_err(IambError::from)?;
@ -536,7 +542,7 @@ impl ChatState {
let config = AttachmentConfig::new(); let config = AttachmentConfig::new();
let resp = room let resp = room
.send_attachment(name.as_ref(), &mime, bytes.as_ref(), config) .send_attachment(name.as_ref(), &mime, bytes, config)
.await .await
.map_err(IambError::from)?; .map_err(IambError::from)?;

View file

@ -1,6 +1,6 @@
//! # Windows for Matrix rooms and spaces //! # Windows for Matrix rooms and spaces
use matrix_sdk::{ use matrix_sdk::{
room::{Invited, Room as MatrixRoom}, room::Room as MatrixRoom,
ruma::{ ruma::{
events::{ events::{
room::{name::RoomNameEventContent, topic::RoomTopicEventContent}, room::{name::RoomNameEventContent, topic::RoomTopicEventContent},
@ -9,6 +9,7 @@ use matrix_sdk::{
RoomId, RoomId,
}, },
DisplayName, DisplayName,
RoomState as MatrixRoomState,
}; };
use ratatui::{ use ratatui::{
@ -114,7 +115,7 @@ impl RoomState {
fn draw_invite( fn draw_invite(
&self, &self,
invited: Invited, invited: MatrixRoom,
area: Rect, area: Rect,
buf: &mut Buffer, buf: &mut Buffer,
store: &mut ProgramStore, store: &mut ProgramStore,
@ -177,12 +178,12 @@ impl RoomState {
) -> IambResult<Vec<(Action<IambInfo>, ProgramContext)>> { ) -> IambResult<Vec<(Action<IambInfo>, ProgramContext)>> {
match act { match act {
RoomAction::InviteAccept => { RoomAction::InviteAccept => {
if let Some(room) = store.application.worker.client.get_invited_room(self.id()) { if let Some(room) = store.application.worker.client.get_room(self.id()) {
let details = room.invite_details().await.map_err(IambError::from)?; let details = room.invite_details().await.map_err(IambError::from)?;
let details = details.invitee.event().original_content(); let details = details.invitee.event().original_content();
let is_direct = details.and_then(|ev| ev.is_direct).unwrap_or_default(); let is_direct = details.and_then(|ev| ev.is_direct).unwrap_or_default();
room.accept_invitation().await.map_err(IambError::from)?; room.join().await.map_err(IambError::from)?;
if is_direct { if is_direct {
room.set_is_direct(true).await.map_err(IambError::from)?; room.set_is_direct(true).await.map_err(IambError::from)?;
@ -194,8 +195,8 @@ impl RoomState {
} }
}, },
RoomAction::InviteReject => { RoomAction::InviteReject => {
if let Some(room) = store.application.worker.client.get_invited_room(self.id()) { if let Some(room) = store.application.worker.client.get_room(self.id()) {
room.reject_invitation().await.map_err(IambError::from)?; room.leave().await.map_err(IambError::from)?;
Ok(vec![]) Ok(vec![])
} else { } else {
@ -203,7 +204,7 @@ impl RoomState {
} }
}, },
RoomAction::InviteSend(user) => { RoomAction::InviteSend(user) => {
if let Some(room) = store.application.worker.client.get_joined_room(self.id()) { if let Some(room) = store.application.worker.client.get_room(self.id()) {
room.invite_user_by_id(user.as_ref()).await.map_err(IambError::from)?; room.invite_user_by_id(user.as_ref()).await.map_err(IambError::from)?;
Ok(vec![]) Ok(vec![])
@ -212,7 +213,7 @@ impl RoomState {
} }
}, },
RoomAction::Leave(skip_confirm) => { RoomAction::Leave(skip_confirm) => {
if let Some(room) = store.application.worker.client.get_joined_room(self.id()) { if let Some(room) = store.application.worker.client.get_room(self.id()) {
if skip_confirm { if skip_confirm {
room.leave().await.map_err(IambError::from)?; room.leave().await.map_err(IambError::from)?;
@ -247,7 +248,7 @@ impl RoomState {
match field { match field {
RoomField::Name => { RoomField::Name => {
let ev = RoomNameEventContent::new(value.into()); let ev = RoomNameEventContent::new(value);
let _ = room.send_state_event(ev).await.map_err(IambError::from)?; let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
}, },
RoomField::Tag(tag) => { RoomField::Tag(tag) => {
@ -272,7 +273,7 @@ impl RoomState {
match field { match field {
RoomField::Name => { RoomField::Name => {
let ev = RoomNameEventContent::new(None); let ev = RoomNameEventContent::new("".into());
let _ = room.send_state_event(ev).await.map_err(IambError::from)?; let _ = room.send_state_event(ev).await.map_err(IambError::from)?;
}, },
RoomField::Tag(tag) => { RoomField::Tag(tag) => {
@ -381,12 +382,12 @@ impl TerminalCursor for RoomState {
impl WindowOps<IambInfo> for RoomState { impl WindowOps<IambInfo> for RoomState {
fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) {
if let MatrixRoom::Invited(_) = self.room() { if self.room().state() == MatrixRoomState::Invited {
self.refresh_room(store); self.refresh_room(store);
} }
if let MatrixRoom::Invited(invited) = self.room() { if self.room().state() == MatrixRoomState::Invited {
self.draw_invite(invited.clone(), area, buf, store); self.draw_invite(self.room().clone(), area, buf, store);
} }
match self { match self {

View file

@ -5,8 +5,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::fs::File; use std::ops::Deref;
use std::io::BufWriter;
use std::str::FromStr; use std::str::FromStr;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::Arc; use std::sync::Arc;
@ -17,13 +16,15 @@ use gethostname::gethostname;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{error, warn}; use tracing::{error, warn};
use url::Url;
use matrix_sdk::{ use matrix_sdk::{
config::{RequestConfig, SyncSettings}, config::{RequestConfig, SyncSettings},
encryption::verification::{SasVerification, Verification}, encryption::verification::{SasVerification, Verification},
event_handler::Ctx, event_handler::Ctx,
matrix_auth::MatrixSession,
reqwest, reqwest,
room::{Invited, Messages, MessagesOptions, Room as MatrixRoom, RoomMember}, room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
ruma::{ ruma::{
api::client::{ api::client::{
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter}, filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
@ -42,7 +43,8 @@ use matrix_sdk::{
}, },
presence::PresenceEvent, presence::PresenceEvent,
reaction::ReactionEventContent, reaction::ReactionEventContent,
receipt::{ReceiptEventContent, ReceiptType}, receipt::ReceiptType,
receipt::{ReceiptEventContent, ReceiptThread},
room::{ room::{
encryption::RoomEncryptionEventContent, encryption::RoomEncryptionEventContent,
member::OriginalSyncRoomMemberEvent, member::OriginalSyncRoomMemberEvent,
@ -73,8 +75,9 @@ use matrix_sdk::{
RoomVersionId, RoomVersionId,
}, },
Client, Client,
ClientBuildError,
DisplayName, DisplayName,
Session, RoomMemberships,
}; };
use modalkit::errors::UIError; use modalkit::errors::UIError;
@ -106,9 +109,13 @@ fn initial_devname() -> String {
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy()) format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
} }
async fn is_direct(room: &MatrixRoom) -> bool {
room.deref().is_direct().await.unwrap_or_default()
}
pub async fn create_room( pub async fn create_room(
client: &Client, client: &Client,
room_alias_name: Option<&str>, room_alias_name: Option<String>,
rt: CreateRoomType, rt: CreateRoomType,
flags: CreateRoomFlags, flags: CreateRoomFlags,
) -> IambResult<OwnedRoomId> { ) -> IambResult<OwnedRoomId> {
@ -154,8 +161,8 @@ pub async fn create_room(
let request = assign!(CreateRoomRequest::new(), { let request = assign!(CreateRoomRequest::new(), {
room_alias_name, room_alias_name,
creation_content, creation_content,
initial_state: initial_state.as_slice(), initial_state,
invite: invite.as_slice(), invite,
is_direct, is_direct,
visibility, visibility,
preset, preset,
@ -164,27 +171,31 @@ pub async fn create_room(
let resp = client.create_room(request).await.map_err(IambError::from)?; let resp = client.create_room(request).await.map_err(IambError::from)?;
if is_direct { if is_direct {
if let Some(room) = client.get_room(&resp.room_id) { if let Some(room) = client.get_room(resp.room_id()) {
room.set_is_direct(true).await.map_err(IambError::from)?; room.set_is_direct(true).await.map_err(IambError::from)?;
} else { } else {
error!( error!(
room_id = resp.room_id.as_str(), room_id = resp.room_id().as_str(),
"Couldn't set is_direct for new direct message room" "Couldn't set is_direct for new direct message room"
); );
} }
} }
return Ok(resp.room_id); return Ok(resp.room_id().to_owned());
} }
async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id: &EventId) { async fn update_event_receipts(info: &mut RoomInfo, room: &MatrixRoom, event_id: &EventId) {
let receipts = match room.event_read_receipts(event_id).await { let receipts = match room
.load_event_receipts(ReceiptType::Read, ReceiptThread::Main, event_id)
.await
{
Ok(receipts) => receipts, Ok(receipts) => receipts,
Err(e) => { Err(e) => {
tracing::warn!(?event_id, "failed to get event receipts: {e}"); tracing::warn!(?event_id, "failed to get event receipts: {e}");
return; return;
}, },
}; };
for (user_id, _) in receipts { for (user_id, _) in receipts {
info.set_receipt(user_id, event_id.to_owned()); info.set_receipt(user_id, event_id.to_owned());
} }
@ -339,7 +350,10 @@ async fn load_older(client: &Client, store: &AsyncProgramStore) -> usize {
async fn members_load(client: &Client, room_id: &RoomId) -> IambResult<Vec<RoomMember>> { async fn members_load(client: &Client, room_id: &RoomId) -> IambResult<Vec<RoomMember>> {
if let Some(room) = client.get_room(room_id) { if let Some(room) = client.get_room(room_id) {
Ok(room.members_no_sync().await.map_err(IambError::from)?) Ok(room
.members_no_sync(RoomMemberships::all())
.await
.map_err(IambError::from)?)
} else { } else {
Err(IambError::UnknownRoom(room_id.to_owned()).into()) Err(IambError::UnknownRoom(room_id.to_owned()).into())
} }
@ -388,12 +402,12 @@ async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) {
names.push((room.room_id().to_owned(), name)); names.push((room.room_id().to_owned(), name));
if room.is_direct() { if is_direct(&room).await {
dms.push(Arc::new((room.into(), tags))); dms.push(Arc::new((room, tags)));
} else if room.is_space() { } else if room.is_space() {
spaces.push(Arc::new((room.into(), tags))); spaces.push(Arc::new((room, tags)));
} else { } else {
rooms.push(Arc::new((room.into(), tags))); rooms.push(Arc::new((room, tags)));
} }
} }
@ -403,12 +417,12 @@ async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) {
names.push((room.room_id().to_owned(), name)); names.push((room.room_id().to_owned(), name));
if room.is_direct() { if is_direct(&room).await {
dms.push(Arc::new((room.into(), tags))); dms.push(Arc::new((room, tags)));
} else if room.is_space() { } else if room.is_space() {
spaces.push(Arc::new((room.into(), tags))); spaces.push(Arc::new((room, tags)));
} else { } else {
rooms.push(Arc::new((room.into(), tags))); rooms.push(Arc::new((room, tags)));
} }
} }
@ -458,10 +472,20 @@ async fn send_receipts_forever(client: &Client, store: &AsyncProgramStore) {
drop(locked); drop(locked);
for (room_id, new_receipt) in updates { for (room_id, new_receipt) in updates {
let Some(room) = client.get_joined_room(&room_id) else { use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType;
let Some(room) = client.get_room(&room_id) else {
continue; continue;
}; };
match room.read_receipt(&new_receipt).await {
match room
.send_single_receipt(
ReceiptType::Read,
ReceiptThread::Unthreaded,
new_receipt.clone(),
)
.await
{
Ok(()) => { Ok(()) => {
sent.insert(room_id, new_receipt); sent.insert(room_id, new_receipt);
}, },
@ -471,7 +495,7 @@ async fn send_receipts_forever(client: &Client, store: &AsyncProgramStore) {
} }
} }
pub async fn do_first_sync(client: Client, store: &AsyncProgramStore) { pub async fn do_first_sync(client: &Client, store: &AsyncProgramStore) {
// Perform an initial, lazily-loaded sync. // Perform an initial, lazily-loaded sync.
let mut room = RoomEventFilter::default(); let mut room = RoomEventFilter::default();
room.lazy_load_options = LazyLoadOptions::Enabled { include_redundant_members: false }; room.lazy_load_options = LazyLoadOptions::Enabled { include_redundant_members: false };
@ -490,7 +514,7 @@ pub async fn do_first_sync(client: Client, store: &AsyncProgramStore) {
} }
// Populate sync_info with our initial set of rooms/dms/spaces. // Populate sync_info with our initial set of rooms/dms/spaces.
refresh_rooms(&client, store).await; refresh_rooms(client, store).await;
// Insert Need::Messages to fetch accurate recent timestamps in the background. // Insert Need::Messages to fetch accurate recent timestamps in the background.
let mut locked = store.lock().await; let mut locked = store.lock().await;
@ -509,7 +533,7 @@ pub async fn do_first_sync(client: Client, store: &AsyncProgramStore) {
#[derive(Debug)] #[derive(Debug)]
pub enum LoginStyle { pub enum LoginStyle {
SessionRestore(Session), SessionRestore(MatrixSession),
Password(String), Password(String),
SingleSignOn, SingleSignOn,
} }
@ -543,7 +567,7 @@ pub enum WorkerTask {
Init(AsyncProgramStore, ClientReply<()>), Init(AsyncProgramStore, ClientReply<()>),
Login(LoginStyle, ClientReply<IambResult<EditInfo>>), Login(LoginStyle, ClientReply<IambResult<EditInfo>>),
Logout(String, ClientReply<IambResult<EditInfo>>), Logout(String, ClientReply<IambResult<EditInfo>>),
GetInviter(Invited, ClientReply<IambResult<Option<RoomMember>>>), GetInviter(MatrixRoom, ClientReply<IambResult<Option<RoomMember>>>),
GetRoom(OwnedRoomId, ClientReply<IambResult<FetchedRoom>>), GetRoom(OwnedRoomId, ClientReply<IambResult<FetchedRoom>>),
JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>), JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>),
Members(OwnedRoomId, ClientReply<IambResult<Vec<RoomMember>>>), Members(OwnedRoomId, ClientReply<IambResult<Vec<RoomMember>>>),
@ -618,6 +642,56 @@ impl Debug for WorkerTask {
} }
} }
async fn create_client_inner(
homeserver: &Option<Url>,
settings: &ApplicationSettings,
) -> Result<Client, ClientBuildError> {
let req_timeout = Duration::from_secs(settings.tunables.request_timeout);
// Set up the HTTP client.
let http = reqwest::Client::builder()
.user_agent(IAMB_USER_AGENT)
.timeout(req_timeout)
.pool_idle_timeout(Duration::from_secs(60))
.pool_max_idle_per_host(10)
.tcp_keepalive(Duration::from_secs(10))
.build()
.unwrap();
let req_config = RequestConfig::new().timeout(req_timeout).retry_timeout(req_timeout);
// Set up the Matrix client for the selected profile.
let builder = Client::builder()
.http_client(http)
.sqlite_store(settings.sqlite_dir.as_path(), None)
.request_config(req_config);
let builder = if let Some(url) = homeserver {
// Use the explicitly specified homeserver.
builder.homeserver_url(url.as_str())
} else {
// Try to discover the homeserver from the user ID.
let account = &settings.profile;
builder.server_name(account.user_id.server_name())
};
builder.build().await
}
pub async fn create_client(settings: &ApplicationSettings) -> Client {
let account = &settings.profile;
let res = match create_client_inner(&account.url, settings).await {
Err(ClientBuildError::AutoDiscovery(_)) => {
let url = format!("https://{}/", account.user_id.server_name().as_str());
let url = Url::parse(&url).unwrap();
create_client_inner(&Some(url), settings).await
},
res => res,
};
res.expect("Failed to instantiate client")
}
#[derive(Clone)] #[derive(Clone)]
pub struct Requester { pub struct Requester {
pub client: Client, pub client: Client,
@ -649,7 +723,7 @@ impl Requester {
return response.recv(); return response.recv();
} }
pub fn get_inviter(&self, invite: Invited) -> IambResult<Option<RoomMember>> { pub fn get_inviter(&self, invite: MatrixRoom) -> IambResult<Option<RoomMember>> {
let (reply, response) = oneshot(); let (reply, response) = oneshot();
self.tx.send(WorkerTask::GetInviter(invite, reply)).unwrap(); self.tx.send(WorkerTask::GetInviter(invite, reply)).unwrap();
@ -719,40 +793,8 @@ pub struct ClientWorker {
} }
impl ClientWorker { impl ClientWorker {
pub async fn spawn(settings: ApplicationSettings) -> Requester { pub async fn spawn(client: Client, settings: ApplicationSettings) -> Requester {
let (tx, rx) = unbounded_channel(); let (tx, rx) = unbounded_channel();
let account = &settings.profile;
let req_timeout = Duration::from_secs(settings.tunables.request_timeout);
// Set up the HTTP client.
let http = reqwest::Client::builder()
.user_agent(IAMB_USER_AGENT)
.timeout(req_timeout)
.pool_idle_timeout(Duration::from_secs(60))
.pool_max_idle_per_host(10)
.tcp_keepalive(Duration::from_secs(10))
.build()
.unwrap();
let req_config = RequestConfig::new().timeout(req_timeout).retry_timeout(req_timeout);
// Set up the Matrix client for the selected profile.
let builder = Client::builder()
.http_client(Arc::new(http))
.sled_store(settings.matrix_dir.as_path(), None)
.expect("Failed to setup up sled store for Matrix SDK")
.request_config(req_config);
let builder = if let Some(url) = account.url.as_ref() {
// Use the explicitly specified homeserver.
builder.homeserver_url(url.as_str())
} else {
// Try to discover the homeserver from the user ID.
builder.server_name(account.user_id.server_name())
};
let client = builder.build().await.expect("Failed to instantiate Matrix client");
let mut worker = ClientWorker { let mut worker = ClientWorker {
initialized: false, initialized: false,
@ -872,15 +914,13 @@ impl ClientWorker {
store: Ctx<AsyncProgramStore>| { store: Ctx<AsyncProgramStore>| {
async move { async move {
if let SyncStateEvent::Original(ev) = ev { if let SyncStateEvent::Original(ev) = ev {
if let Some(room_name) = ev.content.name {
let room_id = room.room_id().to_owned(); let room_id = room.room_id().to_owned();
let room_name = Some(room_name.to_string()); let room_name = Some(ev.content.name);
let mut locked = store.lock().await; let mut locked = store.lock().await;
let info = locked.application.rooms.get_or_default(room_id.clone()); let info = locked.application.rooms.get_or_default(room_id.clone());
info.name = room_name; info.name = room_name;
} }
} }
}
}, },
); );
@ -980,7 +1020,11 @@ impl ClientWorker {
let mut locked = store.lock().await; let mut locked = store.lock().await;
let info = locked.application.get_room_info(room_id.to_owned()); let info = locked.application.get_room_info(room_id.to_owned());
match info.keys.get(&ev.redacts) { let Some(redacts) = &ev.redacts else {
return;
};
match info.keys.get(redacts) {
None => return, None => return,
Some(EventLocation::Message(key)) => { Some(EventLocation::Message(key)) => {
if let Some(msg) = info.messages.get_mut(key) { if let Some(msg) = info.messages.get_mut(key) {
@ -990,10 +1034,10 @@ impl ClientWorker {
}, },
Some(EventLocation::Reaction(event_id)) => { Some(EventLocation::Reaction(event_id)) => {
if let Some(reactions) = info.reactions.get_mut(event_id) { if let Some(reactions) = info.reactions.get_mut(event_id) {
reactions.remove(&ev.redacts); reactions.remove(redacts);
} }
info.keys.remove(&ev.redacts); info.keys.remove(redacts);
}, },
} }
} }
@ -1165,22 +1209,22 @@ impl ClientWorker {
match style { match style {
LoginStyle::SessionRestore(session) => { LoginStyle::SessionRestore(session) => {
client.restore_login(session).await.map_err(IambError::from)?; client.restore_session(session).await.map_err(IambError::from)?;
}, },
LoginStyle::Password(password) => { LoginStyle::Password(password) => {
let resp = client let resp = client
.matrix_auth()
.login_username(&self.settings.profile.user_id, &password) .login_username(&self.settings.profile.user_id, &password)
.initial_device_display_name(initial_devname().as_str()) .initial_device_display_name(initial_devname().as_str())
.send() .send()
.await .await
.map_err(IambError::from)?; .map_err(IambError::from)?;
let file = File::create(self.settings.session_json.as_path())?; let session = MatrixSession::from(&resp);
let writer = BufWriter::new(file); self.settings.write_session(session)?;
let session = Session::from(resp);
serde_json::to_writer(writer, &session).map_err(IambError::from)?;
}, },
LoginStyle::SingleSignOn => { LoginStyle::SingleSignOn => {
let resp = client let resp = client
.matrix_auth()
.login_sso(|url| { .login_sso(|url| {
let opened = format!( let opened = format!(
"The following URL should have been opened in your browser:\n {url}" "The following URL should have been opened in your browser:\n {url}"
@ -1197,10 +1241,8 @@ impl ClientWorker {
.await .await
.map_err(IambError::from)?; .map_err(IambError::from)?;
let file = File::create(self.settings.session_json.as_path())?; let session = MatrixSession::from(&resp);
let writer = BufWriter::new(file); self.settings.write_session(session)?;
let session = Session::from(resp);
serde_json::to_writer(writer, &session).map_err(IambError::from)?;
}, },
} }
@ -1213,7 +1255,7 @@ impl ClientWorker {
}) })
.into(); .into();
Ok(Some(InfoMessage::from("Successfully logged in!"))) Ok(Some(InfoMessage::from("* Successfully logged in!")))
} }
async fn logout(&mut self, user_id: String) -> IambResult<EditInfo> { async fn logout(&mut self, user_id: String) -> IambResult<EditInfo> {
@ -1228,7 +1270,7 @@ impl ClientWorker {
} }
// Send the logout request. // Send the logout request.
if let Err(e) = self.client.logout().await { if let Err(e) = self.client.matrix_auth().logout().await {
let msg = format!("Failed to logout: {e}"); let msg = format!("Failed to logout: {e}");
let err = UIError::Failure(msg); let err = UIError::Failure(msg);
@ -1243,7 +1285,7 @@ impl ClientWorker {
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<OwnedRoomId> { async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<OwnedRoomId> {
for room in self.client.rooms() { for room in self.client.rooms() {
if !room.is_direct() { if !is_direct(&room).await {
continue; continue;
} }
@ -1267,7 +1309,7 @@ impl ClientWorker {
}) })
} }
async fn get_inviter(&mut self, invited: Invited) -> IambResult<Option<RoomMember>> { async fn get_inviter(&mut self, invited: MatrixRoom) -> IambResult<Option<RoomMember>> {
let details = invited.invite_details().await.map_err(IambError::from)?; let details = invited.invite_details().await.map_err(IambError::from)?;
Ok(details.inviter) Ok(details.inviter)
@ -1287,7 +1329,7 @@ impl ClientWorker {
async fn join_room(&mut self, name: String) -> IambResult<OwnedRoomId> { async fn join_room(&mut self, name: String) -> IambResult<OwnedRoomId> {
if let Ok(alias_id) = OwnedRoomOrAliasId::from_str(name.as_str()) { if let Ok(alias_id) = OwnedRoomOrAliasId::from_str(name.as_str()) {
match self.client.join_room_by_id_or_alias(&alias_id, &[]).await { match self.client.join_room_by_id_or_alias(&alias_id, &[]).await {
Ok(resp) => Ok(resp.room_id), Ok(resp) => Ok(resp.room_id().to_owned()),
Err(e) => { Err(e) => {
let msg = e.to_string(); let msg = e.to_string();
let err = UIError::Failure(msg); let err = UIError::Failure(msg);
@ -1307,14 +1349,14 @@ impl ClientWorker {
async fn members(&mut self, room_id: OwnedRoomId) -> IambResult<Vec<RoomMember>> { async fn members(&mut self, room_id: OwnedRoomId) -> IambResult<Vec<RoomMember>> {
if let Some(room) = self.client.get_room(room_id.as_ref()) { if let Some(room) = self.client.get_room(room_id.as_ref()) {
Ok(room.active_members().await.map_err(IambError::from)?) Ok(room.members(RoomMemberships::ACTIVE).await.map_err(IambError::from)?)
} else { } else {
Err(IambError::UnknownRoom(room_id).into()) Err(IambError::UnknownRoom(room_id).into())
} }
} }
async fn space_members(&mut self, space: OwnedRoomId) -> IambResult<Vec<OwnedRoomId>> { async fn space_members(&mut self, space: OwnedRoomId) -> IambResult<Vec<OwnedRoomId>> {
let mut req = SpaceHierarchyRequest::new(&space); let mut req = SpaceHierarchyRequest::new(space);
req.limit = Some(1000u32.into()); req.limit = Some(1000u32.into());
req.max_depth = Some(1u32.into()); req.max_depth = Some(1u32.into());
@ -1326,7 +1368,7 @@ impl ClientWorker {
} }
async fn typing_notice(&mut self, room_id: OwnedRoomId) { async fn typing_notice(&mut self, room_id: OwnedRoomId) {
if let Some(room) = self.client.get_joined_room(room_id.as_ref()) { if let Some(room) = self.client.get_room(room_id.as_ref()) {
let _ = room.typing_notice(true).await; let _ = room.typing_notice(true).await;
} }
} }