Added AbilityBot module

This commit is contained in:
Abbas Abou Daya 2017-06-30 19:29:10 +03:00
parent 0e6e9e7e99
commit 6df6627821
22 changed files with 3876 additions and 0 deletions

View File

@ -13,6 +13,7 @@
<module>telegrambots</module>
<module>telegrambots-meta</module>
<module>telegrambots-extensions</module>
<module>telegrambots-abilities</module>
</modules>
<licenses>

View 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.

View 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>

View File

@ -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)));
}
}

View File

@ -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);
}

View File

@ -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...";
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}