]> git.basschouten.com Git - openhab-addons.git/blob
8dbb5e341aff613da998bba7eae84da0e285e125
[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.net.URLEncoder;
18 import java.nio.charset.StandardCharsets;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.stream.Collectors;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentProvider;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.pushover.internal.config.PushoverAccountConfiguration;
35 import org.openhab.binding.pushover.internal.dto.Sound;
36 import org.openhab.core.cache.ExpiringCache;
37 import org.openhab.core.i18n.CommunicationException;
38 import org.openhab.core.i18n.ConfigurationException;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 import com.google.gson.JsonElement;
43 import com.google.gson.JsonObject;
44 import com.google.gson.JsonParser;
45 import com.google.gson.JsonSyntaxException;
46
47 /**
48  * The {@link PushoverAPIConnection} is responsible for handling the connections to Pushover Messages API.
49  *
50  * @author Christoph Weitkamp - Initial contribution
51  */
52 @NonNullByDefault
53 public class PushoverAPIConnection {
54
55     private static final String JSON_VALUE_ERRORS = "errors";
56     private static final String JSON_VALUE_RECEIPT = "receipt";
57     private static final String JSON_VALUE_SOUNDS = "sounds";
58     private static final String JSON_VALUE_STATUS = "status";
59
60     private final Logger logger = LoggerFactory.getLogger(PushoverAPIConnection.class);
61
62     private static final String VALIDATE_URL = "https://api.pushover.net/1/users/validate.json";
63     private static final String MESSAGE_URL = "https://api.pushover.net/1/messages.json";
64     private static final String CANCEL_MESSAGE_URL = "https://api.pushover.net/1/receipts/%s/cancel.json";
65     private static final String SOUNDS_URL = "https://api.pushover.net/1/sounds.json";
66
67     private final HttpClient httpClient;
68     private final PushoverAccountConfiguration config;
69
70     private final ExpiringCache<List<Sound>> cache = new ExpiringCache<>(TimeUnit.DAYS.toMillis(1),
71             this::getSoundsFromSource);
72
73     public PushoverAPIConnection(HttpClient httpClient, PushoverAccountConfiguration config) {
74         this.httpClient = httpClient;
75         this.config = config;
76     }
77
78     public boolean validateUser() throws CommunicationException, ConfigurationException {
79         return getMessageStatus(
80                 post(VALIDATE_URL, PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
81     }
82
83     public boolean sendMessage(PushoverMessageBuilder message) throws CommunicationException, ConfigurationException {
84         return getMessageStatus(post(MESSAGE_URL, message.build()));
85     }
86
87     public String sendPriorityMessage(PushoverMessageBuilder message)
88             throws CommunicationException, ConfigurationException {
89         final JsonObject json = JsonParser.parseString(post(MESSAGE_URL, message.build())).getAsJsonObject();
90         return getMessageStatus(json) && json.has(JSON_VALUE_RECEIPT) ? json.get(JSON_VALUE_RECEIPT).getAsString() : "";
91     }
92
93     public boolean cancelPriorityMessage(String receipt) throws CommunicationException, ConfigurationException {
94         return getMessageStatus(post(String.format(CANCEL_MESSAGE_URL, receipt),
95                 PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
96     }
97
98     public @Nullable List<Sound> getSounds() {
99         return cache.getValue();
100     }
101
102     private List<Sound> getSoundsFromSource() throws CommunicationException, ConfigurationException {
103         final String localApikey = config.apikey;
104         if (localApikey == null || localApikey.isBlank()) {
105             throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_APIKEY);
106         }
107
108         try {
109             final String content = get(
110                     buildURL(SOUNDS_URL, Map.of(PushoverMessageBuilder.MESSAGE_KEY_TOKEN, localApikey)));
111             final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
112             final JsonObject jsonSounds = json.has(JSON_VALUE_SOUNDS) ? json.get(JSON_VALUE_SOUNDS).getAsJsonObject()
113                     : null;
114             if (jsonSounds != null) {
115                 List<Sound> sounds = jsonSounds.entrySet().stream()
116                         .map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString()))
117                         .collect(Collectors.toList());
118                 sounds.add(PushoverAccountConfiguration.SOUND_DEFAULT);
119                 return sounds;
120             }
121         } catch (JsonSyntaxException e) {
122             // do nothing
123         }
124         return List.of();
125     }
126
127     private String buildURL(String url, Map<String, String> requestParams) {
128         return requestParams.keySet().stream().map(key -> key + "=" + encodeParam(requestParams.get(key)))
129                 .collect(Collectors.joining("&", url + "?", ""));
130     }
131
132     private String encodeParam(@Nullable String value) {
133         return value == null ? "" : URLEncoder.encode(value, StandardCharsets.UTF_8);
134     }
135
136     private String get(String url) throws CommunicationException, ConfigurationException {
137         return executeRequest(HttpMethod.GET, url, null);
138     }
139
140     private String post(String url, ContentProvider body) throws CommunicationException, ConfigurationException {
141         return executeRequest(HttpMethod.POST, url, body);
142     }
143
144     private synchronized String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body)
145             throws CommunicationException, ConfigurationException {
146         logger.trace("Pushover request: {} - URL = '{}'", httpMethod, url);
147         try {
148             final Request request = httpClient.newRequest(url).method(httpMethod).timeout(config.timeout,
149                     TimeUnit.SECONDS);
150
151             if (body != null) {
152                 if (logger.isTraceEnabled()) {
153                     logger.trace("Pushover request body: '{}'", body);
154                 }
155                 request.content(body);
156             }
157
158             final ContentResponse contentResponse = request.send();
159
160             final int httpStatus = contentResponse.getStatus();
161             final String content = contentResponse.getContentAsString();
162             logger.trace("Pushover response: status = {}, content = '{}'", httpStatus, content);
163             switch (httpStatus) {
164                 case HttpStatus.OK_200:
165                     return content;
166                 case HttpStatus.BAD_REQUEST_400:
167                     logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
168                     throw new ConfigurationException(getMessageError(content));
169                 default:
170                     logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
171                     throw new CommunicationException(content);
172             }
173         } catch (ExecutionException e) {
174             String message = e.getMessage();
175             logger.debug("ExecutionException occurred during execution: {}", message, e);
176             throw new CommunicationException(message == null ? TEXT_OFFLINE_COMMUNICATION_ERROR : message,
177                     e.getCause());
178         } catch (TimeoutException e) {
179             String message = e.getMessage();
180             logger.debug("TimeoutException occurred during execution: {}", message, e);
181             throw new CommunicationException(message == null ? TEXT_OFFLINE_COMMUNICATION_ERROR : message);
182         } catch (InterruptedException e) {
183             Thread.currentThread().interrupt();
184             String message = e.getMessage();
185             logger.debug("InterruptedException occurred during execution: {}", message, e);
186             throw new CommunicationException(message == null ? TEXT_OFFLINE_COMMUNICATION_ERROR : message);
187         }
188     }
189
190     private String getMessageError(String content) {
191         final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
192         final JsonElement errorsElement = json.get(JSON_VALUE_ERRORS);
193         if (errorsElement != null && errorsElement.isJsonArray()) {
194             return errorsElement.getAsJsonArray().toString();
195         }
196         return TEXT_OFFLINE_CONF_ERROR_UNKNOWN;
197     }
198
199     private boolean getMessageStatus(String content) {
200         final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
201         return json.has(JSON_VALUE_STATUS) ? json.get(JSON_VALUE_STATUS).getAsInt() == 1 : false;
202     }
203
204     private boolean getMessageStatus(JsonObject json) {
205         return json.has(JSON_VALUE_STATUS) ? json.get(JSON_VALUE_STATUS).getAsInt() == 1 : false;
206     }
207 }