[Из песочницы] Telegram-бот в качестве подарка
Привет, Хабр! Сегодня я хочу рассказать о том, как писал Telegram-бота, да не простого, а подарочного. Прошу под кат тех, кому эта история кажется забавной, а также тех, кто пытается писать своих Telegram-ботов на Java. Возможно, мой небольшой опыт будет в чем-то полезен.
Краткая предыстория
Я работаю программистом чуть более шести лет, пишу в основном на Java и 1С, звезд с неба не хватаю, но поставленные задачи выполняю.
Весной 2017 года меня заинтересовало создание ботов для различных мессенджеров. Сначала хорошей идеей казалось создание бота в Viber. В Сибири он наиболее популярен, в нем сидят практически все знакомые, корпоративные чаты ведутся тоже в нем. Кроме того, вдохновляла эта статья. Однако, создание публичного аккаунта оказалось не такой простой задачей — на все запросы приходили немногословные отказы.
Помучавшись с Вайбером около недели, я обратил внимание на Телеграм, оказалось, что зарегистрировать бота там очень легко, а жизнь джава-программиста облегчается наличием библиотеки TelegramBots. В качестве пробы пера был написан бот, позволяющий получать некоторые отчеты из корпоративных систем, потом был бот со справочной информацией, а осенью уже для себя я попробовал сделать бота для предварительной записи в салон красоты. Все это были интересные поделки, которые нравились окружающим, а первые два бота даже реально использовались в работе. Благодаря этому, в голове постоянно крутились мысли о том, как еще можно применить ботов в быту и народном хозяйстве.
Здесь необходимо заметить, что у коллеги в самом начале декабря намечался день рождения, ну и естественно не поздравить её было нельзя. Достаточно очевидно, что коллеги с системным мышлением в качестве подарков могут рассчитывать не только на конфеты, цветы и прочие ништяки, но и на что-нибудь, претендующее на остроумность. Так в 2016 году в качестве подарка была закастомлена главная страница той системы, которую мы вместе делаем, и в день рождения вместо стандартной заставки коллегу встречало поздравление. Вроде бы мелочь, но в нашей организации никого так не поздравляли и подарок, как говорится, зашел, да так зашел, что коллега посчитала это поздравление самым лучшим в тот день (надеюсь, так и было). После таких достижений было понятно, что в 2017 году надо развить тему и снова дополнить стандартный поздравительный набор чем-нибудь айтишным. В голове смутно бродили мысли о том, что «что-нибудь айтишное» могло бы быть связано с ботами, но четкой идеи не было, и я почти смирился с тем, что ничего оригинального придумать не получится. Время шло, до дня рождения оставалось 5 дней…
Я возвращался с работы домой, пробка текла вяло, и можно было углубиться в свои мысли: планы разработок, технические решения, автоматизация с помощью ботов, предвкушение пятничного пива, в общем, стандартная каша. Вдруг в голове промелькнула мысль, что коллеге все же надо подарить бота, пускай совсем простенького, например, выдающего разные памятные для коллеги фоточки. Мысль показалась совсем несвежей, однако я начал прикидывать, какие фото можно будет использовать. При этом фото было много, а вот обстоятельства, при которых они делались, вспоминались уже с трудом. Тут-то и появилась идея подарочного бота: шуточная викторина, в которой каждый вопрос сопровождался бы фотографиями коллеги и несколькими вариантами ответов, при этом на каждый ответ бот выдавал бы какой-то смешной комментарий. Эту идею я счел с одной стороны достаточно оригинальной, а с другой — осуществимой за оставшееся до дня рождения время.
Подготовка и создание базы вопросов
На выполнение всех работ оставалось всего 4 дня, а если говорить точнее, то 4 вечера, да и то неполных. Под рукой имелись исходники трех других ботов, которые можно использовать как «запчасти», и было понятно, что впереди ждет увлекательное ралли на велосипеде из костылей.
В качестве языка программирования была выбрана Java, в качестве библиотеки, для работы с API Telegram — TelegramBots, для хранения базы вопросов использована СУБД H2.
Первой задачей являлось создание базы вопросов. Для этого пришлось провести большую работу по сбору фотографий с телефона, рабочего и домашнего компьютеров и соцсетей. Полученные фотографии были структурированы таким образом, что получилось 26 вопросов, к каждому из которых прилагалось от 2 до 4 фото и 4 варианта ответа. При этом заведомо правильных вариантов ответа не предусматривалось, а ответ на каждый вопрос просто сопровождался комментарием. Хотелось также сделать сохранение истории выбранных вариантов ответа, но в самый последний момент я просто забыл прикрутить эту фичу.
Раскладка фотографий и придумывание вопросов оказались очень трудоемким процессом, и на них ушло полтора вечера.
Далее была реализована база данных, хранящая вопросы. Ниже приведены описание таблиц базы данных и DDL-скрипт.
CLS_QUEST
— таблица, содержащая тексты вопросовCLS_QUEST_PHOTO
— таблица содержащая относительные пути к фотографиям, которые связаны с задаваемым вопросом; сами фотографии лежат в файловой системе в папках, соответствующих вопросу.CLS_ANSWER
— таблица, содержащая варианты ответов на вопрос, а также комментарии к каждому варианту ответа
CREATE SCHEMA IF NOT EXISTS QUE;
SET SCHEMA QUE;
CREATE TABLE QUE.CLS_QUEST(
ID BIGINT IDENTITY,
IS_DELETED INT DEFAULT 0,
QUEST_TEXT CLOB
);
CREATE TABLE QUE.CLS_QUEST_PHOTO(
ID BIGINT IDENTITY,
ID_QUEST BIGINT NOT NULL,
IS_DELETED INT DEFAULT 0,
REL_FILE_PATH CLOB,
PHOTO_TEXT CLOB,
FOREIGN KEY(ID_QUEST) REFERENCES CLS_QUEST(ID)
);
CREATE TABLE QUE.CLS_ANSWER(
ID BIGINT IDENTITY,
ID_QUEST BIGINT NOT NULL,
IS_DELETED INT DEFAULT 0,
ANSWER_TEXT CLOB,
ANSWER_COMMENT CLOB,
FOREIGN KEY(ID_QUEST) REFERENCES CLS_QUEST(ID)
);
После создания база была наполнена данными вручную, благо в Netbeans, который я использую как среду разработки, достаточно удобный редактор SQL-скриптов.
По истечении двух дней база вопросов и фотографий была готова, времени оставалось совсем немного, пора было переходить к созданию самого бота.
Каркас бота
Напомню, что для создания бота в Telegram необходимо написать @BotFather, пользуясь командой /newbot ввести для бота отображаемое имя и имя пользователя. После выполнения этих действий будет получен токен для доступа к API Telegram. Выглядит это примерно так.
Для красоты добавим фото профиля с помощью /setuserpic.
Теперь перейдем к созданию самого бота с помощью TelegramBots. Напомню, что Telegram позволяет создавать боты работающие с Webhooks и LongPolling-боты. Выбран был второй вариант. Для создания LongPolling-бота, необходимо реализовать собственный класс, наследующий классу org.telegram.telegrambots.bots.TelegramLongPollingBot
.
public class Bot extends TelegramLongPollingBot {
private static final String TOKEN = "TOKEN";
private static final String USERNAME = "USERNAME";
public Bot() {
}
public Bot(DefaultBotOptions options) {
super(options);
}
@Override
public String getBotToken() {
return TOKEN;
}
@Override
public String getBotUsername() {
return USERNAME;
}
@Override
public void onUpdateReceived(Update update) {
if (update.hasMessage() && update.getMessage().hasText()) {
processCommand(update);
} else if (update.hasCallbackQuery()) {
processCallbackQuery(update);
}
}
}
TOKEN
— токен для доступа к API Telegram, полученный на этапе регистрации бота.USERNAME
— имя бота, полученное на этапе регистрации бота.
Метод onUpdateReceived
вызывается при поступлении боту «входящих обновлений». В нашем боте нас интересует обработка текстовых команд (если быть честным, то только команды /start) и обработка колбэков (обратных вызовов), возникающих при нажатии на кнопки инлайн-клавиатуры (размещается в области сообщений).
Бот проверяет, является ли входящее обновление текстовым сообщением update.hasMessage() && update.getMessage().hasText()
или колбэком update.hasCallbackQuery()
, после чего вызывает соответствующие методы для обработки. О содержимом этих методов поговорим немного позже.
Создаваемый бот является обычным консольным приложением и его запуск выглядит следующим образом:
public class Main {
public static void main(String[] args) {
ApiContextInitializer.init();
TelegramBotsApi botsApi = new TelegramBotsApi();
Runnable r = () -> {
Bot bot = null;
HttpHost proxy = AppEnv.getContext().getProxy();
if (proxy == null) {
bot = new Bot();
} else {
DefaultBotOptions instance = ApiContext
.getInstance(DefaultBotOptions.class);
RequestConfig rc = RequestConfig.custom()
.setProxy(proxy).build();
instance.setRequestConfig(rc);
bot = new Bot(instance);
}
try {
botsApi.registerBot(bot);
AppEnv.getContext().getMenuManager().setBot(bot);
} catch (TelegramApiRequestException ex) {
Logger.getLogger(Main.class.getName())
.log(Level.SEVERE, null, ex);
}
};
r.run();
while (true) {
try {
Thread.sleep(80000L);
} catch (InterruptedException ex) {
Logger.getLogger(Main.class.getName())
.log(Level.SEVERE, null, ex);
}
}
}
}
Ничего сложного в инициализации бота нет, однако хочу обратить внимание, что достаточно важно предусмотреть возможность указать боту прокси. В нашем случае настройки прокси хранятся в обычном properties-файле, откуда считываются при старте программы. Также замечу, что в приложении используется собственный нехороший велосипед в виде некоего подобия глобального контекста AppEnv.getContext()
. На момент написания бота исправлять это было некогда, но в новых «поделках» удалось изжить этот велосипед и использовать вместо него Google Guice.
Приветственное сообщение
Работа бота естественно начинается с обработки команды /start. Как было написано выше, эта команда обрабатывается методом processCommand
.
В начале метода объявим смайлы, которые будут использоваться в тексте приветственного сообщения.
final String smiling_face_with_heart_eyes =
new String(Character.toChars(0x1F60D));
final String winking_face = new String(Character.toChars(0x1F609));
final String bouquet = new String(Character.toChars(0x1F490));
final String party_popper = new String(Character.toChars(0x1F389));
Далее производится проверка введенной команды и если это команда /start, то формируется ответное сообщение answerMessage
. У сообщения устанавливается текст setText()
, включается поддержка некоторых html-теговsetParseMode("HTML")
и устанавливается идентификатор чата, в который сообщение будет отправлено setChatId(update.getMessage().getChatId())
. Осталось только добавить кнопку «Начать». Для этого сформируем инлайн-клавиатуру и добавим ее в ответ:
SendMessage answerMessage = null;
String text = update.getMessage().getText();
if ("/start".equalsIgnoreCase(text)) {
answerMessage = new SendMessage();
answerMessage.setText("Привет!" + smiling_face_with_heart_eyes +
"\nВо-первых с днем рождения!"
+ bouquet + bouquet + bouquet + party_popper
+ " А во-вторых, ты готова поиграть в увлекательную викторину?");
answerMessage.setParseMode("HTML");
answerMessage.setChatId(update.getMessage().getChatId());
InlineKeyboardMarkup markup = keyboard(update);
answerMessage.setReplyMarkup(markup);
}
private InlineKeyboardMarkup keyboard(Update update) {
final InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
List> keyboard = new ArrayList<>();
keyboard.add(Arrays.asList(buttonMain()));
markup.setKeyboard(keyboard);
return markup;
}
private InlineKeyboardButton buttonMain() {
final String OPEN_MAIN = "OM";
final String winking_face = new String(Character.toChars(0x1F609));
InlineKeyboardButton button = new InlineKeyboardButtonBuilder()
.setText("Начать!" + winking_face)
.setCallbackData(new ActionBuilder(marshaller)
.setName(OPEN_MAIN)
.asString())
.build();
return button;
}
Интересным моментом является установка данных колбэка. Эти данные могут использоваться при обработке нажатия кнопок. В нашем случае в данные колбэка записывается сериализованный в JSON объект. Этот способ тяжеловесен для данной задачи, но позволяет работать с данными возврата без лишних заморочек на преобразование. Данные возврата формируются в специальном билдере ActionBuilder
.
public class Action {
protected String name = "";
protected String id = "";
protected String value = "";
public String getName() {
return name;
}
public void setName(String value) {
this.name = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
public class ActionBuilder {
private final DocumentMarshaller marshaller;
private Action action = new Action();
public ActionBuilder(DocumentMarshaller marshaller) {
this. marshaller = marshaller;
}
public ActionBuilder setName(String name) {
action.setName(name);
return this;
}
public ActionBuilder setValue(String name) {
action.setValue(name);
return this;
}
public String asString() {
return marshaller.marshal(action, "Action");
}
public Action build() {
return action;
}
public Action build(Update update) {
String data = update.getCallbackQuery().getData();
if (data == null) {
return null;
}
action = marshaller.unmarshal(data, "Action");
if (action == null) {
return null;
}
return action;
}
}
Для того, чтобы ActionBuilder
мог вернуть JSON ему необходимо передать маршаллер. Здесь и далее при упоминании переменной marshaller подразумевается, что она является объектом класса, реализующего интерфейс DocumentMarshaller
.
public interface DocumentMarshaller {
String marshal(T document);
T unmarshal(String str);
T unmarshal(String str, Class clazz);
}
Маршаллер, который используется в ActionBuilder
, реализован с использованием Jackson.
И, наконец, производится отправка сообщения:
try {
if (answerMessage != null) {
execute(answerMessage);
}
} catch (TelegramApiException ex) {
Logger.getLogger(Bot.class.getName())
.log(Level.SEVERE, null, ex);
}
В итоге, приветственное сообщение выглядит вот так.
Задаем вопросы
Оставалось сделать самое интересное — реализовать логику работы бота.
Для работы с базой вопросов использовалось JPA. Приведу код классов-сущностей.
public abstract class Classifier implements Serializable {
private static final long serialVersionUID = 1L;
public Classifier() {
}
public abstract Long getId();
public abstract Integer getIsDeleted();
public abstract void setIsDeleted(Integer isDeleted);
}
@Entity
@Table(name = "CLS_ANSWER", catalog = "QUEB", schema = "QUE")
@XmlRootElement
public class ClsAnswer extends Classifier implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Column(name = "IS_DELETED")
private Integer isDeleted;
@Lob
@Column(name = "ANSWER_TEXT")
private String answerText;
@Lob
@Column(name = "ANSWER_COMMENT")
private String answerComment;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idAnswer")
private Collection regQuestAnswerCollection;
@JoinColumn(name = "ID_QUEST", referencedColumnName = "ID")
@ManyToOne(optional = false)
private ClsQuest idQuest;
public ClsAnswer() {
}
public ClsAnswer(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getIsDeleted() {
return isDeleted;
}
public void setIsDeleted(Integer isDeleted) {
this.isDeleted = isDeleted;
}
public String getAnswerText() {
return answerText;
}
public void setAnswerText(String answerText) {
this.answerText = answerText;
}
public String getAnswerComment() {
return answerComment;
}
public void setAnswerComment(String answerComment) {
this.answerComment = answerComment;
}
@XmlTransient
public Collection getRegQuestAnswerCollection() {
return regQuestAnswerCollection;
}
public void setRegQuestAnswerCollection(Collection regQuestAnswerCollection) {
this.regQuestAnswerCollection = regQuestAnswerCollection;
}
public ClsQuest getIdQuest() {
return idQuest;
}
public void setIdQuest(ClsQuest idQuest) {
this.idQuest = idQuest;
}
}
@Entity
@Table(name = "CLS_QUEST", catalog = "QUEB", schema = "QUE")
@XmlRootElement
public class ClsQuest extends Classifier implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Column(name = "IS_DELETED")
private Integer isDeleted;
@Lob
@Column(name = "QUEST_TEXT")
private String questText;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idQuest")
private Collection regQuestAnswerCollection;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idQuest")
private Collection clsAnswerCollection;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "idQuest")
private Collection clsQuestPhotoCollection;
public ClsQuest() {
}
public ClsQuest(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getIsDeleted() {
return isDeleted;
}
public void setIsDeleted(Integer isDeleted) {
this.isDeleted = isDeleted;
}
public String getQuestText() {
return questText;
}
public void setQuestText(String questText) {
this.questText = questText;
}
@XmlTransient
public Collection getRegQuestAnswerCollection() {
return regQuestAnswerCollection;
}
public void setRegQuestAnswerCollection(Collection regQuestAnswerCollection) {
this.regQuestAnswerCollection = regQuestAnswerCollection;
}
@XmlTransient
public Collection getClsAnswerCollection() {
return clsAnswerCollection;
}
public void setClsAnswerCollection(Collection clsAnswerCollection) {
this.clsAnswerCollection = clsAnswerCollection;
}
@XmlTransient
public Collection getClsQuestPhotoCollection() {
return clsQuestPhotoCollection;
}
public void setClsQuestPhotoCollection(Collection clsQuestPhotoCollection) {
this.clsQuestPhotoCollection = clsQuestPhotoCollection;
}
}
@Entity
@Table(name = "CLS_QUEST_PHOTO", catalog = "QUEB", schema = "QUE")
@XmlRootElement
public class ClsQuestPhoto extends Classifier implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "ID")
private Long id;
@Column(name = "IS_DELETED")
private Integer isDeleted;
@Lob
@Column(name = "REL_FILE_PATH")
private String relFilePath;
@Lob
@Column(name = "PHOTO_TEXT")
private String photoText;
@JoinColumn(name = "ID_QUEST", referencedColumnName = "ID")
@ManyToOne(optional = false)
private ClsQuest idQuest;
public ClsQuestPhoto() {
}
public ClsQuestPhoto(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getIsDeleted() {
return isDeleted;
}
public void setIsDeleted(Integer isDeleted) {
this.isDeleted = isDeleted;
}
public String getRelFilePath() {
return relFilePath;
}
public void setRelFilePath(String relFilePath) {
this.relFilePath = relFilePath;
}
public String getPhotoText() {
return photoText;
}
public void setPhotoText(String photoText) {
this.photoText = photoText;
}
public ClsQuest getIdQuest() {
return idQuest;
}
public void setIdQuest(ClsQuest idQuest) {
this.idQuest = idQuest;
}
}
Также замечу, что здесь и далее для доступа к данным используется объект, реализующий интерфейс ClassifierRepository
, а при упоминании переменной classifierRepository
подразумевается, что она является объектом класса, реализующего интерфейс ClassifierRepository
public interface ClassifierRepository {
void add(T classifier);
List find(Class clazz);
T find(Class clazz, Long id);
List find(Class clazz, boolean isDeleted);
List getAll(Class clazz);
List getAll(Class clazz, boolean isDeleted);
}
Теперь перейдем к тому моменту, когда нажимается кнопка «Начать!». В этот самый момент бот обрабатывает очередную порцию входящей информации и вызывает ранее упоминавшийся метод processCallbackQuery()
. В начале метода обрабатывается входящее обновление, а также извлекаются данные колбэка. На основании данных колбэка определяется, было ли произведено нажатие на кнопку «Начать!» OPEN_MAIN.equals(action.getName()
, либо была нажата кнопка ответа на очередной вопрос. GET_ANSWER.equals(action.getName())
.
final String OPEN_MAIN = "OM";
final String GET_ANSWER = "GA";
Action action = new ActionBuilder(marshaller).buld(update);
String data = update.getCallbackQuery().getData();
Long chatId = update.getCallbackQuery().getMessage().getChatId();
Если викторина только начата, то необходимо проинициализировать список вопросов и задать первый вопрос.
if (OPEN_MAIN.equals(action.getName())) {
initQuests(update);
sendQuest(update);
}
Сейчас рассмотрим инициализацию списка вопросов initQuests()
:
private void initQuests(Update update) {
QuestStateHolder questStateHolder = new QuestStateHolder();
List q = classifierRepository.find(ClsQuest.class, false);
Collections.shuffle(q);
questStateHolder.put(update, new QuestEnumeration(q));
}
В методе initQuests
сначала получим все 26 вопросов, а потом перемешаем в случайном порядке. После этого вопросы положим в QuestEnumeration
, откуда будем получать их по одному, до тех пор, пока не будут получены все 26 вопросов. QuestEnumeration
добавим в объект специального класса QuestStateHolder
, хранящего соответствие пользователя и его текущей сессии вопросов. Код классов QuestStateHolder
и QuestEnumeration
ниже.
public class QuestStateHolder{
private Map questStates = new HashMap<>();
public QuestEnumeration get(User user) {
return questStates.get(user.getId()) == null ? null : questStates.get(user.getId());
}
public QuestEnumeration get(Update update) {
User u = getUserFromUpdate(update);
return get(u);
}
public void put(Update update, QuestEnumeration questEnumeration) {
User u = getUserFromUpdate(update);
put(u, questEnumeration);
}
public void put(User user, QuestEnumeration questEnumeration) {
questStates.put(user.getId(), questEnumeration);
}
static User getUserFromUpdate(Update update) {
return update.getMessage() != null ? update.getMessage().getFrom()
: update.getCallbackQuery().getFrom();
}
}
public class QuestEnumeration implements Enumeration{
private List quests = new ArrayList<>();
private Integer currentQuest = 0;
public QuestEnumeration(List quests){
this.quests.addAll(quests);
}
@Override
public boolean hasMoreElements() {
return currentQuest < quests.size();
}
@Override
public ClsQuest nextElement() {
ClsQuest q = null;
if (hasMoreElements()){
q = quests.get(currentQuest);
currentQuest++;
}
return q;
}
public Integer getCurrentQuest(){
return currentQuest;
}
}
После инициализации задается первый вопрос. Но об этом поговорим чуть позже. А пока рассмотрим ситуацию, когда пришел ответ на уже заданный вопрос и боту необходимо отправить комментарий, относящийся к этому варианту ответа. Здесь все достаточно просто, сначала ищем ответ в базе данных (уникальный идентификатор варианта ответа сохранен в CallbackData
кнопки, на которую было произведено нажатие):
Long answId = Long.parseLong(action.getValue());
ClsAnswer answ = classifierRepository.find(ClsAnswer.class, answId);
Потом готовим на основе найденного ответа сообщение и отправляем его:
SendMessage comment = new SendMessage();
comment.setParseMode("HTML");
comment.setText("Твой ответ: "
+ answ.getAnswerText()
+ "\nКомментарий к ответу: "
+ answ.getAnswerComment() + "\n");
comment.setChatId(chatId);
execute(comment);
Теперь рассмотрим метод sendQuest
, который отправляет очередной вопрос. Начинается все с получения очередного вопроса:
QuestEnumeration qe = questStateHolder.get(update);
ClsQuest nextQuest = qe.nextElement();
Если Enumeration еще содержит элементы, то готовим вопрос к отправке, в противном случае пора выводить сообщение об окончании викторины. Отправляем сам вопрос:
Long chatId = update.getCallbackQuery().getMessage().getChatId();
SendMessage quest = new SendMessage();
quest.setParseMode("HTML");
quest.setText("Вопрос " + qe.getCurrentQuest() + ": "
+ nextQuest.getQuestText());
quest.setChatId(chatId);
execute(quest);
Теперь отправляем фотографии, относящиеся к данному вопросу:
for (ClsQuestPhoto clsQuestPhoto : nextQuest.getClsQuestPhotoCollection()) {
SendPhoto sendPhoto = new SendPhoto();
sendPhoto.setChatId(chatId);
sendPhoto.setNewPhoto(new File("\\photo" + clsQuestPhoto.getRelFilePath()));
sendPhoto(sendPhoto);
}
И, наконец, варианты ответа:
SendMessage answers = new SendMessage();
answers.setParseMode("HTML");
answers.setText("Варианты ответа:");
answers.setChatId(chatId);
answers.setReplyMarkup(keyboardAnswer(update, nextQuest));
execute(answers);
Клавиатура с вариантами ответа формируется следующим образом
private InlineKeyboardMarkup keyboardAnswer(Update update, ClsQuest quest) {
final InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
List> keyboard = new ArrayList<>();
for (ClsAnswer clsAnswer : quest.getClsAnswerCollection()) {
keyboard.add(Arrays.asList(buttonAnswer(clsAnswer)));
}
markup.setKeyboard(keyboard);
return markup;
}
private InlineKeyboardButton buttonAnswer(ClsAnswer clsAnswer) {
InlineKeyboardButton button = new InlineKeyboardButtonBuilder()
.setText(clsAnswer.getAnswerText())
.setCallbackData(new ActionBuilder(marshaller)
.setName(GET_ANSWER)
.setValue(clsAnswer.getId().toString())
.asString())
.build();
return button;
}
Вопрос отправлен. Выглядеть это будет примерно так (фотографии немного размыты, чтобы никого не смущать).
В тот момент, когда вопросы закончились, будет сформировано сообщение об окончании викторины:
SendMessage answers = new SendMessage();
answers.setParseMode("HTML");
answers.setText("Ну вот и все! Подробности на процедуре награждения \n "
+ "Если хочешь начать заново нажми кнопку 'Начать' или введи /start");
answers.setChatId(chatId);
execute(answers);
И в самом конце отправим забавный стикер:
SendSticker sticker = new SendSticker();
sticker.setChatId(chatId);
File stikerFile = new File("\\photo\\stiker.png");
sticker.setNewSticker(stikerFile);
sendSticker(sticker);
На этом работа бота завершается.
Работа над всей описанной функциональностью была завершена где-то в 11 часов вечера дня, предшествующего дню X. Замечу, что имея в виду некоторые особенности, я понимал, что запустить бота надо ровно в 12 ночи. В связи с этим я испытывал некоторый цейтнот (почему и забыл про историю ответов). Кроме того коллегу необходимо было как-то оповестить об этой викторине. По ряду причин просто скинуть ссылку я не мог, поэтому доверил оповещение другому боту (благо в процессе тестирования идентификатор пользователя был сохранен и бот мог свободно писать). На этом история написания бота заканчивается.
Заключение
Каких-то глубоких выводов из этой истории не будет. Сам бот был принят с интересом и понравился, так что старался я не зря. Если переходить к технической части вопроса, то работа над ботом позволила задуматься над тем, что от некоторых костылей и велосипедов пора избавляться, что я и попытался сделать в следующем боте.
Также хочу заметить, что сама идея ботов очень перспективна. Есть множество повседневных задач от заказа пиццы до вызова такси, которые предлагается решать посредством мобильных приложений, сайтов и их мобильных версий или телефонных звонков оператору. С одной стороны все эти способы доказали свою эффективность, удобны и в ближайшее время вряд ли изменятся. С другой стороны приложения для мобильных устройств хотя и богаты функциональностью, но требуют установки, обновления и изучения их интерфейса, а еще имеют свойство кушать батарею. Сайты и их мобильные версии требуют от пользователя как минимум перехода в браузер и работы с новым интерфейсом, что не всегда удобно, особенно на мобильных устройствах. Взаимодействие по телефону удобно для многих, но не подразумевает какой-либо визуализации в принципе, а кроме того оператор всегда будет узким местом системы. При решении тех же задач боты не требуют установки чего-либо кроме мессендежера, не требуют изучения новых интерфейсов и позволяют пользователю работать асинхронно (в отличие от телефонного звонка) в относительно знакомом интерфейсе мессенджера. Мессенджер в таком случае предоставляет некую среду для клиент-серверного взаимодействия, где в роли сервера выступает бот, а клиентская часть реализована средствами мессенджера. Безусловно, при работе с ботами для пользователя также возникают различные трудности, в чем-то схожие с трудностями работы с текстовыми интерфейсами, в чем-то обусловленные ограничениями самих мессенджеров. Но все равно боты, ориентированные на решение небольших повседневных задач (или на развлечения, как бот, описанный в этой статье) представляются перспективными.
Этой мой первый пост, буду признателен за конструктивную обратную связь!