added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.telegram-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-telegram" description="Telegram Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.telegram/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Provides the actions for the Telegram API.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface ITelegramActions {
public boolean sendTelegramAnswer(@Nullable Long chatId, @Nullable String replyId, @Nullable String message);
public boolean sendTelegramAnswer(@Nullable String replyId, @Nullable String message);
public boolean sendTelegram(@Nullable Long chatId, @Nullable String message);
public boolean sendTelegram(@Nullable String message);
public boolean sendTelegramQuery(@Nullable Long chatId, @Nullable String message, @Nullable String replyId,
@Nullable String... buttons);
public boolean sendTelegramQuery(@Nullable String message, @Nullable String replyId, @Nullable String... buttons);
public boolean sendTelegram(@Nullable Long chatId, @Nullable String message, @Nullable Object... args);
public boolean sendTelegram(@Nullable String message, @Nullable Object... args);
public boolean sendTelegramPhoto(@Nullable Long chatId, @Nullable String photoURL, @Nullable String caption,
@Nullable String username, @Nullable String password);
public boolean sendTelegramPhoto(@Nullable String photoURL, @Nullable String caption, @Nullable String username,
@Nullable String password);
}

View File

@@ -0,0 +1,500 @@
/**
* Copyright (c) 2010-2020 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.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.io.IOUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.B64Code;
import org.openhab.binding.telegram.internal.TelegramHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
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.
* <p>
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
* the test <i>actions instanceof TelegramActions</i> fails. This test can fail
* due to an issue in openHAB core v2.5.0 where the {@link TelegramActions} class
* can be loaded by a different classloader than the <i>actions</i> instance.
*
* @author Alexander Krasnogolowy - Initial contribution
*
*/
@ThingActionsScope(name = "telegram")
@NonNullByDefault
public class TelegramActions implements ThingActions, ITelegramActions {
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;
}
@NonNullByDefault
private static class BasicResult implements Authentication.Result {
private final HttpHeader header;
private final URI uri;
private final String value;
public BasicResult(HttpHeader header, URI uri, String value) {
this.header = header;
this.uri = uri;
this.value = value;
}
@Override
public URI getURI() {
return this.uri;
}
@Override
public void apply(@Nullable Request request) {
if (request != null) {
request.header(this.header, this.value);
}
}
@Override
public String toString() {
return String.format("Basic authentication result for %s", this.uri);
}
}
@Override
@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;
}
@Override
@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.getReceiverChatIds()) {
if (!sendTelegramAnswer(chatId, replyId, message)) {
return false;
}
}
}
return true;
}
@Override
@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);
}
@Override
@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.getReceiverChatIds()) {
if (!sendTelegram(chatId, message)) {
return false;
}
}
}
return true;
}
@Override
@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);
}
@Override
@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.getReceiverChatIds()) {
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;
}
@Override
@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,
@ActionInput(name = "args") @Nullable Object... args) {
return sendTelegram(chatId, String.format(message, args));
}
@Override
@RuleAction(label = "Telegram message", description = "Sends a Telegram via Telegram API")
public boolean sendTelegram(@ActionInput(name = "message") @Nullable String message,
@ActionInput(name = "args") @Nullable Object... args) {
TelegramHandler localHandler = handler;
if (localHandler != null) {
for (Long chatId : localHandler.getReceiverChatIds()) {
if (!sendTelegram(chatId, message, 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) {
return sendTelegramPhoto(chatId, photoURL, caption, null, null);
}
@Override
@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,
@ActionInput(name = "username") @Nullable String username,
@ActionInput(name = "password") @Nullable String password) {
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.");
HttpClient client = localHandler.getClient();
if (client == null) {
return false;
}
Request request = client.newRequest(photoURL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS);
if (username != null && password != null) {
AuthenticationStore auth = client.getAuthenticationStore();
URI uri = URI.create(photoURL);
auth.addAuthenticationResult(new BasicResult(HttpHeader.AUTHORIZATION, uri,
"Basic " + B64Code.encode(username + ":" + password, StandardCharsets.ISO_8859_1)));
}
try {
ContentResponse contentResponse = request.send();
if (contentResponse.getStatus() == 200) {
byte[] fileContent = contentResponse.getContent();
sendPhoto = new SendPhoto(chatId, fileContent);
} else {
logger.warn("Download from {} failed with status: {}", photoURL, contentResponse.getStatus());
return false;
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("Download from {} failed with exception: {}", photoURL, e.getMessage());
return false;
}
} 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.");
final String photoB64Data;
if (photoURL.startsWith("data:")) { // support data URI scheme
String[] photoURLParts = photoURL.split(",");
if (photoURLParts.length > 1) {
photoB64Data = photoURLParts[1];
} else {
logger.warn("The provided base64 string is not a valid data URI scheme");
return false;
}
} else {
photoB64Data = photoURL;
}
InputStream is = Base64.getDecoder()
.wrap(new ByteArrayInputStream(photoB64Data.getBytes(StandardCharsets.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;
}
}
sendPhoto.caption(caption);
if (localHandler.getParseMode() != null) {
sendPhoto.parseMode(localHandler.getParseMode());
}
return evaluateResponse(localHandler.execute(sendPhoto));
}
return false;
}
@Override
@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,
@ActionInput(name = "username") @Nullable String username,
@ActionInput(name = "password") @Nullable String password) {
TelegramHandler localHandler = handler;
if (localHandler != null) {
for (Long chatId : localHandler.getReceiverChatIds()) {
if (!sendTelegramPhoto(chatId, photoURL, caption, username, password)) {
return false;
}
}
}
return true;
}
@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) {
return sendTelegramPhoto(photoURL, caption, null, null);
}
// legacy delegate methods
/* APIs without chatId parameter */
public static boolean sendTelegram(@Nullable ThingActions actions, @Nullable String format,
@Nullable Object... args) {
return invokeMethodOf(actions).sendTelegram(format, args);
}
public static boolean sendTelegramQuery(@Nullable ThingActions actions, @Nullable String message,
@Nullable String replyId, @Nullable String... buttons) {
return invokeMethodOf(actions).sendTelegramQuery(message, replyId, buttons);
}
public static boolean sendTelegramPhoto(@Nullable ThingActions actions, @Nullable String photoURL,
@Nullable String caption) {
return invokeMethodOf(actions).sendTelegramPhoto(photoURL, caption, null, null);
}
public static boolean sendTelegramPhoto(@Nullable ThingActions actions, @Nullable String photoURL,
@Nullable String caption, @Nullable String username, @Nullable String password) {
return invokeMethodOf(actions).sendTelegramPhoto(photoURL, caption, username, password);
}
public static boolean sendTelegramAnswer(@Nullable ThingActions actions, @Nullable String replyId,
@Nullable String message) {
return invokeMethodOf(actions).sendTelegramAnswer(replyId, message);
}
/* APIs with chatId parameter */
public static boolean sendTelegram(@Nullable ThingActions actions, @Nullable Long chatId, @Nullable String format,
@Nullable Object... args) {
return invokeMethodOf(actions).sendTelegram(chatId, format, args);
}
public static boolean sendTelegramQuery(@Nullable ThingActions actions, @Nullable Long chatId,
@Nullable String message, @Nullable String replyId, @Nullable String... buttons) {
return invokeMethodOf(actions).sendTelegramQuery(chatId, message, replyId, buttons);
}
public static boolean sendTelegramPhoto(@Nullable ThingActions actions, @Nullable Long chatId,
@Nullable String photoURL, @Nullable String caption) {
return invokeMethodOf(actions).sendTelegramPhoto(chatId, photoURL, caption, null, null);
}
public static boolean sendTelegramPhoto(@Nullable ThingActions actions, @Nullable Long chatId,
@Nullable String photoURL, @Nullable String caption, @Nullable String username, @Nullable String password) {
return invokeMethodOf(actions).sendTelegramPhoto(chatId, photoURL, caption, username, password);
}
public static boolean sendTelegramAnswer(@Nullable ThingActions actions, @Nullable Long chatId,
@Nullable String replyId, @Nullable String message) {
return invokeMethodOf(actions).sendTelegramAnswer(chatId, replyId, message);
}
public static boolean sendTelegramAnswer(@Nullable ThingActions actions, @Nullable String chatId,
@Nullable String replyId, @Nullable String message) {
return invokeMethodOf(actions).sendTelegramAnswer(Long.valueOf(chatId), replyId, message);
}
private static ITelegramActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(TelegramActions.class.getName())) {
if (actions instanceof ITelegramActions) {
return (ITelegramActions) actions;
} else {
return (ITelegramActions) Proxy.newProxyInstance(ITelegramActions.class.getClassLoader(),
new Class[] { ITelegramActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of TelegramActions");
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (TelegramHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.openhab.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 LASTMESSAGEURL = "lastMessageURL";
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";
public static final String LONGPOLLINGTIME = "longPollingTime";
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link TelegramConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Jens Runge - Initial contribution
*/
@NonNullByDefault
public class TelegramConfiguration {
/**
* Sample configuration parameter. Replace with your own.
*/
private @Nullable String botUsername;
private @Nullable String botToken;
private @Nullable List<String> chatIds;
private @Nullable String proxyHost;
private @Nullable Integer proxyPort;
private @Nullable String proxyType;
private String parseMode = "";
private int longPollingTime;
public @Nullable String getBotUsername() {
return botUsername;
}
public @Nullable String getBotToken() {
return botToken;
}
public @Nullable List<String> getChatIds() {
return chatIds;
}
public String getParseMode() {
return parseMode;
}
public @Nullable String getProxyHost() {
return proxyHost;
}
public @Nullable Integer getProxyPort() {
return proxyPort;
}
public @Nullable String getProxyType() {
return proxyType;
}
public int getLongPollingTime() {
return longPollingTime;
}
}

View File

@@ -0,0 +1,431 @@
/**
* Copyright (c) 2010-2020 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.net.InetSocketAddress;
import java.net.Proxy;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.telegram.bot.TelegramActions;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.PhotoSize;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.BaseRequest;
import com.pengrad.telegrambot.request.GetFile;
import com.pengrad.telegrambot.request.GetUpdates;
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
* @author Alexander Krasnogolowy - using Telegram library from pengrad
* @author Jan N. Klug - handle file attachments
*/
@NonNullByDefault
public class TelegramHandler extends BaseThingHandler {
@NonNullByDefault
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<Long> authorizedSenderChatId = new ArrayList<>();
private final List<Long> receiverChatId = new ArrayList<>();
private final Logger logger = LoggerFactory.getLogger(TelegramHandler.class);
private @Nullable ScheduledFuture<?> thingOnlineStatusJob;
// 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<ReplyKey, String> 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<ReplyKey, Integer> replyIdToMessageId = new HashMap<>();
private @Nullable TelegramBot bot;
private @Nullable OkHttpClient botLibClient;
private @Nullable HttpClient downloadDataClient;
private @Nullable ParseMode parseMode;
public TelegramHandler(Thing thing, @Nullable HttpClient httpClient) {
super(thing);
downloadDataClient = httpClient;
}
@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();
List<String> chatIds = config.getChatIds();
if (chatIds != null) {
createReceiverChatIdsAndAuthorizedSenderChatIds(chatIds);
}
String parseModeAsString = config.getParseMode();
if (!parseModeAsString.isEmpty()) {
try {
parseMode = ParseMode.valueOf(parseModeAsString);
} catch (IllegalArgumentException e) {
logger.warn("parseMode is invalid and will be ignored. Only Markdown or HTML are allowed values");
}
}
OkHttpClient.Builder prepareConnection = new OkHttpClient.Builder().connectTimeout(75, TimeUnit.SECONDS)
.readTimeout(75, TimeUnit.SECONDS);
String proxyHost = config.getProxyHost();
Integer proxyPort = config.getProxyPort();
String proxyType = config.getProxyType();
if (proxyHost != null && proxyPort != null) {
InetSocketAddress proxyAddr = new InetSocketAddress(proxyHost, proxyPort);
Proxy.Type proxyTypeParam = Proxy.Type.SOCKS;
if ("HTTP".equals(proxyType)) {
proxyTypeParam = Proxy.Type.HTTP;
}
Proxy proxy = new Proxy(proxyTypeParam, proxyAddr);
logger.debug("{} Proxy {}:{} is used for telegram ", proxyTypeParam, proxyHost, proxyPort);
prepareConnection.proxy(proxy);
}
botLibClient = prepareConnection.build();
updateStatus(ThingStatus.UNKNOWN);
delayThingOnlineStatus();
TelegramBot localBot = bot = new TelegramBot.Builder(botToken).okHttpClient(botLibClient).build();
localBot.setUpdatesListener(this::handleUpdates, this::handleExceptions,
getGetUpdatesRequest(config.getLongPollingTime()));
}
private void createReceiverChatIdsAndAuthorizedSenderChatIds(List<String> chatIds) {
authorizedSenderChatId.clear();
receiverChatId.clear();
for (String chatIdStr : chatIds) {
String trimmedChatId = chatIdStr.trim();
try {
if (trimmedChatId.startsWith("<")) {
// inbound only
authorizedSenderChatId.add(Long.valueOf(trimmedChatId.substring(1)));
} else if (trimmedChatId.startsWith(">")) {
// outbound only
receiverChatId.add(Long.valueOf(trimmedChatId.substring(1)));
} else {
// bi-directional (default)
Long chatId = Long.valueOf(trimmedChatId);
authorizedSenderChatId.add(chatId);
receiverChatId.add(chatId);
}
} catch (NumberFormatException e) {
logger.warn("The chat id {} is not a number and will be ignored", chatIdStr);
}
}
}
private GetUpdates getGetUpdatesRequest(int longPollingTime) {
return new GetUpdates().timeout(longPollingTime * 1000);
}
private void handleExceptions(TelegramException exception) {
final TelegramBot localBot = bot;
if (exception != null) {
if (exception.response() != null) {
BaseResponse localResponse = exception.response();
if (localResponse.errorCode() == 401) { // unauthorized
cancelThingOnlineStatusJob();
if (localBot != null) {
localBot.removeGetUpdatesListener();
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unauthorized attempt to connect to the Telegram server, please check if the bot token is valid");
return;
}
}
if (exception.getCause() != null) { // cause is only non-null in case of an IOException
cancelThingOnlineStatusJob();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.getMessage());
delayThingOnlineStatus();
return;
}
logger.warn("Telegram exception: {}", exception.getMessage());
}
}
private String getFullDownloadUrl(String fileId) {
final TelegramBot bot = this.bot;
if (bot == null) {
return "";
}
return bot.getFullFilePath(bot.execute(new GetFile(fileId)).file());
}
private int handleUpdates(List<Update> updates) {
final TelegramBot localBot = bot;
if (localBot == null) {
logger.warn("Cannot process updates if no telegram bot is present.");
return UpdatesListener.CONFIRMED_UPDATES_NONE;
}
cancelThingOnlineStatusJob();
updateStatus(ThingStatus.ONLINE);
for (Update update : updates) {
String lastMessageText = null;
Integer lastMessageDate = null;
String lastMessageFirstName = null;
String lastMessageLastName = null;
String lastMessageUsername = null;
String lastMessageURL = null;
Long chatId = null;
String replyId = null;
Message message = update.message();
if (message != null) {
chatId = message.chat().id();
if (!authorizedSenderChatId.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
}
// process content
if (message.audio() != null) {
lastMessageURL = getFullDownloadUrl(message.audio().fileId());
} else if (message.document() != null) {
lastMessageURL = getFullDownloadUrl(message.document().fileId());
} else if (message.photo() != null) {
PhotoSize[] photoSizes = message.photo();
logger.trace("Received photos {}", Arrays.asList(photoSizes));
Arrays.sort(photoSizes, Comparator.comparingInt(PhotoSize::fileSize).reversed());
lastMessageURL = getFullDownloadUrl(photoSizes[0].fileId());
} else if (message.text() != null) {
lastMessageText = message.text();
} else if (message.video() != null) {
lastMessageURL = getFullDownloadUrl(message.video().fileId());
} else if (message.voice() != null) {
lastMessageURL = getFullDownloadUrl(message.voice().fileId());
} else {
logger.debug("Received message with unsupported content: {}", message);
continue;
}
// 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 (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);
} 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(LASTMESSAGEURL, lastMessageURL != null ? new StringType(lastMessageURL) : 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;
}
private synchronized void delayThingOnlineStatus() {
thingOnlineStatusJob = scheduler.schedule(() -> {
// if no error was returned within 10s, we assume the initialization went well
updateStatus(ThingStatus.ONLINE);
}, 10, TimeUnit.SECONDS);
}
private synchronized void cancelThingOnlineStatusJob() {
final ScheduledFuture<?> thingOnlineStatusJob = this.thingOnlineStatusJob;
if (thingOnlineStatusJob != null) {
thingOnlineStatusJob.cancel(true);
this.thingOnlineStatusJob = null;
}
}
@Override
public void dispose() {
logger.debug("Trying to dispose Telegram client");
cancelThingOnlineStatusJob();
OkHttpClient localClient = botLibClient;
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<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(TelegramActions.class);
}
/**
* get the list of all authorized senders
*
* @return list of chatIds
*/
public List<Long> getAuthorizedSenderChatIds() {
return authorizedSenderChatId;
}
/**
* get the list of all receivers
*
* @return list of chatIds
*/
public List<Long> getReceiverChatIds() {
return receiverChatId;
}
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 <T extends BaseRequest, R extends BaseResponse> R execute(BaseRequest<T, R> request) {
TelegramBot localBot = bot;
return localBot != null ? localBot.execute(request) : null;
}
@Nullable
public HttpClient getClient() {
return downloadDataClient;
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 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.jetty.client.HttpClient;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* 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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(TELEGRAM_THING);
private final HttpClient httpClient;
@Activate
public TelegramHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@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, httpClient);
}
return null;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="telegram" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Telegram Binding</name>
<description>This is the binding for Telegram. It allows to send and receive messages.</description>
<author>Alexander Krasnogolowy</author>
</binding:binding>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="telegram"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="telegramBot">
<label>Telegram Bot</label>
<description>Thing to receive the latest message send to a Telegram Bot.</description>
<channels>
<channel id="lastMessageText" typeId="lastMessageText"/>
<channel id="lastMessageURL" typeId="lastMessageURL"/>
<channel id="lastMessageDate" typeId="lastMessageDate"/>
<channel id="lastMessageName" typeId="lastMessageName"/>
<channel id="lastMessageUsername" typeId="lastMessageUsername"/>
<channel id="chatId" typeId="chatId"/>
<channel id="replyId" typeId="replyId"/>
</channels>
<config-description>
<parameter name="botToken" type="text" required="true">
<label>Bot Token</label>
<description>Enter the bot token you received from the "BotFather".</description>
</parameter>
<parameter name="chatIds" type="text" required="true" multiple="true">
<label>Chat Id(s)</label>
<description>One or more chat id(s). Access modifiers ("&lt;" for inbound only, "&gt;" for outbound only) can be
used as prefix (optional).</description>
</parameter>
<parameter name="parseMode" type="text" required="false">
<label>Parse Mode</label>
<options>
<option value="">No Formatting</option>
<option value="HTML">HTML</option>
<option value="Markdown">Markdown</option>
</options>
<default></default>
<description>Support for formatted messages, values: Markdown or HTML. Default: no formatting is used.</description>
</parameter>
<parameter name="proxyHost" type="text">
<context>network-address</context>
<label>Proxy Host</label>
<description>Enter your proxy host. It will be used for telegram binding only and doesn't affect entire system.</description>
</parameter>
<parameter name="proxyPort" type="integer" max="65535" min="1" required="false">
<label>Proxy Port</label>
<description>Enter your proxy port.</description>
</parameter>
<parameter name="proxyType" type="text" required="false">
<label>Proxy Type</label>
<options>
<option value="SOCKS5">SOCKS5</option>
<option value="HTTP">HTTP</option>
</options>
<default>SOCKS5</default>
<description>Enter your proxy type. Default: SOCKS5</description>
</parameter>
<parameter name="longPollingTime" type="integer" min="0" max="50" unit="s">
<label>Long Polling Time</label>
<description>Enter the long polling time in seconds.</description>
<default>25</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="lastMessageText">
<item-type>String</item-type>
<label>Last Message Text</label>
<description>Contains the latest message text as a string</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastMessageURL">
<item-type>String</item-type>
<label>Last Message URL</label>
<description>Contains the URL of the latest message</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastMessageDate">
<item-type>DateTime</item-type>
<label>Last Message Date</label>
<description>Contains the latest message date as a DateTime</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastMessageName">
<item-type>String</item-type>
<label>Last Message Name</label>
<description>Contains the latest message senders name as a string</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastMessageUsername">
<item-type>String</item-type>
<label>Last Message Username</label>
<description>Contains the latest message senders username as a string</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="chatId">
<item-type>String</item-type>
<label>Chat Id</label>
<description>Contains the id of chat from where the message was received.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="replyId">
<item-type>String</item-type>
<label>Reply Id</label>
<description>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.</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>