notify: add gotify endpoint

Add an endpoint for Gotify [1], showing the how easy it is to add new
endpoint implementations.

[1] https://gotify.net/

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
This commit is contained in:
Lukas Wagner 2023-07-20 16:31:36 +02:00 committed by Wolfgang Bumiller
parent 21c5c9a0c7
commit 990fc8efd2
5 changed files with 163 additions and 3 deletions

View File

@ -12,6 +12,7 @@ handlebars = { workspace = true, optional = true }
lazy_static.workspace = true
log.workspace = true
openssl.workspace = true
proxmox-http = { workspace = true, features = ["client-sync"], optional = true }
proxmox-schema = { workspace = true, features = ["api-macro", "api-types"]}
proxmox-section-config = { workspace = true }
proxmox-sys = { workspace = true, optional = true }
@ -20,5 +21,6 @@ serde = { workspace = true, features = ["derive"]}
serde_json.workspace = true
[features]
default = ["sendmail"]
default = ["sendmail", "gotify"]
sendmail = ["dep:handlebars", "dep:proxmox-sys"]
gotify = ["dep:proxmox-http"]

View File

@ -24,6 +24,17 @@ fn config_init() -> SectionConfig {
SENDMAIL_SCHEMA,
));
}
#[cfg(feature = "gotify")]
{
use crate::endpoints::gotify::{GotifyConfig, GOTIFY_TYPENAME};
const GOTIFY_SCHEMA: &ObjectSchema = GotifyConfig::API_SCHEMA.unwrap_object_schema();
config.register_plugin(SectionConfigPlugin::new(
GOTIFY_TYPENAME.to_string(),
Some(String::from("name")),
GOTIFY_SCHEMA,
));
}
config
}
@ -31,6 +42,18 @@ fn config_init() -> SectionConfig {
fn private_config_init() -> SectionConfig {
let mut config = SectionConfig::new(&BACKEND_NAME_SCHEMA);
#[cfg(feature = "gotify")]
{
use crate::endpoints::gotify::{GotifyPrivateConfig, GOTIFY_TYPENAME};
const GOTIFY_SCHEMA: &ObjectSchema = GotifyPrivateConfig::API_SCHEMA.unwrap_object_schema();
config.register_plugin(SectionConfigPlugin::new(
GOTIFY_TYPENAME.to_string(),
Some(String::from("name")),
GOTIFY_SCHEMA,
));
}
config
}

View File

@ -0,0 +1,117 @@
use std::collections::HashMap;
use crate::schema::ENTITY_NAME_SCHEMA;
use crate::{Endpoint, Error, Notification, Severity};
use proxmox_schema::api_types::COMMENT_SCHEMA;
use serde::{Deserialize, Serialize};
use proxmox_http::client::sync::Client;
use proxmox_http::{HttpClient, HttpOptions};
use proxmox_schema::{api, Updater};
#[derive(Serialize)]
struct GotifyMessageBody<'a> {
title: &'a str,
message: &'a str,
priority: u32,
}
fn severity_to_priority(level: Severity) -> u32 {
match level {
Severity::Info => 1,
Severity::Notice => 3,
Severity::Warning => 5,
Severity::Error => 9,
}
}
pub(crate) const GOTIFY_TYPENAME: &str = "gotify";
#[api(
properties: {
name: {
schema: ENTITY_NAME_SCHEMA,
},
comment: {
optional: true,
schema: COMMENT_SCHEMA,
},
}
)]
#[derive(Serialize, Deserialize, Updater, Default)]
#[serde(rename_all = "kebab-case")]
/// Config for Gotify notification endpoints
pub struct GotifyConfig {
/// Name of the endpoint
#[updater(skip)]
pub name: String,
/// Gotify Server URL
pub server: String,
/// Comment
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
#[api()]
#[derive(Serialize, Deserialize, Clone, Updater)]
#[serde(rename_all = "kebab-case")]
/// Private configuration for Gotify notification endpoints.
/// This config will be saved to a separate configuration file with stricter
/// permissions (root:root 0600)
pub struct GotifyPrivateConfig {
/// Name of the endpoint
#[updater(skip)]
pub name: String,
/// Authentication token
pub token: String,
}
/// A Gotify notification endpoint.
pub struct GotifyEndpoint {
pub config: GotifyConfig,
pub private_config: GotifyPrivateConfig,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DeleteableGotifyProperty {
Comment,
}
impl Endpoint for GotifyEndpoint {
fn send(&self, notification: &Notification) -> Result<(), Error> {
// TODO: What about proxy configuration?
let client = Client::new(HttpOptions::default());
let uri = format!("{}/message", self.config.server);
let body = GotifyMessageBody {
title: &notification.title,
message: &notification.body,
priority: severity_to_priority(notification.severity),
};
let body = serde_json::to_vec(&body)
.map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
let extra_headers = HashMap::from([(
"Authorization".into(),
format!("Bearer {}", self.private_config.token),
)]);
client
.post(
&uri,
Some(body.as_slice()),
Some("application/json"),
Some(&extra_headers),
)
.map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
Ok(())
}
fn name(&self) -> &str {
&self.config.name
}
}

View File

@ -1,2 +1,4 @@
#[cfg(feature = "gotify")]
pub mod gotify;
#[cfg(feature = "sendmail")]
pub mod sendmail;

View File

@ -201,7 +201,6 @@ impl Bus {
let mut endpoints = HashMap::new();
// Instantiate endpoints
#[cfg(feature = "sendmail")]
{
use endpoints::sendmail::SENDMAIL_TYPENAME;
@ -218,6 +217,23 @@ impl Bus {
);
}
#[cfg(feature = "gotify")]
{
use endpoints::gotify::GOTIFY_TYPENAME;
use endpoints::gotify::{GotifyConfig, GotifyEndpoint, GotifyPrivateConfig};
endpoints.extend(
parse_endpoints_with_private_config!(
config,
GotifyConfig,
GotifyPrivateConfig,
GotifyEndpoint,
GOTIFY_TYPENAME
)?
.into_iter()
.map(|e| (e.name().into(), e)),
);
}
Ok(Bus { endpoints })
}