Added AbilityBot module
This commit is contained in:
parent
0e6e9e7e99
commit
6df6627821
1
pom.xml
1
pom.xml
@ -13,6 +13,7 @@
|
||||
<module>telegrambots</module>
|
||||
<module>telegrambots-meta</module>
|
||||
<module>telegrambots-extensions</module>
|
||||
<module>telegrambots-abilities</module>
|
||||
</modules>
|
||||
|
||||
<licenses>
|
||||
|
170
telegrambots-abilities/README.md
Normal file
170
telegrambots-abilities/README.md
Normal file
@ -0,0 +1,170 @@
|
||||
<div align="center">
|
||||
<img src="https://github.com/addo37/AbilityBots/blob/gh-pages/images/API%20BOT-03.png?raw=true" alt="abilitybots" width="200" height="200"/>
|
||||
|
||||
[![Build Status](https://travis-ci.org/rubenlagus/TelegramBots.svg?branch=master)](https://travis-ci.org/rubenlagus/TelegramBots)
|
||||
[![Jitpack](https://jitpack.io/v/rubenlagus/TelegramBots.svg)](https://jitpack.io/#rubenlagus/TelegramBots)
|
||||
[![JavaDoc](http://svgur.com/i/1Ex.svg)](https://addo37.github.io/AbilityBots/)
|
||||
[![Telegram](http://trellobot.doomdns.org/telegrambadge.svg)](https://telegram.me/JavaBotsApi)
|
||||
[![ghit.me](https://ghit.me/badge.svg?repo=rubenlagus/TelegramBots)](https://ghit.me/repo/rubenlagus/TelegramBots)
|
||||
|
||||
</div>
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
**Maven**
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.telegram</groupId>
|
||||
<artifactId>telegrambots-abilities</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Gradle**
|
||||
|
||||
```gradle
|
||||
compile "org.telegram:telegrambots-abilities:3.1.1"
|
||||
```
|
||||
|
||||
**JitPack** - [JitPack](https://jitpack.io/#rubenlagus/TelegramBots/v3.1.1)
|
||||
|
||||
**Plain imports** - [Jar](https://github.com/addo37/AbilityBots/releases/download/v1.2.5/AbilityBots-1.2.5.jar) | [fatJar](https://github.com/addo37/AbilityBots/releases/download/v1.2.5/AbilityBots-with-dependencies-1.2.5.jar)
|
||||
|
||||
Motivation
|
||||
----------
|
||||
Ever since I've started programming bots for Telegram, I've been using the Telegram Bot Java API. It's a basic and nicely done API that is a 1-to-1 translation of the HTTP API exposed by Telegram.
|
||||
|
||||
Dealing with a basic API has its advantages and disadvantages. Obviously, there's nothing hidden. If it's there on Telegram, it's here in the Java API.
|
||||
When you want to implement a feature in your bot, you start asking these questions:
|
||||
|
||||
* The **WHO**?
|
||||
* Who is going to use this feature? Should they be allowed to use all the features?
|
||||
* The **WHAT**?
|
||||
* Under what conditions should I allow this feature?
|
||||
* Should the message have a photo? A document? Oh, maybe a callback query?
|
||||
* The **HOW**?
|
||||
* If my bot crashes, how can I resume my operation?
|
||||
* Should I utilize a DB?
|
||||
* How can I separate logic execution of different features?
|
||||
* How can I unit-test my feature outside of Telegram?
|
||||
|
||||
Every time you write a command or a feature, you will need to answer these questions and ensure that your feature logic works.
|
||||
|
||||
Ability Bot Abstraction
|
||||
-----------------------
|
||||
After implementing my fifth bot using that API, I had had it with the amount of **boilerplate code** that was needed for every added feature. Methods were getting overly-complex and readability became subpar.
|
||||
That is where the notion of another layer of abstraction (AbilityBot) began taking shape.
|
||||
|
||||
The AbilityBot abstraction defines a new object, named **Ability**. An ability combines conditions, flags, action, post-action and replies.
|
||||
As an example, here is a code-snippet of an ability that creates a ***/hello*** command:
|
||||
|
||||
```java
|
||||
public Ability sayHelloWorld() {
|
||||
return Ability
|
||||
.builder()
|
||||
.name("hello")
|
||||
.info("says hello world!")
|
||||
.input(0)
|
||||
.locality(USER)
|
||||
.privacy(ADMIN)
|
||||
.action(ctx -> sender.send("Hello world!", ctx.chatId()))
|
||||
.post(ctx -> sender.send("Bye world!", ctx.chatId()))
|
||||
.build();
|
||||
}
|
||||
```
|
||||
Here is a breakdown of the above code snippet:
|
||||
* *.name()* - the name of the ability (essentially, this is the command)
|
||||
* *.info()* - provides information for the command
|
||||
* More on this later, but it basically centralizes command information in-code.
|
||||
* *.input()* - the number of input arguments needed, 0 is for do-not-care
|
||||
* *.locality()* - this answers where you want the ability to be available
|
||||
* In GROUP, USER private chats or ALL (both)
|
||||
* *.privacy()* - this answers who you want to access your ability
|
||||
* CREATOR, ADMIN, or everyone as PUBLIC
|
||||
* *.action()* - the feature logic resides here (a lambda function that takes a *MessageContext*)
|
||||
* *MessageContext* provides fast accessors for the **chatId**, **user** and the underlying **update**. It also conforms to the specifications of the basic API.
|
||||
* *.post()* - the logic executed **after** your main action finishes execution
|
||||
|
||||
The following is a snippet of how this would look like with the plain basic API.
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void onUpdateReceived(Update update) {
|
||||
// Global checks...
|
||||
// Switch, if, logic to route to hello world method
|
||||
// Execute method
|
||||
}
|
||||
|
||||
public void sayHelloWorld(Update update) {
|
||||
if (!update.hasMessage() || !update.getMessage().isUserMessage() || !update.getMessage().hasText() || update.getMessage.getText().isEmpty())
|
||||
return;
|
||||
User maybeAdmin = update.getMessage().getFrom();
|
||||
/* Query DB for if the user is an admin, can be SQL, Reddis, Ignite, etc...
|
||||
If user is not an admin, then return here.
|
||||
*/
|
||||
|
||||
SendMessage snd = new SendMessage();
|
||||
snd.setChatId(update.getMessage().getChatId());
|
||||
snd.setText("Hello world!");
|
||||
|
||||
try {
|
||||
sendMessage(snd);
|
||||
} catch (TelegramApiException e) {
|
||||
BotLogger.error("Could not send message", TAG, e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
I will leave you the choice to decide between the two snippets as to which is more **readable**, **writable** and **testable**.
|
||||
|
||||
***You can do so much more with abilities, besides plain commands. Head over to our [examples](#examples) to check out all of its features!***
|
||||
|
||||
Objective
|
||||
---------
|
||||
The AbilityBot abstraction intends to provide the following:
|
||||
* New feature is a new **Ability**, a new method - no fuss, zero overhead, no cross-code with other features
|
||||
* Argument length on a command is as easy as changing a single integer
|
||||
* Privacy settings per Ability - access levels to Abilities! User | Admin | Creator
|
||||
* Embedded database - available for every declared ability
|
||||
* Proxy sender interface - enhances testability; accurate results pre-release
|
||||
|
||||
Alongside these exciting core features of the AbilityBot, the following have been introduced:
|
||||
* The bot automatically maintains an up-to-date set of all the users who have contacted the bot
|
||||
* up-to-date: if a user changes their Username, First Name or Last Name, the bot updates the respective field in the embedded-DB
|
||||
* Backup and recovery for the DB
|
||||
* Default implementation relies on JSON/Jackson
|
||||
* Ban and unban users from accessing your bots
|
||||
* The bot will execute the shortest path to discard the update the next time they try to spam
|
||||
* Promote and demote users as bot administrators
|
||||
* Allows admins to execute admin abilities
|
||||
|
||||
What's next?
|
||||
------------
|
||||
I am looking forward to:
|
||||
* Provide a trigger to record metrics per ability
|
||||
* Implement AsyncAbility
|
||||
* Maintain integration with the latest updates on the basic API
|
||||
* Enrich the bot with features requested by the community
|
||||
|
||||
Examples
|
||||
-------------------
|
||||
* [Example Bots](https://github.com/addo37/ExampleBots)
|
||||
|
||||
Do you have a project that uses **AbilityBots**? Let us know!
|
||||
|
||||
Support
|
||||
-------
|
||||
For issues and features, please use GitHub's [issues](https://github.com/rubenlagus/TelegramBots/issues) tab.
|
||||
|
||||
For quick feedback, chatting or just having fun, please come and join us in our Telegram Supergroup.
|
||||
|
||||
[![Telegram](http://trellobot.doomdns.org/telegrambadge.svg)](https://telegram.me/JavaBotsApi)
|
||||
|
||||
Credits
|
||||
-------
|
||||
This project would not have been made possible had it not been for [Ruben](https://github.com/rubenlagus)'s work with the [Telegram Bot Java API](https://github.com/rubenlagus/TelegramBots).
|
||||
I strongly urge you to check out that project and implement a bot to have a sense of how the basic API feels like.
|
||||
Ruben has done a great job in supplying a clear and straightforward API that conforms to Telegram's HTTP API.
|
||||
There is also a chat for that API.
|
253
telegrambots-abilities/pom.xml
Normal file
253
telegrambots-abilities/pom.xml
Normal file
@ -0,0 +1,253 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.telegram</groupId>
|
||||
<artifactId>telegrambots-abilities</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Telegram Ability Bot</name>
|
||||
<url>https://github.com/rubenlagus/TelegramBots</url>
|
||||
<description>AbilityBot Extension and Abstraction</description>
|
||||
|
||||
<issueManagement>
|
||||
<url>https://github.com/rubenlagus/TelegramBots/issues</url>
|
||||
<system>GitHub Issues</system>
|
||||
</issueManagement>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/rubenlagus/TelegramBots</url>
|
||||
<connection>scm:git:git://github.com/rubenlagus/TelegramBots.git</connection>
|
||||
<developerConnection>scm:git:git@github.com:rubenlagus/TelegramBots.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<ciManagement>
|
||||
<url>https://travis-ci.org/rubenlagus/TelegramBots</url>
|
||||
<system>Travis</system>
|
||||
</ciManagement>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<email>abbas.aboudayya@gmail.com</email>
|
||||
<name>Abbas Abou Daya</name>
|
||||
<url>https://github.com/addo37</url>
|
||||
<id>addo37</id>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>http://www.opensource.org/licenses/mit-license.php</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>ossrh</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>ossrh</id>
|
||||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<bots.version>3.1.0</bots.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.telegram</groupId>
|
||||
<artifactId>telegrambots</artifactId>
|
||||
<version>${bots.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapdb</groupId>
|
||||
<artifactId>mapdb</artifactId>
|
||||
<version>3.0.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>19.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-all</artifactId>
|
||||
<version>2.0.2-beta</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.11</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<directory>${project.basedir}/target</directory>
|
||||
<outputDirectory>${project.build.directory}/classes</outputDirectory>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
<testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
|
||||
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>1.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.6.3</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-clean-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>clean-project</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.6</version>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.10.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<additionalparam>-Xdoclint:none</additionalparam>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.7.7.201606060606</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>test</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>1.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce-versions</id>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<rules>
|
||||
<DependencyConvergence />
|
||||
</rules>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>2.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy</id>
|
||||
<phase>package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
@ -0,0 +1,739 @@
|
||||
package org.telegram.abilitybots.api.bot;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.telegram.abilitybots.api.db.DBContext;
|
||||
import org.telegram.abilitybots.api.objects.*;
|
||||
import org.telegram.abilitybots.api.sender.DefaultMessageSender;
|
||||
import org.telegram.abilitybots.api.sender.MessageSender;
|
||||
import org.telegram.abilitybots.api.util.AbilityUtils;
|
||||
import org.telegram.abilitybots.api.util.Pair;
|
||||
import org.telegram.abilitybots.api.util.Trio;
|
||||
import org.telegram.telegrambots.api.methods.GetFile;
|
||||
import org.telegram.telegrambots.api.methods.send.SendDocument;
|
||||
import org.telegram.telegrambots.api.objects.Message;
|
||||
import org.telegram.telegrambots.api.objects.Update;
|
||||
import org.telegram.telegrambots.bots.DefaultBotOptions;
|
||||
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
|
||||
import org.telegram.telegrambots.exceptions.TelegramApiException;
|
||||
import org.telegram.telegrambots.logging.BotLogger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static java.lang.String.format;
|
||||
import static java.time.ZonedDateTime.now;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Objects.nonNull;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static java.util.function.Function.identity;
|
||||
import static java.util.regex.Pattern.CASE_INSENSITIVE;
|
||||
import static java.util.regex.Pattern.compile;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
import static jersey.repackaged.com.google.common.base.Throwables.propagate;
|
||||
import static org.telegram.abilitybots.api.db.MapDBContext.onlineInstance;
|
||||
import static org.telegram.abilitybots.api.objects.Ability.builder;
|
||||
import static org.telegram.abilitybots.api.objects.EndUser.fromUser;
|
||||
import static org.telegram.abilitybots.api.objects.Flag.*;
|
||||
import static org.telegram.abilitybots.api.objects.Locality.*;
|
||||
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
|
||||
import static org.telegram.abilitybots.api.objects.Privacy.*;
|
||||
import static org.telegram.abilitybots.api.util.AbilityUtils.*;
|
||||
|
||||
/**
|
||||
* The <b>father</b> of all ability bots. Bots that need to utilize abilities need to extend this bot.
|
||||
* <p>
|
||||
* It's important to note that this bot strictly extends {@link TelegramLongPollingBot}.
|
||||
* <p>
|
||||
* All bots extending the {@link AbilityBot} get implicit abilities:
|
||||
* <ul>
|
||||
* <li>/claim - Claims this bot</li>
|
||||
* <ul>
|
||||
* <li>Sets the user as the {@link Privacy#CREATOR} of the bot</li>
|
||||
* <li>Only the user with the ID returned by {@link AbilityBot#creatorId()} can genuinely claim the bot</li>
|
||||
* </ul>
|
||||
* <li>/commands - reports all user-defined commands (abilities)</li>
|
||||
* <ul>
|
||||
* <li>The same format acceptable by BotFather</li>
|
||||
* </ul>
|
||||
* <li>/backup - returns a backup of the bot database</li>
|
||||
* <li>/recover - recovers the database</li>
|
||||
* <li>/promote <code>@username</code> - promotes user to bot admin</li>
|
||||
* <li>/demote <code>@username</code> - demotes bot admin to user</li>
|
||||
* <li>/ban <code>@username</code> - bans the user from accessing your bot commands and features</li>
|
||||
* <li>/unban <code>@username</code> - lifts the ban from the user</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Additional information of the implicit abilities are present in the methods that declare them.
|
||||
* <p>
|
||||
* The two most important handles in the AbilityBot are the {@link DBContext} <b><code>db</code></b> and the {@link MessageSender} <b><code>sender</code></b>.
|
||||
* All bots extending AbilityBot can use both handles in their update consumers.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public abstract class AbilityBot extends TelegramLongPollingBot {
|
||||
private static final String TAG = AbilityBot.class.getSimpleName();
|
||||
|
||||
// DB objects
|
||||
public static final String ADMINS = "ADMINS";
|
||||
public static final String USERS = "USERS";
|
||||
public static final String USER_ID = "USER_ID";
|
||||
public static final String BLACKLIST = "BLACKLIST";
|
||||
|
||||
// Factory commands
|
||||
protected static final String DEFAULT = "default";
|
||||
protected static final String CLAIM = "claim";
|
||||
protected static final String BAN = "ban";
|
||||
protected static final String PROMOTE = "promote";
|
||||
protected static final String DEMOTE = "demote";
|
||||
protected static final String UNBAN = "unban";
|
||||
protected static final String BACKUP = "backup";
|
||||
protected static final String RECOVER = "recover";
|
||||
protected static final String COMMANDS = "commands";
|
||||
|
||||
// Messages
|
||||
protected static final String RECOVERY_MESSAGE = "I am ready to receive the backup file. Please reply to this message with the backup file attached.";
|
||||
protected static final String RECOVER_SUCCESS = "I have successfully recovered.";
|
||||
|
||||
// DB and sender
|
||||
protected final DBContext db;
|
||||
protected MessageSender sender;
|
||||
|
||||
// Bot token and username
|
||||
private final String botToken;
|
||||
private final String botUsername;
|
||||
|
||||
// Ability registry
|
||||
private Map<String, Ability> abilities;
|
||||
|
||||
// Reply registry
|
||||
private List<Reply> replies;
|
||||
|
||||
protected AbilityBot(String botToken, String botUsername, DBContext db, DefaultBotOptions botOptions) {
|
||||
super(botOptions);
|
||||
|
||||
this.botToken = botToken;
|
||||
this.botUsername = botUsername;
|
||||
this.db = db;
|
||||
this.sender = new DefaultMessageSender(this);
|
||||
|
||||
registerAbilities();
|
||||
}
|
||||
|
||||
protected AbilityBot(String botToken, String botUsername, DBContext db) {
|
||||
this(botToken, botUsername, db, new DefaultBotOptions());
|
||||
}
|
||||
|
||||
protected AbilityBot(String botToken, String botUsername, DefaultBotOptions botOptions) {
|
||||
this(botToken, botUsername, onlineInstance(botUsername), botOptions);
|
||||
}
|
||||
|
||||
protected AbilityBot(String botToken, String botUsername) {
|
||||
this(botToken, botUsername, onlineInstance(botUsername));
|
||||
}
|
||||
|
||||
public abstract int creatorId();
|
||||
|
||||
/**
|
||||
* @return the map of ID -> EndUser
|
||||
*/
|
||||
protected Map<Integer, EndUser> users() {
|
||||
return db.getMap(USERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the map of Username -> ID
|
||||
*/
|
||||
protected Map<String, Integer> userIds() {
|
||||
return db.getMap(USER_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a blacklist containing all the IDs of the banned users
|
||||
*/
|
||||
protected Set<Integer> blacklist() {
|
||||
return db.getSet(BLACKLIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an admin set of all the IDs of bot administrators
|
||||
*/
|
||||
protected Set<Integer> admins() {
|
||||
return db.getSet(ADMINS);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method contains the stream of actions that are applied on any update.
|
||||
* <p>
|
||||
* It will correctly handle addition of users into the DB and the execution of abilities and replies.
|
||||
*
|
||||
* @param update the update received by Telegram's API
|
||||
*/
|
||||
@Override
|
||||
public void onUpdateReceived(Update update) {
|
||||
BotLogger.info(format("New update [%s] received at %s", update.getUpdateId(), now()), format("%s - %s", TAG, botUsername));
|
||||
BotLogger.info(update.toString(), TAG);
|
||||
long millisStarted = System.currentTimeMillis();
|
||||
|
||||
Stream.of(update)
|
||||
.filter(this::checkGlobalFlags)
|
||||
.filter(this::checkBlacklist)
|
||||
.map(this::addUser)
|
||||
.filter(this::filterReply)
|
||||
.map(this::getAbility)
|
||||
.filter(this::validateAbility)
|
||||
.filter(this::checkPrivacy)
|
||||
.filter(this::checkLocality)
|
||||
.filter(this::checkInput)
|
||||
.filter(this::checkMessageFlags)
|
||||
.map(this::getContext)
|
||||
.map(this::consumeUpdate)
|
||||
.forEach(this::postConsumption);
|
||||
|
||||
long processingTime = System.currentTimeMillis() - millisStarted;
|
||||
BotLogger.info(format("Processing of update [%s] ended at %s%n---> Processing time: [%d ms] <---%n", update.getUpdateId(), now(), processingTime), format("%s - %s", TAG, botUsername));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBotToken() {
|
||||
return botToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBotUsername() {
|
||||
return botUsername;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the update against the provided global flags. The default implementation requires a {@link Flag#MESSAGE}.
|
||||
* <p>
|
||||
* This method should be <b>overridden</b> if the user wants updates that don't require a MESSAGE to pass through.
|
||||
*
|
||||
* @param update a Telegram {@link Update}
|
||||
* @return <tt>true</tt> if the update satisfies the global flags
|
||||
*/
|
||||
protected boolean checkGlobalFlags(Update update) {
|
||||
return MESSAGE.test(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user with the specified username.
|
||||
*
|
||||
* @param username the username of the required user
|
||||
* @return the user
|
||||
*/
|
||||
protected EndUser getUser(String username) {
|
||||
Integer id = userIds().get(username.toLowerCase());
|
||||
if (id == null) {
|
||||
throw new IllegalStateException(format("Could not find ID corresponding to username [%s]", username));
|
||||
}
|
||||
|
||||
return getUser(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user with the specified ID.
|
||||
*
|
||||
* @param id the id of the required user
|
||||
* @return the user
|
||||
*/
|
||||
protected EndUser getUser(int id) {
|
||||
EndUser endUser = users().get(id);
|
||||
if (endUser == null) {
|
||||
throw new IllegalStateException(format("Could not find user corresponding to id [%d]", id));
|
||||
}
|
||||
|
||||
return endUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user with the specified username. If user was not found, the bot will send a message on Telegram.
|
||||
*
|
||||
* @param username the username of the required user
|
||||
* @return the id of the user
|
||||
*/
|
||||
protected int getUserIdSendError(String username, long chatId) {
|
||||
try {
|
||||
return getUser(username).id();
|
||||
} catch (IllegalStateException ex) {
|
||||
sender.send(format("Sorry, I could not find the user [%s].", username), chatId);
|
||||
throw propagate(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Format of the report:
|
||||
* <p>
|
||||
* [command1] - [description1]
|
||||
* <p>
|
||||
* [command2] - [description2]
|
||||
* <p>
|
||||
* ...
|
||||
* <p>
|
||||
* Once you invoke it, the bot will send the available commands to the chat. This is a public ability so anyone can invoke it.
|
||||
* <p>
|
||||
* Usage: <code>/commands</code>
|
||||
*
|
||||
* @return the ability to report commands defined by the child bot.
|
||||
*/
|
||||
public Ability reportCommands() {
|
||||
return builder()
|
||||
.name(COMMANDS)
|
||||
.locality(ALL)
|
||||
.privacy(PUBLIC)
|
||||
.input(0)
|
||||
.action(ctx -> {
|
||||
String commands = abilities.entrySet().stream()
|
||||
.filter(entry -> nonNull(entry.getValue().info()))
|
||||
.map(entry -> {
|
||||
String name = entry.getValue().name();
|
||||
String info = entry.getValue().info();
|
||||
return format("%s - %s", name, info);
|
||||
})
|
||||
.sorted()
|
||||
.reduce((a, b) -> format("%s%n%s", a, b))
|
||||
.orElse("No public commands found.");
|
||||
|
||||
sender.send(commands, ctx.chatId());
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* This backup ability returns the object defined by {@link DBContext#backup()} as a message document.
|
||||
* <p>
|
||||
* This is a high-profile ability and is restricted to the CREATOR only.
|
||||
* <p>
|
||||
* Usage: <code>/backup</code>
|
||||
*
|
||||
* @return the ability to back-up the database of the bot
|
||||
*/
|
||||
public Ability backupDB() {
|
||||
return builder()
|
||||
.name(BACKUP)
|
||||
.locality(USER)
|
||||
.privacy(CREATOR)
|
||||
.input(0)
|
||||
.action(ctx -> {
|
||||
File backup = new File("backup.json");
|
||||
|
||||
try (PrintStream printStream = new PrintStream(backup)) {
|
||||
printStream.print(db.backup());
|
||||
sender.sendDocument(new SendDocument()
|
||||
.setNewDocument(backup)
|
||||
.setChatId(ctx.chatId())
|
||||
);
|
||||
} catch (FileNotFoundException e) {
|
||||
BotLogger.error("Error while fetching backup", TAG, e);
|
||||
} catch (TelegramApiException e) {
|
||||
BotLogger.error("Error while sending document/backup file", TAG, e);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovers the bot database using {@link DBContext#recover(Object)}.
|
||||
* <p>
|
||||
* The bot recovery process hugely depends on the implementation of the recovery method of {@link DBContext}.
|
||||
* <p>
|
||||
* Usage: <code>/recover</code>
|
||||
*
|
||||
* @return the ability to recover the database of the bot
|
||||
*/
|
||||
public Ability recoverDB() {
|
||||
return builder()
|
||||
.name(RECOVER)
|
||||
.locality(USER)
|
||||
.privacy(CREATOR)
|
||||
.input(0)
|
||||
.action(ctx -> sender.forceReply(RECOVERY_MESSAGE, ctx.chatId()))
|
||||
.reply(update -> {
|
||||
Long chatId = update.getMessage().getChatId();
|
||||
String fileId = update.getMessage().getDocument().getFileId();
|
||||
|
||||
try (FileReader reader = new FileReader(downloadFileWithId(fileId))) {
|
||||
String backupData = IOUtils.toString(reader);
|
||||
if (db.recover(backupData)) {
|
||||
sender.send(RECOVER_SUCCESS, chatId);
|
||||
} else {
|
||||
sender.send("Oops, something went wrong during recovery.", chatId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
BotLogger.error("Could not recover DB from backup", TAG, e);
|
||||
sender.send("I have failed to recover.", chatId);
|
||||
}
|
||||
}, MESSAGE, DOCUMENT, REPLY, isReplyTo(RECOVERY_MESSAGE))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Banned users are accumulated in the blacklist. Use {@link DBContext#getSet(String)} with name specified by {@link AbilityBot#BLACKLIST}.
|
||||
* <p>
|
||||
* Usage: <code>/ban @username</code>
|
||||
* <p>
|
||||
* <u>Note that admins who try to ban the creator, get banned.</u>
|
||||
*
|
||||
* @return the ability to ban the user from any kind of <b>bot interaction</b>
|
||||
*/
|
||||
public Ability banUser() {
|
||||
return builder()
|
||||
.name(BAN)
|
||||
.locality(ALL)
|
||||
.privacy(ADMIN)
|
||||
.input(1)
|
||||
.action(ctx -> {
|
||||
String username = stripTag(ctx.firstArg());
|
||||
int userId = getUserIdSendError(username, ctx.chatId());
|
||||
String bannedUser;
|
||||
|
||||
// Protection from abuse
|
||||
if (userId == creatorId()) {
|
||||
userId = ctx.user().id();
|
||||
bannedUser = isNullOrEmpty(ctx.user().username()) ? addTag(ctx.user().username()) : ctx.user().shortName();
|
||||
} else {
|
||||
bannedUser = addTag(username);
|
||||
}
|
||||
|
||||
Set<Integer> blacklist = blacklist();
|
||||
if (blacklist.contains(userId))
|
||||
sender.sendMd(format("%s is already *banned*.", bannedUser), ctx.chatId());
|
||||
else {
|
||||
blacklist.add(userId);
|
||||
sender.sendMd(format("%s is now *banned*.", bannedUser), ctx.chatId());
|
||||
}
|
||||
})
|
||||
.post(commitTo(db))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage: <code>/unban @username</code>
|
||||
*
|
||||
* @return the ability to unban a user
|
||||
*/
|
||||
public Ability unbanUser() {
|
||||
return builder()
|
||||
.name(UNBAN)
|
||||
.locality(ALL)
|
||||
.privacy(ADMIN)
|
||||
.input(1)
|
||||
.action(ctx -> {
|
||||
String username = stripTag(ctx.firstArg());
|
||||
Integer userId = getUserIdSendError(username, ctx.chatId());
|
||||
|
||||
Set<Integer> blacklist = blacklist();
|
||||
|
||||
if (!blacklist.remove(userId))
|
||||
sender.sendMd(format("@%s is *not* on the *blacklist*.", username), ctx.chatId());
|
||||
else {
|
||||
sender.sendMd(format("@%s, your ban has been *lifted*.", username), ctx.chatId());
|
||||
}
|
||||
})
|
||||
.post(commitTo(db))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ability to promote a user to a bot admin
|
||||
*/
|
||||
public Ability promoteAdmin() {
|
||||
return builder()
|
||||
.name(PROMOTE)
|
||||
.locality(ALL)
|
||||
.privacy(ADMIN)
|
||||
.input(1)
|
||||
.action(ctx -> {
|
||||
String username = stripTag(ctx.firstArg());
|
||||
Integer userId = getUserIdSendError(username, ctx.chatId());
|
||||
|
||||
Set<Integer> admins = admins();
|
||||
if (admins.contains(userId))
|
||||
sender.sendMd(format("@%s is already an *admin*.", username), ctx.chatId());
|
||||
else {
|
||||
admins.add(userId);
|
||||
sender.sendMd(format("@%s has been *promoted*.", username), ctx.chatId());
|
||||
}
|
||||
}).post(commitTo(db))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ability to demote an admin to a user
|
||||
*/
|
||||
public Ability demoteAdmin() {
|
||||
return builder()
|
||||
.name(DEMOTE)
|
||||
.locality(ALL)
|
||||
.privacy(ADMIN)
|
||||
.input(1)
|
||||
.action(ctx -> {
|
||||
String username = stripTag(ctx.firstArg());
|
||||
Integer userId = getUserIdSendError(username, ctx.chatId());
|
||||
|
||||
Set<Integer> admins = admins();
|
||||
if (admins.remove(userId)) {
|
||||
sender.sendMd(format("@%s has been *demoted*.", username), ctx.chatId());
|
||||
} else {
|
||||
sender.sendMd(format("@%s is *not* an *admin*.", username), ctx.chatId());
|
||||
}
|
||||
})
|
||||
.post(commitTo(db))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular users and admins who try to claim the bot will get <b>banned</b>.
|
||||
*
|
||||
* @return the ability to claim yourself as the master and creator of the bot
|
||||
*/
|
||||
public Ability claimCreator() {
|
||||
return builder()
|
||||
.name(CLAIM)
|
||||
.locality(ALL)
|
||||
.privacy(PUBLIC)
|
||||
.input(0)
|
||||
.action(ctx -> {
|
||||
if (ctx.user().id() == creatorId()) {
|
||||
Set<Integer> admins = admins();
|
||||
int id = creatorId();
|
||||
long chatId = ctx.chatId();
|
||||
|
||||
if (admins.contains(id))
|
||||
sender.send("You're already my master.", chatId);
|
||||
else {
|
||||
admins.add(id);
|
||||
sender.send("You're now my master.", chatId);
|
||||
}
|
||||
} else {
|
||||
// This is not a joke
|
||||
abilities.get(BAN).action().accept(newContext(ctx.update(), ctx.user(), ctx.chatId(), ctx.user().username()));
|
||||
}
|
||||
})
|
||||
.post(commitTo(db))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the declared abilities using method reflection. Also, replies are accumulated using the built abilities and standalone methods that return a Reply.
|
||||
* <p>
|
||||
* <b>Only abilities and replies with the <u>public</u> accessor are registered!</b>
|
||||
*/
|
||||
private void registerAbilities() {
|
||||
try {
|
||||
abilities = stream(this.getClass().getMethods())
|
||||
.filter(method -> method.getReturnType().equals(Ability.class))
|
||||
.map(this::returnAbility)
|
||||
.collect(toMap(Ability::name, identity()));
|
||||
|
||||
Stream<Reply> methodReplies = stream(this.getClass().getMethods())
|
||||
.filter(method -> method.getReturnType().equals(Reply.class))
|
||||
.map(this::returnReply);
|
||||
|
||||
Stream<Reply> abilityReplies = abilities.values().stream()
|
||||
.flatMap(ability -> ability.replies().stream());
|
||||
|
||||
replies = Stream.concat(methodReplies, abilityReplies).collect(toList());
|
||||
} catch (IllegalStateException e) {
|
||||
BotLogger.error(TAG, "Duplicate names found while registering abilities. Make sure that the abilities declared don't clash with the reserved ones.", e);
|
||||
throw propagate(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the method and retrieves its return {@link Ability}.
|
||||
*
|
||||
* @param method a method that returns an ability
|
||||
* @return the ability returned by the method
|
||||
*/
|
||||
private Ability returnAbility(Method method) {
|
||||
try {
|
||||
return (Ability) method.invoke(this);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
BotLogger.error("Could not add ability", TAG, e);
|
||||
throw propagate(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the method and retrieves its returned Reply.
|
||||
*
|
||||
* @param method a method that returns a reply
|
||||
* @return the reply returned by the method
|
||||
*/
|
||||
private Reply returnReply(Method method) {
|
||||
try {
|
||||
return (Reply) method.invoke(this);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
BotLogger.error("Could not add reply", TAG, e);
|
||||
throw propagate(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void postConsumption(Pair<MessageContext, Ability> pair) {
|
||||
ofNullable(pair.b().postAction())
|
||||
.ifPresent(consumer -> consumer.accept(pair.a()));
|
||||
}
|
||||
|
||||
Pair<MessageContext, Ability> consumeUpdate(Pair<MessageContext, Ability> pair) {
|
||||
pair.b().action().accept(pair.a());
|
||||
return pair;
|
||||
}
|
||||
|
||||
Pair<MessageContext, Ability> getContext(Trio<Update, Ability, String[]> trio) {
|
||||
Update update = trio.a();
|
||||
EndUser user = fromUser(AbilityUtils.getUser(update));
|
||||
|
||||
return Pair.of(newContext(update, user, getChatId(update), trio.c()), trio.b());
|
||||
}
|
||||
|
||||
boolean checkBlacklist(Update update) {
|
||||
Integer id = AbilityUtils.getUser(update).getId();
|
||||
|
||||
return id == creatorId() || !blacklist().contains(id);
|
||||
}
|
||||
|
||||
boolean checkInput(Trio<Update, Ability, String[]> trio) {
|
||||
String[] tokens = trio.c();
|
||||
int abilityTokens = trio.b().tokens();
|
||||
|
||||
boolean isOk = abilityTokens == 0 || (tokens.length > 0 && tokens.length == abilityTokens);
|
||||
|
||||
if (!isOk)
|
||||
sender.send(String.format("Sorry, this feature requires %d additional %s.", abilityTokens, abilityTokens == 1 ? "input" : "inputs"), getChatId(trio.a()));
|
||||
return isOk;
|
||||
}
|
||||
|
||||
boolean checkLocality(Trio<Update, Ability, String[]> trio) {
|
||||
Update update = trio.a();
|
||||
Locality locality = isUserMessage(update) ? USER : GROUP;
|
||||
Locality abilityLocality = trio.b().locality();
|
||||
|
||||
boolean isOk = abilityLocality == ALL || locality == abilityLocality;
|
||||
|
||||
if (!isOk)
|
||||
sender.send(String.format("Sorry, %s-only feature.", abilityLocality.toString().toLowerCase()), getChatId(trio.a()));
|
||||
return isOk;
|
||||
}
|
||||
|
||||
boolean checkPrivacy(Trio<Update, Ability, String[]> trio) {
|
||||
Update update = trio.a();
|
||||
EndUser user = fromUser(AbilityUtils.getUser(update));
|
||||
Privacy privacy;
|
||||
int id = user.id();
|
||||
|
||||
privacy = isCreator(id) ? CREATOR : isAdmin(id) ? ADMIN : PUBLIC;
|
||||
|
||||
boolean isOk = privacy.compareTo(trio.b().privacy()) >= 0;
|
||||
|
||||
if (!isOk)
|
||||
sender.send(String.format("Sorry, %s-only feature.", trio.b().privacy().toString().toLowerCase()), getChatId(trio.a()));
|
||||
return isOk;
|
||||
}
|
||||
|
||||
private boolean isCreator(int id) {
|
||||
return id == creatorId();
|
||||
}
|
||||
|
||||
private boolean isAdmin(Integer id) {
|
||||
return admins().contains(id);
|
||||
}
|
||||
|
||||
boolean validateAbility(Trio<Update, Ability, String[]> trio) {
|
||||
return trio.b() != null;
|
||||
}
|
||||
|
||||
Trio<Update, Ability, String[]> getAbility(Update update) {
|
||||
// Handle updates without messages
|
||||
// Passing through this function means that the global flags have passed
|
||||
Message msg = update.getMessage();
|
||||
if (!update.hasMessage() || !msg.hasText())
|
||||
return Trio.of(update, abilities.get(DEFAULT), new String[]{});
|
||||
|
||||
// Priority goes to text before captions
|
||||
String[] tokens = msg.getText().split(" ");
|
||||
|
||||
if (tokens[0].startsWith("/")) {
|
||||
String abilityToken = stripBotUsername(tokens[0].substring(1));
|
||||
Ability ability = abilities.get(abilityToken);
|
||||
tokens = Arrays.copyOfRange(tokens, 1, tokens.length);
|
||||
return Trio.of(update, ability, tokens);
|
||||
} else {
|
||||
Ability ability = abilities.get(DEFAULT);
|
||||
return Trio.of(update, ability, tokens);
|
||||
}
|
||||
}
|
||||
|
||||
private String stripBotUsername(String token) {
|
||||
return compile(format("@%s", botUsername), CASE_INSENSITIVE)
|
||||
.matcher(token)
|
||||
.replaceAll("");
|
||||
}
|
||||
|
||||
Update addUser(Update update) {
|
||||
EndUser endUser = fromUser(AbilityUtils.getUser(update));
|
||||
|
||||
users().compute(endUser.id(), (id, user) -> {
|
||||
if (user == null) {
|
||||
updateUserId(user, endUser);
|
||||
return endUser;
|
||||
}
|
||||
|
||||
if (!user.equals(endUser)) {
|
||||
updateUserId(user, endUser);
|
||||
return endUser;
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
|
||||
db.commit();
|
||||
return update;
|
||||
}
|
||||
|
||||
private void updateUserId(EndUser oldUser, EndUser newUser) {
|
||||
if (oldUser != null && oldUser.username() != null) {
|
||||
// Remove old username -> ID
|
||||
userIds().remove(oldUser.username());
|
||||
}
|
||||
|
||||
if (newUser.username() != null) {
|
||||
// Add new mapping with the new username
|
||||
userIds().put(newUser.username().toLowerCase(), newUser.id());
|
||||
}
|
||||
}
|
||||
|
||||
boolean filterReply(Update update) {
|
||||
return replies.stream()
|
||||
.filter(reply -> reply.isOkFor(update))
|
||||
.map(reply -> {
|
||||
reply.actOn(update);
|
||||
return false;
|
||||
})
|
||||
.reduce(true, Boolean::logicalAnd);
|
||||
}
|
||||
|
||||
boolean checkMessageFlags(Trio<Update, Ability, String[]> trio) {
|
||||
Ability ability = trio.b();
|
||||
Update update = trio.a();
|
||||
|
||||
return ability.flags().stream()
|
||||
.reduce(true, (flag, nextFlag) -> flag && nextFlag.test(update), Boolean::logicalAnd);
|
||||
}
|
||||
|
||||
private File downloadFileWithId(String fileId) throws TelegramApiException {
|
||||
return sender.downloadFile(sender.getFile(new GetFile().setFileId(fileId)));
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package org.telegram.abilitybots.api.db;
|
||||
|
||||
import org.telegram.abilitybots.api.bot.AbilityBot;
|
||||
import org.telegram.telegrambots.api.objects.Update;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This interface represents the high-level methods exposed to the user when handling an {@link Update}.
|
||||
* Example usage:
|
||||
* <p><code>Ability.builder().action(ctx -> {db.getSet(USERS); doSomething();})* </code></p>
|
||||
* {@link AbilityBot} contains a handle on the <code>db</code> that the user can use inside his declared abilities.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public interface DBContext extends Closeable {
|
||||
/**
|
||||
* @param name the unique name of the {@link List}
|
||||
* @param <T> the type that the List holds
|
||||
* @return the List with the specified name
|
||||
*/
|
||||
<T> List<T> getList(String name);
|
||||
|
||||
/**
|
||||
* @param name the unique name of the {@link Map}
|
||||
* @param <K> the type of the Map keys
|
||||
* @param <V> the type of the Map values
|
||||
* @return the Map with the specified name
|
||||
*/
|
||||
<K, V> Map<K, V> getMap(String name);
|
||||
|
||||
/**
|
||||
* @param name the unique name of the {@link Set}
|
||||
* @param <T> the type that the Set holds
|
||||
* @return the Set with the specified name
|
||||
*/
|
||||
<T> Set<T> getSet(String name);
|
||||
|
||||
/**
|
||||
* @return a high-level summary of the database structures (Sets, Lists, Maps, ...) present.
|
||||
*/
|
||||
String summary();
|
||||
|
||||
/**
|
||||
* Implementations of this method are free to return any object such as XML, JSON, etc...
|
||||
*
|
||||
* @return a backup of the DB
|
||||
*/
|
||||
Object backup();
|
||||
|
||||
/**
|
||||
* The object passed to this method need to conform to the implementation of the {@link DBContext#backup()} method.
|
||||
*
|
||||
* @param backup the backup of the database containing all the structures
|
||||
* @return <tt>true</tt> if the database successfully recovered
|
||||
*/
|
||||
boolean recover(Object backup);
|
||||
|
||||
/**
|
||||
* @param name the name of the data structure
|
||||
* @return the high-level information of the structure
|
||||
*/
|
||||
String info(String name);
|
||||
|
||||
/**
|
||||
* Commits the database to its persistent layer. Implementations are free to not implement this method as it is not compulsory.
|
||||
*/
|
||||
void commit();
|
||||
|
||||
/**
|
||||
* Clears the data structures present in the database.
|
||||
* <p>
|
||||
* This method does not delete the data-structure themselves, but leaves them empty.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @param name the name of the data structure
|
||||
* @return <tt>true</tt> if this database contains the specified structure name
|
||||
*/
|
||||
boolean contains(String name);
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
package org.telegram.abilitybots.api.db;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.mapdb.DB;
|
||||
import org.mapdb.DBMaker;
|
||||
import org.mapdb.Serializer;
|
||||
import org.telegram.abilitybots.api.util.Pair;
|
||||
import org.telegram.telegrambots.logging.BotLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
import static com.google.common.collect.Maps.newHashMap;
|
||||
import static com.google.common.collect.Sets.newHashSet;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
import static java.util.stream.StreamSupport.stream;
|
||||
import static org.mapdb.Serializer.JAVA;
|
||||
import static org.telegram.abilitybots.api.bot.AbilityBot.USERS;
|
||||
|
||||
/**
|
||||
* An implementation of {@link DBContext} that relies on a {@link DB}.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
* @see <a href="https://github.com/jankotek/mapdb">MapDB project</a>
|
||||
*/
|
||||
public class MapDBContext implements DBContext {
|
||||
private static final String TAG = DBContext.class.getSimpleName();
|
||||
|
||||
private final DB db;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MapDBContext(DB db) {
|
||||
this.db = db;
|
||||
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper.enableDefaultTyping();
|
||||
}
|
||||
|
||||
/**
|
||||
* This DB returned by this method does not trigger deletion on JVM shutdown.
|
||||
*
|
||||
* @param name name of the DB file
|
||||
* @return an online instance of {@link MapDBContext}
|
||||
*/
|
||||
public static DBContext onlineInstance(String name) {
|
||||
DB db = DBMaker
|
||||
.fileDB(name)
|
||||
.fileMmapEnableIfSupported()
|
||||
.closeOnJvmShutdown()
|
||||
.transactionEnable()
|
||||
.make();
|
||||
|
||||
return new MapDBContext(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* This DB returned by this method gets deleted on JVM shutdown.
|
||||
*
|
||||
* @param name name of the DB file
|
||||
* @return an offline instance of {@link MapDBContext}
|
||||
*/
|
||||
public static DBContext offlineInstance(String name) {
|
||||
DB db = DBMaker
|
||||
.fileDB(name)
|
||||
.fileMmapEnableIfSupported()
|
||||
.closeOnJvmShutdown()
|
||||
.cleanerHackEnable()
|
||||
.transactionEnable()
|
||||
.fileDeleteAfterClose()
|
||||
.make();
|
||||
|
||||
return new MapDBContext(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> getList(String name) {
|
||||
return (List<T>) db.<T>indexTreeList(name, Serializer.JAVA).createOrOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <K, V> Map<K, V> getMap(String name) {
|
||||
return db.<K, V>hashMap(name, JAVA, JAVA).createOrOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Set<T> getSet(String name) {
|
||||
return (Set<T>) db.<T>hashSet(name, JAVA).createOrOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String summary() {
|
||||
return stream(db.getAllNames().spliterator(), false)
|
||||
.map(this::info)
|
||||
.reduce(new StringJoiner("\n"), StringJoiner::add, StringJoiner::merge)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object backup() {
|
||||
Map<String, Object> collectedMap = localCopy();
|
||||
return writeAsString(collectedMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean recover(Object backup) {
|
||||
Map<String, Object> snapshot = localCopy();
|
||||
|
||||
try {
|
||||
Map<String, Object> backupData = objectMapper.readValue(backup.toString(), new TypeReference<HashMap<String, Object>>() {
|
||||
});
|
||||
doRecover(backupData);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
BotLogger.error(format("Could not recover DB data from file with String representation %s", backup), TAG, e);
|
||||
// Attempt to fallback to data snapshot before recovery
|
||||
doRecover(snapshot);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String info(String name) {
|
||||
Object struct = db.get(name);
|
||||
if (isNull(struct))
|
||||
throw new IllegalStateException(format("DB structure with name [%s] does not exist", name));
|
||||
|
||||
if (struct instanceof Set)
|
||||
return format("%s - Set - %d", name, ((Set) struct).size());
|
||||
else if (struct instanceof List)
|
||||
return format("%s - List - %d", name, ((List) struct).size());
|
||||
else if (struct instanceof Map)
|
||||
return format("%s - Map - %d", name, ((Map) struct).size());
|
||||
else
|
||||
return format("%s - %s", name, struct.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
db.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
db.getAllNames().forEach(name -> {
|
||||
Object struct = db.get(name);
|
||||
if (struct instanceof Collection)
|
||||
((Collection) struct).clear();
|
||||
else if (struct instanceof Map)
|
||||
((Map) struct).clear();
|
||||
});
|
||||
commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String name) {
|
||||
return db.exists(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
db.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a local non-thread safe copy of the database
|
||||
*/
|
||||
private Map<String, Object> localCopy() {
|
||||
return db.getAll().entrySet().stream().map(entry -> {
|
||||
Object struct = entry.getValue();
|
||||
if (struct instanceof Set)
|
||||
return Pair.of(entry.getKey(), newHashSet((Set) struct));
|
||||
else if (struct instanceof List)
|
||||
return Pair.of(entry.getKey(), newArrayList((List) struct));
|
||||
else if (struct instanceof Map)
|
||||
return Pair.of(entry.getKey(), newHashMap((Map) struct));
|
||||
else
|
||||
return Pair.of(entry.getKey(), struct);
|
||||
}).collect(toMap(pair -> (String) pair.a(), Pair::b));
|
||||
}
|
||||
|
||||
private void doRecover(Map<String, Object> backupData) {
|
||||
clear();
|
||||
backupData.forEach((name, value) -> {
|
||||
|
||||
if (value instanceof Set) {
|
||||
Set entrySet = (Set) value;
|
||||
getSet(name).addAll(entrySet);
|
||||
} else if (value instanceof Map) {
|
||||
Map<Object, Object> entryMap = (Map) value;
|
||||
|
||||
// TODO: This is ugly
|
||||
// Special handling of USERS since the key is an integer. JSON by default considers a map a JSONObject.
|
||||
// Keys are serialized and deserialized as String
|
||||
if (name.equals(USERS))
|
||||
entryMap = entryMap.entrySet().stream()
|
||||
.map(entry -> Pair.of(Integer.parseInt(entry.getKey().toString()), entry.getValue()))
|
||||
.collect(toMap(Pair::a, Pair::b));
|
||||
|
||||
getMap(name).putAll(entryMap);
|
||||
} else if (value instanceof List) {
|
||||
List entryList = (List) value;
|
||||
getList(name).addAll(entryList);
|
||||
} else {
|
||||
BotLogger.error(TAG, format("Unable to identify object type during DB recovery, entry name: %s", name));
|
||||
}
|
||||
});
|
||||
commit();
|
||||
}
|
||||
|
||||
private String writeAsString(Object obj) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
BotLogger.info(format("Failed to read the JSON representation of object: %s", obj), TAG, e);
|
||||
return "Error reading required data...";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import org.telegram.telegrambots.api.objects.Update;
|
||||
import org.telegram.telegrambots.logging.BotLogger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Objects.hash;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.apache.commons.lang3.StringUtils.*;
|
||||
|
||||
/**
|
||||
* An ability is a fully-fledged bot action that contains all the necessary information to process:
|
||||
* <ol>
|
||||
* <li>A response to a command</li>
|
||||
* <li>A post-response to a command</li>
|
||||
* <li>A reply to a sequence of actions</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* In-order to instantiate an ability, you can call {@link Ability#builder()} to get the {@link AbilityBuilder}.
|
||||
* Once you're done setting your ability, you'll call {@link AbilityBuilder#build()} to get your constructed ability.
|
||||
* <p>
|
||||
* The only optional fields in an ability are {@link Ability#info}, {@link Ability#postAction}, {@link Ability#flags} and {@link Ability#replies}.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public final class Ability {
|
||||
private static final String TAG = Ability.class.getSimpleName();
|
||||
|
||||
private final String name;
|
||||
private final String info;
|
||||
private final Locality locality;
|
||||
private final Privacy privacy;
|
||||
private final int argNum;
|
||||
private final Consumer<MessageContext> action;
|
||||
private final Consumer<MessageContext> postAction;
|
||||
private final List<Reply> replies;
|
||||
private final List<Predicate<Update>> flags;
|
||||
|
||||
private Ability(String name, String info, Locality locality, Privacy privacy, int argNum, Consumer<MessageContext> action, Consumer<MessageContext> postAction, List<Reply> replies, Predicate<Update>... flags) {
|
||||
checkArgument(!isEmpty(name), "Method name cannot be empty");
|
||||
checkArgument(!containsWhitespace(name), "Method name cannot contain spaces");
|
||||
checkArgument(isAlphanumeric(name), "Method name can only be alpha-numeric", name);
|
||||
this.name = name;
|
||||
this.info = info;
|
||||
|
||||
this.locality = checkNotNull(locality, "Please specify a valid locality setting. Use the Locality enum class");
|
||||
this.privacy = checkNotNull(privacy, "Please specify a valid privacy setting. Use the Privacy enum class");
|
||||
|
||||
checkArgument(argNum >= 0, "The number of arguments the method can handle CANNOT be negative. " +
|
||||
"Use the number 0 if the method ignores the arguments OR uses as many as appended");
|
||||
this.argNum = argNum;
|
||||
|
||||
this.action = checkNotNull(action, "Method action can't be empty. Please assign a function by using .action() method");
|
||||
if (postAction == null)
|
||||
BotLogger.info(TAG, format("No post action was detected for method with name [%s]", name));
|
||||
|
||||
this.flags = ofNullable(flags).map(Arrays::asList).orElse(newArrayList());
|
||||
|
||||
this.postAction = postAction;
|
||||
this.replies = replies;
|
||||
}
|
||||
|
||||
public static AbilityBuilder builder() {
|
||||
return new AbilityBuilder();
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String info() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public Locality locality() {
|
||||
return locality;
|
||||
}
|
||||
|
||||
public Privacy privacy() {
|
||||
return privacy;
|
||||
}
|
||||
|
||||
public int tokens() {
|
||||
return argNum;
|
||||
}
|
||||
|
||||
public Consumer<MessageContext> action() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public Consumer<MessageContext> postAction() {
|
||||
return postAction;
|
||||
}
|
||||
|
||||
public List<Reply> replies() {
|
||||
return replies;
|
||||
}
|
||||
|
||||
public List<Predicate<Update>> flags() {
|
||||
return flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("locality", locality)
|
||||
.add("privacy", privacy)
|
||||
.add("argNum", argNum)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Ability ability = (Ability) o;
|
||||
return argNum == ability.argNum &&
|
||||
Objects.equal(name, ability.name) &&
|
||||
locality == ability.locality &&
|
||||
privacy == ability.privacy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash(name, info, locality, privacy, argNum, action, postAction, replies, flags);
|
||||
}
|
||||
|
||||
public static class AbilityBuilder {
|
||||
private String name;
|
||||
private String info;
|
||||
private Privacy privacy;
|
||||
private Locality locality;
|
||||
private int argNum;
|
||||
private Consumer<MessageContext> consumer;
|
||||
private Consumer<MessageContext> postConsumer;
|
||||
private List<Reply> replies;
|
||||
private Flag[] flags;
|
||||
|
||||
private AbilityBuilder() {
|
||||
replies = newArrayList();
|
||||
}
|
||||
|
||||
public AbilityBuilder action(Consumer<MessageContext> consumer) {
|
||||
this.consumer = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder info(String info) {
|
||||
this.info = info;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder flag(Flag... flags) {
|
||||
this.flags = flags;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder locality(Locality type) {
|
||||
this.locality = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder input(int argNum) {
|
||||
this.argNum = argNum;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder privacy(Privacy privacy) {
|
||||
this.privacy = privacy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbilityBuilder post(Consumer<MessageContext> postConsumer) {
|
||||
this.postConsumer = postConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final AbilityBuilder reply(Consumer<Update> action, Predicate<Update>... conditions) {
|
||||
replies.add(Reply.of(action, conditions));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Ability build() {
|
||||
return new Ability(name, info, locality, privacy, argNum, consumer, postConsumer, replies, flags);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
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.telegram.telegrambots.api.objects.User;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isEmpty;
|
||||
|
||||
/**
|
||||
* This class serves the purpose of separating the basic Telegram {@link User} and the augmented {@link EndUser}.
|
||||
* <p>
|
||||
* It adds proper hashCode, equals, toString as well as useful utility methods such as {@link EndUser#shortName} and {@link EndUser#fullName}.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public final class EndUser implements Serializable {
|
||||
@JsonProperty("id")
|
||||
private final Integer id;
|
||||
@JsonProperty("firstName")
|
||||
private final String firstName;
|
||||
@JsonProperty("lastName")
|
||||
private final String lastName;
|
||||
@JsonProperty("username")
|
||||
private final String username;
|
||||
|
||||
private EndUser(Integer id, String firstName, String lastName, String username) {
|
||||
this.id = id;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static EndUser endUser(@JsonProperty("id") Integer id,
|
||||
@JsonProperty("firstName") String firstName,
|
||||
@JsonProperty("lastName") String lastName,
|
||||
@JsonProperty("username") String username) {
|
||||
return new EndUser(id, firstName, lastName, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@link EndUser} from a {@link User}.
|
||||
*
|
||||
* @param user the Telegram user
|
||||
* @return an augmented end-user
|
||||
*/
|
||||
public static EndUser fromUser(User user) {
|
||||
return new EndUser(user.getId(), user.getFirstName(), user.getLastName(), user.getUserName());
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String firstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String lastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public String username() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* The full name is identified as the concatenation of the first and last name, separated by a space.
|
||||
* This method can return an empty name if both first and last name are empty.
|
||||
*
|
||||
* @return the full name of the user
|
||||
*/
|
||||
public String fullName() {
|
||||
StringJoiner name = new StringJoiner(" ");
|
||||
|
||||
if (!isEmpty(firstName))
|
||||
name.add(firstName);
|
||||
if (!isEmpty(lastName))
|
||||
name.add(lastName);
|
||||
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The short name is one of the following:
|
||||
* <ol>
|
||||
* <li>First name</li>
|
||||
* <li>Last name</li>
|
||||
* <li>Username</li>
|
||||
* </ol>
|
||||
* The method will try to return the first valid name in the specified order.
|
||||
*
|
||||
* @return the short name of the user
|
||||
*/
|
||||
public String shortName() {
|
||||
if (!isEmpty(firstName))
|
||||
return firstName;
|
||||
|
||||
if (!isEmpty(lastName))
|
||||
return lastName;
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
EndUser endUser = (EndUser) o;
|
||||
return Objects.equals(id, endUser.id) &&
|
||||
Objects.equals(firstName, endUser.firstName) &&
|
||||
Objects.equals(lastName, endUser.lastName) &&
|
||||
Objects.equals(username, endUser.username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, firstName, lastName, username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("id", id)
|
||||
.add("firstName", firstName)
|
||||
.add("lastName", lastName)
|
||||
.add("username", username)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder;
|
||||
import org.telegram.telegrambots.api.objects.Update;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
/**
|
||||
* Flags are an conditions that are applied on an {@link Update}.
|
||||
* <p>
|
||||
* They can be used on {@link AbilityBuilder#flag(Flag...)} and on the post conditions in {@link AbilityBuilder#reply(Consumer, Predicate[])}.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public enum Flag implements Predicate<Update> {
|
||||
// Update Flags
|
||||
NONE(update -> true),
|
||||
MESSAGE(Update::hasMessage),
|
||||
CALLBACK_QUERY(Update::hasCallbackQuery),
|
||||
CHANNEL_POST(Update::hasChannelPost),
|
||||
EDITED_CHANNEL_POST(Update::hasEditedChannelPost),
|
||||
EDITED_MESSAGE(Update::hasEditedMessage),
|
||||
INLINE_QUERY(Update::hasInlineQuery),
|
||||
CHOSEN_INLINE_QUERY(Update::hasChosenInlineQuery),
|
||||
|
||||
// Message Flags
|
||||
REPLY(update -> update.getMessage().isReply()),
|
||||
DOCUMENT(upd -> upd.getMessage().hasDocument()),
|
||||
TEXT(upd -> upd.getMessage().hasText()),
|
||||
PHOTO(upd -> upd.getMessage().hasPhoto()),
|
||||
LOCATION(upd -> upd.getMessage().hasLocation()),
|
||||
CAPTION(upd -> nonNull(upd.getMessage().getCaption()));
|
||||
|
||||
private final Predicate<Update> predicate;
|
||||
|
||||
Flag(Predicate<Update> predicate) {
|
||||
this.predicate = predicate;
|
||||
}
|
||||
|
||||
public boolean test(Update update) {
|
||||
return nonNull(update) && predicate.test(update);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
/**
|
||||
* Locality identifies the location in which you want your message to be accessed.
|
||||
* <p>
|
||||
* If locality of your message is set to <code>USER</code>, then the ability will only be executed if its being called in a user private chat.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public enum Locality {
|
||||
/**
|
||||
* Ability would be valid for groups and private user chats
|
||||
*/
|
||||
ALL,
|
||||
/**
|
||||
* Only user chats
|
||||
*/
|
||||
USER,
|
||||
/**
|
||||
* Only group chats
|
||||
*/
|
||||
GROUP
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import org.telegram.telegrambots.api.objects.Update;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* MessageContext is a wrapper class to the {@link Update}, originating end-user and the arguments present in its message (if any).
|
||||
* <p>
|
||||
* A user is not bound to the usage of this higher level context as it's possible to fetch the underlying {@link Update} using {@link MessageContext#update()}.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public class MessageContext {
|
||||
private final EndUser user;
|
||||
private final Long chatId;
|
||||
private final String[] arguments;
|
||||
private final Update update;
|
||||
|
||||
private MessageContext(Update update, EndUser user, Long chatId, String[] arguments) {
|
||||
this.user = user;
|
||||
this.chatId = chatId;
|
||||
this.update = update;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
public static MessageContext newContext(Update update, EndUser user, Long chatId, String... arguments) {
|
||||
return new MessageContext(update, user, chatId, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the originating Telegram user of this update
|
||||
*/
|
||||
public EndUser user() {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the originating chatId, maps correctly to both group and user-private chats
|
||||
*/
|
||||
public Long chatId() {
|
||||
return chatId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there's no message in the update, then this will an empty array.
|
||||
*
|
||||
* @return the text sent by the user message.
|
||||
*/
|
||||
public String[] arguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first argument directly after the command
|
||||
* @throws IllegalStateException if message has no arguments
|
||||
*/
|
||||
public String firstArg() {
|
||||
checkLength();
|
||||
return arguments[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the second argument directly after the command
|
||||
* @throws IllegalStateException if message has no arguments
|
||||
*/
|
||||
public String secondArg() {
|
||||
checkLength();
|
||||
return arguments[1 % arguments.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the third argument directly after the command
|
||||
* @throws IllegalStateException if message has no arguments
|
||||
*/
|
||||
public String thirdArg() {
|
||||
checkLength();
|
||||
return arguments[2 % arguments.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the actual update behind this context
|
||||
*/
|
||||
public Update update() {
|
||||
return update;
|
||||
}
|
||||
|
||||
private void checkLength() {
|
||||
if (arguments.length == 0)
|
||||
throw new IllegalStateException("This message has no arguments");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("user", user)
|
||||
.add("chatId", chatId)
|
||||
.add("arguments", arguments)
|
||||
.add("update", update)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
MessageContext that = (MessageContext) o;
|
||||
return Objects.equal(user, that.user) &&
|
||||
Objects.equal(chatId, that.chatId) &&
|
||||
Arrays.equals(arguments, that.arguments) &&
|
||||
Objects.equal(update, that.update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(user, chatId, Arrays.hashCode(arguments), update);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
/**
|
||||
* Privacy represents a restriction on <b>who</b> can use the ability.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public enum Privacy {
|
||||
/**
|
||||
* Anybody who is not a bot admin or its creator will be considered as a public user.
|
||||
*/
|
||||
PUBLIC,
|
||||
/**
|
||||
* A global admin of the bot, regardless of the group the bot is in.
|
||||
*/
|
||||
ADMIN,
|
||||
/**
|
||||
* The creator of the bot.
|
||||
*/
|
||||
CREATOR
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import org.telegram.telegrambots.api.objects.Update;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
/**
|
||||
* A reply consists of update conditionals and an action to be applied on the update.
|
||||
* <p>
|
||||
* If an update satisfies the {@link Reply#conditions}set by the reply, then it's safe to {@link Reply#actOn(Update)}.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public final class Reply {
|
||||
public final List<Predicate<Update>> conditions;
|
||||
public final Consumer<Update> action;
|
||||
|
||||
private Reply(List<Predicate<Update>> conditions, Consumer<Update> action) {
|
||||
this.conditions = conditions;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public static Reply of(Consumer<Update> action, Predicate<Update>... conditions) {
|
||||
return new Reply(asList(conditions), action);
|
||||
}
|
||||
|
||||
public boolean isOkFor(Update update) {
|
||||
return conditions.stream().reduce(true, (state, cond) -> state && cond.test(update), Boolean::logicalAnd);
|
||||
}
|
||||
|
||||
public void actOn(Update update) {
|
||||
action.accept(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Reply reply = (Reply) o;
|
||||
return Objects.equals(conditions, reply.conditions) &&
|
||||
Objects.equals(action, reply.action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(conditions, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("conditions", conditions)
|
||||
.add("action", action)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,493 @@
|
||||
package org.telegram.abilitybots.api.sender;
|
||||
|
||||
import org.telegram.telegrambots.api.methods.*;
|
||||
import org.telegram.telegrambots.api.methods.games.GetGameHighScores;
|
||||
import org.telegram.telegrambots.api.methods.games.SetGameScore;
|
||||
import org.telegram.telegrambots.api.methods.groupadministration.*;
|
||||
import org.telegram.telegrambots.api.methods.pinnedmessages.PinChatMessage;
|
||||
import org.telegram.telegrambots.api.methods.pinnedmessages.UnpinChatMessage;
|
||||
import org.telegram.telegrambots.api.methods.send.*;
|
||||
import org.telegram.telegrambots.api.methods.updates.DeleteWebhook;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.DeleteMessage;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageCaption;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageReplyMarkup;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageText;
|
||||
import org.telegram.telegrambots.api.objects.*;
|
||||
import org.telegram.telegrambots.api.objects.games.GameHighScore;
|
||||
import org.telegram.telegrambots.api.objects.replykeyboard.ForceReplyKeyboard;
|
||||
import org.telegram.telegrambots.bots.DefaultAbsSender;
|
||||
import org.telegram.telegrambots.exceptions.TelegramApiException;
|
||||
import org.telegram.telegrambots.logging.BotLogger;
|
||||
import org.telegram.telegrambots.updateshandlers.DownloadFileCallback;
|
||||
import org.telegram.telegrambots.updateshandlers.SentCallback;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
/**
|
||||
* The default implementation of the {@link MessageSender}. This serves as a proxy to the {@link DefaultAbsSender} methods.
|
||||
* <p>Most of the methods below will be directly calling the bot's similar functions. However, there are some methods introduced to ease sending messages such as:</p>
|
||||
* <ol>
|
||||
* <li>{@link DefaultMessageSender#sendMd(String, long)} - with markdown</li>
|
||||
* <li>{@link DefaultMessageSender#send(String, long)} - without markdown</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public class DefaultMessageSender implements MessageSender {
|
||||
private static final String TAG = MessageSender.class.getName();
|
||||
|
||||
private DefaultAbsSender bot;
|
||||
|
||||
public DefaultMessageSender(DefaultAbsSender bot) {
|
||||
this.bot = bot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Message> send(String message, long id) {
|
||||
return doSendMessage(message, id, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Message> sendMd(String message, long id) {
|
||||
return doSendMessage(message, id, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Message> forceReply(String message, long id) {
|
||||
SendMessage msg = new SendMessage();
|
||||
msg.setText(message);
|
||||
msg.setChatId(id);
|
||||
msg.setReplyMarkup(new ForceReplyKeyboard());
|
||||
|
||||
return optionalSendMessage(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean answerInlineQuery(AnswerInlineQuery answerInlineQuery) throws TelegramApiException {
|
||||
return bot.answerInlineQuery(answerInlineQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean sendChatAction(SendChatAction sendChatAction) throws TelegramApiException {
|
||||
return bot.sendChatAction(sendChatAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message forwardMessage(ForwardMessage forwardMessage) throws TelegramApiException {
|
||||
return bot.forwardMessage(forwardMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendLocation(SendLocation sendLocation) throws TelegramApiException {
|
||||
return bot.sendLocation(sendLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendVenue(SendVenue sendVenue) throws TelegramApiException {
|
||||
return bot.sendVenue(sendVenue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendContact(SendContact sendContact) throws TelegramApiException {
|
||||
return bot.sendContact(sendContact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean kickMember(KickChatMember kickChatMember) throws TelegramApiException {
|
||||
return bot.kickMember(kickChatMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean unbanMember(UnbanChatMember unbanChatMember) throws TelegramApiException {
|
||||
return bot.unbanMember(unbanChatMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean leaveChat(LeaveChat leaveChat) throws TelegramApiException {
|
||||
return bot.leaveChat(leaveChat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Chat getChat(GetChat getChat) throws TelegramApiException {
|
||||
return bot.getChat(getChat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMember> getChatAdministrators(GetChatAdministrators getChatAdministrators) throws TelegramApiException {
|
||||
return bot.getChatAdministrators(getChatAdministrators);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatMember getChatMember(GetChatMember getChatMember) throws TelegramApiException {
|
||||
return bot.getChatMember(getChatMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getChatMemberCount(GetChatMemberCount getChatMemberCount) throws TelegramApiException {
|
||||
return bot.getChatMemberCount(getChatMemberCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean setChatPhoto(SetChatPhoto setChatPhoto) throws TelegramApiException {
|
||||
return bot.setChatPhoto(setChatPhoto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deleteChatPhoto(DeleteChatPhoto deleteChatPhoto) throws TelegramApiException {
|
||||
return bot.deleteChatPhoto(deleteChatPhoto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChatPhoto(DeleteChatPhoto deleteChatPhoto, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.deleteChatPhoto(deleteChatPhoto, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean pinChatMessage(PinChatMessage pinChatMessage) throws TelegramApiException {
|
||||
return bot.pinChatMessage(pinChatMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pinChatMessage(PinChatMessage pinChatMessage, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.pinChatMessage(pinChatMessage, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean unpinChatMessage(UnpinChatMessage unpinChatMessage) throws TelegramApiException {
|
||||
return bot.unpinChatMessage(unpinChatMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unpinChatMessage(UnpinChatMessage unpinChatMessage, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.unpinChatMessage(unpinChatMessage, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean promoteChatMember(PromoteChatMember promoteChatMember) throws TelegramApiException {
|
||||
return bot.promoteChatMember(promoteChatMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void promoteChatMember(PromoteChatMember promoteChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.promoteChatMember(promoteChatMember, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean restrictChatMember(RestrictChatMember restrictChatMember) throws TelegramApiException {
|
||||
return bot.restrictChatMember(restrictChatMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restrictChatMember(RestrictChatMember restrictChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.restrictChatMember(restrictChatMember, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean setChatDescription(SetChatDescription setChatDescription) throws TelegramApiException {
|
||||
return bot.setChatDescription(setChatDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChatDescription(SetChatDescription setChatDescription, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.setChatDescription(setChatDescription, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean setChatTite(SetChatTitle setChatTitle) throws TelegramApiException {
|
||||
return bot.setChatTitle(setChatTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChatTite(SetChatTitle setChatTitle, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.setChatTitle(setChatTitle, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String exportChatInviteLink(ExportChatInviteLink exportChatInviteLink) throws TelegramApiException {
|
||||
return bot.exportChatInviteLink(exportChatInviteLink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportChatInviteLinkAsync(ExportChatInviteLink exportChatInviteLink, SentCallback<String> sentCallback) throws TelegramApiException {
|
||||
bot.exportChatInviteLinkAsync(exportChatInviteLink, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deleteMessage(DeleteMessage deleteMessage) throws TelegramApiException {
|
||||
return bot.deleteMessage(deleteMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMessageAsync(DeleteMessage deleteMessage, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.deleteMessage(deleteMessage, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Serializable editMessageText(EditMessageText editMessageText) throws TelegramApiException {
|
||||
return bot.editMessageText(editMessageText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Serializable editMessageCaption(EditMessageCaption editMessageCaption) throws TelegramApiException {
|
||||
return bot.editMessageCaption(editMessageCaption);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Serializable editMessageReplyMarkup(EditMessageReplyMarkup editMessageReplyMarkup) throws TelegramApiException {
|
||||
return bot.editMessageReplyMarkup(editMessageReplyMarkup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean answerCallbackQuery(AnswerCallbackQuery answerCallbackQuery) throws TelegramApiException {
|
||||
return bot.answerCallbackQuery(answerCallbackQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfilePhotos getUserProfilePhotos(GetUserProfilePhotos getUserProfilePhotos) throws TelegramApiException {
|
||||
return bot.getUserProfilePhotos(getUserProfilePhotos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.File downloadFile(String path) throws TelegramApiException {
|
||||
return bot.downloadFile(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadFileAsync(String path, DownloadFileCallback<String> callback) throws TelegramApiException {
|
||||
bot.downloadFileAsync(path, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.File downloadFile(File file) throws TelegramApiException {
|
||||
return bot.downloadFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadFileAsync(File file, DownloadFileCallback<File> callback) throws TelegramApiException {
|
||||
bot.downloadFileAsync(file, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile(GetFile getFile) throws TelegramApiException {
|
||||
return bot.getFile(getFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getMe() throws TelegramApiException {
|
||||
return bot.getMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebhookInfo getWebhookInfo() throws TelegramApiException {
|
||||
return bot.getWebhookInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Serializable setGameScore(SetGameScore setGameScore) throws TelegramApiException {
|
||||
return bot.setGameScore(setGameScore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Serializable getGameHighScores(GetGameHighScores getGameHighScores) throws TelegramApiException {
|
||||
return bot.getGameHighScores(getGameHighScores);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendGame(SendGame sendGame) throws TelegramApiException {
|
||||
return bot.sendGame(sendGame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deleteWebhook(DeleteWebhook deleteWebhook) throws TelegramApiException {
|
||||
return bot.deleteWebhook(deleteWebhook);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendMessage(SendMessage sendMessage) throws TelegramApiException {
|
||||
return bot.sendMessage(sendMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessageAsync(SendMessage sendMessage, SentCallback<Message> sentCallback) throws TelegramApiException {
|
||||
bot.sendMessageAsync(sendMessage, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void answerInlineQueryAsync(AnswerInlineQuery answerInlineQuery, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.answerInlineQueryAsync(answerInlineQuery, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendChatActionAsync(SendChatAction sendChatAction, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.sendChatActionAsync(sendChatAction, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forwardMessageAsync(ForwardMessage forwardMessage, SentCallback<Message> sentCallback) throws TelegramApiException {
|
||||
bot.forwardMessageAsync(forwardMessage, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLocationAsync(SendLocation sendLocation, SentCallback<Message> sentCallback) throws TelegramApiException {
|
||||
bot.sendLocationAsync(sendLocation, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendVenueAsync(SendVenue sendVenue, SentCallback<Message> sentCallback) throws TelegramApiException {
|
||||
bot.sendVenueAsync(sendVenue, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendContactAsync(SendContact sendContact, SentCallback<Message> sentCallback) throws TelegramApiException {
|
||||
bot.sendContactAsync(sendContact, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kickMemberAsync(KickChatMember kickChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.kickMemberAsync(kickChatMember, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbanMemberAsync(UnbanChatMember unbanChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.unbanMemberAsync(unbanChatMember, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void leaveChatAsync(LeaveChat leaveChat, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.leaveChatAsync(leaveChat, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getChatAsync(GetChat getChat, SentCallback<Chat> sentCallback) throws TelegramApiException {
|
||||
bot.getChatAsync(getChat, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getChatAdministratorsAsync(GetChatAdministrators getChatAdministrators, SentCallback<ArrayList<ChatMember>> sentCallback) throws TelegramApiException {
|
||||
bot.getChatAdministratorsAsync(getChatAdministrators, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getChatMemberAsync(GetChatMember getChatMember, SentCallback<ChatMember> sentCallback) throws TelegramApiException {
|
||||
bot.getChatMemberAsync(getChatMember, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getChatMemberCountAsync(GetChatMemberCount getChatMemberCount, SentCallback<Integer> sentCallback) throws TelegramApiException {
|
||||
bot.getChatMemberCountAsync(getChatMemberCount, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void editMessageTextAsync(EditMessageText editMessageText, SentCallback<Serializable> sentCallback) throws TelegramApiException {
|
||||
bot.editMessageTextAsync(editMessageText, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void editMessageCaptionAsync(EditMessageCaption editMessageCaption, SentCallback<Serializable> sentCallback) throws TelegramApiException {
|
||||
bot.editMessageCaptionAsync(editMessageCaption, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void editMessageReplyMarkup(EditMessageReplyMarkup editMessageReplyMarkup, SentCallback<Serializable> sentCallback) throws TelegramApiException {
|
||||
bot.editMessageReplyMarkup(editMessageReplyMarkup, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void answerCallbackQueryAsync(AnswerCallbackQuery answerCallbackQuery, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.answerCallbackQueryAsync(answerCallbackQuery, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getUserProfilePhotosAsync(GetUserProfilePhotos getUserProfilePhotos, SentCallback<UserProfilePhotos> sentCallback) throws TelegramApiException {
|
||||
bot.getUserProfilePhotosAsync(getUserProfilePhotos, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getFileAsync(GetFile getFile, SentCallback<File> sentCallback) throws TelegramApiException {
|
||||
bot.getFileAsync(getFile, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getMeAsync(SentCallback<User> sentCallback) throws TelegramApiException {
|
||||
bot.getMeAsync(sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getWebhookInfoAsync(SentCallback<WebhookInfo> sentCallback) throws TelegramApiException {
|
||||
bot.getWebhookInfoAsync(sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGameScoreAsync(SetGameScore setGameScore, SentCallback<Serializable> sentCallback) throws TelegramApiException {
|
||||
bot.setGameScoreAsync(setGameScore, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getGameHighScoresAsync(GetGameHighScores getGameHighScores, SentCallback<ArrayList<GameHighScore>> sentCallback) throws TelegramApiException {
|
||||
bot.getGameHighScoresAsync(getGameHighScores, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendGameAsync(SendGame sendGame, SentCallback<Message> sentCallback) throws TelegramApiException {
|
||||
bot.sendGameAsync(sendGame, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteWebhook(DeleteWebhook deleteWebhook, SentCallback<Boolean> sentCallback) throws TelegramApiException {
|
||||
bot.deleteWebhook(deleteWebhook, sentCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendDocument(SendDocument sendDocument) throws TelegramApiException {
|
||||
return bot.sendDocument(sendDocument);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendPhoto(SendPhoto sendPhoto) throws TelegramApiException {
|
||||
return bot.sendPhoto(sendPhoto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendVideo(SendVideo sendVideo) throws TelegramApiException {
|
||||
return bot.sendVideo(sendVideo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendSticker(SendSticker sendSticker) throws TelegramApiException {
|
||||
return bot.sendSticker(sendSticker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendAudio(SendAudio sendAudio) throws TelegramApiException {
|
||||
return bot.sendAudio(sendAudio);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendVoice(SendVoice sendVoice) throws TelegramApiException {
|
||||
return bot.sendVoice(sendVoice);
|
||||
}
|
||||
|
||||
private Optional<Message> doSendMessage(String txt, long groupId, boolean format) {
|
||||
SendMessage smsg = new SendMessage();
|
||||
smsg.setChatId(groupId);
|
||||
smsg.setText(txt);
|
||||
smsg.enableMarkdown(format);
|
||||
|
||||
return optionalSendMessage(smsg);
|
||||
}
|
||||
|
||||
private Optional<Message> optionalSendMessage(SendMessage smsg) {
|
||||
try {
|
||||
return ofNullable(sendMessage(smsg));
|
||||
} catch (TelegramApiException e) {
|
||||
BotLogger.error("Could not send message", TAG, e);
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
package org.telegram.abilitybots.api.sender;
|
||||
|
||||
import org.telegram.telegrambots.api.methods.*;
|
||||
import org.telegram.telegrambots.api.methods.games.GetGameHighScores;
|
||||
import org.telegram.telegrambots.api.methods.games.SetGameScore;
|
||||
import org.telegram.telegrambots.api.methods.groupadministration.*;
|
||||
import org.telegram.telegrambots.api.methods.pinnedmessages.PinChatMessage;
|
||||
import org.telegram.telegrambots.api.methods.pinnedmessages.UnpinChatMessage;
|
||||
import org.telegram.telegrambots.api.methods.send.*;
|
||||
import org.telegram.telegrambots.api.methods.updates.DeleteWebhook;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.DeleteMessage;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageCaption;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageReplyMarkup;
|
||||
import org.telegram.telegrambots.api.methods.updatingmessages.EditMessageText;
|
||||
import org.telegram.telegrambots.api.objects.*;
|
||||
import org.telegram.telegrambots.api.objects.games.GameHighScore;
|
||||
import org.telegram.telegrambots.bots.DefaultAbsSender;
|
||||
import org.telegram.telegrambots.exceptions.TelegramApiException;
|
||||
import org.telegram.telegrambots.updateshandlers.DownloadFileCallback;
|
||||
import org.telegram.telegrambots.updateshandlers.SentCallback;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A sender interface that replicates {@link DefaultAbsSender} methods.
|
||||
*
|
||||
* @author Abbas Abou Daya
|
||||
*/
|
||||
public interface MessageSender {
|
||||
Optional<Message> send(String message, long id);
|
||||
|
||||
Optional<Message> sendMd(String message, long id);
|
||||
|
||||
Optional<Message> forceReply(String message, long id);
|
||||
|
||||
Boolean answerInlineQuery(AnswerInlineQuery answerInlineQuery) throws TelegramApiException;
|
||||
|
||||
Boolean sendChatAction(SendChatAction sendChatAction) throws TelegramApiException;
|
||||
|
||||
Message forwardMessage(ForwardMessage forwardMessage) throws TelegramApiException;
|
||||
|
||||
Message sendLocation(SendLocation sendLocation) throws TelegramApiException;
|
||||
|
||||
Message sendVenue(SendVenue sendVenue) throws TelegramApiException;
|
||||
|
||||
Message sendContact(SendContact sendContact) throws TelegramApiException;
|
||||
|
||||
Boolean kickMember(KickChatMember kickChatMember) throws TelegramApiException;
|
||||
|
||||
Boolean unbanMember(UnbanChatMember unbanChatMember) throws TelegramApiException;
|
||||
|
||||
Boolean leaveChat(LeaveChat leaveChat) throws TelegramApiException;
|
||||
|
||||
Chat getChat(GetChat getChat) throws TelegramApiException;
|
||||
|
||||
List<ChatMember> getChatAdministrators(GetChatAdministrators getChatAdministrators) throws TelegramApiException;
|
||||
|
||||
ChatMember getChatMember(GetChatMember getChatMember) throws TelegramApiException;
|
||||
|
||||
Integer getChatMemberCount(GetChatMemberCount getChatMemberCount) throws TelegramApiException;
|
||||
|
||||
Boolean setChatPhoto(SetChatPhoto setChatPhoto) throws TelegramApiException;
|
||||
|
||||
Boolean deleteChatPhoto(DeleteChatPhoto deleteChatPhoto) throws TelegramApiException;
|
||||
|
||||
void deleteChatPhoto(DeleteChatPhoto deleteChatPhoto, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Boolean pinChatMessage(PinChatMessage pinChatMessage) throws TelegramApiException;
|
||||
|
||||
void pinChatMessage(PinChatMessage pinChatMessage, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Boolean unpinChatMessage(UnpinChatMessage unpinChatMessage) throws TelegramApiException;
|
||||
|
||||
void unpinChatMessage(UnpinChatMessage unpinChatMessage, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Boolean promoteChatMember(PromoteChatMember promoteChatMember) throws TelegramApiException;
|
||||
|
||||
void promoteChatMember(PromoteChatMember promoteChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Boolean restrictChatMember(RestrictChatMember restrictChatMember) throws TelegramApiException;
|
||||
|
||||
void restrictChatMember(RestrictChatMember restrictChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Boolean setChatDescription(SetChatDescription setChatDescription) throws TelegramApiException;
|
||||
|
||||
void setChatDescription(SetChatDescription setChatDescription, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Boolean setChatTite(SetChatTitle setChatTitle) throws TelegramApiException;
|
||||
|
||||
void setChatTite(SetChatTitle setChatTitle, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
String exportChatInviteLink(ExportChatInviteLink exportChatInviteLink) throws TelegramApiException;
|
||||
|
||||
void exportChatInviteLinkAsync(ExportChatInviteLink exportChatInviteLink, SentCallback<String> sentCallback) throws TelegramApiException;
|
||||
|
||||
Boolean deleteMessage(DeleteMessage deleteMessage) throws TelegramApiException;
|
||||
|
||||
void deleteMessageAsync(DeleteMessage deleteMessage, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Serializable editMessageText(EditMessageText editMessageText) throws TelegramApiException;
|
||||
|
||||
Serializable editMessageCaption(EditMessageCaption editMessageCaption) throws TelegramApiException;
|
||||
|
||||
Serializable editMessageReplyMarkup(EditMessageReplyMarkup editMessageReplyMarkup) throws TelegramApiException;
|
||||
|
||||
Boolean answerCallbackQuery(AnswerCallbackQuery answerCallbackQuery) throws TelegramApiException;
|
||||
|
||||
UserProfilePhotos getUserProfilePhotos(GetUserProfilePhotos getUserProfilePhotos) throws TelegramApiException;
|
||||
|
||||
java.io.File downloadFile(String path) throws TelegramApiException;
|
||||
|
||||
void downloadFileAsync(String path, DownloadFileCallback<String> callback) throws TelegramApiException;
|
||||
|
||||
java.io.File downloadFile(File file) throws TelegramApiException;
|
||||
|
||||
void downloadFileAsync(File file, DownloadFileCallback<File> callback) throws TelegramApiException;
|
||||
|
||||
File getFile(GetFile getFile) throws TelegramApiException;
|
||||
|
||||
User getMe() throws TelegramApiException;
|
||||
|
||||
WebhookInfo getWebhookInfo() throws TelegramApiException;
|
||||
|
||||
Serializable setGameScore(SetGameScore setGameScore) throws TelegramApiException;
|
||||
|
||||
Serializable getGameHighScores(GetGameHighScores getGameHighScores) throws TelegramApiException;
|
||||
|
||||
Message sendGame(SendGame sendGame) throws TelegramApiException;
|
||||
|
||||
Boolean deleteWebhook(DeleteWebhook deleteWebhook) throws TelegramApiException;
|
||||
|
||||
Message sendMessage(SendMessage sendMessage) throws TelegramApiException;
|
||||
|
||||
void sendMessageAsync(SendMessage sendMessage, SentCallback<Message> sentCallback) throws TelegramApiException;
|
||||
|
||||
void answerInlineQueryAsync(AnswerInlineQuery answerInlineQuery, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
void sendChatActionAsync(SendChatAction sendChatAction, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
void forwardMessageAsync(ForwardMessage forwardMessage, SentCallback<Message> sentCallback) throws TelegramApiException;
|
||||
|
||||
void sendLocationAsync(SendLocation sendLocation, SentCallback<Message> sentCallback) throws TelegramApiException;
|
||||
|
||||
void sendVenueAsync(SendVenue sendVenue, SentCallback<Message> sentCallback) throws TelegramApiException;
|
||||
|
||||
void sendContactAsync(SendContact sendContact, SentCallback<Message> sentCallback) throws TelegramApiException;
|
||||
|
||||
void kickMemberAsync(KickChatMember kickChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
void unbanMemberAsync(UnbanChatMember unbanChatMember, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
void leaveChatAsync(LeaveChat leaveChat, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getChatAsync(GetChat getChat, SentCallback<Chat> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getChatAdministratorsAsync(GetChatAdministrators getChatAdministrators, SentCallback<ArrayList<ChatMember>> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getChatMemberAsync(GetChatMember getChatMember, SentCallback<ChatMember> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getChatMemberCountAsync(GetChatMemberCount getChatMemberCount, SentCallback<Integer> sentCallback) throws TelegramApiException;
|
||||
|
||||
void editMessageTextAsync(EditMessageText editMessageText, SentCallback<Serializable> sentCallback) throws TelegramApiException;
|
||||
|
||||
void editMessageCaptionAsync(EditMessageCaption editMessageCaption, SentCallback<Serializable> sentCallback) throws TelegramApiException;
|
||||
|
||||
void editMessageReplyMarkup(EditMessageReplyMarkup editMessageReplyMarkup, SentCallback<Serializable> sentCallback) throws TelegramApiException;
|
||||
|
||||
void answerCallbackQueryAsync(AnswerCallbackQuery answerCallbackQuery, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getUserProfilePhotosAsync(GetUserProfilePhotos getUserProfilePhotos, SentCallback<UserProfilePhotos> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getFileAsync(GetFile getFile, SentCallback<File> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getMeAsync(SentCallback<User> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getWebhookInfoAsync(SentCallback<WebhookInfo> sentCallback) throws TelegramApiException;
|
||||
|
||||
void setGameScoreAsync(SetGameScore setGameScore, SentCallback<Serializable> sentCallback) throws TelegramApiException;
|
||||
|
||||
void getGameHighScoresAsync(GetGameHighScores getGameHighScores, SentCallback<ArrayList<GameHighScore>> sentCallback) throws TelegramApiException;
|
||||
|
||||
void sendGameAsync(SendGame sendGame, SentCallback<Message> sentCallback) throws TelegramApiException;
|
||||
|
||||
void deleteWebhook(DeleteWebhook deleteWebhook, SentCallback<Boolean> sentCallback) throws TelegramApiException;
|
||||
|
||||
Message sendDocument(SendDocument sendDocument) throws TelegramApiException;
|
||||
|
||||
Message sendPhoto(SendPhoto sendPhoto) throws TelegramApiException;
|
||||
|
||||
Message sendVideo(SendVideo sendVideo) throws TelegramApiException;
|
||||
|
||||
Message sendSticker(SendSticker sendSticker) throws TelegramApiException;
|
||||
|
||||
Message sendAudio(SendAudio sendAudio) throws TelegramApiException;
|
||||
|
||||
Message sendVoice(SendVoice sendVoice) throws TelegramApiException;
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package org.telegram.abilitybots.api.util;
|
||||
|
||||
import org.telegram.abilitybots.api.db.DBContext;
|
||||
import org.telegram.abilitybots.api.objects.MessageContext;
|
||||
import org.telegram.telegrambots.api.objects.Update;
|
||||
import org.telegram.telegrambots.api.objects.User;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.telegram.abilitybots.api.objects.Flag.*;
|
||||
|
||||
/**
|
||||
* Helper and utility methods
|
||||
*/
|
||||
public final class AbilityUtils {
|
||||
private AbilityUtils() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param username any username
|
||||
* @return the username with the preceding "@" stripped off
|
||||
*/
|
||||
public static String stripTag(String username) {
|
||||
String lowerCase = username.toLowerCase();
|
||||
return lowerCase.startsWith("@") ? lowerCase.substring(1, lowerCase.length()) : lowerCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits to DB.
|
||||
*
|
||||
* @param db the database to commit on
|
||||
* @return a lambda consumer that takes in a {@link MessageContext}, used in post actions for abilities
|
||||
*/
|
||||
public static Consumer<MessageContext> commitTo(DBContext db) {
|
||||
return ctx -> db.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the user who caused the update.
|
||||
*
|
||||
* @param update a Telegram {@link Update}
|
||||
* @return the originating user
|
||||
* @throws IllegalStateException if the user could not be found
|
||||
*/
|
||||
public static User getUser(Update update) {
|
||||
if (MESSAGE.test(update)) {
|
||||
return update.getMessage().getFrom();
|
||||
} else if (CALLBACK_QUERY.test(update)) {
|
||||
return update.getCallbackQuery().getFrom();
|
||||
} else if (INLINE_QUERY.test(update)) {
|
||||
return update.getInlineQuery().getFrom();
|
||||
} else if (CHANNEL_POST.test(update)) {
|
||||
return update.getChannelPost().getFrom();
|
||||
} else if (EDITED_CHANNEL_POST.test(update)) {
|
||||
return update.getEditedChannelPost().getFrom();
|
||||
} else if (EDITED_MESSAGE.test(update)) {
|
||||
return update.getEditedMessage().getFrom();
|
||||
} else if (CHOSEN_INLINE_QUERY.test(update)) {
|
||||
return update.getChosenInlineQuery().getFrom();
|
||||
} else {
|
||||
throw new IllegalStateException("Could not retrieve originating user from update");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the direct chat ID of the specified update.
|
||||
*
|
||||
* @param update a Telegram {@link Update}
|
||||
* @return the originating chat ID
|
||||
* @throws IllegalStateException if the chat ID could not be found
|
||||
*/
|
||||
public static Long getChatId(Update update) {
|
||||
if (MESSAGE.test(update)) {
|
||||
return update.getMessage().getChatId();
|
||||
} else if (CALLBACK_QUERY.test(update)) {
|
||||
return update.getCallbackQuery().getMessage().getChatId();
|
||||
} else if (INLINE_QUERY.test(update)) {
|
||||
return (long) update.getInlineQuery().getFrom().getId();
|
||||
} else if (CHANNEL_POST.test(update)) {
|
||||
return update.getChannelPost().getChatId();
|
||||
} else if (EDITED_CHANNEL_POST.test(update)) {
|
||||
return update.getEditedChannelPost().getChatId();
|
||||
} else if (EDITED_MESSAGE.test(update)) {
|
||||
return update.getEditedMessage().getChatId();
|
||||
} else if (CHOSEN_INLINE_QUERY.test(update)) {
|
||||
return (long) update.getChosenInlineQuery().getFrom().getId();
|
||||
} else {
|
||||
throw new IllegalStateException("Could not retrieve originating chat ID from update");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param update a Telegram {@link Update}
|
||||
* @return <tt>true</tt> if the update contains contains a private user message
|
||||
*/
|
||||
public static boolean isUserMessage(Update update) {
|
||||
if (MESSAGE.test(update)) {
|
||||
return update.getMessage().isUserMessage();
|
||||
} else if (CALLBACK_QUERY.test(update)) {
|
||||
return update.getCallbackQuery().getMessage().isUserMessage();
|
||||
} else if (CHANNEL_POST.test(update)) {
|
||||
return update.getChannelPost().isUserMessage();
|
||||
} else if (EDITED_CHANNEL_POST.test(update)) {
|
||||
return update.getEditedChannelPost().isUserMessage();
|
||||
} else if (EDITED_MESSAGE.test(update)) {
|
||||
return update.getEditedMessage().isUserMessage();
|
||||
} else if (CHOSEN_INLINE_QUERY.test(update) || INLINE_QUERY.test(update)) {
|
||||
return true;
|
||||
} else {
|
||||
throw new IllegalStateException("Could not retrieve update context origin (user/group)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param username the username to add the tag to
|
||||
* @return the username prefixed with the "@" tag.
|
||||
*/
|
||||
public static String addTag(String username) {
|
||||
return "@" + username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param msg the message to be replied to
|
||||
* @return a predicate that asserts that the update is a reply to the specified message.
|
||||
*/
|
||||
public static Predicate<Update> isReplyTo(String msg) {
|
||||
return update -> update.getMessage().getReplyToMessage().getText().equals(msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package org.telegram.abilitybots.api.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Pair<A, B> {
|
||||
@JsonProperty("a")
|
||||
private final A a;
|
||||
@JsonProperty("b")
|
||||
private final B b;
|
||||
|
||||
private Pair(A a, B b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static <A, B> Pair<A, B> of(@JsonProperty("a") A a, @JsonProperty("b") B b) {
|
||||
return new Pair<>(a, b);
|
||||
}
|
||||
|
||||
public A a() {
|
||||
return a;
|
||||
}
|
||||
|
||||
public B b() {
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Pair<?, ?> pair = (Pair<?, ?>) o;
|
||||
return Objects.equals(a, pair.a) &&
|
||||
Objects.equals(b, pair.b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(a, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("a", a)
|
||||
.add("b", b)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package org.telegram.abilitybots.api.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.MoreObjects;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Trio<A, B, C> {
|
||||
@JsonProperty("a")
|
||||
private final A a;
|
||||
@JsonProperty("b")
|
||||
private final B b;
|
||||
@JsonProperty("c")
|
||||
private final C c;
|
||||
|
||||
private Trio(A a, B b, C c) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
this.c = c;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static <A, B, C> Trio<A, B, C> of(@JsonProperty("a") A a, @JsonProperty("b") B b, @JsonProperty("c") C c) {
|
||||
return new Trio<>(a, b, c);
|
||||
}
|
||||
|
||||
public A a() {
|
||||
return a;
|
||||
}
|
||||
|
||||
public B b() {
|
||||
return b;
|
||||
}
|
||||
|
||||
public C c() {
|
||||
return c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Trio<?, ?, ?> trio = (Trio<?, ?, ?>) o;
|
||||
return Objects.equals(a, trio.a) &&
|
||||
Objects.equals(b, trio.b) &&
|
||||
Objects.equals(c, trio.c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(a, b, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("a", a)
|
||||
.add("b", b)
|
||||
.add("c", c)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,589 @@
|
||||
package org.telegram.abilitybots.api.bot;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.Files;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Matchers;
|
||||
import org.telegram.abilitybots.api.db.DBContext;
|
||||
import org.telegram.abilitybots.api.objects.*;
|
||||
import org.telegram.abilitybots.api.sender.MessageSender;
|
||||
import org.telegram.abilitybots.api.util.Pair;
|
||||
import org.telegram.abilitybots.api.util.Trio;
|
||||
import org.telegram.telegrambots.api.objects.*;
|
||||
import org.telegram.telegrambots.exceptions.TelegramApiException;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.google.common.collect.Sets.newHashSet;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.apache.commons.lang3.ArrayUtils.addAll;
|
||||
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.internal.verification.VerificationModeFactory.times;
|
||||
import static org.telegram.abilitybots.api.bot.AbilityBot.RECOVERY_MESSAGE;
|
||||
import static org.telegram.abilitybots.api.bot.AbilityBot.RECOVER_SUCCESS;
|
||||
import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder;
|
||||
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
||||
import static org.telegram.abilitybots.api.objects.EndUser.endUser;
|
||||
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.Locality.ALL;
|
||||
import static org.telegram.abilitybots.api.objects.Locality.GROUP;
|
||||
import static org.telegram.abilitybots.api.objects.MessageContext.newContext;
|
||||
import static org.telegram.abilitybots.api.objects.Privacy.ADMIN;
|
||||
import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC;
|
||||
|
||||
public class AbilityBotTest {
|
||||
public static final String[] EMPTY_ARRAY = {};
|
||||
public static final long GROUP_ID = 10L;
|
||||
public static final String TEST = "test";
|
||||
public static final String[] TEXT = {TEST};
|
||||
public static final EndUser MUSER = endUser(1, "first", "last", "username");
|
||||
public static final EndUser CREATOR = endUser(1337, "creatorFirst", "creatorLast", "creatorUsername");
|
||||
|
||||
private DefaultBot bot;
|
||||
private DBContext db;
|
||||
private MessageSender sender;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
db = offlineInstance("db");
|
||||
bot = new DefaultBot(EMPTY, EMPTY, db);
|
||||
sender = mock(MessageSender.class);
|
||||
bot.setSender(sender);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendsPrivacyViolation() {
|
||||
Update update = mockFullUpdate(MUSER, "/admin");
|
||||
|
||||
bot.onUpdateReceived(update);
|
||||
|
||||
verify(sender, times(1)).send(format("Sorry, %s-only feature.", "admin"), MUSER.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendsLocalityViolation() {
|
||||
Update update = mockFullUpdate(MUSER, "/group");
|
||||
|
||||
bot.onUpdateReceived(update);
|
||||
|
||||
verify(sender, times(1)).send(format("Sorry, %s-only feature.", "group"), MUSER.id());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendsInputArgsViolation() {
|
||||
Update update = mockFullUpdate(MUSER, "/count 1 2 3");
|
||||
|
||||
bot.onUpdateReceived(update);
|
||||
|
||||
verify(sender, times(1)).send(format("Sorry, this feature requires %d additional inputs.", 4), MUSER.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canProcessRepliesIfSatisfyRequirements() {
|
||||
Update update = mockFullUpdate(MUSER, "must reply");
|
||||
|
||||
// False means the update was not pushed down the stream since it has been consumed by the reply
|
||||
assertFalse(bot.filterReply(update));
|
||||
verify(sender, times(1)).send("reply", MUSER.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canBackupDB() throws TelegramApiException {
|
||||
MessageContext context = defaultContext();
|
||||
|
||||
bot.backupDB().action().accept(context);
|
||||
|
||||
verify(sender, times(1)).sendDocument(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canRecoverDB() throws TelegramApiException, IOException {
|
||||
Update update = mockBackupUpdate();
|
||||
Object backup = getDbBackup();
|
||||
java.io.File backupFile = createBackupFile(backup);
|
||||
|
||||
when(sender.downloadFile(Matchers.any(File.class))).thenReturn(backupFile);
|
||||
bot.recoverDB().replies().get(0).actOn(update);
|
||||
|
||||
verify(sender, times(1)).send(RECOVER_SUCCESS, GROUP_ID);
|
||||
assertEquals("Bot recovered but the DB is still not in sync", db.getSet(TEST), newHashSet(TEST));
|
||||
assertTrue("Could not delete backup file", backupFile.delete());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canFilterOutReplies() {
|
||||
Update update = mock(Update.class);
|
||||
when(update.hasMessage()).thenReturn(false);
|
||||
|
||||
assertTrue(bot.filterReply(update));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canDemote() {
|
||||
addUsers(MUSER);
|
||||
bot.admins().add(MUSER.id());
|
||||
|
||||
MessageContext context = defaultContext();
|
||||
|
||||
bot.demoteAdmin().action().accept(context);
|
||||
|
||||
Set<Integer> actual = bot.admins();
|
||||
Set<Integer> expected = emptySet();
|
||||
assertEquals("Could not sudont super-admin", expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canPromote() {
|
||||
addUsers(MUSER);
|
||||
|
||||
MessageContext context = defaultContext();
|
||||
|
||||
bot.promoteAdmin().action().accept(context);
|
||||
|
||||
Set<Integer> actual = bot.admins();
|
||||
Set<Integer> expected = newHashSet(MUSER.id());
|
||||
assertEquals("Could not sudo user", expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canBanUser() {
|
||||
addUsers(MUSER);
|
||||
MessageContext context = defaultContext();
|
||||
|
||||
bot.banUser().action().accept(context);
|
||||
|
||||
Set<Integer> actual = bot.blacklist();
|
||||
Set<Integer> expected = newHashSet(MUSER.id());
|
||||
assertEquals("The ban was not emplaced", expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canUnbanUser() {
|
||||
addUsers(MUSER);
|
||||
bot.blacklist().add(MUSER.id());
|
||||
|
||||
MessageContext context = defaultContext();
|
||||
|
||||
bot.unbanUser().action().accept(context);
|
||||
|
||||
Set<Integer> actual = bot.blacklist();
|
||||
Set<Integer> expected = newHashSet();
|
||||
assertEquals("The ban was not lifted", expected, actual);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private MessageContext defaultContext() {
|
||||
MessageContext context = mock(MessageContext.class);
|
||||
when(context.user()).thenReturn(CREATOR);
|
||||
when(context.firstArg()).thenReturn(MUSER.username());
|
||||
return context;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cannotBanCreator() {
|
||||
addUsers(MUSER, CREATOR);
|
||||
MessageContext context = mock(MessageContext.class);
|
||||
when(context.user()).thenReturn(MUSER);
|
||||
when(context.firstArg()).thenReturn(CREATOR.username());
|
||||
|
||||
bot.banUser().action().accept(context);
|
||||
|
||||
Set<Integer> actual = bot.blacklist();
|
||||
Set<Integer> expected = newHashSet(MUSER.id());
|
||||
assertEquals("Impostor was not added to the blacklist", expected, actual);
|
||||
}
|
||||
|
||||
private void addUsers(EndUser... users) {
|
||||
Arrays.stream(users).forEach(user -> {
|
||||
bot.users().put(user.id(), user);
|
||||
bot.userIds().put(user.username().toLowerCase(), user.id());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creatorCanClaimBot() {
|
||||
MessageContext context = mock(MessageContext.class);
|
||||
when(context.user()).thenReturn(CREATOR);
|
||||
|
||||
bot.claimCreator().action().accept(context);
|
||||
|
||||
Set<Integer> actual = bot.admins();
|
||||
Set<Integer> expected = newHashSet(CREATOR.id());
|
||||
assertEquals("Creator was not properly added to the super admins set", expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userGetsBannedIfClaimsBot() {
|
||||
addUsers(MUSER);
|
||||
MessageContext context = mock(MessageContext.class);
|
||||
when(context.user()).thenReturn(MUSER);
|
||||
|
||||
bot.claimCreator().action().accept(context);
|
||||
|
||||
Set<Integer> actual = bot.blacklist();
|
||||
Set<Integer> expected = newHashSet(MUSER.id());
|
||||
assertEquals("Could not find user on the blacklist", expected, actual);
|
||||
|
||||
actual = bot.admins();
|
||||
expected = emptySet();
|
||||
assertEquals("Admins set is not empty", expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bannedCreatorPassesBlacklistCheck() {
|
||||
bot.blacklist().add(CREATOR.id());
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
User user = mock(User.class);
|
||||
|
||||
mockUser(update, message, user);
|
||||
|
||||
boolean notBanned = bot.checkBlacklist(update);
|
||||
assertTrue("Creator is banned", notBanned);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canAddUser() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
User user = mock(User.class);
|
||||
|
||||
mockAlternateUser(update, message, user, MUSER);
|
||||
|
||||
bot.addUser(update);
|
||||
|
||||
Map<String, Integer> expectedUserIds = ImmutableMap.of(MUSER.username(), MUSER.id());
|
||||
Map<Integer, EndUser> expectedUsers = ImmutableMap.of(MUSER.id(), MUSER);
|
||||
assertEquals("User was not added", expectedUserIds, bot.userIds());
|
||||
assertEquals("User was not added", expectedUsers, bot.users());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canEditUser() {
|
||||
addUsers(MUSER);
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
User user = mock(User.class);
|
||||
|
||||
String newUsername = MUSER.username() + "-test";
|
||||
String newFirstName = MUSER.firstName() + "-test";
|
||||
String newLastName = MUSER.lastName() + "-test";
|
||||
int sameId = MUSER.id();
|
||||
EndUser changedUser = endUser(sameId, newFirstName, newLastName, newUsername);
|
||||
|
||||
mockAlternateUser(update, message, user, changedUser);
|
||||
|
||||
bot.addUser(update);
|
||||
|
||||
Map<String, Integer> expectedUserIds = ImmutableMap.of(changedUser.username(), changedUser.id());
|
||||
Map<Integer, EndUser> expectedUsers = ImmutableMap.of(changedUser.id(), changedUser);
|
||||
assertEquals("User was not properly edited", bot.userIds(), expectedUserIds);
|
||||
assertEquals("User was not properly edited", expectedUsers, expectedUsers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canValidateAbility() {
|
||||
Trio<Update, Ability, String[]> invalidPair = Trio.of(null, null, null);
|
||||
Ability validAbility = getDefaultBuilder().build();
|
||||
Trio<Update, Ability, String[]> validPair = Trio.of(null, validAbility, null);
|
||||
|
||||
assertEquals("Bot can't validate ability properly", false, bot.validateAbility(invalidPair));
|
||||
assertEquals("Bot can't validate ability properly", true, bot.validateAbility(validPair));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canCheckInput() {
|
||||
Update update = mockFullUpdate(MUSER, "/something");
|
||||
Ability abilityWithOneInput = getDefaultBuilder()
|
||||
.build();
|
||||
Ability abilityWithZeroInput = getDefaultBuilder()
|
||||
.input(0)
|
||||
.build();
|
||||
|
||||
Trio<Update, Ability, String[]> trioOneArg = Trio.of(update, abilityWithOneInput, TEXT);
|
||||
Trio<Update, Ability, String[]> trioZeroArg = Trio.of(update, abilityWithZeroInput, TEXT);
|
||||
|
||||
assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioOneArg));
|
||||
|
||||
trioOneArg = Trio.of(update, abilityWithOneInput, addAll(TEXT, TEXT));
|
||||
assertEquals("Unexpected result when applying token filter", false, bot.checkInput(trioOneArg));
|
||||
|
||||
assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioZeroArg));
|
||||
|
||||
trioZeroArg = Trio.of(update, abilityWithZeroInput, EMPTY_ARRAY);
|
||||
assertEquals("Unexpected result when applying token filter", true, bot.checkInput(trioZeroArg));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canCheckPrivacy() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
org.telegram.telegrambots.api.objects.User user = mock(User.class);
|
||||
Ability publicAbility = getDefaultBuilder().privacy(PUBLIC).build();
|
||||
Ability adminAbility = getDefaultBuilder().privacy(ADMIN).build();
|
||||
Ability creatorAbility = getDefaultBuilder().privacy(Privacy.CREATOR).build();
|
||||
|
||||
Trio<Update, Ability, String[]> publicTrio = Trio.of(update, publicAbility, TEXT);
|
||||
Trio<Update, Ability, String[]> adminTrio = Trio.of(update, adminAbility, TEXT);
|
||||
Trio<Update, Ability, String[]> creatorTrio = Trio.of(update, creatorAbility, TEXT);
|
||||
|
||||
mockUser(update, message, user);
|
||||
|
||||
assertEquals("Unexpected result when checking for privacy", true, bot.checkPrivacy(publicTrio));
|
||||
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(adminTrio));
|
||||
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canBlockAdminsFromCreatorAbilities() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
org.telegram.telegrambots.api.objects.User user = mock(User.class);
|
||||
Ability creatorAbility = getDefaultBuilder().privacy(Privacy.CREATOR).build();
|
||||
|
||||
Trio<Update, Ability, String[]> creatorTrio = Trio.of(update, creatorAbility, TEXT);
|
||||
|
||||
bot.admins().add(MUSER.id());
|
||||
mockUser(update, message, user);
|
||||
|
||||
assertEquals("Unexpected result when checking for privacy", false, bot.checkPrivacy(creatorTrio));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canCheckLocality() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
User user = mock(User.class);
|
||||
Ability allAbility = getDefaultBuilder().locality(ALL).build();
|
||||
Ability userAbility = getDefaultBuilder().locality(Locality.USER).build();
|
||||
Ability groupAbility = getDefaultBuilder().locality(GROUP).build();
|
||||
|
||||
Trio<Update, Ability, String[]> publicTrio = Trio.of(update, allAbility, TEXT);
|
||||
Trio<Update, Ability, String[]> userTrio = Trio.of(update, userAbility, TEXT);
|
||||
Trio<Update, Ability, String[]> groupTrio = Trio.of(update, groupAbility, TEXT);
|
||||
|
||||
mockUser(update, message, user);
|
||||
when(message.isUserMessage()).thenReturn(true);
|
||||
|
||||
assertEquals("Unexpected result when checking for locality", true, bot.checkLocality(publicTrio));
|
||||
assertEquals("Unexpected result when checking for locality", true, bot.checkLocality(userTrio));
|
||||
assertEquals("Unexpected result when checking for locality", false, bot.checkLocality(groupTrio));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canRetrieveContext() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
User user = mock(User.class);
|
||||
Ability ability = getDefaultBuilder().build();
|
||||
Trio<Update, Ability, String[]> trio = Trio.of(update, ability, TEXT);
|
||||
|
||||
when(message.getChatId()).thenReturn(GROUP_ID);
|
||||
mockUser(update, message, user);
|
||||
|
||||
Pair<MessageContext, Ability> actualPair = bot.getContext(trio);
|
||||
Pair<MessageContext, Ability> expectedPair = Pair.of(newContext(update, MUSER, GROUP_ID, TEXT), ability);
|
||||
|
||||
assertEquals("Unexpected result when fetching for context", expectedPair, actualPair);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canCheckGlobalFlags() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
|
||||
when(update.hasMessage()).thenReturn(true);
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
assertEquals("Unexpected result when checking for locality", true, bot.checkGlobalFlags(update));
|
||||
}
|
||||
|
||||
@Test(expected = ArithmeticException.class)
|
||||
public void canConsumeUpdate() {
|
||||
Ability ability = getDefaultBuilder()
|
||||
.action((context) -> {
|
||||
int x = 1 / 0;
|
||||
}).build();
|
||||
MessageContext context = mock(MessageContext.class);
|
||||
|
||||
Pair<MessageContext, Ability> pair = Pair.of(context, ability);
|
||||
|
||||
bot.consumeUpdate(pair);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canFetchAbility() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
|
||||
String text = "/test";
|
||||
when(update.hasMessage()).thenReturn(true);
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
when(update.getMessage().hasText()).thenReturn(true);
|
||||
when(message.getText()).thenReturn(text);
|
||||
|
||||
Trio<Update, Ability, String[]> trio = bot.getAbility(update);
|
||||
|
||||
Ability expected = bot.testAbility();
|
||||
Ability actual = trio.b();
|
||||
|
||||
assertEquals("Wrong ability was fetched", expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canFetchDefaultAbility() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
|
||||
String text = "test tags";
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
when(message.getText()).thenReturn(text);
|
||||
|
||||
Trio<Update, Ability, String[]> trio = bot.getAbility(update);
|
||||
|
||||
Ability expected = bot.defaultAbility();
|
||||
Ability actual = trio.b();
|
||||
|
||||
assertEquals("Wrong ability was fetched", expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canCheckAbilityFlags() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
|
||||
when(update.hasMessage()).thenReturn(true);
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
when(message.hasDocument()).thenReturn(false);
|
||||
when(message.hasText()).thenReturn(true);
|
||||
|
||||
Ability documentAbility = getDefaultBuilder().flag(DOCUMENT, MESSAGE).build();
|
||||
Ability textAbility = getDefaultBuilder().flag(Flag.TEXT, MESSAGE).build();
|
||||
|
||||
Trio<Update, Ability, String[]> docTrio = Trio.of(update, documentAbility, TEXT);
|
||||
Trio<Update, Ability, String[]> textTrio = Trio.of(update, textAbility, TEXT);
|
||||
|
||||
assertEquals("Unexpected result when checking for message flags", false, bot.checkMessageFlags(docTrio));
|
||||
assertEquals("Unexpected result when checking for message flags", true, bot.checkMessageFlags(textTrio));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canReportCommands() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
|
||||
when(update.hasMessage()).thenReturn(true);
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
when(message.hasText()).thenReturn(true);
|
||||
MessageContext context = mock(MessageContext.class);
|
||||
when(context.chatId()).thenReturn(GROUP_ID);
|
||||
|
||||
bot.reportCommands().action().accept(context);
|
||||
|
||||
verify(sender, times(1)).send("default - dis iz default command", GROUP_ID);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws IOException {
|
||||
db.clear();
|
||||
db.close();
|
||||
}
|
||||
|
||||
private User mockUser(EndUser fromUser) {
|
||||
User user = mock(User.class);
|
||||
when(user.getId()).thenReturn(fromUser.id());
|
||||
when(user.getUserName()).thenReturn(fromUser.username());
|
||||
when(user.getFirstName()).thenReturn(fromUser.firstName());
|
||||
when(user.getLastName()).thenReturn(fromUser.lastName());
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Update mockFullUpdate(EndUser fromUser, String args) {
|
||||
bot.users().put(MUSER.id(), MUSER);
|
||||
bot.users().put(CREATOR.id(), CREATOR);
|
||||
bot.userIds().put(CREATOR.username(), CREATOR.id());
|
||||
bot.userIds().put(MUSER.username(), MUSER.id());
|
||||
|
||||
bot.admins().add(CREATOR.id());
|
||||
|
||||
User user = mockUser(fromUser);
|
||||
|
||||
Update update = mock(Update.class);
|
||||
when(update.hasMessage()).thenReturn(true);
|
||||
Message message = mock(Message.class);
|
||||
when(message.getFrom()).thenReturn(user);
|
||||
when(message.getText()).thenReturn(args);
|
||||
when(message.hasText()).thenReturn(true);
|
||||
when(message.isUserMessage()).thenReturn(true);
|
||||
when(message.getChatId()).thenReturn((long) fromUser.id());
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
return update;
|
||||
}
|
||||
|
||||
private void mockUser(Update update, Message message, User user) {
|
||||
when(update.hasMessage()).thenReturn(true);
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
when(message.getFrom()).thenReturn(user);
|
||||
when(user.getFirstName()).thenReturn(MUSER.firstName());
|
||||
when(user.getLastName()).thenReturn(MUSER.lastName());
|
||||
when(user.getId()).thenReturn(MUSER.id());
|
||||
when(user.getUserName()).thenReturn(MUSER.username());
|
||||
}
|
||||
|
||||
private void mockAlternateUser(Update update, Message message, User user, EndUser changedUser) {
|
||||
when(user.getId()).thenReturn(changedUser.id());
|
||||
when(user.getFirstName()).thenReturn(changedUser.firstName());
|
||||
when(user.getLastName()).thenReturn(changedUser.lastName());
|
||||
when(user.getUserName()).thenReturn(changedUser.username());
|
||||
when(message.getFrom()).thenReturn(user);
|
||||
when(update.hasMessage()).thenReturn(true);
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
}
|
||||
|
||||
private Update mockBackupUpdate() {
|
||||
Update update = mock(Update.class);
|
||||
Message message = mock(Message.class);
|
||||
Message botMessage = mock(Message.class);
|
||||
Document document = mock(Document.class);
|
||||
|
||||
when(update.getMessage()).thenReturn(message);
|
||||
when(message.getDocument()).thenReturn(document);
|
||||
when(botMessage.getText()).thenReturn(RECOVERY_MESSAGE);
|
||||
when(message.isReply()).thenReturn(true);
|
||||
when(message.hasDocument()).thenReturn(true);
|
||||
when(message.getReplyToMessage()).thenReturn(botMessage);
|
||||
when(message.getChatId()).thenReturn(GROUP_ID);
|
||||
return update;
|
||||
}
|
||||
|
||||
private Object getDbBackup() {
|
||||
db.getSet(TEST).add(TEST);
|
||||
Object backup = db.backup();
|
||||
db.clear();
|
||||
return backup;
|
||||
}
|
||||
|
||||
private java.io.File createBackupFile(Object backup) throws IOException {
|
||||
java.io.File backupFile = new java.io.File(TEST);
|
||||
BufferedWriter writer = Files.newWriter(backupFile, Charset.defaultCharset());
|
||||
writer.write(backup.toString());
|
||||
writer.flush();
|
||||
writer.close();
|
||||
return backupFile;
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package org.telegram.abilitybots.api.bot;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.telegram.abilitybots.api.db.DBContext;
|
||||
import org.telegram.abilitybots.api.objects.Ability;
|
||||
import org.telegram.abilitybots.api.objects.Ability.AbilityBuilder;
|
||||
import org.telegram.abilitybots.api.sender.MessageSender;
|
||||
|
||||
import static org.telegram.abilitybots.api.objects.Ability.builder;
|
||||
import static org.telegram.abilitybots.api.objects.Flag.CALLBACK_QUERY;
|
||||
import static org.telegram.abilitybots.api.objects.Flag.MESSAGE;
|
||||
import static org.telegram.abilitybots.api.objects.Locality.*;
|
||||
import static org.telegram.abilitybots.api.objects.Privacy.ADMIN;
|
||||
import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC;
|
||||
|
||||
public class DefaultBot extends AbilityBot {
|
||||
|
||||
public DefaultBot(String token, String username, DBContext db) {
|
||||
super(token, username, db);
|
||||
}
|
||||
|
||||
public static AbilityBuilder getDefaultBuilder() {
|
||||
return builder()
|
||||
.name("test")
|
||||
.privacy(PUBLIC)
|
||||
.locality(ALL)
|
||||
.input(1)
|
||||
.action(ctx -> {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int creatorId() {
|
||||
return 1337;
|
||||
}
|
||||
|
||||
public Ability defaultAbility() {
|
||||
return getDefaultBuilder()
|
||||
.name(DEFAULT)
|
||||
.info("dis iz default command")
|
||||
.reply(upd -> sender.send("reply", upd.getMessage().getChatId()), MESSAGE, update -> update.getMessage().getText().equals("must reply"))
|
||||
.reply(upd -> sender.send("reply", upd.getCallbackQuery().getMessage().getChatId()), CALLBACK_QUERY)
|
||||
.build();
|
||||
}
|
||||
|
||||
public Ability adminAbility() {
|
||||
return getDefaultBuilder()
|
||||
.name("admin")
|
||||
.privacy(ADMIN)
|
||||
.build();
|
||||
}
|
||||
|
||||
public Ability groupAbility() {
|
||||
return getDefaultBuilder()
|
||||
.name("group")
|
||||
.privacy(PUBLIC)
|
||||
.locality(GROUP)
|
||||
.build();
|
||||
}
|
||||
|
||||
public Ability multipleInputAbility() {
|
||||
return getDefaultBuilder()
|
||||
.name("count")
|
||||
.privacy(PUBLIC)
|
||||
.locality(USER)
|
||||
.input(4)
|
||||
.build();
|
||||
}
|
||||
|
||||
public Ability testAbility() {
|
||||
return getDefaultBuilder().build();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setSender(MessageSender sender) {
|
||||
this.sender = sender;
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package org.telegram.abilitybots.api.db;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.telegram.abilitybots.api.objects.EndUser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.google.common.collect.Maps.newHashMap;
|
||||
import static com.google.common.collect.Sets.newHashSet;
|
||||
import static java.lang.String.format;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.telegram.abilitybots.api.bot.AbilityBot.USERS;
|
||||
import static org.telegram.abilitybots.api.bot.AbilityBot.USER_ID;
|
||||
import static org.telegram.abilitybots.api.bot.AbilityBotTest.CREATOR;
|
||||
import static org.telegram.abilitybots.api.bot.AbilityBotTest.MUSER;
|
||||
import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;
|
||||
|
||||
public class MapDBContextTest {
|
||||
|
||||
private static final String TEST = "TEST";
|
||||
private DBContext db;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
db = offlineInstance("db");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canRecoverDB() throws IOException {
|
||||
Map<Integer, EndUser> users = db.getMap(USERS);
|
||||
Map<String, Integer> userIds = db.getMap(USER_ID);
|
||||
users.put(CREATOR.id(), CREATOR);
|
||||
users.put(MUSER.id(), MUSER);
|
||||
userIds.put(CREATOR.username(), CREATOR.id());
|
||||
userIds.put(MUSER.username(), MUSER.id());
|
||||
|
||||
db.getSet("AYRE").add(123123);
|
||||
Map<Integer, EndUser> originalUsers = newHashMap(users);
|
||||
String beforeBackupInfo = db.info(USERS);
|
||||
|
||||
Object jsonBackup = db.backup();
|
||||
db.clear();
|
||||
boolean recovered = db.recover(jsonBackup);
|
||||
|
||||
Map<Integer, EndUser> recoveredUsers = db.getMap(USERS);
|
||||
String afterRecoveryInfo = db.info(USERS);
|
||||
|
||||
assertTrue("Could not recover database successfully", recovered);
|
||||
assertEquals("Map info before and after recovery is different", beforeBackupInfo, afterRecoveryInfo);
|
||||
assertEquals("Map before and after recovery are not equal", originalUsers, recoveredUsers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canFallbackDBIfRecoveryFails() throws IOException {
|
||||
Set<EndUser> users = db.getSet(USERS);
|
||||
users.add(CREATOR);
|
||||
users.add(MUSER);
|
||||
|
||||
Set<EndUser> originalSet = newHashSet(users);
|
||||
Object jsonBackup = db.backup();
|
||||
String corruptBackup = "!@#$" + String.valueOf(jsonBackup);
|
||||
boolean recovered = db.recover(corruptBackup);
|
||||
|
||||
Set<EndUser> recoveredSet = db.getSet(USERS);
|
||||
|
||||
assertEquals("Recovery was successful from a CORRUPT backup", false, recovered);
|
||||
assertEquals("Set before and after corrupt recovery are not equal", originalSet, recoveredSet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canGetSummary() throws IOException {
|
||||
String anotherTest = TEST + 1;
|
||||
db.getSet(TEST).add(TEST);
|
||||
db.getSet(anotherTest).add(anotherTest);
|
||||
|
||||
String actualSummary = db.summary();
|
||||
// Name - Type - Number of "rows"
|
||||
String expectedSummary = format("%s - Set - 1\n%s - Set - 1", TEST, anotherTest);
|
||||
|
||||
assertEquals("Actual DB summary does not match that of the expected", expectedSummary, actualSummary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canGetInfo() throws IOException {
|
||||
db.getSet(TEST).add(TEST);
|
||||
|
||||
String actualInfo = db.info(TEST);
|
||||
// JSON
|
||||
String expectedInfo = "TEST - Set - 1";
|
||||
|
||||
assertEquals("Actual DB structure info does not match that of the expected", expectedInfo, actualInfo);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void cantGetInfoFromNonexistentDBStructureName() throws IOException {
|
||||
db.info(TEST);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws IOException {
|
||||
db.clear();
|
||||
db.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.telegram.abilitybots.api.objects;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.telegram.abilitybots.api.bot.DefaultBot.getDefaultBuilder;
|
||||
|
||||
public class AbilityTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void argumentsCannotBeNegative() {
|
||||
getDefaultBuilder().input(-4).build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nameCannotBeEmpty() {
|
||||
getDefaultBuilder().name("").build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nameCannotBeNull() {
|
||||
getDefaultBuilder().name(null).build();
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void consumerCannotBeNull() {
|
||||
getDefaultBuilder().action(null).build();
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void localityCannotBeNull() {
|
||||
getDefaultBuilder().locality(null).build();
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void privacyCannotBeNull() {
|
||||
getDefaultBuilder().privacy(null).build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nameCannotContainSpaces() {
|
||||
getDefaultBuilder().name("test test").build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void abilityEqualsMethod() {
|
||||
Ability ability1 = getDefaultBuilder().build();
|
||||
Ability ability2 = getDefaultBuilder().build();
|
||||
Ability ability3 = getDefaultBuilder().name("anotherconsumer").build();
|
||||
Ability ability4 = getDefaultBuilder().action((context) -> {
|
||||
}).build();
|
||||
|
||||
assertEquals("Abilities should not be equal", ability1, ability2);
|
||||
assertEquals("Abilities should not be equal", ability1, ability4);
|
||||
assertNotEquals("Abilities should be equal", ability1, ability3);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user