You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

610 lines
19 KiB

mod account_settings;
mod avatar;
mod content;
mod create_dm_dialog;
mod event_source_dialog;
mod join_room_dialog;
mod media_viewer;
mod notifications;
pub mod room;
mod room_creation;
mod room_list;
mod settings;
mod sidebar;
mod user;
pub mod verification;
mod view;
use std::{collections::HashSet, time::Duration};
use adw::{prelude::*, subclass::prelude::*};
use futures::StreamExt;
use gettextrs::gettext;
use gtk::{
self, gio, glib,
glib::{clone, signal::SignalHandlerId},
};
use log::{debug, error};
use matrix_sdk::{
config::SyncSettings,
room::Room as MatrixRoom,
ruma::{
api::client::{
error::ErrorKind,
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
session::logout,
},
assign,
events::{
direct::DirectEventContent, room::encryption::SyncRoomEncryptionEvent,
GlobalAccountDataEvent,
},
},
sync::SyncResponse,
Client,
};
use tokio::task::JoinHandle;
use url::Url;
pub use self::{
account_settings::AccountSettings,
avatar::{AvatarData, AvatarImage, AvatarUriSource},
content::verification::SessionVerification,
create_dm_dialog::CreateDmDialog,
room::{Event, Room},
room_creation::RoomCreation,
settings::SessionSettings,
user::{User, UserActions, UserExt},
view::SessionView,
};
use self::{
media_viewer::MediaViewer, notifications::Notifications, room_list::RoomList,
verification::VerificationList,
};
use crate::{
secret::StoredSession,
session::sidebar::{ItemList, SidebarListModel},
spawn, spawn_tokio,
utils::{
check_if_reachable,
matrix::{self, ClientSetupError},
},
};
/// The state of the session.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, glib::Enum)]
#[repr(i32)]
#[enum_type(name = "SessionState")]
pub enum SessionState {
LoggedOut = -1,
#[default]
Init = 0,
InitialSync = 1,
Ready = 2,
}
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "BoxedStoredSession")]
struct BoxedStoredSession(StoredSession);
mod imp {
use std::cell::{Cell, RefCell};
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default)]
pub struct Session {
pub client: OnceCell<Client>,
pub sidebar_list_model: OnceCell<SidebarListModel>,
pub user: OnceCell<User>,
pub state: Cell<SessionState>,
pub info: OnceCell<StoredSession>,
pub sync_tokio_handle: RefCell<Option<JoinHandle<()>>>,
pub offline_handler_id: RefCell<Option<SignalHandlerId>>,
pub offline: Cell<bool>,
pub settings: OnceCell<SessionSettings>,
pub notifications: Notifications,
}
#[glib::object_subclass]
impl ObjectSubclass for Session {
const NAME: &'static str = "Session";
type Type = super::Session;
}
impl ObjectImpl for Session {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoxed::builder::<BoxedStoredSession>("info")
.write_only()
.construct_only()
.build(),
glib::ParamSpecString::builder("session-id")
.read_only()
.build(),
glib::ParamSpecObject::builder::<SidebarListModel>("sidebar-list-model")
.read_only()
.build(),
glib::ParamSpecObject::builder::<User>("user")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("offline")
.read_only()
.build(),
glib::ParamSpecEnum::builder::<SessionState>("state")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"info" => self
.info
.set(value.get::<BoxedStoredSession>().unwrap().0)
.unwrap(),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"session-id" => obj.session_id().to_value(),
"sidebar-list-model" => obj.sidebar_list_model().to_value(),
"user" => obj.user().to_value(),
"offline" => obj.is_offline().to_value(),
"state" => obj.state().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.settings
.set(SessionSettings::new(obj.session_id()))
.unwrap();
self.notifications.set_session(Some(&obj));
let monitor = gio::NetworkMonitor::default();
let handler_id = monitor.connect_network_changed(clone!(@weak obj => move |_, _| {
spawn!(clone!(@weak obj => async move {
obj.update_offline().await;
}));
}));
self.offline_handler_id.replace(Some(handler_id));
}
fn dispose(&self) {
// Needs to be disconnected or else it may restart the sync
if let Some(handler_id) = self.offline_handler_id.take() {
gio::NetworkMonitor::default().disconnect(handler_id);
}
if let Some(handle) = self.sync_tokio_handle.take() {
handle.abort();
}
}
}
}
glib::wrapper! {
/// A Matrix user session.
pub struct Session(ObjectSubclass<imp::Session>);
}
impl Session {
/// Create a new session.
pub async fn new(homeserver: Url, data: matrix_sdk::Session) -> Result<Self, ClientSetupError> {
let stored_session = StoredSession::with_login_data(homeserver, data);
Self::restore(stored_session).await
}
/// Restore a stored session.
pub async fn restore(stored_session: StoredSession) -> Result<Self, ClientSetupError> {
let obj = glib::Object::builder::<Self>()
.property("info", BoxedStoredSession(stored_session.clone()))
.build();
let client =
spawn_tokio!(async move { matrix::client_with_stored_session(stored_session).await })
.await
.unwrap()?;
let imp = obj.imp();
imp.client.set(client).unwrap();
let user = User::new(&obj, &obj.info().user_id);
imp.user.set(user).unwrap();
obj.notify("user");
Ok(obj)
}
/// The info to store this session.
pub fn info(&self) -> &StoredSession {
self.imp().info.get().unwrap()
}
/// The unique local ID for this session.
pub fn session_id(&self) -> &str {
self.info().id()
}
/// The current state of the session.
pub fn state(&self) -> SessionState {
self.imp().state.get()
}
/// Set the current state of the session.
fn set_state(&self, state: SessionState) {
let old_state = self.state();
if old_state == SessionState::LoggedOut || old_state == state {
// The session should be dismissed when it has been logged out, so
// we don't accept anymore state changes.
return;
}
self.imp().state.set(state);
self.notify("state");
}
pub async fn prepare(&self) {
self.update_user_profile();
self.update_offline().await;
self.room_list().load();
self.setup_direct_room_handler();
self.setup_room_encrypted_changes();
self.set_state(SessionState::InitialSync);
self.sync();
debug!("A new session was prepared");
}
fn sync(&self) {
if self.state() < SessionState::InitialSync || self.is_offline() {
return;
}
let client = self.client();
let session_weak: glib::SendWeakRef<Session> = self.downgrade().into();
let handle = spawn_tokio!(async move {
// TODO: only create the filter once and reuse it in the future
let room_event_filter = assign!(RoomEventFilter::default(), {
lazy_load_options: LazyLoadOptions::Enabled {include_redundant_members: false},
});
let filter = assign!(FilterDefinition::default(), {
room: assign!(RoomFilter::empty(), {
include_leave: true,
state: room_event_filter,
}),
});
let sync_settings = SyncSettings::new()
.timeout(Duration::from_secs(30))
.filter(filter.into());
let mut sync_stream = Box::pin(client.sync_stream(sync_settings).await);
while let Some(response) = sync_stream.next().await {
let session_weak = session_weak.clone();
let ctx = glib::MainContext::default();
ctx.spawn(async move {
if let Some(session) = session_weak.upgrade() {
session.handle_sync_response(response);
}
});
}
});
self.imp().sync_tokio_handle.replace(Some(handle));
}
/// Whether this session is verified with cross-signing.
pub async fn is_verified(&self) -> bool {
let client = self.client();
let e2ee_device_handle = spawn_tokio!(async move {
let user_id = client.user_id().unwrap();
let device_id = client.device_id().unwrap();
client.encryption().get_device(user_id, device_id).await
});
match e2ee_device_handle.await.unwrap() {
Ok(Some(device)) => device.is_verified_with_cross_signing(),
Ok(None) => {
error!("Could not find this session’s encryption profile");
false
}
Err(error) => {
error!("Failed to get session’s encryption profile: {error}");
false
}
}
}
pub async fn finish_initialization(&self) {
let obj_weak = glib::SendWeakRef::from(self.downgrade());
self.client()
.register_notification_handler(move |notification, _, _| {
let obj_weak = obj_weak.clone();
async move {
let ctx = glib::MainContext::default();
ctx.spawn(async move {
spawn!(async move {
if let Some(obj) = obj_weak.upgrade() {
obj.notifications().show(notification);
}
});
});
}
})
.await;
}
/// The current settings for this session.
pub fn settings(&self) -> &SessionSettings {
self.imp().settings.get().unwrap()
}
pub fn room_list(&self) -> &RoomList {
self.sidebar_list_model().item_list().room_list()
}
pub fn verification_list(&self) -> &VerificationList {
self.sidebar_list_model().item_list().verification_list()
}
/// The list model of the sidebar.
pub fn sidebar_list_model(&self) -> &SidebarListModel {
self.imp().sidebar_list_model.get_or_init(|| {
let item_list = ItemList::new(&RoomList::new(self), &VerificationList::new(self));
SidebarListModel::new(&item_list)
})
}
/// The user of this session.
pub fn user(&self) -> Option<&User> {
self.imp().user.get()
}
/// Update the profile of this session’s user.
///
/// Fetches the updated profile and updates the local data.
pub fn update_user_profile(&self) {
let client = self.client();
let user = self.user().unwrap().to_owned();
let handle = spawn_tokio!(async move { client.account().get_profile().await });
spawn!(glib::PRIORITY_LOW, async move {
match handle.await.unwrap() {
Ok(res) => {
user.set_display_name(res.displayname);
user.set_avatar_url(res.avatar_url);
}
Err(error) => error!("Couldn’t fetch account metadata: {error}"),
}
});
}
pub fn client(&self) -> Client {
self.imp()
.client
.get()
.expect("The session wasn't prepared")
.clone()
}
/// Whether this session has a connection to the homeserver.
pub fn is_offline(&self) -> bool {
self.imp().offline.get()
}
async fn update_offline(&self) {
let imp = self.imp();
let monitor = gio::NetworkMonitor::default();
let is_offline = if monitor.is_network_available() {
if let Some(info) = imp.info.get() {
!check_if_reachable(&info.homeserver).await
} else {
false
}
} else {
true
};
if self.is_offline() == is_offline {
return;
}
if is_offline {
debug!("This session is now offline");
} else {
debug!("This session is now online");
}
imp.offline.set(is_offline);
if let Some(handle) = imp.sync_tokio_handle.take() {
handle.abort();
}
// Restart the sync loop when online
self.sync();
self.notify("offline");
}
pub fn connect_logged_out<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_notify_local(Some("state"), move |obj, _| {
if obj.state() == SessionState::LoggedOut {
f(obj);
}
})
}
pub fn connect_ready<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_notify_local(Some("state"), move |obj, _| {
if obj.state() == SessionState::Ready {
f(obj);
}
})
}
fn handle_sync_response(&self, response: Result<SyncResponse, matrix_sdk::Error>) {
debug!("Received sync response");
match response {
Ok(response) => {
self.room_list().handle_response_rooms(response.rooms);
self.verification_list()
.handle_response_to_device(response.to_device);
if self.state() < SessionState::Ready {
self.set_state(SessionState::Ready);
spawn!(clone!(@weak self as obj => async move {
obj.finish_initialization().await;
}));
}
}
Err(error) => {
if let Some(kind) = error.client_api_error_kind() {
if matches!(kind, ErrorKind::UnknownToken { .. }) {
self.handle_logged_out();
}
}
error!("Failed to perform sync: {error}");
}
}
}
pub async fn logout(&self) -> Result<(), String> {
debug!("The session is about to be logged out");
let client = self.client();
let handle = spawn_tokio!(async move {
let request = logout::v3::Request::new();
client.send(request, None).await
});
match handle.await.unwrap() {
Ok(_) => {
self.cleanup_session().await;
Ok(())
}
Err(error) => {
error!("Couldn’t logout the session: {error}");
Err(gettext("Failed to logout the session."))
}
}
}
/// Handle that the session has been logged out.
///
/// This should only be called if the session has been logged out without
/// `Session::logout`.
pub fn handle_logged_out(&self) {
// TODO: Show error screen. See: https://gitlab.gnome.org/GNOME/fractal/-/issues/901
spawn!(
glib::PRIORITY_LOW,
clone!(@strong self as obj => async move {
obj.cleanup_session().await;
})
);
}
async fn cleanup_session(&self) {
let imp = self.imp();
self.set_state(SessionState::LoggedOut);
if let Some(handle) = imp.sync_tokio_handle.take() {
handle.abort();
}
if let Some(settings) = imp.settings.get() {
settings.delete();
}
imp.info.get().unwrap().clone().delete(None, false).await;
self.notifications().clear();
debug!("The logged out session was cleaned up");
}
fn setup_direct_room_handler(&self) {
spawn!(
glib::PRIORITY_DEFAULT_IDLE,
clone!(@weak self as obj => async move {
let obj_weak = glib::SendWeakRef::from(obj.downgrade());
obj.client().add_event_handler(
move |event: GlobalAccountDataEvent<DirectEventContent>| {
let obj_weak = obj_weak.clone();
async move {
let ctx = glib::MainContext::default();
ctx.spawn(async move {
spawn!(async move {
if let Some(session) = obj_weak.upgrade() {
let room_ids = event.content.iter().fold(HashSet::new(), |mut acc, (_, rooms)| {
acc.extend(rooms);
acc
});
for room_id in room_ids {
if let Some(room) = session.room_list().get(room_id) {
room.load_category();
}
}
}
});
});
}
},
);
})
);
}
fn setup_room_encrypted_changes(&self) {
let session_weak = glib::SendWeakRef::from(self.downgrade());
let client = self.client();
spawn_tokio!(async move {
client.add_event_handler(move |_: SyncRoomEncryptionEvent, matrix_room: MatrixRoom| {
let session_weak = session_weak.clone();
async move {
let ctx = glib::MainContext::default();
ctx.spawn(async move {
if let Some(session) = session_weak.upgrade() {
if let Some(room) = session.room_list().get(matrix_room.room_id()) {
room.set_is_encrypted(true);
}
}
});
}
});
});
}
pub fn notifications(&self) -> &Notifications {
&self.imp().notifications
}
}