]>
Commit | Line | Data |
---|---|---|
990fc8ef LW |
1 | use std::collections::HashMap; |
2 | ||
990fc8ef | 3 | use serde::{Deserialize, Serialize}; |
48657113 | 4 | use serde_json::json; |
990fc8ef LW |
5 | |
6 | use proxmox_http::client::sync::Client; | |
d44ce2c7 | 7 | use proxmox_http::{HttpClient, HttpOptions, ProxyConfig}; |
7cb339df | 8 | use proxmox_schema::api_types::COMMENT_SCHEMA; |
990fc8ef LW |
9 | use proxmox_schema::{api, Updater}; |
10 | ||
7cb339df | 11 | use crate::context::context; |
1516cc26 | 12 | use crate::renderer::TemplateType; |
7cb339df | 13 | use crate::schema::ENTITY_NAME_SCHEMA; |
9bea76c6 | 14 | use crate::{renderer, Content, Endpoint, Error, Notification, Origin, Severity}; |
7cb339df | 15 | |
990fc8ef LW |
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, | |
5f7ac875 | 22 | Severity::Unknown => 3, |
990fc8ef LW |
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 { | |
08b7c501 | 43 | /// Name of the endpoint. |
990fc8ef LW |
44 | #[updater(skip)] |
45 | pub name: String, | |
08b7c501 | 46 | /// Gotify Server URL. |
990fc8ef | 47 | pub server: String, |
08b7c501 | 48 | /// Comment. |
990fc8ef LW |
49 | #[serde(skip_serializing_if = "Option::is_none")] |
50 | pub comment: Option<String>, | |
b421a7ca LW |
51 | /// Deprecated. |
52 | #[serde(skip_serializing)] | |
53 | #[updater(skip)] | |
ee0ab52b | 54 | pub filter: Option<String>, |
306f4005 LW |
55 | /// Disable this target. |
56 | #[serde(skip_serializing_if = "Option::is_none")] | |
57 | pub disable: Option<bool>, | |
9bea76c6 LW |
58 | /// Origin of this config entry. |
59 | #[serde(skip_serializing_if = "Option::is_none")] | |
60 | #[updater(skip)] | |
61 | pub origin: Option<Origin>, | |
990fc8ef LW |
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 | ||
1a40d340 | 84 | #[api] |
990fc8ef LW |
85 | #[derive(Serialize, Deserialize)] |
86 | #[serde(rename_all = "kebab-case")] | |
87 | pub enum DeleteableGotifyProperty { | |
1a40d340 | 88 | /// Delete `comment` |
990fc8ef | 89 | Comment, |
1a40d340 | 90 | /// Delete `disable` |
306f4005 | 91 | Disable, |
990fc8ef LW |
92 | } |
93 | ||
94 | impl Endpoint for GotifyEndpoint { | |
95 | fn send(&self, notification: &Notification) -> Result<(), Error> { | |
df4858e9 LW |
96 | let (title, message) = match ¬ification.content { |
97 | Content::Template { | |
1516cc26 | 98 | template_name, |
b421a7ca | 99 | data, |
df4858e9 LW |
100 | } => { |
101 | let rendered_title = | |
1516cc26 | 102 | renderer::render_template(TemplateType::Subject, template_name, data)?; |
df4858e9 | 103 | let rendered_message = |
1516cc26 | 104 | renderer::render_template(TemplateType::PlaintextBody, template_name, data)?; |
df4858e9 LW |
105 | |
106 | (rendered_title, rendered_message) | |
107 | } | |
5f7ac875 LW |
108 | #[cfg(feature = "mail-forwarder")] |
109 | Content::ForwardedMail { title, body, .. } => (title.clone(), body.clone()), | |
df4858e9 | 110 | }; |
48657113 LW |
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, | |
b421a7ca | 119 | "priority": severity_to_priority(notification.metadata.severity), |
48657113 LW |
120 | "extras": { |
121 | "client::display": { | |
122 | "contentType": "text/markdown" | |
123 | } | |
124 | } | |
125 | }); | |
990fc8ef LW |
126 | |
127 | let body = serde_json::to_vec(&body) | |
128 | .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?; | |
6b393ac0 LW |
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 | ]); | |
990fc8ef | 136 | |
d44ce2c7 LW |
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 | ||
990fc8ef LW |
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 | } | |
306f4005 LW |
166 | |
167 | /// Check if the endpoint is disabled | |
168 | fn disabled(&self) -> bool { | |
169 | self.config.disable.unwrap_or_default() | |
170 | } | |
990fc8ef | 171 | } |