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,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.telegram</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,25 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
Java Telegram Bot API
* License: Apache License 2.0
* Project: https://github.com/pengrad/java-telegram-bot-api
* Source: https://github.com/pengrad/java-telegram-bot-api
okhttp
* License: Apache License 2.0
* Project: https://square.github.io/okhttp/
* Source: https://github.com/square/okhttp

View File

@@ -0,0 +1,340 @@
# Telegram Binding
The Telegram binding allows sending and receiving messages to and from Telegram clients (https://telegram.org), by using the Telegram Bot API.
# Prerequisites
As described in the Telegram Bot API, this is the manual procedure needed in order to get the necessary information.
1. Create the Bot and get the Token
- On a Telegram client open a chat with BotFather.
- Send `/newbot` to BotFather and fill in all the needed information. The authentication token that is given will be needed in the next steps.
2. Create the destination chat
- Open a chat with your new Bot and send any message to it. The next step will not work unless you send a message to your bot first.
3. Get the chatID
- Open a browser and invoke `https://api.telegram.org/bot<token>/getUpdates` (where `<token>` is the authentication token previously obtained)
- Look at the JSON result to find the value of `id`: that's the chatID.
Note that if using a Telegram group chat, the group chatIDs are prefixed with a dash that must be included in the config (e.g. `-22334455`).
If this does not work for you (the JSON response may be empty), or you want to send to *more* than one recipient (= another chatID), the alternative is to contact (= open a chat with) a Telegram bot to respond with the chatID.
There's a number of them such as `@myidbot` or `@chatid_echo_bot` - open a chat, eventually tap `/start` and it will return the chatID you're looking for.
Another option is `@getidsbot` which gives you much more information.
Note bots may work or not at any time so eventually you need to try another one.
4. Test the bot
- Open this URL in your web browser, replacing <token> with the authentication token and <chatId> with the chatId:
- `https://api.telegram.org/bot<token>/sendMessage?chat_id=<chatId>&text=testing`
- Your Telegram-bot should send you a message with the text: `testing`
**Notice:** By default your bot will only receive messages that either start with the '/' symbol or mention the bot by username (or if you talk to it directly). However, if you add your bot to a group you must either talk to BotFather and send the command "/setprivacy" and then disable it or you give admin rights to your bot in that group. Otherwise you will not be able to receive those messages.
## Supported Things
**telegramBot** - A Telegram Bot that can send and receive messages.
The Telegram binding supports the following things which origin from the latest message sent to the Telegram bot:
* message text or URL
* message date
* full name of sender (first name + last name)
* username of sender
* chat id (used to identify the chat of the last message)
* reply id (used to identify an answer from a user of a previously sent message by the binding)
Please note that the things cannot be used to send messages.
In order to send a message, an action must be used instead.
## Thing Configuration
**telegramBot** parameters:
| Property | Default | Required | Description |
|-------------------------|---------|:--------:|----------------------------------------------------------------------------------------------|
| `chatIds` | | Yes | Comma-separated list of chat ids |
| `botToken` | | Yes | authentication token |
| `parseMode` | None | No | Support for formatted messages, values: Markdown or HTML. |
| `proxyHost` | None | No | Proxy host for telegram binding. |
| `proxyPort` | None | No | Proxy port for telegram binding. |
| `proxyType` | SOCKS5 | No | Type of proxy server for telegram binding (SOCKS5 or HTTP). Default: SOCKS5 |
| `longPollingTime` | 25 | No | Timespan for long polling the telegram API |
By default chat ids are bi-directionally, i.e. they can send and receive messages.
They can be prefixed with an access modifier:
- `<` restricts the chat to send only, i.e. this chat id can send messages to openHAB, but will never receive a notification.
- `>` restricts the chat to receive only, i.e. this chat id will receive all notifications, but messages from this chat id will be discarded.
To use the reply function, chat ids need to be bi-directional.
telegram.thing (no proxy):
```
Thing telegram:telegramBot:Telegram_Bot [ chatIds="ID", botToken="TOKEN" ]
```
telegram.thing (multiple chat ids, one bi-directional chat (ID1), one outbound-only (ID2)):
```
Thing telegram:telegramBot:Telegram_Bot [ chatIds="ID1",">ID2", botToken="TOKEN" ]
```
telegram.thing (markdown format):
```
Thing telegram:telegramBot:Telegram_Bot [ chatIds="ID", botToken="TOKEN", parseMode ="Markdown" ]
```
telegram.thing (SOCKS5 proxy server is used):
```
Thing telegram:telegramBot:Telegram_Bot [ chatIds="ID", botToken="TOKEN", proxyHost="HOST", proxyPort="PORT", proxyType="TYPE" ]
```
or HTTP proxy server
```
Thing telegram:telegramBot:Telegram_Bot [ chatIds="ID", botToken="TOKEN", proxyHost="localhost", proxyPort="8123", proxyType="HTTP" ]
```
## Channels
| Channel Type ID | Item Type | Description |
|--------------------------------------|-----------|-----------------------------------------------------------------|
| lastMessageText | String | The last received message |
| lastMessageURL | String | The URL of the last received message content |
| lastMessageDate | DateTime | The date of the last received message (UTC) |
| lastMessageName | String | The full name of the sender of the last received message |
| lastMessageUsername | String | The username of the sender of the last received message |
| chatId | String | The id of the chat of the last received message |
| replyId | String | The id of the reply which was passed to sendTelegram() as replyId argument. This id can be used to have an unambiguous assignment of the users reply to the message which was sent by the bot |
All channels are read-only.
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`.
## Rule Actions
This binding includes a rule action, which allows to send Telegram messages from within rules.
```
val telegramAction = getActions("telegram","telegram:telegramBot:<uid>")
```
where uid is the Thing UID of the Telegram thing (not the chat id!).
Once this action instance is retrieved, you can invoke the `sendTelegram' method on it:
```
telegramAction.sendTelegram("Hello world!")
```
The following actions are supported.
Each of the actions returns true on success or false on failure.
### Actions to send messages to all configured chats
These actions will send a message to all chat ids configured for this bot.
| Action | Description |
|----------------------------|--------------|
| sendTelegram(String message) | Sends a message. |
| sendTelegram(String format, Object... args) | Sends a formatted message (See https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html for more information).
| sendTelegramQuery(String message, String replyId, String... buttons) | Sends a question to the user that can be answered via the defined buttons. The replyId can be freely choosen and is sent back with the answer. Then, the id is required to identify what question has been answered (e.g. in case of multiple open questions). The final result looks like this: ![Telegram Inline Keyboard](doc/queryExample.png). |
| sendTelegramAnswer(String replyId, String message) | Sends a message after the user has answered a question. You should *always* call this method after you received an answer. It will remove buttons from the specific question and will also stop the progress bar displayed at the client side. If no message is necessary, just pass `null` here. |
| sendTelegramPhoto(String photoURL, String caption) | Sends a picture. The URL can be specified using the http, https, and file protocols or a base64 encoded image (simple base64 data or data URI scheme). |
| sendTelegramPhoto(String photoURL, String caption, String username, String password) | Sends a picture which is downloaded from a username/password protected http/https address. |
### Actions to send messages to a particular chat
Just put the chat id (must be a long value!) as the first argument to one of the above mentioned APIs:
```
telegramAction.sendTelegram(1234567L, "Hello world!")
```
## Full Example
### Send a text message to telegram chat
telegram.rules
```java
rule "Send telegram with Fixed Message"
when
Item Foo changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegram("item Foo changed")
end
```
### Send a text message with a formatted message
telegram.rules
```java
rule "Send telegram with Formatted Message"
when
Item Foo changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegram("item Foo changed to %s and number is %.1f", Foo.state.toString, 23.56)
end
```
### Send an image to telegram chat
`http`, `https`, and `file` are the only protocols allowed or a base64 encoded image.
telegram.rules
```java
rule "Send telegram with image and caption from image accessible by url"
when
Item Light_GF_Living_Table changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegramPhoto("http://www.openhab.org/assets/images/openhab-logo-top.png",
"sent from openHAB")
end
```
telegram.rules
```java
rule "Send telegram with image without caption from image accessible by url"
when
Item Light_GF_Living_Table changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegramPhoto("http://www.openhab.org/assets/images/openhab-logo-top.png",
null)
end
```
telegram.rules
```java
rule "Send telegram with image from password protected http source"
when
Item Light_GF_Living_Table changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegramPhoto("http://192.168.1.5/doorcam/picture.jpg", "Door Camera", "user", "mypassword")
end
```
To send a base64 jpeg or png image:
telegram.rules
```java
rule "Send telegram with base64 image and caption"
when
Item Light_GF_Living_Table changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
// image as base64 string
var String base64Image = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAS1BMVEUAAABAQEA9QUc7P0Y0OD88QEY+QUhmaW7c3N3w8PBlaG0+QUjb29w5PUU3O0G+vsigoas6P0WfoKo4O0I9QUdkZ2w9Qkg+QkkkSUnT3FKbAAAAGXRSTlMACJbx//CV9v//9pT/7Ur//+z/SfD2kpMHrnfDaAAAAGhJREFUeAHt1bUBAzAMRFGZmcL7LxpOalN5r/evLIlgGwBgXMhxSjP64sa6cdYH+hLWzYiKvqSbI4kQeEt5PlBealsMFIkAAgi8HNriOLcjduLTafWwBB9n3p8v/+Ma1Mxxvd4IAGCzB4xDPuBRkEZiAAAAAElFTkSuQmCC"
telegramAction.sendTelegramPhoto(base64Image, "battery of motion sensor is empty")
// image as base64 string in data URI scheme
var String base64ImageDataURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAS1BMVEUAAABAQEA9QUc7P0Y0OD88QEY+QUhmaW7c3N3w8PBlaG0+QUjb29w5PUU3O0G+vsigoas6P0WfoKo4O0I9QUdkZ2w9Qkg+QkkkSUnT3FKbAAAAGXRSTlMACJbx//CV9v//9pT/7Ur//+z/SfD2kpMHrnfDaAAAAGhJREFUeAHt1bUBAzAMRFGZmcL7LxpOalN5r/evLIlgGwBgXMhxSjP64sa6cdYH+hLWzYiKvqSbI4kQeEt5PlBealsMFIkAAgi8HNriOLcjduLTafWwBB9n3p8v/+Ma1Mxxvd4IAGCzB4xDPuBRkEZiAAAAAElFTkSuQmCC"
telegramAction.sendTelegramPhoto(base64ImageDataURI, "battery of motion sensor is empty")
end
```
To send an image that resides on the local computer file system:
telegram.rules
```java
rule "Send telegram with local image and caption"
when
Item Light_GF_Living_Table changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegramPhoto("file://C:/mypicture.jpg", "sent from openHAB")
end
```
To send an image based on an Image Item:
telegram.rules
```java
rule "Send telegram with Image Item image and caption"
when
Item Webcam_Image changed
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegramPhoto(Webcam_Image.state.toFullString, "sent from openHAB")
end
```
To receive a message and react on that:
telegram.items
```php
String telegramMessage "Telegram Message" { channel = "telegram:telegramBot:2b155b22:lastMessageText" }
```
telegram.rules
```java
rule "Receive telegram"
when
Item telegramMessage received update "lights off"
then
gLights.sendCommand(OFF)
end
```
To send a question with two alternatives and a reply from the bot:
telegram.items
```php
String telegramReplyId "Telegram Reply Id" { channel = "telegram:telegramBot:2b155b22:replyId" }
```
telegram.rules
```java
rule "Send telegram with question"
when
Item Presence changed to OFF
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
telegramAction.sendTelegramQuery("No one is at home, but some lights are still on. Do you want me to turn off the lights?", "Reply_Lights", "Yes", "No")
end
rule "Reply handler for lights"
when
Item telegramReplyId received update Reply_Lights
then
val telegramAction = getActions("telegram","telegram:telegramBot:2b155b22")
if (telegramMessage.state.toString == "Yes")
{
gLights.sendCommand(OFF)
telegramAction.sendTelegramAnswer(telegramReplyId.state.toString, "Ok, lights are *off* now.")
}
else
{
telegramAction.sendTelegramAnswer(telegramReplyId.state.toString, "Ok, I'll leave them *on*.")
}
end
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.telegram</artifactId>
<name>openHAB Add-ons :: Bundles :: Telegram Binding</name>
<properties>
<bnd.importpackage>!android.*,!com.android.org.*,!dalvik.*,!javax.annotation.meta.*,!org.apache.harmony.*,!org.conscrypt.*,!sun.*</bnd.importpackage>
</properties>
<dependencies>
<dependency>
<groupId>com.github.pengrad</groupId>
<artifactId>java-telegram-bot-api</artifactId>
<version>4.4.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.12.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.15.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>3.12.3</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

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>