]> git.proxmox.com Git - proxmox.git/blob - proxmox-notify/src/endpoints/gotify.rs
notify: endpoints: matcher: improve descriptions for API types
[proxmox.git] / proxmox-notify / src / endpoints / gotify.rs
1 use std::collections::HashMap;
2
3 use serde::{Deserialize, Serialize};
4 use serde_json::json;
5
6 use proxmox_http::client::sync::Client;
7 use proxmox_http::{HttpClient, HttpOptions, ProxyConfig};
8 use proxmox_schema::api_types::COMMENT_SCHEMA;
9 use proxmox_schema::{api, Updater};
10
11 use crate::context::context;
12 use crate::renderer::TemplateType;
13 use crate::schema::ENTITY_NAME_SCHEMA;
14 use crate::{renderer, Content, Endpoint, Error, Notification, Origin, Severity};
15
16 fn severity_to_priority(level: Severity) -> u32 {
17 match level {
18 Severity::Info => 1,
19 Severity::Notice => 3,
20 Severity::Warning => 5,
21 Severity::Error => 9,
22 Severity::Unknown => 3,
23 }
24 }
25
26 pub(crate) const GOTIFY_TYPENAME: &str = "gotify";
27
28 #[api(
29 properties: {
30 name: {
31 schema: ENTITY_NAME_SCHEMA,
32 },
33 comment: {
34 optional: true,
35 schema: COMMENT_SCHEMA,
36 },
37 }
38 )]
39 #[derive(Serialize, Deserialize, Updater, Default)]
40 #[serde(rename_all = "kebab-case")]
41 /// Config for Gotify notification endpoints
42 pub struct GotifyConfig {
43 /// Name of the endpoint.
44 #[updater(skip)]
45 pub name: String,
46 /// Gotify Server URL.
47 pub server: String,
48 /// Comment.
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub comment: Option<String>,
51 /// Deprecated.
52 #[serde(skip_serializing)]
53 #[updater(skip)]
54 pub filter: Option<String>,
55 /// Disable this target.
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub disable: Option<bool>,
58 /// Origin of this config entry.
59 #[serde(skip_serializing_if = "Option::is_none")]
60 #[updater(skip)]
61 pub origin: Option<Origin>,
62 }
63
64 #[api()]
65 #[derive(Serialize, Deserialize, Clone, Updater)]
66 #[serde(rename_all = "kebab-case")]
67 /// Private configuration for Gotify notification endpoints.
68 /// This config will be saved to a separate configuration file with stricter
69 /// permissions (root:root 0600)
70 pub struct GotifyPrivateConfig {
71 /// Name of the endpoint
72 #[updater(skip)]
73 pub name: String,
74 /// Authentication token
75 pub token: String,
76 }
77
78 /// A Gotify notification endpoint.
79 pub struct GotifyEndpoint {
80 pub config: GotifyConfig,
81 pub private_config: GotifyPrivateConfig,
82 }
83
84 #[api]
85 #[derive(Serialize, Deserialize)]
86 #[serde(rename_all = "kebab-case")]
87 pub enum DeleteableGotifyProperty {
88 /// Delete `comment`
89 Comment,
90 /// Delete `disable`
91 Disable,
92 }
93
94 impl Endpoint for GotifyEndpoint {
95 fn send(&self, notification: &Notification) -> Result<(), Error> {
96 let (title, message) = match &notification.content {
97 Content::Template {
98 template_name,
99 data,
100 } => {
101 let rendered_title =
102 renderer::render_template(TemplateType::Subject, template_name, data)?;
103 let rendered_message =
104 renderer::render_template(TemplateType::PlaintextBody, template_name, data)?;
105
106 (rendered_title, rendered_message)
107 }
108 #[cfg(feature = "mail-forwarder")]
109 Content::ForwardedMail { title, body, .. } => (title.clone(), body.clone()),
110 };
111
112 // We don't have a TemplateRenderer::Markdown yet, so simply put everything
113 // in code tags. Otherwise tables etc. are not formatted properly
114 let message = format!("```\n{message}\n```");
115
116 let body = json!({
117 "title": &title,
118 "message": &message,
119 "priority": severity_to_priority(notification.metadata.severity),
120 "extras": {
121 "client::display": {
122 "contentType": "text/markdown"
123 }
124 }
125 });
126
127 let body = serde_json::to_vec(&body)
128 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
129 let extra_headers = HashMap::from([
130 (
131 "Authorization".into(),
132 format!("Bearer {}", self.private_config.token),
133 ),
134 ("X-Gotify-Key".into(), self.private_config.token.clone()),
135 ]);
136
137 let proxy_config = context()
138 .http_proxy_config()
139 .map(|url| ProxyConfig::parse_proxy_url(&url))
140 .transpose()
141 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
142
143 let options = HttpOptions {
144 proxy_config,
145 ..Default::default()
146 };
147
148 let client = Client::new(options);
149 let uri = format!("{}/message", self.config.server);
150
151 client
152 .post(
153 &uri,
154 Some(body.as_slice()),
155 Some("application/json"),
156 Some(&extra_headers),
157 )
158 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
159
160 Ok(())
161 }
162
163 fn name(&self) -> &str {
164 &self.config.name
165 }
166
167 /// Check if the endpoint is disabled
168 fn disabled(&self) -> bool {
169 self.config.disable.unwrap_or_default()
170 }
171 }