Compare commits
No commits in common. "58ce7e6ed5022f5e29b80b89a59c9c5650016acb" and "d4f9e7f882e802bd85635e708e29613b990ec5d7" have entirely different histories.
58ce7e6ed5
...
d4f9e7f882
|
@ -428,7 +428,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gemfreely"
|
||||
version = "0.1.4"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atom_syndication",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gemfreely"
|
||||
version = "0.1.4"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
description = "Synchronize Gemini protocol blogs to the Fediverse"
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
pub(crate) mod sync;
|
||||
pub(crate) mod login;
|
||||
pub(crate) mod logout;
|
|
@ -1,123 +0,0 @@
|
|||
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(())
|
||||
}
|
110
src/gemfeed.rs
110
src/gemfeed.rs
|
@ -18,10 +18,6 @@ use crate::Cli;
|
|||
static GEMFEED_POST_REGEX: Lazy<regex::Regex> =
|
||||
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 {
|
||||
match node {
|
||||
GemtextNode::Link {
|
||||
|
@ -166,7 +162,7 @@ impl Gemfeed {
|
|||
|
||||
fn load_from_ast(url: &Url, feed: &GemtextAst) -> Result<Gemfeed> {
|
||||
let feed_title = feed.inner().iter().find_map(|node| match node {
|
||||
GemtextNode::Heading { level, text } if is_header(*level) => Some(text),
|
||||
GemtextNode::Heading { level, text } if *level == (1 as usize) => Some(text),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
|
@ -429,110 +425,6 @@ mod gemfeed_tests {
|
|||
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_at_start() -> 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_in_middle() -> 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_at_end() -> 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_ignores_non_post_links() -> Result<()> {
|
||||
let gemfeed: String = r#"
|
||||
|
|
142
src/main.rs
142
src/main.rs
|
@ -1,13 +1,15 @@
|
|||
use crate::commands::sync::SyncCommand;
|
||||
use crate::{gemfeed::Gemfeed, wf::WriteFreelyCredentials};
|
||||
use clap::{Parser, Subcommand};
|
||||
use commands::{login::LoginCommand, logout::LogoutCommand};
|
||||
use gemfeed::GemfeedParserSettings;
|
||||
use std::collections::HashSet;
|
||||
use url::Url;
|
||||
|
||||
use anyhow::Result;
|
||||
use wf::WriteFreely;
|
||||
|
||||
mod gemfeed;
|
||||
mod sanitization;
|
||||
mod wf;
|
||||
mod commands;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
|
@ -74,15 +76,143 @@ 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]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if let Some(ref cmd) = cli.command {
|
||||
match cmd {
|
||||
Command::Login { .. } => LoginCommand::try_from(&cli)?.execute().await,
|
||||
Command::Logout { .. } => LogoutCommand::try_from(&cli)?.execute().await,
|
||||
Command::Sync { .. } => SyncCommand::try_from(&cli)?.execute().await,
|
||||
Command::Login {
|
||||
ref wf_url,
|
||||
ref username,
|
||||
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 {
|
||||
Ok(())
|
||||
|
|
Loading…
Reference in New Issue