diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4b536bb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "webmention-filer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +#webmention = { version = "0.5", features = ["receive"] } +webmention = { git = "https://github.com/DanielMowitz/webmention.git", features = [ "receive" ] } +native-tls = "0.2" +url = { version = "2.2", features = [ "serde" ] } # working with URLs +anyhow = "1" # wrapping errors +rocket = "=0.5.0-rc.3" +clap = { version = "4.3", features = [ "derive" ] } # for CLI +tokio = "1" +chrono = "0.4" +async-std = "1.12" + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e953e99 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,130 @@ +#[macro_use] extern crate rocket; + +use url::Url; +use clap::Parser; + +mod storage { + use std::sync::{Arc, Mutex}; + + use webmention::{Webmention, storage::WebmentionStorage, error::WebmentionError}; + use url::Url; + use std::fs::{OpenOptions, create_dir_all}; + use std::io::prelude::*; + + #[derive(Debug)] + pub struct DailyFileStorage { + storage_dir: String, + mentions: Arc>>, + } + + impl DailyFileStorage { + pub fn new(storage_dir: String) -> Self { + DailyFileStorage { + storage_dir, + mentions: Arc::new(Mutex::new(Vec::new())), + } + } + } + + impl WebmentionStorage for DailyFileStorage { + fn store(&self, mention: Webmention) -> Result<(), WebmentionError> { + { + let now = chrono::offset::Local::now(); + let daily_file_name = format!("{}/{}", + self.storage_dir, + now.date_naive()); + let mention_line = format!("{}\t{} mentioned {}", + now.time(), + mention.source, + mention.target); + + create_dir_all(self.storage_dir.clone()).unwrap(); + let mut daily_file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(daily_file_name) + .unwrap(); + + if let Err(e) = writeln!(daily_file, "{}", mention_line) { + eprintln!("Couldn't write to file: {}", e); + } + } + Ok(()) + } + + fn lookup_by_target(&self, url: Url) -> Result, WebmentionError> { + let mut view: Vec = Vec::new(); + let lock = self.mentions.lock().unwrap(); + for mention in lock.iter() { + if mention.target.eq(&url) { + view.push(mention.clone()); + } + } + Ok(view) + } + } +} + +mod receive { + use rocket::{form::Form, State}; + use url::Url; + use crate::storage::DailyFileStorage; + + #[derive(FromForm)] + pub struct WebmentionAttempt { + source: String, + target: String, + } + + #[post("/webmention", data = "")] + pub async fn webmention_endpoint( + storage: &State, + config_url: &State, + webmention: Form, + ) -> Option<&'static str> { + let urls = ( + Url::parse(&webmention.source), + Url::parse(&webmention.target), + ); + + if let Ok(source_url) = urls.0 { + if let Ok(target_url) = urls.1 { + if target_url.domain() != config_url.domain() { + return None; + } + + println!("Calling store with source {} and target {}", &source_url, &target_url); + match webmention::receive_webmention( + &**storage, + &source_url, + &target_url, + ).await { + Ok(true) => return Some("OK"), + Ok(false) => {println!("Webmention check did not succeed!"); return None}, + Err(e) => {println!("{:?}", e); return None}, + } + } + } + return None + } +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(short, long)] + domain: String, + #[arg(short, long, default_value_t = String::from("/tmp/webmention-storage"))] + storage_dir: String, +} + +#[launch] +fn rocket() -> _ { + let args = Args::parse(); + + rocket::build() + .manage(crate::storage::DailyFileStorage::new(args.storage_dir)) + .manage(Url::parse(&args.domain).unwrap()) + .mount("/", routes![receive::webmention_endpoint]) +}