summary refs log tree commit diff
path: root/src/lib.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2024-08-25 03:04:22 +0300
committerVika <vika@fireburn.ru>2024-08-25 03:04:22 +0300
commit847648330cc0e7af59fa6923f45222726d404250 (patch)
tree0c2e9c3d80e5de187889d43233011aae26a8b8b0 /src/lib.rs
parent96f1c3580e8dab10ad862c9e08baaf09b96e0174 (diff)
Prototype for signing in with IndieAuth
The code is really janky and unpolished, the error handling is
TERRIBLE, and I think I can't publish it like this. This'll need a
refactor, but it'll come tomorrow.
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs154
1 files changed, 118 insertions, 36 deletions
diff --git a/src/lib.rs b/src/lib.rs
index f2436f8..d530212 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,7 +1,8 @@
 use std::{borrow::Borrow, sync::Arc};
 
 use adw::prelude::*;
-use relm4::{gtk, prelude::{AsyncComponent, AsyncComponentParts, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
+use libsecret::prelude::{RetrievableExtManual, RetrievableExt};
+use relm4::{gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
 
 pub mod components {
     pub(crate) mod smart_summary;
@@ -16,6 +17,9 @@ pub mod components {
 
     pub(crate) mod tag_pill;
     pub(crate) use tag_pill::{TagPill, TagPillDelete};
+
+    pub mod signin;
+    pub use signin::{SignIn, Output as SignInOutput};
 }
 
 use components::post_editor::Post;
@@ -24,6 +28,7 @@ pub mod secrets;
 pub mod micropub;
 pub mod util;
 pub const APPLICATION_ID: &str = "xyz.vikanezrimaya.kittybox.Bowl";
+pub const CLIENT_ID_STR: &str = "https://kittybox.fireburn.ru/bowl/";
 
 pub const VISIBILITY: [&str; 2] = ["public", "private"];
 
@@ -33,7 +38,7 @@ pub struct App {
 }
 #[derive(Debug)]
 enum AuthState {
-    LoggedOut(gtk::EntryBuffer),
+    LoggedOut(AsyncController<components::SignIn>),
     LoggedIn {
         submit_busy_guard: Option<gtk::gio::ApplicationBusyGuard>,
         post_editor: Controller<components::PostEditor<micropub::Error>>,
@@ -45,7 +50,7 @@ enum AuthState {
 #[doc(hidden)]
 pub enum Input {
     SubmitButtonPressed,
-    Authorize,
+    Authorize(Box<components::SignInOutput>),
     PostEditor(Option<Post>)
 }
 
@@ -55,10 +60,6 @@ pub struct AppRootWidgets {
     toolbar_view: adw::ToolbarView,
     top_bar: adw::HeaderBar,
     top_bar_btn: gtk::Button,
-
-    login_box: gtk::Box,
-    login_label: gtk::Label,
-    login_button: gtk::Button,
 }
 
 //#[relm4::component(pub async)]
@@ -79,20 +80,82 @@ impl AsyncComponent for App {
     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");
 
         window
     }
 
+    fn init_loading_widgets(_root: Self::Root) -> Option<relm4::loading_widgets::LoadingWidgets> {
+        let root = gtk::Box::default();
+        let spinner = gtk::Spinner::builder()
+            .spinning(true)
+            .halign(gtk::Align::Center)
+            .valign(gtk::Align::Center)
+            .build();
+
+        root.append(&spinner);
+        Some(LoadingWidgets::new(root, spinner))
+    }
+
     /// Initialize the UI and model.
     async fn init(
         _init: Self::Init,
         window: Self::Root,
         sender: AsyncComponentSender<Self>,
     ) -> AsyncComponentParts<Self> {
+        let schema = crate::secrets::get_schema();
+        let state = match libsecret::password_search_future(Some(&schema), {
+            let mut attrs = std::collections::HashMap::default();
+            attrs.insert(crate::secrets::TOKEN_KIND, crate::secrets::ACCESS_TOKEN);
+            attrs
+        }, libsecret::SearchFlags::ALL).await {
+            Ok(mut retrievables) => {
+                if retrievables.is_empty() {
+                    AuthState::LoggedOut(
+                        components::SignIn::builder()
+                            .launch(glib::Uri::parse(
+                                CLIENT_ID_STR, glib::UriFlags::NONE
+                            ).unwrap())
+                            .forward(sender.input_sender(), |o| Self::Input::Authorize(Box::new(o)))
+                    )
+                } else {
+                    retrievables.sort_by_key(|s| s.created());
+                    let retrievable = retrievables.last().unwrap();
+                    let attrs = retrievable.attributes();
+
+                    let micropub_uri = attrs
+                        .get(crate::secrets::MICROPUB)
+                        .and_then(|v| glib::Uri::parse(v, glib::UriFlags::NONE).ok())
+                        .unwrap();
+
+                    AuthState::LoggedIn {
+                        post_editor: components::PostEditor::builder()
+                            .launch(None)
+                            .forward(sender.clone().input_sender(), Self::Input::PostEditor),
+                        micropub: crate::micropub::Client::new(
+                            micropub_uri,
+                            retrievable.retrieve_secret_future().await.unwrap().unwrap().text().unwrap().to_string()
+                        ),
+                        submit_busy_guard: None
+                    }
+                }
+            },
+            Err(err) => {
+                log::warn!("Error retrieving secrets: {}", err);
+                AuthState::LoggedOut(
+                    components::SignIn::builder()
+                        .launch(glib::Uri::parse(
+                            CLIENT_ID_STR, glib::UriFlags::NONE
+                        ).unwrap())
+                        .forward(sender.input_sender(), |o| Self::Input::Authorize(Box::new(o)))
+                )
+            },
+            
+        };
         let model = App {
-            state: AuthState::LoggedOut(Default::default())
+            state,
         };
 
         let mut widgets = Self::Widgets {
@@ -111,19 +174,6 @@ impl AsyncComponent for App {
             move |_button| sender.input(Self::Input::SubmitButtonPressed)
         ));
 
-        widgets.login_box.set_orientation(gtk::Orientation::Vertical);
-        widgets.login_box.append(&widgets.login_label);
-        widgets.login_box.append(&widgets.login_button);
-
-        widgets.login_label.set_text("You need to authorize first.");
-
-        widgets.login_button.set_label("Pretend to authorize");
-        widgets.login_button.set_tooltip("(check MICROPUB_URI and MICROPUB_TOKEN environment variables)");
-        widgets.login_button.connect_clicked(glib::clone!(
-            #[strong] sender,
-            move |_button| sender.input(Self::Input::Authorize)
-        ));
-
         widgets.root.set_content(Some(&widgets.toolbar_view));
 
         // Separate component choosing logic from initialization. We
@@ -137,11 +187,10 @@ impl AsyncComponent for App {
     fn update_view(&self, widgets: &mut Self::Widgets, _sender: AsyncComponentSender<Self>) {
         // Bind the child component, if any, here.
         match &self.state {
-            AuthState::LoggedOut(_entry_buffer) => {
+            AuthState::LoggedOut(signin) => {
                 widgets.root.set_title(Some("Sign in with your website"));
-                widgets.toolbar_view.set_content(None::<&gtk::Box>);
+                widgets.toolbar_view.set_content(Some(signin.widget()));
                 widgets.top_bar_btn.set_visible(false);
-                widgets.toolbar_view.set_content(Some(&widgets.login_box));
             },
             AuthState::LoggedIn {
                 post_editor,
@@ -164,22 +213,55 @@ impl AsyncComponent for App {
         _root: &Self::Root
     ) {
         match message {
-            Input::Authorize => {
+            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();
+                attributes.insert(secrets::ME, _me.as_str());
+                attributes.insert(secrets::TOKEN_KIND, secrets::ACCESS_TOKEN);
+                attributes.insert(secrets::MICROPUB, _micropub.as_str());
+                let exp = data.expires_in
+                    .as_ref()
+                    .map(std::time::Duration::as_secs)
+                    .as_ref()
+                    .map(u64::to_string);
+                if let Some(expires_in) = exp.as_deref() {
+                    attributes.insert(secrets::EXPIRES_IN, expires_in);
+                }
+
+                match libsecret::password_store_future(
+                    Some(&schema),
+                    attributes.clone(),
+                    Some(libsecret::COLLECTION_DEFAULT),
+                    data.me.to_str().as_str(),
+                    &data.access_token
+                ).await {
+                    Ok(()) => {},
+                    Err(err) => log::error!("Failed to store access token to the secret store: {}", err),
+                }
+                if let Some(refresh_token) = data.refresh_token.as_deref() {
+                    attributes.insert(secrets::TOKEN_KIND, secrets::REFRESH_TOKEN);
+                    attributes.remove(secrets::EXPIRES_IN);
+                    match libsecret::password_store_future(
+                        Some(&schema),
+                        attributes,
+                        Some(libsecret::COLLECTION_DEFAULT),
+                        data.me.to_str().as_str(),
+                        refresh_token
+                    ).await {
+                        Ok(()) => {},
+                        Err(err) => log::error!("Failed to store refresh token to the secret store: {}", err),
+                    }
+                }
+
                 self.state = AuthState::LoggedIn {
                     post_editor: components::PostEditor::builder()
                         .launch(None)
                         .forward(_sender.clone().input_sender(), Self::Input::PostEditor),
-                    micropub: std::env::var("MICROPUB_TOKEN").ok()
-                        .and_then(|token| {
-                            Some((token, glib::Uri::parse(
-                                &std::env::var("MICROPUB_URI").ok()?,
-                                glib::UriFlags::NONE
-                            ).ok()?))
-                        })
-                        .map(|(token, uri)| crate::micropub::Client::new(
-                            uri, token
-                        ))
-                        .unwrap(),
+                    micropub: crate::micropub::Client::new(
+                        data.micropub.clone(), data.access_token.clone()
+                    ),
                     submit_busy_guard: None
                 };
             },