diff --git a/CODEOWNERS b/CODEOWNERS index 34786eb9a53b0..a02fc5446f22f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ /bundles/org.openhab.binding.systeminfo/ @svilenvul /bundles/org.openhab.binding.tado/ @dfrommi /bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag +/bundles/org.openhab.binding.telegram/ @ZzetT /bundles/org.openhab.binding.tellstick/ @jarlebh /bundles/org.openhab.binding.tesla/ @kgoderis /bundles/org.openhab.binding.toon/ @jongj diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 8b9c459a6bd83..7bda1bc9d2002 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -835,6 +835,11 @@ org.openhab.binding.tankerkoenig ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.telegram + ${project.version} + org.openhab.addons.bundles org.openhab.binding.tellstick diff --git a/bundles/org.openhab.binding.telegram/.classpath b/bundles/org.openhab.binding.telegram/.classpath new file mode 100644 index 0000000000000..5d788f3a5f722 --- /dev/null +++ b/bundles/org.openhab.binding.telegram/.classpath @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.telegram/.project b/bundles/org.openhab.binding.telegram/.project new file mode 100644 index 0000000000000..bdbfb24eed036 --- /dev/null +++ b/bundles/org.openhab.binding.telegram/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.telegram + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.telegram/NOTICE b/bundles/org.openhab.binding.telegram/NOTICE new file mode 100644 index 0000000000000..97ac7e3daaa02 --- /dev/null +++ b/bundles/org.openhab.binding.telegram/NOTICE @@ -0,0 +1,25 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab2-addons + +== Third-party Content + +Java Telegram Bot API +* License: Apache License 2.0 +* Project: https://github.com/pengrad/java-telegram-bot-api +* Source: https://github.com/pengrad/java-telegram-bot-api + +okhttp +* License: Apache License 2.0 +* Project: https://square.github.io/okhttp/ +* Source: https://github.com/square/okhttp \ No newline at end of file diff --git a/bundles/org.openhab.binding.telegram/README.md b/bundles/org.openhab.binding.telegram/README.md new file mode 100644 index 0000000000000..aef0855cfedaf --- /dev/null +++ b/bundles/org.openhab.binding.telegram/README.md @@ -0,0 +1,267 @@ +# Telegram Binding + +The Telegram binding allows sending and receiving messages to and from Telegram clients (https://telegram.org), by using the Telegram Bot API. + +# Prerequisites + +As described in the Telegram Bot API, this is the manual procedure needed in order to get the necessary information. + +1. Create the Bot and get the Token + +- On a Telegram client open a chat with BotFather. +- Send `/newbot` to BotFather and fill in all the needed information. The authentication token that is given will be needed in the next steps. + +2. Create the destination chat + +- Open a chat with your new Bot and send any message to it. The next step will not work unless you send a message to your bot first. + +3. Get the chatId + +- Open a browser and invoke `https://api.telegram.org/bot/getUpdates` (where `` is the authentication token previously obtained) +- Look at the JSON result to find the value of `id`. That is the chatId. Note that if using a Telegram group chat, the group chatIds are prefixed with a dash that must be included in the config file. (e.g. bot1.chatId: -22334455) + +4. Test the bot + +- Open this URL in your web browser, replacing with the authentication token and with the chatId: +- `https://api.telegram.org/bot/sendMessage?chat_id=&text=testing` +- Your Telegram-bot should send you a message with the text: `testing` + +**Notice:** By default your bot will only receive messages that either start with the '/' symbol or mention the bot by username (or if you talk to it directly). However, if you add your bot to a group you must either talk to BotFather and send the command "/setprivacy" and then disable it or you give admin rights to your bot in that group. Otherwise you will not be able to receive those messages. + +## Supported Things + +**telegramBot** - A Telegram Bot that can send and receive messages. + +The Telegram binding supports the following things which origin from the latest message sent to the Telegram bot: + +* message text +* message date +* full name of sender (first name + last name) +* username of sender +* chat id (used to identify the chat of the last message) +* reply id (used to identify an answer from a user of a previously sent message by the binding) + +Please note that the things cannot be used to send messages. In order to send a message, an action must be used instead. + +## Thing Configuration + +**telegramBot** parameters: + +| Property | Default | Required | Description | +|-------------------------|---------|:--------:|----------------------------------------------------------------------------------------------| +| `chatIds` | | Yes | Comma-separated list of chat ids | +| `botToken` | | Yes | authentication token | +| `parseMode` | None | No | Support for formatted messages, values: Markdown or HTML. | + +## Channels + +| Channel Type ID | Item Type | Description | +|--------------------------------------|-----------|-----------------------------------------------------------------| +| lastMessageText | String | The last received message | +| lastMessageDate | DateTime | The date of the last received message (UTC) | +| lastMessageName | String | The full name of the sender of the last received message | +| lastMessageUsername | String | The username of the sender of the last received message | +| chatId | String | The id of the chat of the last received meesage | +| replyId | String | The id of the reply which was passed to sendTelegram() as replyId argument. This id can be used to have an unambiguous assignment of the users reply to the message which was sent by the bot | + + +## Rule Actions + +This binding includes a rule action, which allows to send Telegram messages from within rules. + +``` +val telegramAction = getActions("telegram","telegram:telegramBot:") +``` + +where uid is the Thing UID of the Telegram thing (not the chat id!). + + +Once this action instance is retrieved, you can invoke the `sendTelegram' method on it: + +``` +telegramAction.sendTelegram("Hello world!") +``` + +The following actions are supported. +Each of the actions returns true on success or false on failure. + +### Actions to send messages to all configured chats + +These actions will send a message to all chat ids configured for this bot. + +| Action | Description | +|----------------------------|--------------| +| sendTelegram(String message) | Sends a message. | +| sendTelegram(String format, Object... args) | Sends a formatted message (See https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html for more information). +| sendTelegramQuery(String message, String replyId, String... buttons) | Sends a question to the user that can be answered via the defined buttons. The replyId can be freely choosen and is sent back with the answer. Then, the id is required to identify what question has been answered (e.g. in case of multiple open questions). The final result looks like this: ![Telegram Inline Keyboard](doc/queryExample.png). | +| sendTelegramAnswer(String replyId, String message) | Sends a message after the user has answered a question. You should *always* call this method after you received an answer. It will remove buttons from the specific question and will also stop the progress bar displayed at the client side. If no message is necessary, just pass `null` here. | +| sendTelegramPhoto(String photoURL, String caption) | Sends a picture. The URL can be specified using the http, https, and file protocols or a base64 encoded image. | + +### Actions to send messages to a particular chat + +Just put the chat id (must be a long value!) as the first argument to one of the above mentioned APIs: + +``` +telegramAction.sendTelegram(1234567L, "Hello world!") +``` + +## Full Example + +### Send a text message to telegram chat + +telegram.rules + +```java +rule "Send telegram with Fixed Message" +when + Item Foo changed +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + telegramAction.sendTelegram("item Foo changed") +end +``` + +### Send a text message with a formatted message + +telegram.rules + +```java +rule "Send telegram with Formatted Message" +when + Item Foo changed +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + telegramAction.sendTelegram("item Foo changed to %s and number is %.1f", Foo.state.toString, 23.56) +end +``` + +### Send an image to telegram chat + +`http`, `https`, and `file` are the only protocols allowed or a base64 encoded image. + +telegram.rules + +```java +rule "Send telegram with image and caption from image accessible by url" +when + Item Light_GF_Living_Table changed +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + telegramAction.sendTelegramPhoto("http://www.openhab.org/assets/images/openhab-logo-top.png", + "sent from openHAB") +end +``` + +telegram.rules + +```java +rule "Send telegram with image without caption from image accessible by url" +when + Item Light_GF_Living_Table changed +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + telegramAction.sendTelegramPhoto("http://www.openhab.org/assets/images/openhab-logo-top.png", + null) +end +``` + +To send a base64 jpeg or png image: + +telegram.rules + +```java +rule "Send telegram with base64 image and caption" +when + Item Light_GF_Living_Table changed +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + var String base64Image = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAS1BMVEUAAABAQEA9QUc7P0Y0OD88QEY+QUhmaW7c3N3w8PBlaG0+QUjb29w5PUU3O0G+vsigoas6P0WfoKo4O0I9QUdkZ2w9Qkg+QkkkSUnT3FKbAAAAGXRSTlMACJbx//CV9v//9pT/7Ur//+z/SfD2kpMHrnfDaAAAAGhJREFUeAHt1bUBAzAMRFGZmcL7LxpOalN5r/evLIlgGwBgXMhxSjP64sa6cdYH+hLWzYiKvqSbI4kQeEt5PlBealsMFIkAAgi8HNriOLcjduLTafWwBB9n3p8v/+Ma1Mxxvd4IAGCzB4xDPuBRkEZiAAAAAElFTkSuQmCC" + telegramAction.sendTelegramPhoto(base64Image, "battery of motion sensor is empty") +end +``` + +To send an image that resides on the local computer file system: + +telegram.rules + +```java +rule "Send telegram with local image and caption" +when + Item Light_GF_Living_Table changed +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + telegramAction.sendTelegramPhoto("file://C:/mypicture.jpg", "sent from openHAB") +end +``` + +To send an image based on an Image Item: + +telegram.rules + +```java +rule "Send telegram with Image Item image and caption" +when + Item Webcam_Image changed +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + telegramAction.sendTelegramPhoto(Webcam_Image.state.toFullString, "sent from openHAB") +end +``` + +To receive a message and react on that: + +telegram.items + +```php +String telegramMessage "Telegram Message" { channel = "telegram:telegramBot:2b155b22:lastMessageText" } +``` + +telegram.rules + +```java +rule "Receive telegram" +when + Item telegramMessage received update "lights off" +then + gLights.sendCommand(OFF) +end +``` + +To send a question with two alternatives and a reply from the bot: + +telegram.items + +```php +String telegramReplyId "Telegram Reply Id" { channel = "telegram:telegramBot:2b155b22:replyId" } +``` + +telegram.rules + +```java +rule "Send telegram with question" +when + Item Presence changed to OFF +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + telegramAction.sendTelegramQuery("No one is at home, but some lights are still on. Do you want me to turn off the lights?", "Reply_Lights", "Yes", "No") +end + + +rule "Reply handler for lights" +when + Item telegramReplyId received update Reply_Lights +then + val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22") + + if (telegramMessage.state.toString == "Yes") + { + gLights.sendCommand(OFF) + telegramAction.sendTelegramAnswer(telegramReplyId.state.toString, "Ok, lights are *off* now.") + } + else + { + telegramAction.sendTelegramAnswer(telegramReplyId.state.toString, "Ok, I'll leave them *on*.") + } +end +``` + diff --git a/bundles/org.openhab.binding.telegram/doc/queryExample.png b/bundles/org.openhab.binding.telegram/doc/queryExample.png new file mode 100644 index 0000000000000..eb76c72d7bb3c Binary files /dev/null and b/bundles/org.openhab.binding.telegram/doc/queryExample.png differ diff --git a/bundles/org.openhab.binding.telegram/pom.xml b/bundles/org.openhab.binding.telegram/pom.xml new file mode 100644 index 0000000000000..3329660941eea --- /dev/null +++ b/bundles/org.openhab.binding.telegram/pom.xml @@ -0,0 +1,47 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.0-SNAPSHOT + + + org.openhab.binding.telegram + + openHAB Add-ons :: Bundles :: Telegram Binding + + + !android.*,!com.android.org.*,!dalvik.*,!javax.annotation.meta.*,!org.apache.harmony.*,!org.conscrypt.*,!sun.* + + + + + com.github.pengrad + java-telegram-bot-api + 4.4.0 + compile + + + com.squareup.okhttp3 + okhttp + 3.12.3 + compile + + + com.squareup.okio + okio + 1.15.0 + compile + + + com.squareup.okhttp3 + logging-interceptor + 3.12.3 + + + + diff --git a/bundles/org.openhab.binding.telegram/src/main/feature/feature.xml b/bundles/org.openhab.binding.telegram/src/main/feature/feature.xml new file mode 100644 index 0000000000000..546b8a90dd885 --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/feature/feature.xml @@ -0,0 +1,13 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${project.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.telegram/${project.version} + wrap:mvn:com.github.pengrad/java-telegram-bot-api/4.4.0 + wrap:mvn:com.squareup.okhttp3/okhttp/3.12.3 + wrap:mvn:com.squareup.okio/okio/1.15.0 + wrap:mvn:com.squareup.okhttp3/logging-interceptor/3.12.3 + + diff --git a/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/bot/TelegramActions.java b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/bot/TelegramActions.java new file mode 100644 index 0000000000000..35879f7254eb6 --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/bot/TelegramActions.java @@ -0,0 +1,398 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.telegram.bot; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.Base64; + +import org.apache.commons.io.IOUtils; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.binding.ThingActions; +import org.eclipse.smarthome.core.thing.binding.ThingActionsScope; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.telegram.internal.TelegramHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.AnswerCallbackQuery; +import com.pengrad.telegrambot.request.EditMessageReplyMarkup; +import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.request.SendPhoto; +import com.pengrad.telegrambot.response.BaseResponse; +import com.pengrad.telegrambot.response.SendResponse; + +/** + * Provides the actions for the Telegram API. + * + * @author Alexander Krasnogolowy - Initial contribution + * + */ +@ThingActionsScope(name = "telegram") +@NonNullByDefault +public class TelegramActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(TelegramActions.class); + private @Nullable TelegramHandler handler; + + private boolean evaluateResponse(@Nullable BaseResponse response) { + if (response != null && !response.isOk()) { + logger.warn("Failed to send telegram message: {}", response.description()); + return false; + } + return true; + } + + @RuleAction(label = "Telegram answer", description = "Sends a Telegram answer via Telegram API") + public boolean sendTelegramAnswer(@ActionInput(name = "chatId") @Nullable Long chatId, + @ActionInput(name = "replyId") @Nullable String replyId, + @ActionInput(name = "message") @Nullable String message) { + if (replyId == null) { + logger.warn("ReplyId not defined; action skipped."); + return false; + } + if (chatId == null) { + logger.warn("chatId not defined; action skipped."); + return false; + } + TelegramHandler localHandler = handler; + if (localHandler != null) { + String callbackId = localHandler.getCallbackId(chatId, replyId); + if (callbackId != null) { + AnswerCallbackQuery answerCallbackQuery = new AnswerCallbackQuery( + localHandler.getCallbackId(chatId, replyId)); + logger.debug("AnswerCallbackQuery for chatId {} and replyId {} is the callbackId {}", chatId, replyId, + localHandler.getCallbackId(chatId, replyId)); + // we could directly set the text here, but this + // doesn't result in a real message only in a + // little popup or in an alert, so the only purpose + // is to stop the progress bar on client side + if (!evaluateResponse(localHandler.execute(answerCallbackQuery))) { + return false; + } + } + Integer messageId = localHandler.removeMessageId(chatId, replyId); + logger.debug("remove messageId {} for chatId {} and replyId {}", messageId, chatId, replyId); + + EditMessageReplyMarkup editReplyMarkup = new EditMessageReplyMarkup(chatId, messageId.intValue()) + .replyMarkup(new InlineKeyboardMarkup(new InlineKeyboardButton[0]));// remove reply markup from + // old message + if (!evaluateResponse(localHandler.execute(editReplyMarkup))) { + return false; + } + return message != null ? sendTelegram(chatId, message) : true; + + } + return false; + } + + @RuleAction(label = "Telegram answer", description = "Sends a Telegram answer via Telegram API") + public boolean sendTelegramAnswer(@ActionInput(name = "replyId") @Nullable String replyId, + @ActionInput(name = "message") @Nullable String message) { + TelegramHandler localHandler = handler; + if (localHandler != null) { + for (Long chatId : localHandler.getChatIds()) { + if (!sendTelegramAnswer(chatId, replyId, message)) { + return false; + } + } + } + return true; + } + + @RuleAction(label = "Telegram message", description = "Sends a Telegram via Telegram API") + public boolean sendTelegram(@ActionInput(name = "chatId") @Nullable Long chatId, + @ActionInput(name = "message") @Nullable String message) { + return sendTelegramGeneral(chatId, message, (String) null); + } + + @RuleAction(label = "Telegram message", description = "Sends a Telegram via Telegram API") + public boolean sendTelegram(@ActionInput(name = "message") @Nullable String message) { + TelegramHandler localHandler = handler; + if (localHandler != null) { + for (Long chatId : localHandler.getChatIds()) { + if (!sendTelegram(chatId, message)) { + return false; + } + } + } + return true; + } + + @RuleAction(label = "Telegram message", description = "Sends a Telegram via Telegram API") + public boolean sendTelegramQuery(@ActionInput(name = "chatId") @Nullable Long chatId, + @ActionInput(name = "message") @Nullable String message, + @ActionInput(name = "replyId") @Nullable String replyId, + @ActionInput(name = "buttons") @Nullable String... buttons) { + return sendTelegramGeneral(chatId, message, replyId, buttons); + } + + @RuleAction(label = "Telegram message", description = "Sends a Telegram via Telegram API") + public boolean sendTelegramQuery(@ActionInput(name = "message") @Nullable String message, + @ActionInput(name = "replyId") @Nullable String replyId, + @ActionInput(name = "buttons") @Nullable String... buttons) { + TelegramHandler localHandler = handler; + if (localHandler != null) { + for (Long chatId : localHandler.getChatIds()) { + if (!sendTelegramQuery(chatId, message, replyId, buttons)) { + return false; + } + } + } + return true; + } + + private boolean sendTelegramGeneral(@ActionInput(name = "chatId") @Nullable Long chatId, @Nullable String message, + @Nullable String replyId, @Nullable String... buttons) { + if (message == null) { + logger.warn("Message not defined; action skipped."); + return false; + } + if (chatId == null) { + logger.warn("chatId not defined; action skipped."); + return false; + } + TelegramHandler localHandler = handler; + if (localHandler != null) { + SendMessage sendMessage = new SendMessage(chatId, message); + if (localHandler.getParseMode() != null) { + sendMessage.parseMode(localHandler.getParseMode()); + } + if (replyId != null) { + if (!replyId.contains(" ")) { + if (buttons.length > 0) { + InlineKeyboardButton[][] keyboard2D = new InlineKeyboardButton[1][]; + InlineKeyboardButton[] keyboard = new InlineKeyboardButton[buttons.length]; + keyboard2D[0] = keyboard; + for (int i = 0; i < buttons.length; i++) { + keyboard[i] = new InlineKeyboardButton(buttons[i]).callbackData(replyId + " " + buttons[i]); + } + InlineKeyboardMarkup keyBoardMarkup = new InlineKeyboardMarkup(keyboard2D); + sendMessage.replyMarkup(keyBoardMarkup); + } else { + logger.warn( + "The replyId {} for message {} is given, but no buttons are defined. ReplyMarkup will be ignored.", + replyId, message); + } + } else { + logger.warn("replyId {} must not contain spaces. ReplyMarkup will be ignored.", replyId); + } + } + SendResponse retMessage = localHandler.execute(sendMessage); + if (!evaluateResponse(retMessage)) { + return false; + } + if (replyId != null && retMessage != null) { + logger.debug("Adding chatId {}, replyId {} and messageId {}", chatId, replyId, + retMessage.message().messageId()); + localHandler.addMessageId(chatId, replyId, retMessage.message().messageId()); + } + return true; + } + return false; + } + + @RuleAction(label = "Telegram message", description = "Sends a Telegram via Telegram API") + public boolean sendTelegram(@ActionInput(name = "chatId") @Nullable Long chatId, + @ActionInput(name = "message") @Nullable String format, + @ActionInput(name = "args") @Nullable Object... args) { + return sendTelegram(chatId, String.format(format, args)); + } + + @RuleAction(label = "Telegram message", description = "Sends a Telegram via Telegram API") + public boolean sendTelegram(@ActionInput(name = "message") @Nullable String format, + @ActionInput(name = "args") @Nullable Object... args) { + TelegramHandler localHandler = handler; + if (localHandler != null) { + for (Long chatId : localHandler.getChatIds()) { + if (!sendTelegram(chatId, format, args)) { + return false; + } + } + } + return true; + } + + @RuleAction(label = "Telegram photo", description = "Sends a Picture via Telegram API") + public boolean sendTelegramPhoto(@ActionInput(name = "chatId") @Nullable Long chatId, + @ActionInput(name = "photoURL") @Nullable String photoURL, + @ActionInput(name = "caption") @Nullable String caption) { + if (photoURL == null) { + logger.warn("Photo URL not defined; unable to retrieve photo for sending."); + return false; + } + if (chatId == null) { + logger.warn("chatId not defined; action skipped."); + return false; + } + + TelegramHandler localHandler = handler; + if (localHandler != null) { + final SendPhoto sendPhoto; + + if (photoURL.toLowerCase().startsWith("http")) { + // load image from url + logger.debug("Photo URL provided."); + sendPhoto = new SendPhoto(chatId, photoURL); + } else if (photoURL.toLowerCase().startsWith("file")) { + // Load image from local file system + logger.debug("Read file from local file system: {}", photoURL); + try { + URL url = new URL(photoURL); + sendPhoto = new SendPhoto(chatId, Paths.get(url.getPath()).toFile()); + } catch (MalformedURLException e) { + logger.warn("Malformed URL: {}", photoURL); + return false; + } + } else { + // Load image from provided base64 image + logger.debug("Photo base64 provided; converting to binary."); + try { + InputStream is = Base64.getDecoder().wrap(new ByteArrayInputStream(photoURL.getBytes("UTF-8"))); + try { + byte[] photoBytes = IOUtils.toByteArray(is); + sendPhoto = new SendPhoto(chatId, photoBytes); + } catch (IOException e) { + logger.warn("Malformed base64 string: {}", e.getMessage()); + return false; + } + } catch (UnsupportedEncodingException e) { + logger.warn("Cannot parse data fetched from photo URL as an image. Error: {}", e.getMessage()); + return false; + } + } + sendPhoto.caption(caption); + if (localHandler.getParseMode() != null) { + sendPhoto.parseMode(localHandler.getParseMode()); + } + return evaluateResponse(localHandler.execute(sendPhoto)); + } + return false; + } + + @RuleAction(label = "Telegram photo", description = "Sends a Picture via Telegram API") + public boolean sendTelegramPhoto(@ActionInput(name = "photoURL") @Nullable String photoURL, + @ActionInput(name = "caption") @Nullable String caption) { + TelegramHandler localHandler = handler; + if (localHandler != null) { + for (Long chatId : localHandler.getChatIds()) { + if (!sendTelegramPhoto(chatId, photoURL, caption)) { + return false; + } + } + } + return true; + } + + // legacy delegate methods + + public static boolean sendTelegram(@Nullable ThingActions actions, @Nullable String format, + @Nullable Object... args) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegram(format, args); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegramQuery(@Nullable ThingActions actions, @Nullable String message, + @Nullable String replyId, @Nullable String... buttons) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegramQuery(message, replyId, buttons); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegramPhoto(@Nullable ThingActions actions, @Nullable String photoURL, + @Nullable String caption) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegramPhoto(photoURL, caption); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegramAnswer(@Nullable ThingActions actions, @Nullable String replyId, + @Nullable String message) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegramAnswer(replyId, message); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegram(@Nullable ThingActions actions, @Nullable Long chatId, @Nullable String format, + @Nullable Object... args) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegram(chatId, format, args); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegramQuery(@Nullable ThingActions actions, @Nullable Long chatId, + @Nullable String message, @Nullable String replyId, @Nullable String... buttons) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegramQuery(chatId, message, replyId, buttons); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegramPhoto(@Nullable ThingActions actions, @Nullable Long chatId, + @Nullable String photoURL, @Nullable String caption) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegramPhoto(chatId, photoURL, caption); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegramAnswer(@Nullable ThingActions actions, @Nullable Long chatId, + @Nullable String replyId, @Nullable String message) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegramAnswer(chatId, replyId, message); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + public static boolean sendTelegramAnswer(@Nullable ThingActions actions, @Nullable String chatId, + @Nullable String replyId, @Nullable String message) { + if (actions instanceof TelegramActions) { + return ((TelegramActions) actions).sendTelegramAnswer(Long.valueOf(chatId), replyId, message); + } else { + throw new IllegalArgumentException("Instance is not a TelegramActions class."); + } + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + this.handler = (TelegramHandler) handler; + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } +} diff --git a/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramBindingConstants.java b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramBindingConstants.java new file mode 100644 index 0000000000000..75b287d3edbdd --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramBindingConstants.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.telegram.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +/** + * The {@link TelegramBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jens Runge - Initial contribution + */ +@NonNullByDefault +public class TelegramBindingConstants { + + private static final String BINDING_ID = "telegram"; + + // List of all Thing Type UIDs + public static final ThingTypeUID TELEGRAM_THING = new ThingTypeUID(BINDING_ID, "telegramBot"); + + // List of all Channel ids + public static final String LASTMESSAGETEXT = "lastMessageText"; + public static final String LASTMESSAGEDATE = "lastMessageDate"; + public static final String LASTMESSAGENAME = "lastMessageName"; + public static final String LASTMESSAGEUSERNAME = "lastMessageUsername"; + public static final String CHATID = "chatId"; + public static final String REPLYID = "replyId"; +} diff --git a/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramConfiguration.java b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramConfiguration.java new file mode 100644 index 0000000000000..2a54aec9b2dfa --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramConfiguration.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.telegram.internal; + +import java.util.List; + +/** + * The {@link TelegramConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jens Runge - Initial contribution + */ +public class TelegramConfiguration { + + /** + * Sample configuration parameter. Replace with your own. + */ + private String botUsername, botToken; + private List chatIds; + private String parseMode; + + public String getBotUsername() { + return botUsername; + } + + public String getBotToken() { + return botToken; + } + + public List getChatIds() { + return chatIds; + } + + public String getParseMode() { + return parseMode; + } + +} diff --git a/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramHandler.java b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramHandler.java new file mode 100644 index 0000000000000..f1558b956b56a --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramHandler.java @@ -0,0 +1,277 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.telegram.internal; + +import static org.openhab.binding.telegram.internal.TelegramBindingConstants.*; + +import java.time.ZonedDateTime; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DateTimeType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.telegram.bot.TelegramActions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pengrad.telegrambot.ExceptionHandler; +import com.pengrad.telegrambot.TelegramBot; +import com.pengrad.telegrambot.TelegramException; +import com.pengrad.telegrambot.UpdatesListener; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.response.BaseResponse; + +import okhttp3.OkHttpClient; + +/** + * The {@link TelegramHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jens Runge - Initial contribution + */ +@NonNullByDefault +public class TelegramHandler extends BaseThingHandler { + + private class ReplyKey { + + final Long chatId; + final String replyId; + + public ReplyKey(Long chatId, String replyId) { + this.chatId = chatId; + this.replyId = replyId; + } + + @Override + public int hashCode() { + return Objects.hash(chatId, replyId); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ReplyKey other = (ReplyKey) obj; + return Objects.equals(chatId, other.chatId) && Objects.equals(replyId, other.replyId); + } + } + + private final List chatIds = new ArrayList(); + private final Logger logger = LoggerFactory.getLogger(TelegramHandler.class); + + // Keep track of the callback id created by Telegram. This must be sent back in the answerCallbackQuery + // to stop the progress bar in the Telegram client + private final Map replyIdToCallbackId = new HashMap<>(); + // Keep track of message id sent with reply markup because we want to remove the markup after the user provided an + // answer and need the id of the original message + private final Map replyIdToMessageId = new HashMap<>(); + + + private @Nullable TelegramBot bot; + private @Nullable OkHttpClient client; + private @Nullable ParseMode parseMode; + + public TelegramHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // no commands to handle + } + + @Override + public void initialize() { + TelegramConfiguration config = getConfigAs(TelegramConfiguration.class); + + String botToken = config.getBotToken(); + chatIds.clear(); + for (String chatIdStr : config.getChatIds()) { + try { + chatIds.add(Long.valueOf(chatIdStr)); + } catch (NumberFormatException e) { + logger.warn("The chat id {} is not a number and will be ignored", chatIdStr); + } + } + if (config.getParseMode() != null) { + try { + parseMode = ParseMode.valueOf(config.getParseMode()); + } catch (IllegalArgumentException e) { + logger.warn("parseMode is invalid and will be ignored. Only Markdown or HTML are allowed values"); + } + } + + client = new OkHttpClient.Builder().connectTimeout(75, TimeUnit.SECONDS).readTimeout(75, TimeUnit.SECONDS) + .build(); + updateStatus(ThingStatus.ONLINE); + TelegramBot localBot = bot = new TelegramBot.Builder(botToken).okHttpClient(client).build(); + localBot.setUpdatesListener(updates -> { + for (Update update : updates) { + String lastMessageText = null; + Integer lastMessageDate = null; + String lastMessageFirstName = null; + String lastMessageLastName = null; + String lastMessageUsername = null; + Long chatId = null; + String replyId = null; + if (update.message() != null && update.message().text() != null) { + Message message = update.message(); + chatId = message.chat().id(); + if (!chatIds.contains(chatId)) { + logger.warn( + "Ignored message from unknown chat id {}. If you know the sender of that chat, add it to the list of chat ids in the thing configuration to authorize it", + chatId); + continue; // this is very important regarding security to avoid commands from an unknown + // chat + } + + lastMessageText = message.text(); + lastMessageDate = message.date(); + lastMessageFirstName = message.from().firstName(); + lastMessageLastName = message.from().lastName(); + lastMessageUsername = message.from().username(); + } else if (update.callbackQuery() != null && update.callbackQuery().message() != null + && update.callbackQuery().message().text() != null) { + String[] callbackData = update.callbackQuery().data().split(" ", 2); + + if (callbackData.length == 2) { + replyId = callbackData[0]; + lastMessageText = callbackData[1]; + lastMessageDate = update.callbackQuery().message().date(); + lastMessageFirstName = update.callbackQuery().from().firstName(); + lastMessageLastName = update.callbackQuery().from().lastName(); + lastMessageUsername = update.callbackQuery().message().from().username(); + chatId = update.callbackQuery().message().chat().id(); + replyIdToCallbackId.put(new ReplyKey(chatId, replyId), update.callbackQuery().id()); + logger.debug("Received callbackId {} for chatId {} and replyId {}", + update.callbackQuery().id(), chatId, replyId); + } else { + logger.warn( + "The received callback query {} has not the right format (must be seperated by spaces)", + update.callbackQuery().data()); + } + } + updateChannel(LASTMESSAGETEXT, + lastMessageText != null ? new StringType(lastMessageText) : UnDefType.NULL); + updateChannel(LASTMESSAGEDATE, + lastMessageDate != null + ? new DateTimeType(ZonedDateTime.ofInstant( + Instant.ofEpochSecond(lastMessageDate.intValue()), ZoneOffset.UTC)) + : UnDefType.NULL); + updateChannel(LASTMESSAGENAME, + (lastMessageFirstName != null || lastMessageLastName != null) + ? new StringType((lastMessageFirstName != null ? lastMessageFirstName + " " : "") + + (lastMessageLastName != null ? lastMessageLastName : "")) + : UnDefType.NULL); + updateChannel(LASTMESSAGEUSERNAME, + lastMessageUsername != null ? new StringType(lastMessageUsername) : UnDefType.NULL); + updateChannel(CHATID, chatId != null ? new StringType(chatId.toString()) : UnDefType.NULL); + updateChannel(REPLYID, replyId != null ? new StringType(replyId) : UnDefType.NULL); + } + return UpdatesListener.CONFIRMED_UPDATES_ALL; + }, exception -> { + if (exception != null) { + logger.warn("Telegram exception: {}", exception.getMessage()); + if (exception.response() != null) { + BaseResponse localResponse = exception.response(); + if (localResponse.errorCode() == 401) { + logger.error("Bot token invalid, disable thing {}", getThing().getUID()); + localBot.removeGetUpdatesListener(); + updateStatus(ThingStatus.OFFLINE); + } + } + } + }); + } + + @Override + public void dispose() { + logger.debug("Trying to dispose Telegram client"); + OkHttpClient localClient = client; + TelegramBot localBot = bot; + if (localClient != null && localBot != null) { + localBot.removeGetUpdatesListener(); + localClient.dispatcher().executorService().shutdown(); + localClient.connectionPool().evictAll(); + logger.debug("Telegram client closed"); + } + super.dispose(); + } + + public void updateChannel(String channelName, State state) { + updateState(new ChannelUID(getThing().getUID(), channelName), state); + } + + @Override + public Collection> getServices() { + return Collections.singleton(TelegramActions.class); + } + + public List getChatIds() { + return chatIds; + } + + public void addMessageId(Long chatId, String replyId, Integer messageId) { + replyIdToMessageId.put(new ReplyKey(chatId, replyId), messageId); + } + + @Nullable + public String getCallbackId(Long chatId, String replyId) { + return replyIdToCallbackId.get(new ReplyKey(chatId, replyId)); + } + + public Integer removeMessageId(Long chatId, String replyId) { + return replyIdToMessageId.remove(new ReplyKey(chatId, replyId)); + } + + @Nullable + public ParseMode getParseMode() { + return parseMode; + } + + @SuppressWarnings("rawtypes") + @Nullable + public R execute(BaseRequest request) { + TelegramBot localBot = bot; + return localBot != null ? localBot.execute(request) : null; + } + +} diff --git a/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramHandlerFactory.java b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramHandlerFactory.java new file mode 100644 index 0000000000000..a455e2c383918 --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramHandlerFactory.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2019 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.telegram.internal; + +import static org.openhab.binding.telegram.internal.TelegramBindingConstants.TELEGRAM_THING; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link TelegramHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jens Runge - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.telegram", service = ThingHandlerFactory.class) +public class TelegramHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(TELEGRAM_THING); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (TELEGRAM_THING.equals(thingTypeUID)) { + return new TelegramHandler(thing); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.telegram/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.telegram/src/main/resources/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..4f7e3db69e427 --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/resources/ESH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Telegram Binding + This is the binding for Telegram. It allows to send and receive messages. + Alexander Krasnogolowy + + diff --git a/bundles/org.openhab.binding.telegram/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.telegram/src/main/resources/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..096a21522797e --- /dev/null +++ b/bundles/org.openhab.binding.telegram/src/main/resources/ESH-INF/thing/thing-types.xml @@ -0,0 +1,84 @@ + + + + + + Thing to receive the latest message send to a Telegram Bot. + + + + + + + + + + + + + + Enter the bot token you received from the "BotFather". + + + + Enter your chat id. Only messages from this id will be send to openHAB. + + + + + + + + + Support for formatted messages, values: Markdown or HTML. Default: no formatting is used. + + + + + + + String + + Contains the latest message text as a string + + + + + DateTime + + Contains the latest message date as a DateTime + + + + + String + + Contains the latest message senders name as a string + + + + + String + + Contains the latest message senders username as a string + + + + + String + + Contains the id of chat from where the message was received. + + + + + String + + Contains the id of the reply which was passed to sendTelegram() as replyId. This id can be used to have an unambiguous assignment of the user reply to the message which was sent by the bot. + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index acb5450a4841d..a748072d1eec5 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -204,6 +204,7 @@ org.openhab.binding.systeminfo org.openhab.binding.tado org.openhab.binding.tankerkoenig + org.openhab.binding.telegram org.openhab.binding.tellstick org.openhab.binding.tesla org.openhab.binding.toon