Java telegram bot tutorial

Telegram боты на Java и где они обитают

В этом посте хочется разобрать создание ботов в телеграмме, ведь их очень интересно писать (по крайней мере, для новичков).

Для начала нам нужно создать приложение на спринге. Но я думаю, каждый уже умеет это делать.

Затем добавим зависимости, многие пользуются telegrambots-spring-boot-starter, но мне как-то не довелось увидеться с ним, поэтому используем самый обычный API.

 org.telegram telegrambots 6.5.0 

Теперь создадим файл application.yaml в папке resources. В нём напишем токен бота.
Telegram-bots ещё требует имя, но вводить настоящее — не обязательно.

bot: token: 6098243395:AAFwSeKCFxh6kOTPPfcSYTdTuhqRZyBfULA

Создадим наш первый и основной компонент. В нём мы будем регистрировать бота и обрабатывать сообщения.

@Component public class BotComponent extends TelegramLongPollingBot < // Создаём их объект для регистрации private final TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class); // Достаём токен бота @Value("$") private String botToken; @PostConstruct private void init() throws TelegramApiException < telegramBotsApi.registerBot(this); // Регистрируем бота >public BotComponent() throws TelegramApiException <> @Override public void onUpdateReceived(Update update) < //Проверим, работает ли наш бот. System.out.println(update.getMessage().getText()); >@Override public String getBotUsername() < return "bot"; >@Override public String getBotToken() < return botToken; >>

Теперь начинаем работать с косяками api телеграмма и как-то их обрабатывать.
Самая главная проблема — у api телеграмма отсутствует один общий интерфейс, который бы объединял все возможные виды апдейта (за исключением BotApiMethod). Обычное сообщение и SendPhoto разделены и у них нет ничего общего, а нам нужно выдавить абстракции для того, чтобы всё легко расширялось, поэтому нам придётся поговнокодить. (Возможно реализация этого может выглядеть лучше).

В том числе, нам нужно определить тип сообщения, для дальнейшего правильного использования.

Для этого создадим класс ClassifiedUpdate. Я использую Lombok, если вас это испугало, то почитайте, что это такое.

public class ClassifiedUpdate < @Getter private final TelegramType telegramType; // enum, чтобы всё выглядило красиво @Getter private final Long userId; // тот же chat-id, но выглядит красивее и получить его легче @Getter private String name; // получим имя пользователя. Именно имя, не @username @Getter private String commandName; // если это команда, то запишем её @Getter private final Update update; // сохраним сам update, чтобы в случае чего, его можно было достать @Getter private final Listargs; // просто поделим текст сообщения, в будущем это поможет @Getter private String userName; // @username public ClassifiedUpdate(Update update) < this.update = update; this.telegramType = handleTelegramType(); this.userId = handleUserId(); this.args = handleArgs(); this.commandName = handleCommandName(); >//Обработаем команду. public String handleCommandName() < if(update.hasMessage()) < if(update.getMessage().hasText()) < if(update.getMessage().getText().startsWith("/")) < return update.getMessage().getText().split(" ")[0]; >else return update.getMessage().getText(); > > if(update.hasCallbackQuery()) < return update.getCallbackQuery().getData().split(" ")[0]; >return ""; > //Обработаем тип сообщения private TelegramType handleTelegramType() < if(update.hasCallbackQuery()) return TelegramType.CallBack; if(update.hasMessage()) < if(update.getMessage().hasText()) < if(update.getMessage().getText().startsWith("/")) return TelegramType.Command; else return TelegramType.Text; >else if(update.getMessage().hasSuccessfulPayment()) < return TelegramType.SuccessPayment; >else if(update.getMessage().hasPhoto()) return TelegramType.Photo; > else if(update.hasPreCheckoutQuery()) < return TelegramType.PreCheckoutQuery; >else if(update.hasChatJoinRequest()) < return TelegramType.ChatJoinRequest; >else if(update.hasChannelPost()) < return TelegramType.ChannelPost; >else if(update.hasMyChatMember()) < return TelegramType.MyChatMember; >if(update.getMessage().hasDocument()) < return TelegramType.Text; >return TelegramType.Unknown; > //Достанем userId, имя и username из любого типа сообщений. private Long handleUserId() < if (telegramType == TelegramType.PreCheckoutQuery) < name = getNameByUser(update.getPreCheckoutQuery().getFrom()); userName = update.getPreCheckoutQuery().getFrom().getUserName(); return update.getPreCheckoutQuery().getFrom().getId(); >else if(telegramType == TelegramType.ChatJoinRequest) < name = getNameByUser(update.getChatJoinRequest().getUser()); userName = update.getChatJoinRequest().getUser().getUserName(); return update.getChatJoinRequest().getUser().getId(); >else if (telegramType == TelegramType.CallBack) < name = getNameByUser(update.getCallbackQuery().getFrom()); userName = update.getCallbackQuery().getFrom().getUserName(); return update.getCallbackQuery().getFrom().getId(); >else if(telegramType == TelegramType.MyChatMember) < name = update.getMyChatMember().getChat().getTitle(); userName = update.getMyChatMember().getChat().getUserName(); return update.getMyChatMember().getFrom().getId(); >else < name = getNameByUser(update.getMessage().getFrom()); userName = update.getMessage().getFrom().getUserName(); return update.getMessage().getFrom().getId(); >> //Разделим сообщение на аргументы private List handleArgs() < Listlist = new LinkedList<>(); if(telegramType == TelegramType.Command) < String[] args = getUpdate().getMessage().getText().split(" "); Collections.addAll(list, args); list.remove(0); return list; >else if (telegramType == TelegramType.Text) < list.add(getUpdate().getMessage().getText()); return list; >else if (telegramType == TelegramType.CallBack) < String[] args = getUpdate().getCallbackQuery().getData().split(" "); Collections.addAll(list, args); list.remove(0); return list; >return new ArrayList<>(); > //Вынесли имя в другой метод private String getNameByUser(User user) < if(user.getIsBot()) return "BOT"; if(!user.getFirstName().isBlank() || !user.getFirstName().isEmpty()) return user.getFirstName(); if(!user.getUserName().isBlank() || !user.getUserName().isEmpty()) return user.getUserName(); return "noname"; >//Лог public String getLog()

Это выглядит ужасно и некрасиво, обязательно как-то отрефакторим это, но не сегодня.
Хотел бы объяснить, зачем я разделил @username и Имя Фамилия.

Дело в том, что некоторые пользователи не имеют имя и фамилию в настройках профиля, а некоторые имеют только это. В общем, мы предусмотрели этот момент. И теперь если мы захотим написать: Привет, Илья! У нас никогда не будет: Привет, null!. Мы ведь не хотим отставать от глаза бога.

Читайте также:  Boolean in python format string

Тем, кому лень писать код, держите TelegramType:

Двигаемся дальше, мы обработали их апдейт и теперь нам пора обработать свой апдейт, но перед этим нам нужно создать ещё свой ответ. Выглядит он не так ужасно, но ужасно 🙂

Это нам очень сильно поможет в будущем, нужно только верить.

@Data public class Answer < private SendDocument sendDocument; private SendPhoto sendPhoto; private SendVideo sendVideo; private SendVideoNote sendVideoNote; private SendSticker sendSticker; private SendAudio sendAudio; private SendVoice sendVoice; private SendMediaGroup sendMediaGroup; private SetChatPhoto setChatPhoto; private AddStickerToSet addStickerToSet; private SetStickerSetThumb setStickerSetThumb; private CreateNewStickerSet createNewStickerSet; private UploadStickerFile uploadStickerFile; private EditMessageMedia editMessageMedia; private SendAnimation sendAnimation; private BotApiMethodbotApiMethod; >

На самом деле, всё можно сделать и без этого класса, если вы собираетесь отвечать пользователю только сообщениями или коллбэками. Потому что в будущем этот класс ещё и увеличит немного кода. Я лишь стараюсь увеличить расширяемость, чтобы внедрение новой фичи делалось быстро и легко.

Теперь нам как-то нужно работать с пользователями, поэтому с помощью Spring JPA создадим сущность пользователя.

@Entity @Table(name = "users") @Getter @Setter public class User

Как вы можете заметить, у пользователя есть состояние, это поможет нам для проведения интерактивов и т.д. Также я использую у permissions тип Long, потому что обычно это:

Это просто и удобно и лениво, но если кто-то хочет, то может заморочиться.
Вернёмся к состоянию, напишем простую сущность для состояния :

@Entity @Table(name = "state") @Getter @Setter public class State < @GeneratedValue(strategy = GenerationType.IDENTITY) @Id private Long Id; @Column(name = "value") private String stateValue; public boolean inState() < return stateValue != null; >@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn private User user; >

Для чего нам нужно состояние?
К примеру, пользователь захотел пополнить баланс, и мы просим его ввести сумму пополнения. Если мы не узнаем, что прямо сейчас он вводит сумму пополнения, то будем обрабатывать его команду: 100, как обычную. В общем, нам нужно состояние.

Читайте также:  Php get file content image

Дальше нам нужно создать обработчик сообщений, в нашем случае они будут разные и их будет много, поэтому создадим интерфейс Handler.

@MappedSuperclass public interface Handler < // Какой тип сообщения будет обработан TelegramType getHandleType(); // Приоритет обработчика int priority(); // Условия, при которых мы воспользуемся этим обработчиком boolean condition(User user, ClassifiedUpdate update); // В этом методе, с помощью апдейта мы будем получать answer Answer getAnswer(User user, ClassifiedUpdate update); >

Обработчик выполняет функцию хранения комманд. Теперь нам нужно создать команды для обработчика. Создадим интерфейс Command.

@MappedSuperclass public interface Command < // Каким обработчиком будет пользоваться команда Class handler(); // С помощью чего мы найдём эту команду Object getFindBy(); // Ну и тут мы уже получим ответ на самом деле Answer getAnswer(ClassifiedUpdate update, User user); >

Теперь как-то надо найти команды для обработчика, поэтому создадим класс AbstractHandler.

@MappedSuperclass public abstract class AbstractHandler implements Handler < protected final MapallCommands = new HashMap<>(); // Найдём все команды для обработчика @Autowired private List commands; protected abstract HashMap createMap(); // Тут мы распихиваем команды по хэшмапе, чтобы потом было удобнее доставать :/ @PostConstruct private void init() < commands.forEach(c -> < allCommands.put(c.getFindBy(), c); if(Objects.equals(c.handler().getName(), this.getClass().getName())) < createMap().put(c.getFindBy(), c); System.out.println(c.getClass().getSimpleName() + " was added for " + this.getClass().getSimpleName()); >>); > >

Это конечно всё хорошо, но нам нужно собрать все обработчики в одном месте. И отправить наш ClassifiedUpdate в эту бездонную бочку. Назовём эту штуку HandlersMap, просто потому что я снова распихиваю обработчики по хэшмапе 🙂

@Component public class HandlersMap < private HashMap> hashMap = new HashMap<>(); private final List handlers; // Тут точно также находим все обработчики, просто в первом случае я использовал // @Autowired. Это немного лучше. public HandlersMap(List handlers) < this.handlers = handlers; >@PostConstruct private void init() < for(Handler handler : handlers) < if(!hashMap.containsKey(handler.getHandleType())) hashMap.put(handler.getHandleType(), new ArrayList<>()); hashMap.get(handler.getHandleType()).add(handler); > hashMap.values().forEach(h -> h.sort(new Comparator() < @Override public int compare(Handler o1, Handler o2) < return o2.priority() - o1.priority(); >>)); > public Answer execute(ClassifiedUpdate classifiedUpdate, User user) < if(!hashMap.containsKey(classifiedUpdate.getTelegramType())) return new Answer(); for (Handler handler : hashMap.get(classifiedUpdate.getTelegramType())) < if(handler.condition(user, classifiedUpdate)) return handler.getAnswer(user, classifiedUpdate); >return null; > >

Теперь нам нужна ещё прослойка в виде ClassifiedUpdateHandler’a. Там мы будем доставать пользователя из базы данных и может что-то ещё. Просто добавим его.
Класс ClassifiedUpdateHandler:

@Service public class ClassifiedUpdateHandler < private final UserService userService; private final HandlersMap commandMap; public ClassifiedUpdateHandler(UserService userService, HandlersMap commandMap) < this.userService = userService; this.commandMap = commandMap; >public Answer request(ClassifiedUpdate classifiedUpdate) < return commandMap.execute(classifiedUpdate, userService.findUserByUpdate(classifiedUpdate)); >>

Тут ничего особенного, пропустим объяснения. Намного интереснее в классе UserService.
До этого, благо, мы успели всё обработать и на 100% достать id пользователя и его имя.

@Service public class UserService < private final UserRepository userRepository; private final StateRepository stateRepository; public UserService(UserRepository userRepository, StateRepository stateRepository) < this.userRepository = userRepository; this.stateRepository = stateRepository; >public User findUserByUpdate(ClassifiedUpdate classifiedUpdate) < // Проверим, существует ли этот пользователь. if(userRepository.findByChatId(classifiedUpdate.getUserId()) != null) < User user = userRepository.findByChatId(classifiedUpdate.getUserId()); // Если мы не смогли до этого записать имя пользователя, то запишем его. if(user.getUserName() == null && classifiedUpdate.getUserName() != null) user.setUserName(classifiedUpdate.getUserName()); // Проверим менял ли пользователя имя. if(user.getUserName() != null) if (!user.getUserName().equals(classifiedUpdate.getUserName())) user.setUserName(classifiedUpdate.getUserName()); if(!user.getName().equals(classifiedUpdate.getName())) user.setName(classifiedUpdate.getName()); return user; >try < User user = new User(); user.setName(classifiedUpdate.getName()); user.setPermissions(0L); user.setChatId(classifiedUpdate.getUserId()); user.setUserName(classifiedUpdate.getUserName()); State state = new State(); state.setStateValue(null); state.setUser(user); stateRepository.save(state); user.setState(state); userRepository.save(user); return user; >catch (Exception e) < e.printStackTrace(); >return null; > > 

Всё готово, теперь пора создать наш первый Handler и Command для примера. Но для начала напишем Builder для сообщений.

public class SendMessageBuilder < private SendMessage sendMessage; public SendMessageBuilder() < this.sendMessage = new SendMessage(); >public SendMessageBuilder chatId(Long chatId) < this.sendMessage.setChatId(chatId); return this; >public SendMessageBuilder message(String message) < this.sendMessage.setText(message); return this; >public Answer build() throws Exception < if(sendMessage.getChatId() == null) throw new Exception("Id must be not null"); Answer answer = new Answer(); answer.setBotApiMethod(sendMessage); return answer; >>

Вот теперь можем написать Handler и Command.

@Component public class CommandHandler extends AbstractHandler < private HashMaphashMap = new HashMap<>(); @Override protected HashMap createMap() < return hashMap; >@Override public TelegramType getHandleType() < return TelegramType.Command; >@Override public int priority() < return 1; >@Override public boolean condition(User user, ClassifiedUpdate update) < return hashMap.containsKey(update.getCommandName()); >@Override public Answer getAnswer(User user, ClassifiedUpdate update) < return hashMap.get(update.getCommandName()).getAnswer(update, user); >>
@Component public class StartCommand implements Command < @Override public Class handler() < return CommandHandler.class; >@Override public Object getFindBy() < return "/start"; >@SneakyThrows @Override public Answer getAnswer(ClassifiedUpdate update, User user) < return new SendMessageBuilder().chatId(user.getChatId()).message("Hello!").build(); >>

Я постарался сделать практическое пособие. Тут нужно много чего дорабатывать.

Читайте также:  Apache with php module

Код я писал очень давно, поэтому что-то возможно уже нужно обновить, просто решил опубликовать свои наработки в открытый доступ.

В итоге должен получиться простой и расширяемый бот.

Если эта статья вам понравиться, то можно всё допилить и получить невероятно мощную штуку для написания телеграмм ботов, к примеру, выкатить свои аннотации и т.д.

Спасибо за внимание!

Источник

Оцените статью