]> git.basschouten.com Git - openhab-addons.git/blob
e155d6b52a81e26ae9907fb76323ed55c9d35a48
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.telegram.internal.action;
14
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.net.MalformedURLException;
19 import java.net.URI;
20 import java.net.URL;
21 import java.nio.charset.StandardCharsets;
22 import java.nio.file.Paths;
23 import java.util.Base64;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27
28 import org.apache.commons.io.IOUtils;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.eclipse.jetty.client.api.Authentication;
33 import org.eclipse.jetty.client.api.AuthenticationStore;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.http.HttpHeader;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.util.B64Code;
39 import org.openhab.binding.telegram.internal.TelegramHandler;
40 import org.openhab.core.automation.annotation.ActionInput;
41 import org.openhab.core.automation.annotation.RuleAction;
42 import org.openhab.core.thing.binding.ThingActions;
43 import org.openhab.core.thing.binding.ThingActionsScope;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
49 import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup;
50 import com.pengrad.telegrambot.request.AnswerCallbackQuery;
51 import com.pengrad.telegrambot.request.EditMessageReplyMarkup;
52 import com.pengrad.telegrambot.request.SendMessage;
53 import com.pengrad.telegrambot.request.SendPhoto;
54 import com.pengrad.telegrambot.response.BaseResponse;
55 import com.pengrad.telegrambot.response.SendResponse;
56
57 /**
58  * Provides the actions for the Telegram API.
59  *
60  * @author Alexander Krasnogolowy - Initial contribution
61  */
62 @ThingActionsScope(name = "telegram")
63 @NonNullByDefault
64 public class TelegramActions implements ThingActions {
65     private final Logger logger = LoggerFactory.getLogger(TelegramActions.class);
66     private @Nullable TelegramHandler handler;
67
68     private boolean evaluateResponse(@Nullable BaseResponse response) {
69         if (response != null && !response.isOk()) {
70             logger.warn("Failed to send telegram message: {}", response.description());
71             return false;
72         }
73         return true;
74     }
75
76     private static class BasicResult implements Authentication.Result {
77
78         private final HttpHeader header;
79         private final URI uri;
80         private final String value;
81
82         public BasicResult(HttpHeader header, URI uri, String value) {
83             this.header = header;
84             this.uri = uri;
85             this.value = value;
86         }
87
88         @Override
89         public URI getURI() {
90             return this.uri;
91         }
92
93         @Override
94         public void apply(@Nullable Request request) {
95             if (request != null) {
96                 request.header(this.header, this.value);
97             }
98         }
99
100         @Override
101         public String toString() {
102             return String.format("Basic authentication result for %s", this.uri);
103         }
104     }
105
106     @RuleAction(label = "send an answer", description = "Send a Telegram answer using the Telegram API.")
107     public boolean sendTelegramAnswer(@ActionInput(name = "chatId") @Nullable Long chatId,
108             @ActionInput(name = "replyId") @Nullable String replyId,
109             @ActionInput(name = "message") @Nullable String message) {
110         if (replyId == null) {
111             logger.warn("ReplyId not defined; action skipped.");
112             return false;
113         }
114         if (chatId == null) {
115             logger.warn("chatId not defined; action skipped.");
116             return false;
117         }
118         TelegramHandler localHandler = handler;
119         if (localHandler != null) {
120             String callbackId = localHandler.getCallbackId(chatId, replyId);
121             if (callbackId != null) {
122                 AnswerCallbackQuery answerCallbackQuery = new AnswerCallbackQuery(
123                         localHandler.getCallbackId(chatId, replyId));
124                 logger.debug("AnswerCallbackQuery for chatId {} and replyId {} is the callbackId {}", chatId, replyId,
125                         localHandler.getCallbackId(chatId, replyId));
126                 // we could directly set the text here, but this
127                 // doesn't result in a real message only in a
128                 // little popup or in an alert, so the only purpose
129                 // is to stop the progress bar on client side
130                 if (!evaluateResponse(localHandler.execute(answerCallbackQuery))) {
131                     return false;
132                 }
133             }
134             Integer messageId = localHandler.removeMessageId(chatId, replyId);
135             logger.debug("remove messageId {} for chatId {} and replyId {}", messageId, chatId, replyId);
136
137             EditMessageReplyMarkup editReplyMarkup = new EditMessageReplyMarkup(chatId, messageId.intValue())
138                     .replyMarkup(new InlineKeyboardMarkup(new InlineKeyboardButton[0]));// remove reply markup from
139                                                                                         // old message
140             if (!evaluateResponse(localHandler.execute(editReplyMarkup))) {
141                 return false;
142             }
143             return message != null ? sendTelegram(chatId, message) : true;
144         }
145         return false;
146     }
147
148     @RuleAction(label = "send an answer", description = "Send a Telegram answer using the Telegram API.")
149     public boolean sendTelegramAnswer(@ActionInput(name = "replyId") @Nullable String replyId,
150             @ActionInput(name = "message") @Nullable String message) {
151         TelegramHandler localHandler = handler;
152         if (localHandler != null) {
153             for (Long chatId : localHandler.getReceiverChatIds()) {
154                 if (!sendTelegramAnswer(chatId, replyId, message)) {
155                     return false;
156                 }
157             }
158         }
159         return true;
160     }
161
162     @RuleAction(label = "send a message", description = "Send a Telegram message using the Telegram API.")
163     public boolean sendTelegram(@ActionInput(name = "chatId") @Nullable Long chatId,
164             @ActionInput(name = "message") @Nullable String message) {
165         return sendTelegramGeneral(chatId, message, (String) null);
166     }
167
168     @RuleAction(label = "send a message", description = "Send a Telegram message using the Telegram API.")
169     public boolean sendTelegram(@ActionInput(name = "message") @Nullable String message) {
170         TelegramHandler localHandler = handler;
171         if (localHandler != null) {
172             for (Long chatId : localHandler.getReceiverChatIds()) {
173                 if (!sendTelegram(chatId, message)) {
174                     return false;
175                 }
176             }
177         }
178         return true;
179     }
180
181     @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
182     public boolean sendTelegramQuery(@ActionInput(name = "chatId") @Nullable Long chatId,
183             @ActionInput(name = "message") @Nullable String message,
184             @ActionInput(name = "replyId") @Nullable String replyId,
185             @ActionInput(name = "buttons") @Nullable String... buttons) {
186         return sendTelegramGeneral(chatId, message, replyId, buttons);
187     }
188
189     @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
190     public boolean sendTelegramQuery(@ActionInput(name = "message") @Nullable String message,
191             @ActionInput(name = "replyId") @Nullable String replyId,
192             @ActionInput(name = "buttons") @Nullable String... buttons) {
193         TelegramHandler localHandler = handler;
194         if (localHandler != null) {
195             for (Long chatId : localHandler.getReceiverChatIds()) {
196                 if (!sendTelegramQuery(chatId, message, replyId, buttons)) {
197                     return false;
198                 }
199             }
200         }
201         return true;
202     }
203
204     private boolean sendTelegramGeneral(@ActionInput(name = "chatId") @Nullable Long chatId, @Nullable String message,
205             @Nullable String replyId, @Nullable String... buttons) {
206         if (message == null) {
207             logger.warn("Message not defined; action skipped.");
208             return false;
209         }
210         if (chatId == null) {
211             logger.warn("chatId not defined; action skipped.");
212             return false;
213         }
214         TelegramHandler localHandler = handler;
215         if (localHandler != null) {
216             SendMessage sendMessage = new SendMessage(chatId, message);
217             if (localHandler.getParseMode() != null) {
218                 sendMessage.parseMode(localHandler.getParseMode());
219             }
220             if (replyId != null) {
221                 if (!replyId.contains(" ")) {
222                     if (buttons.length > 0) {
223                         InlineKeyboardButton[][] keyboard2D = new InlineKeyboardButton[1][];
224                         InlineKeyboardButton[] keyboard = new InlineKeyboardButton[buttons.length];
225                         keyboard2D[0] = keyboard;
226                         for (int i = 0; i < buttons.length; i++) {
227                             keyboard[i] = new InlineKeyboardButton(buttons[i]).callbackData(replyId + " " + buttons[i]);
228                         }
229                         InlineKeyboardMarkup keyBoardMarkup = new InlineKeyboardMarkup(keyboard2D);
230                         sendMessage.replyMarkup(keyBoardMarkup);
231                     } else {
232                         logger.warn(
233                                 "The replyId {} for message {} is given, but no buttons are defined. ReplyMarkup will be ignored.",
234                                 replyId, message);
235                     }
236                 } else {
237                     logger.warn("replyId {} must not contain spaces. ReplyMarkup will be ignored.", replyId);
238                 }
239             }
240             SendResponse retMessage = localHandler.execute(sendMessage);
241             if (!evaluateResponse(retMessage)) {
242                 return false;
243             }
244             if (replyId != null && retMessage != null) {
245                 logger.debug("Adding chatId {}, replyId {} and messageId {}", chatId, replyId,
246                         retMessage.message().messageId());
247                 localHandler.addMessageId(chatId, replyId, retMessage.message().messageId());
248             }
249             return true;
250         }
251         return false;
252     }
253
254     @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
255     public boolean sendTelegram(@ActionInput(name = "chatId") @Nullable Long chatId,
256             @ActionInput(name = "message") @Nullable String message,
257             @ActionInput(name = "args") @Nullable Object... args) {
258         return sendTelegram(chatId, String.format(message, args));
259     }
260
261     @RuleAction(label = "send a message", description = "Send a Telegram using the Telegram API.")
262     public boolean sendTelegram(@ActionInput(name = "message") @Nullable String message,
263             @ActionInput(name = "args") @Nullable Object... args) {
264         TelegramHandler localHandler = handler;
265         if (localHandler != null) {
266             for (Long chatId : localHandler.getReceiverChatIds()) {
267                 if (!sendTelegram(chatId, message, args)) {
268                     return false;
269                 }
270             }
271         }
272         return true;
273     }
274
275     @RuleAction(label = "send a photo", description = "Send a picture using the Telegram API.")
276     public boolean sendTelegramPhoto(@ActionInput(name = "chatId") @Nullable Long chatId,
277             @ActionInput(name = "photoURL") @Nullable String photoURL,
278             @ActionInput(name = "caption") @Nullable String caption) {
279         return sendTelegramPhoto(chatId, photoURL, caption, null, null);
280     }
281
282     @RuleAction(label = "send a photo", description = "Send a picture using the Telegram API.")
283     public boolean sendTelegramPhoto(@ActionInput(name = "chatId") @Nullable Long chatId,
284             @ActionInput(name = "photoURL") @Nullable String photoURL,
285             @ActionInput(name = "caption") @Nullable String caption,
286             @ActionInput(name = "username") @Nullable String username,
287             @ActionInput(name = "password") @Nullable String password) {
288         if (photoURL == null) {
289             logger.warn("Photo URL not defined; unable to retrieve photo for sending.");
290             return false;
291         }
292         if (chatId == null) {
293             logger.warn("chatId not defined; action skipped.");
294             return false;
295         }
296
297         TelegramHandler localHandler = handler;
298         if (localHandler != null) {
299             final SendPhoto sendPhoto;
300
301             if (photoURL.toLowerCase().startsWith("http")) {
302                 // load image from url
303                 logger.debug("Photo URL provided.");
304                 HttpClient client = localHandler.getClient();
305                 if (client == null) {
306                     return false;
307                 }
308                 Request request = client.newRequest(photoURL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS);
309                 if (username != null && password != null) {
310                     AuthenticationStore auth = client.getAuthenticationStore();
311                     URI uri = URI.create(photoURL);
312                     auth.addAuthenticationResult(new BasicResult(HttpHeader.AUTHORIZATION, uri,
313                             "Basic " + B64Code.encode(username + ":" + password, StandardCharsets.ISO_8859_1)));
314                 }
315                 try {
316                     ContentResponse contentResponse = request.send();
317                     if (contentResponse.getStatus() == 200) {
318                         byte[] fileContent = contentResponse.getContent();
319                         sendPhoto = new SendPhoto(chatId, fileContent);
320                     } else {
321                         logger.warn("Download from {} failed with status: {}", photoURL, contentResponse.getStatus());
322                         return false;
323                     }
324                 } catch (InterruptedException | TimeoutException | ExecutionException e) {
325                     logger.warn("Download from {} failed with exception: {}", photoURL, e.getMessage());
326                     return false;
327                 }
328             } else if (photoURL.toLowerCase().startsWith("file")) {
329                 // Load image from local file system
330                 logger.debug("Read file from local file system: {}", photoURL);
331                 try {
332                     URL url = new URL(photoURL);
333                     sendPhoto = new SendPhoto(chatId, Paths.get(url.getPath()).toFile());
334                 } catch (MalformedURLException e) {
335                     logger.warn("Malformed URL: {}", photoURL);
336                     return false;
337                 }
338             } else {
339                 // Load image from provided base64 image
340                 logger.debug("Photo base64 provided; converting to binary.");
341                 final String photoB64Data;
342                 if (photoURL.startsWith("data:")) { // support data URI scheme
343                     String[] photoURLParts = photoURL.split(",");
344                     if (photoURLParts.length > 1) {
345                         photoB64Data = photoURLParts[1];
346                     } else {
347                         logger.warn("The provided base64 string is not a valid data URI scheme");
348                         return false;
349                     }
350                 } else {
351                     photoB64Data = photoURL;
352                 }
353                 InputStream is = Base64.getDecoder()
354                         .wrap(new ByteArrayInputStream(photoB64Data.getBytes(StandardCharsets.UTF_8)));
355                 try {
356                     byte[] photoBytes = IOUtils.toByteArray(is);
357                     sendPhoto = new SendPhoto(chatId, photoBytes);
358                 } catch (IOException e) {
359                     logger.warn("Malformed base64 string: {}", e.getMessage());
360                     return false;
361                 }
362             }
363             sendPhoto.caption(caption);
364             if (localHandler.getParseMode() != null) {
365                 sendPhoto.parseMode(localHandler.getParseMode());
366             }
367             return evaluateResponse(localHandler.execute(sendPhoto));
368         }
369         return false;
370     }
371
372     @RuleAction(label = "send a photo", description = "Send a Picture using the Telegram API.")
373     public boolean sendTelegramPhoto(@ActionInput(name = "photoURL") @Nullable String photoURL,
374             @ActionInput(name = "caption") @Nullable String caption,
375             @ActionInput(name = "username") @Nullable String username,
376             @ActionInput(name = "password") @Nullable String password) {
377         TelegramHandler localHandler = handler;
378         if (localHandler != null) {
379             for (Long chatId : localHandler.getReceiverChatIds()) {
380                 if (!sendTelegramPhoto(chatId, photoURL, caption, username, password)) {
381                     return false;
382                 }
383             }
384         }
385         return true;
386     }
387
388     @RuleAction(label = "send a photo", description = "Send a Picture using the Telegram API.")
389     public boolean sendTelegramPhoto(@ActionInput(name = "photoURL") @Nullable String photoURL,
390             @ActionInput(name = "caption") @Nullable String caption) {
391         return sendTelegramPhoto(photoURL, caption, null, null);
392     }
393
394     // legacy delegate methods
395     /* APIs without chatId parameter */
396     public static boolean sendTelegram(ThingActions actions, @Nullable String format, @Nullable Object... args) {
397         return ((TelegramActions) actions).sendTelegram(format, args);
398     }
399
400     public static boolean sendTelegramQuery(ThingActions actions, @Nullable String message, @Nullable String replyId,
401             @Nullable String... buttons) {
402         return ((TelegramActions) actions).sendTelegramQuery(message, replyId, buttons);
403     }
404
405     public static boolean sendTelegramPhoto(ThingActions actions, @Nullable String photoURL, @Nullable String caption) {
406         return ((TelegramActions) actions).sendTelegramPhoto(photoURL, caption, null, null);
407     }
408
409     public static boolean sendTelegramPhoto(ThingActions actions, @Nullable String photoURL, @Nullable String caption,
410             @Nullable String username, @Nullable String password) {
411         return ((TelegramActions) actions).sendTelegramPhoto(photoURL, caption, username, password);
412     }
413
414     public static boolean sendTelegramAnswer(ThingActions actions, @Nullable String replyId, @Nullable String message) {
415         return ((TelegramActions) actions).sendTelegramAnswer(replyId, message);
416     }
417
418     /* APIs with chatId parameter */
419
420     public static boolean sendTelegram(ThingActions actions, @Nullable Long chatId, @Nullable String format,
421             @Nullable Object... args) {
422         return ((TelegramActions) actions).sendTelegram(chatId, format, args);
423     }
424
425     public static boolean sendTelegramQuery(ThingActions actions, @Nullable Long chatId, @Nullable String message,
426             @Nullable String replyId, @Nullable String... buttons) {
427         return ((TelegramActions) actions).sendTelegramQuery(chatId, message, replyId, buttons);
428     }
429
430     public static boolean sendTelegramPhoto(ThingActions actions, @Nullable Long chatId, @Nullable String photoURL,
431             @Nullable String caption) {
432         return ((TelegramActions) actions).sendTelegramPhoto(chatId, photoURL, caption, null, null);
433     }
434
435     public static boolean sendTelegramPhoto(ThingActions actions, @Nullable Long chatId, @Nullable String photoURL,
436             @Nullable String caption, @Nullable String username, @Nullable String password) {
437         return ((TelegramActions) actions).sendTelegramPhoto(chatId, photoURL, caption, username, password);
438     }
439
440     public static boolean sendTelegramAnswer(ThingActions actions, @Nullable Long chatId, @Nullable String replyId,
441             @Nullable String message) {
442         return ((TelegramActions) actions).sendTelegramAnswer(chatId, replyId, message);
443     }
444
445     public static boolean sendTelegramAnswer(ThingActions actions, @Nullable String chatId, @Nullable String replyId,
446             @Nullable String message) {
447         return ((TelegramActions) actions).sendTelegramAnswer(Long.valueOf(chatId), replyId, message);
448     }
449
450     @Override
451     public void setThingHandler(@Nullable ThingHandler handler) {
452         this.handler = (TelegramHandler) handler;
453     }
454
455     @Override
456     public @Nullable ThingHandler getThingHandler() {
457         return handler;
458     }
459 }