summary refs log tree commit diff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs406
1 files changed, 246 insertions, 160 deletions
diff --git a/src/lib.rs b/src/lib.rs
index fd4b51c..84379cc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,7 +1,16 @@
-use gettextrs::*;
 use adw::prelude::*;
-use libsecret::prelude::{RetrievableExtManual, RetrievableExt};
-use relm4::{actions::{RelmAction, RelmActionGroup}, gtk, loading_widgets::LoadingWidgets, prelude::{AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController, ComponentController, Controller}, AsyncComponentSender, Component, RelmWidgetExt};
+use gettextrs::*;
+use libsecret::prelude::{RetrievableExt, RetrievableExtManual};
+use relm4::{
+    actions::{RelmAction, RelmActionGroup},
+    gtk,
+    loading_widgets::LoadingWidgets,
+    prelude::{
+        AsyncComponent, AsyncComponentController, AsyncComponentParts, AsyncController,
+        ComponentController, Controller,
+    },
+    AsyncComponentSender, Component, RelmWidgetExt,
+};
 
 pub mod icons {
     include!(concat!(env!("OUT_DIR"), "/icons.rs"));
@@ -11,29 +20,30 @@ pub mod components {
     pub(crate) mod smart_summary;
     #[cfg(feature = "smart-summary")]
     pub(crate) use smart_summary::{
-        SmartSummaryButton, Output as SmartSummaryOutput, Input as SmartSummaryInput
+        Input as SmartSummaryInput, Output as SmartSummaryOutput, SmartSummaryButton,
     };
 
     pub(crate) mod post_editor;
-    pub(crate) use post_editor::{
-        PostEditor, Input as PostEditorInput
-    };
+    pub(crate) use post_editor::{Input as PostEditorInput, PostEditor};
 
     pub(crate) mod tag_pill;
     // pub(crate) use tag_pill::{TagPill, TagPillDelete}
 
     pub mod signin;
-    pub use signin::{SignIn, Output as SignInOutput, Error as SignInError};
+    pub use signin::{Error as SignInError, Output as SignInOutput, SignIn};
 
     pub mod preferences;
     pub use preferences::Preferences;
 }
 
-use components::{post_editor::{Post, PostConversionSettings}, PostEditorInput};
+use components::{
+    post_editor::{Post, PostConversionSettings},
+    PostEditorInput,
+};
 use soup::prelude::SessionExt;
 
-pub mod secrets;
 pub mod micropub;
+pub mod secrets;
 pub mod util;
 pub const APPLICATION_ID: &str = env!("APP_ID");
 pub const CLIENT_ID_STR: &str = "https://kittybox.fireburn.ru/bowl/";
@@ -53,7 +63,11 @@ pub struct App {
 }
 
 impl App {
-    async fn authorize(schema: &libsecret::Schema, http: soup::Session, data: Box<components::SignInOutput>) -> Result<micropub::Client, glib::Error> {
+    async fn authorize(
+        schema: &libsecret::Schema,
+        http: soup::Session,
+        data: Box<components::SignInOutput>,
+    ) -> Result<micropub::Client, glib::Error> {
         let mut attributes = std::collections::HashMap::new();
         let me = data.me.to_string();
         let _micropub = data.micropub.to_string();
@@ -62,7 +76,8 @@ impl App {
         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
+        let exp = data
+            .expires_in
             .as_ref()
             .map(std::time::Duration::as_secs)
             .as_ref()
@@ -76,13 +91,15 @@ impl App {
             attributes.clone(),
             Some(libsecret::COLLECTION_DEFAULT),
             &gettext!("Micropub access token for {}", &data.me),
-            &data.access_token
-        ).await {
-            Ok(()) => {},
+            &data.access_token,
+        )
+        .await
+        {
+            Ok(()) => {}
             Err(err) => {
                 log::error!("Failed to store access token to the secret store: {}", err);
-                return Err(err)
-            },
+                return Err(err);
+            }
         }
         if let Some(refresh_token) = data.refresh_token.as_deref() {
             attributes.insert(secrets::TOKEN_KIND, secrets::REFRESH_TOKEN);
@@ -92,28 +109,42 @@ impl App {
                 attributes,
                 Some(libsecret::COLLECTION_DEFAULT),
                 &format!("Micropub refresh token for {}", &data.me),
-                refresh_token
-            ).await {
-                Ok(()) => {},
+                refresh_token,
+            )
+            .await
+            {
+                Ok(()) => {}
                 Err(err) => {
                     log::error!("Failed to store refresh token to the secret store: {}", err);
-                    return Err(err)
-                },
+                    return Err(err);
+                }
             }
         }
 
         Ok(micropub::Client::new(
-            http.clone(), data.micropub.clone(), data.access_token.clone(), me
+            http.clone(),
+            data.micropub.clone(),
+            data.access_token.clone(),
+            me,
         ))
     }
 
-    async fn refresh_token(schema: &libsecret::Schema, http: soup::Session, me: String) -> 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::REFRESH_TOKEN);
-            attrs.insert(crate::secrets::ME, &me);
-            attrs
-        }, libsecret::SearchFlags::ALL).await?;
+    async fn refresh_token(
+        schema: &libsecret::Schema,
+        http: soup::Session,
+        me: String,
+    ) -> 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::REFRESH_TOKEN);
+                attrs.insert(crate::secrets::ME, &me);
+                attrs
+            },
+            libsecret::SearchFlags::ALL,
+        )
+        .await?;
 
         if retrievables.is_empty() {
             Ok(None)
@@ -127,7 +158,9 @@ impl App {
                     .map(|(k, v)| (k.as_str(), v.as_str()))
                     .collect();
 
-                let token = retrievable.retrieve_secret_future().await?
+                let token = retrievable
+                    .retrieve_secret_future()
+                    .await?
                     .unwrap()
                     .text()
                     .unwrap()
@@ -135,30 +168,40 @@ impl App {
 
                 let url = glib::Uri::parse(me.as_str(), glib::UriFlags::SCHEME_NORMALIZE)?;
 
-                let (metadata, micropub_uri) = match crate::components::signin::get_metadata(http.clone(), url).await {
-                    Ok(res) => res,
-                    Err(err) => {
-                        tracing::warn!("failed to fetch metadata to refresh an expired token: {}", err);
-                        return Ok(None)
-                    }
-                };
+                let (metadata, micropub_uri) =
+                    match crate::components::signin::get_metadata(http.clone(), url).await {
+                        Ok(res) => res,
+                        Err(err) => {
+                            tracing::warn!(
+                                "failed to fetch metadata to refresh an expired token: {}",
+                                err
+                            );
+                            return Ok(None);
+                        }
+                    };
 
                 let grant = kittybox_indieauth::GrantRequest::RefreshToken {
-                    refresh_token: token, client_id: CLIENT_ID_STR.parse().unwrap(), scope: None
+                    refresh_token: token,
+                    client_id: CLIENT_ID_STR.parse().unwrap(),
+                    scope: None,
                 };
 
-                let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE).unwrap();
+                let url = glib::Uri::parse(metadata.token_endpoint.as_str(), glib::UriFlags::NONE)
+                    .unwrap();
                 let msg = soup::Message::from_uri("POST", &url);
                 let headers = msg.request_headers().unwrap();
                 headers.append("Accept", "application/json");
                 msg.set_request_body_from_bytes(
                     Some("application/x-www-form-urlencoded"),
                     Some(&glib::Bytes::from_owned(
-                        serde_urlencoded::to_string(grant).unwrap().into_bytes()
-                    ))
+                        serde_urlencoded::to_string(grant).unwrap().into_bytes(),
+                    )),
                 );
 
-                match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await {
+                match http
+                    .send_and_read_future(&msg, glib::Priority::DEFAULT)
+                    .await
+                {
                     Ok(body) if msg.status() == soup::Status::Ok => {
                         match serde_json::from_slice::<kittybox_indieauth::GrantResponse>(&body) {
                             Ok(kittybox_indieauth::GrantResponse::ProfileUrl(_)) => unreachable!(),
@@ -170,81 +213,96 @@ impl App {
                                 state: _,
                                 expires_in,
                                 profile,
-                                refresh_token
+                                refresh_token,
                             }) => {
                                 if refresh_token.is_some() {
                                     // Get rid of the old refresh token.
-                                    let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;                                    
+                                    let _ =
+                                        libsecret::password_clear_future(Some(schema), attrs_ref)
+                                            .await;
                                 };
                                 let micropub = Self::authorize(
-                                    schema, http, Box::new(components::SignInOutput {
-                                        me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE).unwrap(),
+                                    schema,
+                                    http,
+                                    Box::new(components::SignInOutput {
+                                        me: glib::Uri::parse(me.as_str(), glib::UriFlags::NONE)
+                                            .unwrap(),
                                         scope: scope.unwrap_or_else(components::SignIn::scopes),
                                         micropub: micropub_uri,
-                                        userinfo: metadata.userinfo_endpoint
-                                            .map(|u| glib::Uri::parse(
-                                                u.as_str(), glib::UriFlags::NONE
-                                            ).unwrap()),
+                                        userinfo: metadata.userinfo_endpoint.map(|u| {
+                                            glib::Uri::parse(u.as_str(), glib::UriFlags::NONE)
+                                                .unwrap()
+                                        }),
                                         access_token,
                                         refresh_token,
                                         expires_in: expires_in.map(std::time::Duration::from_secs),
                                         profile,
-                                    })
-                                ).await?;
+                                    }),
+                                )
+                                .await?;
 
-                                return Ok(Some(micropub))
-                            },
+                                return Ok(Some(micropub));
+                            }
                             Err(err) => {
                                 tracing::warn!("failed to refresh token for {}: failed to parse grant response: {}", me, err);
-                                return Ok(None)
-                            },
-                        }
-                    },
-                    Ok(body) => {
-                        match serde_json::from_slice::<kittybox_indieauth::Error>(&body) {
-                            Ok(err) => {
-                                tracing::warn!("failed to refresh token for {}: token endpoint error: {}", me, err);
-                                continue;
-                            },
-                            Err(err) => {
-                                tracing::warn!("failed to refresh token for {}: error parsing token endpoint error: {}", me, err);
-                                tracing::warn!("token endpoint response verbatim follows:\n{}", String::from_utf8_lossy(&body));
-                                return Ok(None)
+                                return Ok(None);
                             }
                         }
+                    }
+                    Ok(body) => match serde_json::from_slice::<kittybox_indieauth::Error>(&body) {
+                        Ok(err) => {
+                            tracing::warn!(
+                                "failed to refresh token for {}: token endpoint error: {}",
+                                me,
+                                err
+                            );
+                            continue;
+                        }
+                        Err(err) => {
+                            tracing::warn!("failed to refresh token for {}: error parsing token endpoint error: {}", me, err);
+                            tracing::warn!(
+                                "token endpoint response verbatim follows:\n{}",
+                                String::from_utf8_lossy(&body)
+                            );
+                            return Ok(None);
+                        }
                     },
                     Err(err) => return Err(err),
                 };
-
             }
             unreachable!()
         }
     }
 
-    async fn revoke_token(http: soup::Session, me: String, token: String) -> Result<Option<()>, components::SignInError> {
-        let url = glib::Uri::parse(
-            me.as_str(),
-            glib::UriFlags::SCHEME_NORMALIZE
-        )?;
+    async fn revoke_token(
+        http: soup::Session,
+        me: String,
+        token: String,
+    ) -> Result<Option<()>, components::SignInError> {
+        let url = glib::Uri::parse(me.as_str(), glib::UriFlags::SCHEME_NORMALIZE)?;
 
         let (metadata, _) = crate::components::signin::get_metadata(http.clone(), url).await?;
 
         let endpoint = match metadata.revocation_endpoint {
             Some(endpoint) => match metadata.revocation_endpoint_auth_methods_supported {
-                Some(methods) => if methods.iter().any(|i| matches!(i, kittybox_indieauth::RevocationEndpointAuthMethod::None)) {
-                    glib::Uri::parse(endpoint.as_str(), glib::UriFlags::NONE).unwrap()
-                } else {
-                    tracing::warn!("couldn't revoke token: revocation endpoint doesn't support unauthenticated requests\n(Note: this is a violation of the IndieAuth specification)");
-                    return Ok(None)
-                },
+                Some(methods) => {
+                    if methods.iter().any(|i| {
+                        matches!(i, kittybox_indieauth::RevocationEndpointAuthMethod::None)
+                    }) {
+                        glib::Uri::parse(endpoint.as_str(), glib::UriFlags::NONE).unwrap()
+                    } else {
+                        tracing::warn!("couldn't revoke token: revocation endpoint doesn't support unauthenticated requests\n(Note: this is a violation of the IndieAuth specification)");
+                        return Ok(None);
+                    }
+                }
                 None => {
                     tracing::warn!("couldn't revoke token: revocation endpoint doesn't support unauthenticated requests\n(Note: this is a violation of the IndieAuth specification)");
-                    return Ok(None)
+                    return Ok(None);
                 }
             },
             None => {
                 tracing::warn!("couldn't revoke token: revocation endpoint not found");
-                return Ok(None)
+                return Ok(None);
             }
         };
         let msg = soup::Message::from_uri("POST", &endpoint);
@@ -253,37 +311,55 @@ impl App {
         msg.set_request_body_from_bytes(
             Some("application/x-www-form-urlencoded"),
             Some(&glib::Bytes::from_owned(
-                serde_urlencoded::to_string(
-                    kittybox_indieauth::TokenRevocationRequest { token }
-                ).unwrap().into_bytes()
-            ))
+                serde_urlencoded::to_string(kittybox_indieauth::TokenRevocationRequest { token })
+                    .unwrap()
+                    .into_bytes(),
+            )),
         );
 
-        match http.send_and_read_future(&msg, glib::Priority::DEFAULT).await {
-            Ok(_) if msg.status() == soup::Status::Ok => {
-                Ok(Some(()))
-            },
+        match http
+            .send_and_read_future(&msg, glib::Priority::DEFAULT)
+            .await
+        {
+            Ok(_) if msg.status() == soup::Status::Ok => Ok(Some(())),
             Ok(body) => {
-                tracing::warn!("couldn't revoke token: revocation endpoint returned non-200: {:?}", msg.status());
+                tracing::warn!(
+                    "couldn't revoke token: revocation endpoint returned non-200: {:?}",
+                    msg.status()
+                );
                 match serde_json::from_slice::<kittybox_indieauth::Error>(&body) {
                     Ok(err) => tracing::warn!("revocation endpoint returned an error: {}", err),
-                    Err(_) => tracing::warn!("couldn't parse revocation endpoint error, response verbatim follows:\n{}", String::from_utf8_lossy(&body))
+                    Err(_) => tracing::warn!(
+                        "couldn't parse revocation endpoint error, response verbatim follows:\n{}",
+                        String::from_utf8_lossy(&body)
+                    ),
                 }
                 Ok(None)
-            },
+            }
             Err(err) => {
-                tracing::warn!("couldn't revoke token: error contacting revocation endpoint: {:?}", err);
+                tracing::warn!(
+                    "couldn't revoke token: error contacting revocation endpoint: {:?}",
+                    err
+                );
                 Err(err.into())
             }
         }
     }
 
-    async fn get_login_state(schema: &libsecret::Schema, http: soup::Session) -> 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);
-            attrs
-        }, libsecret::SearchFlags::ALL).await?;
+    async fn get_login_state(
+        schema: &libsecret::Schema,
+        http: soup::Session,
+    ) -> 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);
+                attrs
+            },
+            libsecret::SearchFlags::ALL,
+        )
+        .await?;
 
         if retrievables.is_empty() {
             Ok(None)
@@ -298,26 +374,27 @@ impl App {
                     .collect();
                 let micropub_uri = match attrs
                     .get(crate::secrets::MICROPUB)
-                    .and_then(|v| glib::Uri::parse(
-                        v, glib::UriFlags::NONE
-                    ).ok()) {
-                        Some(uri) => uri,
-                        None => {
-                            let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;
-                            continue
-                        },
-                    };
+                    .and_then(|v| glib::Uri::parse(v, glib::UriFlags::NONE).ok())
+                {
+                    Some(uri) => uri,
+                    None => {
+                        let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;
+                        continue;
+                    }
+                };
 
                 let me = attrs.get(crate::secrets::ME).unwrap().to_string();
                 let micropub = crate::micropub::Client::new(
                     http.clone(),
                     micropub_uri,
-                    retrievable.retrieve_secret_future().await?
+                    retrievable
+                        .retrieve_secret_future()
+                        .await?
                         .unwrap()
                         .text()
                         .unwrap()
                         .to_string(),
-                    me.clone()
+                    me.clone(),
                 );
 
                 // Skip the token if we can't access ?q=config
@@ -327,21 +404,22 @@ impl App {
                         // Token may have expired. See if we have a refresh token and renew.
                         let _ = libsecret::password_clear_future(Some(schema), attrs_ref).await;
 
-                        match Self::refresh_token(
-                            schema, http.clone(),
-                            me.clone()
-                        ).await {
+                        match Self::refresh_token(schema, http.clone(), me.clone()).await {
                             Ok(None) => continue,
                             Err(err) => {
-                                tracing::warn!("error refreshing Micropub token for {}: {}", &me, err);
-                                continue
-                            },
-                            Ok(Some(micropub)) => return Ok(Some(micropub))
+                                tracing::warn!(
+                                    "error refreshing Micropub token for {}: {}",
+                                    &me,
+                                    err
+                                );
+                                continue;
+                            }
+                            Ok(Some(micropub)) => return Ok(Some(micropub)),
                         }
                     }
                 }
 
-                return Ok(Some(micropub))
+                return Ok(Some(micropub));
             }
 
             Ok(None)
@@ -461,12 +539,17 @@ impl AsyncComponent for App {
     ) -> AsyncComponentParts<Self> {
         let secret_schema = crate::secrets::get_schema();
         let http = soup::Session::builder()
-            .user_agent(concat!(env!("CARGO_PKG_NAME"),"/",env!("CARGO_PKG_VERSION")," "))
+            .user_agent(concat!(
+                env!("CARGO_PKG_NAME"),
+                "/",
+                env!("CARGO_PKG_VERSION"),
+                " "
+            ))
             .build();
 
-        let state = App::get_login_state(
-            &secret_schema, http.clone()
-        ).await.unwrap();
+        let state = App::get_login_state(&secret_schema, http.clone())
+            .await
+            .unwrap();
 
         let model = App {
             submit_busy_guard: None,
@@ -487,13 +570,13 @@ impl AsyncComponent for App {
                     .forward(sender.input_sender(), Self::Input::PostEditor)
             },
             signin: components::SignIn::builder()
-                .launch((glib::Uri::parse(
-                    CLIENT_ID_STR, glib::UriFlags::NONE
-                ).unwrap(), http))
-                .forward(
-                    sender.input_sender(),
-                    |o| Self::Input::Authorize(Box::new(o))
-                )
+                .launch((
+                    glib::Uri::parse(CLIENT_ID_STR, glib::UriFlags::NONE).unwrap(),
+                    http,
+                ))
+                .forward(sender.input_sender(), |o| {
+                    Self::Input::Authorize(Box::new(o))
+                }),
         };
 
         let widgets = view_output!();
@@ -504,20 +587,18 @@ impl AsyncComponent for App {
             App::about().present(weak_window.upgrade().as_ref());
         });
         let weak_window = window.downgrade();
-        let preferences_action: RelmAction<PreferencesAction> = RelmAction::new_stateless(move |_| {
-            // This could be built as an action that sends an input to open preferences.
-            //
-            // But I find this an acceptable alternative.
-            let mut prefs = components::Preferences::builder()
-                .launch(())
-                .detach();
-
-            prefs.emit(weak_window.upgrade().map(|w| w.upcast()));
-            prefs.detach_runtime();
-        });
-        let sign_out_action: RelmAction<SignOutAction> = RelmAction::new_stateless(move |_| {
-            input_sender.emit(Input::SignOut)
-        });
+        let preferences_action: RelmAction<PreferencesAction> =
+            RelmAction::new_stateless(move |_| {
+                // This could be built as an action that sends an input to open preferences.
+                //
+                // But I find this an acceptable alternative.
+                let mut prefs = components::Preferences::builder().launch(()).detach();
+
+                prefs.emit(weak_window.upgrade().map(|w| w.upcast()));
+                prefs.detach_runtime();
+            });
+        let sign_out_action: RelmAction<SignOutAction> =
+            RelmAction::new_stateless(move |_| input_sender.emit(Input::SignOut));
         let mut action_group: RelmActionGroup<AppActionGroup> = RelmActionGroup::new();
         action_group.add_action(about_action);
         action_group.add_action(preferences_action);
@@ -527,12 +608,11 @@ impl AsyncComponent for App {
         AsyncComponentParts { model, widgets }
     }
 
-
     async fn update(
         &mut self,
         message: Self::Input,
         _sender: AsyncComponentSender<Self>,
-        _root: &Self::Root
+        _root: &Self::Root,
     ) {
         match message {
             Input::SignOut => {
@@ -540,37 +620,43 @@ impl AsyncComponent for App {
                     let _ = libsecret::password_clear_future(
                         Some(&self.secret_schema),
                         Default::default(),
-                    ).await;
-                    let _ = Self::revoke_token(
-                        self.http.clone(), micropub.me, micropub.access_token
-                    ).await;
+                    )
+                    .await;
+                    let _ =
+                        Self::revoke_token(self.http.clone(), micropub.me, micropub.access_token)
+                            .await;
                 }
-            },
+            }
             Input::Authorize(data) => {
-                if let Ok(micropub) = Self::authorize(&self.secret_schema, self.http.clone(), data).await {
+                if let Ok(micropub) =
+                    Self::authorize(&self.secret_schema, self.http.clone(), data).await
+                {
                     self.micropub = Some(micropub);
                 }
-            },
+            }
             Input::SubmitButtonPressed => {
                 if self.micropub.is_some() {
                     self.submit_busy_guard = Some(relm4::main_adw_application().mark_busy());
                     // TODO: too easy to deadlock here, refactor to take a channel
                     self.post_editor.emit(PostEditorInput::Submit);
                 };
-            },
+            }
             Input::PostEditor(None) => {
                 self.submit_busy_guard = None;
             }
             Input::PostEditor(Some(post)) => {
                 if let Some(micropub) = self.micropub.as_ref() {
                     let mf2 = post.into_mf2(PostConversionSettings {
-                        send_html_directly: self.settings.get("send-html-directly")
+                        send_html_directly: self.settings.get("send-html-directly"),
                     });
-                    log::debug!("Submitting post: {:#}", serde_json::to_string(&mf2).unwrap());
+                    log::debug!(
+                        "Submitting post: {:#}",
+                        serde_json::to_string(&mf2).unwrap()
+                    );
                     match micropub.send_post(mf2).await {
                         Ok(uri) => {
                             self.post_editor.emit(PostEditorInput::SubmitDone(uri));
-                        },
+                        }
                         Err(err) => {
                             log::warn!("Error sending post: {}", err);
                             self.post_editor.emit(PostEditorInput::SubmitError(err));
@@ -578,7 +664,7 @@ impl AsyncComponent for App {
                     }
                 }
                 self.submit_busy_guard = None;
-            },
+            }
         }
     }
 }