diff options
author | Vika <vika@fireburn.ru> | 2024-08-25 17:36:24 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2024-08-25 17:39:12 +0300 |
commit | 15b66ea78b619696c15414d1008acfe460c94028 (patch) | |
tree | 7116cdf7d216c0dc633fdcc08f7f687e717fa264 | |
parent | 8d182c68cc2ed973514276e1470f6d68ac1544e8 (diff) | |
download | bowl-15b66ea78b619696c15414d1008acfe460c94028.tar.zst |
Simplify the main component a lot
Uses the macro again, tries to store only the relevant parts in enums (i.e. the Micropub client, which requires a token).
-rw-r--r-- | src/lib.rs | 223 | ||||
-rw-r--r-- | src/secrets.rs | 2 |
2 files changed, 95 insertions, 130 deletions
diff --git a/src/lib.rs b/src/lib.rs index 913a9a4..52a18ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -use std::{borrow::Borrow, sync::Arc}; - use adw::prelude::*; use libsecret::prelude::{RetrievableExtManual, RetrievableExt}; use relm4::{gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt}; @@ -16,7 +14,7 @@ pub mod components { }; pub(crate) mod tag_pill; - pub(crate) use tag_pill::{TagPill, TagPillDelete}; + // pub(crate) use tag_pill::{TagPill, TagPillDelete}; pub mod signin; pub use signin::{SignIn, Output as SignInOutput}; @@ -34,26 +32,19 @@ pub const VISIBILITY: [&str; 2] = ["public", "private"]; #[derive(Debug)] pub struct App { - state: AuthState, - secret_schema: libsecret::Schema, http: soup::Session, + submit_busy_guard: Option<gtk::gio::ApplicationBusyGuard>, + + // TODO: make this support multiple users + micropub: Option<micropub::Client>, signin: AsyncController<components::SignIn>, post_editor: Controller<components::PostEditor<micropub::Error>>, } -#[derive(Debug)] -enum AuthState { - LoggedOut, - LoggedIn { - submit_busy_guard: Option<gtk::gio::ApplicationBusyGuard>, - micropub: micropub::Client - } -} - impl App { - async fn get_login_state(schema: &libsecret::Schema) -> Result<AuthState, glib::Error> { + async fn get_login_state(schema: &libsecret::Schema) -> Result<Option<micropub::Client>, glib::Error> { let mut retrievables = libsecret::password_search_future(Some(schema), { let mut attrs = std::collections::HashMap::default(); attrs.insert(crate::secrets::TOKEN_KIND, crate::secrets::ACCESS_TOKEN); @@ -78,7 +69,7 @@ impl App { })?; if retrievables.is_empty() { - Ok(AuthState::LoggedOut) + Ok(None) } else { retrievables.sort_by_key(|s| s.created()); let iterable = retrievables.iter().rev(); @@ -100,30 +91,36 @@ impl App { }, }; - return Ok(AuthState::LoggedIn { - micropub: crate::micropub::Client::new( - micropub_uri, - retrievable.retrieve_secret_future().await - // SAFETY: see above - .map_err(|e| unsafe { - let ptr = e.as_ptr(); - std::mem::forget(e); - glib::translate::from_glib_full::<_, glib::Error>( - // We can't name the original type here. - #[allow(clippy::missing_transmute_annotations)] - std::mem::transmute::<_, *mut glib::ffi::GError>(ptr) - ) - })? - .unwrap() - .text() - .unwrap() - .to_string() - ), - submit_busy_guard: None - }) + let micropub = crate::micropub::Client::new( + micropub_uri, + retrievable.retrieve_secret_future().await + // SAFETY: see above + .map_err(|e| unsafe { + let ptr = e.as_ptr(); + std::mem::forget(e); + glib::translate::from_glib_full::<_, glib::Error>( + // We can't name the original type here. + #[allow(clippy::missing_transmute_annotations)] + std::mem::transmute::<_, *mut glib::ffi::GError>(ptr) + ) + })? + .unwrap() + .text() + .unwrap() + .to_string() + ); + + // Skip the token if we can't access ?q=config + if let Err(micropub::Error::Micropub(err)) = micropub.config().await { + if err.error == kittybox_util::micropub::ErrorKind::NotAuthorized { + continue; + } + } + + return Ok(Some(micropub)) } - Ok(AuthState::LoggedOut) + Ok(None) } } } @@ -136,15 +133,7 @@ pub enum Input { PostEditor(Option<Post>) } -#[derive(Default, Debug)] -pub struct AppRootWidgets { - root: adw::ApplicationWindow, - toolbar_view: adw::ToolbarView, - top_bar: adw::HeaderBar, - top_bar_btn: gtk::Button, -} - -//#[relm4::component(pub async)] +#[relm4::component(pub async)] impl AsyncComponent for App { /// The type of the messages that this component can receive. type Input = Input; @@ -155,23 +144,49 @@ impl AsyncComponent for App { /// The type of the command outputs that this component can receive. type CommandOutput = (); - type Widgets = AppRootWidgets; - - type Root = adw::ApplicationWindow; + view! { + #[root] + adw::ApplicationWindow { + #[iterate] + add_css_class: [ + #[cfg(debug_assertions)] "devel" + ], + + #[watch] + set_title: if model.micropub.is_none() { + Some("Bowl – Sign in with your website") + } else { + Some("Bowl") + }, - fn init_root() -> Self::Root { - let window = Self::Root::default(); - window.set_size_request(360, 294); - window.set_default_size(360, 640); - #[cfg(debug_assertions)] - window.add_css_class("devel"); + adw::ToolbarView { + add_top_bar = &adw::HeaderBar { + pack_end = >k::Button { + set_icon_name: "document-send-symbolic", + set_tooltip: "Send post", + set_visible: model.micropub.is_some(), + set_sensitive: model.submit_busy_guard.is_none(), + connect_clicked => Self::Input::SubmitButtonPressed, + } + }, - window + #[transition = "Crossfade"] + match model.micropub.as_ref() { + Some(_) => model.post_editor.widget().clone(), + None => model.signin.widget().clone(), + } + }, + } } fn init_loading_widgets(root: Self::Root) -> Option<relm4::loading_widgets::LoadingWidgets> { + root.set_size_request(360, 294); + root.set_default_size(360, 640); + let spinner = gtk::Spinner::builder() .spinning(true) + .height_request(32) + .width_request(32) .halign(gtk::Align::Center) .valign(gtk::Align::Center) .build(); @@ -186,16 +201,18 @@ impl AsyncComponent for App { window: Self::Root, sender: AsyncComponentSender<Self>, ) -> AsyncComponentParts<Self> { - let schema = crate::secrets::get_schema(); - let state = App::get_login_state(&schema).await.unwrap(); + let secret_schema = crate::secrets::get_schema(); + let state = App::get_login_state(&secret_schema).await.unwrap(); let http = soup::Session::builder() .user_agent(concat!(env!("CARGO_PKG_NAME"),"/",env!("CARGO_PKG_VERSION")," ")) .build(); + let model = App { - state, + submit_busy_guard: None, http: http.clone(), - secret_schema: crate::secrets::get_schema(), + micropub: state, + secret_schema, post_editor: components::PostEditor::builder() .launch(None) @@ -210,53 +227,12 @@ impl AsyncComponent for App { ) }; - let mut widgets = Self::Widgets { - root: window, - ..Self::Widgets::default() - }; - - widgets.toolbar_view.add_top_bar(&widgets.top_bar); - - widgets.top_bar.pack_end(&widgets.top_bar_btn); - - widgets.top_bar_btn.set_icon_name("document-send-symbolic"); - widgets.top_bar_btn.set_tooltip("Send post"); - widgets.top_bar_btn.connect_clicked(glib::clone!( - #[strong] sender, - move |_button| sender.input(Self::Input::SubmitButtonPressed) - )); - - widgets.root.set_content(Some(&widgets.toolbar_view)); - - // Separate component choosing logic from initialization. We - // already have all the parts here, might as well use them. - model.update_view(&mut widgets, sender); + let widgets = view_output!(); AsyncComponentParts { model, widgets } } - fn update_view(&self, widgets: &mut Self::Widgets, _sender: AsyncComponentSender<Self>) { - // Bind the child component, if any, here. - match &self.state { - AuthState::LoggedOut => { - widgets.root.set_title(Some("Sign in with your website")); - widgets.toolbar_view.set_content(Some(self.signin.widget())); - widgets.top_bar_btn.set_visible(false); - }, - AuthState::LoggedIn { - submit_busy_guard, - .. - } => { - widgets.root.set_title(Some("Create post")); - widgets.toolbar_view.set_content(Some(self.post_editor.widget())); - widgets.top_bar_btn.set_sensitive(submit_busy_guard.is_none()); - widgets.top_bar_btn.set_visible(true); - } - } - } - - async fn update( &mut self, message: Self::Input, @@ -265,13 +241,14 @@ impl AsyncComponent for App { ) { match message { Input::Authorize(data) => { - let schema = crate::secrets::get_schema(); let mut attributes = std::collections::HashMap::new(); let _me = data.me.to_string(); let _micropub = data.micropub.to_string(); + let scope = data.scope.to_string(); attributes.insert(secrets::ME, _me.as_str()); attributes.insert(secrets::TOKEN_KIND, secrets::ACCESS_TOKEN); attributes.insert(secrets::MICROPUB, _micropub.as_str()); + attributes.insert(secrets::SCOPE, scope.as_str()); let exp = data.expires_in .as_ref() .map(std::time::Duration::as_secs) @@ -282,10 +259,10 @@ impl AsyncComponent for App { } match libsecret::password_store_future( - Some(&schema), + Some(&self.secret_schema), attributes.clone(), Some(libsecret::COLLECTION_DEFAULT), - data.me.to_str().as_str(), + &format!("Micropub access token for {}", &data.me), &data.access_token ).await { Ok(()) => {}, @@ -295,10 +272,10 @@ impl AsyncComponent for App { attributes.insert(secrets::TOKEN_KIND, secrets::REFRESH_TOKEN); attributes.remove(secrets::EXPIRES_IN); match libsecret::password_store_future( - Some(&schema), + Some(&self.secret_schema), attributes, Some(libsecret::COLLECTION_DEFAULT), - data.me.to_str().as_str(), + &format!("Micropub refresh token for {}", &data.me), refresh_token ).await { Ok(()) => {}, @@ -306,35 +283,21 @@ impl AsyncComponent for App { } } - self.state = AuthState::LoggedIn { - micropub: crate::micropub::Client::new( - data.micropub.clone(), data.access_token.clone() - ), - submit_busy_guard: None - }; + self.micropub = Some(crate::micropub::Client::new( + data.micropub.clone(), data.access_token.clone() + )); }, Input::SubmitButtonPressed => { - if let AuthState::LoggedIn { - ref mut submit_busy_guard, - .. - } = &mut self.state { - *submit_busy_guard = Some(relm4::main_adw_application().mark_busy()); + if self.micropub.is_some() { + self.submit_busy_guard = Some(relm4::main_adw_application().mark_busy()); self.post_editor.emit(PostEditorInput::Submit); }; }, Input::PostEditor(None) => { - if let AuthState::LoggedIn { - ref mut submit_busy_guard, - .. - } = &mut self.state { - *submit_busy_guard = None; - } + self.submit_busy_guard = None; } Input::PostEditor(Some(post)) => { - if let AuthState::LoggedIn { - ref mut submit_busy_guard, - ref micropub - } = &mut self.state { + if let Some(micropub) = self.micropub.as_ref() { let mf2 = post.into(); log::debug!("Submitting post: {:#}", serde_json::to_string(&mf2).unwrap()); match micropub.send_post(mf2).await { @@ -346,8 +309,8 @@ impl AsyncComponent for App { self.post_editor.emit(PostEditorInput::SubmitError(err)); } } - *submit_busy_guard = None; } + self.submit_busy_guard = None; }, } } diff --git a/src/secrets.rs b/src/secrets.rs index c8c9bd7..fa74aa5 100644 --- a/src/secrets.rs +++ b/src/secrets.rs @@ -5,6 +5,7 @@ pub const ME: &str = "me"; pub const TOKEN_KIND: &str = "token_kind"; pub const EXPIRES_IN: &str = "expires_in"; pub const MICROPUB: &str = "micropub"; +pub const SCOPE: &str = "scope"; pub fn get_schema() -> libsecret::Schema { let mut attrs = std::collections::HashMap::new(); @@ -12,6 +13,7 @@ pub fn get_schema() -> libsecret::Schema { attrs.insert(TOKEN_KIND, libsecret::SchemaAttributeType::String); attrs.insert(MICROPUB, libsecret::SchemaAttributeType::String); attrs.insert(EXPIRES_IN, libsecret::SchemaAttributeType::Integer); + attrs.insert(SCOPE, libsecret::SchemaAttributeType::String); libsecret::Schema::new("org.indieweb.indieauth.BearerCredential", libsecret::SchemaFlags::NONE, attrs) } |