]>
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 WB |
11 | use crate::context::context; |
12 | use crate::renderer::TemplateRenderer; | |
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 { | |
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>, | |
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 | ||
84 | #[derive(Serialize, Deserialize)] | |
85 | #[serde(rename_all = "kebab-case")] | |
86 | pub enum DeleteableGotifyProperty { | |
87 | Comment, | |
306f4005 | 88 | Disable, |
990fc8ef LW |
89 | } |
90 | ||
91 | impl Endpoint for GotifyEndpoint { | |
92 | fn send(&self, notification: &Notification) -> Result<(), Error> { | |
df4858e9 LW |
93 | let (title, message) = match ¬ification.content { |
94 | Content::Template { | |
95 | title_template, | |
96 | body_template, | |
b421a7ca | 97 | data, |
df4858e9 LW |
98 | } => { |
99 | let rendered_title = | |
100 | renderer::render_template(TemplateRenderer::Plaintext, title_template, data)?; | |
101 | let rendered_message = | |
102 | renderer::render_template(TemplateRenderer::Plaintext, body_template, data)?; | |
103 | ||
104 | (rendered_title, rendered_message) | |
105 | } | |
5f7ac875 LW |
106 | #[cfg(feature = "mail-forwarder")] |
107 | Content::ForwardedMail { title, body, .. } => (title.clone(), body.clone()), | |
df4858e9 | 108 | }; |
48657113 LW |
109 | |
110 | // We don't have a TemplateRenderer::Markdown yet, so simply put everything | |
111 | // in code tags. Otherwise tables etc. are not formatted properly | |
112 | let message = format!("```\n{message}\n```"); | |
113 | ||
114 | let body = json!({ | |
115 | "title": &title, | |
116 | "message": &message, | |
b421a7ca | 117 | "priority": severity_to_priority(notification.metadata.severity), |
48657113 LW |
118 | "extras": { |
119 | "client::display": { | |
120 | "contentType": "text/markdown" | |
121 | } | |
122 | } | |
123 | }); | |
990fc8ef LW |
124 | |
125 | let body = serde_json::to_vec(&body) | |
126 | .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?; | |
6b393ac0 LW |
127 | let extra_headers = HashMap::from([ |
128 | ( | |
129 | "Authorization".into(), | |
130 | format!("Bearer {}", self.private_config.token), | |
131 | ), | |
132 | ("X-Gotify-Key".into(), self.private_config.token.clone()), | |
133 | ]); | |
990fc8ef | 134 | |
d44ce2c7 LW |
135 | let proxy_config = context() |
136 | .http_proxy_config() | |
137 | .map(|url| ProxyConfig::parse_proxy_url(&url)) | |
138 | .transpose() | |
139 | .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?; | |
140 | ||
141 | let options = HttpOptions { | |
142 | proxy_config, | |
143 | ..Default::default() | |
144 | }; | |
145 | ||
146 | let client = Client::new(options); | |
147 | let uri = format!("{}/message", self.config.server); | |
148 | ||
990fc8ef LW |
149 | client |
150 | .post( | |
151 | &uri, | |
152 | Some(body.as_slice()), | |
153 | Some("application/json"), | |
154 | Some(&extra_headers), | |
155 | ) | |
156 | .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?; | |
157 | ||
158 | Ok(()) | |
159 | } | |
160 | ||
161 | fn name(&self) -> &str { | |
162 | &self.config.name | |
163 | } | |
306f4005 LW |
164 | |
165 | /// Check if the endpoint is disabled | |
166 | fn disabled(&self) -> bool { | |
167 | self.config.disable.unwrap_or_default() | |
168 | } | |
990fc8ef | 169 | } |