From 847648330cc0e7af59fa6923f45222726d404250 Mon Sep 17 00:00:00 2001 From: Vika Date: Sun, 25 Aug 2024 03:04:22 +0300 Subject: 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. --- src/lib.rs | 154 ++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 118 insertions(+), 36 deletions(-) (limited to 'src/lib.rs') 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), LoggedIn { submit_busy_guard: Option, post_editor: Controller>, @@ -45,7 +50,7 @@ enum AuthState { #[doc(hidden)] pub enum Input { SubmitButtonPressed, - Authorize, + Authorize(Box), PostEditor(Option) } @@ -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 { + 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, ) -> AsyncComponentParts { + 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) { // 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::<>k::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 }; }, -- cgit 1.4.1