summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2024-08-25 17:36:24 +0300
committerVika <vika@fireburn.ru>2024-08-25 17:39:12 +0300
commit15b66ea78b619696c15414d1008acfe460c94028 (patch)
tree7116cdf7d216c0dc633fdcc08f7f687e717fa264
parent8d182c68cc2ed973514276e1470f6d68ac1544e8 (diff)
downloadbowl-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.rs223
-rw-r--r--src/secrets.rs2
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 = &gtk::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)
 }