]> git.basschouten.com Git - openhab-addons.git/blob
513c774b51c1929e1e76389d11c52c2f8fb8a863
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.pushover.internal.connection;
14
15 import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
16
17 import java.io.File;
18 import java.io.IOException;
19 import java.nio.file.Files;
20 import java.nio.file.Path;
21 import java.time.Duration;
22 import java.util.List;
23 import java.util.stream.Collectors;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.api.ContentProvider;
28 import org.eclipse.jetty.client.util.MultiPartContentProvider;
29 import org.eclipse.jetty.client.util.PathContentProvider;
30 import org.eclipse.jetty.client.util.StringContentProvider;
31 import org.openhab.core.i18n.CommunicationException;
32 import org.openhab.core.i18n.ConfigurationException;
33 import org.openhab.core.io.net.http.HttpUtil;
34 import org.openhab.core.library.types.RawType;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * The {@link PushoverMessageBuilder} builds the body for Pushover Messages API requests.
40  *
41  * @author Christoph Weitkamp - Initial contribution
42  */
43 @NonNullByDefault
44 public class PushoverMessageBuilder {
45
46     private final Logger logger = LoggerFactory.getLogger(PushoverMessageBuilder.class);
47
48     public static final String MESSAGE_KEY_TOKEN = "token";
49     private static final String MESSAGE_KEY_USER = "user";
50     private static final String MESSAGE_KEY_MESSAGE = "message";
51     private static final String MESSAGE_KEY_TITLE = "title";
52     private static final String MESSAGE_KEY_DEVICE = "device";
53     private static final String MESSAGE_KEY_PRIORITY = "priority";
54     private static final String MESSAGE_KEY_RETRY = "retry";
55     private static final String MESSAGE_KEY_EXPIRE = "expire";
56     private static final String MESSAGE_KEY_TTL = "ttl";
57     private static final String MESSAGE_KEY_URL = "url";
58     private static final String MESSAGE_KEY_URL_TITLE = "url_title";
59     private static final String MESSAGE_KEY_SOUND = "sound";
60     private static final String MESSAGE_KEY_ATTACHMENT = "attachment";
61     public static final String MESSAGE_KEY_HTML = "html";
62     public static final String MESSAGE_KEY_MONOSPACE = "monospace";
63
64     private static final int MAX_MESSAGE_LENGTH = 1024;
65     private static final int MAX_TITLE_LENGTH = 250;
66     private static final int MAX_DEVICE_LENGTH = 25;
67     private static final List<Integer> VALID_PRIORITY_LIST = List.of(-2, -1, 0, 1, 2);
68     private static final int DEFAULT_PRIORITY = 0;
69     public static final int EMERGENCY_PRIORITY = 2;
70     private static final int MIN_RETRY_SECONDS = 30;
71     private static final int MAX_EXPIRE_SECONDS = 10800;
72     private static final int MAX_URL_LENGTH = 512;
73     private static final int MAX_URL_TITLE_LENGTH = 100;
74     public static final String DEFAULT_CONTENT_TYPE = "image/jpeg";
75
76     private final MultiPartContentProvider body = new MultiPartContentProvider();
77
78     private @Nullable String message;
79     private @Nullable String title;
80     private @Nullable String device;
81     private int priority = DEFAULT_PRIORITY;
82     private int retry = 300;
83     private int expire = 3600;
84     private Duration ttl = Duration.ZERO;
85     private @Nullable String url;
86     private @Nullable String urlTitle;
87     private @Nullable String sound;
88     private @Nullable String attachment;
89     private @Nullable String contentType;
90     private boolean html = false;
91     private boolean monospace = false;
92
93     private PushoverMessageBuilder(String apikey, String user) throws ConfigurationException {
94         body.addFieldPart(MESSAGE_KEY_TOKEN, new StringContentProvider(apikey), null);
95         body.addFieldPart(MESSAGE_KEY_USER, new StringContentProvider(user), null);
96     }
97
98     public static PushoverMessageBuilder getInstance(@Nullable String apikey, @Nullable String user)
99             throws ConfigurationException {
100         if (apikey == null || apikey.isBlank()) {
101             throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_APIKEY);
102         }
103
104         if (user == null || user.isBlank()) {
105             throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_USER);
106         }
107
108         return new PushoverMessageBuilder(apikey, user);
109     }
110
111     public PushoverMessageBuilder withMessage(String message) {
112         this.message = message;
113         return this;
114     }
115
116     public PushoverMessageBuilder withTitle(String title) {
117         this.title = title;
118         return this;
119     }
120
121     public PushoverMessageBuilder withDevice(String device) {
122         this.device = device;
123         return this;
124     }
125
126     public PushoverMessageBuilder withPriority(int priority) {
127         this.priority = priority;
128         return this;
129     }
130
131     public PushoverMessageBuilder withRetry(int retry) {
132         this.retry = retry;
133         return this;
134     }
135
136     public PushoverMessageBuilder withExpire(int expire) {
137         this.expire = expire;
138         return this;
139     }
140
141     public PushoverMessageBuilder withTTL(Duration ttl) {
142         this.ttl = ttl;
143         return this;
144     }
145
146     public PushoverMessageBuilder withUrl(String url) {
147         this.url = url;
148         return this;
149     }
150
151     public PushoverMessageBuilder withUrlTitle(String urlTitle) {
152         this.urlTitle = urlTitle;
153         return this;
154     }
155
156     public PushoverMessageBuilder withSound(String sound) {
157         this.sound = sound;
158         return this;
159     }
160
161     public PushoverMessageBuilder withAttachment(String attachment) {
162         this.attachment = attachment;
163         return this;
164     }
165
166     public PushoverMessageBuilder withContentType(String contentType) {
167         this.contentType = contentType;
168         return this;
169     }
170
171     public PushoverMessageBuilder withHtmlFormatting() {
172         this.html = true;
173         return this;
174     }
175
176     public PushoverMessageBuilder withMonospaceFormatting() {
177         this.monospace = true;
178         return this;
179     }
180
181     public ContentProvider build() throws CommunicationException {
182         String message = this.message;
183         if (message != null) {
184             if (message.length() > MAX_MESSAGE_LENGTH) {
185                 throw new IllegalArgumentException(String.format(
186                         "Skip sending the message as 'message' is longer than %d characters.", MAX_MESSAGE_LENGTH));
187             }
188             body.addFieldPart(MESSAGE_KEY_MESSAGE, new StringContentProvider(message), null);
189         }
190
191         String title = this.title;
192         if (title != null) {
193             if (title.length() > MAX_TITLE_LENGTH) {
194                 throw new IllegalArgumentException(String
195                         .format("Skip sending the message as 'title' is longer than %d characters.", MAX_TITLE_LENGTH));
196             }
197             body.addFieldPart(MESSAGE_KEY_TITLE, new StringContentProvider(title), null);
198         }
199
200         String device = this.device;
201         if (device != null) {
202             if (device.length() > MAX_DEVICE_LENGTH) {
203                 logger.warn("Skip 'device' as it is longer than {} characters. Got: {}.", MAX_DEVICE_LENGTH, device);
204             } else {
205                 body.addFieldPart(MESSAGE_KEY_DEVICE, new StringContentProvider(device), null);
206             }
207         }
208
209         if (priority != DEFAULT_PRIORITY) {
210             if (VALID_PRIORITY_LIST.contains(priority)) {
211                 body.addFieldPart(MESSAGE_KEY_PRIORITY, new StringContentProvider(String.valueOf(priority)), null);
212
213                 if (priority == EMERGENCY_PRIORITY) {
214                     if (retry < MIN_RETRY_SECONDS) {
215                         logger.warn("Retry value of {} is too small. Using default value of {}.", retry,
216                                 MIN_RETRY_SECONDS);
217                         body.addFieldPart(MESSAGE_KEY_RETRY,
218                                 new StringContentProvider(String.valueOf(MIN_RETRY_SECONDS)), null);
219                     } else {
220                         body.addFieldPart(MESSAGE_KEY_RETRY, new StringContentProvider(String.valueOf(retry)), null);
221                     }
222
223                     if (0 < expire && expire <= MAX_EXPIRE_SECONDS) {
224                         body.addFieldPart(MESSAGE_KEY_EXPIRE, new StringContentProvider(String.valueOf(expire)), null);
225                     } else {
226                         logger.warn("Expire value of {} is invalid. Using default value of {}.", expire,
227                                 MAX_EXPIRE_SECONDS);
228                         body.addFieldPart(MESSAGE_KEY_EXPIRE,
229                                 new StringContentProvider(String.valueOf(MAX_EXPIRE_SECONDS)), null);
230                     }
231                 }
232             } else {
233                 logger.warn("Invalid 'priority', skipping. Expected: {}. Got: {}.",
234                         VALID_PRIORITY_LIST.stream().map(i -> i.toString()).collect(Collectors.joining(",")), priority);
235             }
236         }
237
238         if (!ttl.isZero()) {
239             if (priority == EMERGENCY_PRIORITY) {
240                 logger.warn("TTL value of {} will be ignored for emergency priority.", ttl);
241             }
242             body.addFieldPart(MESSAGE_KEY_TTL, new StringContentProvider(String.valueOf(ttl.getSeconds())), null);
243         }
244
245         String url = this.url;
246         if (url != null) {
247             if (url.length() > MAX_URL_LENGTH) {
248                 throw new IllegalArgumentException(String
249                         .format("Skip sending the message as 'url' is longer than %d characters.", MAX_URL_LENGTH));
250             }
251             body.addFieldPart(MESSAGE_KEY_URL, new StringContentProvider(url), null);
252
253             String urlTitle = this.urlTitle;
254             if (urlTitle != null) {
255                 if (urlTitle.length() > MAX_URL_TITLE_LENGTH) {
256                     throw new IllegalArgumentException(
257                             String.format("Skip sending the message as 'urlTitle' is longer than %d characters.",
258                                     MAX_URL_TITLE_LENGTH));
259                 }
260                 body.addFieldPart(MESSAGE_KEY_URL_TITLE, new StringContentProvider(urlTitle), null);
261             }
262         }
263
264         if (sound != null) {
265             body.addFieldPart(MESSAGE_KEY_SOUND, new StringContentProvider(sound), null);
266         }
267
268         String attachment = this.attachment;
269         if (attachment != null) {
270             if (attachment.startsWith("http")) { // support data HTTP(S) scheme
271                 RawType rawImage = HttpUtil.downloadImage(attachment, 10000);
272                 if (rawImage == null) {
273                     throw new IllegalArgumentException(
274                             String.format("Skip sending the message as content '%s' does not exist.", attachment));
275                 }
276                 addFilePart(createTempFile(rawImage.getBytes()),
277                         contentType == null ? rawImage.getMimeType() : contentType);
278             } else if (attachment.startsWith("data:")) { // support data URI scheme
279                 try {
280                     RawType rawImage = RawType.valueOf(attachment);
281                     addFilePart(createTempFile(rawImage.getBytes()),
282                             contentType == null ? rawImage.getMimeType() : contentType);
283                 } catch (IllegalArgumentException e) {
284                     throw new IllegalArgumentException(String
285                             .format("Skip sending the message because data URI scheme is invalid: %s", e.getMessage()));
286                 }
287             } else {
288                 File file = new File(attachment);
289                 if (!file.exists()) {
290                     throw new IllegalArgumentException(
291                             String.format("Skip sending the message as file '%s' does not exist.", attachment));
292                 }
293                 addFilePart(file.toPath(), contentType);
294             }
295         }
296
297         if (html) {
298             body.addFieldPart(MESSAGE_KEY_HTML, new StringContentProvider("1"), null);
299         } else if (monospace) {
300             body.addFieldPart(MESSAGE_KEY_MONOSPACE, new StringContentProvider("1"), null);
301         }
302
303         return body;
304     }
305
306     private Path createTempFile(byte[] data) throws CommunicationException {
307         try {
308             Path tmpFile = Files.createTempFile("pushover-", ".tmp");
309             return Files.write(tmpFile, data);
310         } catch (IOException e) {
311             logger.debug("IOException occurred while creating temp file - skip sending the message: {}", e.getMessage(),
312                     e);
313             throw new CommunicationException(TEXT_ERROR_SKIP_SENDING_MESSAGE, e.getCause(), e.getLocalizedMessage());
314         }
315     }
316
317     private void addFilePart(Path path, @Nullable String contentType) throws CommunicationException {
318         try {
319             body.addFilePart(MESSAGE_KEY_ATTACHMENT, path.toFile().getName(),
320                     new PathContentProvider(contentType == null ? DEFAULT_CONTENT_TYPE : contentType, path), null);
321         } catch (IOException e) {
322             logger.debug("IOException occurred while adding content - skip sending the message: {}", e.getMessage(), e);
323             throw new CommunicationException(TEXT_ERROR_SKIP_SENDING_MESSAGE, e.getCause(), e.getLocalizedMessage());
324         }
325     }
326 }