From f7e4bf178b2a60dbacf3d1c89a402806f25dafee Mon Sep 17 00:00:00 2001 From: Thoq Date: Fri, 9 Oct 2026 18:18:49 -0400 Subject: [PATCH] new: setup keyring and rage-format this shitaah formatting --- Cargo.lock | 139 ++++++++++++++++++++++++++ Cargo.toml | 1 + src/backend/credentials.rs | 1 + src/backend/format.rs | 7 +- src/backend/mod.rs | 196 ++++++++++++++++++++++++++----------- src/backend/navigation.rs | 136 ++++++++++++++++++------- src/backend/room.rs | 188 +++++++++++++++++++++++++---------- src/backend/session.rs | 171 +++++++++++++++++++++++++------- src/helpers/mod.rs | 2 +- src/helpers/system.rs | 10 +- src/main.rs | 8 +- src/model.rs | 77 ++++++++++----- 12 files changed, 718 insertions(+), 218 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a684a5..6d7519c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,6 +256,17 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "apple-native-keyring-store" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a" +dependencies = [ + "keyring-core", + "log", + "security-framework", +] + [[package]] name = "arboard" version = "3.6.1" @@ -3395,6 +3406,27 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyring" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2270074a3d26bcac93c1dc5d2845eb4c089e8d761ccf6e0ea266a16004640627" +dependencies = [ + "apple-native-keyring-store", + "keyring-core", + "windows-native-keyring-store", + "zbus-secret-service-keyring-store", +] + +[[package]] +name = "keyring-core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" +dependencies = [ + "log", +] + [[package]] name = "khronos_api" version = "3.1.0" @@ -4313,12 +4345,75 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -5962,6 +6057,25 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "secret-service" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5107b24b91445dd2aa449a258a1807b63240942157292354dc5bfdbeb8bc6db8" +dependencies = [ + "aes", + "cbc", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hybrid-array", + "num", + "once_cell", + "serde", + "sha2 0.11.0", + "zbus", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -6829,6 +6943,7 @@ dependencies = [ "eyeball-im", "futures-util", "hex", + "keyring", "matrix-sdk", "matrix-sdk-ui", "rand 0.10.3", @@ -7900,6 +8015,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-native-keyring-store" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8" +dependencies = [ + "byteorder", + "keyring-core", + "regex", + "windows-sys 0.61.2", + "zeroize", +] + [[package]] name = "windows-numerics" version = "0.3.1" @@ -8396,6 +8524,17 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zbus-secret-service-keyring-store" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74801d001b9e7729adb4f1825b67b398185fed424749aa3d8bacf70417137d9a" +dependencies = [ + "keyring-core", + "secret-service", + "zbus", +] + [[package]] name = "zbus_macros" version = "5.19.0" diff --git a/Cargo.toml b/Cargo.toml index b0f758d..7b85b93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ rand = "0.10.3" hex = "0.4.3" dotenvy = "0.15.7" +keyring = "4.2.0" [build-dependencies] slint-build = "1.18.1" diff --git a/src/backend/credentials.rs b/src/backend/credentials.rs index e69de29..8b13789 100644 --- a/src/backend/credentials.rs +++ b/src/backend/credentials.rs @@ -0,0 +1 @@ + diff --git a/src/backend/format.rs b/src/backend/format.rs index ed6e712..d170479 100644 --- a/src/backend/format.rs +++ b/src/backend/format.rs @@ -2,7 +2,10 @@ use chrono::{DateTime, Datelike, Local, TimeZone}; use matrix_sdk::ruma::MilliSecondsSinceUnixEpoch; pub fn to_local(timestamp: MilliSecondsSinceUnixEpoch) -> DateTime { let ms: i64 = i64::try_from(u64::from(timestamp.0)).unwrap_or(0); - Local.timestamp_millis_opt(ms).single().unwrap_or_else(Local::now) + Local + .timestamp_millis_opt(ms) + .single() + .unwrap_or_else(Local::now) } /// "Today at 13:00" / "Yesterday at 13:00" / "2026-10-09 13:00" @@ -40,4 +43,4 @@ pub fn truncate(string: &str, max_characters: usize) -> String { } else { flat } -} \ No newline at end of file +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 6c589a7..a739f0f 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,29 +1,37 @@ -mod format; -mod session; -mod room; -mod navigation; mod credentials; +mod format; +mod navigation; +mod room; +mod session; +use anyhow::{Context, Result, anyhow}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use anyhow::{Context, Result, anyhow}; use std::time::{Duration, Instant}; -use matrix_sdk::{Client, Room, RoomState, SessionChange, ruma::{ - EventId, OwnedRoomId, OwnedTransactionId, RoomId, RoomOrAliasId, OwnedUserId, - events::room::message::{RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, -}, room::edit::EditedContent, OwnedServerName}; +use matrix_sdk::{ + Client, OwnedServerName, Room, RoomState, SessionChange, + room::edit::EditedContent, + ruma::{ + EventId, OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, RoomOrAliasId, + events::room::message::{RoomMessageEventContent, RoomMessageEventContentWithoutRelation}, + }, +}; -use matrix_sdk_ui::{sync_service::{State as SyncState, SyncService}, spaces::SpaceService, timeline::TimelineEventItemId, Timeline}; +use crate::backend::room::OpenRoom; +use crate::backend::session::SavedSession; +use crate::model::{Command, Event, UserInfo}; +use matrix_sdk_ui::{ + Timeline, + spaces::SpaceService, + sync_service::{State as SyncState, SyncService}, + timeline::TimelineEventItemId, +}; use tokio::{ sync::{mpsc, watch}, task::JoinHandle, }; -use crate::backend::room::OpenRoom; -use crate::backend::session::SavedSession; -use crate::model::{Command, Event, UserInfo}; - /// How the backend delivers events. The UI layer provides this. pub type Sink = Arc; @@ -62,7 +70,7 @@ struct Backend { /// Tasks that live as long as the login does. background: Vec, - open: Option + open: Option, } impl Backend { @@ -75,7 +83,7 @@ impl Backend { selected_tx: watch::channel(None).0, via: Arc::new(Mutex::new(HashMap::new())), background: Vec::new(), - open: None + open: None, } } @@ -89,7 +97,7 @@ impl Backend { while let Some(command) = rx.recv().await { if let Err(error) = self.handle(command).await { tracing::error!("Command failed: {error:#}"); - self.emit(Event::Toast(format!("{error:#}"))) + self.emit(Event::Toast(format!("{error:#}"))); } } } @@ -97,10 +105,14 @@ impl Backend { // Session async fn try_restore(&mut self) { - let saved: SavedSession = match session::load() { + let saved: SavedSession = match session::load().await { Ok(Some(session)) => session, Ok(None) => return self.emit(Event::NeedLogin { error: None }), - Err(error) => return self.emit(Event::NeedLogin { error: Some(format!("{error:#}")) }) + Err(error) => { + return self.emit(Event::NeedLogin { + error: Some(format!("{error:#}")), + }); + } }; match session::restore(&saved).await { @@ -108,13 +120,17 @@ impl Backend { self.store_passphrase = saved.store_passphrase; if let Err(error) = self.on_logged_in(client).await { - self.emit(Event::NeedLogin { error: Some(format!("{error:#}")) }) + self.emit(Event::NeedLogin { + error: Some(format!("{error:#}")), + }); } } Err(error) => { tracing::warn!("Restoration of session failed: {error:#}"); - self.emit(Event::NeedLogin { error: Some("Couldn't restore your last session. Please sign in again.".into()) }) + self.emit(Event::NeedLogin { + error: Some("Couldn't restore your last session. Please sign in again.".into()), + }); } } } @@ -126,12 +142,15 @@ impl Backend { let (client, passphrase) = session::login(homeserver, username, password).await?; self.store_passphrase = passphrase; self.on_logged_in(client).await - }.await; + } + .await; self.emit(Event::LoginBusy(false)); if let Err(error) = result { - self.emit(Event::NeedLogin { error: Some(format!("{error:#}")) }) + self.emit(Event::NeedLogin { + error: Some(format!("{error:#}")), + }); } } @@ -148,31 +167,54 @@ impl Backend { let _ = client.matrix_auth().logout().await; } - session::clear(); + // yet again, fuck you + let _ = session::clear().await; self.selected_tx.send_replace(None); self.emit(Event::NeedLogin { error: None }); } async fn on_logged_in(&mut self, client: Client) -> Result<()> { - let id: String = client.user_id().map(ToString::to_string).unwrap_or_default(); - let name: String = client.account() + let id: String = client.user_id().map_or_default(ToString::to_string); + let name: String = client + .account() .get_display_name() .await .ok() .flatten() - .unwrap_or_else(|| id.trim_start_matches('@').split(':').next().unwrap_or("").to_owned()); + .unwrap_or_else(|| { + id.trim_start_matches('@') + .split(':') + .next() + .unwrap_or("") + .to_owned() + }); self.emit(Event::LoggedIn(UserInfo { id, name })); - let sync: Arc = Arc::new(SyncService::builder(client.clone()).build().await.context("Couldn't start sync (does homeserver support sliding sync?)")?); + let sync: Arc = Arc::new( + SyncService::builder(client.clone()) + .build() + .await + .context("Couldn't start sync (does homeserver support sliding sync?)")?, + ); sync.start().await; let spaces: Arc = Arc::new(SpaceService::new(client.clone()).await); self.background = vec![ - Task::spawn(navigation::run(client.clone(), spaces, self.selected_tx.subscribe(), self.emit.clone(), self.via.clone())), - Task::spawn(watch_session(client.clone(), self.store_passphrase.clone(), self.emit.clone())), + Task::spawn(navigation::run( + client.clone(), + spaces, + self.selected_tx.subscribe(), + self.emit.clone(), + self.via.clone(), + )), + Task::spawn(watch_session( + client.clone(), + self.store_passphrase.clone(), + self.emit.clone(), + )), Task::spawn(watch_sync(sync.clone(), self.emit.clone())), ]; @@ -187,12 +229,19 @@ impl Backend { async fn handle(&mut self, command: Command) -> Result<()> { match command { - Command::Login { homeserver, username, password} => self.login(&homeserver, &username, &password).await, + Command::Login { + homeserver, + username, + password, + } => self.login(&homeserver, &username, &password).await, Command::Logout => self.logout().await, Command::SelectServer(id) => { - let parsed: Option = id.filter(|string| !string.is_empty()).map(RoomId::parse).transpose()?; + let parsed: Option = id + .filter(|string| !string.is_empty()) + .map(RoomId::parse) + .transpose()?; self.selected_tx.send_replace(parsed); } @@ -202,8 +251,11 @@ impl Backend { Command::Edit { id, text } => { let item: TimelineEventItemId = parse_item_id(&id)?; - let content: RoomMessageEventContentWithoutRelation = RoomMessageEventContentWithoutRelation::text_markdown(text); - self.timeline()?.edit(&item, EditedContent::RoomMessage(content)).await?; + let content: RoomMessageEventContentWithoutRelation = + RoomMessageEventContentWithoutRelation::text_markdown(text); + self.timeline()? + .edit(&item, EditedContent::RoomMessage(content)) + .await?; } Command::Delete { id } => { @@ -211,12 +263,19 @@ impl Backend { } Command::ToggleReaction { id, key } => { - self.timeline()?.toggle_reaction(&parse_item_id(&id)?, &key).await?; + self.timeline()? + .toggle_reaction(&parse_item_id(&id)?, &key) + .await?; } Command::LoadOlder => { if let Some(open) = &self.open { - tokio::spawn(room::load_older(open.timeline.clone(), open.loading_older.clone(), open.reached_start.clone(), self.emit.clone())); + tokio::spawn(room::load_older( + open.timeline.clone(), + open.loading_older.clone(), + open.reached_start.clone(), + self.emit.clone(), + )); } } @@ -226,25 +285,36 @@ impl Backend { Ok(()) } - fn timeline(&self) -> Result> { - self.open.as_ref().map(|open| open.timeline.clone()).ok_or_else(|| anyhow!("No room is open.")) + fn timeline(&self) -> Result> { + self.open + .as_ref() + .map(|open| open.timeline.clone()) + .ok_or_else(|| anyhow!("No room is open.")) } async fn select_room(&mut self, room_id: &str) -> Result<()> { - let client: Client = self.client.clone().ok_or_else(|| anyhow!("Not signed in."))?; + let client: Client = self + .client + .clone() + .ok_or_else(|| anyhow!("Not signed in."))?; let room_id: OwnedRoomId = RoomId::parse(room_id)?; let room: Room = match client.get_room(&room_id) { Some(room) if room.state() == RoomState::Joined => room, - _ => self.join(&client, &room_id).await? + _ => self.join(&client, &room_id).await?, }; if let Some(sync) = &self.sync { - sync.room_list_service().set_room_subscriptions(&[room.room_id()]).await; + sync.room_list_service() + .set_room_subscriptions(&[room.room_id()]) + .await; } self.open = None; - let own: OwnedUserId = client.user_id().ok_or_else(|| anyhow!("No user id"))?.to_owned(); + let own: OwnedUserId = client + .user_id() + .ok_or_else(|| anyhow!("No user id"))? + .to_owned(); self.open = Some(room::open(room, own, self.emit.clone()).await?); Ok(()) @@ -253,7 +323,12 @@ impl Backend { async fn join(&mut self, client: &Client, room_id: &OwnedRoomId) -> Result { self.emit(Event::Toast("Joining...".into())); - let via: Vec = self.via.lock().ok().and_then(|m| m.get(room_id).cloned()).unwrap_or_default(); + let via: Vec = self + .via + .lock() + .ok() + .and_then(|m| m.get(room_id).cloned()) + .unwrap_or_default(); let room_id: &RoomId = room_id; let target: &RoomOrAliasId = <&RoomOrAliasId>::from(room_id); @@ -271,11 +346,16 @@ impl Backend { match reply_to.filter(|str| !str.is_empty()) { Some(event_id) => { - let content: RoomMessageEventContentWithoutRelation = RoomMessageEventContentWithoutRelation::text_markdown(text); - timeline.send_reply(content, EventId::parse(event_id)?).await?; + let content: RoomMessageEventContentWithoutRelation = + RoomMessageEventContentWithoutRelation::text_markdown(text); + timeline + .send_reply(content, EventId::parse(event_id)?) + .await?; } None => { - timeline.send(RoomMessageEventContent::text_markdown(text).into()).await?; + timeline + .send(RoomMessageEventContent::text_markdown(text).into()) + .await?; } } @@ -293,11 +373,12 @@ impl Backend { } fn typing(&mut self) { - let Some(open) = &mut self.open else { - return - }; + let Some(open) = &mut self.open else { return }; - if open.last_typing_sent.is_some_and(|time| time.elapsed() < Duration::from_secs(4)) { + if open + .last_typing_sent + .is_some_and(|time| time.elapsed() < Duration::from_secs(4)) + { return; } @@ -309,14 +390,13 @@ impl Backend { let _ = room.typing_notice(true).await; }); } - } // UI IDs are event ids, or `txn:` for local echoes that haven't been sent yet. fn parse_item_id(id: &str) -> Result { Ok(match id.strip_prefix("txn:") { Some(txn) => TimelineEventItemId::TransactionId(OwnedTransactionId::from(txn)), - None => TimelineEventItemId::EventId(EventId::parse(id)?) + None => TimelineEventItemId::EventId(EventId::parse(id)?), }) } @@ -326,14 +406,20 @@ async fn watch_session(client: Client, passphrase: String, emit: Sink) { loop { match rx.recv().await { Ok(SessionChange::TokensRefreshed) => { - if let Err(e) = session::save(&client, &passphrase) { + if let Err(e) = session::save(&client, &passphrase).await { tracing::warn!("Couldn't persist refreshed tokens: {e:#}"); } } Ok(SessionChange::UnknownToken(_)) => { - session::clear(); - emit(Event::NeedLogin { error: Some("Your session expired or was signed out elsewhere. Please sign in again.".into()) }); + // fuck you + let _ = session::clear().await; + emit(Event::NeedLogin { + error: Some( + "Your session expired or was signed out elsewhere. Please sign in again." + .into(), + ), + }); } Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {} diff --git a/src/backend/navigation.rs b/src/backend/navigation.rs index fcc72f8..e0d7d6b 100644 --- a/src/backend/navigation.rs +++ b/src/backend/navigation.rs @@ -1,18 +1,20 @@ -use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, Mutex}, - time::Duration -}; use eyeball_im::Vector; use futures_util::StreamExt; use matrix_sdk::{ + Client, Room, RoomState, ruma::{OwnedRoomId, OwnedServerName, room::RoomType}, - Client, Room, RoomState }; -use matrix_sdk_ui::spaces::{room_list::SpaceRoomListPaginationState, SpaceRoom, SpaceRoomList, SpaceService}; +use matrix_sdk_ui::spaces::{ + SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState, +}; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, + time::Duration, +}; -use tokio::sync::{broadcast::error::RecvError, watch}; use super::Sink; +use tokio::sync::{broadcast::error::RecvError, watch}; use crate::model::{ChannelKind, ChannelRow, Event, ServerRow}; @@ -31,7 +33,7 @@ fn is_voice(t: Option<&RoomType>) -> bool { #[derive(Default, Clone, Copy)] struct Counts { unread: bool, - mentions: u32 + mentions: u32, } impl Counts { @@ -51,7 +53,7 @@ impl Counts { struct ServerView { id: OwnedRoomId, list: SpaceRoomList, - categories: HashMap + categories: HashMap, } async fn paginate_all(list: &SpaceRoomList) { @@ -74,34 +76,59 @@ impl ServerView { async fn open(spaces: &SpaceService, id: OwnedRoomId) -> Self { let list: SpaceRoomList = spaces.space_room_list(id.clone()).await; paginate_all(&list).await; - Self { id, list, categories: HashMap::new() } + Self { + id, + list, + categories: HashMap::new(), + } } async fn name(&self) -> String { - self.list.space().map_or_else(|| self.id.to_string(), |space| space.display_name) + self.list + .space() + .map_or_else(|| self.id.to_string(), |space| space.display_name) } /// Flatten the space into sidebar rows:
/// Loose channels first, then one category per sub-space. - async fn rows(&mut self, client: &Client, spaces: &SpaceService, via: &ViaMap) -> Vec { + async fn rows( + &mut self, + client: &Client, + spaces: &SpaceService, + via: &ViaMap, + ) -> Vec { let children: Vec = self.list.rooms().await; let mut rows: Vec = Vec::new(); - for room in children.iter().filter(|room| !is_space(room.room_type.as_ref())) { + for room in children + .iter() + .filter(|room| !is_space(room.room_type.as_ref())) + { rows.push(channel_row(client, room, via)) } - for space in children.iter().filter(|space| is_space(space.room_type.as_ref())) { + for space in children + .iter() + .filter(|space| is_space(space.room_type.as_ref())) + { if !self.categories.contains_key(&space.room_id) { let list: SpaceRoomList = spaces.space_room_list(self.id.clone()).await; paginate_all(&list).await; self.categories.insert(space.room_id.clone(), list); } - rows.push(ChannelRow::category(space.room_id.as_str(), &space.display_name)); + rows.push(ChannelRow::category( + space.room_id.as_str(), + &space.display_name, + )); if let Some(list) = self.categories.get_mut(&space.room_id) { - for room in list.rooms().await.iter().filter(|room| !is_space(room.room_type.as_ref())) { + for room in list + .rooms() + .await + .iter() + .filter(|room| !is_space(room.room_type.as_ref())) + { rows.push(channel_row(client, room, via)) } } @@ -113,7 +140,10 @@ impl ServerView { fn channel_row(client: &Client, room: &SpaceRoom, via: &ViaMap) -> ChannelRow { let joined: bool = room.state == Some(RoomState::Joined); - let counts: Counts = client.get_room(&room.room_id).map(|room| Counts::of(&room)).unwrap_or_default(); + let counts: Counts = client + .get_room(&room.room_id) + .map(|room| Counts::of(&room)) + .unwrap_or_default(); if !joined && let Ok(mut map) = via.lock() { map.insert(room.room_id.clone(), room.via.clone()); @@ -122,11 +152,15 @@ fn channel_row(client: &Client, room: &SpaceRoom, via: &ViaMap) -> ChannelRow { ChannelRow { id: room.room_id.to_string(), name: room.display_name.clone(), - kind: if is_voice(room.room_type.as_ref()) { ChannelKind::Voice } else { ChannelKind::Text }, + kind: if is_voice(room.room_type.as_ref()) { + ChannelKind::Voice + } else { + ChannelKind::Text + }, unread: joined && counts.unread, mentions: if joined { counts.mentions } else { 0 }, joined, - is_dm: false + is_dm: false, } } @@ -137,20 +171,29 @@ async fn home_rows(client: &Client, spaces: &SpaceService) -> (Vec, let mut total: Counts = Counts::default(); for room in client.joined_rooms() { - if room.is_space() { continue; } + if room.is_space() { + continue; + } let counts: Counts = Counts::of(&room); - let name: String = room.display_name().await.map_or_else(|_| room.room_id().to_string(), |name| name.to_string()); + let name: String = room + .display_name() + .await + .map_or_else(|_| room.room_id().to_string(), |name| name.to_string()); let is_dm: bool = room.is_direct().await.unwrap_or(false); let mut row = ChannelRow { id: room.room_id().to_string(), name, - kind: if is_voice(room.room_type().as_ref()) { ChannelKind::Voice } else { ChannelKind::Text }, + kind: if is_voice(room.room_type().as_ref()) { + ChannelKind::Voice + } else { + ChannelKind::Text + }, unread: counts.unread, mentions: counts.mentions, joined: true, - is_dm + is_dm, }; if is_dm { @@ -158,7 +201,11 @@ async fn home_rows(client: &Client, spaces: &SpaceService) -> (Vec, let recency: u64 = room.recency_stamp().map_or(0, u64::from); dms.push((recency, row)); - } else if spaces.joined_parent_ids_of_child(room.room_id()).await.is_empty() { + } else if spaces + .joined_parent_ids_of_child(room.room_id()) + .await + .is_empty() + { total.add(counts); row.is_dm = false; others.push(row) @@ -182,11 +229,15 @@ async fn server_counts(client: &Client, spaces: &SpaceService) -> HashMap = HashMap::new(); for room in client.joined_rooms() { - if room.is_space() { continue; } + if room.is_space() { + continue; + } let counts: Counts = Counts::of(&room); - if !counts.unread && counts.mentions == 0 { continue } + if !counts.unread && counts.mentions == 0 { + continue; + } let ancestors: HashSet = spaces.top_level_ancestors_of(room.room_id()).await; @@ -198,7 +249,13 @@ async fn server_counts(client: &Client, spaces: &SpaceService) -> HashMap, mut selected: watch::Receiver>, emit: Sink, via: ViaMap) { +pub async fn run( + client: Client, + spaces: Arc, + mut selected: watch::Receiver>, + emit: Sink, + via: ViaMap, +) { let (initial, stream) = spaces.subscribe_to_top_level_joined_spaces().await; let mut top: Vector = initial; @@ -213,14 +270,15 @@ pub async fn run(client: Client, spaces: Arc, mut selected: watch: if view.as_ref().map(|view| &view.id) != wanted.as_ref() { view = match wanted { Some(id) => Some(ServerView::open(&spaces, id).await), - None => None + None => None, }; } // Rail let counts: HashMap = server_counts(&client, &spaces).await; let (home, home_counts) = home_rows(&client, &spaces).await; - let servers: Vec = top.iter() + let servers: Vec = top + .iter() .map(|room| { let c = counts.get(&room.room_id).copied().unwrap_or_default(); @@ -228,11 +286,16 @@ pub async fn run(client: Client, spaces: Arc, mut selected: watch: id: room.room_id.to_string(), name: room.display_name.clone(), unread: c.unread, - mentions: c.mentions + mentions: c.mentions, } - }).collect(); + }) + .collect(); - emit(Event::Servers { servers, home_unread: home_counts.unread, home_mentions: home_counts.mentions }); + emit(Event::Servers { + servers, + home_unread: home_counts.unread, + home_mentions: home_counts.mentions, + }); // Sidebar match view.as_mut() { @@ -242,7 +305,10 @@ pub async fn run(client: Client, spaces: Arc, mut selected: watch: emit(Event::Channels { title, channels }); } - None => emit(Event::Channels { title: "Direct Messages".to_owned(), channels: home }) + None => emit(Event::Channels { + title: "Direct Messages".to_owned(), + channels: home, + }), } // Sleeping until something changes. @@ -271,4 +337,4 @@ pub async fn run(client: Client, spaces: Arc, mut selected: watch: } } } -} \ No newline at end of file +} diff --git a/src/backend/room.rs b/src/backend/room.rs index 526a587..a99a801 100644 --- a/src/backend/room.rs +++ b/src/backend/room.rs @@ -1,15 +1,18 @@ use std::{ - sync::{Arc, atomic::{AtomicBool, Ordering}}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, time::{Duration, Instant}, }; use matrix_sdk::{ + Room, RoomDisplayName, RoomMemberships, ruma::{ OwnedUserId, UserId, api::client::receipt::create_receipt::v3::ReceiptType, events::room::{message::MessageType, power_levels::UserPowerLevel}, }, - Room, RoomMemberships, RoomDisplayName }; use matrix_sdk_ui::timeline::{ @@ -17,14 +20,13 @@ use matrix_sdk_ui::timeline::{ TimelineItemContent, TimelineItemKind, VirtualTimelineItem, }; -use futures_util::StreamExt; -use eyeball_im::Vector; -use anyhow::Result; -use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch}; -use tokio::sync::broadcast::error::RecvError; -use crate::model::{Event, MemberRow, MessageKind, MessageRow, ReactionRow, RoomInfo}; use super::{Sink, Task, format}; - +use crate::model::{Event, MemberRow, MessageKind, MessageRow, ReactionRow, RoomInfo}; +use anyhow::Result; +use eyeball_im::Vector; +use futures_util::StreamExt; +use matrix_sdk::ruma::MilliSecondsSinceUnixEpoch; +use tokio::sync::broadcast::error::RecvError; const GROUP_WINDOW_MS: u64 = 7 * 60 * 1000; const PAGE_SIZE: u16 = 30; @@ -37,18 +39,21 @@ pub struct OpenRoom { pub last_typing_sent: Option, /// Dropping `OpenRoom` will abort these. - _tasks: Vec + _tasks: Vec, } pub async fn open(room: Room, own: OwnedUserId, emit: Sink) -> Result { - let name: String = room.display_name().await.map_or_else(|_| room.room_id().to_string(), |name: RoomDisplayName| name.to_string()); + let name: String = room.display_name().await.map_or_else( + |_| room.room_id().to_string(), + |name: RoomDisplayName| name.to_string(), + ); emit(Event::RoomOpened(RoomInfo { id: room.room_id().to_string(), name, topic: room.topic().unwrap_or_default(), is_dm: room.is_direct().await.unwrap_or(false), - encrypted: room.encryption_state().is_encrypted() + encrypted: room.encryption_state().is_encrypted(), })); let timeline: Arc = Arc::new(room.timeline().await?); @@ -56,16 +61,34 @@ pub async fn open(room: Room, own: OwnedUserId, emit: Sink) -> Result let reached_start: Arc = Arc::new(AtomicBool::new(false)); let tasks: Vec = vec![ - Task::spawn(messages_task(timeline.clone(), own, emit.clone(), loading_older.clone(), reached_start.clone())), + Task::spawn(messages_task( + timeline.clone(), + own, + emit.clone(), + loading_older.clone(), + reached_start.clone(), + )), Task::spawn(members_task(room.clone(), emit.clone())), Task::spawn(typing_task(room.clone(), emit)), ]; - Ok(OpenRoom { room, timeline, loading_older, reached_start, last_typing_sent: None, _tasks: tasks }) + Ok(OpenRoom { + room, + timeline, + loading_older, + reached_start, + last_typing_sent: None, + _tasks: tasks, + }) } /// Fetch one more page of history. -pub async fn load_older(timeline: Arc, loading: Arc, reached_start: Arc, emit: Sink) { +pub async fn load_older( + timeline: Arc, + loading: Arc, + reached_start: Arc, + emit: Sink, +) { if reached_start.load(Ordering::Relaxed) || loading.swap(true, Ordering::AcqRel) { return; } @@ -74,21 +97,32 @@ pub async fn load_older(timeline: Arc, loading: Arc, reach match timeline.paginate_backwards(PAGE_SIZE).await { Ok(true) => reached_start.store(true, Ordering::Relaxed), Ok(false) => {} - Err(error) => tracing::warn!("Pagination failed: {error}") + Err(error) => tracing::warn!("Pagination failed: {error}"), } loading.store(false, Ordering::Release); emit(Event::LoadingOlder(false)); } -async fn messages_task(timeline: Arc, own: OwnedUserId, emit: Sink, loading: Arc, reached_start: Arc) { +async fn messages_task( + timeline: Arc, + own: OwnedUserId, + emit: Sink, + loading: Arc, + reached_start: Arc, +) { let (mut items, stream) = timeline.subscribe().await; let mut stream = Box::pin(stream); emit(Event::Messages(render(&items, &own))); // Backfilling so a freshly opened room isn't nearly empty. - tokio::spawn(load_older(timeline.clone(), loading, reached_start, emit.clone())); + tokio::spawn(load_older( + timeline.clone(), + loading, + reached_start, + emit.clone(), + )); while let Some(differences) = stream.next().await { for difference in differences { @@ -113,8 +147,11 @@ async fn messages_task(timeline: Arc, own: OwnedUserId, emit: Sink, lo fn name_of(profile: &TimelineDetails, id: &UserId) -> String { match profile { - TimelineDetails::Ready(profile) => profile.display_name.clone().unwrap_or_else(|| id.localpart().to_owned()), - _ => id.localpart().to_owned() + TimelineDetails::Ready(profile) => profile + .display_name + .clone() + .unwrap_or_else(|| id.localpart().to_owned()), + _ => id.localpart().to_owned(), } } @@ -127,14 +164,14 @@ fn body_of(kind: &MsgLikeKind) -> Option { MessageType::Audio(content) => format!("[audio] {}", content.body), MessageType::Location(content) => format!("[location] {}", content.body), MessageType::Emote(content) => format!("/me {}", content.body), - _ => message.body().to_owned() + _ => message.body().to_owned(), }, MsgLikeKind::Sticker(_) => "[sticker]".to_owned(), MsgLikeKind::Poll(_) => "[poll]".to_owned(), MsgLikeKind::Redacted => "This message was deleted".to_owned(), MsgLikeKind::UnableToDecrypt(_) => "Unable to decrypt this message (yet).".to_owned(), - _ => return None + _ => return None, }) } @@ -145,7 +182,10 @@ fn render(items: &Vector>, own: &UserId) -> Vec { for item in items { match item.kind() { TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => { - out.push(MessageRow::plain(MessageKind::DateDivider, format::date_label(*timestamp))); + out.push(MessageRow::plain( + MessageKind::DateDivider, + format::date_label(*timestamp), + )); previous = None; } @@ -163,29 +203,38 @@ fn render(items: &Vector>, own: &UserId) -> Vec { match event.content() { TimelineItemContent::MsgLike(content) => { - let Some(body) = body_of(&content.kind) else { continue }; + let Some(body) = body_of(&content.kind) else { + continue; + }; let timestamp_ms: u64 = u64::from(timestamp.0); let same_group: bool = previous.as_ref().is_some_and(|(id, t)| { - id == sender_id.as_str() && timestamp_ms.saturating_sub(*t) < GROUP_WINDOW_MS + id == sender_id.as_str() + && timestamp_ms.saturating_sub(*t) < GROUP_WINDOW_MS }); - let reply = content.in_reply_to.as_ref().map(|reply| match &reply.event { - TimelineDetails::Ready(event) => { - let who: String = name_of(&event.sender_profile, &event.sender); - let what: String = match &event.content { - TimelineItemContent::MsgLike(message) => body_of(&message.kind).unwrap_or_default(), - _ => String::new() - }; + let reply = content + .in_reply_to + .as_ref() + .map(|reply| match &reply.event { + TimelineDetails::Ready(event) => { + let who: String = name_of(&event.sender_profile, &event.sender); + let what: String = match &event.content { + TimelineItemContent::MsgLike(message) => { + body_of(&message.kind).unwrap_or_default() + } + _ => String::new(), + }; - // TODO: Make max characters configurable. - (who, format::truncate(&what, 80)) - } + // TODO: Make max characters configurable. + (who, format::truncate(&what, 80)) + } - _ => ("a message".to_owned(), String::new()) - }); + _ => ("a message".to_owned(), String::new()), + }); - let reactions: Vec = content.reactions + let reactions: Vec = content + .reactions .iter() .map(|(key, senders)| ReactionRow { key: key.clone(), @@ -196,7 +245,10 @@ fn render(items: &Vector>, own: &UserId) -> Vec { let id: String = match event.event_id() { Some(event) => event.to_string(), - None => event.transaction_id().map(|transaction| format!("txn:{transaction}")).unwrap_or_default() + None => event + .transaction_id() + .map(|transaction| format!("txn:{transaction}")) + .unwrap_or_default(), }; out.push(MessageRow { @@ -220,16 +272,23 @@ fn render(items: &Vector>, own: &UserId) -> Vec { TimelineItemContent::MembershipChange(change) => { let verb: &str = match change.change() { - Some(MembershipChange::Joined | MembershipChange::InvitationAccepted) => "joined", + Some( + MembershipChange::Joined | MembershipChange::InvitationAccepted, + ) => "joined", Some(MembershipChange::Left) => "left", Some(MembershipChange::Invited) => "was invited", Some(MembershipChange::Kicked) => "was removed", Some(MembershipChange::Banned) => "was banned", - _ => continue + _ => continue, }; - let who: String = change.display_name().unwrap_or_else(|| change.user_id().localpart().to_owned()); - out.push(MessageRow::plain(MessageKind::Notice, format!("{who} {verb}"))); + let who: String = change + .display_name() + .unwrap_or_else(|| change.user_id().localpart().to_owned()); + out.push(MessageRow::plain( + MessageKind::Notice, + format!("{who} {verb}"), + )); previous = None; } @@ -265,27 +324,50 @@ fn power_of(level: UserPowerLevel) -> i64 { match level { UserPowerLevel::Infinite => i64::MAX, UserPowerLevel::Int(i) => i64::from(i), - _ => 0 + _ => 0, } } fn group_members(members: Vec) -> Vec { - let mut groups: [(&str, Vec); 3] = [("ADMINS", Vec::new()), ("MODERATORS", Vec::new()), ("MEMBERS", Vec::new())]; + let mut groups: [(&str, Vec); 3] = [ + ("ADMINS", Vec::new()), + ("MODERATORS", Vec::new()), + ("MEMBERS", Vec::new()), + ]; for member in members { let level: i64 = power_of(member.power_level()); - let idx: usize = if level >= 100 { 0 } else if level >= 50 { 1 } else { 2 }; - let name: String = member.display_name().map_or_else(|| member.user_id().localpart().to_owned(), ToOwned::to_owned); - groups[idx].1.push(MemberRow { id: member.user_id().to_string(), name, is_header: false }); + let idx: usize = if level >= 100 { + 0 + } else if level >= 50 { + 1 + } else { + 2 + }; + let name: String = member.display_name().map_or_else( + || member.user_id().localpart().to_owned(), + ToOwned::to_owned, + ); + groups[idx].1.push(MemberRow { + id: member.user_id().to_string(), + name, + is_header: false, + }); } let mut out: Vec = Vec::new(); for (title, mut rows) in groups { - if rows.is_empty() { continue; } + if rows.is_empty() { + continue; + } rows.sort_by_key(|row| row.name.to_lowercase()); - out.push(MemberRow { id: String::new(), name: format!("{title} - {}", rows.len()), is_header: true }); + out.push(MemberRow { + id: String::new(), + name: format!("{title} - {}", rows.len()), + is_header: true, + }); out.extend(rows); } @@ -307,7 +389,7 @@ async fn typing_task(room: Room, emit: Sink) { for id in ids { let name: Option = match room.get_member_no_sync(&id).await { Ok(Some(member)) => member.display_name().map(ToOwned::to_owned), - _ => None + _ => None, }; names.push(name.unwrap_or_else(|| id.localpart().to_owned())); @@ -317,7 +399,7 @@ async fn typing_task(room: Room, emit: Sink) { } Err(RecvError::Lagged(_)) => {} - Err(RecvError::Closed) => break + Err(RecvError::Closed) => break, } } -} \ No newline at end of file +} diff --git a/src/backend/session.rs b/src/backend/session.rs index 3b2e65e..74d7533 100644 --- a/src/backend/session.rs +++ b/src/backend/session.rs @@ -1,10 +1,15 @@ use matrix_sdk::{Client, authentication::matrix::MatrixSession, store::RoomLoadSettings}; use anyhow::{Context, Result, bail}; +use keyring::{Entry, Error as KeyringError}; use serde::{Deserialize, Serialize}; -use std::{fs, path::PathBuf}; use crate::helpers; +use std::sync::Mutex; +use std::{fs, path::PathBuf}; + +const KEYRING_SERVICE: &str = "tofu.matrix-session"; +static STORAGE_LOCK: Mutex<()> = Mutex::new(()); #[derive(Serialize, Deserialize)] pub struct SavedSession { @@ -32,58 +37,142 @@ fn random_passphrase() -> String { hex::encode(rand::random::<[u8; 32]>()) } -pub fn load() -> Result> { - let path: PathBuf = session_file(); +fn credential_entry() -> Result { + let directory = data_dir(); + fs::create_dir_all(&directory).context("Creating the session data directory.")?; - if !path.exists() { - return Ok(None); - } + let directory = fs::canonicalize(directory).context("Resolving the session data directory.")?; - let raw: String = fs::read_to_string(&path).with_context(|| format!("Reading '{}'.", path.display()))?; - Ok(Some(serde_json::from_str(&raw).context("Parsing 'session.json'.")?)) + let profile = directory + .to_str() + .context("The session data directory must have a UTF-8 path.")?; + + Entry::new(KEYRING_SERVICE, profile).context("Opening the session keyring entry.") } -pub fn save(client: &Client, store_passphrase: &str) -> Result<()> { - let Some(session) = client.matrix_auth().session() else { bail!("Client has no Matrix session to save."); }; +async fn storage_task(operation: F) -> Result +where + T: Send + 'static, + F: FnOnce() -> Result + Send + 'static, +{ + tokio::task::spawn_blocking(move || { + let _guard = STORAGE_LOCK + .lock() + .map_err(|_| anyhow::anyhow!("Session storage lock was poisoned."))?; + + operation() + }) + .await + .context("Session storage task failed.")? +} + +fn remove_legacy_session() -> Result<()> { + match fs::remove_file(session_file()) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error).context("Removing the plaintext session file."), + } +} + +pub async fn load() -> Result> { + storage_task(|| { + let entry = credential_entry()?; + + match entry.get_password() { + Ok(raw) => { + let saved = serde_json::from_str(&raw).context("Parsing the keyring session.")?; + + // Also, clean up a legacy file left by interrupted migration. + remove_legacy_session()?; + Ok(Some(saved)) + } + Err(KeyringError::NoEntry) => { + let raw = match fs::read_to_string(session_file()) { + Ok(raw) => raw, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(None); + } + Err(error) => { + return Err(error).context("Reading the legacy session file."); + } + }; + + let saved: SavedSession = + serde_json::from_str(&raw).context("Parsing the legacy session file.")?; + + entry + .set_password(&serde_json::to_string(&saved)?) + .context("Migrating the session to the keyring.")?; + + // Never remove the old file before successful keyring storage. + remove_legacy_session()?; + Ok(Some(saved)) + } + Err(error) => Err(error).context("Reading the session from the keyring."), + } + }) + .await +} + +//todo: use multiple credentials as to not overflow the windows keyring +pub async fn save(client: &Client, store_passphrase: &str) -> Result<()> { + let Some(session) = client.matrix_auth().session() else { + bail!("Client has no Matrix session to save."); + }; let saved = SavedSession { homeserver: client.homeserver().to_string(), store_passphrase: store_passphrase.to_owned(), - session + session, }; - fs::create_dir_all(data_dir())?; + storage_task(move || { + credential_entry()? + .set_password(&serde_json::to_string(&saved)?) + .context("Saving the session to the keyring.")?; - let path: PathBuf = session_file(); - - fs::write(&path, serde_json::to_vec_pretty(&saved)?)?; - - #[cfg(unix)] { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; - } - - Ok(()) + remove_legacy_session() + }) + .await } -/// Forget stored account.
-/// Deletes tokens and the local store. -pub fn clear() { - let _ = fs::remove_file(session_file()); - let _ = fs::remove_dir_all(store_dir()); +/// Forget the stored account, including credentials and the local store. +pub async fn clear() -> Result<()> { + storage_task(|| { + match credential_entry()?.delete_credential() { + Ok(()) | Err(KeyringError::NoEntry) => {} + Err(error) => { + return Err(error).context("Deleting the session from the keyring."); + } + } + + remove_legacy_session()?; + + match fs::remove_dir_all(store_dir()) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error).context("Deleting the local Matrix store."), + } + }) + .await } pub async fn login(server: &str, username: &str, password: &str) -> Result<(Client, String)> { - let server: &str = server.trim(); - let server: &str = if server.is_empty() { "matrix.org" } else { server }; + let server = server.trim(); + let server = if server.is_empty() { + "matrix.org" + } else { + server + }; - clear(); // A new login should replace any previous login's local data. + // Preserves the current single-store replacement behavior. + clear().await?; fs::create_dir_all(data_dir())?; - let passphrase: String = random_passphrase(); + let passphrase = random_passphrase(); - let client: Client = Client::builder() + let client = Client::builder() .server_name_or_homeserver_url(server) .sqlite_store(store_dir(), Some(&passphrase)) .handle_refresh_tokens() @@ -91,14 +180,21 @@ pub async fn login(server: &str, username: &str, password: &str) -> Result<(Clie .await .with_context(|| format!("Couldn't reach homeserver '{server}'."))?; - client.matrix_auth() + client + .matrix_auth() .login_username(username.trim(), password) .initial_device_display_name("Tofu") .send() .await - .context("Login failed (check username or password; some servers only allow SS0/0IDC login)")?; + .context( + "Login failed (check username or password; some servers only allow SSO/OIDC login).", + )?; - let _ = save(&client, &passphrase); + if let Err(error) = save(&client, &passphrase).await { + // Avoid leaving a newly created server session after persistence fails. + let _ = client.matrix_auth().logout().await; + return Err(error); + } Ok((client, passphrase)) } @@ -112,9 +208,10 @@ pub async fn restore(saved: &SavedSession) -> Result { .build() .await?; - client.matrix_auth() + client + .matrix_auth() .restore_session(saved.session.clone(), RoomLoadSettings::default()) .await?; Ok(client) -} \ No newline at end of file +} diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index 3aaf5b1..ac77f63 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -1 +1 @@ -pub mod system; \ No newline at end of file +pub mod system; diff --git a/src/helpers/system.rs b/src/helpers/system.rs index 1ba04b7..f4de65f 100644 --- a/src/helpers/system.rs +++ b/src/helpers/system.rs @@ -1,14 +1,8 @@ -use std::env; -use std::ffi::OsString; -use std::path::PathBuf; +use std::{env, ffi::OsString, path::PathBuf}; pub fn is_absolute_path(path: OsString) -> Option { let path = PathBuf::from(path); - if path.is_absolute() { - Some(path) - } else { - None - } + path.is_absolute().then_some(path) } pub fn home_dir() -> Option { diff --git a/src/main.rs b/src/main.rs index a7e5503..7c1cac3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ #![recursion_limit = "256"] mod backend; -mod model; pub mod bridge; mod helpers; +mod model; -use std::sync::Arc; use slint::Weak; +use std::sync::Arc; use tracing_subscriber::EnvFilter; slint::include_modules!(); @@ -15,7 +15,9 @@ fn main() -> anyhow::Result<()> { let _ = dotenvy::dotenv(); tracing_subscriber::fmt() - .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn,Tofu=info"))) + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn,Tofu=info")), + ) .init(); let runtime = tokio::runtime::Runtime::new()?; diff --git a/src/model.rs b/src/model.rs index ad70adc..cf844dc 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,4 +1,3 @@ - /// Deterministic hue for a user/room/space id, so each gets a stable avatar colour even before avatars are loaded. pub fn hue_for(id: &str) -> f32 { let mut hash: u32 = 0x811c_9dc5; @@ -8,7 +7,8 @@ pub fn hue_for(id: &str) -> f32 { hash = hash.wrapping_mul(0x0100_0193); } - #[allow(clippy::cast_precision_loss)] { + #[allow(clippy::cast_precision_loss)] + { (hash % 360) as f32 } } @@ -24,7 +24,7 @@ pub fn initial_of(name: &str) -> String { #[derive(Debug, Clone)] pub struct UserInfo { pub id: String, - pub name: String + pub name: String, } // A "server" equals a top-level Matrix space. @@ -33,12 +33,14 @@ pub struct ServerRow { pub id: String, pub name: String, pub unread: bool, - pub mentions: u32 + pub mentions: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ChannelKind { - Text, Voice, Category + Text, + Voice, + Category, } // A "channel" equals a Matrix room. @@ -50,7 +52,7 @@ pub struct ChannelRow { pub unread: bool, pub mentions: u32, pub joined: bool, - pub is_dm: bool + pub is_dm: bool, } impl ChannelRow { @@ -62,7 +64,7 @@ impl ChannelRow { unread: false, mentions: 0, joined: true, - is_dm: false + is_dm: false, } } } @@ -73,19 +75,22 @@ pub struct RoomInfo { pub name: String, pub topic: String, pub is_dm: bool, - pub encrypted: bool + pub encrypted: bool, } #[derive(Debug, Clone)] pub struct ReactionRow { pub key: String, pub count: u32, - pub mine: bool + pub mine: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MessageKind { - Message, DateDivider, Notice, StartOfRoom + Message, + DateDivider, + Notice, + StartOfRoom, } #[derive(Debug, Clone)] @@ -102,7 +107,7 @@ pub struct MessageRow { pub own: bool, pub pending: bool, pub reply: Option<(String, String)>, - pub reactions: Vec + pub reactions: Vec, } impl MessageRow { @@ -120,7 +125,7 @@ impl MessageRow { own: false, pending: false, reply: None, - reactions: Vec::new() + reactions: Vec::new(), } } } @@ -129,18 +134,27 @@ impl MessageRow { pub struct MemberRow { pub id: String, pub name: String, - pub is_header: bool + pub is_header: bool, } /// Everything the backend can tell the frontend. #[derive(Debug, Clone)] pub enum Event { - NeedLogin { error: Option }, + NeedLogin { + error: Option, + }, LoginBusy(bool), LoggedIn(UserInfo), - Servers { servers: Vec, home_unread: bool, home_mentions: u32 }, + Servers { + servers: Vec, + home_unread: bool, + home_mentions: u32, + }, /// Channel (or DM) list for the currently selected server / Home. - Channels { title: String, channels: Vec }, + Channels { + title: String, + channels: Vec, + }, RoomOpened(RoomInfo), Messages(Vec), Members(Vec), @@ -148,24 +162,39 @@ pub enum Event { LoadingOlder(bool), /// Short message for the toast banner. - Toast(String) + Toast(String), } /// Everything the frontend can ask the backend. #[derive(Debug, Clone)] pub enum Command { - Login { homeserver: String, username: String, password: String }, + Login { + homeserver: String, + username: String, + password: String, + }, Logout, /// `None` = Home, DMs or Rooms outside any space. SelectServer(Option), SelectRoom(String), - Send { reply_to: Option, text: String }, - Edit { id: String, text: String }, - Delete { id: String }, - ToggleReaction { id: String, key: String }, + Send { + reply_to: Option, + text: String, + }, + Edit { + id: String, + text: String, + }, + Delete { + id: String, + }, + ToggleReaction { + id: String, + key: String, + }, LoadOlder, /// The user is typing in the composer. (Throttled by the backend). - Typing -} \ No newline at end of file + Typing, +}