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