2 * Copyright (c) 2010-2020 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.telegram.bot;
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;
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;
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;
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;
60 * Provides the actions for the Telegram API.
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.
67 * @author Alexander Krasnogolowy - Initial contribution
70 @ThingActionsScope(name = "telegram")
72 public class TelegramActions implements ThingActions, ITelegramActions {
73 private final Logger logger = LoggerFactory.getLogger(TelegramActions.class);
74 private @Nullable TelegramHandler handler;
76 private boolean evaluateResponse(@Nullable BaseResponse response) {
77 if (response != null && !response.isOk()) {
78 logger.warn("Failed to send telegram message: {}", response.description());
85 private static class BasicResult implements Authentication.Result {
87 private final HttpHeader header;
88 private final URI uri;
89 private final String value;
91 public BasicResult(HttpHeader header, URI uri, String value) {
103 public void apply(@Nullable Request request) {
104 if (request != null) {
105 request.header(this.header, this.value);
110 public String toString() {
111 return String.format("Basic authentication result for %s", this.uri);
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.");
124 if (chatId == null) {
125 logger.warn("chatId not defined; action skipped.");
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))) {
144 Integer messageId = localHandler.removeMessageId(chatId, replyId);
145 logger.debug("remove messageId {} for chatId {} and replyId {}", messageId, chatId, replyId);
147 EditMessageReplyMarkup editReplyMarkup = new EditMessageReplyMarkup(chatId, messageId.intValue())
148 .replyMarkup(new InlineKeyboardMarkup(new InlineKeyboardButton[0]));// remove reply markup from
150 if (!evaluateResponse(localHandler.execute(editReplyMarkup))) {
153 return message != null ? sendTelegram(chatId, message) : true;
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)) {
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);
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)) {
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);
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)) {
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.");
225 if (chatId == null) {
226 logger.warn("chatId not defined; action skipped.");
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());
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]);
244 InlineKeyboardMarkup keyBoardMarkup = new InlineKeyboardMarkup(keyboard2D);
245 sendMessage.replyMarkup(keyBoardMarkup);
248 "The replyId {} for message {} is given, but no buttons are defined. ReplyMarkup will be ignored.",
252 logger.warn("replyId {} must not contain spaces. ReplyMarkup will be ignored.", replyId);
255 SendResponse retMessage = localHandler.execute(sendMessage);
256 if (!evaluateResponse(retMessage)) {
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());
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));
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)) {
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);
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.");
310 if (chatId == null) {
311 logger.warn("chatId not defined; action skipped.");
315 TelegramHandler localHandler = handler;
316 if (localHandler != null) {
317 final SendPhoto sendPhoto;
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) {
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)));
334 ContentResponse contentResponse = request.send();
335 if (contentResponse.getStatus() == 200) {
336 byte[] fileContent = contentResponse.getContent();
337 sendPhoto = new SendPhoto(chatId, fileContent);
339 logger.warn("Download from {} failed with status: {}", photoURL, contentResponse.getStatus());
342 } catch (InterruptedException | TimeoutException | ExecutionException e) {
343 logger.warn("Download from {} failed with exception: {}", photoURL, e.getMessage());
346 } else if (photoURL.toLowerCase().startsWith("file")) {
347 // Load image from local file system
348 logger.debug("Read file from local file system: {}", photoURL);
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);
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];
365 logger.warn("The provided base64 string is not a valid data URI scheme");
369 photoB64Data = photoURL;
371 InputStream is = Base64.getDecoder()
372 .wrap(new ByteArrayInputStream(photoB64Data.getBytes(StandardCharsets.UTF_8)));
374 byte[] photoBytes = IOUtils.toByteArray(is);
375 sendPhoto = new SendPhoto(chatId, photoBytes);
376 } catch (IOException e) {
377 logger.warn("Malformed base64 string: {}", e.getMessage());
381 sendPhoto.caption(caption);
382 if (localHandler.getParseMode() != null) {
383 sendPhoto.parseMode(localHandler.getParseMode());
385 return evaluateResponse(localHandler.execute(sendPhoto));
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)) {
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);
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);
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);
425 public static boolean sendTelegramPhoto(@Nullable ThingActions actions, @Nullable String photoURL,
426 @Nullable String caption) {
427 return invokeMethodOf(actions).sendTelegramPhoto(photoURL, caption, null, null);
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);
435 public static boolean sendTelegramAnswer(@Nullable ThingActions actions, @Nullable String replyId,
436 @Nullable String message) {
437 return invokeMethodOf(actions).sendTelegramAnswer(replyId, message);
440 /* APIs with chatId parameter */
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);
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);
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);
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);
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);
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);
472 private static ITelegramActions invokeMethodOf(@Nullable ThingActions actions) {
473 if (actions == null) {
474 throw new IllegalArgumentException("actions cannot be null");
476 if (actions.getClass().getName().equals(TelegramActions.class.getName())) {
477 if (actions instanceof ITelegramActions) {
478 return (ITelegramActions) actions;
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);
488 throw new IllegalArgumentException("Actions is not an instance of TelegramActions");
492 public void setThingHandler(@Nullable ThingHandler handler) {
493 this.handler = (TelegramHandler) handler;
497 public @Nullable ThingHandler getThingHandler() {