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