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
with:
submodules: true
- name: Install Rust (1.67 w/ clippy)
uses: dtolnay/rust-toolchain@1.67
- name: Install Rust (1.70 w/ clippy)
uses: dtolnay/rust-toolchain@1.70
with:
components: clippy
- 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"]
keywords = ["matrix", "chat", "tui", "vim"]
categories = ["command-line-utilities"]
rust-version = "1.67"
rust-version = "1.70"
build = "build.rs"
[build-dependencies]
@ -40,12 +40,15 @@ markup5ever_rcdom = "0.2.0"
mime = "^0.3.16"
mime_guess = "^2.0.4"
open = "3.2.0"
rand = "0.8.5"
ratatui = "0.23"
ratatui-image = { version = "0.4.3", features = ["serde"] }
regex = "^1.5"
rpassword = "^7.2"
serde = "^1.0"
serde_json = "^1.0"
sled = "0.34.7"
temp-dir = "0.1.12"
thiserror = "^1.0.37"
tracing = "~0.1.36"
tracing-appender = "~0.2.2"
@ -66,9 +69,9 @@ git = "https://github.com/ulyssa/modalkit"
rev = "cb8c8aeb9a499b9b16615ce144f9014d78036e01"
[dependencies.matrix-sdk]
version = "^0.6.2"
version = "0.7.1"
default-features = false
features = ["e2e-encryption", "sled", "rustls-tls", "sso-login"]
features = ["e2e-encryption", "rustls-tls", "bundled-sqlite", "sso-login"]
[dependencies.tokio]
version = "1.24.1"

View file

@ -22,7 +22,7 @@ website, [iamb.chat].
## 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

View file

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

View file

@ -5,20 +5,29 @@ use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::BufReader;
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};
use std::process;
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::text::Span;
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 url::Url;
use super::base::{IambId, RoomInfo, SortColumn, SortFieldRoom, SortFieldUser, SortOrder};
use super::base::{
IambError,
IambId,
RoomInfo,
SortColumn,
SortFieldRoom,
SortFieldUser,
SortOrder,
};
macro_rules! usage {
( $($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)]
pub struct UserDisplayTunables {
pub color: Option<UserColor>,
@ -421,14 +464,35 @@ impl Tunables {
#[derive(Clone)]
pub struct DirectoryValues {
pub cache: PathBuf,
pub data: PathBuf,
pub logs: PathBuf,
pub downloads: Option<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)]
pub struct Directories {
pub cache: Option<PathBuf>,
pub data: Option<PathBuf>,
pub logs: Option<PathBuf>,
pub downloads: Option<PathBuf>,
pub image_previews: Option<PathBuf>,
@ -438,6 +502,7 @@ impl Directories {
fn merge(self, other: Self) -> Self {
Directories {
cache: self.cache.or(other.cache),
data: self.data.or(other.data),
logs: self.logs.or(other.logs),
downloads: self.downloads.or(other.downloads),
image_previews: self.image_previews.or(other.image_previews),
@ -454,6 +519,15 @@ impl Directories {
})
.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 mut dir = cache.clone();
dir.push("logs");
@ -468,7 +542,7 @@ impl Directories {
dir
});
DirectoryValues { cache, logs, downloads, image_previews }
DirectoryValues { cache, data, logs, downloads, image_previews }
}
}
@ -540,9 +614,11 @@ impl IambConfig {
#[derive(Clone)]
pub struct ApplicationSettings {
pub matrix_dir: PathBuf,
pub layout_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: ProfileConfig,
pub tunables: TunableValues,
@ -602,17 +678,30 @@ impl ApplicationSettings {
let dirs = profile.dirs.take().unwrap_or_default().merge(dirs);
let dirs = dirs.values();
// Create directories
dirs.create_dir_all()?;
// Set up paths that live inside the profile's data directory.
let mut profile_dir = config_dir.clone();
profile_dir.push("profiles");
profile_dir.push(profile_name.as_str());
let mut matrix_dir = profile_dir.clone();
matrix_dir.push("matrix");
let mut profile_data_dir = dirs.data.clone();
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");
let mut session_json_old = profile_dir;
session_json_old.push("session.json");
// Set up paths that live inside the profile's cache directory.
let mut cache_dir = dirs.cache.clone();
cache_dir.push("profiles");
@ -622,9 +711,11 @@ impl ApplicationSettings {
layout_json.push("layout.json");
let settings = ApplicationSettings {
matrix_dir,
sled_dir,
layout_json,
session_json,
session_json_old,
sqlite_dir,
profile_name,
profile,
tunables,
@ -635,6 +726,21 @@ impl ApplicationSettings {
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> {
let (color, c) = self
.tunables

View file

@ -19,7 +19,7 @@ use std::collections::VecDeque;
use std::convert::TryFrom;
use std::fmt::Display;
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::process;
use std::sync::atomic::{AtomicUsize, Ordering};
@ -27,7 +27,10 @@ use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use matrix_sdk::crypto::encrypt_room_key_export;
use matrix_sdk::ruma::OwnedUserId;
use rand::{distributions::Alphanumeric, Rng};
use temp_dir::TempDir;
use tokio::sync::Mutex as AsyncMutex;
use tracing_subscriber::FmtSubscriber;
@ -62,6 +65,7 @@ mod config;
mod keybindings;
mod message;
mod preview;
mod sled_export;
mod util;
mod windows;
mod worker;
@ -558,7 +562,7 @@ impl Application {
match action {
HomeserverAction::CreateRoom(alias, vis, flags) => {
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 target = OpenTarget::Application(room);
let action = WindowAction::Switch(target);
@ -659,26 +663,47 @@ impl Application {
}
}
async fn login(worker: Requester, settings: &ApplicationSettings) -> IambResult<()> {
println!("Logging in for {}...", settings.profile.user_id);
fn gen_passphrase() -> String {
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() {
let file = File::open(settings.session_json.as_path())?;
let reader = BufReader::new(file);
let session = serde_json::from_reader(reader).map_err(IambError::from)?;
let session = settings.read_session(&settings.session_json)?;
worker.login(LoginStyle::SessionRestore(session.into()))?;
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(());
}
loop {
println!("Please select login type: [p]assword / [s]ingle sign on");
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let login_style = match input.chars().next().map(|c| c.to_ascii_lowercase()) {
let login_style =
match read_response("Please select login type: [p]assword / [s]ingle sign on")
.chars()
.next()
.map(|c| c.to_ascii_lowercase())
{
None | Some('p') => {
let password = rpassword::prompt_password("Password: ")?;
LoginStyle::Password(password)
@ -713,17 +738,142 @@ fn print_exit<T: Display, N>(v: T) -> N {
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<()> {
// 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.
let worker = ClientWorker::spawn(settings.clone()).await;
let client = worker.client.clone();
let worker = ClientWorker::spawn(client.clone(), settings.clone()).await;
let store = ChatStore::new(worker.clone(), settings.clone());
let store = Store::new(store);
let store = Arc::new(AsyncMutex::new(store));
worker.init(store.clone());
login(worker, &settings).await.unwrap_or_else(print_exit);
worker::do_first_sync(client, &store).await;
if let Some((keydir, pass)) = import_keys {
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() {
let _ = crossterm::terminal::disable_raw_mode();
@ -767,10 +917,6 @@ fn main() -> IambResult<()> {
let log_prefix = format!("iamb-log-{}", settings.profile_name);
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, 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 comrak::{markdown_to_html, ComrakOptions};
use serde_json::json;
use unicode_width::UnicodeWidthStr;
use matrix_sdk::ruma::{
@ -33,7 +34,7 @@ use matrix_sdk::ruma::{
redaction::SyncRoomRedactionEvent,
},
AnyMessageLikeEvent,
Redact,
RedactContent,
RedactedUnsigned,
},
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)]
pub enum MessageEvent {
EncryptedOriginal(Box<OriginalRoomEncryptedEvent>),
@ -419,7 +442,14 @@ impl MessageEvent {
MessageEvent::Redacted(_) => return,
MessageEvent::Local(_, _) => return,
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));
},
}
@ -455,11 +485,7 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
}
fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> {
let reason = unsigned
.redacted_because
.as_ref()
.and_then(|e| e.as_original())
.and_then(|r| r.content.reason.as_ref());
let reason = unsigned.redacted_because.content.reason.as_ref();
if let Some(r) = reason {
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 {
DirectoryValues {
cache: PathBuf::new(),
data: PathBuf::new(),
logs: PathBuf::new(),
downloads: None,
image_previews: PathBuf::new(),
@ -202,9 +203,11 @@ pub fn mock_tunables() -> TunableValues {
pub fn mock_settings() -> ApplicationSettings {
ApplicationSettings {
matrix_dir: PathBuf::new(),
layout_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: ProfileConfig {

View file

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

View file

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

View file

@ -5,8 +5,7 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::{Debug, Formatter};
use std::fs::File;
use std::io::BufWriter;
use std::ops::Deref;
use std::str::FromStr;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::Arc;
@ -17,13 +16,15 @@ use gethostname::gethostname;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use tokio::task::JoinHandle;
use tracing::{error, warn};
use url::Url;
use matrix_sdk::{
config::{RequestConfig, SyncSettings},
encryption::verification::{SasVerification, Verification},
event_handler::Ctx,
matrix_auth::MatrixSession,
reqwest,
room::{Invited, Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
room::{Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
ruma::{
api::client::{
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
@ -42,7 +43,8 @@ use matrix_sdk::{
},
presence::PresenceEvent,
reaction::ReactionEventContent,
receipt::{ReceiptEventContent, ReceiptType},
receipt::ReceiptType,
receipt::{ReceiptEventContent, ReceiptThread},
room::{
encryption::RoomEncryptionEventContent,
member::OriginalSyncRoomMemberEvent,
@ -73,8 +75,9 @@ use matrix_sdk::{
RoomVersionId,
},
Client,
ClientBuildError,
DisplayName,
Session,
RoomMemberships,
};
use modalkit::errors::UIError;
@ -106,9 +109,13 @@ fn initial_devname() -> String {
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(
client: &Client,
room_alias_name: Option<&str>,
room_alias_name: Option<String>,
rt: CreateRoomType,
flags: CreateRoomFlags,
) -> IambResult<OwnedRoomId> {
@ -154,8 +161,8 @@ pub async fn create_room(
let request = assign!(CreateRoomRequest::new(), {
room_alias_name,
creation_content,
initial_state: initial_state.as_slice(),
invite: invite.as_slice(),
initial_state,
invite,
is_direct,
visibility,
preset,
@ -164,27 +171,31 @@ pub async fn create_room(
let resp = client.create_room(request).await.map_err(IambError::from)?;
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)?;
} else {
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"
);
}
}
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) {
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,
Err(e) => {
tracing::warn!(?event_id, "failed to get event receipts: {e}");
return;
},
};
for (user_id, _) in receipts {
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>> {
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 {
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));
if room.is_direct() {
dms.push(Arc::new((room.into(), tags)));
if is_direct(&room).await {
dms.push(Arc::new((room, tags)));
} else if room.is_space() {
spaces.push(Arc::new((room.into(), tags)));
spaces.push(Arc::new((room, tags)));
} 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));
if room.is_direct() {
dms.push(Arc::new((room.into(), tags)));
if is_direct(&room).await {
dms.push(Arc::new((room, tags)));
} else if room.is_space() {
spaces.push(Arc::new((room.into(), tags)));
spaces.push(Arc::new((room, tags)));
} 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);
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;
};
match room.read_receipt(&new_receipt).await {
match room
.send_single_receipt(
ReceiptType::Read,
ReceiptThread::Unthreaded,
new_receipt.clone(),
)
.await
{
Ok(()) => {
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.
let mut room = RoomEventFilter::default();
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.
refresh_rooms(&client, store).await;
refresh_rooms(client, store).await;
// Insert Need::Messages to fetch accurate recent timestamps in the background.
let mut locked = store.lock().await;
@ -509,7 +533,7 @@ pub async fn do_first_sync(client: Client, store: &AsyncProgramStore) {
#[derive(Debug)]
pub enum LoginStyle {
SessionRestore(Session),
SessionRestore(MatrixSession),
Password(String),
SingleSignOn,
}
@ -543,7 +567,7 @@ pub enum WorkerTask {
Init(AsyncProgramStore, ClientReply<()>),
Login(LoginStyle, 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>>),
JoinRoom(String, ClientReply<IambResult<OwnedRoomId>>),
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)]
pub struct Requester {
pub client: Client,
@ -649,7 +723,7 @@ impl Requester {
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();
self.tx.send(WorkerTask::GetInviter(invite, reply)).unwrap();
@ -719,40 +793,8 @@ pub struct 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 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 {
initialized: false,
@ -872,15 +914,13 @@ impl ClientWorker {
store: Ctx<AsyncProgramStore>| {
async move {
if let SyncStateEvent::Original(ev) = ev {
if let Some(room_name) = ev.content.name {
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 info = locked.application.rooms.get_or_default(room_id.clone());
info.name = room_name;
}
}
}
},
);
@ -980,7 +1020,11 @@ impl ClientWorker {
let mut locked = store.lock().await;
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,
Some(EventLocation::Message(key)) => {
if let Some(msg) = info.messages.get_mut(key) {
@ -990,10 +1034,10 @@ impl ClientWorker {
},
Some(EventLocation::Reaction(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 {
LoginStyle::SessionRestore(session) => {
client.restore_login(session).await.map_err(IambError::from)?;
client.restore_session(session).await.map_err(IambError::from)?;
},
LoginStyle::Password(password) => {
let resp = client
.matrix_auth()
.login_username(&self.settings.profile.user_id, &password)
.initial_device_display_name(initial_devname().as_str())
.send()
.await
.map_err(IambError::from)?;
let file = File::create(self.settings.session_json.as_path())?;
let writer = BufWriter::new(file);
let session = Session::from(resp);
serde_json::to_writer(writer, &session).map_err(IambError::from)?;
let session = MatrixSession::from(&resp);
self.settings.write_session(session)?;
},
LoginStyle::SingleSignOn => {
let resp = client
.matrix_auth()
.login_sso(|url| {
let opened = format!(
"The following URL should have been opened in your browser:\n {url}"
@ -1197,10 +1241,8 @@ impl ClientWorker {
.await
.map_err(IambError::from)?;
let file = File::create(self.settings.session_json.as_path())?;
let writer = BufWriter::new(file);
let session = Session::from(resp);
serde_json::to_writer(writer, &session).map_err(IambError::from)?;
let session = MatrixSession::from(&resp);
self.settings.write_session(session)?;
},
}
@ -1213,7 +1255,7 @@ impl ClientWorker {
})
.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> {
@ -1228,7 +1270,7 @@ impl ClientWorker {
}
// 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 err = UIError::Failure(msg);
@ -1243,7 +1285,7 @@ impl ClientWorker {
async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<OwnedRoomId> {
for room in self.client.rooms() {
if !room.is_direct() {
if !is_direct(&room).await {
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)?;
Ok(details.inviter)
@ -1287,7 +1329,7 @@ impl ClientWorker {
async fn join_room(&mut self, name: String) -> IambResult<OwnedRoomId> {
if let Ok(alias_id) = OwnedRoomOrAliasId::from_str(name.as_str()) {
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) => {
let msg = e.to_string();
let err = UIError::Failure(msg);
@ -1307,14 +1349,14 @@ impl ClientWorker {
async fn members(&mut self, room_id: OwnedRoomId) -> IambResult<Vec<RoomMember>> {
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 {
Err(IambError::UnknownRoom(room_id).into())
}
}
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.max_depth = Some(1u32.into());
@ -1326,7 +1368,7 @@ impl ClientWorker {
}
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;
}
}