少年,来个telebot耍耍?
前段时间忽然对telegram bot感兴趣,于是动手写了一个,功能非常简单,就是把转发给它的所有图片都保存下来~
基础知识
官方文档里面的东西非常庞杂,简明扼要地总结出以下几点:
- 想要折腾telegram bot,首先要申请一个telebot,申请的方式是打开@botfather,跟他聊几句,然后按他的指引创建一个bot。重要的是拿到一个token,以及你的bot的名字,所有的bot名字都以bot结尾。拿到的token形如
123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getMe,botname形如mylovelytelebot -
所有的API都以REST API的方式提供,即向某个固定格式的URL发起get或者post请求,该网址如:
https://api.telegram.org/bot<token>/METHOD_NAME 其中token就是上面申请的token,method_name是官方支持的各种操作,完整例子如: https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getMe所以,操作bot的方式就是不断向
https://api.telegram.org/bot<token>/METHOD_NAME这样一个地址发请求,获取别人发到telebot的信息,决定如何处理,并发回处理的结果。有些方法是接受参数的,既可以用get方法,将参数在url中写成param,也就是:
https://api.telegram.org/bot<token>/METHOD_NAME?param1=xxx¶m2=xxx也可以POST一个JSON或XML来传递参数。
-
对于一个聊天机器人来说,最重要的操作当然是获取发给自己的信息了,官方提供了两种方式来获取聊天信息,分别是
getUpdates方法,和webHook。
getUpdates方法:就是向:https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getUpdates发送一个get请求,将会得到一个Json对象,这个Json对象是Update对象的序列化结果。在各种封装好的库里面,都有这样的一个对象。webHook,就是自己开一个web服务器,在某个端口和地址上监听HTTP的POST请求,再将这个地址注册到telegram,之后telegram会保证你的bot收到信息之后,发送请求到这个地址,你就可以处理它了,详细情况一会再讲。
- 获取消息之后,就是对消息进行各种处理,你可以保存消息内容,不做任何处理,也可以通过调用各种method,向bot发送各种命令,如发送信息、发送语音、发送图片、发送视频、删除表情、删除信息等。都是不同的method。
可用的库
上面的链接里各种封装好的库都有,不用再自己写繁琐的HTTP请求了。作为一个工作是写java的,虽然很多时候深感java的verbose,但仍然觉得自己的所有项目都应该用java写~主要是练手,所以以下示例代码都是java写的。
两个示例代码
为了不被如何发请求这种细节所困扰,我用了官方推荐的java框架,在maven中添加:
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId>
<version>4.1.2</version>
</dependency>
为了处理别人发给聊天机器人的信息,我们首先要获取信息,这也是两个示例代码的区别所在。正如上文所说,获取信息有两种方式,一种是getUpdates方法,由你主动去服务器pull回来结果,一种是注册webhook,当有结果时,服务器主动push到你的机器来处理。
在这里我并没有使用这个库推荐的编程方式,即继承抽象类TelegramLongPollingBot或TelegramWebhookBot,之后注册到TelegramBotsApi的方式,因为没心思去追踪注册后到底怎么处理消息的,所以只是用了他们的一些类和工具,轮询和webhook都是自己实现。
- getUpdates
这是你主动向服务器发申请取得updates,一般用轮询。
public class LongPullingService extends DefaultAbsSender { //BotConfig是自己的写的类,保存了类似文件保存地址、botToken等数据,结构在最后 private final BotConfig botConfig; //用来保存哪些数据处理过了。因为轮询时会得到重复的数据 private Set<Integer> downloaded = new HashSet<>(); //downloaded持久化后的文件,因为懒得用数据库,所以直接写txt了 private java.io.File downloadedRecord; private int offset = 0; public LongPullingService(DefaultBotOptions options, BotConfig botConfig) { super(options); log.info("longPullingService bot config is " + botConfig.toString()); this.botConfig = botConfig; this.downloadedRecord = new java.io.File(botConfig.getDownloadRootFolder(), botConfig.getDownloadedRecord()); try { downloaded.addAll(FileUtils.readLines(downloadedRecord, "utf-8").stream().map(Integer::valueOf).collect(Collectors.toSet())); } catch (Exception e) { //no file? } } public void pulling() { while (true) { try { GetUpdates getUpdates = new GetUpdates().setOffset(offset).setLimit(100); List<Update> updates = sendApiMethod(getUpdates); for (Update update : updates) { //自己写的方法,处理update onUpdateReceived(update); //自己写的方法,以秒为单位休眠 sleep(1); } if (updates.size() == 100) { offset++; } sleep(10); } catch (Exception e) { sleep(300); } } } private void onUpdateReceived(Update update) { log.info("get unprocessed update " + update.getUpdateId()); //这是自己写的方法 downLoadFile(update); } @Override public String getBotToken() { return botConfig.getToken(); } //下载方法的简单示例 public void downLoadFile(Update update){ Message message = update.getMessage(); List<PhotoSize> photos = message.getPhoto(); //在update中只能得到一个fileId,还需要向服务器发送一个GetFile请求获取文件下载地址 GetFile getFile = null; String fileTypeSuffix = null; //一般一个message里有好几个图片,有缩略图级别的,有高清大图级别的 //在这里找到最大的那张图。(一般就是最后一张) if (photos != null && !photos.isEmpty()) { PhotoSize largestOne = photos.get(photos.size()-1); getFile = new GetFile().setFileId(largestOne.getFileId()); fileTypeSuffix = "/picture/%s.jpg"; } else { return; } java.io.File savedFile = new java.io.File(botConfig.getDownloadRootFolder(), String.format(fileTypeSuffix, System.currentTimeMillis())); try { //获取一个File对象,这个File是telebot库的File,不是java.io.File File file = sendApiMethod(getFile); //自写方法,用httpClient下载图片。为什么不用库自带的版本呢?因为库自带的下载方法直接用了FileUtils.copyURLToFile()下载文件,而且接口没提供代理选项,在公司内网等网络环境下不可用 //所以这个方法主要是用来使用代理下载图片的。具体代码不附了 downloadFileUseClient(file, savedFile); log.info(savedFile.getAbsolutePath() + " saved succeed"); } catch (Exception e) { log.info(savedFile.getAbsolutePath() + " saved failed", e); } } } - webHook
使用webHook就麻烦多了,根据官网上的说法:
You’ll need a server that:
- Supports IPv4, IPv6 is currently not supported for Webhooks.
- Accepts incoming POSTs from 149.154.167.197-233 on port 443,80,88 or 8443.
- Is able to handle TLS1.0+ HTTPS-traffic.
- Provides a supported, non-wildcard, verified or self-signed certificate.
- Uses a CN or SAN that matches the domain you’ve supplied on setup.
- Supplies all intermediate certificates to complete a verification chain.
所以为了使用webHook,分为这么几个步骤:
- 有个网站,可以让你监听请求;
- 为这个网站申请一个可用的证书(或者自签的证书),因为webHook必须在Https上传输信息
- 打开一个端口来监听请求。
- 将webHook注册到telegram
既然诸位能看到这篇文章,显然前两步我早都轻车熟路了,唯一要做的就是写一个webServer。Springboot走起……
//webHookService public class WebHookService extends DefaultAbsSender { private final BotConfig botConfig; @Autowired public WebHookService(ProxyOptions options, BotConfig botConfig) { //不用维护已处理的信息记录,因为webhook只会推送一遍 //如果需要容错处理,就用数据库先记录一下好了 super(options); this.botConfig = botConfig; } public void processUpdate(Update update) { downLoadFile(update); } public void setWebHook() { //这是库里提供的接口。其实certpath直接传个null就可以了,因为telegram访问你的webhook时会自己获取证书 WebhookUtils.setWebhook(this, botConfig.getWebHookUrl(), botConfig.getCertPath()); } @Override public String getBotToken() { return botConfig.getToken(); } }controller
@RestController @RequestMapping("/") @ConditionalOnBean(WebHookService.class) public class WebHookController { private final WebHookService webHookService; @Autowired public WebHookController(WebHookService webHookService) { this.webHookService = webHookService; } @PostMapping("/your/webhookUrl") public void webHook(@RequestBody Update update) { webHookService.processUpdate(update); } @GetMapping //一个测试方法,可访问telebot.history-of-my-life.com看到这段话 public String hello() { return "<html>" + "<h1>welcome</h1>" + "<p>I'm a telegram robot, Love ya!</p>" + "<p>you will never be able to find my real work path!</p>" + "</html>"; } }设置server监听端口(springboot,application.yml):
server: port: 8443 #webHook仅支持443,80,88,8443端口(即使用了80端口,也必须是https连接)main如下:
@SpringBootApplication @Log public class Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(Application.class, args); if (context.containsBean("longPullingService")) { LongPullingService longPullingService = context.getBean("longPullingService", LongPullingService.class); longPullingService.removeWebHook(); log.info("webHook removed"); longPullingService.pulling(); } else { WebHookService webHookService = context.getBean("webHookService", WebHookService.class); webHookService.setWebHook(); log.info("webHook settled"); } } }之后将服务器部署到vps上运行就可以了。
既然已经获得了Update,接下来怎么处理Update,怎么回复Update,就纯粹是自己的业务逻辑了,想怎么玩怎么玩。
用到的类
botConfig,用的是SpringBoot的机制,所有设置都从配置文件中获取
@Component
@Data
@ConfigurationProperties(prefix = "bot", ignoreInvalidFields = true)
public class BotConfig {
String token;
String botUserName;
String downloadRootFolder;
String webHookUrl;
String downloadedRecord;
String certPath;
}
对应的配置文件(application.yml):
bot:
token: yourtoken
botUserName: yourBotName
downloadRootFolder: /path/to/save/photo
downloadedRecord: history.txt #相对上一个目录
webHookUrl: https://www.yourdomain/your/path/to/webHook
downloadedRecord: history.txt
填的坑
看到官方API上说setWebHook方法有可选参数,其中一个是上传公钥,于是我就很老实地上传了,就是下面的方法:
public void setWebHook() {
WebhookUtils.setWebhook(this, botConfig.getWebHookUrl(), botConfig.getCertPath());
}
如果设置了证书路径,就会上传证书。但不知道怎么回事,我上传的cert总是验证失败,telegram就是不能把信息push过来,后来看到一篇文章:
https://stackoverflow.com/questions/44885229/unable-to-use-lets-encrypt-cert-in-telegram-bot-webhook-self-signed-cert-works
You mentioned that you used the
setwebhook?url=method with an additional argument (passing the certificate…). Somehow this isn´t needed. You can just enter the url starting withhttps://and you are fine.
也就是说不用设置,其实telegram可以获取到证书的。不上传证书果然好了,不明所以……
进阶
- 需要代理的网络环境下如何设置:
其实库已经封装好了代理的细节,只需要通过
DefaultBotOptions设置代理细节就好,即:@Bean @Autowired public LongPullingService longPullingService(BotConfig botconfig){ DefaultBotOptions options = new DefaultBotOptions(); //可选值NO_PROXY, HTTP, SOCKS4, SOCKS5 options.setProxyType(ProxyType.HTTP); options.setProxyHost(host); options.setProxyPort(port); new LongPullingService(options, botConfig; }需要在注意的是,虽然这里设置了代理,但是下载文件时库并没有使用代理,而是用了
FileUtils.copyUrlToFile()方法,所以如果网络环境不好,不能用库来下载文件,只能自己写下载方法。 -
通过nginx反向代理
如果不想向外暴露更多的端口,比如我的这个小站,已经绑定很多子域名了,都是nginx反代,这个也就一样处理了。
nginx的设置如下:
server { listen 80; # 修改成自己的域名 server_name telebot.history-of-my-life.com; # force redirect to https return 301 https://http_hostrequest_uri; server_tokens off; } server { listen 443 ssl; ssl on; ssl_certificate /path/to/cert/telebot.crt; ssl_certificate_key /path/to/cert/telebot.key; server_name telebot.history-of-my-life.com; ssl_session_timeout 5m; #ssl_session_cache shared:SSL:5m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-SEED-SHA:DHE-RSA-CAMELLIA128-SHA:HIGH:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS'; ssl_prefer_server_ciphers on; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; server_tokens off; location / { proxy_set_header Host http_host; proxy_set_header X-Real-IPremote_addr; proxy_set_header X-Forwarded-For proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Hostserver_name; proxy_set_header X-Forwarded-Proto https; ## 转发到9527端口 proxy_pass http://127.0.0.1:9527; access_log /var/log/nginx/telebot.access.log; error_log /var/log/nginx/telebot.error.log; proxy_read_timeout 1200s; client_max_body_size 0; } }为了不暴露9527端口,application.yml设置如下:
server: address: 127.0.0.1 #只监听本地的连接,其他主机的访问会被拒绝 port: 9527具体建站细节请参考我之前的文章,有介绍证书怎么申请的。
3 条评论
mz · 2019年4月28日 下午10:50
厉害
刘富贵 · 2021年12月8日 下午5:02
大佬我想问一下本地环境可以启动吗
astupidcoder · 2021年12月18日 下午8:32
可以启动,但是外网访问不到你啊……