Move command executions into their own files, and fix bug with emoji in gemfeed title.
This commit is contained in:
parent
d4f9e7f882
commit
9958783e62
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::{
|
||||||
|
wf::{WriteFreely, WriteFreelyCredentials},
|
||||||
|
Cli, Command,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use std::result::Result as StdResult;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub(crate) struct LoginCommand<'a> {
|
||||||
|
wf_url: &'a str,
|
||||||
|
username: &'a str,
|
||||||
|
password: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a Cli> for LoginCommand<'a> {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(cli: &'a Cli) -> StdResult<Self, Self::Error> {
|
||||||
|
if let Some(Command::Login {
|
||||||
|
ref wf_url,
|
||||||
|
ref username,
|
||||||
|
ref password,
|
||||||
|
}) = cli.command
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
wf_url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not a valid login command"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a LoginCommand<'a>> for WriteFreelyCredentials<'a> {
|
||||||
|
fn from(cmd: &'a LoginCommand<'a>) -> Self {
|
||||||
|
WriteFreelyCredentials::UsernameAndPassword(cmd.username, cmd.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginCommand<'_> {
|
||||||
|
pub async fn execute(self) -> Result<()> {
|
||||||
|
let wf_url = Url::parse(self.wf_url)?;
|
||||||
|
let creds = WriteFreelyCredentials::from(&self);
|
||||||
|
let wf_client = WriteFreely::new(&wf_url, &self.username, &creds).await?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
wf_client.access_token().unwrap_or("[No Token Returned]")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
use crate::wf::{WriteFreely, WriteFreelyCredentials};
|
||||||
|
use crate::{Cli, Command};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use std::result::Result as StdResult;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub(crate) struct LogoutCommand<'a> {
|
||||||
|
wf_url: &'a str,
|
||||||
|
wf_alias: &'a str,
|
||||||
|
wf_access_token: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a Cli> for LogoutCommand<'a> {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
fn try_from(cli: &'a Cli) -> StdResult<Self, Self::Error> {
|
||||||
|
if let Some(Command::Logout { ref wf_url }) = cli.command {
|
||||||
|
let wf_access_token = cli
|
||||||
|
.wf_access_token
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(anyhow!("WriteFreely access token required"))?;
|
||||||
|
|
||||||
|
let wf_alias = cli
|
||||||
|
.wf_alias
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(anyhow!("WriteFreely alias required"))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
wf_url,
|
||||||
|
wf_access_token,
|
||||||
|
wf_alias,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not a valid logout command"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&LogoutCommand<'a>> for WriteFreelyCredentials<'a> {
|
||||||
|
fn from(cmd: &LogoutCommand<'a>) -> Self {
|
||||||
|
WriteFreelyCredentials::AccessToken(cmd.wf_access_token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogoutCommand<'_> {
|
||||||
|
pub async fn execute(self) -> Result<()> {
|
||||||
|
let wf_url = Url::parse(self.wf_url)?;
|
||||||
|
let creds = WriteFreelyCredentials::from(&self);
|
||||||
|
|
||||||
|
let wf_client = WriteFreely::new(&wf_url, &self.wf_alias, &creds).await?;
|
||||||
|
wf_client.logout().await?;
|
||||||
|
|
||||||
|
println!("Successfully logged out from {}", wf_url);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub(crate) mod sync;
|
||||||
|
pub(crate) mod login;
|
||||||
|
pub(crate) mod logout;
|
|
@ -0,0 +1,123 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::gemfeed::{Gemfeed, GemfeedParserSettings};
|
||||||
|
use crate::sanitization;
|
||||||
|
use crate::wf::{WriteFreely, WriteFreelyCredentials};
|
||||||
|
use crate::Cli;
|
||||||
|
use crate::Command;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
struct SanitizeConfig<'a> {
|
||||||
|
strip_before_marker: &'a Option<String>,
|
||||||
|
strip_after_marker: &'a Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SyncCommand<'a> {
|
||||||
|
parser_settings: GemfeedParserSettings<'a>,
|
||||||
|
wf_alias: &'a str,
|
||||||
|
wf_token: &'a str,
|
||||||
|
gemlog_url: &'a str,
|
||||||
|
wf_url: &'a str,
|
||||||
|
config: SanitizeConfig<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a Cli> for SyncCommand<'a> {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(cli: &'a Cli) -> std::prelude::v1::Result<Self, Self::Error> {
|
||||||
|
if let Some(Command::Sync {
|
||||||
|
ref wf_url,
|
||||||
|
ref gemlog_url,
|
||||||
|
ref strip_before_marker,
|
||||||
|
ref strip_after_marker,
|
||||||
|
}) = cli.command
|
||||||
|
{
|
||||||
|
let wf_token = cli
|
||||||
|
.wf_access_token
|
||||||
|
.as_deref()
|
||||||
|
.ok_or(anyhow!("WriteFreely access token required"))?;
|
||||||
|
|
||||||
|
let sanitize_cfg = SanitizeConfig {
|
||||||
|
strip_before_marker,
|
||||||
|
strip_after_marker,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
wf_url,
|
||||||
|
gemlog_url,
|
||||||
|
wf_token,
|
||||||
|
config: sanitize_cfg,
|
||||||
|
parser_settings: GemfeedParserSettings::from(cli),
|
||||||
|
wf_alias: cli.wf_alias.as_deref().expect("WriteFreely Alias required"),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid sync command"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncCommand<'_> {
|
||||||
|
pub async fn execute(self) -> Result<()> {
|
||||||
|
let gemfeed_url = Url::parse(self.gemlog_url)?;
|
||||||
|
let wf_url = Url::parse(self.wf_url)?;
|
||||||
|
|
||||||
|
let wf_creds = WriteFreelyCredentials::AccessToken(self.wf_token);
|
||||||
|
let wf_client = WriteFreely::new(&wf_url, self.wf_alias, &wf_creds).await?;
|
||||||
|
|
||||||
|
let mut gemfeed = Gemfeed::load_with_settings(&gemfeed_url, &self.parser_settings)?;
|
||||||
|
sync_gemlog(&self.config, &mut gemfeed, &wf_client).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_gemlog(
|
||||||
|
config: &SanitizeConfig<'_>,
|
||||||
|
gemfeed: &mut Gemfeed,
|
||||||
|
wf: &WriteFreely,
|
||||||
|
) -> Result<()> {
|
||||||
|
println!(
|
||||||
|
"Beginning sync of posts for WriteFreely user: {}",
|
||||||
|
wf.user().await?
|
||||||
|
);
|
||||||
|
|
||||||
|
let wf_slugs: HashSet<_> = wf.slugs().await?.into_iter().collect();
|
||||||
|
let gemfeed_slugs: HashSet<_> = gemfeed.slugs().into_iter().collect();
|
||||||
|
let slugs_to_post: Vec<_> = gemfeed_slugs.difference(&wf_slugs).collect();
|
||||||
|
|
||||||
|
sanitize_gemlogs(gemfeed, config)?;
|
||||||
|
|
||||||
|
let gemlogs_to_post = slugs_to_post
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|slug| gemfeed.find_entry_by_slug(slug));
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
for entry in gemlogs_to_post {
|
||||||
|
let post = wf.create_post(entry).await?;
|
||||||
|
count += 1;
|
||||||
|
println!(
|
||||||
|
"Created post: {} [title={}]",
|
||||||
|
post.id,
|
||||||
|
post.title.unwrap_or_default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Post synchronization complete [posts synced={}]", count);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_gemlogs(gemfeed: &mut Gemfeed, config: &SanitizeConfig) -> Result<()> {
|
||||||
|
for entry in gemfeed.entries_mut() {
|
||||||
|
if let Some(ref before_marker) = config.strip_before_marker {
|
||||||
|
sanitization::strip_before(entry, before_marker)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref after_marker) = config.strip_after_marker {
|
||||||
|
sanitization::strip_after(entry, after_marker)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -18,6 +18,10 @@ use crate::Cli;
|
||||||
static GEMFEED_POST_REGEX: Lazy<regex::Regex> =
|
static GEMFEED_POST_REGEX: Lazy<regex::Regex> =
|
||||||
Lazy::new(|| Regex::new(r#"(\d\d\d\d-\d\d-\d\d)"#).unwrap());
|
Lazy::new(|| Regex::new(r#"(\d\d\d\d-\d\d-\d\d)"#).unwrap());
|
||||||
|
|
||||||
|
fn is_header(level: usize) -> bool {
|
||||||
|
// For some reason, Germ reports headers with an emoji as header level 0.
|
||||||
|
level == 0 || level == 1
|
||||||
|
}
|
||||||
fn is_gemfeed_post_link(node: &GemtextNode) -> bool {
|
fn is_gemfeed_post_link(node: &GemtextNode) -> bool {
|
||||||
match node {
|
match node {
|
||||||
GemtextNode::Link {
|
GemtextNode::Link {
|
||||||
|
@ -162,7 +166,7 @@ impl Gemfeed {
|
||||||
|
|
||||||
fn load_from_ast(url: &Url, feed: &GemtextAst) -> Result<Gemfeed> {
|
fn load_from_ast(url: &Url, feed: &GemtextAst) -> Result<Gemfeed> {
|
||||||
let feed_title = feed.inner().iter().find_map(|node| match node {
|
let feed_title = feed.inner().iter().find_map(|node| match node {
|
||||||
GemtextNode::Heading { level, text } if *level == (1 as usize) => Some(text),
|
GemtextNode::Heading { level, text } if is_header(*level) => Some(text),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -425,6 +429,58 @@ mod gemfeed_tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_gemfeed_valid_if_has_title() -> Result<()> {
|
||||||
|
let gemfeed: String = r#"
|
||||||
|
# My Gemfeed
|
||||||
|
|
||||||
|
This is a gemfeed with a title.
|
||||||
|
=> atom.xml Atom Feed
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
=> post2.gmi 2023-03-05 Post 2
|
||||||
|
=> post1.gmi 2023-02-01 Post 1
|
||||||
|
"#
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim_start())
|
||||||
|
.map(|line| format!("{}\n", line))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let base_url = Url::parse("gemini://example.com/posts")?;
|
||||||
|
let ast = GemtextAst::from_string(gemfeed);
|
||||||
|
let result = Gemfeed::load_from_ast(&base_url, &ast);
|
||||||
|
|
||||||
|
assert!(matches!(result, Ok(_)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_gemfeed_valid_if_has_title_with_emoji() -> Result<()> {
|
||||||
|
let gemfeed: String = r#"
|
||||||
|
# 🖊️ My Gemfeed
|
||||||
|
|
||||||
|
This is a gemfeed with a title.
|
||||||
|
=> atom.xml Atom Feed
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
=> post2.gmi 2023-03-05 Post 2
|
||||||
|
=> post1.gmi 2023-02-01 Post 1
|
||||||
|
"#
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim_start())
|
||||||
|
.map(|line| format!("{}\n", line))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let base_url = Url::parse("gemini://example.com/posts")?;
|
||||||
|
let ast = GemtextAst::from_string(gemfeed);
|
||||||
|
let result = Gemfeed::load_from_ast(&base_url, &ast);
|
||||||
|
|
||||||
|
assert!(matches!(result, Ok(_)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_gemfeed_ignores_non_post_links() -> Result<()> {
|
fn parse_gemfeed_ignores_non_post_links() -> Result<()> {
|
||||||
let gemfeed: String = r#"
|
let gemfeed: String = r#"
|
||||||
|
|
142
src/main.rs
142
src/main.rs
|
@ -1,15 +1,13 @@
|
||||||
use crate::{gemfeed::Gemfeed, wf::WriteFreelyCredentials};
|
use crate::commands::sync::SyncCommand;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use gemfeed::GemfeedParserSettings;
|
use commands::{login::LoginCommand, logout::LogoutCommand};
|
||||||
use std::collections::HashSet;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use wf::WriteFreely;
|
|
||||||
|
|
||||||
mod gemfeed;
|
mod gemfeed;
|
||||||
mod sanitization;
|
mod sanitization;
|
||||||
mod wf;
|
mod wf;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
|
@ -76,143 +74,15 @@ enum Command {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SanitizeConfig<'a> {
|
|
||||||
strip_before_marker: &'a Option<String>,
|
|
||||||
strip_after_marker: &'a Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sanitize_gemlogs(gemfeed: &mut Gemfeed, config: &SanitizeConfig) -> Result<()> {
|
|
||||||
for entry in gemfeed.entries_mut() {
|
|
||||||
if let Some(ref before_marker) = config.strip_before_marker {
|
|
||||||
sanitization::strip_before(entry, before_marker)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref after_marker) = config.strip_after_marker {
|
|
||||||
sanitization::strip_after(entry, after_marker)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sync(
|
|
||||||
cli: &Cli,
|
|
||||||
gemlog_url: &str,
|
|
||||||
wf_url: &str,
|
|
||||||
config: &SanitizeConfig<'_>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let wf_token = cli
|
|
||||||
.wf_access_token
|
|
||||||
.as_deref()
|
|
||||||
.expect("WriteFreely access token required");
|
|
||||||
|
|
||||||
let settings = GemfeedParserSettings::from(cli);
|
|
||||||
let gemfeed_url = Url::parse(gemlog_url)?;
|
|
||||||
let wf_url = Url::parse(wf_url)?;
|
|
||||||
|
|
||||||
let wf_creds = WriteFreelyCredentials::AccessToken(wf_token);
|
|
||||||
let wf_alias = cli.wf_alias.as_deref().expect("WriteFreely Alias required");
|
|
||||||
let wf_client = wf::WriteFreely::new(&wf_url, wf_alias, &wf_creds).await?;
|
|
||||||
|
|
||||||
let mut gemfeed = Gemfeed::load_with_settings(&gemfeed_url, &settings)?;
|
|
||||||
sync_gemlog(&config, &mut gemfeed, &wf_client).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sync_gemlog(
|
|
||||||
config: &SanitizeConfig<'_>,
|
|
||||||
gemfeed: &mut Gemfeed,
|
|
||||||
wf: &WriteFreely,
|
|
||||||
) -> Result<()> {
|
|
||||||
println!(
|
|
||||||
"Beginning sync of posts for WriteFreely user: {}",
|
|
||||||
wf.user().await?
|
|
||||||
);
|
|
||||||
|
|
||||||
let wf_slugs: HashSet<_> = wf.slugs().await?.into_iter().collect();
|
|
||||||
let gemfeed_slugs: HashSet<_> = gemfeed.slugs().into_iter().collect();
|
|
||||||
let slugs_to_post: Vec<_> = gemfeed_slugs.difference(&wf_slugs).collect();
|
|
||||||
|
|
||||||
sanitize_gemlogs(gemfeed, config)?;
|
|
||||||
|
|
||||||
let gemlogs_to_post = slugs_to_post
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|slug| gemfeed.find_entry_by_slug(slug));
|
|
||||||
|
|
||||||
let mut count = 0;
|
|
||||||
for entry in gemlogs_to_post {
|
|
||||||
let post = wf.create_post(entry).await?;
|
|
||||||
count += 1;
|
|
||||||
println!(
|
|
||||||
"Created post: {} [title={}]",
|
|
||||||
post.id,
|
|
||||||
post.title.unwrap_or_default()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Post synchronization complete [posts synced={}]", count);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wf_login(wf_url: &str, username: &str, password: &str) -> Result<()> {
|
|
||||||
let wf_url = Url::parse(wf_url)?;
|
|
||||||
let creds = WriteFreelyCredentials::UsernameAndPassword(username, password);
|
|
||||||
|
|
||||||
let wf_client = wf::WriteFreely::new(&wf_url, &username, &creds).await?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
wf_client.access_token().unwrap_or("[No Token Returned]")
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wf_logout(wf_url: &str, wf_alias: &str, access_token: &str) -> Result<()> {
|
|
||||||
let wf_url = Url::parse(wf_url)?;
|
|
||||||
let creds = WriteFreelyCredentials::AccessToken(access_token);
|
|
||||||
|
|
||||||
let wf_client = wf::WriteFreely::new(&wf_url, &wf_alias, &creds).await?;
|
|
||||||
wf_client.logout().await?;
|
|
||||||
|
|
||||||
println!("Successfully logged out from {}", wf_url);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
if let Some(ref cmd) = cli.command {
|
if let Some(ref cmd) = cli.command {
|
||||||
match cmd {
|
match cmd {
|
||||||
Command::Login {
|
Command::Login { .. } => LoginCommand::try_from(&cli)?.execute().await,
|
||||||
ref wf_url,
|
Command::Logout { .. } => LogoutCommand::try_from(&cli)?.execute().await,
|
||||||
ref username,
|
Command::Sync { .. } => SyncCommand::try_from(&cli)?.execute().await,
|
||||||
ref password,
|
|
||||||
} => wf_login(wf_url, username, password).await,
|
|
||||||
Command::Logout { ref wf_url } => {
|
|
||||||
wf_logout(
|
|
||||||
wf_url,
|
|
||||||
&cli.wf_alias.as_deref().expect("WriteFreely alias required"),
|
|
||||||
&cli.wf_access_token.expect("Access token required"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Command::Sync {
|
|
||||||
wf_url,
|
|
||||||
gemlog_url,
|
|
||||||
strip_before_marker,
|
|
||||||
strip_after_marker,
|
|
||||||
} => {
|
|
||||||
let sanitize_cfg = SanitizeConfig {
|
|
||||||
strip_before_marker,
|
|
||||||
strip_after_marker,
|
|
||||||
};
|
|
||||||
sync(&cli, gemlog_url, wf_url, &sanitize_cfg).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Reference in New Issue