initial commit

This commit is contained in:
2021-08-10 23:22:25 +03:00
commit 18465ada30
42 changed files with 1647 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

17
README.MD Normal file
View File

@@ -0,0 +1,17 @@
## Простой фреймворк\фасад для Telegram-бота
### Цели
* Предоставить удобные абстракции
* Предоставить инструменты для тестирования
* Отделить логику бота от конкретной реализации
### Основная идея
Цепочка из `Command` принимает `BotRequest` и `BotResponse`, и после обработки вызывает следующий `Command`.
`BotRequest` содержит информации о действии пользователя.
`BotResponse` позволяет отправить ответ в виде `Screen`.
`Screen` содержит отображаемый текст и действия -- кнопки для клавиатуры или inline-кнопки с данными.

57
pom.xml Normal file
View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>stbf-parent</name>
<description>Simple Telegram Bot Facade</description>
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.6</lombok.version>
<slf4j.version>1.7.25</slf4j.version>
</properties>
<modules>
<module>stbf-api</module>
<module>stbf-pengrad</module>
<module>stbf-test</module>
<module>stbf-common</module>
<module>stbf-rubenlagus</module>
</modules>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

30
stbf-api/pom.xml Normal file
View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>stbf-api</artifactId>
<name>stbf-api</name>
<description>Simple Telegram Bot Facade API Module</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,57 @@
package ru.penkrat.stbf.api;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@Builder
@ToString(of = "text")
public class Action {
public static Action simple(String text) {
return builder().text(text).build();
}
public static Action simple(String text, String cmd) {
validateCmd(cmd);
return builder().text(text).cmd(cmd).build();
}
public static Action callback(String text, String callbackData) {
return builder().inline(true).text(text).callbackData(callbackData).build();
}
public static Action requestContact(String text) {
return builder().text(text).requestContact(true).build();
}
private boolean inline;
private String text;
private String cmd;
// in-line
private String callbackData;
private String url;
// keyboard
private boolean requestContact;
private boolean requestLocation;
/**
* Text of the command, 1-32 characters. Can contain only lowercase English
* letters, digits and underscores.
*/
private static void validateCmd(@NonNull String cmd) {
if (cmd.length() > 32) {
throw new IllegalArgumentException("Max length - 32 characters");
}
if (!cmd.startsWith("/")) {
throw new IllegalArgumentException("Command must start / character");
}
// TODO validate lowercase etc
}
}

View File

@@ -0,0 +1,39 @@
package ru.penkrat.stbf.api;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class BotCommandChain implements CommandChain {
private final List<Command> commands;
public BotCommandChain() {
this(new ArrayList<>());
}
@Override
public void processCommand(BotRequest botRequest, BotResponse botResponse) {
if (!commands.isEmpty()) {
try {
Command command = commands.get(0);
log.debug("Run command {}", command.getClass().getSimpleName());
command.process(botRequest, botResponse,
new BotCommandChain(commands.subList(1, commands.size())));
} catch (Exception e) {
log.error("Error in command:", e);
}
}
}
public BotCommandChain add(Command cmd) {
commands.add(cmd);
return this;
}
}

View File

@@ -0,0 +1,17 @@
package ru.penkrat.stbf.api;
import java.util.Optional;
public interface BotRequest {
Optional<String> getMessageText();
Optional<String> getPhoneNumber();
Optional<String> getCallbackData();
Optional<String> getCallbackMessageText();
Long getChatId();
}

View File

@@ -0,0 +1,15 @@
package ru.penkrat.stbf.api;
public interface BotResponse {
void send(Screen screen);
void sendFile(String filename, byte[] data);
void editMessage(String text);
void deleteMessage();
void edit(Screen screen);
}

View File

@@ -0,0 +1,7 @@
package ru.penkrat.stbf.api;
public interface Command {
void process(BotRequest botRequest, BotResponse botResponse, CommandChain chain);
}

View File

@@ -0,0 +1,7 @@
package ru.penkrat.stbf.api;
public interface CommandChain {
void processCommand(BotRequest botRequest, BotResponse botResponse);
}

View File

@@ -0,0 +1,5 @@
package ru.penkrat.stbf.api;
public interface Keyboard {
}

View File

@@ -0,0 +1,33 @@
package ru.penkrat.stbf.api;
import java.util.ServiceLoader;
public interface KeyboardBuilder {
ServiceLoader<KeyboardBuilder> keyboardBuilderLoader = ServiceLoader.load(KeyboardBuilder.class);
public static KeyboardBuilder newKeyboard() {
for (KeyboardBuilder kb : keyboardBuilderLoader) {
return kb.newInstance();
}
throw new IllegalStateException("No service KeyboardBuilder found");
}
KeyboardBuilder newInstance();
public static Keyboard singleKey(Action action) {
return newKeyboard().add(action).build();
}
default KeyboardBuilder add(String text, String callbackData) {
return add(Action.callback(text, callbackData));
}
KeyboardBuilder add(Action action);
KeyboardBuilder row(Action... buttons);
KeyboardBuilder column(Action... buttons);
Keyboard build();
}

View File

@@ -0,0 +1,8 @@
package ru.penkrat.stbf.api;
@FunctionalInterface
public interface RequestMatcher {
boolean match(BotRequest botRequest);
}

View File

@@ -0,0 +1,14 @@
package ru.penkrat.stbf.api;
public interface Screen {
String getText();
default Keyboard getKeyboard() {
return null;
}
default ScreenProperties getScreenProperties() {
return ScreenProperties.DEFAULT;
}
}

View File

@@ -0,0 +1,23 @@
package ru.penkrat.stbf.api;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Getter
@Setter
@Accessors(chain = true)
public class ScreenProperties {
public static final ScreenProperties DEFAULT = new ScreenProperties()
.setDisableWebPagePreview(true)
.setDisableNotification(true)
.setParseModeHtml(true);
private boolean disableWebPagePreview;
private boolean disableNotification;
private boolean parseModeHtml;
}

View File

@@ -0,0 +1 @@
ru.penkrat.stbf.impl.pengrad.PengradKeyboardBuilder

41
stbf-common/pom.xml Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>stbf-common</artifactId>
<name>stbf-common</name>
<dependencies>
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,29 @@
package ru.penkrat.stbf.common.command;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Command;
import ru.penkrat.stbf.api.CommandChain;
import ru.penkrat.stbf.api.RequestMatcher;
import ru.penkrat.stbf.tools.RequestMatchers;
public abstract class AbstractActionCommand implements Command {
private RequestMatcher matcher;
public AbstractActionCommand(Action action) {
matcher = RequestMatchers.action(action);
}
@Override
public void process(BotRequest botRequest, BotResponse botResponse, CommandChain chain) {
if (matcher.match(botRequest)) {
doProcess(botRequest, botResponse);
}
chain.processCommand(botRequest, botResponse);
}
protected abstract void doProcess(BotRequest botRequest, BotResponse botResponse);
}

View File

@@ -0,0 +1,36 @@
package ru.penkrat.stbf.common.command;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Command;
import ru.penkrat.stbf.api.CommandChain;
import ru.penkrat.stbf.api.RequestMatcher;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.common.screen.TextScreen;
import ru.penkrat.stbf.tools.RequestMatchers;
public class SimpleCommand implements Command {
private final RequestMatcher matcher;
private final Screen screen;
public SimpleCommand(String input, String output) {
this(Action.simple(input), new TextScreen(output));
}
public SimpleCommand(Action action, Screen screen) {
this.screen = screen;
matcher = RequestMatchers.action(action);
}
@Override
public void process(BotRequest botRequest, BotResponse botResponse, CommandChain chain) {
if (matcher.match(botRequest)) {
botResponse.send(screen);
}
chain.processCommand(botRequest, botResponse);
}
}

View File

@@ -0,0 +1,34 @@
package ru.penkrat.stbf.common.screen;
import lombok.Getter;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.Screen;
import ru.penkrat.stbf.api.KeyboardBuilder;
@Getter
public class TextScreen implements Screen {
private final String text;
private final Keyboard keyboard;
public TextScreen(String text) {
this.text = text;
this.keyboard = null;
}
public TextScreen(String text, Keyboard keyboard) {
this.text = text;
this.keyboard = keyboard;
}
public TextScreen(String text, Action btn) {
this(text, KeyboardBuilder.singleKey(btn));
}
public TextScreen(String text, Action btn1, Action btn2) {
this(text, KeyboardBuilder.newKeyboard().add(btn1).add(btn2).build());
}
}

View File

@@ -0,0 +1,20 @@
package ru.penkrat.stbf.tools;
import lombok.experimental.UtilityClass;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.RequestMatcher;
@UtilityClass
public class RequestMatchers {
public RequestMatcher action(Action action) {
return request -> request.getMessageText()
.filter(text -> matchText(action, text))
.isPresent();
}
private static boolean matchText(Action action, String inputText) {
return inputText.equalsIgnoreCase(action.getText())
|| (action.getCmd() != null && inputText.equalsIgnoreCase(action.getCmd()));
}
}

View File

@@ -0,0 +1,20 @@
package ru.penkrat.stbf;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
/**
* Unit test for simple App.
*/
public class AppTest
{
/**
* Rigorous Test :-)
*/
@Test
public void shouldAnswerWithTrue()
{
assertTrue( true );
}
}

43
stbf-pengrad/pom.xml Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>stbf-pengrad</artifactId>
<name>stbf-pengrad</name>
<description>Simple Telegram Bot Facade with com.github.pengrad implementation</description>
<dependencies>
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.pengrad</groupId>
<artifactId>java-telegram-bot-api</artifactId>
<version>[4.9.0,5.2.0]</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,61 @@
package ru.penkrat.stbf.impl.pengrad;
import static lombok.AccessLevel.PROTECTED;
import java.util.Optional;
import com.pengrad.telegrambot.model.CallbackQuery;
import com.pengrad.telegrambot.model.Contact;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotRequest;
@RequiredArgsConstructor
public class BotRequestImpl implements BotRequest {
@Getter(PROTECTED)
private final Update update;
@Override
public Optional<String> getMessageText() {
if (update.message() != null) {
return Optional.ofNullable(update.message().text());
}
return Optional.empty();
}
@Override
public Optional<String> getPhoneNumber() {
return Optional.of(update)
.map(Update::message)
.map(Message::contact)
.map(Contact::phoneNumber);
}
@Override
public Optional<String> getCallbackData() {
return Optional.of(update)
.map(Update::callbackQuery)
.map(CallbackQuery::data);
}
@Override
public Optional<String> getCallbackMessageText() {
return Optional.of(update)
.map(Update::callbackQuery)
.map(CallbackQuery::message)
.map(Message::text);
}
@Override
public Long getChatId() {
return Optional.of(update)
.map(Update::callbackQuery)
.map(CallbackQuery::message)
.orElseGet(() -> update.message()).chat().id();
}
}

View File

@@ -0,0 +1,151 @@
package ru.penkrat.stbf.impl.pengrad;
import java.lang.reflect.Field;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.gson.internal.reflect.ReflectionAccessor;
import com.pengrad.telegrambot.TelegramBot;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.model.request.ReplyKeyboardMarkup;
import com.pengrad.telegrambot.request.DeleteMessage;
import com.pengrad.telegrambot.request.EditMessageText;
import com.pengrad.telegrambot.request.SendDocument;
import com.pengrad.telegrambot.request.SendMessage;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Screen;
@Slf4j
@RequiredArgsConstructor
public class BotResponseImpl implements BotResponse {
private final Update update;
private final TelegramBot telegramBot;
@Override
public void send(Screen screen) {
log.debug("Send message: \n============\n{}\n============", screen.getText().trim());
SendMessage sendMessage = new SendMessage(chatId(), screen.getText().trim())
.parseMode(screen.getScreenProperties().isParseModeHtml() ? ParseMode.HTML : ParseMode.MarkdownV2)
.disableWebPagePreview(screen.getScreenProperties().isDisableWebPagePreview())
.disableNotification(screen.getScreenProperties().isDisableNotification());
if (screen.getKeyboard() instanceof KeyboardImpl) {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
KeyboardButton[][] keyboard = kk.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (inlineKeyboard != null && inlineKeyboard.length > 0) {
logKeyboard(inlineKeyboard);
sendMessage = sendMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else if (keyboard != null && keyboard.length > 0) {
logKeyboard(keyboard);
sendMessage = sendMessage.replyMarkup(new ReplyKeyboardMarkup(keyboard));
} else {
log.debug("No keyboard");
}
}
telegramBot.execute(sendMessage);
}
@Override
public void sendFile(String filename, byte[] data) {
telegramBot.execute(new SendDocument(chatId(), data).fileName(filename));
}
@Override
@Deprecated
public void editMessage(String text) {
telegramBot.execute(new EditMessageText(chatId(), messageId(), text)
.parseMode(ParseMode.HTML)
.disableWebPagePreview(true));
}
@Override
public void edit(Screen screen) {
EditMessageText editMessage = new EditMessageText(chatId(), messageId(), screen.getText())
.parseMode(screen.getScreenProperties().isParseModeHtml() ? ParseMode.HTML : ParseMode.MarkdownV2)
.disableWebPagePreview(screen.getScreenProperties().isDisableWebPagePreview());
if (screen.getKeyboard() instanceof KeyboardImpl) {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
InlineKeyboardButton[][] inlineKeyboard = kk.getInlineKeyboard();
if (inlineKeyboard != null && inlineKeyboard.length > 0) {
logKeyboard(inlineKeyboard);
editMessage = editMessage.replyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else {
log.debug("No keyboard");
}
}
telegramBot.execute(editMessage);
}
@Override
public void deleteMessage() {
telegramBot.execute(new DeleteMessage(chatId(), messageId()));
}
private Long chatId() {
if (update.callbackQuery() != null) {
return update.callbackQuery().message().chat().id();
}
return update.message().chat().id();
}
private Integer messageId() {
if (update.callbackQuery() != null) {
return update.callbackQuery().message().messageId();
}
return update.message().messageId();
}
private void logKeyboard(KeyboardButton[][] keyboard) {
if (log.isDebugEnabled()) {
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
log.debug("Keyboard: {}", row);
}
}
}
}
private void logKeyboard(InlineKeyboardButton[][] keyboard) {
if (log.isDebugEnabled()) {
for (int i = 0; i < keyboard.length; i++) {
if (keyboard[i].length > 0) {
String row = Stream.of(keyboard[i]).map(this::getText).collect(Collectors.joining(" | "));
log.debug("Inline keyboard: {}", row);
}
}
}
}
@SneakyThrows
private String getText(KeyboardButton btn) {
Field text = KeyboardButton.class.getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
return String.valueOf(text.get(btn));
}
@SneakyThrows
private String getText(InlineKeyboardButton btn) {
Field text = InlineKeyboardButton.class.getDeclaredField("text");
ReflectionAccessor.getInstance().makeAccessible(text);
return String.valueOf(text.get(btn));
}
}

View File

@@ -0,0 +1,16 @@
package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import lombok.Value;
import ru.penkrat.stbf.api.Keyboard;
@Value
class KeyboardImpl implements Keyboard {
private KeyboardButton[][] keyboard;
private InlineKeyboardButton[][] inlineKeyboard;
}

View File

@@ -0,0 +1,149 @@
package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.model.request.KeyboardButton;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
public class PengradKeyboardBuilder implements KeyboardBuilder {
private KeyboardButton[][] keyboard;
private InlineKeyboardButton[][] inlineKeyboard;
public static Keyboard singleKey(Action action) {
return KeyboardBuilder.newKeyboard().add(action).build();
}
@Override
public Keyboard build() {
return new KeyboardImpl(keyboard, inlineKeyboard);
}
public KeyboardBuilder addGetContact(String text) {
put(new KeyboardButton(text).requestContact(true));
return self();
}
public KeyboardBuilder add(String text) {
put(new KeyboardButton(text));
return self();
}
@Override
public KeyboardBuilder add(Action action) {
put(action);
return self();
}
public KeyboardBuilder row(KeyboardButton... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
nextRow();
return self();
}
@Override
public KeyboardBuilder row(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
nextRow();
return self();
}
@Override
public KeyboardBuilder column(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
if (buttons[i].isInline()) {
nextRowI();
} else {
nextRow();
}
}
return self();
}
public KeyboardBuilder addNl(String text) {
add(text);
nextRow();
return self();
}
public PengradKeyboardBuilder add(String text, String data) {
put(new InlineKeyboardButton(text).callbackData(data));
return self();
}
public KeyboardBuilder addNl(String text, String data) {
add(text, data);
nextRowI();
return self();
}
private void nextRow() {
KeyboardButton[][] n = new KeyboardButton[keyboard.length + 1][];
System.arraycopy(keyboard, 0, n, 0, keyboard.length);
keyboard = n;
keyboard[keyboard.length - 1] = new KeyboardButton[] {};
}
private void nextRowI() {
InlineKeyboardButton[][] n = new InlineKeyboardButton[inlineKeyboard.length + 1][];
System.arraycopy(inlineKeyboard, 0, n, 0, inlineKeyboard.length);
inlineKeyboard = n;
inlineKeyboard[inlineKeyboard.length - 1] = new InlineKeyboardButton[] {};
}
protected PengradKeyboardBuilder self() {
return this;
}
private void put(Action action) {
if (action.isInline()) {
put(new InlineKeyboardButton(action.getText()).callbackData(action.getCallbackData()));
} else {
put(new KeyboardButton(action.getText()).requestContact(action.isRequestContact()));
}
}
private void put(KeyboardButton btn) {
if (keyboard == null) {
keyboard = new KeyboardButton[][] {
new KeyboardButton[] { btn }
};
} else {
KeyboardButton[] k = keyboard[keyboard.length - 1];
KeyboardButton[] n = new KeyboardButton[k.length + 1];
System.arraycopy(k, 0, n, 0, k.length);
n[k.length] = btn;
keyboard[keyboard.length - 1] = n;
}
}
private void put(InlineKeyboardButton btn) {
if (inlineKeyboard == null) {
inlineKeyboard = new InlineKeyboardButton[][] {
new InlineKeyboardButton[] { btn }
};
} else {
InlineKeyboardButton[] k = inlineKeyboard[inlineKeyboard.length - 1];
InlineKeyboardButton[] n = new InlineKeyboardButton[k.length + 1];
System.arraycopy(k, 0, n, 0, k.length);
n[k.length] = btn;
inlineKeyboard[inlineKeyboard.length - 1] = n;
}
}
@Override
public KeyboardBuilder newInstance() {
return new PengradKeyboardBuilder();
}
}

View File

@@ -0,0 +1,35 @@
package ru.penkrat.stbf.impl.pengrad;
import com.pengrad.telegrambot.TelegramBot;
import com.pengrad.telegrambot.UpdatesListener;
import com.pengrad.telegrambot.model.Update;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.CommandChain;
@Slf4j
public class PengradTelegramBot extends TelegramBot implements AutoCloseable {
public PengradTelegramBot(String botToken, CommandChain commandChain) {
super(botToken);
this.setUpdatesListener(updates -> {
for (Update update : updates) {
try {
commandChain.processCommand(
new BotRequestImpl(update),
new BotResponseImpl(update, this));
} catch (Exception e) {
log.error("Bot Error:", e);
}
}
return UpdatesListener.CONFIRMED_UPDATES_ALL;
});
}
@Override
public void close() throws Exception {
removeGetUpdatesListener();
log.debug("Bot closed.");
}
}

View File

@@ -0,0 +1 @@
ru.penkrat.stbf.impl.pengrad.PengradKeyboardBuilder

41
stbf-rubenlagus/pom.xml Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>stbf-rubenlagus</artifactId>
<name>stbf-rubenlagus</name>
<description>Simple Telegram Bot Facade with https://github.com/rubenlagus/TelegramBots implementation</description>
<dependencies>
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,58 @@
package ru.penkrat.stbf.impl.rubenlagus;
import static lombok.AccessLevel.PROTECTED;
import java.util.Optional;
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
import org.telegram.telegrambots.meta.api.objects.Contact;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotRequest;
@RequiredArgsConstructor
class BotRequestImpl implements BotRequest {
@Getter(PROTECTED)
private final Update update;
@Override
public Optional<String> getMessageText() {
if (update.hasMessage()) {
return Optional.ofNullable(update.getMessage().getText());
}
return Optional.empty();
}
@Override
public Optional<String> getPhoneNumber() {
return Optional.of(update)
.map(Update::getMessage)
.map(Message::getContact)
.map(Contact::getPhoneNumber);
}
@Override
public Optional<String> getCallbackData() {
return Optional.of(update)
.map(Update::getCallbackQuery)
.map(CallbackQuery::getData);
}
@Override
public Optional<String> getCallbackMessageText() {
return Optional.of(update)
.map(Update::getCallbackQuery)
.map(CallbackQuery::getMessage)
.map(Message::getText);
}
@Override
public Long getChatId() {
return update.getMessage().getChatId();
}
}

View File

@@ -0,0 +1,113 @@
package ru.penkrat.stbf.impl.rubenlagus;
import java.util.List;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage;
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboardMarkup;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardRow;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Screen;
@Slf4j
@RequiredArgsConstructor
class BotResponseImpl implements BotResponse {
private final Update update;
private final TelegramLongPollingBot bot;
@Override
public void send(Screen screen) {
SendMessage send = new SendMessage(chatId(), screen.getText());
if (screen.getKeyboard() instanceof KeyboardImpl) {
KeyboardImpl kk = (KeyboardImpl) screen.getKeyboard();
List<KeyboardRow> keyboard = kk.getKeyboard();
List<List<InlineKeyboardButton>> inlineKeyboard = kk.getInlineKeyboard();
if (inlineKeyboard != null && !inlineKeyboard.isEmpty()) {
send.setReplyMarkup(new InlineKeyboardMarkup(inlineKeyboard));
} else if (keyboard != null && !keyboard.isEmpty()) {
send.setReplyMarkup(new ReplyKeyboardMarkup(keyboard));
} else {
log.debug("No keyboard");
}
}
try {
bot.execute(send);
} catch (TelegramApiException e) {
log.error("Send error", e);
}
}
@Override
public void sendFile(String filename, byte[] data) {
throw new IllegalStateException("Not inplemented");
}
@Override
public void editMessage(String text) {
EditMessageText send = new EditMessageText(text);
send.setChatId(chatId());
send.setMessageId(messageId());
if (update.hasCallbackQuery()) {
send.setInlineMessageId(update.getCallbackQuery().getInlineMessageId());
}
try {
bot.execute(send);
} catch (TelegramApiException e) {
log.error("Send error", e);
}
}
@Override
public void deleteMessage() {
DeleteMessage send = new DeleteMessage(chatId(), messageId());
try {
bot.execute(send);
} catch (TelegramApiException e) {
log.error("Send error", e);
}
}
@Override
public void edit(Screen screen) {
EditMessageText send = new EditMessageText(screen.getText());
send.setChatId(chatId());
send.setMessageId(messageId());
if (update.hasCallbackQuery()) {
send.setInlineMessageId(update.getCallbackQuery().getInlineMessageId());
}
try {
bot.execute(send);
} catch (TelegramApiException e) {
log.error("Send error", e);
}
}
private String chatId() {
if (update.hasCallbackQuery()) {
return Long.toString(update.getCallbackQuery().getMessage().getChatId());
}
return Long.toString(update.getMessage().getChatId());
}
private Integer messageId() {
if (update.hasCallbackQuery()) {
return Integer.parseInt(update.getCallbackQuery().getInlineMessageId());
}
return update.getMessage().getMessageId();
}
}

View File

@@ -0,0 +1,18 @@
package ru.penkrat.stbf.impl.rubenlagus;
import java.util.List;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardRow;
import lombok.Value;
import ru.penkrat.stbf.api.Keyboard;
@Value
class KeyboardImpl implements Keyboard {
private List<KeyboardRow> keyboard;
private List<List<InlineKeyboardButton>> inlineKeyboard;
}

View File

@@ -0,0 +1,150 @@
package ru.penkrat.stbf.impl.rubenlagus;
import java.util.ArrayList;
import java.util.List;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardButton;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardRow;
import ru.penkrat.stbf.api.Action;
import ru.penkrat.stbf.api.Keyboard;
import ru.penkrat.stbf.api.KeyboardBuilder;
public class RubenlagusKeyboardBuilder implements KeyboardBuilder {
private List<KeyboardRow> keyboard;
private List<List<InlineKeyboardButton>> inlineKeyboard;
public static Keyboard singleKey(Action action) {
return KeyboardBuilder.newKeyboard().add(action).build();
}
@Override
public Keyboard build() {
return new KeyboardImpl(keyboard, inlineKeyboard);
}
public KeyboardBuilder addGetContact(String text) {
KeyboardButton keyboardButton = new KeyboardButton(text);
keyboardButton.setRequestContact(true);
put(keyboardButton);
return self();
}
public KeyboardBuilder add(String text) {
put(new KeyboardButton(text));
return self();
}
@Override
public KeyboardBuilder add(Action action) {
put(action);
return self();
}
public KeyboardBuilder row(KeyboardButton... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
nextRow();
return self();
}
@Override
public KeyboardBuilder row(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
}
nextRow();
return self();
}
@Override
public KeyboardBuilder column(Action... buttons) {
for (int i = 0; i < buttons.length; i++) {
put(buttons[i]);
if (buttons[i].isInline()) {
nextRowI();
} else {
nextRow();
}
}
return self();
}
public KeyboardBuilder addNl(String text) {
add(text);
nextRow();
return self();
}
public RubenlagusKeyboardBuilder add(String text, String data) {
InlineKeyboardButton inlineKeyboardButton = new InlineKeyboardButton(text);
inlineKeyboardButton.setCallbackData(data);
put(inlineKeyboardButton);
return self();
}
public KeyboardBuilder addNl(String text, String data) {
add(text, data);
nextRowI();
return self();
}
private void nextRow() {
KeyboardRow row = new KeyboardRow();
keyboard.add(row);
}
private void nextRowI() {
List<InlineKeyboardButton> row = new ArrayList<>();
inlineKeyboard.add(row);
}
protected RubenlagusKeyboardBuilder self() {
return this;
}
private void put(Action action) {
if (action.isInline()) {
InlineKeyboardButton inlineKeyboardButton = new InlineKeyboardButton(action.getText());
inlineKeyboardButton.setCallbackData(action.getCallbackData());
put(inlineKeyboardButton);
} else {
KeyboardButton keyboardButton = new KeyboardButton(action.getText());
keyboardButton.setRequestContact(action.isRequestContact());
put(keyboardButton);
}
}
private void put(KeyboardButton btn) {
if (keyboard == null) {
keyboard = new ArrayList<KeyboardRow>();
KeyboardRow row = new KeyboardRow();
keyboard.add(row);
row.add(btn);
} else {
keyboard.get(keyboard.size() - 1).add(btn);
}
}
private void put(InlineKeyboardButton btn) {
if (inlineKeyboard == null) {
inlineKeyboard = new ArrayList<List<InlineKeyboardButton>>();
List<InlineKeyboardButton> row = new ArrayList<>();
inlineKeyboard.add(row);
row.add(btn);
} else {
inlineKeyboard.get(inlineKeyboard.size() - 1).add(btn);
}
}
@Override
public KeyboardBuilder newInstance() {
return new RubenlagusKeyboardBuilder();
}
}

View File

@@ -0,0 +1,54 @@
package ru.penkrat.stbf.impl.rubenlagus;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.TelegramBotsApi;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
import lombok.extern.slf4j.Slf4j;
import ru.penkrat.stbf.api.BotCommandChain;
@Slf4j
public class RubenlagusTelegramBot extends TelegramLongPollingBot {
private final String botUsername;
private final String botToken;
private final BotCommandChain commandChain;
public RubenlagusTelegramBot(String botUsername, String botToken, BotCommandChain botCommandChain) {
this.botUsername = botUsername;
this.botToken = botToken;
this.commandChain = botCommandChain;
TelegramBotsApi telegramBotsApi;
try {
telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class);
telegramBotsApi.registerBot(this);
} catch (TelegramApiException e) {
log.error("Error", e);
}
}
@Override
public void onUpdateReceived(Update update) {
try {
commandChain.processCommand(
new BotRequestImpl(update),
new BotResponseImpl(update, this));
} catch (Exception e) {
log.error("Bot Error:", e);
}
}
@Override
public String getBotUsername() {
return botUsername;
}
@Override
public String getBotToken() {
return botToken;
}
}

View File

@@ -0,0 +1 @@
ru.penkrat.stbf.impl.rubenlagus.RubenlagusKeyboardBuilder

53
stbf-test/pom.xml Normal file
View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>stbf-parent</artifactId>
<groupId>ru.penkrat.stbf</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>stbf-test</artifactId>
<name>stbf-test</name>
<dependencies>
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>ru.penkrat.stbf</groupId>
<artifactId>stbf-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,42 @@
package ru.penkrat.stbf.test;
import java.util.Optional;
import java.util.Random;
import lombok.Builder;
import lombok.Getter;
import ru.penkrat.stbf.api.BotRequest;
@Builder
class BotRequestImpl implements BotRequest {
private String messageText;
private String phoneNumber;
private String callbackData;
private String callbackMessageText;
@Getter
@Builder.Default
private Long chatId = new Random().nextLong();
@Override
public Optional<String> getMessageText() {
return Optional.ofNullable(messageText);
}
@Override
public Optional<String> getPhoneNumber() {
return Optional.ofNullable(phoneNumber);
}
@Override
public Optional<String> getCallbackData() {
return Optional.ofNullable(callbackData);
}
@Override
public Optional<String> getCallbackMessageText() {
return Optional.ofNullable(callbackMessageText);
}
}

View File

@@ -0,0 +1,43 @@
package ru.penkrat.stbf.test;
import ru.penkrat.stbf.api.BotResponse;
import ru.penkrat.stbf.api.Screen;
class BotResponseImpl implements BotResponse {
private Result result;
public BotResponseImpl(Result result) {
this.result = result;
}
@Override
public void send(Screen screen) {
result.addScreen(screen);
}
@Override
public void sendFile(String filename, byte[] data) {
// TODO Auto-generated method stub
}
@Override
public void editMessage(String text) {
// TODO Auto-generated method stub
}
@Override
public void deleteMessage() {
// TODO Auto-generated method stub
}
@Override
public void edit(Screen screen) {
// TODO Auto-generated method stub
}
}

View File

@@ -0,0 +1,23 @@
package ru.penkrat.stbf.test;
import java.util.ArrayList;
import java.util.List;
import ru.penkrat.stbf.api.Screen;
public class Result {
private final List<Screen> screens = new ArrayList<>();
public String text() {
if (screens.isEmpty()) {
return null;
}
return screens.get(0).getText();
}
public void addScreen(Screen screen) {
screens.add(screen);
}
}

View File

@@ -0,0 +1,43 @@
package ru.penkrat.stbf.test;
import lombok.RequiredArgsConstructor;
import ru.penkrat.stbf.api.BotCommandChain;
import ru.penkrat.stbf.api.BotRequest;
import ru.penkrat.stbf.api.Command;
import ru.penkrat.stbf.api.CommandChain;
import ru.penkrat.stbf.test.BotRequestImpl;
import ru.penkrat.stbf.test.BotResponseImpl;
@RequiredArgsConstructor
public class TestBot {
public static TestBot from(Command... commands) {
BotCommandChain bcc = new BotCommandChain();
for (int i = 0; i < commands.length; i++) {
bcc.add(commands[i]);
}
return new TestBot(bcc);
}
private final CommandChain commandChain;
public Result whenText(String input) {
return process(BotRequestImpl.builder().messageText(input).build());
}
public Result whenPhoneNumber(String input) {
return process(BotRequestImpl.builder().phoneNumber(input).build());
}
public Result whenCallback(String data) {
return process(BotRequestImpl.builder().callbackData(data).build());
}
private Result process(BotRequest botRequest) {
Result result = new Result();
commandChain.processCommand(botRequest, new BotResponseImpl(result));
return result;
}
}

View File

@@ -0,0 +1,23 @@
package ru.penkrat.stbf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import org.junit.Test;
import ru.penkrat.stbf.api.Command;
import ru.penkrat.stbf.common.command.SimpleCommand;
import ru.penkrat.stbf.test.Result;
import ru.penkrat.stbf.test.TestBot;
public class BotTest {
private Command start = new SimpleCommand("/start", "Hello, world");
@Test
public void shouldAnswerWithHello() {
Result result = TestBot.from(start).whenText("/start");
assertThat(result.text(), containsString("Hello, world"));
}
}