我的名字是弗拉基米尔。我今年43岁。读者,如果你已经超过 40 岁了,那么是的,40 岁以后你可以成为一名程序员,如果你愿意的话。我的工作与编程完全无关,我是自动化领域的项目经理等。但我正计划改变我的职业。哦,这些新趋势......每 5-7 年改变一次您的活动领域。 所以:这个项目相当大,所以有些要点必须省略或简单谈一下,希望读者知道如何谷歌。互联网上充斥着根据长轮询原理工作的电报机器人的出版物。而按照Webhook原理工作的却很少。这是什么? 长轮询- 这意味着您的应用程序本身将以一定的频率缓慢地轮询电报服务器以获取消息。 Webhook - 意味着电报服务器将立即将消息重定向到您指定的服务器。在我们的例子中,这是由 Heroku 服务提供的。当然,您可以在 Telegram 网站上阅读有关所有这些以及有关机器人的更多信息 - https://tlgrm.ru/docs/bots/api 机器人界面如下所示: 我认为这个应用程序正是作为一个训练项目的原因是在写作时我从这个机器人中学到了比训练时更多的信息。你想学习编程吗?开始写代码!!!但!不会有关于如何将应用程序上传到 github 或如何创建数据库的详细说明。互联网上有很多这样的内容,并且描述得很详细;此外,这将是一个很长的阅读过程。该应用程序的工作方式如下:输入事件的描述,输入事件的日期和时间,选择频率(您可以执行一次,您可以每天在某个时间提醒,您可以执行一次每月的某个时间,或每年一次)。通知的变体可以无限添加;我有很多想法。接下来,输入的数据被保存到数据库中(在 Heroku 上也是免费部署的,10,000 行是免费的)然后,在服务器时间 0:00 的一天开始时,Spring 根据条件从数据库中检索所有事件应该在那天触发并在指定时间发送它们执行。注意力!!!该计划的这一部分是实验性的!有一个更简单、更真实的实现!这样做是专门为了了解时间类别是如何运作的!您可以通过在购物车中输入 @calendar_event_bot 来亲手触摸工作机器人,但不要指望它,因为我仍在取笑它。代码 - https://github.com/papoff8295/webHookBotForHabr 基本上,要启动自己的项目,您需要执行以下步骤: • 向@BotFather注册,这并不难,获取令牌和名称 • 在 github 上分叉项目 • 注册在Heroku上,创建一个应用程序(我们将逐步完成它),从您的存储库进行部署。• 在 Heroku 上创建数据库 • 将存储库中的相应字段替换为您自己的字段(令牌、实体中表的名称、webHookPath、用户名、密码和数据库路径,这些都将被解析) • 让 Heroku 工作 24/ 7 使用https://uptimerobot.com/ 项目的最终结构如下: 首先我们在https://start.spring.io中创建一个项目选择我们需要的依赖如图: 选择我们自己的为项目命名并单击“生成”。然后系统将提示您将项目保存到磁盘。剩下的就是从开发环境中打开pom.xml文件。您面前有一个已完成的项目。现在我们只需要添加我们的主库。我使用了https://github.com/rubenlagus/TelegramBots中的库 一般来说,您可能会感到困惑并且不需要它。毕竟,工作的全部要点就是连接这样的 URL: https://api.telegram.org/bot1866835969:AAE6gJG6ptUyqhV2XX0MxyUak4QbAGGnz10/setWebhook?url=https://e9c658b548aa.ngrok.io 让我们稍微看一下: https: //api.telegram.org – 电报服务器。 bot1866835969:AAE6gJG6ptUyqhV2XX0MxyUak4QbAGGnz10/ - bot 一词后面是您在注册机器人时收到的秘密令牌。 setWebhook?url=https://e9c658b548aa.ngrok.io – 方法的名称及其参数。在这种情况下,我们安装您的 webhook 服务器,所有消息都将发送到它。总的来说,我认为该项目对于发布来说并不算太小,但如果手动实现,它通常会难以阅读。所以, pom文件的最终样子是:
<!--?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>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-parent</artifactid>
<version>2.5.0</version>
<relativepath> <!-- lookup parent from repository -->
</relativepath></parent>
<groupid>ru.popov</groupid>
<artifactid>telegrambot</artifactid>
<version>0.0.1-SNAPSHOT</version>
<name>telegrambot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-data-jpa</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-test</artifactid>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.telegram/telegrambots-spring-boot-starter -->
<dependency>
<groupid>org.telegram</groupid>
<artifactid>telegrambots-spring-boot-starter</artifactid>
<version>5.2.0</version>
</dependency>
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<version>1.18.16</version>
</dependency>
<dependency>
<groupid>org.postgresql</groupid>
<artifactid>postgresql</artifactid>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-maven-plugin</artifactid>
</plugin>
</plugins>
</build>
</project>
一切准备就绪,可以编写我们的机器人了。让我们创建TelegramBot类。文件夹的名字我就不写了,大家可以在上面的项目结构中查看。
@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramBot extends SpringWebhookBot {
String botPath;
String botUsername;
String botToken;
private TelegramFacade telegramFacade;
public TelegramBot(TelegramFacade telegramFacade, DefaultBotOptions options, SetWebhook setWebhook) {
super(options, setWebhook);
this.telegramFacade = telegramFacade;
}
public TelegramBot(TelegramFacade telegramFacade, SetWebhook setWebhook) {
super(setWebhook);
this.telegramFacade = telegramFacade;
}
@Override
public BotApiMethod<!--?--> onWebhookUpdateReceived(Update update) {
return telegramFacade.handleUpdate(update);
}
}
该类从我们的电报库扩展了SpringWebhookBot,我们只需要实现一种方法,即onWebhookUpdateReceived。它接受解析后的JSON作为 Update 对象,并返回电报服务器想要从我们那里“听到”的内容。这里我们有来自Lombok库的注释。Lombok – 让程序员的生活更轻松!嗯,就是这样。我们不需要重新定义 getter 和 setter,Lombok 为我们做了这件事,而且我们也不需要编写访问级别标识符。不再值得写的是,这是通过注解@Getter、@Setter、@FieldDefaults完成的。 botPath 字段表示我们的 webhook 地址,稍后我们将在 Heroku 上收到该地址。botUsername字段表示我们的机器人的名称,我们在 Telegram 中注册我们的机器人时将收到该名称。botToken字段是我们的令牌,我们在 Telegram 中注册机器人时将收到该令牌。telegramFacade字段是我们将进行消息处理的类,稍后我们将返回它,现在让它为红色。现在是时候联系@BotFather并获取令人垂涎的 botToken 和 botUsername。 只要写电报给他,他就会告诉你一切。我们将数据写入application.properties,最终它会是这样的:
server#server.port=5000
telegrambot.userName=@calendar_event_bot
telegrambot.botToken=1731265488:AAFDjUSk3vu5SFfgdfh556gOOFmuml7SqEjwrmnEF5Ak
#telegrambot.webHookPath=https://telegrambotsimpl.herokuapp.com/
telegrambot.webHookPath=https://f5d6beeb7b93.ngrok.io
telegrambot.adminId=39376213
eventservice.period =600000
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/telegramUsers
spring.datasource.username=postgres
spring.datasource.password=password
#spring.datasource.url=jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um126le5notq?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory
#spring.datasource.username=ulmbeywyhvsxa
#spring.datasource.password=4c7646c69dbbgeacb98fa96e8daa6d9b1bl4894e67f3f3ddd6a27fe7b0537fd
此配置配置为使用本地数据库;稍后我们将进行必要的更改。将botToken和用户名替换为您自己的。直接在应用程序中使用 application.properties 中的数据并不好。让我们根据这些数据创建一个 bean 或一个包装类。
@Component
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramBotConfig {
@Value("${telegrambot.webHookPath}")
String webHookPath;
@Value("${telegrambot.userName}")
String userName;
@Value("${telegrambot.botToken}")
String botToken;
这里,@Value 注释初始化 application.properties 文件中的相应行,Spring 默认情况下知道该文件。并且@Component注解会在应用程序启动时为我们创建一个Bean。现在让我们转到 Spring 配置文件:
@Configuration
public class AppConfig {
private final TelegramBotConfig botConfig;
public AppConfig(TelegramBotConfig botConfig) {
this.botConfig = botConfig;
}
@Bean
public SetWebhook setWebhookInstance() {
return SetWebhook.builder().url(botConfig.getWebHookPath()).build();
}
@Bean
public TelegramBot springWebhookBot(SetWebhook setWebhook, TelegramFacade telegramFacade) {
TelegramBot bot = new TelegramBot(telegramFacade, setWebhook);
bot.setBotToken(botConfig.getBotToken());
bot.setBotUsername(botConfig.getUserName());
bot.setBotPath(botConfig.getWebHookPath());
return bot;
}
}
这里没有什么魔法;在启动时,Spring 为我们创建 SetWebhook 和 TelegramBot 对象。现在让我们为消息创建入口点:
@RestController
public class WebhookController {
private final TelegramBot telegramBot;
public WebhookController(TelegramBot telegramBot) {
this.telegramBot = telegramBot;
}
// point for message
@PostMapping("/")
public BotApiMethod<!--?--> onUpdateReceived(@RequestBody Update update) {
return telegramBot.onWebhookUpdateReceived(update);
}
@GetMapping
public ResponseEntity get() {
return ResponseEntity.ok().build();
}
}
Telegram 服务器使用 POST 方法将 JSON 格式的消息发送到注册的 webhook 地址,我们的控制器接收它们并以 Update 对象的形式将它们传输到 telegram 库。我就是这样做的 get 方法)现在我们只需要在TelegramFacade类中实现一些处理消息和响应的逻辑,我将给出它的简短代码,以便您可以启动应用程序,然后走自己的路或切换到部署在 Heroku 上,那么它将是完整版本:
@Component
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramFacade {
public BotApiMethod<!--?--> handleUpdate(Update update) {
if (update.hasCallbackQuery()) {
CallbackQuery callbackQuery = update.getCallbackQuery();
return null;
} else {
Message message = update.getMessage();
SendMessage sendMessage = new SendMessage();
sendMessage.setChatId(String.valueOf(message.getChatId()));
if (message.hasText()) {
sendMessage.setText("Hello world");
return sendMessage;
}
}
return null;
}
}
此方法将响应任何 Hello world! 为了启动我们的应用程序,我们只需要确保我们可以直接从 IDEA 测试我们的应用程序。为此,我们需要下载 ngrok 实用程序。https://ngrok.com/download 这个实用程序是一个命令行,为我们提供了 2 小时的临时地址,并将所有消息重定向到本地服务器的指定端口。我们启动并在行中写入ngrok http 5000(或者您可以指定您的端口): 我们得到结果: https: //23b1a54ccbbd.ngrok.io - 这是我们的 webhook 地址。你可能已经注意到,在启动tomcat服务器时,我们在属性文件中写入了server.port=5000,它将占用5000端口,请确保这些字段相同。另外,不要忘记该地址的有效期为两个小时。在命令行上,这由“会话过期”字段进行监控。当时间用完时,您将需要再次获取地址并完成在电报上注册的过程。现在我们采取 https://api.telegram.org/bot1866835969:AAE6gJG6ptUyqhV2XX0MxyUak4QbAGGnz10/setWebhook?url=https://e9c658b548aa.ngrok.io 行并通过灵活的手部动作,我们将令牌替换为我们的,将 url 替换为我们的,粘贴将结果行输入浏览器并单击 Enter。您应该得到以下结果: 就是这样,现在您可以运行该应用程序: 检查您的类的main方法是否如下所示:
@SpringBootApplication
public class TelegramBotApplication {
public static void main(String[] args) {
SpringApplication.run(TelegramBotApplication.class, args);
}
}
如果您所做的一切正确,现在您的机器人将用短语“ Hello world” 响应任何消息。然后你就可以走自己的路了。如果您和我一样并且有兴趣完成所有步骤,那么让我们开始为数据库编写实体并创建数据库本身。让我们从数据库开始:正如我已经说过的,我假设您已经具备了使用数据库的最低技能,并且安装了本地postgreSQL数据库,如果没有,请按照此路径操作。https://www.postgresql.org/download/ 在 application.properties 文件中,将数据库登录名和密码替换为您自己的。在IDEA中,右侧有一个数据库选项卡,您需要在其中单击+/Data source/PostgreSQL。因此,当您单击“测试连接”时,您应该会得到满意的结果: 现在您可以直接从开发环境创建带有表的数据库,或者您可以使用位于开始菜单中的pgadmin Web 界面。我们需要 3 个表:
CREATE TABLE users
(
id INTEGER PRIMARY KEY UNIQUE NOT NULL,
name VARCHAR,
time_zone INTEGER DEFAULT 0,
on_off BOOLEAN DEFAULT true
);
CREATE TABLE user_events
(
user_id INTEGER ,
time timestamp ,
description varchar ,
event_id serial,
event_freq varchar default 'TIME',
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
CREATE TABLE event_cash
(
time timestamp ,
description varchar ,
user_id INTEGER ,
id serial
);
我建议单独创建这个脚本;我们需要它在 Heroku 上创建数据库,以免编写两次。我们走一会儿吧。我会立即说,由于它的具体情况以及我对使用Time类的无尽渴望,我们只需要event_cash表即可与 Heroku 一起使用;它在本地版本中没有用处。在users表中,我们将记录telegram 用户的帐户ID、姓名(可能不存在)、将计算用户的时区以正确发送通知,以及发送通知的开/关状态。我们将在user_events表中记录用户id 、通知时间、描述,自动生成事件的id,并设置通知的频率。event_cash表将在发送之前记录通知,如果发送,则将其从表中删除。表已准备就绪,现在让我们添加实体。
@Entity
@Table(name = "user_events")
@Getter
@Setter
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column( name = "event_id", columnDefinition = "serial")
private int eventId;
@Column(name = "time")
@NotNull(message = "Need date!")
private Date date;
@Column(name = "description")
@Size(min = 4, max = 200, message = "Description must be between 0 and 200 chars!")
private String description;
@Column(name = "event_freq", columnDefinition = "TIME")
@Enumerated(EnumType.STRING)
private EventFreq freq;
@ManyToOne(fetch=FetchType.EAGER)
@JoinColumn(name="user_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private User user;
public Event() {
}
public Event(int eventId,
@NotNull(message = "Need date!") Date date,
@Size(min = 4, max = 200, message = "Description must be between 0 and 200 chars!")
String description,
EventFreq freq, User user) {
this.eventId = eventId;
this.date = date;
this.description = description;
this.freq = freq;
this.user = user;
}
}
@Entity
@Table(name = "users")
@Getter
@Setter
public class User {
@Id
@Column(name = "id")
private long id;
@Column(name = "name")
private String name;
@Column(name = "time_zone", columnDefinition = "default 0")
//sets the broadcast time of events for your time zone
private int timeZone;
@OneToMany(mappedBy="user")
private List<event> events;
@Column(name = "on_off")
// on/off send event
private boolean on;
public User() {
}
}
</event>
@Entity
@Table(name = "event_cash")
@Getter
@Setter
//serves to save unhandled events after rebooting heroku
public class EventCashEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column( name = "id", columnDefinition = "serial")
private long id;
@Column(name = "time")
private Date date;
@Column(name = "description")
private String description;
@Column(name = "user_id")
private long userId;
public EventCashEntity() {
}
public static EventCashEntity eventTo(Date date, String description, long userId) {
EventCashEntity eventCashEntity = new EventCashEntity();
eventCashEntity.setDate(date);
eventCashEntity.setDescription(description);
eventCashEntity.setUserId(userId);
return eventCashEntity;
}
}
让我们回顾一下要点。 @Entity – 标记我们的dada jpa的类,该类是数据库的实体,即 当从数据库中检索数据时,它将以 Event、User 和 EventCashEntity 对象的形式呈现给我们。 @Table – 我们说数据库中表的名称。为了确保表名不带有红色下划线,我们需要在IDEA中同意建议的纠错选项,然后单击分配数据源。并在那里选择我们的基地。 @id 和 @GenerateValue - Spring 使用它来创建数据库(如果数据库尚不存在)。 @Column用于指示表中字段的名称(如果它们不匹配),但良好代码的规则建议您始终这样写。OneToMany态度- 我建议花时间弄清楚它是什么在这里 https://en.wikibooks.org/wiki/Java_Persistence 我无法更清楚地解释它,请相信我。我只想说,在这种情况下,@OneToMany注释表示一个用户可以有多个事件,并且它们将以列表的形式提供给我们。现在我们需要从表中获取数据。在SRING DATA JPA库中,所有内容都已为我们编写,我们只需为每个表创建一个接口并从 JpaRepository 扩展它。
public interface EventRepository extends JpaRepository<event, long=""> {
Event findByEventId(long id);
}
public interface UserRepository extends JpaRepository<user, long=""> {
User findById(long id);
}
public interface EventCashRepository extends JpaRepository<eventcashentity, long=""> {
EventCashEntity findById(long id);
}
</eventcashentity,></user,></event,>
使用数据库的主要逻辑被转移到服务,即所谓的数据访问对象(DAO):
@Service
public class UserDAO {
private final UserRepository userRepository;
@Autowired
public UserDAO(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findByUserId(long id) {
return userRepository.findById(id);
}
public List<user> findAllUsers() {
return userRepository.findAll();
}
public void removeUser(User user) {
userRepository.delete(user);
}
public void save(User user) {
userRepository.save(user);
}
public boolean isExist(long id) {
User user = findByUserId(id);
return user != null;
}
}
@Service
public class EventDAO {
private final UserRepository userRepository;
private final EventRepository eventRepository;
@Autowired
public EventDAO(UserRepository userRepository, EventRepository eventRepository) {
this.userRepository = userRepository;
this.eventRepository = eventRepository;
}
public List<event> findByUserId(long userId) {
User user = userRepository.findById(userId);
return user.getEvents();
}
public List<event> findAllEvent() {
return eventRepository.findAll();
}
public Event findByEventId(long eventId) {
return eventRepository.findByEventId(eventId);
}
public void remove(Event event) {
eventRepository.delete(event);
}
public void save(Event event) {
eventRepository.save(event);
}
}
</event></event></user>
@Service
//handles events not dispatched after reboot heroku
public class EventCashDAO {
private EventCashRepository eventCashRepository;
@Autowired
public void setEventCashRepository(EventCashRepository eventCashRepository) {
this.eventCashRepository = eventCashRepository;
}
public List<eventcashentity> findAllEventCash() {
return eventCashRepository.findAll();
}
public void save(EventCashEntity eventCashEntity) {
eventCashRepository.save(eventCashEntity);
}
public void delete(long id) {
eventCashRepository.deleteById(id);
}
}
</eventcashentity>
在这种情况下,我们没有任何数据处理,我们只是从表中检索数据。我们已经准备好提供T elegramFacade类的完整代码并开始分析逻辑了。
@Component
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TelegramFacade {
final MessageHandler messageHandler;
final CallbackQueryHandler callbackQueryHandler;
final BotStateCash botStateCash;
@Value("${telegrambot.adminId}")
int adminId;
public TelegramFacade(MessageHandler messageHandler, CallbackQueryHandler callbackQueryHandler, BotStateCash botStateCash) {
this.messageHandler = messageHandler;
this.callbackQueryHandler = callbackQueryHandler;
this.botStateCash = botStateCash;
}
public BotApiMethod<!--?--> handleUpdate(Update update) {
if (update.hasCallbackQuery()) {
CallbackQuery callbackQuery = update.getCallbackQuery();
return callbackQueryHandler.processCallbackQuery(callbackQuery);
} else {
Message message = update.getMessage();
if (message != null && message.hasText()) {
return handleInputMessage(message);
}
}
return null;
}
private BotApiMethod<!--?--> handleInputMessage(Message message) {
BotState botState;
String inputMsg = message.getText();
//we process messages of the main menu and any other messages
//set state
switch (inputMsg) {
case "/start":
botState = BotState.START;
break;
case "Мои напоминания":
botState = BotState.MYEVENTS;
break;
case "Создать напоминание":
botState = BotState.CREATE;
break;
case "Отключить напоминания":
case "Включить напоминания":
botState = BotState.ONEVENT;
break;
case "All users":
if (message.getFrom().getId() == adminId)
botState = BotState.ALLUSERS;
else botState = BotState.START;
break;
case "All events":
if (message.getFrom().getId() == adminId)
botState = BotState.ALLEVENTS;
else botState = BotState.START;
break;
default:
botState = botStateCash.getBotStateMap().get(message.getFrom().getId()) == null?
BotState.START: botStateCash.getBotStateMap().get(message.getFrom().getId());
}
//we pass the corresponding state to the handler
//the corresponding method will be called
return messageHandler.handle(message, botState);
}
}
我们看看需要哪些字段
final MessageHandler messageHandler;
final CallbackQueryHandler callbackQueryHandler;
final BotStateCash botStateCash;
如果我们都在一个类中编写代码,那么我们最终会得到一块登上月球的脚布;因此,我们将处理文本消息的逻辑分配给 MessageHandler类,并将处理回调查询消息的逻辑分配给CallbackQueryHandler类。是时候弄清楚allbackquery是什么以及有哪些类型的菜单了。为此,我将给您提供另一张机器人界面的图片: 有两种类型的菜单。那些附加到窗口底部的 - 主菜单,以及那些附加到消息的按钮,例如删除、编辑、更改皮带的按钮。当您单击主菜单按钮时,会发送一条同名消息,例如,当您单击“我的提醒”时,只会发送文本“我的提醒”。并且在安装callbackquery键盘时,会为每个按钮设置一个特定值,并且将发送该值而不将其显示在屏幕上。接下来我们有BotStateCash字段。这是一个专门创建的类,它将监视机器人的状态,注意,这是一个复杂的元素,你需要紧张。超出了字符数,顺便说一下,任何地方都没有写))。这是第二部分的链接
GO TO FULL VERSION