From 604e7a7253b32c61ef707fac2ee953c5ec9684ef Mon Sep 17 00:00:00 2001 From: Michael Murton <6764025+CrazyIvan359@users.noreply.github.com> Date: Sat, 18 Sep 2021 09:08:00 -0400 Subject: [PATCH] [telegram] Add event channels and Answer overload (#9251) * Add event channels and Answer overload Signed-off-by: Michael Murton <6764025+CrazyIvan359@users.noreply.github.com> Signed-off-by: Dave J Schoepel --- .../org.openhab.binding.telegram/README.md | 61 ++++++++- .../internal/TelegramBindingConstants.java | 5 + .../telegram/internal/TelegramHandler.java | 126 +++++++++++++++--- .../internal/action/TelegramActions.java | 70 +++++++--- .../resources/OH-INF/thing/thing-types.xml | 64 +++++++++ 5 files changed, 288 insertions(+), 38 deletions(-) diff --git a/bundles/org.openhab.binding.telegram/README.md b/bundles/org.openhab.binding.telegram/README.md index 51f7c87d2d7e3..f8516f6ac0dc0 100644 --- a/bundles/org.openhab.binding.telegram/README.md +++ b/bundles/org.openhab.binding.telegram/README.md @@ -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 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 index 4c575a49d457e..19cdd09520f31 100644 --- 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 @@ -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"; } 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 index 5126d1c253a46..c5e1c804a6781 100644 --- 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 @@ -49,9 +49,15 @@ 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 @@ * @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 boolean equals(@Nullable Object obj) { } } + private static Gson gson = new Gson(); private final List authorizedSenderChatId = new ArrayList<>(); private final List receiverChatId = new ArrayList<>(); - private final Logger logger = LoggerFactory.getLogger(TelegramHandler.class); private @Nullable ScheduledFuture thingOnlineStatusJob; @@ -247,6 +252,15 @@ private String getFullDownloadUrl(String fileId) { 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 updates) { final TelegramBot localBot = bot; if (localBot == null) { @@ -267,6 +281,7 @@ private int handleUpdates(List updates) { String replyId = null; Message message = update.message(); + CallbackQuery callbackQuery = update.callbackQuery(); if (message != null) { chatId = message.chat().id(); @@ -278,6 +293,60 @@ private int handleUpdates(List updates) { // 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 @@ private int handleUpdates(List updates) { } // 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 void updateChannel(String channelName, State state) { updateState(new ChannelUID(getThing().getUID(), channelName), state); } + public void triggerEvent(String channelName, String payload) { + triggerChannel(channelName, payload); + } + @Override public Collection> getServices() { return Collections.singleton(TelegramActions.class); diff --git a/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/action/TelegramActions.java b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/action/TelegramActions.java index e1a19e3560b7f..90469b3cb4722 100644 --- a/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/action/TelegramActions.java +++ b/bundles/org.openhab.binding.telegram/src/main/java/org/openhab/binding/telegram/internal/action/TelegramActions.java @@ -107,39 +107,30 @@ public String toString() { @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 boolean sendTelegramAnswer(@ActionInput(name = "chatId") @Nullable Long c 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 static boolean sendTelegramAnswer(ThingActions actions, @Nullable String } } + 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; diff --git a/bundles/org.openhab.binding.telegram/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.telegram/src/main/resources/OH-INF/thing/thing-types.xml index 08a5270e5176c..55cbeb00ead07 100644 --- a/bundles/org.openhab.binding.telegram/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.telegram/src/main/resources/OH-INF/thing/thing-types.xml @@ -16,6 +16,10 @@ + + + + @@ -114,4 +118,64 @@ + + trigger + + + + Event payload could contain the following, but `null` values will not be present: +
    +
  • Long `message_id` - Unique message ID in this chat
  • +
  • String `from` - First and/or last name of sender
  • +
  • Long `chat_id` - Unique chat ID
  • +
  • String `text` - Message text
  • +
  • String `animation_url` - URL to download animation from
  • +
  • String `audio_url` - URL to download audio from
  • +
  • String `document_url` - URL to download file from
  • +
  • Array `photo_url` - Array of URLs to download photos from
  • +
  • String `sticker_url` - URL to download sticker from
  • +
  • String `video_url` - URL to download video from
  • +
  • String `video_note_url` - URL to download video note from
  • +
  • String `voice_url` - URL to download voice clip from
  • +
+ ]]> +
+ +
+ + + trigger + + Raw Message from the Telegram library as JSON. + + + + + trigger + + + + Event payload could contain the following, but `null` values will not be present: +
    +
  • Long `message_id` - Unique message ID of the original Query message
  • +
  • String `from` - First and/or last name of sender
  • +
  • Long `chat_id` - Unique chat ID
  • +
  • String `callback_id` - Unique callback ID to send receipt confirmation to
  • +
  • String `reply_id` - Plain text name of original Query
  • +
  • String `text` - Selected response text from options give in original Query
  • +
+ ]]> +
+ +
+ + + trigger + + Raw Callback Query response from the Telegram library encoded as JSON. + + +