[telegram] Add support for sendVideo (MP4) and sendAnimation (GIF) (#8969)

* Add sendVideo and sendAnimation features.
* Re-order functions to keep inline with other functions.
* Readme change to trigger new build.
* Add ability to use raw file paths to send video and animations.
* Change Paths.get to Path.of as JavaDocs recommend.
* Allow absolute paths in SendPhoto methods and update readme.md
* Support for no caption with photo.
* Add absolute path support for png and webp.
* Add all file types requested.
* Remove multiple OR and only do lowercase once.

Signed-off-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Matthew Skinner 2020-11-11 14:50:58 +11:00 committed by GitHub
parent 5a1428dddc
commit 7e5be7ef47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 236 additions and 40 deletions

View File

@ -4,9 +4,9 @@ The Telegram binding allows sending and receiving messages to and from Telegram
# Prerequisites
As described in the Telegram Bot API, this is the manual procedure needed in order to get the necessary information.
As described in the Telegram Bot API (https://core.telegram.org/bots#6-botfather), this is the manual procedure needed in order to get the necessary information.
1. Create the Bot and get the Token
1. Create a new 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.
@ -32,13 +32,15 @@ Note bots may work or not at any time so eventually you need to try another one.
- `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.
**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:
The Telegram binding supports the following things which originate from the last message sent to the Telegram bot:
* message text or URL
* message date
@ -47,22 +49,22 @@ The Telegram binding supports the following things which origin from the latest
* 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.
Please note that the binding channels 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 |
| Property | Default | Required | Description |
|-|-|-|-|
| `chatIds` | | Yes | A list of chatIds that are entered one per line in the UI, or are comma separated values when using textual config. |
| `botToken` | | Yes | Authentication token that looks like 1122334455:AABBCCDDEEFFGG1122334455667788 |
| `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). |
| `longPollingTime` | 25 | No | Timespan in seconds 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:
@ -84,7 +86,6 @@ telegram.thing (multiple chat ids, one bi-directional chat (ID1), one outbound-o
Thing telegram:telegramBot:Telegram_Bot [ chatIds="ID1",">ID2", botToken="TOKEN" ]
```
telegram.thing (markdown format):
```
@ -103,7 +104,6 @@ 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 |
@ -123,7 +123,7 @@ If the message did contain an audio, photo, video or voice, the URL to retrieve
## Rule Actions
This binding includes a rule action, which allows to send Telegram messages from within rules.
This binding includes a number of rule actions, which allow the sending of Telegram messages from within rules.
```
val telegramAction = getActions("telegram","telegram:telegramBot:<uid>")
@ -150,8 +150,18 @@ These actions will send a message to all chat ids configured for this bot.
| 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) | Sends a picture. Can be one of the URL formats, see the Note below, 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. |
| sendTelegramAnimation(String animationURL, String caption) | Send animation files either GIF or H.264/MPEG-4 AVC video without sound. |
| sendTelegramVideo(String videoURL, String caption) | Send MP4 video files up to 50MB. |
**Note:** In actions that require a file URL, the following formats are acceptable:
+ http://foo.com/bar.jpg
+ https://foo.com/bar.jpg
+ file://c:\\foo\\bar.jpg
+ c:\\foo\\bar.jpg
+ /etc/openhab/html/bar.jpg
### Actions to send messages to a particular chat
@ -336,5 +346,4 @@ then
telegramAction.sendTelegramAnswer(telegramReplyId.state.toString, "Ok, I'll leave them *on*.")
}
end
```
```

View File

@ -22,7 +22,7 @@
<dependency>
<groupId>com.github.pengrad</groupId>
<artifactId>java-telegram-bot-api</artifactId>
<version>4.4.0</version>
<version>4.9.0</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.telegram.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
@ -25,6 +27,8 @@ import org.openhab.core.thing.ThingTypeUID;
public class TelegramBindingConstants {
private static final String BINDING_ID = "telegram";
public static final Set<String> PHOTO_EXTENSIONS = Set.of(".jpg", ".jpeg", ".png", ".gif", ".jpe", ".jif", ".jfif",
".jfi", ".webp");
// List of all Thing Type UIDs
public static final ThingTypeUID TELEGRAM_THING = new ThingTypeUID(BINDING_ID, "telegramBot");

View File

@ -214,7 +214,7 @@ public class TelegramHandler extends BaseThingHandler {
return new GetUpdates().timeout(longPollingTime * 1000);
}
private void handleExceptions(TelegramException exception) {
private void handleExceptions(@Nullable TelegramException exception) {
final TelegramBot localBot = bot;
if (exception != null) {
if (exception.response() != null) {

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.telegram.internal.action;
import static org.openhab.binding.telegram.internal.TelegramBindingConstants.PHOTO_EXTENSIONS;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -19,13 +21,11 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.Path;
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;
@ -33,6 +33,7 @@ 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.client.util.FutureResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.B64Code;
@ -49,8 +50,10 @@ 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.SendAnimation;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendPhoto;
import com.pengrad.telegrambot.request.SendVideo;
import com.pengrad.telegrambot.response.BaseResponse;
import com.pengrad.telegrambot.response.SendResponse;
@ -296,14 +299,12 @@ public class TelegramActions implements ThingActions {
logger.warn("chatId not defined; action skipped.");
return false;
}
String lowercasePhotoUrl = photoURL.toLowerCase();
TelegramHandler localHandler = handler;
if (localHandler != null) {
final SendPhoto sendPhoto;
if (photoURL.toLowerCase().startsWith("http")) {
// load image from url
logger.debug("Photo URL provided.");
if (lowercasePhotoUrl.startsWith("http")) {
logger.debug("Http based URL for photo provided.");
HttpClient client = localHandler.getClient();
if (client == null) {
return false;
@ -316,7 +317,10 @@ public class TelegramActions implements ThingActions {
"Basic " + B64Code.encode(username + ":" + password, StandardCharsets.ISO_8859_1)));
}
try {
ContentResponse contentResponse = request.send();
// API has 10mb limit to jpg file size, without this it can only accept 2mb
FutureResponseListener listener = new FutureResponseListener(request, 10 * 1024 * 1024);
request.send(listener);
ContentResponse contentResponse = listener.get();
if (contentResponse.getStatus() == 200) {
byte[] fileContent = contentResponse.getContent();
sendPhoto = new SendPhoto(chatId, fileContent);
@ -324,23 +328,25 @@ public class TelegramActions implements ThingActions {
logger.warn("Download from {} failed with status: {}", photoURL, contentResponse.getStatus());
return false;
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
} catch (InterruptedException | 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
} else if (lowercasePhotoUrl.startsWith("file:")
|| PHOTO_EXTENSIONS.stream().anyMatch(lowercasePhotoUrl::endsWith)) {
logger.debug("Read file from local file system: {}", photoURL);
String temp = photoURL;
if (!lowercasePhotoUrl.startsWith("file:")) {
temp = "file://" + photoURL;
}
try {
URL url = new URL(photoURL);
sendPhoto = new SendPhoto(chatId, Paths.get(url.getPath()).toFile());
sendPhoto = new SendPhoto(chatId, Path.of(new URL(temp).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.");
logger.debug("Base64 image provided; converting to binary.");
final String photoB64Data;
if (photoURL.startsWith("data:")) { // support data URI scheme
String[] photoURLParts = photoURL.split(",");
@ -356,14 +362,16 @@ public class TelegramActions implements ThingActions {
InputStream is = Base64.getDecoder()
.wrap(new ByteArrayInputStream(photoB64Data.getBytes(StandardCharsets.UTF_8)));
try {
byte[] photoBytes = IOUtils.toByteArray(is);
byte[] photoBytes = is.readAllBytes();
sendPhoto = new SendPhoto(chatId, photoBytes);
} catch (IOException e) {
logger.warn("Malformed base64 string: {}", e.getMessage());
return false;
}
}
sendPhoto.caption(caption);
if (caption != null) {
sendPhoto.caption(caption);
}
if (localHandler.getParseMode() != null) {
sendPhoto.parseMode(localHandler.getParseMode());
}
@ -394,6 +402,162 @@ public class TelegramActions implements ThingActions {
return sendTelegramPhoto(photoURL, caption, null, null);
}
@RuleAction(label = "send animation", description = "Send an Animation using the Telegram API.")
public boolean sendTelegramAnimation(@ActionInput(name = "animationURL") @Nullable String animationURL,
@ActionInput(name = "caption") @Nullable String caption) {
TelegramHandler localHandler = handler;
if (localHandler != null) {
for (Long chatId : localHandler.getReceiverChatIds()) {
if (!sendTelegramAnimation(chatId, animationURL, caption)) {
return false;
}
}
}
return true;
}
@RuleAction(label = "send animation", description = "Send an Animation using the Telegram API.")
public boolean sendTelegramAnimation(@ActionInput(name = "chatId") @Nullable Long chatId,
@ActionInput(name = "animationURL") @Nullable String animationURL,
@ActionInput(name = "caption") @Nullable String caption) {
if (animationURL == null) {
logger.warn("Animation URL not defined; unable to retrieve video for sending.");
return false;
}
if (chatId == null) {
logger.warn("chatId not defined; action skipped.");
return false;
}
TelegramHandler localHandler = handler;
if (localHandler != null) {
final SendAnimation sendAnimation;
if (animationURL.toLowerCase().startsWith("http")) {
// load image from url
logger.debug("Animation URL provided.");
HttpClient client = localHandler.getClient();
if (client == null) {
return false;
}
Request request = client.newRequest(animationURL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS);
try {
// 50mb limit to file size
FutureResponseListener listener = new FutureResponseListener(request, 50 * 1024 * 1024);
request.send(listener);
ContentResponse contentResponse = listener.get();
if (contentResponse.getStatus() == 200) {
byte[] fileContent = contentResponse.getContent();
sendAnimation = new SendAnimation(chatId, fileContent);
} else {
logger.warn("Download from {} failed with status: {}", animationURL,
contentResponse.getStatus());
return false;
}
} catch (InterruptedException | ExecutionException e) {
logger.warn("Download from {} failed with exception: {}", animationURL, e.getMessage());
return false;
}
} else {
String temp = animationURL;
if (!animationURL.toLowerCase().startsWith("file:")) {
temp = "file://" + animationURL;
}
// Load video from local file system
logger.debug("Read file from local file system: {}", animationURL);
try {
sendAnimation = new SendAnimation(chatId, Path.of(new URL(temp).getPath()).toFile());
} catch (MalformedURLException e) {
logger.warn("Malformed URL, should start with http or file: {}", animationURL);
return false;
}
}
if (caption != null) {
sendAnimation.caption(caption);
}
if (localHandler.getParseMode() != null) {
sendAnimation.parseMode(localHandler.getParseMode());
}
return evaluateResponse(localHandler.execute(sendAnimation));
}
return false;
}
@RuleAction(label = "send video", description = "Send a Video using the Telegram API.")
public boolean sendTelegramVideo(@ActionInput(name = "videoURL") @Nullable String videoURL,
@ActionInput(name = "caption") @Nullable String caption) {
TelegramHandler localHandler = handler;
if (localHandler != null) {
for (Long chatId : localHandler.getReceiverChatIds()) {
if (!sendTelegramVideo(chatId, videoURL, caption)) {
return false;
}
}
}
return true;
}
@RuleAction(label = "send video", description = "Send a Video using the Telegram API.")
public boolean sendTelegramVideo(@ActionInput(name = "chatId") @Nullable Long chatId,
@ActionInput(name = "videoURL") @Nullable String videoURL,
@ActionInput(name = "caption") @Nullable String caption) {
final SendVideo sendVideo;
if (videoURL == null) {
logger.warn("Video URL not defined; unable to retrieve video for sending.");
return false;
}
if (chatId == null) {
logger.warn("chatId not defined; action skipped.");
return false;
}
TelegramHandler localHandler = handler;
if (localHandler != null) {
if (videoURL.toLowerCase().startsWith("http")) {
logger.debug("Video http://URL provided.");
HttpClient client = localHandler.getClient();
if (client == null) {
return false;
}
Request request = client.newRequest(videoURL).method(HttpMethod.GET).timeout(30, TimeUnit.SECONDS);
try {
// 50mb limit to file size
FutureResponseListener listener = new FutureResponseListener(request, 50 * 1024 * 1024);
request.send(listener);
ContentResponse contentResponse = listener.get();
if (contentResponse.getStatus() == 200) {
byte[] fileContent = contentResponse.getContent();
sendVideo = new SendVideo(chatId, fileContent);
} else {
logger.warn("Download from {} failed with status: {}", videoURL, contentResponse.getStatus());
return false;
}
} catch (InterruptedException | ExecutionException e) {
logger.warn("Download from {} failed with exception: {}", videoURL, e.getMessage());
return false;
}
} else {
String temp = videoURL;
if (!videoURL.toLowerCase().startsWith("file:")) {
temp = "file://" + videoURL;
}
// Load video from local file system with file://path
logger.debug("Read file from local file: {}", videoURL);
try {
sendVideo = new SendVideo(chatId, Path.of(new URL(temp).getPath()).toFile());
} catch (MalformedURLException e) {
logger.warn("Malformed URL, should start with http or file: {}", videoURL);
return false;
}
}
if (caption != null) {
sendVideo.caption(caption);
}
if (localHandler.getParseMode() != null) {
sendVideo.parseMode(localHandler.getParseMode());
}
return evaluateResponse(localHandler.execute(sendVideo));
}
return false;
}
// legacy delegate methods
/* APIs without chatId parameter */
public static boolean sendTelegram(ThingActions actions, @Nullable String format, @Nullable Object... args) {
@ -414,6 +578,15 @@ public class TelegramActions implements ThingActions {
return ((TelegramActions) actions).sendTelegramPhoto(photoURL, caption, username, password);
}
public static boolean sendTelegramAnimation(ThingActions actions, @Nullable String animationURL,
@Nullable String caption) {
return ((TelegramActions) actions).sendTelegramVideo(animationURL, caption);
}
public static boolean sendTelegramVideo(ThingActions actions, @Nullable String videoURL, @Nullable String caption) {
return ((TelegramActions) actions).sendTelegramVideo(videoURL, caption);
}
public static boolean sendTelegramAnswer(ThingActions actions, @Nullable String replyId, @Nullable String message) {
return ((TelegramActions) actions).sendTelegramAnswer(replyId, message);
}
@ -440,6 +613,16 @@ public class TelegramActions implements ThingActions {
return ((TelegramActions) actions).sendTelegramPhoto(chatId, photoURL, caption, username, password);
}
public static boolean sendTelegramAnimation(ThingActions actions, @Nullable Long chatId,
@Nullable String animationURL, @Nullable String caption) {
return ((TelegramActions) actions).sendTelegramVideo(chatId, animationURL, caption);
}
public static boolean sendTelegramVideo(ThingActions actions, @Nullable Long chatId, @Nullable String videoURL,
@Nullable String caption) {
return ((TelegramActions) actions).sendTelegramVideo(chatId, videoURL, caption);
}
public static boolean sendTelegramAnswer(ThingActions actions, @Nullable Long chatId, @Nullable String replyId,
@Nullable String message) {
return ((TelegramActions) actions).sendTelegramAnswer(chatId, replyId, message);