added hooks

This commit is contained in:
TriMill 2022-11-29 08:55:18 -05:00
parent 421befdf32
commit b0bab11be7
9 changed files with 139 additions and 94 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
/target /target
config.json
guids
hook.sh

36
Cargo.lock generated
View file

@ -59,7 +59,7 @@ dependencies = [
"libc", "libc",
"num-integer", "num-integer",
"num-traits", "num-traits",
"time 0.1.44", "time",
"winapi", "winapi",
] ]
@ -436,15 +436,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.12.0" version = "1.12.0"
@ -765,35 +756,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "time"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217"
dependencies = [
"itoa",
"libc",
"num_threads",
"time-macros",
]
[[package]]
name = "time-macros"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
[[package]] [[package]]
name = "tiny_http" name = "tiny_http"
version = "0.11.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca" checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
dependencies = [ dependencies = [
"ascii", "ascii",
"chunked_transfer", "chunked_transfer",
"httpdate",
"log", "log",
"time 0.3.11",
"url",
] ]
[[package]] [[package]]

View file

@ -6,10 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = "0.4.19" chrono = "0.4"
reqwest = { version = "0.11.11", features = ["blocking"]} reqwest = { version = "0.11", features = ["blocking"]}
rss = { version = "2.0.1", default_features = false } rss = { version = "2.0", default_features = false }
serde = { version = "1.0.138", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.82" serde_json = "1.0"
strfmt = "0.1.6" strfmt = "0.1"
tiny_http = "0.11.0" tiny_http = "0.12"

View file

@ -28,6 +28,7 @@ The following fields are optional:
| `worker_threads` | Number of threads to spawn for the web server. | `4` | | `worker_threads` | Number of threads to spawn for the web server. | `4` |
| `port` | Port number for web server | `4400` | | `port` | Port number for web server | `4400` |
| `host` | Host for web server | `127.0.0.1` | | `host` | Host for web server | `127.0.0.1` |
| `hook` | Path to hook to run upon recieving new blog posts | `./hook.sh` |
Here is an example configuration: Here is an example configuration:
@ -46,6 +47,16 @@ Here is an example configuration:
} }
``` ```
## Hook
Every time a new post is loaded, the hook program (if specified) will be executed. This is based on GUIDs, so feeds that do not fill the `guid` field will not trigger hooks. The following environment variables will be set:
- `TITLE` - the original title of the post
- `TITLE_FMT` - the formatted title of the post
- `AUTHOR` - the post's author
- `LINK` - the link to the post
- `GUID` - the post's GUID
- `PUB_DATE` - the post's publishing date
## Status page ## Status page
RSS Bundler also generates a status page, available at `/status`. This page shows the last date a feed was fetched and parsed successfully and, if the last try was erroneous, the error that occured. If an error occurs while fetching or parsing a feed, the last good version will be used instead. RSS Bundler also generates a status page, available at `/status`. This page shows the last date a feed was fetched and parsed successfully and, if the last try was erroneous, the error that occured. If an error occurs while fetching or parsing a feed, the last good version will be used instead.

View file

@ -30,6 +30,9 @@ pub struct Config {
#[serde(default="default_host")] #[serde(default="default_host")]
pub host: String, pub host: String,
#[serde(default)]
pub hook: Option<String>,
pub users: Vec<User>, pub users: Vec<User>,
} }

24
src/hooks.rs Normal file
View file

@ -0,0 +1,24 @@
use std::process::Command;
pub struct HookData {
pub title: String,
pub title_fmt: String,
pub author: String,
pub link: String,
pub guid: String,
pub pub_date: String,
}
pub fn run_hook(hook: String, hookdata: Vec<HookData>) -> Result<(), std::io::Error> {
for data in hookdata {
Command::new(hook.clone())
.env("TITLE", data.title)
.env("TITLE_FMT", data.title_fmt)
.env("AUTHOR", data.author)
.env("LINK", data.link)
.env("GUID", data.guid)
.env("PUB_DATE", data.pub_date)
.spawn()?;
}
Ok(())
}

View file

@ -4,17 +4,19 @@ use chrono::{DateTime, SubsecRound};
use rss::Channel; use rss::Channel;
use strfmt::strfmt; use strfmt::strfmt;
use crate::Feed; use crate::{Feed, State};
use crate::config::{Config, User}; use crate::config::{Config, User};
use crate::hooks::HookData;
pub fn bundle_rss(feeds: &HashMap<User, Feed>, config: &Config) -> Channel { pub fn bundle_rss(state: &mut State, config: &Config) -> (Vec<HookData>, Channel) {
let mut bundle = Channel::default(); let mut bundle = Channel::default();
bundle.set_title(&config.title); bundle.set_title(&config.title);
bundle.set_link(&config.link); bundle.set_link(&config.link);
bundle.description = config.description.clone(); bundle.description = config.description.clone();
bundle.set_generator(Some("RSS Bundler".into())); bundle.set_generator(Some("RSS Bundler".into()));
let mut hookdata = Vec::new();
let mut most_recent_date = None; let mut most_recent_date = None;
for (user, feed) in feeds { for (user, feed) in &state.feeds {
if let Some(channel) = &feed.channel { if let Some(channel) = &feed.channel {
for item in channel.items() { for item in channel.items() {
if let Some(pub_date) = &item.pub_date { if let Some(pub_date) = &item.pub_date {
@ -27,6 +29,9 @@ pub fn bundle_rss(feeds: &HashMap<User, Feed>, config: &Config) -> Channel {
} }
} }
let mut item = item.clone(); let mut item = item.clone();
if item.author.is_none() {
item.set_author(user.name.clone());
}
let item_title = { let item_title = {
let title = item.title.as_ref().unwrap_or(&config.default_title); let title = item.title.as_ref().unwrap_or(&config.default_title);
let mut args = HashMap::new(); let mut args = HashMap::new();
@ -40,10 +45,23 @@ pub fn bundle_rss(feeds: &HashMap<User, Feed>, config: &Config) -> Channel {
} }
} }
}; };
item.set_title(item_title); if let Some(guid) = &item.guid {
if item.author.is_none() { if !state.guids.contains(&guid.value) {
item.set_author(user.name.clone()); state.guids.insert(guid.value.clone());
let data = HookData {
title: item.title.as_ref().unwrap_or(&config.default_title).to_owned(),
title_fmt: item_title.clone(),
author: item.author.clone().unwrap(),
link: item.link.clone().unwrap_or_default(),
guid: item.guid.clone().map(|g| g.value).unwrap_or_default(),
pub_date: item.pub_date.clone().unwrap_or_default(),
};
hookdata.push(data);
}
} }
item.set_title(item_title);
bundle.items.push(item.clone()); bundle.items.push(item.clone());
} }
} }
@ -51,7 +69,7 @@ pub fn bundle_rss(feeds: &HashMap<User, Feed>, config: &Config) -> Channel {
if let Some(date) = most_recent_date { if let Some(date) = most_recent_date {
bundle.set_pub_date(date.to_rfc2822()); bundle.set_pub_date(date.to_rfc2822());
} }
bundle (hookdata, bundle)
} }
pub fn gen_status(feeds: &HashMap<User, Feed>) -> String { pub fn gen_status(feeds: &HashMap<User, Feed>) -> String {

View file

@ -1,17 +1,18 @@
#![warn(clippy::pedantic)] #![warn(clippy::pedantic)]
use std::{collections::HashMap, thread, sync::{Mutex, Arc}, time::Duration, env::args, process::ExitCode, fs}; use std::{collections::{HashMap, HashSet}, thread, sync::{Mutex, Arc}, time::Duration, process::ExitCode, fs, panic::catch_unwind, io::{BufWriter, Write}};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use config::Config; use config::{Config, User};
use query::update_feeds; use query::update_feeds;
use rss::Channel; use rss::Channel;
use crate::{junction::{bundle_rss, gen_status}}; use crate::{junction::{bundle_rss, gen_status}, hooks::run_hook};
mod config; mod config;
mod query; mod query;
mod junction; mod junction;
mod server; mod server;
mod hooks;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Feed { pub struct Feed {
@ -22,37 +23,26 @@ pub struct Feed {
pub struct State { pub struct State {
rss: String, rss: String,
guids: HashSet<String>,
feeds: HashMap<User, Feed>,
status: Option<String>, status: Option<String>,
} }
fn main() -> ExitCode { fn main() -> ExitCode {
let mut args = args(); let config = match load_config() {
let exe = args.next();
let config_file = args.next();
let config_file = match &config_file {
Some(s) if s == "--help" => {
eprintln!(
"Usage: {} <config-file>\nDocumentation available at https://github.com/trimill/rss-bundler",
exe.unwrap_or_else(|| "rssbundler".into()));
return 0.into()
}
Some(file) => file,
None => {
eprintln!("No config file provided.");
return 1.into()
}
};
let config = match load_config(config_file) {
Ok(config) => config, Ok(config) => config,
Err(e) => { Err(e) => {
eprintln!("Error loading config: {}", e); eprintln!("Error loading config: {}", e);
return 1.into() return 1.into()
} }
}; };
let mut feeds = HashMap::new();
let guids = load_guids().unwrap_or_default();
let state = State { let state = State {
rss: "".into(), rss: "".into(),
guids,
feeds: HashMap::new(),
status: None, status: None,
}; };
@ -66,25 +56,50 @@ fn main() -> ExitCode {
let sleep_duration = Duration::from_secs(60 * config.refresh_time); let sleep_duration = Duration::from_secs(60 * config.refresh_time);
loop { loop {
update_feeds(&mut feeds, &config); let result = catch_unwind(|| {
let bundle = bundle_rss(&feeds, &config); let mut guard = state.lock().unwrap();
let status = if config.status_page {
Some(gen_status(&feeds))
} else { None };
let mut guard = state.lock().unwrap(); update_feeds(&mut guard.feeds, &config);
guard.status = status; let (hookdata, bundle) = bundle_rss(&mut guard, &config);
guard.rss = bundle.to_string(); let status = if config.status_page {
drop(guard); Some(gen_status(&guard.feeds))
} else { None };
println!("Feeds updated"); if let Some(hook) = &config.hook {
run_hook(hook.to_owned(), hookdata).unwrap();
}
guard.status = status;
guard.rss = bundle.to_string();
save_guids(&guard.guids).unwrap();
drop(guard);
});
if result.is_err() {
eprintln!("Error occured white updating");
} else {
println!("Feeds updated");
}
thread::sleep(sleep_duration); thread::sleep(sleep_duration);
} }
} }
fn load_config(config_file: &str) -> Result<Config, Box<dyn std::error::Error>> { fn load_config() -> Result<Config, Box<dyn std::error::Error>> {
let content = fs::read_to_string(config_file)?; let content = fs::read_to_string("config.json")?;
let config: Config = serde_json::from_str(&content)?; let config: Config = serde_json::from_str(&content)?;
Ok(config) Ok(config)
} }
fn load_guids() -> Result<HashSet<String>, Box<dyn std::error::Error>> {
let content = fs::read_to_string("guids")?;
Ok(content.split("\n").filter(|x| x.len() > 0).map(str::to_owned).collect())
}
fn save_guids(guids: &HashSet<String>) -> Result<(), Box<dyn std::error::Error>> {
let file = fs::OpenOptions::new().create(true).write(true).open("guids")?;
let mut writer = BufWriter::new(file);
for guid in guids {
writeln!(writer, "{}", guid)?;
}
Ok(())
}

View file

@ -10,17 +10,16 @@ use crate::config::{User, Config};
pub fn update_feeds(feeds: &mut HashMap<User, Feed>, config: &Config) { pub fn update_feeds(feeds: &mut HashMap<User, Feed>, config: &Config) {
let client = Client::new(); let client = Client::new();
for user in &config.users { for user in &config.users {
let feed = match feeds.get_mut(user) { let feed = if let Some(feed) = feeds.get_mut(user) {
Some(feed) => feed, feed
None => { } else {
let feed = Feed { let feed = Feed {
channel: None, channel: None,
error_message: None, error_message: None,
last_fetched: Utc.ymd(1970, 1, 1).and_hms(0, 0, 0) last_fetched: Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)
}; };
feeds.insert(user.clone(), feed); feeds.insert(user.clone(), feed);
feeds.get_mut(user).unwrap() feeds.get_mut(user).unwrap()
}
}; };
let res = client.get(&user.rss) let res = client.get(&user.rss)
.timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(5))