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