initial version

This commit is contained in:
Robin Appelman 2021-01-07 20:40:24 +01:00
commit 51cba405ec
7 changed files with 1414 additions and 5 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
config.toml

1254
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,13 @@
[package]
name = "docker-rss-trigger"
name = "rss-webhook-trigger"
version = "0.1.0"
authors = ["Robin Appelman <robin@icewind.nl>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rss = "1.10"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
color-eyre = "0.5"

17
README.md Normal file
View file

@ -0,0 +1,17 @@
# rss-webhook-trigger
Trigger webhooks from rss feeds
### Configuration
```toml
interval = 600 # optional, defaults to 30 minutes
[[feed]]
feed = "https://example.com/feed1.xml"
hook = "https://hook.example.com/hook1/call"
[[feed]]
feed = "https://example.com/feed2.xml"
hook = "https://hook.example.com/hook2/call"
```

5
config.toml Normal file
View file

@ -0,0 +1,5 @@
interval = 60
[[feed]]
feed = "https://www.teamfortress.com/rss.xml"
hook = "https://hub.docker.com/api/build/v1/source/b6176fa8-7163-45c7-b032-f63b65587481/trigger/85ce8dd5-7fff-400a-b509-3a28ef200267/call/"

28
src/config.rs Normal file
View file

@ -0,0 +1,28 @@
use color_eyre::{eyre::WrapErr, Result};
use serde::{Deserialize, Deserializer};
use std::fs::read_to_string;
use tokio::time::Duration;
#[derive(Debug, Deserialize)]
pub struct Config {
interval: Option<u64>,
pub feed: Vec<FeedConfig>,
}
#[derive(Debug, Deserialize)]
pub struct FeedConfig {
pub feed: String,
pub hook: String,
}
impl Config {
pub fn from_file(path: &str) -> Result<Self> {
let file = read_to_string(path)
.wrap_err_with(|| format!("Failed to open config file {}", path))?;
toml::from_str(&file).wrap_err_with(|| format!("Failed to open config file {}", path))
}
pub fn interval(&self) -> Duration {
Duration::from_secs(self.interval.unwrap_or(30 * 60))
}
}

View file

@ -1,3 +1,103 @@
fn main() {
println!("Hello, world!");
mod config;
use crate::config::Config;
use color_eyre::{
eyre::{eyre, WrapErr},
Result,
};
use reqwest::Client;
use rss::Channel;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use tokio::time::sleep;
#[tokio::main]
async fn main() -> Result<()> {
let mut args = std::env::args();
let bin = args.next().unwrap();
let file = match args.next() {
Some(file) => file,
None => {
eprintln!("Usage {} <config file>", bin);
return Ok(());
}
};
let config = Config::from_file(&file)?;
let mut fetcher = FeedFetcher::default();
loop {
for feed in config.feed.iter() {
match fetcher.is_feed_updated(&feed.feed).await {
Ok(true) => {
println!("Trigering hook for {}", feed.feed);
fetcher.client.post(&feed.hook).send().await?;
}
Err(e) => eprintln!("{:#}", e),
Ok(false) => {}
}
}
sleep(config.interval()).await;
}
}
#[derive(Default)]
pub struct FeedFetcher {
client: Client,
cache: HashMap<String, u64>,
}
impl FeedFetcher {
pub async fn is_feed_updated(&mut self, feed: &str) -> Result<bool> {
let new_key = self.get_feed_key(feed).await?;
Ok(match self.cache.get_mut(feed) {
Some(cached) => {
if *cached != new_key {
*cached = new_key;
true
} else {
false
}
}
None => {
self.cache.insert(feed.into(), new_key);
// dont trigger the actions on start
false
}
})
}
async fn get_feed_key(&self, feed: &str) -> Result<u64> {
let content = self
.client
.get(feed)
.send()
.await
.wrap_err_with(|| eyre!("Failed to load feed {}", feed))?
.bytes()
.await
.wrap_err_with(|| eyre!("Failed to load feed {}", feed))?;
let channel = Channel::read_from(content.as_ref())
.wrap_err_with(|| eyre!("Failed to parse feed {}", feed))?;
let item = channel.items.first().ok_or(eyre!("Empty feed"))?;
let mut hasher = DefaultHasher::new();
if let Some(guid) = item.guid() {
guid.value.hash(&mut hasher);
} else if let Some(date) = item.pub_date() {
date.hash(&mut hasher);
} else if let Some(link) = item.link() {
link.hash(&mut hasher);
} else {
return Err(eyre!("No guid, pubDate or link set on feed item"));
}
Ok(hasher.finish())
}
}