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.pushsafer.internal.connection;
16 import java.io.IOException;
17 import java.nio.charset.StandardCharsets;
18 import java.nio.file.Files;
19 import java.util.Arrays;
20 import java.util.Base64;
21 import java.util.List;
22 import java.util.Properties;
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.StringContentProvider;
30 import org.openhab.core.io.net.http.HttpUtil;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * The {@link PushsaferMessageBuilder} builds the body for Pushsafer Messages API requests.
37 * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
40 public class PushsaferMessageBuilder {
42 private final Logger logger = LoggerFactory.getLogger(PushsaferMessageBuilder.class);
44 public static final String MESSAGE_KEY_TOKEN = "k";
45 public static final String MESSAGE_KEY_USER = "u";
46 private static final String MESSAGE_KEY_MESSAGE = "m";
47 private static final String MESSAGE_KEY_TITLE = "t";
48 private static final String MESSAGE_KEY_DEVICE = "d";
49 private static final String MESSAGE_KEY_ICON = "i";
50 private static final String MESSAGE_KEY_COLOR = "c";
51 private static final String MESSAGE_KEY_VIBRATION = "v";
52 private static final String MESSAGE_KEY_PRIORITY = "pr";
53 private static final String MESSAGE_KEY_RETRY = "re";
54 private static final String MESSAGE_KEY_EXPIRE = "ex";
55 private static final String MESSAGE_KEY_URL = "u";
56 private static final String MESSAGE_KEY_URL_TITLE = "ut";
57 private static final String MESSAGE_KEY_SOUND = "s";
58 private static final String MESSAGE_KEY_TIME2LIVE = "l";
59 private static final String MESSAGE_KEY_ANSWER = "a";
60 private static final String MESSAGE_KEY_CONFIRM = "cr";
61 private static final String MESSAGE_KEY_ATTACHMENT = "p";
62 public static final String MESSAGE_KEY_HTML = "html";
63 public static final String MESSAGE_KEY_MONOSPACE = "monospace";
65 private static final int MAX_MESSAGE_LENGTH = 4096;
66 private static final int MAX_TITLE_LENGTH = 250;
67 private static final int MAX_DEVICE_LENGTH = 25;
68 private static final List<Integer> VALID_PRIORITY_LIST = Arrays.asList(-2, -1, 0, 1, 2);
69 private static final int DEFAULT_PRIORITY = 0;
70 public static final int EMERGENCY_PRIORITY = 2;
71 private static final int MIN_RETRY_SECONDS = 0;
72 private static final int MAX_EXPIRE_SECONDS = 10800;
73 private static final int MAX_URL_LENGTH = 512;
74 private static final int MAX_URL_TITLE_LENGTH = 100;
75 public static final String DEFAULT_CONTENT_TYPE = "jpeg";
76 public static final String DEFAULT_AUTH = "";
78 private final MultiPartContentProvider body = new MultiPartContentProvider();
80 private @Nullable String message;
81 private @Nullable String title;
82 private @Nullable String device;
83 private int priority = DEFAULT_PRIORITY;
84 private int retry = 300;
85 private int expire = 3600;
86 private @Nullable String url;
87 private @Nullable String urlTitle;
88 private @Nullable String sound;
89 private @Nullable String icon;
91 private int time2live;
92 private boolean answer;
93 private @Nullable String color;
94 private @Nullable String vibration;
95 private @Nullable String attachment;
96 private String contentType = DEFAULT_CONTENT_TYPE;
97 private String authentication = DEFAULT_AUTH;
98 private boolean html = false;
99 private boolean monospace = false;
101 private PushsaferMessageBuilder(String apikey, String device) throws PushsaferConfigurationException {
102 body.addFieldPart(MESSAGE_KEY_TOKEN, new StringContentProvider(apikey), null);
103 body.addFieldPart(MESSAGE_KEY_DEVICE, new StringContentProvider(device), null);
106 public static PushsaferMessageBuilder getInstance(@Nullable String apikey, @Nullable String device)
107 throws PushsaferConfigurationException {
108 if (apikey == null || apikey.isEmpty()) {
109 throw new PushsaferConfigurationException("@text/offline.conf-error-missing-apikey");
112 if (device == null || device.isEmpty()) {
113 throw new PushsaferConfigurationException("@text/offline.conf-error-missing-device");
116 return new PushsaferMessageBuilder(apikey, device);
119 public PushsaferMessageBuilder withMessage(String message) {
120 this.message = message;
124 public PushsaferMessageBuilder withTitle(String title) {
129 public PushsaferMessageBuilder withDevice(String device) {
130 this.device = device;
134 public PushsaferMessageBuilder withPriority(int priority) {
135 this.priority = priority;
139 public PushsaferMessageBuilder withRetry(int retry) {
144 public PushsaferMessageBuilder withExpire(int expire) {
145 this.expire = expire;
149 public PushsaferMessageBuilder withUrl(String url) {
154 public PushsaferMessageBuilder withUrlTitle(String urlTitle) {
155 this.urlTitle = urlTitle;
159 public PushsaferMessageBuilder withSound(String sound) {
164 public PushsaferMessageBuilder withIcon(String icon) {
169 public PushsaferMessageBuilder withColor(String color) {
174 public PushsaferMessageBuilder withVibration(String vibration) {
175 this.vibration = vibration;
179 public PushsaferMessageBuilder withAnswer(boolean answer) {
180 this.answer = answer;
184 public PushsaferMessageBuilder withTime2live(int time2live) {
185 this.time2live = time2live;
189 public PushsaferMessageBuilder withConfirm(int confirm) {
190 this.confirm = confirm;
194 public PushsaferMessageBuilder withAttachment(String attachment) {
195 this.attachment = attachment;
199 public PushsaferMessageBuilder withContentType(String contentType) {
200 this.contentType = contentType;
204 public PushsaferMessageBuilder withAuthentication(String authentication) {
205 this.authentication = authentication;
209 public PushsaferMessageBuilder withHtmlFormatting() {
214 public PushsaferMessageBuilder withMonospaceFormatting() {
215 this.monospace = true;
219 public ContentProvider build() throws PushsaferCommunicationException {
220 if (message != null) {
221 if (message.length() > MAX_MESSAGE_LENGTH) {
222 throw new IllegalArgumentException(String.format(
223 "Skip sending the message as 'message' is longer than %d characters.", MAX_MESSAGE_LENGTH));
225 body.addFieldPart(MESSAGE_KEY_MESSAGE, new StringContentProvider(message), null);
229 if (title.length() > MAX_TITLE_LENGTH) {
230 throw new IllegalArgumentException(String
231 .format("Skip sending the message as 'title' is longer than %d characters.", MAX_TITLE_LENGTH));
233 body.addFieldPart(MESSAGE_KEY_TITLE, new StringContentProvider(title), null);
236 if (device != null) {
237 if (device.length() > MAX_DEVICE_LENGTH) {
238 logger.warn("Skip 'device' as it is longer than {} characters. Got: {}.", MAX_DEVICE_LENGTH, device);
240 body.addFieldPart(MESSAGE_KEY_DEVICE, new StringContentProvider(device), null);
244 if (priority != DEFAULT_PRIORITY) {
245 if (VALID_PRIORITY_LIST.contains(priority)) {
246 body.addFieldPart(MESSAGE_KEY_PRIORITY, new StringContentProvider(String.valueOf(priority)), null);
248 if (priority == EMERGENCY_PRIORITY) {
249 if (retry < MIN_RETRY_SECONDS) {
250 logger.warn("Retry value of {} is too small. Using default value of {}.", retry,
252 body.addFieldPart(MESSAGE_KEY_RETRY,
253 new StringContentProvider(String.valueOf(MIN_RETRY_SECONDS)), null);
255 body.addFieldPart(MESSAGE_KEY_RETRY, new StringContentProvider(String.valueOf(retry)), null);
258 if (0 < expire && expire <= MAX_EXPIRE_SECONDS) {
259 body.addFieldPart(MESSAGE_KEY_EXPIRE, new StringContentProvider(String.valueOf(expire)), null);
261 logger.warn("Expire value of {} is invalid. Using default value of {}.", expire,
263 body.addFieldPart(MESSAGE_KEY_EXPIRE,
264 new StringContentProvider(String.valueOf(MAX_EXPIRE_SECONDS)), null);
268 logger.warn("Invalid 'priority', skipping. Expected: {}. Got: {}.",
269 VALID_PRIORITY_LIST.stream().map(i -> i.toString()).collect(Collectors.joining(",")), priority);
274 if (url.length() > MAX_URL_LENGTH) {
275 throw new IllegalArgumentException(String
276 .format("Skip sending the message as 'url' is longer than %d characters.", MAX_URL_LENGTH));
278 body.addFieldPart(MESSAGE_KEY_URL, new StringContentProvider(url), null);
280 if (urlTitle != null) {
281 if (urlTitle.length() > MAX_URL_TITLE_LENGTH) {
282 throw new IllegalArgumentException(
283 String.format("Skip sending the message as 'urlTitle' is longer than %d characters.",
284 MAX_URL_TITLE_LENGTH));
286 body.addFieldPart(MESSAGE_KEY_URL_TITLE, new StringContentProvider(urlTitle), null);
291 body.addFieldPart(MESSAGE_KEY_SOUND, new StringContentProvider(sound), null);
295 body.addFieldPart(MESSAGE_KEY_ICON, new StringContentProvider(icon), null);
299 body.addFieldPart(MESSAGE_KEY_COLOR, new StringContentProvider(color), null);
302 if (vibration != null) {
303 body.addFieldPart(MESSAGE_KEY_VIBRATION, new StringContentProvider(vibration), null);
306 body.addFieldPart(MESSAGE_KEY_CONFIRM, new StringContentProvider(String.valueOf(confirm)), null);
308 body.addFieldPart(MESSAGE_KEY_ANSWER, new StringContentProvider(String.valueOf(answer)), null);
310 body.addFieldPart(MESSAGE_KEY_TIME2LIVE, new StringContentProvider(String.valueOf(time2live)), null);
312 if (attachment != null) {
313 String localAttachment = attachment;
314 final String encodedString;
316 if (localAttachment.startsWith("http")) {
317 Properties headers = new Properties();
318 headers.put("User-Agent", "Mozilla/5.0");
319 if (!authentication.isBlank()) {
320 headers.put("Authorization", "Basic "
321 + Base64.getEncoder().encodeToString(authentication.getBytes(StandardCharsets.UTF_8)));
323 String content = HttpUtil.executeUrl("GET", attachment, headers, null, null, 10);
324 if (content == null) {
325 throw new IllegalArgumentException(
326 String.format("Skip sending the message as content '%s' does not exist.", attachment));
328 encodedString = "data:image/" + contentType + ";base64," + content;
329 } else if (localAttachment.startsWith("data:")) {
330 encodedString = localAttachment;
332 File file = new File(attachment);
333 if (!file.exists()) {
334 throw new IllegalArgumentException(
335 String.format("Skip sending the message as file '%s' does not exist.", attachment));
337 byte[] fileContent = Files.readAllBytes(file.toPath());
338 encodedString = "data:image/" + contentType + ";base64,"
339 + Base64.getEncoder().encodeToString(fileContent);
341 body.addFieldPart(MESSAGE_KEY_ATTACHMENT, new StringContentProvider(encodedString), null);
342 } catch (IOException e) {
343 logger.debug("IOException occurred - skip sending message: {}", e.getLocalizedMessage(), e);
344 throw new PushsaferCommunicationException(
345 String.format("Skip sending the message: %s", e.getLocalizedMessage()), e);
350 body.addFieldPart(MESSAGE_KEY_HTML, new StringContentProvider("1"), null);
351 } else if (monospace) {
352 body.addFieldPart(MESSAGE_KEY_MONOSPACE, new StringContentProvider("1"), null);