Merge commit '1a2e7c9773dbb7d251d30f1f8e3d6538dcd92600'

This commit is contained in:
Andrea Cavalli 2020-07-17 18:53:57 +02:00
commit 2adce4685e
47 changed files with 694 additions and 2209 deletions

7
.gitignore vendored
View File

@ -35,12 +35,11 @@ hs_err_pid*
.idea/ .idea/
copyright/ copyright/
*.iml *.iml
*.ipr
*.iws
.classpath .classpath
.project .project
.settings/ .settings/
#File System specific files #File System specific files
.DS_STORE .DS_Store
# Default ignored files
/Bots.iws

2078
Bots.ipr

File diff suppressed because it is too large Load Diff

View File

@ -27,16 +27,16 @@ Just import add the library to your project with one of these options:
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId> <artifactId>telegrambots</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
``` ```
```gradle ```gradle
compile "org.telegram:telegrambots:4.8" compile "org.telegram:telegrambots:4.9"
``` ```
2. Using Jitpack from [here](https://jitpack.io/#rubenlagus/TelegramBots/4.8) 2. Using Jitpack from [here](https://jitpack.io/#rubenlagus/TelegramBots/4.9)
3. Download the jar(including all dependencies) from [here](https://mvnrepository.com/artifact/org.telegram/telegrambots/4.8) 3. Download the jar(including all dependencies) from [here](https://mvnrepository.com/artifact/org.telegram/telegrambots/4.9)
In order to use Long Polling mode, just create your own bot extending `org.telegram.telegrambots.bots.TelegramLongPollingBot`. In order to use Long Polling mode, just create your own bot extending `org.telegram.telegrambots.bots.TelegramLongPollingBot`.

View File

@ -1,5 +1,13 @@
### <a id="4.8"></a>4.8 ### ### <a id="4.9"></a>4.9 ###
1. Update Api version [4.9](https://core.telegram.org/bots/api-changelog#june-4-2020)
2. Bug fixing: #731, #749, #752 and #753
### <a id="4.8.1"></a>4.8.1 ###
1. Update Api version [4.8](https://core.telegram.org/bots/api-changelog#april-24-2020) 1. Update Api version [4.8](https://core.telegram.org/bots/api-changelog#april-24-2020)
2. Add stats for Abilities
3. New and updated wiki page
4. Spring-boot support for version 2.2.2
5. Bug fixing: #745, #716, #629, #749, #730
### <a id="4.7"></a>4.7 ### ### <a id="4.7"></a>4.7 ###
1. Update Api version [4.7](https://core.telegram.org/bots/api-changelog#march-30-2020) 1. Update Api version [4.7](https://core.telegram.org/bots/api-changelog#march-30-2020)

View File

@ -11,13 +11,13 @@ First you need ot get the library and add it to your project. There are few poss
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId> <artifactId>telegrambots</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
``` ```
* With **Gradle**: * With **Gradle**:
```groovy ```groovy
compile group: 'org.telegram', name: 'telegrambots', version: '4.8' compile group: 'org.telegram', name: 'telegrambots', version: '4.9'
``` ```
2. Don't like **Maven Central Repository**? It can also be taken from [Jitpack](https://jitpack.io/#rubenlagus/TelegramBots). 2. Don't like **Maven Central Repository**? It can also be taken from [Jitpack](https://jitpack.io/#rubenlagus/TelegramBots).

View File

@ -4,6 +4,7 @@
* [[Using HTTP Proxy]] * [[Using HTTP Proxy]]
* [[FAQ]] * [[FAQ]]
* [[Handling Bot Tokens]] * [[Handling Bot Tokens]]
* [[Understanding the Library]]
* AbilityBot * AbilityBot
* [[Simple Example]] * [[Simple Example]]
* [[Hello Ability]] * [[Hello Ability]]

View File

@ -9,8 +9,10 @@ The barebone toggle is used to **turn off** all the default abilities that come
import org.telegram.abilitybots.api.toggle.BareboneToggle; import org.telegram.abilitybots.api.toggle.BareboneToggle;
public class YourAwesomeBot extends AbilityBot { public class YourAwesomeBot extends AbilityBot {
private static final BareboneToggle toggle = new BareboneToggle();
public YourAwesomeBot(String token, String username) { public YourAwesomeBot(String token, String username) {
BareboneToggle toggle = new BareboneToggle();
super(token, username, toggle); super(token, username, toggle);
} }
@ -25,11 +27,12 @@ The custom toggle allows you to customize or turn off the names of the abilities
import org.telegram.abilitybots.api.toggle.CustomToggle; import org.telegram.abilitybots.api.toggle.CustomToggle;
public class YourAwesomeBot extends AbilityBot { public class YourAwesomeBot extends AbilityBot {
public YourAwesomeBot(String token, String username) {
CustomToggle toggle = new CustomToggle() private static final CustomToggle toggle = new CustomToggle()
.turnOff("ban") .turnOff("ban")
.toggle("promote", "upgrade"); .toggle("promote", "upgrade");
public YourAwesomeBot(String token, String username) {
super(token, username, toggle); super(token, username, toggle);
} }

View File

@ -31,4 +31,7 @@ As an example, if you want to restrict the updates to photos only, then you may
public boolean checkGlobalFlags(Update update) { public boolean checkGlobalFlags(Update update) {
return Flag.PHOTO; return Flag.PHOTO;
} }
``` ```
## Statistics
AbilityBot can accrue basic statistics about the usage of your abilities and replies. Simply `enableStats()` on an Ability builder or `enableStats(<name>)` on replies to activate this feature. Once activated, you may call `/stats` and the bot will print a basic list of statistics. At the moment, AbilityBot only tracks hits. In the future, this will be enhanced to track more stats.

View File

@ -54,21 +54,20 @@ public Ability saysHelloWorld() {
The test for this ability would be: The test for this ability would be:
```java ```java
@Test @Test
public void canSayHelloWorld() { public void canSayHelloWorld() {
Update upd = new Update(); Update upd = new Update();
// Create a new EndUser - EndUser is a class similar to Telegram User, but contains // Create a new User - User is a class similar to Telegram User
// some utility methods like fullName() and shortName() for ease of use User user = new User(USER_ID, "Abbas", false, "Abou Daya", "addo37", null);
EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37");
// This is the context that you're used to, it is the necessary conumer item for the ability // This is the context that you're used to, it is the necessary conumer item for the ability
MessageContext context = MessageContext.newContext(upd, endUser, CHAT_ID); MessageContext context = MessageContext.newContext(upd, user, CHAT_ID);
// We consume a context in the lamda declaration, so we pass the context to the action logic // We consume a context in the lamda declaration, so we pass the context to the action logic
bot.saysHelloWorld().action().accept(context); bot.saysHelloWorld().action().accept(context);
// We verify that the sender was called only ONCE and sent Hello World to CHAT_ID // We verify that the silent sender was called only ONCE and sent Hello World to CHAT_ID
// The sender here is a mock! // The silent sender here is a mock!
Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID); Mockito.verify(silent, times(1)).send("Hello World!", CHAT_ID);
} }
``` ```
@ -85,10 +84,10 @@ import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.db.MapDBContext; import org.telegram.abilitybots.api.db.MapDBContext;
import org.telegram.abilitybots.api.objects.EndUser;
import org.telegram.abilitybots.api.objects.MessageContext; import org.telegram.abilitybots.api.objects.MessageContext;
import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.SilentSender;
import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.User;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ -98,36 +97,36 @@ public class ExampleBotTest {
// Your bot handle here // Your bot handle here
private ExampleBot bot; private ExampleBot bot;
// Your sender here // Your sender here. Also you can create MessageSender
private MessageSender sender; private SilentSender silent;
@Before @Before
public void setUp() { public void setUp() {
// Create your bot // Create your bot
bot = new ExampleBot(); bot = new ExampleBot();
// Create a new sender as a mock // Create a new sender as a mock
sender = mock(MessageSender.class); silent = mock(SilentSender.class);
// Set your bot sender to the mocked sender // Set your bot silent sender to the mocked sender
// THIS is the line that prevents your bot from communicating with Telegram servers when it's running its own abilities // THIS is the line that prevents your bot from communicating with Telegram servers when it's running its own abilities
// All method calls will go through the mocked interface -> which would do nothing except logging the fact that you've called this function with the specific arguments // All method calls will go through the mocked interface -> which would do nothing except logging the fact that you've called this function with the specific arguments
bot.sender = sender; // Create setter in your bot
bot.setSilentSender(silent);
} }
@Test @Test
public void canSayHelloWorld() { public void canSayHelloWorld() {
Update upd = new Update(); Update upd = new Update();
// Create a new EndUser - EndUser is a class similar to Telegram User, but contains // Create a new User - User is a class similar to Telegram User
// some utility methods like fullName() and shortName() for ease of use User user = new User(USER_ID, "Abbas", false, "Abou Daya", "addo37", null);
EndUser endUser = EndUser.endUser(USER_ID, "Abbas", "Abou Daya", "addo37");
// This is the context that you're used to, it is the necessary conumer item for the ability // This is the context that you're used to, it is the necessary conumer item for the ability
MessageContext context = MessageContext.newContext(upd, endUser, CHAT_ID); MessageContext context = MessageContext.newContext(upd, user, CHAT_ID);
// We consume a context in the lamda declaration, so we pass the context to the action logic // We consume a context in the lamda declaration, so we pass the context to the action logic
bot.saysHelloWorld().action().accept(context); bot.saysHelloWorld().action().accept(context);
// We verify that the sender was called only ONCE and sent Hello World to CHAT_ID // We verify that the silent sender was called only ONCE and sent Hello World to CHAT_ID
// The sender here is a mock! // The silent sender here is a mock!
Mockito.verify(sender, times(1)).send("Hello World!", CHAT_ID); Mockito.verify(silent, times(1)).send("Hello World!", CHAT_ID);
} }
} }
``` ```
@ -178,11 +177,13 @@ public class ExampleBotTest {
private DBContext db; private DBContext db;
private MessageSender sender; private MessageSender sender;
@Before @Before
public void setUp() { public void setUp() {
bot = new ExampleBot(db); bot = new ExampleBot(db);
sender = mock(MessageSender.class); sender = mock(MessageSender.class);
bot.silent = new SilentSender(sender); SilentSender silent = new SilentSender(sender);
// Create setter in your bot
bot.setSilentSender(silent);
... ...
} }

View File

@ -22,7 +22,7 @@ We'll be introducing an ability that maintains a special counter for every user.
// db.getMap takes in a string, this must be unique and the same everytime you want to call the exact same map // db.getMap takes in a string, this must be unique and the same everytime you want to call the exact same map
// TODO: Using integer as a key in this db map is not recommended, it won't be serialized/deserialized properly if you ever decide to recover/backup db // TODO: Using integer as a key in this db map is not recommended, it won't be serialized/deserialized properly if you ever decide to recover/backup db
Map<String, Integer> countMap = db.getMap("COUNTERS"); Map<String, Integer> countMap = db.getMap("COUNTERS");
int userId = ctx.user().id(); int userId = ctx.user().getId();
// Get and increment counter, put it back in the map // Get and increment counter, put it back in the map
Integer counter = countMap.compute(String.valueOf(userId), (id, count) -> count == null ? 1 : ++count); Integer counter = countMap.compute(String.valueOf(userId), (id, count) -> count == null ? 1 : ++count);
@ -41,7 +41,7 @@ We'll be introducing an ability that maintains a special counter for every user.
*/ */
// Send formatted will enable markdown // Send formatted will enable markdown
String message = String.format("%s, your count is now *%d*!", ctx.user().shortName(), counter); String message = String.format("%s, your count is now *%d*!", ctx.user().getUserName(), counter);
silent.send(message, ctx.chatId()); silent.send(message, ctx.chatId());
}) })
.build(); .build();

View File

@ -9,12 +9,12 @@ As with any Java project, you will need to set your dependencies.
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots-abilities</artifactId> <artifactId>telegrambots-abilities</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
``` ```
* **Gradle** * **Gradle**
```groovy ```groovy
compile group: 'org.telegram', name: 'telegrambots-abilities', version: 'permissions' implementation group: 'org.telegram', name: 'telegrambots-abilities', version: '4.9'
``` ```
* [JitPack](https://jitpack.io/#rubenlagus/TelegramBots) * [JitPack](https://jitpack.io/#rubenlagus/TelegramBots)

View File

@ -70,30 +70,29 @@ Now, after your naughty bot retaliates, the user can say "go left or else" to fo
## Complete Example ## Complete Example
```java ```java
public static class ReplyFlowBot extends AbilityBot { public class ReplyFlowBot extends AbilityBot {
public class ReplyFlowBot extends AbilityBot {
public ReplyFlowBot(String botToken, String botUsername) { public ReplyFlowBot(String botToken, String botUsername) {
super(botToken, botUsername); super(botToken, botUsername);
} }
@Override @Override
public int creatorId() { public int creatorId() {
return <YOUR ID HERE>; return <YOUR ID HERE>;
} }
public ReplyFlow directionFlow() { public ReplyFlow directionFlow() {
Reply saidLeft = Reply.of(upd -> silent.send("Sir, I have gone left.", getChatId(upd)), Reply saidLeft = Reply.of(upd -> silent.send("Sir, I have gone left.", getChatId(upd)),
hasMessageWith("go left or else")); hasMessageWith("go left or else"));
ReplyFlow leftflow = ReplyFlow.builder(db) ReplyFlow leftflow = ReplyFlow.builder(db)
.action(upd -> silent.send("I don't know how to go left.", getChatId(upd))) .action(upd -> silent.send("I don't know how to go left.", getChatId(upd)))
.onlyIf(hasMessageWith("left")) .onlyIf(hasMessageWith("left"))
.next(saidLeft).build(); .next(saidLeft).build();
Reply saidRight = Reply.of(upd -> silent.send("Sir, I have gone right.", getChatId(upd)), Reply saidRight = Reply.of(upd -> silent.send("Sir, I have gone right.", getChatId(upd)),
hasMessageWith("right")); hasMessageWith("right"));
return ReplyFlow.builder(db) return ReplyFlow.builder(db)
.action(upd -> silent.send("Command me to go left or right!", getChatId(upd))) .action(upd -> silent.send("Command me to go left or right!", getChatId(upd)))
.onlyIf(hasMessageWith("wake up")) .onlyIf(hasMessageWith("wake up"))
.next(leftflow) .next(leftflow)
@ -103,9 +102,9 @@ public static class ReplyFlowBot extends AbilityBot {
@NotNull @NotNull
private Predicate<Update> hasMessageWith(String msg) { private Predicate<Update> hasMessageWith(String msg) {
return upd -> upd.getMessage().getText().equalsIgnoreCase(msg); return upd -> upd.getMessage().getText().equalsIgnoreCase(msg);
} }
} }
``` ```
## Inline Declaration ## Inline Declaration
As you can see in the above example, we used a bottom-up approach. We declared the leaf replies before we got to the root reply flow. As you can see in the above example, we used a bottom-up approach. We declared the leaf replies before we got to the root reply flow.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,167 @@
# Understanding the Library
Last updated: 2020-03-04
This little handy guide will not teach you how to do a particular thing in the library.
It will also not teach you how to create bots or anything in the liking of that.
For that take a look at the [Getting Started] guide
This guide will give you a general understanding on how the library function and will answer a lot of frequently
asked questions. I recommend everyone who wants to work with this library more to read this guide.
## Topics
* The library and the bot API
* Understanding how the API is mapped in the library
* The general infrastructure of the library
## The Library and the Bot API
Often in Conversations about the library they talk about the API or Application Programming Interface. Sometimes these
terms are used interchangeably, which is not correct.
To understand the differences between those two things lets take a look at this diagram
![](Telegram-Diagram.png)
As you might have noticed. Our bot never actually talks to the user directly. Actually, Every communication between user
and bot happens on the Telegram Servers (A little disclaimer. I actually don't know the entire infrastructure of telegram.
So take everything between Bot API and Telegram Client with a grain of salt)
Important is that the Library communicates with the Telegram Bot API for everything. Sending Messages, Sending Pictures,
and receiving Updates from Telegram
So we can conclude that the Library is the part of your code that handles all the communication between your bot and the
Bot API. The Bot API is the actual interface telegram offers to implement bots. Everything we can do in the library,
we can also do directly calling the library.
Take this piece of code:
```java
// We initialize our bot in a separate method. See this as the initialization code from the getting started guide
AbsSender ourBot = getOurBot();
// We create the Method class. This one doesn't need any parameters to be able to be send
GetMe getMe = new GetMe();
// At last, we just need to execute the method and get the result
User bot = ourBot.execute(getMe);
```
If we do this for one of our bots this is what theBot will look like:<br>
![](Bot_intellij.png)
(you better be grateful for that picture. Spend an eternity trying to find a username)
We can also go ahead and just call the bot api directly:<br>
![](Bot_curl.png)
We get the same result. The library just handily maps it to an object for us to work with.
So how do we find out how to do things with the library?
## Understanding how the API is mapped in the library
The base for all our operations is the [telegram bot api documentations](https://core.telegram.org/bots/api) . This 220 kB monster of a website will tell us almost everything we need to know about how to use the api.
It is split into multiple sections. We can ignore the sections *Authorizing your bot", "Making Requests" and "Getting Updates". These things are done for us by the library.
So, let's see how we can send a poll for example. First, lets take a look at the *Available Methods*. It already gives us a bunch of send methods
* sendMessage
* sendPhoto
* sendAudio
* sendDocument
* sendVideo
* sendAnimation
* sendVoice
* sendVideoNote
* sendMediaGroup
* sendLocation
* sendVenue
* sendContact
* sendPoll
* sendChatAction
One of them sticks out to us. *sendPoll* looks promising, and when we take a look at the description
`Use this method to send a native poll. On success, the sent Message is returned`
That looks like the thing we need. So let's see what we need to send it
![](poll_params.png)
So as we can see, we need to set 3 things. The id of the chat we want to send it to, a question as a string and an Array of Strings.
This will result in an anonymous regular type poll without mutliselection looking like this when sent:
![](poll_example.png)
We can customize a bunch of other things like create quizzes, send closed polls or reply to a certain message. Now, how is this api object mapped in the library? How *do* we create a poll?
Pretty straight forward. We take our libary, look for the "Go To Class" or "SearchClass" option and type in the name of the Method we are looking for. This will quickly yield a "SendPoll" method.
**Every** Method, listed under *Available Methods* has such a Class. And if we go ahead and scroll down a bit we find a set of fields for the class
```java
@JsonProperty(CHATID_FIELD)
private String chatId; ///< Unique identifier for the target chat or username of the target channel (in the format @channelusername)
@JsonProperty(QUESTION_FIELD)
private String question; ///< Poll question, 1-255 characters
@JsonProperty(OPTIONS_FIELD)
private List<String> options = new ArrayList<>(); ///< List of answer options, 2-10 strings 1-100 characters each
@JsonProperty(ISANONYMOUS_FIELD)
private Boolean isAnonymous; ///< Optional True, if the poll needs to be anonymous, defaults to True
@JsonProperty(TYPE_FIELD)
private String type; ///< Optional Poll type, quiz or regular, defaults to regular
@JsonProperty(ALLOWMULTIPLEANSWERS_FIELD)
private Boolean allowMultipleAnswers; ///< Optional True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False
@JsonProperty(CORRECTOPTIONID_FIELD)
private Integer correctOptionId; ///< Optional 0-based identifier of the correct answer option, required for polls in quiz mode
@JsonProperty(ISCLOSED_FIELD)
private Boolean isClosed; ///< Optional Pass True, if the poll needs to be immediately closed
@JsonProperty(DISABLENOTIFICATION_FIELD)
private Boolean disableNotification; ///< Optional. Sends the message silently. Users will receive a notification with no sound.
@JsonProperty(REPLYTOMESSAGEID_FIELD)
private Integer replyToMessageId; ///< Optional. If the message is a reply, ID of the original message
@JsonProperty(REPLYMARKUP_FIELD)
@JsonDeserialize()
private ReplyKeyboard replyMarkup; ///< Optional. JSON-serialized object for a custom reply keyboard
```
This is a lot at first glance, but if we remember back at the table from the documentation, everything is there. We have the 3 required fields on top (chatId, question and the list of options and everything) else below
Let's reproduce our poll from earlier. We create the Method object and set all required fields. This particular class already allows us to set everything in a constructor, this is normally the case for required fields. All other fields can be set by using the setter method (**Attention** Calling a setter overrides the last value in a field. You can call setter multiple times on a method object, but it's normally not correct to do so)
```java
AbsSender ourBot = getOurBot();
List<String> options = new List<>();
options.add("Yes");
options.add("No");
// Let's just assume we get the chatMessage as a parameter. For example from the message received, or from a database
SendPoll ourPoll = new SendPoll(someChatId, "Some Question", options);
ourBot.execute(ourPoll);
```
This will send the message as expected to telegram and finally to the given chat. But what about return values?
If we go back to the documentation and look at the description it says:
`Use this method to send a native poll. On success, the sent Message is returned`
Message links us to the Message object. An object so big that i wont be showing it's documentation in this guide. It can be found here: [Telegram Bot Api: Message Object](https://core.telegram.org/bots/api#message)
Just the documentation tells us that it returns a Message object. How do we get that object? Easy. Execute() returns it.
```java
Message thePollMessage = ourBot.execute(ourPoll);
```
This again works for any method that returns a value. SendMessage, SendPhoto, SendPoll, GetMe, GetChatMember etc.
## But what about Photos?
Some methods aren't quite as straight forward to use. Let's take a bit of time to look at methods that upload files to telegram. Namely: SendPhoto, SendAnimation, SendSticker, SendDocument, SendVideo, SendVideoNote, SendAudio and SendVoice. All of these require what telegram calls an "InputFile". This can either be a FileId of a previous send file, a url to the file on a public sever or the actual file. In the Library this is mapped using different set methods in the above classes. Currently sending by FileId, sending by java.io.File and sending by InputStream is supported. Sending by URL is not supported as of writing this guide
### FileId
Using a fileId is pretty straight forward. The FileId is returned from telegram upon sending a File. Just set the given FileId and execute the method. Make sure that the FileId actually point to a file of the correct type
### File
The classic approach of creating a java.io.File by Path on your hard drive or by loading a classpath resource.
### InputStream
Sending a file by InputStream will cause the library to read the stream and then converting it into the Request. It's the same as sending a java.io.File, but more convenient some times.
For more information on how to send Files, take a look at the [FAQ: How to send Photos](../FAQ.md). The basic concept will be the same across all Methods.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -7,7 +7,7 @@
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>Bots</artifactId> <artifactId>Bots</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
<version>4.8</version> <version>4.9</version>
<modules> <modules>
<module>telegrambots</module> <module>telegrambots</module>

View File

@ -18,19 +18,19 @@ Usage
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots-abilities</artifactId> <artifactId>telegrambots-abilities</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
``` ```
**Gradle** **Gradle**
```gradle ```gradle
compile "org.telegram:telegrambots-abilities:4.8" compile "org.telegram:telegrambots-abilities:4.9"
``` ```
**JitPack** - [JitPack](https://jitpack.io/#rubenlagus/TelegramBots/v4.8) **JitPack** - [JitPack](https://jitpack.io/#rubenlagus/TelegramBots/v4.9)
**Plain imports** - [Here](https://github.com/rubenlagus/TelegramBots/releases/tag/v4.8) **Plain imports** - [Here](https://github.com/rubenlagus/TelegramBots/releases/tag/v4.9)
Motivation Motivation
---------- ----------

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>Bots</artifactId> <artifactId>Bots</artifactId>
<version>4.8</version> <version>4.9</version>
</parent> </parent>
<artifactId>telegrambots-abilities</artifactId> <artifactId>telegrambots-abilities</artifactId>
@ -84,7 +84,7 @@
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId> <artifactId>telegrambots</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>

View File

@ -1,11 +1,8 @@
package org.telegram.abilitybots.api.bot; package org.telegram.abilitybots.api.bot;
import com.google.common.collect.ImmutableList; import com.google.common.collect.*;
import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -36,15 +33,18 @@ import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static com.google.common.collect.Sets.difference;
import static java.lang.String.format; import static java.lang.String.format;
import static java.time.ZonedDateTime.now; import static java.time.ZonedDateTime.now;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static java.util.regex.Pattern.CASE_INSENSITIVE; import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.compile; import static java.util.regex.Pattern.compile;
import static java.util.stream.Collectors.toSet;
import static org.telegram.abilitybots.api.objects.Locality.*; import static org.telegram.abilitybots.api.objects.Locality.*;
import static org.telegram.abilitybots.api.objects.MessageContext.newContext; import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
import static org.telegram.abilitybots.api.objects.Privacy.*; import static org.telegram.abilitybots.api.objects.Privacy.*;
import static org.telegram.abilitybots.api.objects.Stats.createStats;
import static org.telegram.abilitybots.api.util.AbilityMessageCodes.*; import static org.telegram.abilitybots.api.util.AbilityMessageCodes.*;
import static org.telegram.abilitybots.api.util.AbilityUtils.*; import static org.telegram.abilitybots.api.util.AbilityUtils.*;
@ -90,6 +90,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
public static final String USERS = "USERS"; public static final String USERS = "USERS";
public static final String USER_ID = "USER_ID"; public static final String USER_ID = "USER_ID";
public static final String BLACKLIST = "BLACKLIST"; public static final String BLACKLIST = "BLACKLIST";
public static final String STATS = "ABILITYBOT_STATS";
// DB and sender // DB and sender
protected final DBContext db; protected final DBContext db;
@ -105,6 +106,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
// Ability registry // Ability registry
private Map<String, Ability> abilities; private Map<String, Ability> abilities;
private Map<String, Stats> stats;
// Reply registry // Reply registry
private List<Reply> replies; private List<Reply> replies;
@ -122,6 +124,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
silent = new SilentSender(sender); silent = new SilentSender(sender);
registerAbilities(); registerAbilities();
initStats();
} }
/** /**
@ -152,6 +155,13 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
return db.getSet(ADMINS); return db.getSet(ADMINS);
} }
/**
* @return a mapping of ability and reply names to their corresponding statistics
*/
protected Map<String, Stats> stats() {
return stats;
}
/** /**
* @return the immutable map of <String,Ability> * @return the immutable map of <String,Ability>
*/ */
@ -166,6 +176,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
return replies; return replies;
} }
/** /**
* This method contains the stream of actions that are applied on any update. * This method contains the stream of actions that are applied on any update.
* <p> * <p>
@ -183,6 +194,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
.filter(this::checkBlacklist) .filter(this::checkBlacklist)
.map(this::addUser) .map(this::addUser)
.filter(this::filterReply) .filter(this::filterReply)
.filter(this::hasUser)
.map(this::getAbility) .map(this::getAbility)
.filter(this::validateAbility) .filter(this::validateAbility)
.filter(this::checkPrivacy) .filter(this::checkPrivacy)
@ -191,8 +203,12 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
.filter(this::checkMessageFlags) .filter(this::checkMessageFlags)
.map(this::getContext) .map(this::getContext)
.map(this::consumeUpdate) .map(this::consumeUpdate)
.map(this::updateStats)
.forEach(this::postConsumption); .forEach(this::postConsumption);
// Commit to DB now after all the actions have been dealt
db.commit();
long processingTime = System.currentTimeMillis() - millisStarted; long processingTime = System.currentTimeMillis() - millisStarted;
log.info(format("[%s] Processing of update [%s] ended at %s%n---> Processing time: [%d ms] <---%n", botUsername, update.getUpdateId(), now(), processingTime)); log.info(format("[%s] Processing of update [%s] ended at %s%n---> Processing time: [%d ms] <---%n", botUsername, update.getUpdateId(), now(), processingTime));
} }
@ -275,6 +291,19 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
} }
} }
private void initStats() {
Set<String> enabledStats = Stream.concat(
replies.stream().filter(Reply::statsEnabled).map(Reply::name),
abilities.entrySet().stream()
.filter(entry -> entry.getValue().statsEnabled())
.map(Map.Entry::getKey)).collect(toSet());
stats = db.getMap(STATS);
Set<String> toBeRemoved = difference(stats.keySet(), enabledStats);
toBeRemoved.forEach(stats::remove);
enabledStats.forEach(abName -> stats.computeIfAbsent(abName,
name -> createStats(abName, 0)));
}
/** /**
* @param clazz the type to be tested * @param clazz the type to be tested
* @return a predicate testing the return type of the method corresponding to the class parameter * @return a predicate testing the return type of the method corresponding to the class parameter
@ -344,6 +373,26 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
return pair; return pair;
} }
Pair<MessageContext, Ability> updateStats(Pair<MessageContext, Ability> pair) {
Ability ab = pair.b();
if (ab.statsEnabled()) {
updateStats(pair.b().name());
}
return pair;
}
private void updateReplyStats(Reply reply) {
if (reply.statsEnabled()) {
updateStats(reply.name());
}
}
void updateStats(String name) {
Stats statsObj = stats.get(name);
statsObj.hit();
stats.put(name, statsObj);
}
Pair<MessageContext, Ability> getContext(Trio<Update, Ability, String[]> trio) { Pair<MessageContext, Ability> getContext(Trio<Update, Ability, String[]> trio) {
Update update = trio.a(); Update update = trio.a();
User user = AbilityUtils.getUser(update); User user = AbilityUtils.getUser(update);
@ -465,6 +514,10 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
Update addUser(Update update) { Update addUser(Update update) {
User endUser = AbilityUtils.getUser(update); User endUser = AbilityUtils.getUser(update);
if (endUser.equals(EMPTY_USER)) {
// Can't add an empty user, return the update as is
return update;
}
users().compute(endUser.getId(), (id, user) -> { users().compute(endUser.getId(), (id, user) -> {
if (user == null) { if (user == null) {
@ -480,10 +533,15 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
return user; return user;
}); });
db.commit();
return update; return update;
} }
private boolean hasUser(Update update) {
// Valid updates without users should return an empty user
// Updates that are not recognized by the getUser method will throw an exception
return !AbilityUtils.getUser(update).equals(EMPTY_USER);
}
private void updateUserId(User oldUser, User newUser) { private void updateUserId(User oldUser, User newUser) {
if (oldUser != null && oldUser.getUserName() != null) { if (oldUser != null && oldUser.getUserName() != null) {
// Remove old username -> ID // Remove old username -> ID
@ -501,6 +559,7 @@ public abstract class BaseAbilityBot extends DefaultAbsSender implements Ability
.filter(reply -> reply.isOkFor(update)) .filter(reply -> reply.isOkFor(update))
.map(reply -> { .map(reply -> {
reply.actOn(update); reply.actOn(update);
updateReplyStats(reply);
return false; return false;
}) })
.reduce(true, Boolean::logicalAnd); .reduce(true, Boolean::logicalAnd);

View File

@ -26,6 +26,7 @@ import java.io.PrintStream;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.StringJoiner;
import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.MultimapBuilder.hashKeys; import static com.google.common.collect.MultimapBuilder.hashKeys;
@ -60,7 +61,6 @@ import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_UNBA
import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_UNBAN_SUCCESS; import static org.telegram.abilitybots.api.util.AbilityMessageCodes.ABILITY_UNBAN_SUCCESS;
import static org.telegram.abilitybots.api.util.AbilityMessageCodes.USER_NOT_FOUND; import static org.telegram.abilitybots.api.util.AbilityMessageCodes.USER_NOT_FOUND;
import static org.telegram.abilitybots.api.util.AbilityUtils.addTag; import static org.telegram.abilitybots.api.util.AbilityUtils.addTag;
import static org.telegram.abilitybots.api.util.AbilityUtils.commitTo;
import static org.telegram.abilitybots.api.util.AbilityUtils.escape; import static org.telegram.abilitybots.api.util.AbilityUtils.escape;
import static org.telegram.abilitybots.api.util.AbilityUtils.getLocalizedMessage; import static org.telegram.abilitybots.api.util.AbilityUtils.getLocalizedMessage;
import static org.telegram.abilitybots.api.util.AbilityUtils.shortName; import static org.telegram.abilitybots.api.util.AbilityUtils.shortName;
@ -77,6 +77,7 @@ public final class DefaultAbilities implements AbilityExtension {
public static final String RECOVER = "recover"; public static final String RECOVER = "recover";
public static final String COMMANDS = "commands"; public static final String COMMANDS = "commands";
public static final String REPORT = "report"; public static final String REPORT = "report";
public static final String STATS = "stats";
private static final Logger log = LoggerFactory.getLogger(DefaultAbilities.class); private static final Logger log = LoggerFactory.getLogger(DefaultAbilities.class);
private final BaseAbilityBot bot; private final BaseAbilityBot bot;
@ -180,6 +181,26 @@ public final class DefaultAbilities implements AbilityExtension {
.build(); .build();
} }
/**
* @return the ability to report statistics for abilities and replies.
*/
public Ability reportStats() {
return builder()
.name(STATS)
.locality(ALL)
.privacy(ADMIN)
.input(0)
.action(ctx -> {
String stats = bot.stats().entrySet().stream()
.map(entry -> String.format("%s: %d", entry.getKey(), entry.getValue().hits()))
.reduce(new StringJoiner("\n"), StringJoiner::add, StringJoiner::merge)
.toString();
bot.silent.send(stats, ctx.chatId());
})
.build();
}
/** /**
* This backup ability returns the object defined by {@link DBContext#backup()} as a message document. * This backup ability returns the object defined by {@link DBContext#backup()} as a message document.
* <p> * <p>
@ -213,6 +234,7 @@ public final class DefaultAbilities implements AbilityExtension {
.build(); .build();
} }
/** /**
* Recovers the bot database using {@link DBContext#recover(Object)}. * Recovers the bot database using {@link DBContext#recover(Object)}.
* <p> * <p>
@ -288,7 +310,6 @@ public final class DefaultAbilities implements AbilityExtension {
sendMd(ABILITY_BAN_SUCCESS, ctx, escape(bannedUser)); sendMd(ABILITY_BAN_SUCCESS, ctx, escape(bannedUser));
} }
}) })
.post(commitTo(bot.db))
.build(); .build();
} }
@ -315,7 +336,6 @@ public final class DefaultAbilities implements AbilityExtension {
bot.silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().getLanguageCode(), escape(username)), ctx.chatId()); bot.silent.sendMd(getLocalizedMessage(ABILITY_UNBAN_SUCCESS, ctx.user().getLanguageCode(), escape(username)), ctx.chatId());
} }
}) })
.post(commitTo(bot.db))
.build(); .build();
} }
@ -339,7 +359,7 @@ public final class DefaultAbilities implements AbilityExtension {
admins.add(userId); admins.add(userId);
sendMd(ABILITY_PROMOTE_SUCCESS, ctx, escape(username)); sendMd(ABILITY_PROMOTE_SUCCESS, ctx, escape(username));
} }
}).post(commitTo(bot.db)) })
.build(); .build();
} }
@ -363,7 +383,6 @@ public final class DefaultAbilities implements AbilityExtension {
sendMd(ABILITY_DEMOTE_FAIL, ctx, escape(username)); sendMd(ABILITY_DEMOTE_FAIL, ctx, escape(username));
} }
}) })
.post(commitTo(bot.db))
.build(); .build();
} }
@ -389,7 +408,6 @@ public final class DefaultAbilities implements AbilityExtension {
send(ABILITY_CLAIM_SUCCESS, ctx); send(ABILITY_CLAIM_SUCCESS, ctx);
} }
}) })
.post(commitTo(bot.db))
.build(); .build();
} }

View File

@ -42,13 +42,14 @@ public final class Ability {
private final Locality locality; private final Locality locality;
private final Privacy privacy; private final Privacy privacy;
private final int argNum; private final int argNum;
private final boolean statsEnabled;
private final Consumer<MessageContext> action; private final Consumer<MessageContext> action;
private final Consumer<MessageContext> postAction; private final Consumer<MessageContext> postAction;
private final List<Reply> replies; private final List<Reply> replies;
private final List<Predicate<Update>> flags; private final List<Predicate<Update>> flags;
@SafeVarargs @SafeVarargs
private Ability(String name, String info, Locality locality, Privacy privacy, int argNum, Consumer<MessageContext> action, Consumer<MessageContext> postAction, List<Reply> replies, Predicate<Update>... flags) { private Ability(String name, String info, Locality locality, Privacy privacy, int argNum, boolean statsEnabled, Consumer<MessageContext> action, Consumer<MessageContext> postAction, List<Reply> replies, Predicate<Update>... flags) {
checkArgument(!isEmpty(name), "Method name cannot be empty"); checkArgument(!isEmpty(name), "Method name cannot be empty");
checkArgument(!containsWhitespace(name), "Method name cannot contain spaces"); checkArgument(!containsWhitespace(name), "Method name cannot contain spaces");
checkArgument(isAlphanumeric(name), "Method name can only be alpha-numeric", name); checkArgument(isAlphanumeric(name), "Method name can only be alpha-numeric", name);
@ -70,6 +71,7 @@ public final class Ability {
this.postAction = postAction; this.postAction = postAction;
this.replies = replies; this.replies = replies;
this.statsEnabled = statsEnabled;
} }
public static AbilityBuilder builder() { public static AbilityBuilder builder() {
@ -96,6 +98,10 @@ public final class Ability {
return argNum; return argNum;
} }
public boolean statsEnabled() {
return statsEnabled;
}
public Consumer<MessageContext> action() { public Consumer<MessageContext> action() {
return action; return action;
} }
@ -147,12 +153,14 @@ public final class Ability {
private Privacy privacy; private Privacy privacy;
private Locality locality; private Locality locality;
private int argNum; private int argNum;
private boolean statsEnabled;
private Consumer<MessageContext> action; private Consumer<MessageContext> action;
private Consumer<MessageContext> postAction; private Consumer<MessageContext> postAction;
private List<Reply> replies; private List<Reply> replies;
private Predicate<Update>[] flags; private Predicate<Update>[] flags;
private AbilityBuilder() { private AbilityBuilder() {
statsEnabled = false;
replies = newArrayList(); replies = newArrayList();
} }
@ -186,6 +194,11 @@ public final class Ability {
return this; return this;
} }
public AbilityBuilder enableStats() {
statsEnabled = true;
return this;
}
public AbilityBuilder privacy(Privacy privacy) { public AbilityBuilder privacy(Privacy privacy) {
this.privacy = privacy; this.privacy = privacy;
return this; return this;
@ -202,6 +215,11 @@ public final class Ability {
return this; return this;
} }
public final AbilityBuilder reply(Reply reply) {
replies.add(reply);
return this;
}
public AbilityBuilder basedOn(Ability ability) { public AbilityBuilder basedOn(Ability ability) {
replies.clear(); replies.clear();
replies.addAll(ability.replies()); replies.addAll(ability.replies());
@ -216,7 +234,7 @@ public final class Ability {
} }
public Ability build() { public Ability build() {
return new Ability(name, info, locality, privacy, argNum, action, postAction, replies, flags); return new Ability(name, info, locality, privacy, argNum, statsEnabled, action, postAction, replies, flags);
} }
} }
} }

View File

@ -25,6 +25,10 @@ public enum Flag implements Predicate<Update> {
EDITED_MESSAGE(Update::hasEditedMessage), EDITED_MESSAGE(Update::hasEditedMessage),
INLINE_QUERY(Update::hasInlineQuery), INLINE_QUERY(Update::hasInlineQuery),
CHOSEN_INLINE_QUERY(Update::hasChosenInlineQuery), CHOSEN_INLINE_QUERY(Update::hasChosenInlineQuery),
SHIPPING_QUERY(Update::hasShippingQuery),
PRECHECKOUT_QUERY(Update::hasPreCheckoutQuery),
POLL(Update::hasPoll),
POLL_ANSWER(Update::hasPollAnswer),
// Message Flags // Message Flags
REPLY(upd -> MESSAGE.test(upd) && upd.getMessage().isReply()), REPLY(upd -> MESSAGE.test(upd) && upd.getMessage().isReply()),

View File

@ -26,12 +26,15 @@ import static java.util.Arrays.asList;
public class Reply { public class Reply {
public final List<Predicate<Update>> conditions; public final List<Predicate<Update>> conditions;
public final Consumer<Update> action; public final Consumer<Update> action;
private boolean statsEnabled;
private String name;
Reply(List<Predicate<Update>> conditions, Consumer<Update> action) { Reply(List<Predicate<Update>> conditions, Consumer<Update> action) {
this.conditions = ImmutableList.<Predicate<Update>>builder() this.conditions = ImmutableList.<Predicate<Update>>builder()
.addAll(conditions) .addAll(conditions)
.build(); .build();
this.action = action; this.action = action;
statsEnabled = false;
} }
public static Reply of(Consumer<Update> action, List<Predicate<Update>> conditions) { public static Reply of(Consumer<Update> action, List<Predicate<Update>> conditions) {
@ -65,6 +68,20 @@ public class Reply {
return Stream.of(this); return Stream.of(this);
} }
public Reply enableStats(String name) {
this.name = name;
statsEnabled = true;
return this;
}
public boolean statsEnabled() {
return statsEnabled;
}
public String name() {
return name;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) if (this == o)

View File

@ -0,0 +1,64 @@
package org.telegram.abilitybots.api.objects;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;
import org.json.JSONPropertyIgnore;
import java.io.Serializable;
import java.util.Objects;
/**
* Basic POJO to track ability and reply hits. The current implementation is NOT thread safe.
*
* @author Abbas Abou Daya
*/
public final class Stats implements Serializable {
@JsonProperty
private final String name;
@JsonProperty
private long hits;
private Stats(String name) {
this.name = name;
}
@JsonCreator
public static Stats createStats(@JsonProperty("name") String name, @JsonProperty("hits") long hits) {
return new Stats(name);
}
public String name() {
return name;
}
public long hits() {
return hits;
}
public void hit() {
hits++;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Stats that = (Stats) o;
return hits == that.hits &&
Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, hits);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("hits", hits)
.toString();
}
}

View File

@ -24,6 +24,8 @@ import static org.telegram.abilitybots.api.objects.Flag.*;
* Helper and utility methods * Helper and utility methods
*/ */
public final class AbilityUtils { public final class AbilityUtils {
public static User EMPTY_USER = new User(0, "", false, "", "", "");
private AbilityUtils() { private AbilityUtils() {
} }
@ -69,6 +71,14 @@ public final class AbilityUtils {
return update.getEditedMessage().getFrom(); return update.getEditedMessage().getFrom();
} else if (CHOSEN_INLINE_QUERY.test(update)) { } else if (CHOSEN_INLINE_QUERY.test(update)) {
return update.getChosenInlineQuery().getFrom(); return update.getChosenInlineQuery().getFrom();
} else if (SHIPPING_QUERY.test(update)) {
return update.getShippingQuery().getFrom();
} else if (PRECHECKOUT_QUERY.test(update)) {
return update.getPreCheckoutQuery().getFrom();
} else if (POLL_ANSWER.test(update)) {
return update.getPollAnswer().getUser();
} else if (POLL.test(update)) {
return EMPTY_USER;
} else { } else {
throw new IllegalStateException("Could not retrieve originating user from update"); throw new IllegalStateException("Could not retrieve originating user from update");
} }
@ -140,6 +150,14 @@ public final class AbilityUtils {
return update.getEditedMessage().getChatId(); return update.getEditedMessage().getChatId();
} else if (CHOSEN_INLINE_QUERY.test(update)) { } else if (CHOSEN_INLINE_QUERY.test(update)) {
return (long) update.getChosenInlineQuery().getFrom().getId(); return (long) update.getChosenInlineQuery().getFrom().getId();
} else if (SHIPPING_QUERY.test(update)) {
return (long) update.getShippingQuery().getFrom().getId();
} else if (PRECHECKOUT_QUERY.test(update)) {
return (long) update.getPreCheckoutQuery().getFrom().getId();
} else if (POLL_ANSWER.test(update)) {
return (long) update.getPollAnswer().getUser().getId();
} else if (POLL.test(update)) {
return (long) EMPTY_USER.getId();
} else { } else {
throw new IllegalStateException("Could not retrieve originating chat ID from update"); throw new IllegalStateException("Could not retrieve originating chat ID from update");
} }
@ -160,10 +178,8 @@ public final class AbilityUtils {
return update.getEditedChannelPost().isUserMessage(); return update.getEditedChannelPost().isUserMessage();
} else if (EDITED_MESSAGE.test(update)) { } else if (EDITED_MESSAGE.test(update)) {
return update.getEditedMessage().isUserMessage(); return update.getEditedMessage().isUserMessage();
} else if (CHOSEN_INLINE_QUERY.test(update) || INLINE_QUERY.test(update)) {
return true;
} else { } else {
throw new IllegalStateException("Could not retrieve update context origin (user/group)"); return true;
} }
} }

View File

@ -8,10 +8,12 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers; import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.*; import org.telegram.abilitybots.api.objects.*;
import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.MessageSender;
import org.telegram.abilitybots.api.sender.SilentSender; import org.telegram.abilitybots.api.sender.SilentSender;
import org.telegram.abilitybots.api.util.AbilityUtils;
import org.telegram.abilitybots.api.util.Pair; import org.telegram.abilitybots.api.util.Pair;
import org.telegram.abilitybots.api.util.Trio; import org.telegram.abilitybots.api.util.Trio;
import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators;
@ -25,6 +27,7 @@ import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet; import static com.google.common.collect.Sets.newHashSet;
@ -38,8 +41,8 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.mockito.internal.verification.VerificationModeFactory.times; import static org.mockito.internal.verification.VerificationModeFactory.times;
import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder; import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder;
import static org.telegram.abilitybots.api.bot.TestUtils.*;
import static org.telegram.abilitybots.api.bot.TestUtils.CREATOR; import static org.telegram.abilitybots.api.bot.TestUtils.CREATOR;
import static org.telegram.abilitybots.api.bot.TestUtils.*;
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
import static org.telegram.abilitybots.api.objects.Flag.DOCUMENT; import static org.telegram.abilitybots.api.objects.Flag.DOCUMENT;
import static org.telegram.abilitybots.api.objects.Flag.MESSAGE; import static org.telegram.abilitybots.api.objects.Flag.MESSAGE;
@ -120,6 +123,49 @@ public class AbilityBotTest {
verify(silent, times(1)).send("reply", USER.getId()); verify(silent, times(1)).send("reply", USER.getId());
} }
@Test
void canProcessUpdatesWithoutUserInfo() {
Update update = mock(Update.class);
// At the moment, only poll updates carry no user information
when(update.hasPoll()).thenReturn(true);
bot.onUpdateReceived(update);
}
@Test
void getUserHasAllMethodsDefined() {
Arrays.stream(Update.class.getMethods())
// filter to all these methods of hasXXX (hasPoll, hasMessage, etc...)
.filter(method -> method.getName().startsWith("has"))
// Gotta filter out hashCode
.filter(method -> method.getReturnType().getName().equals("boolean"))
.forEach(method -> {
Update update = mock(Update.class);
try {
// Mock the method and make sure it returns true so that it gets processed by the following method
when(method.invoke(update)).thenReturn(true);
// Call the getUser function, throws an IllegalStateException if there's an update that can't be processed
AbilityUtils.getUser(update);
} catch (IllegalStateException e) {
throw new RuntimeException(
format("Found an update variation that is not handled by the getUser util method [%s]", method.getName()), e);
} catch (NullPointerException | ReflectiveOperationException e) {
// This is fine, the mock isn't complete and we're only
// looking for IllegalStateExceptions thrown by the method
}
});
}
@Test
void getChatIdCanHandleAllKindsOfUpdates() {
handlesAllUpdates(AbilityUtils::getUser);
}
@Test
void getUserCanHandleAllKindsOfUpdates() {
handlesAllUpdates(AbilityUtils::getChatId);
}
@Test @Test
void canBackupDB() throws TelegramApiException { void canBackupDB() throws TelegramApiException {
MessageContext context = defaultContext(); MessageContext context = defaultContext();
@ -130,6 +176,30 @@ public class AbilityBotTest {
verify(sender, times(1)).sendDocument(any()); verify(sender, times(1)).sendDocument(any());
} }
@Test
void canReportStatistics() {
MessageContext context = defaultContext();
defaultAbs.reportStats().action().accept(context);
verify(silent, times(1)).send("count: 0\nmustreply: 0", GROUP_ID);
}
@Test
void canReportUpdatedStatistics() {
Update upd1 = mockFullUpdate(bot, CREATOR, "/count 1 2 3 4");
bot.onUpdateReceived(upd1);
Update upd2 = mockFullUpdate(bot, CREATOR, "must reply");
bot.onUpdateReceived(upd2);
Mockito.reset(silent);
Update statUpd = mockFullUpdate(bot, CREATOR, "/stats");
bot.onUpdateReceived(statUpd);
verify(silent, times(1)).send("count: 1\nmustreply: 1", CREATOR.getId());
}
@Test @Test
void canRecoverDB() throws TelegramApiException, IOException { void canRecoverDB() throws TelegramApiException, IOException {
Update update = mockBackupUpdate(); Update update = mockBackupUpdate();
@ -553,7 +623,7 @@ public class AbilityBotTest {
defaultAbs.commands().action().accept(creatorCtx); defaultAbs.commands().action().accept(creatorCtx);
String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test\nADMIN\n/admin\n/ban\n/demote\n/promote\n/unban\nCREATOR\n/backup\n/claim\n/recover\n/report"; String expected = "PUBLIC\n/commands\n/count\n/default - dis iz default command\n/group\n/test\nADMIN\n/admin\n/ban\n/demote\n/promote\n/stats\n/unban\nCREATOR\n/backup\n/claim\n/recover\n/report";
verify(silent, times(1)).send(expected, GROUP_ID); verify(silent, times(1)).send(expected, GROUP_ID);
} }
@ -574,6 +644,29 @@ public class AbilityBotTest {
verify(silent, times(1)).send(expected, GROUP_ID); verify(silent, times(1)).send(expected, GROUP_ID);
} }
private void handlesAllUpdates(Consumer<Update> utilMethod) {
Arrays.stream(Update.class.getMethods())
// filter to all these methods of hasXXX (hasPoll, hasMessage, etc...)
.filter(method -> method.getName().startsWith("has"))
// Gotta filter out hashCode
.filter(method -> method.getReturnType().getName().equals("boolean"))
.forEach(method -> {
Update update = mock(Update.class);
try {
// Mock the method and make sure it returns true so that it gets processed by the following method
when(method.invoke(update)).thenReturn(true);
// Call the function, throws an IllegalStateException if there's an update that can't be processed
utilMethod.accept(update);
} catch (IllegalStateException e) {
throw new RuntimeException(
format("Found an update variation that is not handled by the getChatId util method [%s]", method.getName()), e);
} catch (NullPointerException | ReflectiveOperationException e) {
// This is fine, the mock isn't complete and we're only
// looking for IllegalStateExceptions thrown by the method
}
});
}
private void mockUser(Update update, Message message, User user) { private void mockUser(Update update, Message message, User user) {
when(update.hasMessage()).thenReturn(true); when(update.hasMessage()).thenReturn(true);
when(update.getMessage()).thenReturn(message); when(update.getMessage()).thenReturn(message);

View File

@ -3,6 +3,7 @@ package org.telegram.abilitybots.api.bot;
import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.Ability; import org.telegram.abilitybots.api.objects.Ability;
import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder; import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder;
import org.telegram.abilitybots.api.objects.Reply;
import org.telegram.abilitybots.api.toggle.AbilityToggle; import org.telegram.abilitybots.api.toggle.AbilityToggle;
import static org.telegram.abilitybots.api.objects.Ability.builder; import static org.telegram.abilitybots.api.objects.Ability.builder;
@ -41,7 +42,7 @@ public class DefaultBot extends AbilityBot {
return getDefaultBuilder() return getDefaultBuilder()
.name(DEFAULT) .name(DEFAULT)
.info("dis iz default command") .info("dis iz default command")
.reply(upd -> silent.send("reply", upd.getMessage().getChatId()), MESSAGE, update -> update.getMessage().getText().equals("must reply")) .reply(Reply.of(upd -> silent.send("reply", upd.getMessage().getChatId()), MESSAGE, update -> update.getMessage().getText().equals("must reply")).enableStats("mustreply"))
.reply(upd -> silent.send("reply", upd.getCallbackQuery().getMessage().getChatId()), CALLBACK_QUERY) .reply(upd -> silent.send("reply", upd.getCallbackQuery().getMessage().getChatId()), CALLBACK_QUERY)
.build(); .build();
} }
@ -67,6 +68,7 @@ public class DefaultBot extends AbilityBot {
.privacy(PUBLIC) .privacy(PUBLIC)
.locality(USER) .locality(USER)
.input(4) .input(4)
.enableStats()
.build(); .build();
} }

View File

@ -5,11 +5,13 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.telegram.abilitybots.api.db.DBContext; import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.objects.Flag;
import org.telegram.abilitybots.api.objects.Reply; import org.telegram.abilitybots.api.objects.Reply;
import org.telegram.abilitybots.api.objects.ReplyFlow; import org.telegram.abilitybots.api.objects.ReplyFlow;
import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.MessageSender;
import org.telegram.abilitybots.api.sender.SilentSender; import org.telegram.abilitybots.api.sender.SilentSender;
import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.polls.Poll;
import java.io.IOException; import java.io.IOException;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -106,6 +108,20 @@ public class ReplyFlowTest {
assertFalse(db.<Long, Integer>getMap(STATES).containsKey(chatId), "User still has state after terminal reply"); assertFalse(db.<Long, Integer>getMap(STATES).containsKey(chatId), "User still has state after terminal reply");
} }
@Test
void repliesHandlePollResponse() {
Update update = mock(Update.class);
when(update.hasPoll()).thenReturn(true);
when(update.hasMessage()).thenReturn(false);
Poll poll = mock(Poll.class);
when(poll.getId()).thenReturn("1");
when(update.getPoll()).thenReturn(poll);
// This should not be processed as a reply, so we wouldn't filter out (true)
assertTrue(bot.filterReply(update));
}
public static class ReplyFlowBot extends AbilityBot { public static class ReplyFlowBot extends AbilityBot {
private ReplyFlowBot(String botToken, String botUsername, DBContext db) { private ReplyFlowBot(String botToken, String botUsername, DBContext db) {
@ -139,7 +155,7 @@ public class ReplyFlowTest {
@NotNull @NotNull
private Predicate<Update> hasMessageWith(String msg) { private Predicate<Update> hasMessageWith(String msg) {
return upd -> upd.getMessage().getText().equalsIgnoreCase(msg); return upd -> Flag.MESSAGE.test(upd) && upd.getMessage().getText().equalsIgnoreCase(msg);
} }
} }
} }

View File

@ -15,7 +15,7 @@ Usage
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots-chat-session-bot</artifactId> <artifactId>telegrambots-chat-session-bot</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
``` ```

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>Bots</artifactId> <artifactId>Bots</artifactId>
<version>4.8</version> <version>4.9</version>
</parent> </parent>
<artifactId>telegrambots-chat-session-bot</artifactId> <artifactId>telegrambots-chat-session-bot</artifactId>
@ -84,7 +84,7 @@
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId> <artifactId>telegrambots</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core --> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->

View File

@ -16,12 +16,12 @@ Just import add the library to your project with one of these options:
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambotsextensions</artifactId> <artifactId>telegrambotsextensions</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
``` ```
2. Using Gradle: 2. Using Gradle:
```gradle ```gradle
compile "org.telegram:telegrambotsextensions:4.8" compile "org.telegram:telegrambotsextensions:4.9"
``` ```

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>Bots</artifactId> <artifactId>Bots</artifactId>
<version>4.8</version> <version>4.9</version>
</parent> </parent>
<artifactId>telegrambotsextensions</artifactId> <artifactId>telegrambotsextensions</artifactId>
@ -75,7 +75,7 @@
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId> <artifactId>telegrambots</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -22,7 +22,6 @@ import java.util.function.BiConsumer;
*/ */
public abstract class TelegramLongPollingCommandBot extends TelegramLongPollingBot implements ICommandRegistry { public abstract class TelegramLongPollingCommandBot extends TelegramLongPollingBot implements ICommandRegistry {
private final CommandRegistry commandRegistry; private final CommandRegistry commandRegistry;
private String botUsername;
/** /**
* Creates a TelegramLongPollingCommandBot using default options * Creates a TelegramLongPollingCommandBot using default options
@ -33,19 +32,6 @@ public abstract class TelegramLongPollingCommandBot extends TelegramLongPollingB
this(ApiContext.getInstance(DefaultBotOptions.class)); this(ApiContext.getInstance(DefaultBotOptions.class));
} }
/**
* Creates a TelegramLongPollingCommandBot using default options
* Use ICommandRegistry's methods on this bot to register commands
*
* @param botUsername Username of the bot
* @deprecated Overwrite {@link #getBotUsername() getBotUsername} instead
*/
@Deprecated
public TelegramLongPollingCommandBot(String botUsername){
this();
this.botUsername = botUsername;
}
/** /**
* Creates a TelegramLongPollingCommandBot with custom options and allowing commands with * Creates a TelegramLongPollingCommandBot with custom options and allowing commands with
* usernames * usernames
@ -152,9 +138,7 @@ public abstract class TelegramLongPollingCommandBot extends TelegramLongPollingB
* @return Bot username * @return Bot username
*/ */
@Override @Override
public String getBotUsername(){ public abstract String getBotUsername();
return this.botUsername;
};
/** /**
* Process all updates, that are not commands. * Process all updates, that are not commands.

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>Bots</artifactId> <artifactId>Bots</artifactId>
<version>4.8</version> <version>4.9</version>
</parent> </parent>
<artifactId>telegrambots-meta</artifactId> <artifactId>telegrambots-meta</artifactId>

View File

@ -10,14 +10,18 @@ import org.telegram.telegrambots.meta.exceptions.TelegramApiRequestException;
import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException; import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* @author Ruben Bermudez * @author Ruben Bermudez
* @version 4.7 * @version 4.7
* Use this method to send a dice, which will have a random value from 1 to 6. On success, the sent Message is returned. * Use this method to send an animated emoji that will display a random value. On success, the sent Message is returned.
* (Yes, we're aware of the proper singular of die. But it's awkward, and we decided to help it change. One dice at a time!)
*/ */
public class SendDice extends BotApiMethod<Message> { public class SendDice extends BotApiMethod<Message> {
private static final List<String> VALIDEMOJIS = Collections.unmodifiableList(Arrays.asList("\uD83C\uDFB2", "\uD83C\uDFAF", "\uD83C\uDFC0"));
public static final String PATH = "sendDice"; public static final String PATH = "sendDice";
private static final String CHATID_FIELD = "chat_id"; private static final String CHATID_FIELD = "chat_id";
@ -28,8 +32,12 @@ public class SendDice extends BotApiMethod<Message> {
@JsonProperty(CHATID_FIELD) @JsonProperty(CHATID_FIELD)
private String chatId; ///< Unique identifier for the target chat or username of the target channel (in the format @channelusername) private String chatId; ///< Unique identifier for the target chat or username of the target channel (in the format @channelusername)
/**
* Emoji on which the dice throw animation is based. Currently, must be one of 🎲, 🎯, or 🏀.
* Dice can have values 1-6 for 🎲 and 🎯, and values 1-5 for 🏀. Defauts to 🎲
*/
@JsonProperty(EMOJI_FIELD) @JsonProperty(EMOJI_FIELD)
private String emoji; ///< Optional. Emoji on which the dice throw animation is based. Currently, must be one of 🎲 or 🎯. Defauts to 🎲 private String emoji;
@JsonProperty(DISABLENOTIFICATION_FIELD) @JsonProperty(DISABLENOTIFICATION_FIELD)
private Boolean disableNotification; ///< Optional. Sends the message silently. Users will receive a notification with no sound. private Boolean disableNotification; ///< Optional. Sends the message silently. Users will receive a notification with no sound.
@JsonProperty(REPLYTOMESSAGEID_FIELD) @JsonProperty(REPLYTOMESSAGEID_FIELD)
@ -121,8 +129,8 @@ public class SendDice extends BotApiMethod<Message> {
if (chatId == null) { if (chatId == null) {
throw new TelegramApiValidationException("ChatId parameter can't be empty", this); throw new TelegramApiValidationException("ChatId parameter can't be empty", this);
} }
if (emoji != null && !emoji.equals("\uD83C\uDFB2") && !emoji.equals("\uD83C\uDFAF")) { if (emoji != null && !VALIDEMOJIS.contains(emoji)) {
throw new TelegramApiValidationException("Only \uD83C\uDFB2 and \uD83C\uDFAF are allowed in Emoji field ", this); throw new TelegramApiValidationException("Only \uD83C\uDFB2, \uD83C\uDFAF or \uD83C\uDFC0 are allowed in Emoji field ", this);
} }
if (replyMarkup != null) { if (replyMarkup != null) {
replyMarkup.validate(); replyMarkup.validate();

View File

@ -6,15 +6,14 @@ import org.telegram.telegrambots.meta.api.interfaces.BotApiObject;
/** /**
* @author Ruben Bermudez * @author Ruben Bermudez
* @version 4.7 * @version 4.7
* This object represents a dice with random value from 1 to 6. * This object represents an animated emoji that displays a random value.
* (Yes, we're aware of the proper singular of die. But it's awkward, and we decided to help it change. One dice at a time!)
*/ */
public class Dice implements BotApiObject { public class Dice implements BotApiObject {
private static final String VALUE_FIELD = "value"; private static final String VALUE_FIELD = "value";
private static final String EMOJI_FIELD = "emoji"; private static final String EMOJI_FIELD = "emoji";
@JsonProperty(VALUE_FIELD) @JsonProperty(VALUE_FIELD)
private Integer value; ///< Value of the dice, 1-6 private Integer value; ///< Value of the dice, 1-6 for 🎲 and 🎯 base emoji, 1-5 for 🏀 base emoji
@JsonProperty(EMOJI_FIELD) @JsonProperty(EMOJI_FIELD)
private String emoji; ///< Emoji on which the dice throw animation is based private String emoji; ///< Emoji on which the dice throw animation is based

View File

@ -68,6 +68,7 @@ public class Message implements BotApiObject {
private static final String POLL_FIELD = "poll"; private static final String POLL_FIELD = "poll";
private static final String REPLY_MARKUP_FIELD = "reply_markup"; private static final String REPLY_MARKUP_FIELD = "reply_markup";
private static final String DICE_FIELD = "dice"; private static final String DICE_FIELD = "dice";
private static final String VIABOT_FIELD = "via_bot";
@JsonProperty(MESSAGEID_FIELD) @JsonProperty(MESSAGEID_FIELD)
private Integer messageId; ///< Integer Unique message identifier private Integer messageId; ///< Integer Unique message identifier
@ -210,7 +211,8 @@ public class Message implements BotApiObject {
private InlineKeyboardMarkup replyMarkup; private InlineKeyboardMarkup replyMarkup;
@JsonProperty(DICE_FIELD) @JsonProperty(DICE_FIELD)
private Dice dice; // Optional. Message is a dice with random value from 1 to 6 private Dice dice; // Optional. Message is a dice with random value from 1 to 6
@JsonProperty(VIABOT_FIELD)
private User viaBot; // Optional. Bot through which the message was sent
public Message() { public Message() {
super(); super();
} }
@ -521,6 +523,14 @@ public class Message implements BotApiObject {
return dice != null; return dice != null;
} }
public User getViaBot() {
return viaBot;
}
public boolean hasViaBot() {
return viaBot != null;
}
public boolean hasReplyMarkup() { public boolean hasReplyMarkup() {
return replyMarkup != null; return replyMarkup != null;
} }
@ -579,6 +589,8 @@ public class Message implements BotApiObject {
", forwardSenderName='" + forwardSenderName + '\'' + ", forwardSenderName='" + forwardSenderName + '\'' +
", poll=" + poll + ", poll=" + poll +
", replyMarkup=" + replyMarkup + ", replyMarkup=" + replyMarkup +
", dice=" + dice +
", viaBot=" + viaBot +
'}'; '}';
} }
} }

View File

@ -7,6 +7,10 @@ import org.telegram.telegrambots.meta.api.objects.inlinequery.inputmessageconten
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException; import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* @author Ruben Bermudez * @author Ruben Bermudez
* @version 1.0 * @version 1.0
@ -16,12 +20,15 @@ import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException;
*/ */
@JsonDeserialize @JsonDeserialize
public class InlineQueryResultGif implements InlineQueryResult { public class InlineQueryResultGif implements InlineQueryResult {
private static final List<String> VALIDTHUMBTYPES = Collections.unmodifiableList(Arrays.asList("image/jpeg", "image/gif", "video/mp4"));
private static final String TYPE_FIELD = "type"; private static final String TYPE_FIELD = "type";
private static final String ID_FIELD = "id"; private static final String ID_FIELD = "id";
private static final String GIFURL_FIELD = "gif_url"; private static final String GIFURL_FIELD = "gif_url";
private static final String GIFWIDTH_FIELD = "gif_width"; private static final String GIFWIDTH_FIELD = "gif_width";
private static final String GIFHEIGHT_FIELD = "gif_height"; private static final String GIFHEIGHT_FIELD = "gif_height";
private static final String THUMBURL_FIELD = "thumb_url"; private static final String THUMBURL_FIELD = "thumb_url";
private static final String THUMBMIMETYPE_FIELD = "thumb_mime_type";
private static final String TITLE_FIELD = "title"; private static final String TITLE_FIELD = "title";
private static final String CAPTION_FIELD = "caption"; private static final String CAPTION_FIELD = "caption";
private static final String INPUTMESSAGECONTENT_FIELD = "input_message_content"; private static final String INPUTMESSAGECONTENT_FIELD = "input_message_content";
@ -40,7 +47,9 @@ public class InlineQueryResultGif implements InlineQueryResult {
@JsonProperty(GIFHEIGHT_FIELD) @JsonProperty(GIFHEIGHT_FIELD)
private Integer gifHeight; ///< Optional. Height of the GIF private Integer gifHeight; ///< Optional. Height of the GIF
@JsonProperty(THUMBURL_FIELD) @JsonProperty(THUMBURL_FIELD)
private String thumbUrl; ///< Optional. URL of a static thumbnail for the result (jpeg or gif) private String thumbUrl; ///< Optional. URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result
@JsonProperty(THUMBMIMETYPE_FIELD)
private String thumbUrlType; ///< Optional. MIME type of the thumbnail, must be one of image/jpeg, image/gif, or video/mp4
@JsonProperty(TITLE_FIELD) @JsonProperty(TITLE_FIELD)
private String title; ///< Optional. Title for the result private String title; ///< Optional. Title for the result
@JsonProperty(CAPTION_FIELD) @JsonProperty(CAPTION_FIELD)
@ -107,6 +116,15 @@ public class InlineQueryResultGif implements InlineQueryResult {
return this; return this;
} }
public String getThumbUrlType() {
return thumbUrlType;
}
public InlineQueryResultGif setThumbUrlType(String thumbUrlType) {
this.thumbUrlType = thumbUrlType;
return this;
}
public String getTitle() { public String getTitle() {
return title; return title;
} }
@ -169,6 +187,9 @@ public class InlineQueryResultGif implements InlineQueryResult {
if (gifUrl == null || gifUrl.isEmpty()) { if (gifUrl == null || gifUrl.isEmpty()) {
throw new TelegramApiValidationException("GifUrl parameter can't be empty", this); throw new TelegramApiValidationException("GifUrl parameter can't be empty", this);
} }
if (thumbUrlType != null && !VALIDTHUMBTYPES.contains(thumbUrlType)) {
throw new TelegramApiValidationException("ThumbUrlType parameter must be one of “image/jpeg”, “image/gif”, or “video/mp4”", this);
}
if (inputMessageContent != null) { if (inputMessageContent != null) {
inputMessageContent.validate(); inputMessageContent.validate();
} }
@ -186,6 +207,7 @@ public class InlineQueryResultGif implements InlineQueryResult {
", gifWidth=" + gifWidth + ", gifWidth=" + gifWidth +
", gifHeight=" + gifHeight + ", gifHeight=" + gifHeight +
", thumbUrl='" + thumbUrl + '\'' + ", thumbUrl='" + thumbUrl + '\'' +
", thumbUrlType='" + thumbUrlType + '\'' +
", title='" + title + '\'' + ", title='" + title + '\'' +
", caption='" + caption + '\'' + ", caption='" + caption + '\'' +
", inputMessageContent=" + inputMessageContent + ", inputMessageContent=" + inputMessageContent +

View File

@ -8,6 +8,10 @@ import org.telegram.telegrambots.meta.api.objects.inlinequery.result.InlineQuery
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException; import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* @author Ruben Bermudez * @author Ruben Bermudez
* @version 1.0 * @version 1.0
@ -17,11 +21,15 @@ import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException;
*/ */
@JsonDeserialize @JsonDeserialize
public class InlineQueryResultCachedGif implements InlineQueryResult { public class InlineQueryResultCachedGif implements InlineQueryResult {
private static final List<String> VALIDTHUMBTYPES = Collections.unmodifiableList(Arrays.asList("image/jpeg", "image/gif", "video/mp4"));
private static final String TYPE_FIELD = "type"; private static final String TYPE_FIELD = "type";
private static final String ID_FIELD = "id"; private static final String ID_FIELD = "id";
private static final String GIF_FILE_ID_FIELD = "gif_file_id"; private static final String GIF_FILE_ID_FIELD = "gif_file_id";
private static final String TITLE_FIELD = "title"; private static final String TITLE_FIELD = "title";
private static final String CAPTION_FIELD = "caption"; private static final String CAPTION_FIELD = "caption";
private static final String THUMBURL_FIELD = "thumb_url";
private static final String THUMBMIMETYPE_FIELD = "thumb_mime_type";
private static final String INPUTMESSAGECONTENT_FIELD = "input_message_content"; private static final String INPUTMESSAGECONTENT_FIELD = "input_message_content";
private static final String REPLY_MARKUP_FIELD = "reply_markup"; private static final String REPLY_MARKUP_FIELD = "reply_markup";
private static final String PARSEMODE_FIELD = "parse_mode"; private static final String PARSEMODE_FIELD = "parse_mode";
@ -42,6 +50,10 @@ public class InlineQueryResultCachedGif implements InlineQueryResult {
private InlineKeyboardMarkup replyMarkup; ///< Optional. Inline keyboard attached to the message private InlineKeyboardMarkup replyMarkup; ///< Optional. Inline keyboard attached to the message
@JsonProperty(PARSEMODE_FIELD) @JsonProperty(PARSEMODE_FIELD)
private String parseMode; ///< Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption. private String parseMode; ///< Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption.
@JsonProperty(THUMBURL_FIELD)
private String thumbUrl; ///< Optional. URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result
@JsonProperty(THUMBMIMETYPE_FIELD)
private String thumbUrlType;
public InlineQueryResultCachedGif() { public InlineQueryResultCachedGif() {
super(); super();
@ -114,6 +126,24 @@ public class InlineQueryResultCachedGif implements InlineQueryResult {
return this; return this;
} }
public String getThumbUrl() {
return thumbUrl;
}
public InlineQueryResultCachedGif setThumbUrl(String thumbUrl) {
this.thumbUrl = thumbUrl;
return this;
}
public String getThumbUrlType() {
return thumbUrlType;
}
public InlineQueryResultCachedGif setThumbUrlType(String thumbUrlType) {
this.thumbUrlType = thumbUrlType;
return this;
}
@Override @Override
public void validate() throws TelegramApiValidationException { public void validate() throws TelegramApiValidationException {
if (id == null || id.isEmpty()) { if (id == null || id.isEmpty()) {
@ -122,6 +152,9 @@ public class InlineQueryResultCachedGif implements InlineQueryResult {
if (gifFileId == null || gifFileId.isEmpty()) { if (gifFileId == null || gifFileId.isEmpty()) {
throw new TelegramApiValidationException("GifFileId parameter can't be empty", this); throw new TelegramApiValidationException("GifFileId parameter can't be empty", this);
} }
if (thumbUrlType != null && !VALIDTHUMBTYPES.contains(thumbUrlType)) {
throw new TelegramApiValidationException("ThumbUrlType parameter must be one of “image/jpeg”, “image/gif”, or “video/mp4”", this);
}
if (inputMessageContent != null) { if (inputMessageContent != null) {
inputMessageContent.validate(); inputMessageContent.validate();
} }
@ -141,6 +174,8 @@ public class InlineQueryResultCachedGif implements InlineQueryResult {
", inputMessageContent=" + inputMessageContent + ", inputMessageContent=" + inputMessageContent +
", replyMarkup=" + replyMarkup + ", replyMarkup=" + replyMarkup +
", parseMode='" + parseMode + '\'' + ", parseMode='" + parseMode + '\'' +
", thumbUrl='" + thumbUrl + '\'' +
", thumbUrlType='" + thumbUrlType + '\'' +
'}'; '}';
} }
} }

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>Bots</artifactId> <artifactId>Bots</artifactId>
<version>4.8</version> <version>4.9</version>
</parent> </parent>
<artifactId>telegrambots-spring-boot-starter</artifactId> <artifactId>telegrambots-spring-boot-starter</artifactId>
@ -70,7 +70,8 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.1.8.RELEASE</spring-boot.version>
<spring-boot.version>2.2.2.RELEASE</spring-boot.version>
</properties> </properties>
<dependencies> <dependencies>
@ -78,7 +79,7 @@
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId> <artifactId>telegrambots</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -4,7 +4,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.meta.TelegramBotsApi;
import org.telegram.telegrambots.meta.api.objects.ApiResponse;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.meta.exceptions.TelegramApiRequestException;
import org.telegram.telegrambots.meta.generics.BotSession; import org.telegram.telegrambots.meta.generics.BotSession;
import org.telegram.telegrambots.meta.generics.LongPollingBot; import org.telegram.telegrambots.meta.generics.LongPollingBot;
import org.telegram.telegrambots.meta.generics.WebhookBot; import org.telegram.telegrambots.meta.generics.WebhookBot;
@ -55,6 +57,9 @@ public class TelegramBotInitializer implements InitializingBean {
private void handleAnnotatedMethod(Object bot, Method method, BotSession session) { private void handleAnnotatedMethod(Object bot, Method method, BotSession session) {
try { try {
TelegramApiRequestException test = new TelegramApiRequestException("Error getting updates", new ApiResponse());
log.error(test.getMessage(), test);
if (method.getParameterCount() > 1) { if (method.getParameterCount() > 1) {
log.warn(format("Method %s of Type %s has too many parameters", log.warn(format("Method %s of Type %s has too many parameters",
method.getName(), method.getDeclaringClass().getCanonicalName())); method.getName(), method.getDeclaringClass().getCanonicalName()));

View File

@ -2,8 +2,9 @@ package org.telegram.telegrambots.starter;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -11,12 +12,11 @@ import org.springframework.context.annotation.Configuration;
import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.meta.TelegramBotsApi;
import org.telegram.telegrambots.meta.generics.LongPollingBot; import org.telegram.telegrambots.meta.generics.LongPollingBot;
import org.telegram.telegrambots.meta.generics.WebhookBot; import org.telegram.telegrambots.meta.generics.WebhookBot;
/** /**
* #TelegramBotsApi added to spring context as well * #TelegramBotsApi added to spring context as well
*/ */
@Configuration @Configuration
@ConditionalOnProperty(prefix="telegrambots",name = "enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnProperty(prefix = "telegrambots", name = "enabled", havingValue = "true", matchIfMissing = true)
public class TelegramBotStarterConfiguration { public class TelegramBotStarterConfiguration {
@Bean @Bean
@ -26,12 +26,12 @@ public class TelegramBotStarterConfiguration {
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public TelegramBotInitializer telegramBotInitializer(TelegramBotsApi telegramBotsApi, public TelegramBotInitializer telegramBotInitializer(TelegramBotsApi telegramBotsApi,
Optional<List<LongPollingBot>> longPollingBots, ObjectProvider<List<LongPollingBot>> longPollingBots,
Optional<List<WebhookBot>> webHookBots) { ObjectProvider<List<WebhookBot>> webHookBots) {
return new TelegramBotInitializer(telegramBotsApi, return new TelegramBotInitializer(telegramBotsApi,
longPollingBots.orElseGet(Collections::emptyList), longPollingBots.getIfAvailable(Collections::emptyList),
webHookBots.orElseGet(Collections::emptyList)); webHookBots.getIfAvailable(Collections::emptyList));
} }
} }

View File

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>Bots</artifactId> <artifactId>Bots</artifactId>
<version>4.8</version> <version>4.9</version>
</parent> </parent>
<artifactId>telegrambots</artifactId> <artifactId>telegrambots</artifactId>
@ -95,7 +95,7 @@
<dependency> <dependency>
<groupId>org.telegram</groupId> <groupId>org.telegram</groupId>
<artifactId>telegrambots-meta</artifactId> <artifactId>telegrambots-meta</artifactId>
<version>4.8</version> <version>4.9</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>

View File

@ -26,6 +26,7 @@ import org.telegram.telegrambots.meta.generics.UpdatesReader;
import java.io.IOException; import java.io.IOException;
import java.io.InvalidObjectException; import java.io.InvalidObjectException;
import java.lang.reflect.InvocationTargetException;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -274,7 +275,15 @@ public class DefaultBotSession implements BotSession {
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.info(e.getLocalizedMessage(), e); log.info(e.getLocalizedMessage(), e);
interrupt(); interrupt();
} catch (InternalError e) {
// handle InternalError to workaround OpenJDK bug (resolved since 13.0)
// https://bugs.openjdk.java.net/browse/JDK-8173620
if (e.getCause() instanceof InvocationTargetException) {
Throwable cause = e.getCause().getCause();
log.error(cause.getLocalizedMessage(), cause);
} else throw e;
} }
return Collections.emptyList(); return Collections.emptyList();
} }
} }