mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Make [notifications.pushover] optional for non-pushover methods
The pushover config section is now only required when notifications.method is "pushover". Validation at startup rejects the missing-section case so a misconfiguration fails fast instead of erroring on every notification. Updates the sample TOML, README and nix module for consistency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6c4f8c22bb
commit
be2a8fce58
@@ -47,6 +47,7 @@ api_key = "CHANGE_ME" # API key the app uses to talk to the backend — change t
|
||||
[notifications]
|
||||
method = "pushover" # Use "none" to only log notifications instead of sending them
|
||||
|
||||
# Only needed when method = "pushover"; omit this section for any other method.
|
||||
[notifications.pushover]
|
||||
token = "" # Your Pushover application token
|
||||
user_key = "" # Your Pushover user key
|
||||
@@ -179,8 +180,8 @@ Options marked **Required** have no built-in default and must be set in your con
|
||||
|`snmp_scanner.timeout`|`5s`|Per-poll SNMP request timeout.|
|
||||
|`notifications.method`|**Required**|For now just pushover, you can set this to "none" to avoid sending notifications (it will just log)|
|
||||
|`notifications.notify_when_not_seen_for`|`1w`|Send a notification if a device comes back online after not being seen for this timeframe (you can use hours, weeks, etc.)|
|
||||
|`notifications.pushover.token`|**Required**|Your pushover token goes here, just copy&paste from their website after creating the app (may be left empty when `method` is `none`)|
|
||||
|`notifications.pushover.user_key`|**Required**|User key goes here, this is the account wide code for pushover (may be left empty when `method` is `none`)|
|
||||
|`notifications.pushover.token`|**Required when `method` is `pushover`**|Your pushover token goes here, just copy&paste from their website after creating the app. The whole `[notifications.pushover]` section may be omitted when `method` is anything other than `pushover`|
|
||||
|`notifications.pushover.user_key`|**Required when `method` is `pushover`**|User key goes here, this is the account wide code for pushover. The whole `[notifications.pushover]` section may be omitted when `method` is anything other than `pushover`|
|
||||
|`retention.window`|`365d`|How long to retain device events and notifications. Records older than this are purged daily. Accepts duration strings (e.g. `90d`, `1y`, `6m`).|
|
||||
|`device_events.deduplication_window`|`1m`|Suppress duplicate device events: if the same scanner sees the same device (same MAC and IPv4) again within this window, only one event is recorded. Accepts duration strings (e.g. `30s`, `1m`, `5m`).|
|
||||
|
||||
|
||||
+18
-9
@@ -185,9 +185,17 @@ fn send_notification(notification: Notification) -> Result<(), Box<dyn Error>> {
|
||||
debug!("About to send notification ({notification}).");
|
||||
|
||||
match get_settings().notifications.method.as_str() {
|
||||
"pushover" => {
|
||||
pushover::send_message(notification.title, notification.body)?;
|
||||
}
|
||||
"pushover" => match &get_settings().notifications.pushover {
|
||||
Some(pushover) => {
|
||||
pushover::send_message(pushover, notification.title, notification.body)?;
|
||||
}
|
||||
None => {
|
||||
error!(
|
||||
"Notification method is 'pushover' but no [notifications.pushover] section is \
|
||||
configured; cannot deliver notification."
|
||||
);
|
||||
}
|
||||
},
|
||||
other => {
|
||||
warn!("Notification method set to '{other}'. Set logs to 'info' to see notifications.");
|
||||
info!("Notification: {}", notification.body);
|
||||
@@ -294,8 +302,12 @@ pub fn trigger_existing_device(
|
||||
let vendor_changed_flag = vendor_changed(&existing_device.vendor, &new_device.vendor);
|
||||
|
||||
if ip_changed || vendor_changed_flag {
|
||||
let (title, body) =
|
||||
render_device_changed(&existing_device, &new_device, ip_changed, vendor_changed_flag);
|
||||
let (title, body) = render_device_changed(
|
||||
&existing_device,
|
||||
&new_device,
|
||||
ip_changed,
|
||||
vendor_changed_flag,
|
||||
);
|
||||
let notification = Notification::new(
|
||||
Utc::now(),
|
||||
NotificationType::DeviceChanged,
|
||||
@@ -411,10 +423,7 @@ mod tests {
|
||||
|
||||
let (title, body) = render_device_back_online(&device, "12d");
|
||||
|
||||
assert_eq!(
|
||||
title,
|
||||
"Device back online after 12d: bobs-iphone.local"
|
||||
);
|
||||
assert_eq!(title, "Device back online after 12d: bobs-iphone.local");
|
||||
assert!(body.contains("Registered to Bob"));
|
||||
assert!(body.contains("Absent for: 12d"));
|
||||
}
|
||||
|
||||
@@ -3,17 +3,13 @@ use pushover::API;
|
||||
use pushover::requests::message::SendMessage;
|
||||
|
||||
use crate::events::error::DeliveryError;
|
||||
use crate::settings::get_settings;
|
||||
use crate::settings::Pushover;
|
||||
|
||||
pub fn send_message(title: String, body: String) -> Result<(), DeliveryError> {
|
||||
pub fn send_message(config: &Pushover, title: String, body: String) -> Result<(), DeliveryError> {
|
||||
debug!("About to send message via pushover ({body})");
|
||||
let api = API::new();
|
||||
|
||||
let mut msg = SendMessage::new(
|
||||
get_settings().notifications.pushover.token.as_str(),
|
||||
get_settings().notifications.pushover.user_key.as_str(),
|
||||
body,
|
||||
);
|
||||
let mut msg = SendMessage::new(config.token.as_str(), config.user_key.as_str(), body);
|
||||
|
||||
msg.set_title(title);
|
||||
|
||||
|
||||
+80
-2
@@ -180,7 +180,8 @@ pub struct Pushover {
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Notifications {
|
||||
pub method: String,
|
||||
pub pushover: Pushover,
|
||||
// Only required when `method` is "pushover"; other methods leave this section out.
|
||||
pub pushover: Option<Pushover>,
|
||||
#[serde(default = "default_notify_when_not_seen_for")]
|
||||
pub notify_when_not_seen_for: DurationString,
|
||||
}
|
||||
@@ -255,7 +256,22 @@ impl Settings {
|
||||
.add_source(File::with_name(config_path.as_str()))
|
||||
.build()?;
|
||||
|
||||
local_settings.try_deserialize()
|
||||
let settings: Settings = local_settings.try_deserialize()?;
|
||||
settings.validate()?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
// Cross-field checks that serde can't express on its own. Run once at load time so a
|
||||
// misconfiguration fails fast at startup rather than on every notification attempt.
|
||||
fn validate(&self) -> Result<(), ConfigError> {
|
||||
if self.notifications.method == "pushover" && self.notifications.pushover.is_none() {
|
||||
return Err(ConfigError::Message(
|
||||
"notifications.method is \"pushover\" but the [notifications.pushover] section is \
|
||||
missing; add it with your token and user_key, or change notifications.method."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,6 +549,68 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_pushover_method_without_section() {
|
||||
// method = "pushover" but no [notifications.pushover] section: must fail at load time.
|
||||
const PUSHOVER_NO_SECTION: &str = r#"
|
||||
[database]
|
||||
path = "./oott.db"
|
||||
[networking]
|
||||
[log]
|
||||
level = "info"
|
||||
[notifications]
|
||||
method = "pushover"
|
||||
[web_server]
|
||||
api_key = "test"
|
||||
"#;
|
||||
let settings = parse(PUSHOVER_NO_SECTION);
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_non_pushover_method_without_section() {
|
||||
let settings = parse(NO_PUSHOVER_CONFIG);
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_pushover_method_with_section() {
|
||||
let settings = parse(BASE_CONFIG);
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
const NO_PUSHOVER_CONFIG: &str = r#"
|
||||
[database]
|
||||
path = "./oott.db"
|
||||
[networking]
|
||||
[log]
|
||||
level = "info"
|
||||
[notifications]
|
||||
method = "none"
|
||||
[web_server]
|
||||
api_key = "test"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn pushover_section_is_optional_for_non_pushover_methods() {
|
||||
// With a non-pushover method, the `[notifications.pushover]` section may be omitted
|
||||
// entirely; it should deserialize to `None` rather than fail parsing.
|
||||
let settings = parse(NO_PUSHOVER_CONFIG);
|
||||
assert_eq!(settings.notifications.method, "none");
|
||||
assert!(settings.notifications.pushover.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pushover_section_is_parsed_when_present() {
|
||||
let settings = parse(BASE_CONFIG);
|
||||
let pushover = settings
|
||||
.notifications
|
||||
.pushover
|
||||
.expect("pushover section should be parsed when present");
|
||||
assert_eq!(pushover.token, "");
|
||||
assert_eq!(pushover.user_key, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn networking_section_is_optional() {
|
||||
// The `[networking]` section has no mandatory fields, so omitting it entirely
|
||||
|
||||
@@ -39,6 +39,8 @@ enabled=true # Set to false to disable the DHCP scanner
|
||||
method="pushover" # For now just pushover, you can set this to "none" to avoid sending notifications (it will just log)
|
||||
notify_when_not_seen_for="1w" # Send a notification if a device comes back online after not being seen for this timeframe. Defaults to "1w" if omitted
|
||||
|
||||
# The [notifications.pushover] section is only required when method="pushover". For any other
|
||||
# method (e.g. "none") you can omit this whole section.
|
||||
[notifications.pushover]
|
||||
token="" # Your pushover token goes here, just copy&paste from their website after creating the app
|
||||
user_key="" # User key goes here, this is the account wide code for pushover
|
||||
|
||||
@@ -111,15 +111,23 @@ in {
|
||||
description = "Send a notification if a device comes back online after not being seen for this timeframe.";
|
||||
default = "1w";
|
||||
};
|
||||
notifications.pushover.token = mkOption {
|
||||
type = types.str;
|
||||
description = "Your pushover token goes here, just copy&paste from their website after creating the app.";
|
||||
default = "";
|
||||
};
|
||||
notifications.pushover.user_key = mkOption {
|
||||
type = types.str;
|
||||
description = "User key goes here, this is the account wide code for pushover.";
|
||||
default = "";
|
||||
notifications.pushover = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
token = mkOption {
|
||||
type = types.str;
|
||||
description = "Your pushover token goes here, just copy&paste from their website after creating the app.";
|
||||
default = "";
|
||||
};
|
||||
user_key = mkOption {
|
||||
type = types.str;
|
||||
description = "User key goes here, this is the account wide code for pushover.";
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
});
|
||||
description = "Pushover credentials. Only required when notifications.method is \"pushover\"; leave it null (the default) for any other method.";
|
||||
default = null;
|
||||
};
|
||||
retention.window = mkOption {
|
||||
type = types.str;
|
||||
|
||||
Reference in New Issue
Block a user