Merge branch 'ntfy' into 'main'
Send ntfy notifications See merge request kernald/reddit-magnet!14
This commit is contained in:
commit
d76a523aeb
10 changed files with 289 additions and 44 deletions
56
Cargo.lock
generated
56
Cargo.lock
generated
|
|
@ -1296,6 +1296,19 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntfy"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bb106c0f23b713c6472d05eae4a8a8fd9b3e57ed4c4c68bce5c92875ceeea1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1490,7 +1503,7 @@ dependencies = [
|
|||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
|
|
@ -1510,7 +1523,7 @@ dependencies = [
|
|||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"thiserror 2.0.12",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
|
|
@ -1592,6 +1605,7 @@ dependencies = [
|
|||
"log",
|
||||
"magnet-url",
|
||||
"multimap",
|
||||
"ntfy",
|
||||
"pretty_env_logger",
|
||||
"regex",
|
||||
"reqwest 0.12.15",
|
||||
|
|
@ -1613,7 +1627,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
|||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1724,6 +1738,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-socks",
|
||||
"tower",
|
||||
"tower-service",
|
||||
"url",
|
||||
|
|
@ -2104,13 +2119,33 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2237,6 +2272,18 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-socks"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"futures-util",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.15"
|
||||
|
|
@ -2417,6 +2464,7 @@ dependencies = [
|
|||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -33,3 +33,4 @@ console = "0.15.8"
|
|||
reqwest = "0.12.15"
|
||||
magnet-url = "2.0.0"
|
||||
urlencoding = "2.1.3"
|
||||
ntfy = "0.7.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::actions::bitmagnet::BitmagnetConfig;
|
||||
use crate::actions::transmission::TransmissionConfig;
|
||||
use crate::args::Args;
|
||||
use crate::notifications::ntfy::NtfyConfig;
|
||||
use color_eyre::eyre::{eyre, Result, WrapErr};
|
||||
use directories::ProjectDirs;
|
||||
use figment::providers::Env;
|
||||
|
|
@ -31,6 +32,9 @@ pub struct Config {
|
|||
#[serde(default)]
|
||||
pub transmission: Option<TransmissionConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub ntfy: Option<NtfyConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub sources: HashMap<String, SourceConfig>,
|
||||
}
|
||||
|
|
|
|||
47
src/main.rs
47
src/main.rs
|
|
@ -5,6 +5,8 @@ use crate::args::Args;
|
|||
use crate::config::{get_db_path, load_config};
|
||||
use crate::db::Database;
|
||||
use crate::magnet::{extract_magnet_links, Magnet};
|
||||
use crate::notifications::notification::Notification;
|
||||
use crate::notifications::ntfy::NtfyNotification;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::{eyre, Result, WrapErr};
|
||||
|
|
@ -21,6 +23,7 @@ mod config;
|
|||
mod db;
|
||||
mod magnet;
|
||||
mod models;
|
||||
mod notifications;
|
||||
mod reddit_client;
|
||||
mod report;
|
||||
mod schema;
|
||||
|
|
@ -172,6 +175,28 @@ async fn main() -> Result<()> {
|
|||
debug!("No Transmission configuration found");
|
||||
}
|
||||
|
||||
// Initialize notifications
|
||||
let mut notifications: Vec<Box<dyn Notification>> = Vec::new();
|
||||
|
||||
// Add Ntfy notification if enabled
|
||||
if let Some(ntfy_config) = conf.ntfy {
|
||||
if ntfy_config.enable {
|
||||
info!("Initializing Ntfy notification");
|
||||
match NtfyNotification::new(ntfy_config) {
|
||||
Ok(ntfy_notification) => {
|
||||
notifications.push(Box::new(ntfy_notification));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize Ntfy notification: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Ntfy notification is disabled");
|
||||
}
|
||||
} else {
|
||||
debug!("No Ntfy configuration found");
|
||||
}
|
||||
|
||||
// Process all actions and collect results
|
||||
let mut action_results = HashMap::new();
|
||||
|
||||
|
|
@ -194,7 +219,27 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
for line in report::generate_report(&action_results, total_new_links).lines() {
|
||||
// Send notifications
|
||||
for notification in ¬ifications {
|
||||
match notification
|
||||
.send_notification(&action_results, total_new_links)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
debug!("Successfully sent notification to {}", notification.name());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to send notification to {}: {}",
|
||||
notification.name(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and display report
|
||||
for line in report::generate_report(&action_results, total_new_links, true).lines() {
|
||||
info!("{}", line);
|
||||
}
|
||||
|
||||
|
|
|
|||
2
src/notifications/mod.rs
Normal file
2
src/notifications/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod notification;
|
||||
pub mod ntfy;
|
||||
31
src/notifications/notification.rs
Normal file
31
src/notifications/notification.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::actions::action::ProcessedMagnets;
|
||||
use async_trait::async_trait;
|
||||
use color_eyre::eyre::Result;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Trait for notification services
|
||||
#[async_trait]
|
||||
pub trait Notification {
|
||||
/// Return the name of the notification service
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Send a notification about the processing results
|
||||
async fn send_notification(
|
||||
&self,
|
||||
action_results: &HashMap<String, ProcessedMagnets>,
|
||||
total_new_links: usize,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Determine if a notification should be sent based on the results
|
||||
fn should_notify(
|
||||
&self,
|
||||
action_results: &HashMap<String, ProcessedMagnets>,
|
||||
total_new_links: usize,
|
||||
) -> bool {
|
||||
// Don't send notification if everything was successful but 0 links were grabbed and processed
|
||||
if total_new_links == 0 && action_results.values().all(|pm| pm.failed.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
23
src/notifications/ntfy/config.rs
Normal file
23
src/notifications/ntfy/config.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Configuration for the ntfy notification service
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NtfyConfig {
|
||||
/// Whether to enable the ntfy notification service
|
||||
#[serde(default)]
|
||||
pub enable: bool,
|
||||
|
||||
/// The host URL of the ntfy server
|
||||
pub host: String,
|
||||
|
||||
/// The username for authentication (optional)
|
||||
#[serde(default)]
|
||||
pub username: Option<String>,
|
||||
|
||||
/// The password for authentication (optional)
|
||||
#[serde(default)]
|
||||
pub password: Option<String>,
|
||||
|
||||
/// The topic to publish notifications to
|
||||
pub topic: String,
|
||||
}
|
||||
5
src/notifications/ntfy/mod.rs
Normal file
5
src/notifications/ntfy/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod config;
|
||||
pub mod notification;
|
||||
|
||||
pub use config::NtfyConfig;
|
||||
pub use notification::NtfyNotification;
|
||||
68
src/notifications/ntfy/notification.rs
Normal file
68
src/notifications/ntfy/notification.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use crate::actions::action::ProcessedMagnets;
|
||||
use crate::notifications::notification::Notification;
|
||||
use crate::notifications::ntfy::config::NtfyConfig;
|
||||
use crate::report;
|
||||
use async_trait::async_trait;
|
||||
use color_eyre::eyre::Result;
|
||||
use log::{debug, info};
|
||||
use ntfy::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Notification service using ntfy
|
||||
pub struct NtfyNotification {
|
||||
config: NtfyConfig,
|
||||
client: Dispatcher<Async>,
|
||||
}
|
||||
|
||||
impl NtfyNotification {
|
||||
/// Create a new NtfyNotification instance
|
||||
pub fn new(config: NtfyConfig) -> Result<Self> {
|
||||
let mut builder = dispatcher::builder(config.host.clone());
|
||||
|
||||
// Add authentication if provided
|
||||
if let (Some(username), Some(password)) = (&config.username, &config.password) {
|
||||
builder = builder.credentials(Auth::credentials(username, password))
|
||||
}
|
||||
|
||||
let client = builder.build_async()?;
|
||||
|
||||
Ok(NtfyNotification { config, client })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Notification for NtfyNotification {
|
||||
fn name(&self) -> &str {
|
||||
"Ntfy"
|
||||
}
|
||||
|
||||
async fn send_notification(
|
||||
&self,
|
||||
action_results: &HashMap<String, ProcessedMagnets>,
|
||||
total_new_links: usize,
|
||||
) -> Result<()> {
|
||||
if !self.should_notify(action_results, total_new_links) {
|
||||
debug!("Skipping notification as there's nothing to report");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let priority = if action_results.values().any(|pm| !pm.failed.is_empty()) {
|
||||
Priority::High
|
||||
} else {
|
||||
Priority::Default
|
||||
};
|
||||
|
||||
let payload = Payload::new(&self.config.topic)
|
||||
.message(report::generate_report(
|
||||
action_results,
|
||||
total_new_links,
|
||||
false,
|
||||
))
|
||||
.priority(priority);
|
||||
|
||||
self.client.send(&payload).await?;
|
||||
|
||||
info!("Sent notification to ntfy topic: {}", self.config.topic);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::actions::action::ProcessedMagnets;
|
||||
use color_eyre::eyre::Result;
|
||||
use console::{style, Emoji};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -17,10 +16,23 @@ fn pluralize(count: usize, singular: &str, plural: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Helper function to conditionally apply style
|
||||
/// Note that passing `true` doesn't enforce colours - console-rs/console has some logic to disable
|
||||
/// them automagically.
|
||||
fn conditional_style(text: &str, use_colors: bool) -> console::StyledObject<&str> {
|
||||
let styled = style(text);
|
||||
if use_colors {
|
||||
styled
|
||||
} else {
|
||||
styled.force_styling(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a report of processed magnets
|
||||
pub fn generate_report(
|
||||
action_results: &HashMap<String, ProcessedMagnets>,
|
||||
total_new_links: usize,
|
||||
use_colors: bool,
|
||||
) -> String {
|
||||
let mut report = String::new();
|
||||
|
||||
|
|
@ -28,11 +40,16 @@ pub fn generate_report(
|
|||
report.push_str(&format!(
|
||||
"{} {} {}\n",
|
||||
SPARKLES,
|
||||
style("Report Summary").bold().underlined(),
|
||||
style(format!(
|
||||
"{} added to the database",
|
||||
pluralize(total_new_links, "new link", "new links")
|
||||
))
|
||||
conditional_style("Report Summary", use_colors)
|
||||
.bold()
|
||||
.underlined(),
|
||||
conditional_style(
|
||||
&format!(
|
||||
"{} added to the database",
|
||||
pluralize(total_new_links, "new link", "new links")
|
||||
),
|
||||
use_colors
|
||||
)
|
||||
.bold()
|
||||
.green(),
|
||||
));
|
||||
|
|
@ -48,11 +65,14 @@ pub fn generate_report(
|
|||
report.push_str(&format!(
|
||||
"{} {} {}\n",
|
||||
ROCKET,
|
||||
style(format!("{}", action_name)).bold().underlined().cyan(),
|
||||
style(format!(
|
||||
"({})",
|
||||
pluralize(total_count, "new link", "new links")
|
||||
))
|
||||
conditional_style(&format!("{}", action_name), use_colors)
|
||||
.bold()
|
||||
.underlined()
|
||||
.cyan(),
|
||||
conditional_style(
|
||||
&format!("({})", pluralize(total_count, "new link", "new links")),
|
||||
use_colors
|
||||
)
|
||||
.bold()
|
||||
));
|
||||
|
||||
|
|
@ -65,16 +85,22 @@ pub fn generate_report(
|
|||
if failed_count > 0 {
|
||||
report.push_str(&format!(
|
||||
" {} Success: {}, {} Failure: {}\n",
|
||||
style("✅").green(),
|
||||
style(success_count).bold().green(),
|
||||
style("❌").red(),
|
||||
style(failed_count).bold().red()
|
||||
conditional_style("✅", use_colors).green(),
|
||||
conditional_style(&success_count.to_string(), use_colors)
|
||||
.bold()
|
||||
.green(),
|
||||
conditional_style("❌", use_colors).red(),
|
||||
conditional_style(&failed_count.to_string(), use_colors)
|
||||
.bold()
|
||||
.red()
|
||||
));
|
||||
} else {
|
||||
report.push_str(&format!(
|
||||
" {} Success: {}\n",
|
||||
style("✅").green(),
|
||||
style(success_count).bold().green()
|
||||
conditional_style("✅", use_colors).green(),
|
||||
conditional_style(&success_count.to_string(), use_colors)
|
||||
.bold()
|
||||
.green()
|
||||
));
|
||||
}
|
||||
report.push_str("\n");
|
||||
|
|
@ -83,14 +109,14 @@ pub fn generate_report(
|
|||
if success_count > 0 {
|
||||
report.push_str(&format!(
|
||||
" {} {}\n",
|
||||
style("✅").green(),
|
||||
style("Added:").bold().green()
|
||||
conditional_style("✅", use_colors).green(),
|
||||
conditional_style("Added:", use_colors).bold().green()
|
||||
));
|
||||
for magnet in &processed_magnets.success {
|
||||
report.push_str(&format!(
|
||||
" {} {}\n",
|
||||
LINK,
|
||||
style(&magnet.title).italic()
|
||||
conditional_style(&magnet.title, use_colors).italic()
|
||||
));
|
||||
}
|
||||
report.push_str("\n");
|
||||
|
|
@ -100,14 +126,14 @@ pub fn generate_report(
|
|||
if failed_count > 0 {
|
||||
report.push_str(&format!(
|
||||
" {} {}\n",
|
||||
style("❌").red(),
|
||||
style("Failed:").bold().red()
|
||||
conditional_style("❌", use_colors).red(),
|
||||
conditional_style("Failed:", use_colors).bold().red()
|
||||
));
|
||||
for magnet in &processed_magnets.failed {
|
||||
report.push_str(&format!(
|
||||
" {} {}\n",
|
||||
WARNING,
|
||||
style(&magnet.title).italic()
|
||||
conditional_style(&magnet.title, use_colors).italic()
|
||||
));
|
||||
}
|
||||
report.push_str("\n");
|
||||
|
|
@ -121,14 +147,7 @@ pub fn generate_report(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::Magnet;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
|
||||
// Helper function to strip ANSI color codes from a string for testing
|
||||
fn strip_ansi_codes(s: &str) -> String {
|
||||
// This regex matches ANSI escape codes
|
||||
let re = regex::Regex::new(r"\x1B\[[0-9;]*[a-zA-Z]").unwrap();
|
||||
re.replace_all(s, "").to_string()
|
||||
}
|
||||
use chrono::DateTime;
|
||||
|
||||
fn create_test_magnet(title: &str) -> Magnet {
|
||||
Magnet {
|
||||
|
|
@ -147,11 +166,10 @@ mod tests {
|
|||
let action_results = HashMap::new();
|
||||
let total_new_links = 0;
|
||||
|
||||
let report = generate_report(&action_results, total_new_links);
|
||||
let clean_report = strip_ansi_codes(&report);
|
||||
let report = generate_report(&action_results, total_new_links, false);
|
||||
|
||||
assert!(clean_report.contains("Report Summary"));
|
||||
assert!(clean_report.contains("0 new links added to the database"));
|
||||
assert!(report.contains("Report Summary"));
|
||||
assert!(report.contains("0 new links added to the database"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -168,7 +186,7 @@ mod tests {
|
|||
action_results.insert("TestAction".to_string(), processed_magnets);
|
||||
let total_new_links = 2;
|
||||
|
||||
let report = strip_ansi_codes(&generate_report(&action_results, total_new_links));
|
||||
let report = generate_report(&action_results, total_new_links, false);
|
||||
|
||||
assert!(report.contains("Report Summary"));
|
||||
assert!(report.contains("2 new links added to the database"));
|
||||
|
|
@ -193,7 +211,7 @@ mod tests {
|
|||
action_results.insert("TestAction".to_string(), processed_magnets);
|
||||
let total_new_links = 0;
|
||||
|
||||
let report = strip_ansi_codes(&generate_report(&action_results, total_new_links));
|
||||
let report = generate_report(&action_results, total_new_links, false);
|
||||
|
||||
assert!(report.contains("Report Summary"));
|
||||
assert!(report.contains("0 new links added to the database"));
|
||||
|
|
@ -215,7 +233,7 @@ mod tests {
|
|||
action_results.insert("TestAction".to_string(), processed_magnets);
|
||||
let total_new_links = 1;
|
||||
|
||||
let report = strip_ansi_codes(&generate_report(&action_results, total_new_links));
|
||||
let report = generate_report(&action_results, total_new_links, false);
|
||||
|
||||
assert!(report.contains("Report Summary"));
|
||||
assert!(report.contains("1 new link added to the database"));
|
||||
|
|
@ -253,7 +271,7 @@ mod tests {
|
|||
action_results.insert("EmptyAction".to_string(), processed_magnets3);
|
||||
let total_new_links = 3;
|
||||
|
||||
let report = strip_ansi_codes(&generate_report(&action_results, total_new_links));
|
||||
let report = generate_report(&action_results, total_new_links, false);
|
||||
|
||||
assert!(report.contains("Report Summary"));
|
||||
assert!(report.contains("3 new links added to the database"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue