]> git.proxmox.com Git - proxmox.git/blob - proxmox-notify/src/endpoints/gotify.rs
notify: cleanup all the imports sections
[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::TemplateRenderer;
13 use crate::schema::ENTITY_NAME_SCHEMA;
14 use crate::{renderer, Endpoint, Error, Notification, 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 }
23 }
24
25 pub(crate) const GOTIFY_TYPENAME: &str = "gotify";
26
27 #[api(
28 properties: {
29 name: {
30 schema: ENTITY_NAME_SCHEMA,
31 },
32 comment: {
33 optional: true,
34 schema: COMMENT_SCHEMA,
35 },
36 filter: {
37 optional: true,
38 schema: ENTITY_NAME_SCHEMA,
39 },
40 }
41 )]
42 #[derive(Serialize, Deserialize, Updater, Default)]
43 #[serde(rename_all = "kebab-case")]
44 /// Config for Gotify notification endpoints
45 pub struct GotifyConfig {
46 /// Name of the endpoint
47 #[updater(skip)]
48 pub name: String,
49 /// Gotify Server URL
50 pub server: String,
51 /// Comment
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub comment: Option<String>,
54 /// Filter to apply
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub filter: Option<String>,
57 }
58
59 #[api()]
60 #[derive(Serialize, Deserialize, Clone, Updater)]
61 #[serde(rename_all = "kebab-case")]
62 /// Private configuration for Gotify notification endpoints.
63 /// This config will be saved to a separate configuration file with stricter
64 /// permissions (root:root 0600)
65 pub struct GotifyPrivateConfig {
66 /// Name of the endpoint
67 #[updater(skip)]
68 pub name: String,
69 /// Authentication token
70 pub token: String,
71 }
72
73 /// A Gotify notification endpoint.
74 pub struct GotifyEndpoint {
75 pub config: GotifyConfig,
76 pub private_config: GotifyPrivateConfig,
77 }
78
79 #[derive(Serialize, Deserialize)]
80 #[serde(rename_all = "kebab-case")]
81 pub enum DeleteableGotifyProperty {
82 Comment,
83 Filter,
84 }
85
86 impl Endpoint for GotifyEndpoint {
87 fn send(&self, notification: &Notification) -> Result<(), Error> {
88 let properties = notification.properties.as_ref();
89
90 let title = renderer::render_template(
91 TemplateRenderer::Plaintext,
92 &notification.title,
93 properties,
94 )?;
95 let message =
96 renderer::render_template(TemplateRenderer::Plaintext, &notification.body, properties)?;
97
98 // We don't have a TemplateRenderer::Markdown yet, so simply put everything
99 // in code tags. Otherwise tables etc. are not formatted properly
100 let message = format!("```\n{message}\n```");
101
102 let body = json!({
103 "title": &title,
104 "message": &message,
105 "priority": severity_to_priority(notification.severity),
106 "extras": {
107 "client::display": {
108 "contentType": "text/markdown"
109 }
110 }
111 });
112
113 let body = serde_json::to_vec(&body)
114 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
115 let extra_headers = HashMap::from([(
116 "Authorization".into(),
117 format!("Bearer {}", self.private_config.token),
118 )]);
119
120 let proxy_config = context()
121 .http_proxy_config()
122 .map(|url| ProxyConfig::parse_proxy_url(&url))
123 .transpose()
124 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
125
126 let options = HttpOptions {
127 proxy_config,
128 ..Default::default()
129 };
130
131 let client = Client::new(options);
132 let uri = format!("{}/message", self.config.server);
133
134 client
135 .post(
136 &uri,
137 Some(body.as_slice()),
138 Some("application/json"),
139 Some(&extra_headers),
140 )
141 .map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
142
143 Ok(())
144 }
145
146 fn name(&self) -> &str {
147 &self.config.name
148 }
149
150 fn filter(&self) -> Option<&str> {
151 self.config.filter.as_deref()
152 }
153 }