Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
projectmoon | 20630beeda | |
projectmoon | 46088bb627 | |
projectmoon | 1fe590925b | |
projectmoon | 3a4f7ba3a4 | |
projectmoon | e9e04b1a0b | |
projectmoon | 8aa7369a12 | |
projectmoon | aab2e0e358 | |
projectmoon | d219e28280 |
|
@ -1,3 +1,6 @@
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
target = "x86_64-unknown-linux-gnu"
|
target = "x86_64-unknown-linux-gnu"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "gemfreely"
|
name = "gemfreely"
|
||||||
version = "0.1.5"
|
version = "0.1.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
description = "Synchronize Gemini protocol blogs to the Fediverse"
|
description = "Synchronize Gemini protocol blogs to the Fediverse"
|
||||||
|
@ -12,7 +12,7 @@ atom_syndication = "0.12.2"
|
||||||
chrono = "0.4.35"
|
chrono = "0.4.35"
|
||||||
clap = { version = "4.5.3", features = ["derive"] }
|
clap = { version = "4.5.3", features = ["derive"] }
|
||||||
gemini-feed = "0.1.0"
|
gemini-feed = "0.1.0"
|
||||||
germ = "0.3"
|
germ = {version = "0.4", features = ["blocking"] }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
regex = "1.10.3"
|
regex = "1.10.3"
|
||||||
tokio = {version = "1.36", features = [ "full" ] }
|
tokio = {version = "1.36", features = [ "full" ] }
|
||||||
|
|
|
@ -27,8 +27,9 @@ allow = [
|
||||||
"MPL-2.0",
|
"MPL-2.0",
|
||||||
"ISC",
|
"ISC",
|
||||||
"Unicode-DFS-2016",
|
"Unicode-DFS-2016",
|
||||||
|
"Unicode-3.0",
|
||||||
"OpenSSL",
|
"OpenSSL",
|
||||||
"BSD-3-Clause"
|
"BSD-3-Clause",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Some crates don't have (easily) machine readable licensing information,
|
# Some crates don't have (easily) machine readable licensing information,
|
||||||
|
@ -56,4 +57,4 @@ multiple-versions = "warn"
|
||||||
wildcards = "allow"
|
wildcards = "allow"
|
||||||
highlight = "all"
|
highlight = "all"
|
||||||
workspace-default-features = "allow"
|
workspace-default-features = "allow"
|
||||||
external-default-features = "allow"
|
external-default-features = "allow"
|
||||||
|
|
|
@ -94,13 +94,18 @@ async fn sync_gemlog(
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for entry in gemlogs_to_post {
|
for entry in gemlogs_to_post {
|
||||||
let post = wf.create_post(entry).await?;
|
let result = wf.create_post(entry).await;
|
||||||
count += 1;
|
count += 1;
|
||||||
println!(
|
|
||||||
"Created post: {} [title={}]",
|
if let Ok(post) = result {
|
||||||
post.id,
|
println!(
|
||||||
post.title.unwrap_or_default()
|
"Created post: {} [title={}]",
|
||||||
);
|
post.id,
|
||||||
|
post.title.unwrap_or_default()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("Error creating post: {} ", result.unwrap_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Post synchronization complete [posts synced={}]", count);
|
println!("Post synchronization complete [posts synced={}]", count);
|
||||||
|
|
131
src/gemfeed.rs
131
src/gemfeed.rs
|
@ -1,7 +1,6 @@
|
||||||
use chrono::{DateTime, NaiveDate, Utc};
|
use chrono::{DateTime, NaiveDate, Utc};
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::result::Result as StdResult;
|
use std::result::Result as StdResult;
|
||||||
use std::slice::IterMut;
|
use std::slice::IterMut;
|
||||||
|
@ -10,7 +9,9 @@ use anyhow::{anyhow, Error, Result};
|
||||||
use atom_syndication::{Entry as AtomEntry, Feed as AtomFeed};
|
use atom_syndication::{Entry as AtomEntry, Feed as AtomFeed};
|
||||||
use germ::ast::{Ast as GemtextAst, Node as GemtextNode};
|
use germ::ast::{Ast as GemtextAst, Node as GemtextNode};
|
||||||
use germ::convert::{self as germ_convert, Target};
|
use germ::convert::{self as germ_convert, Target};
|
||||||
use germ::request::{request as gemini_request, Response as GeminiResponse};
|
use germ::meta::Meta as GeminiMeta;
|
||||||
|
use germ::request::blocking::request as gemini_request;
|
||||||
|
use germ::request::Response as GeminiResponse;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::Cli;
|
use crate::Cli;
|
||||||
|
@ -19,9 +20,9 @@ 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 {
|
fn is_header(level: usize) -> bool {
|
||||||
// For some reason, Germ reports headers with an emoji as header level 0.
|
level == 1
|
||||||
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 {
|
||||||
|
@ -57,17 +58,17 @@ impl GemfeedType {
|
||||||
const ATOM_MIME_TYPES: &'static [&'static str] = &["text/xml", "application/atom+xml"];
|
const ATOM_MIME_TYPES: &'static [&'static str] = &["text/xml", "application/atom+xml"];
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Cow<'_, str>> for GemfeedType {
|
impl From<GeminiMeta> for GemfeedType {
|
||||||
// See https://github.com/gemrest/germ/issues/2. Will be converted
|
// See https://github.com/gemrest/germ/issues/2. Will be converted
|
||||||
// to use germ Meta struct after this is fixed.
|
// to use germ Meta struct after this is fixed.
|
||||||
fn from(mime: Cow<'_, str>) -> Self {
|
fn from(meta: GeminiMeta) -> Self {
|
||||||
let is_atom = Self::ATOM_MIME_TYPES
|
let is_atom = Self::ATOM_MIME_TYPES
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|atom_mime| mime.contains(atom_mime));
|
.any(|atom_mime| meta.mime().contains(atom_mime));
|
||||||
|
|
||||||
if is_atom {
|
if is_atom {
|
||||||
GemfeedType::Atom
|
GemfeedType::Atom
|
||||||
} else if mime.contains("text/gemini") {
|
} else if meta.mime().contains("text/gemini") {
|
||||||
GemfeedType::Gemtext
|
GemfeedType::Gemtext
|
||||||
} else {
|
} else {
|
||||||
GemfeedType::Unknown
|
GemfeedType::Unknown
|
||||||
|
@ -127,7 +128,9 @@ impl Gemfeed {
|
||||||
|
|
||||||
pub fn load_with_settings(url: &Url, settings: &GemfeedParserSettings) -> Result<Gemfeed> {
|
pub fn load_with_settings(url: &Url, settings: &GemfeedParserSettings) -> Result<Gemfeed> {
|
||||||
let resp = gemini_request(url)?;
|
let resp = gemini_request(url)?;
|
||||||
match GemfeedType::from(resp.meta()) {
|
let meta = GeminiMeta::from_string(resp.meta());
|
||||||
|
|
||||||
|
match GemfeedType::from(meta) {
|
||||||
GemfeedType::Gemtext => Self::load_from_gemfeed(url, resp),
|
GemfeedType::Gemtext => Self::load_from_gemfeed(url, resp),
|
||||||
GemfeedType::Atom => Self::load_from_atom(url, resp, &settings),
|
GemfeedType::Atom => Self::load_from_atom(url, resp, &settings),
|
||||||
_ => Err(anyhow!(
|
_ => Err(anyhow!(
|
||||||
|
@ -143,7 +146,11 @@ impl Gemfeed {
|
||||||
settings: &GemfeedParserSettings,
|
settings: &GemfeedParserSettings,
|
||||||
) -> Result<Gemfeed> {
|
) -> Result<Gemfeed> {
|
||||||
if let Some(content) = resp.content() {
|
if let Some(content) = resp.content() {
|
||||||
let feed = content.parse::<AtomFeed>()?;
|
let feed = match content.parse::<AtomFeed>() {
|
||||||
|
Ok(feed) => feed,
|
||||||
|
Err(_) => return Err(anyhow!("Could not parse Atom feed")),
|
||||||
|
};
|
||||||
|
|
||||||
let entries = parse_atom(&feed, settings)?;
|
let entries = parse_atom(&feed, settings)?;
|
||||||
let title = feed.title();
|
let title = feed.title();
|
||||||
Ok(Self::new(url, title, entries))
|
Ok(Self::new(url, title, entries))
|
||||||
|
@ -221,8 +228,31 @@ pub struct GemfeedEntry {
|
||||||
body: OnceCell<String>,
|
body: OnceCell<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for GemfeedEntry {
|
||||||
|
fn default() -> Self {
|
||||||
|
GemfeedEntry {
|
||||||
|
body: OnceCell::default(),
|
||||||
|
title: String::default(),
|
||||||
|
slug: String::default(),
|
||||||
|
url: Url::parse("gemini://example.com").unwrap(),
|
||||||
|
published: Option::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl GemfeedEntry {
|
impl GemfeedEntry {
|
||||||
|
/// Consumes self to forcibly set body to the given string.
|
||||||
|
pub fn with_body(self, body: String) -> GemfeedEntry {
|
||||||
|
GemfeedEntry {
|
||||||
|
title: self.title,
|
||||||
|
slug: self.slug,
|
||||||
|
published: self.published,
|
||||||
|
url: self.url,
|
||||||
|
body: OnceCell::from(body),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_gemtext(base_url: &Url, node: &GemtextNode) -> Result<GemfeedEntry> {
|
pub fn from_gemtext(base_url: &Url, node: &GemtextNode) -> Result<GemfeedEntry> {
|
||||||
let link = GemfeedLink::try_from(node)?;
|
let link = GemfeedLink::try_from(node)?;
|
||||||
// Gemfeeds have only the date--according to spec, it should
|
// Gemfeeds have only the date--according to spec, it should
|
||||||
|
@ -402,6 +432,63 @@ impl TryFrom<&AtomEntry> for GemfeedLink {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod gemfeed_entry_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_markdown_with_gt_lt_title() -> Result<()> {
|
||||||
|
let gemtext: String = r#"
|
||||||
|
# This is gemtext <dyn>
|
||||||
|
|
||||||
|
With a > in it.
|
||||||
|
"#
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim_start())
|
||||||
|
.map(|line| format!("{}\n", line))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let entry = GemfeedEntry {
|
||||||
|
published: None,
|
||||||
|
slug: "".to_string(),
|
||||||
|
title: "".to_string(),
|
||||||
|
url: Url::parse("gemini://example.com")?,
|
||||||
|
body: OnceCell::from(gemtext),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = entry.body_as_markdown();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_markdown_with_gt_lt() -> Result<()> {
|
||||||
|
let gemtext: String = r#"
|
||||||
|
# This is gemtext
|
||||||
|
|
||||||
|
With a < in > it.
|
||||||
|
"#
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim_start())
|
||||||
|
.map(|line| format!("{}\n", line))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let entry = GemfeedEntry {
|
||||||
|
published: None,
|
||||||
|
slug: "".to_string(),
|
||||||
|
title: "".to_string(),
|
||||||
|
url: Url::parse("gemini://example.com")?,
|
||||||
|
body: OnceCell::from(gemtext),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = entry.body_as_markdown();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod gemfeed_tests {
|
mod gemfeed_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -582,6 +669,30 @@ mod gemfeed_tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_gemfeed_handles_gt_lt() -> Result<()> {
|
||||||
|
let gemfeed: String = r#"
|
||||||
|
# My Gemfeed
|
||||||
|
|
||||||
|
This is a gemfeed.
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
=> post2.gmi 2023-03-05 Post 2 <dyn>
|
||||||
|
=> 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 results = parse_gemfeed(&base_url, &ast)?;
|
||||||
|
assert_eq!(results.len(), 2);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn convert_gemfeed_links_success() -> Result<()> {
|
fn convert_gemfeed_links_success() -> Result<()> {
|
||||||
let gemfeed_links: String = r#"
|
let gemfeed_links: String = r#"
|
||||||
|
|
21
src/wf.rs
21
src/wf.rs
|
@ -99,3 +99,24 @@ impl TryFrom<&GemfeedEntry> for PostCreateRequest {
|
||||||
Ok(req)
|
Ok(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn tryfrom_to_request_handles_gt_lt() {
|
||||||
|
let gemtext: String = r#"
|
||||||
|
# This is gemtext <dyn>
|
||||||
|
|
||||||
|
With a > in it.
|
||||||
|
"#
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim_start())
|
||||||
|
.map(|line| format!("{}\n", line))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let entry = GemfeedEntry::default().with_body(gemtext);
|
||||||
|
let result = PostCreateRequest::try_from(entry);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue