mirror of
https://codeberg.org/icewind/rss-webhook-trigger.git
synced 2026-06-03 09:54:18 +02:00
initial version
This commit is contained in:
parent
0ee3aec432
commit
51cba405ec
7 changed files with 1414 additions and 5 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
config.toml
|
||||||
1254
Cargo.lock
generated
Normal file
1254
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
|
@ -1,9 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "docker-rss-trigger"
|
name = "rss-webhook-trigger"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Robin Appelman <robin@icewind.nl>"]
|
authors = ["Robin Appelman <robin@icewind.nl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[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
17
README.md
Normal 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
5
config.toml
Normal 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
28
src/config.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/main.rs
104
src/main.rs
|
|
@ -1,3 +1,103 @@
|
||||||
fn main() {
|
mod config;
|
||||||
println!("Hello, world!");
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue