]> git.basschouten.com Git - openhab-addons.git/commitdiff
[telegram] Add event channels and Answer overload (#9251)
authorMichael Murton <6764025+CrazyIvan359@users.noreply.github.com>
Sat, 18 Sep 2021 13:08:00 +0000 (09:08 -0400)
committerGitHub <noreply@github.com>
Sat, 18 Sep 2021 13:08:00 +0000 (15:08 +0200)
* Add event channels and Answer overload

Signed-off-by: Michael Murton <6764025+CrazyIvan359@users.noreply.github.com>
bundles/org.openhab.binding.telegram/README.md
bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramBindingConstants.java
bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/TelegramHandler.java
bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/action/TelegramActions.java
bundles/org.openhab.binding.telegram/src/main/resources/OH-INF/thing/thing-types.xml

index 51f7c87d2d7e32e5177296b6448c6876361da2b6..f8516f6ac0dc031bb8d6c05857bf32760578f057 100644 (file)
@@ -41,7 +41,7 @@ Otherwise you will not be able to receive those messages.
 
 **telegramBot** - A Telegram Bot that can send and receive messages.
 
-The Telegram binding supports the following things which originate from the last message sent to the Telegram bot:
+The Telegram binding supports the following state channels which originate from the last message sent to the Telegram bot:
 
 * message text or URL
 * message date
@@ -50,6 +50,8 @@ The Telegram binding supports the following things which originate from the last
 * 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)
 
+There are also event channels that provide received messages or query callback responses as JSON payloads for easier handling in rules.
+
 Please note that the binding channels cannot be used to send messages.
 In order to send a message, an action must be used instead.
 
@@ -105,7 +107,7 @@ or HTTP proxy server
 Thing telegram:telegramBot:Telegram_Bot [ chatIds="ID", botToken="TOKEN", proxyHost="localhost", proxyPort="8123", proxyType="HTTP" ]
 ```
 
-## Channels
+## State Channels
 
 | Channel Type ID                      | Item Type | Description                                                     |
 |--------------------------------------|-----------|-----------------------------------------------------------------|
@@ -122,6 +124,52 @@ Either `lastMessageText` or `lastMessageURL` are populated for a given message.
 If the message did contain text, the content is written to `lastMessageText`.
 If the message did contain an audio, photo, video or voice, the URL to retrieve that content can be found in `lastMessageURL`.
 
+## Event Channels
+
+### messageEvent
+
+When a message is received this channel will be triggered with a simplified version of the message data as the `event`, payload encoded as a JSON string.
+The following table shows the possible fields, any `null` values will be missing from the JSON payload.
+
+| Field            | Type   | Description                           |
+|------------------|--------|---------------------------------------|
+| `message_id`     | Long   | Unique message ID in this chat        |
+| `from`           | String | First and/or last name of sender      |
+| `chat_id`        | Long   | Unique chat ID                        |
+| `text`           | String | Message text                          |
+| `animation_url`  | String | URL to download animation from        |
+| `audio_url`      | String | URL to download audio from            |
+| `document_url`   | String | URL to download file from             |
+| `photo_url`      | Array  | Array of URLs to download photos from |
+| `sticker_url`    | String | URL to download sticker from          |
+| `video_url`      | String | URL to download video from            |
+| `video_note_url` | String | URL to download video note from       |
+| `voice_url`      | String | URL to download voice clip from       |
+
+### messageRawEvent
+
+When a message is received this channel will be triggered with the raw message data as the `event` payload, encoded as a JSON string.
+See the [`Message` class for details](https://github.com/pengrad/java-telegram-bot-api/blob/4.9.0/library/src/main/java/com/pengrad/telegrambot/model/Message.java)
+
+### callbackEvent
+
+When a Callback Query response is received this channel will be triggered with a simplified version of the callback data as the `event`, payload encoded as a JSON string.
+The following table shows the possible fields, any `null` values will be missing from the JSON payload.
+
+| Field         | Type   | Description                                                |
+|---------------|--------|------------------------------------------------------------|
+| `message_id`  | Long   | Unique message ID of the original Query message            |
+| `from`        | String | First and/or last name of sender                           |
+| `chat_id`     | Long   | Unique chat ID                                             |
+| `callback_id` | String | Unique callback ID to send receipt confirmation to         |
+| `reply_id`    | String | Plain text name of original Query                          |
+| `text`        | String | Selected response text from options give in original Query |
+
+### callbackRawEvent
+
+When a Callback Query response is received this channel will be triggered with the raw callback data as the `event` payload, encoded as a JSON string.
+See the [`CallbackQuery` class for details](https://github.com/pengrad/java-telegram-bot-api/blob/4.9.0/library/src/main/java/com/pengrad/telegrambot/model/CallbackQuery.java)
+
 ## Rule Actions
 
 This binding includes a number of rule actions, which allow the sending of Telegram messages from within rules.
@@ -172,6 +220,15 @@ Just put the chat id (must be a long value!) followed by an "L" as the first arg
 telegramAction.sendTelegram(1234567L, "Hello world!")
 ```
 
+### Advanced Callback Query Response
+
+This binding stores the `callbackId` and recalls it using the `replyId`, but this information is lost if openHAB restarts.
+If you store the `callbackId`, `chatId`, and optionally `messageId` somewhere that will be persisted when openHAB shuts down, you can use the following overload of `sendTelegramAnswer` to respond to any Callback Query.
+
+```
+telegramAction.sendTelegramAnswer(chatId, callbackId, messageId, message)
+```
+
 ## Full Example
 
 ### Send a text message to telegram chat
index 4c575a49d457e96f628129ec7be1770f51ef1132..19cdd09520f31147e1dc614fddf63626c8ac79f8 100644 (file)
@@ -41,4 +41,9 @@ public class TelegramBindingConstants {
     public static final String LASTMESSAGEUSERNAME = "lastMessageUsername";
     public static final String CHATID = "chatId";
     public static final String REPLYID = "replyId";
+    public static final String LONGPOLLINGTIME = "longPollingTime";
+    public static final String MESSAGEEVENT = "messageEvent";
+    public static final String MESSAGERAWEVENT = "messageRawEvent";
+    public static final String CALLBACKEVENT = "callbackEvent";
+    public static final String CALLBACKRAWEVENT = "callbackRawEvent";
 }
index 5126d1c253a46278864099041997ce72a2e2660d..c5e1c804a67817727569a683860fabefb259e9c3 100644 (file)
@@ -49,9 +49,15 @@ import org.openhab.core.types.UnDefType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
 import com.pengrad.telegrambot.TelegramBot;
 import com.pengrad.telegrambot.TelegramException;
 import com.pengrad.telegrambot.UpdatesListener;
+import com.pengrad.telegrambot.model.CallbackQuery;
 import com.pengrad.telegrambot.model.Message;
 import com.pengrad.telegrambot.model.PhotoSize;
 import com.pengrad.telegrambot.model.Update;
@@ -70,13 +76,12 @@ import okhttp3.OkHttpClient;
  * @author Jens Runge - Initial contribution
  * @author Alexander Krasnogolowy - using Telegram library from pengrad
  * @author Jan N. Klug - handle file attachments
+ * @author Michael Murton - add trigger channel
  */
 @NonNullByDefault
 public class TelegramHandler extends BaseThingHandler {
 
-    @NonNullByDefault
     private class ReplyKey {
-
         final Long chatId;
         final String replyId;
 
@@ -106,9 +111,9 @@ public class TelegramHandler extends BaseThingHandler {
         }
     }
 
+    private static Gson gson = new Gson();
     private final List<Long> authorizedSenderChatId = new ArrayList<>();
     private final List<Long> receiverChatId = new ArrayList<>();
-
     private final Logger logger = LoggerFactory.getLogger(TelegramHandler.class);
     private @Nullable ScheduledFuture<?> thingOnlineStatusJob;
 
@@ -247,6 +252,15 @@ public class TelegramHandler extends BaseThingHandler {
         return bot.getFullFilePath(bot.execute(new GetFile(fileId)).file());
     }
 
+    private void addFileUrlsToPayload(JsonObject filePayload) {
+        filePayload.addProperty("file_url",
+                getFullDownloadUrl(filePayload.getAsJsonPrimitive("file_id").getAsString()));
+        if (filePayload.has("thumb")) {
+            filePayload.getAsJsonObject("thumb").addProperty("file_url", getFullDownloadUrl(
+                    filePayload.getAsJsonObject("thumb").getAsJsonPrimitive("file_id").getAsString()));
+        }
+    }
+
     private int handleUpdates(List<Update> updates) {
         final TelegramBot localBot = bot;
         if (localBot == null) {
@@ -267,6 +281,7 @@ public class TelegramHandler extends BaseThingHandler {
             String replyId = null;
 
             Message message = update.message();
+            CallbackQuery callbackQuery = update.callbackQuery();
 
             if (message != null) {
                 chatId = message.chat().id();
@@ -278,6 +293,60 @@ public class TelegramHandler extends BaseThingHandler {
                     // chat
                 }
 
+                // build and publish messageEvent trigger channel payload
+                JsonObject messageRaw = JsonParser.parseString(gson.toJson(message)).getAsJsonObject();
+                JsonObject messagePayload = new JsonObject();
+                messagePayload.addProperty("message_id", message.messageId());
+                messagePayload.addProperty("from",
+                        String.join(" ", new String[] { message.from().firstName(), message.from().lastName() }));
+                messagePayload.addProperty("chat_id", message.chat().id());
+                if (messageRaw.has("text")) {
+                    messagePayload.addProperty("text", message.text());
+                }
+                if (messageRaw.has("animation")) {
+                    addFileUrlsToPayload(messageRaw.getAsJsonObject("animation"));
+                    messagePayload.add("animation_url", messageRaw.getAsJsonObject("animation").get("file_url"));
+                }
+                if (messageRaw.has("audio")) {
+                    addFileUrlsToPayload(messageRaw.getAsJsonObject("audio"));
+                    messagePayload.add("audio_url", messageRaw.getAsJsonObject("audio").get("file_url"));
+                }
+                if (messageRaw.has("document")) {
+                    addFileUrlsToPayload(messageRaw.getAsJsonObject("document"));
+                    messagePayload.add("document_url", messageRaw.getAsJsonObject("document").get("file_url"));
+                }
+                if (messageRaw.has("photo")) {
+                    JsonArray photoURLArray = new JsonArray();
+                    for (JsonElement photoPayload : messageRaw.getAsJsonArray("photo")) {
+                        JsonObject photoPayloadObject = photoPayload.getAsJsonObject();
+                        String photoURL = getFullDownloadUrl(
+                                photoPayloadObject.getAsJsonPrimitive("file_id").getAsString());
+                        photoPayloadObject.addProperty("file_url", photoURL);
+                        photoURLArray.add(photoURL);
+                    }
+                    messagePayload.add("photo_url", photoURLArray);
+                }
+                if (messageRaw.has("sticker")) {
+                    addFileUrlsToPayload(messageRaw.getAsJsonObject("sticker"));
+                    messagePayload.add("sticker_url", messageRaw.getAsJsonObject("sticker").get("file_url"));
+                }
+                if (messageRaw.has("video")) {
+                    addFileUrlsToPayload(messageRaw.getAsJsonObject("video"));
+                    messagePayload.add("video_url", messageRaw.getAsJsonObject("video").get("file_url"));
+                }
+                if (messageRaw.has("video_note")) {
+                    addFileUrlsToPayload(messageRaw.getAsJsonObject("video_note"));
+                    messagePayload.add("video_note_url", messageRaw.getAsJsonObject("video_note").get("file_url"));
+                }
+                if (messageRaw.has("voice")) {
+                    JsonObject voicePayload = messageRaw.getAsJsonObject("voice");
+                    String voiceURL = getFullDownloadUrl(voicePayload.getAsJsonPrimitive("file_id").getAsString());
+                    voicePayload.addProperty("file_url", voiceURL);
+                    messagePayload.addProperty("voice_url", voiceURL);
+                }
+                triggerEvent(MESSAGEEVENT, messagePayload.toString());
+                triggerEvent(MESSAGERAWEVENT, messageRaw.toString());
+
                 // process content
                 if (message.audio() != null) {
                     lastMessageURL = getFullDownloadUrl(message.audio().fileId());
@@ -300,28 +369,43 @@ public class TelegramHandler extends BaseThingHandler {
                 }
 
                 // process metadata
-                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 (lastMessageURL != null || lastMessageText != null) {
+                    lastMessageDate = message.date();
+                    lastMessageFirstName = message.from().firstName();
+                    lastMessageLastName = message.from().lastName();
+                    lastMessageUsername = message.from().username();
+                }
+            } else if (callbackQuery != null && callbackQuery.message() != null
+                    && callbackQuery.message().text() != null) {
+                String[] callbackData = 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().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);
+                    lastMessageDate = callbackQuery.message().date();
+                    lastMessageFirstName = callbackQuery.from().firstName();
+                    lastMessageLastName = callbackQuery.from().lastName();
+                    lastMessageUsername = callbackQuery.from().username();
+                    chatId = callbackQuery.message().chat().id();
+                    replyIdToCallbackId.put(new ReplyKey(chatId, replyId), callbackQuery.id());
+
+                    // build and publish callbackEvent trigger channel payload
+                    JsonObject callbackRaw = JsonParser.parseString(gson.toJson(callbackQuery)).getAsJsonObject();
+                    JsonObject callbackPayload = new JsonObject();
+                    callbackPayload.addProperty("message_id", callbackQuery.message().messageId());
+                    callbackPayload.addProperty("from", lastMessageFirstName + " " + lastMessageLastName);
+                    callbackPayload.addProperty("chat_id", callbackQuery.message().chat().id());
+                    callbackPayload.addProperty("callback_id", callbackQuery.id());
+                    callbackPayload.addProperty("reply_id", callbackData[0]);
+                    callbackPayload.addProperty("text", callbackData[1]);
+                    triggerEvent(CALLBACKEVENT, callbackPayload.toString());
+                    triggerEvent(CALLBACKRAWEVENT, callbackRaw.toString());
+
+                    logger.debug("Received callbackId {} for chatId {} and replyId {}", callbackQuery.id(), chatId,
+                            replyId);
                 } else {
                     logger.warn("The received callback query {} has not the right format (must be seperated by spaces)",
-                            update.callbackQuery().data());
+                            callbackQuery.data());
                 }
             }
             updateChannel(CHATID, chatId != null ? new StringType(chatId.toString()) : UnDefType.NULL);
@@ -376,6 +460,10 @@ public class TelegramHandler extends BaseThingHandler {
         updateState(new ChannelUID(getThing().getUID(), channelName), state);
     }
 
+    public void triggerEvent(String channelName, String payload) {
+        triggerChannel(channelName, payload);
+    }
+
     @Override
     public Collection<Class<? extends ThingHandlerService>> getServices() {
         return Collections.singleton(TelegramActions.class);
index e1a19e3560b7fad81e68954dc2c8eb93264e137b..90469b3cb4722198b8f18707a4a3e1d643edb1ad 100644 (file)
@@ -107,39 +107,30 @@ public class TelegramActions implements ThingActions {
 
     @RuleAction(label = "send an answer", description = "Send a Telegram answer using the Telegram API.")
     public boolean sendTelegramAnswer(@ActionInput(name = "chatId") @Nullable Long chatId,
-            @ActionInput(name = "replyId") @Nullable String replyId,
+            @ActionInput(name = "callbackId") @Nullable String callbackId,
+            @ActionInput(name = "messageId") @Nullable Long messageId,
             @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;
         }
+        if (messageId == null) {
+            logger.warn("messageId 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));
+                AnswerCallbackQuery answerCallbackQuery = new AnswerCallbackQuery(callbackId);
                 // 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
+                logger.debug("Answering query with callbackId '{}'", callbackId);
                 if (!evaluateResponse(localHandler.execute(answerCallbackQuery))) {
                     return false;
                 }
             }
-            Integer messageId = localHandler.removeMessageId(chatId, replyId);
-            if (messageId == null) {
-                logger.warn("messageId could not be found for chatId {} and replyId {}", chatId, replyId);
-                return false;
-            }
-            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
@@ -151,6 +142,33 @@ public class TelegramActions implements ThingActions {
         return false;
     }
 
+    @RuleAction(label = "send an answer", description = "Send a Telegram answer using the 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) {
+                logger.debug("AnswerCallbackQuery for chatId {} and replyId {} is the callbackId {}", chatId, replyId,
+                        callbackId);
+            }
+            Integer messageId = localHandler.removeMessageId(chatId, replyId);
+            logger.debug("remove messageId {} for chatId {} and replyId {}", messageId, chatId, replyId);
+
+            return sendTelegramAnswer(chatId, callbackId, messageId != null ? Long.valueOf(messageId) : null, message);
+        }
+        return false;
+    }
+
     @RuleAction(label = "send an answer", description = "Send a Telegram answer using the Telegram API.")
     public boolean sendTelegramAnswer(@ActionInput(name = "replyId") @Nullable String replyId,
             @ActionInput(name = "message") @Nullable String message) {
@@ -652,6 +670,24 @@ public class TelegramActions implements ThingActions {
         }
     }
 
+    public static boolean sendTelegramAnswer(ThingActions actions, @Nullable Long chatId, @Nullable String callbackId,
+            @Nullable Long messageId, @Nullable String message) {
+        return ((TelegramActions) actions).sendTelegramAnswer(chatId, callbackId, messageId, message);
+    }
+
+    public static boolean sendTelegramAnswer(ThingActions actions, @Nullable String chatId, @Nullable String callbackId,
+            @Nullable String messageId, @Nullable String message) {
+        if (actions instanceof TelegramActions) {
+            if (chatId == null) {
+                return false;
+            }
+            return ((TelegramActions) actions).sendTelegramAnswer(Long.valueOf(chatId), callbackId,
+                    messageId != null ? Long.parseLong(messageId) : null, message);
+        } else {
+            throw new IllegalArgumentException("Actions is not an instance of TelegramActions");
+        }
+    }
+
     @Override
     public void setThingHandler(@Nullable ThingHandler handler) {
         this.handler = (TelegramHandler) handler;
index 08a5270e5176cf151d941612c0cb9786dd9bd929..55cbeb00ead07cdf37c6cf7b204865f4a8e5e4a9 100644 (file)
                        <channel id="lastMessageUsername" typeId="lastMessageUsername"/>
                        <channel id="chatId" typeId="chatId"/>
                        <channel id="replyId" typeId="replyId"/>
+                       <channel id="messageEvent" typeId="messageEvent"/>
+                       <channel id="messageRawEvent" typeId="messageRawEvent"/>
+                       <channel id="callbackEvent" typeId="callbackEvent"/>
+                       <channel id="callbackRawEvent" typeId="callbackRawEvent"/>
                </channels>
 
                <config-description>
                <state readOnly="true"/>
        </channel-type>
 
+       <channel-type id="messageEvent" advanced="true">
+               <kind>trigger</kind>
+               <label>Message Received</label>
+               <description>
+                       <![CDATA[
+                       Message encoded as JSON.<br />
+                       Event payload could contain the following, but `null` values will not be present:
+                       <ul>
+                               <li>Long `message_id` - Unique message ID in this chat</li>
+                               <li>String `from` - First and/or last name of sender</li>
+                               <li>Long `chat_id` - Unique chat ID</li>
+                               <li>String `text` - Message text</li>
+                               <li>String `animation_url` - URL to download animation from</li>
+                               <li>String `audio_url` - URL to download audio from</li>
+                               <li>String `document_url` - URL to download file from</li>
+                               <li>Array `photo_url` - Array of URLs to download photos from</li>
+                               <li>String `sticker_url` - URL to download sticker from</li>
+                               <li>String `video_url` - URL to download video from</li>
+                               <li>String `video_note_url` - URL to download video note from</li>
+                               <li>String `voice_url` - URL to download voice clip from</li>
+                       </ul>
+                       ]]>
+               </description>
+               <event></event>
+       </channel-type>
+
+       <channel-type id="messageRawEvent" advanced="true">
+               <kind>trigger</kind>
+               <label>Raw Message Received</label>
+               <description>Raw Message from the Telegram library as JSON.</description>
+               <event></event>
+       </channel-type>
+
+       <channel-type id="callbackEvent" advanced="true">
+               <kind>trigger</kind>
+               <label>Query Callback Received</label>
+               <description>
+                       <![CDATA[
+                       Callback Query response encoded as JSON.<br />
+                       Event payload could contain the following, but `null` values will not be present:
+                       <ul>
+                               <li>Long `message_id` - Unique message ID of the original Query message</li>
+                               <li>String `from` - First and/or last name of sender</li>
+                               <li>Long `chat_id` - Unique chat ID</li>
+                               <li>String `callback_id` - Unique callback ID to send receipt confirmation to</li>
+                               <li>String `reply_id` - Plain text name of original Query</li>
+                               <li>String `text` - Selected response text from options give in original Query</li>
+                       </ul>
+                       ]]>
+               </description>
+               <event></event>
+       </channel-type>
+
+       <channel-type id="callbackRawEvent" advanced="true">
+               <kind>trigger</kind>
+               <label>Raw Callback Query Received</label>
+               <description>Raw Callback Query response from the Telegram library encoded as JSON.</description>
+               <event></event>
+       </channel-type>
+
 </thing:thing-descriptions>