[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>
This commit is contained in:
Michael Murton
2021-09-18 09:08:00 -04:00
committed by GitHub
parent 88975dcd13
commit 51cb1aabd5
5 changed files with 292 additions and 42 deletions

View 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";
}

View 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);

View File

@@ -105,6 +105,43 @@ 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 = "callbackId") @Nullable String callbackId,
@ActionInput(name = "messageId") @Nullable Long messageId,
@ActionInput(name = "message") @Nullable String message) {
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) {
if (callbackId != null) {
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;
}
}
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 = "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,
@@ -121,32 +158,13 @@ public class TelegramActions implements ThingActions {
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;
}
callbackId);
}
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
if (!evaluateResponse(localHandler.execute(editReplyMarkup))) {
return false;
}
return message != null ? sendTelegram(chatId, message) : true;
return sendTelegramAnswer(chatId, callbackId, messageId != null ? Long.valueOf(messageId) : null, message);
}
return false;
}
@@ -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;

View File

@@ -16,6 +16,10 @@
<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>
@@ -114,4 +118,64 @@
<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>