2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pushover.internal.connection;
15 import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
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;
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;
39 * The {@link PushoverMessageBuilder} builds the body for Pushover Messages API requests.
41 * @author Christoph Weitkamp - Initial contribution
44 public class PushoverMessageBuilder {
46 private final Logger logger = LoggerFactory.getLogger(PushoverMessageBuilder.class);
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";
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";
76 private final MultiPartContentProvider body = new MultiPartContentProvider();
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;
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);
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);
104 if (user == null || user.isBlank()) {
105 throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_USER);
108 return new PushoverMessageBuilder(apikey, user);
111 public PushoverMessageBuilder withMessage(String message) {
112 this.message = message;
116 public PushoverMessageBuilder withTitle(String title) {
121 public PushoverMessageBuilder withDevice(String device) {
122 this.device = device;
126 public PushoverMessageBuilder withPriority(int priority) {
127 this.priority = priority;
131 public PushoverMessageBuilder withRetry(int retry) {
136 public PushoverMessageBuilder withExpire(int expire) {
137 this.expire = expire;
141 public PushoverMessageBuilder withTTL(Duration ttl) {
146 public PushoverMessageBuilder withUrl(String url) {
151 public PushoverMessageBuilder withUrlTitle(String urlTitle) {
152 this.urlTitle = urlTitle;
156 public PushoverMessageBuilder withSound(String sound) {
161 public PushoverMessageBuilder withAttachment(String attachment) {
162 this.attachment = attachment;
166 public PushoverMessageBuilder withContentType(String contentType) {
167 this.contentType = contentType;
171 public PushoverMessageBuilder withHtmlFormatting() {
176 public PushoverMessageBuilder withMonospaceFormatting() {
177 this.monospace = true;
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));
188 body.addFieldPart(MESSAGE_KEY_MESSAGE, new StringContentProvider(message), null);
191 String title = this.title;
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));
197 body.addFieldPart(MESSAGE_KEY_TITLE, new StringContentProvider(title), null);
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);
205 body.addFieldPart(MESSAGE_KEY_DEVICE, new StringContentProvider(device), null);
209 if (priority != DEFAULT_PRIORITY) {
210 if (VALID_PRIORITY_LIST.contains(priority)) {
211 body.addFieldPart(MESSAGE_KEY_PRIORITY, new StringContentProvider(String.valueOf(priority)), null);
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,
217 body.addFieldPart(MESSAGE_KEY_RETRY,
218 new StringContentProvider(String.valueOf(MIN_RETRY_SECONDS)), null);
220 body.addFieldPart(MESSAGE_KEY_RETRY, new StringContentProvider(String.valueOf(retry)), null);
223 if (0 < expire && expire <= MAX_EXPIRE_SECONDS) {
224 body.addFieldPart(MESSAGE_KEY_EXPIRE, new StringContentProvider(String.valueOf(expire)), null);
226 logger.warn("Expire value of {} is invalid. Using default value of {}.", expire,
228 body.addFieldPart(MESSAGE_KEY_EXPIRE,
229 new StringContentProvider(String.valueOf(MAX_EXPIRE_SECONDS)), null);
233 logger.warn("Invalid 'priority', skipping. Expected: {}. Got: {}.",
234 VALID_PRIORITY_LIST.stream().map(i -> i.toString()).collect(Collectors.joining(",")), priority);
239 if (priority == EMERGENCY_PRIORITY) {
240 logger.warn("TTL value of {} will be ignored for emergency priority.", ttl);
242 body.addFieldPart(MESSAGE_KEY_TTL, new StringContentProvider(String.valueOf(ttl.getSeconds())), null);
245 String url = this.url;
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));
251 body.addFieldPart(MESSAGE_KEY_URL, new StringContentProvider(url), null);
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));
260 body.addFieldPart(MESSAGE_KEY_URL_TITLE, new StringContentProvider(urlTitle), null);
265 body.addFieldPart(MESSAGE_KEY_SOUND, new StringContentProvider(sound), null);
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));
276 addFilePart(createTempFile(rawImage.getBytes()),
277 contentType == null ? rawImage.getMimeType() : contentType);
278 } else if (attachment.startsWith("data:")) { // support data URI scheme
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()));
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));
293 addFilePart(file.toPath(), contentType);
298 body.addFieldPart(MESSAGE_KEY_HTML, new StringContentProvider("1"), null);
299 } else if (monospace) {
300 body.addFieldPart(MESSAGE_KEY_MONOSPACE, new StringContentProvider("1"), null);
306 private Path createTempFile(byte[] data) throws CommunicationException {
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(),
313 throw new CommunicationException(TEXT_ERROR_SKIP_SENDING_MESSAGE, e.getCause(), e.getLocalizedMessage());
317 private void addFilePart(Path path, @Nullable String contentType) throws CommunicationException {
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());