快速上手Telegram Bot 对话机器人
- 打开Telegram,添加BotFather发送/newbot创建tg bot机器人
- 获取tg bot的token,后续通过接口操作机器人都需要用到该token
- 选择通讯方式,长轮询或手动绑定webhook,是接收 tg bot 发送消息的两种方式
一:创建自己的 Telegram bot
向BotFather发送/newbot创建机器人,输入两次名称,第一次是用户名称呼,第二次是@后面的名字,后续需要使用到代码中的也是第二次输入的名称。


创建成功机器人后,可在BotFather输入/mybots查看或管理你的所有机器人。
下面的图片命令基本都是见名知意了

在上述图片位置 Edit commads 可以实现机器人的命令提示符

二:获取到Token后可以直接通过接口操作你的机器人

官方接口文档:Telegram Bot 官方文档
简版文档: Telegram Bot 中文版文档
三:选择部署方式(长轮询和设置webhook)
你的 bot 可以主动拉取信息(长轮询),或者是由 Telegram 服务器主动推送过来(webhook)。
- 长轮询:主动的给 Telegram 服务器发送了一个请求,来询问是否有新的 update。不需要域名不需要配置,添加telegram的jar包,继承TelegramLongPollingBot重写它的onUpdateReceived方法,在项目启动是开启telegram机器监听,即可接收到你的Telegram bot接收到的消息,让机器人主动向聊天框发送消息用内置的execute方法即可。
- webhook:为 Telegram 提供一个可以从公共互联网上访问的 URL(需要有域名的接口)。 无论何时,只要有新的信息发送到你的 bot,Telegram服务器将主动把消息内容封装成Update对象请求到你的url地址,发送消息需要使用官方文档的sendMessage方法。
详情比较参考: Telegram Bot长轮询与webhook的区别

长轮询_Demo
- 首先 添加 依赖
<!-- telegrambots -->
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots</artifactId>
<version>5.7.1</version>
</dependency>
- 继承TelegramLongPollingBot类,根据你的botToken和botName生产你的机器人,接收消息重写onUpdateReceived(长轮询拉消息的方法),发送消息可以直接使用execute()发送消息。
@Component
public class ExecBot extends TelegramLongPollingBot {
// 填你自己的token和username
private String botToken = "7003012345:abc...";
private String botName = "you_bot_name";// newbot时你的第二个名字
public ExecBot() {
this(new DefaultBotOptions());
}
public ExecBot(DefaultBotOptions options) {
super(options);
}
@Override
public String getBotToken() {
return botToken;
}
@Override
public String getBotUsername() {
return botName;
}
@Override
public void onUpdateReceived(Update update) {
if (update.hasMessage()) {
Message message = update.getMessage();
Long chatId = message.getChatId();
String text = message.getText().trim();
String firstName = message.getChat().getFirstName();
String type = message.getChat().getType();
String title = message.getChat().getTitle();
// todo 拿到text 也就是用户输入的内容,进行自己的逻辑判断回复各种消息
sendMsg("你好", chatId);
}
}
//回复消息
public void sendMsg(String text, Long chatId) {
SendMessage response = new SendMessage();
response.setChatId(String.valueOf(chatId));
response.setText(text);
response.setParseMode("html");
try {
execute(response);
} catch (TelegramApiException e) {
e.getMessage();
}
}
}
- 在启动类开启telegram机器监听
@SpringBootApplication
public class TelegramApplication {
public static void main(String[] args) {
SpringApplication.run(TelegramApplication.class, args);
/*
* 开启telegram机器监听
*/
DefaultBotOptions botOptions = new DefaultBotOptions();
// 如果发布线上,服务器不在国内就可以注释吊这一部分,直接new个空的DefaltBotOptions就行了
botOptions.setProxyHost("127.0.0.1");
botOptions.setProxyPort(7890);
botOptions.setProxyType(DefaultBotOptions.ProxyType.SOCKS5);
DefaultBotSession defaultBotSession = new DefaultBotSession();
defaultBotSession.setOptions(botOptions);
ViaBotService viaBotService = context.getBean(ViaBotService.class);
List<ViaBot> viaBots = viaBotService.list();
if (CollectionUtils.isNotEmpty(viaBots)) {
// 启动你的所有机器人
viaBots.forEach(bot -> {
try {
TelegramBotsApi telegramBotsApi = new TelegramBotsApi(defaultBotSession.getClass());
TelegramBot telegramBot = new TelegramBot(botOptions, bot.getViaBotId(), bot.getViaBotName(), bot.getViaBotToken());
telegramBotsApi.registerBot(telegramBot);
log.info("启动TG机器人viaBotId:{},botName:{},botToken:{},成功", bot.getViaBotId(), bot.getViaBotName(), bot.getViaBotToken());
} catch (TelegramApiException e) {
log.error("启动TG机器viaBotId:{},botName:{},botToken:{},失败!", bot.getViaBotId(), bot.getViaBotName(), bot.getViaBotToken());
throw new RuntimeException(e);
}
});
}
}
}
webhook_Demo
<需要服务器有域名,并且通过代理转发或者服务器就在国外>
只需要写一个接口
@Slf4j
@Controller
public class TelegramWebhookController {
@ResponseBody
@RequestMapping(value = "/test/tg/update")
public void doNotify(HttpServletRequest request) {
log.info("telegram 推送的 request :" + request);
JSONObject params = requestKitBean.getReqParamJSON();
// 参数懒得看文档的就直接debug查看那些是需要用的参数,手动取就行
// todo 拿到text 也就是用户输入的内容,进行自己的逻辑判断回复各种消息
}
}
然后请求一遍setWebhook
这样就将消息推送地址指向了你的接口,可以使用getWebhook查看已绑定的接口地址。
主动向群聊发送信息就用官方文档中的sendMessage发送即可。

我们发送消息是根据chat_id来进行查找聊天框的,无论是私聊或者群聊,每个聊天框都有属于自己的chat_id,所以在第一次接收到用户发来的/start或者别的消息时,尽量将chat_id保留下来,这样后面可以通过chat_id来向用户发送消息。
发送消息请求方法,自己写个http请求
public void doSendMessage(PushMessageDetailDto dto) {
log.info("[PushService] doSendMessage PushMessageDetailDto = {}", JSON.toJSON(dto));
boolean responseResults = false;
String url = TelegramProperties.url
.replace("<token>", TelegramProperties.token)
.replace("<method>", dto.getMethodName());
HttpClient client = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(url);
log.info("[PushService] doSendMessage url = {}", url);
// 创建 JSON 请求体
JSONObject json = new JSONObject();
json.put("chat_id", dto.getChatId());
json.put("text", dto.getText());
json.put("parse_mode", "HTML");
log.info("[PushService] doSendMessage json = {}", JSON.toJSON(json));
try {
// 设置请求实体为 JSON 格式, 请求实体必须为utf-8,这里不设置则发送消息的中文/中文符则会变成乱码
StringEntity entity = new StringEntity(json.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json; charset=UTF-8");
post.setEntity(entity);
// 执行请求
HttpResponse response = client.execute(post);
// 后续判断是否成功等
log.info("[PushService] doSendMessage response = {}", JSON.toJSON(response));
} catch (Exception e) {
log.error("[PushService] error", e);
}
}
请求实体类
public class PushMessageDetailDto {
/**
* telegram发送消息 请求体 实体类
*/
// 发送信息接口
private final String methodName = "sendMessage";
/**
* 目标聊天(chat_id)或目标频道的用户名的唯一标识符(格式为@channelusername)
*/
private Long chatId;
/**
* 待发送消息的文本,实体解析后为1-4096个字符
*/
private String text;
/**
* 消息文本中的实体解析模式。有关更多详细信息,请参见格式化选项。 支持 parse_mode=HTML 或 parse_mode=Markdown
*/
private String parseMode;
/**
* 禁用此消息中链接的链接预览
*/
private boolean disableWebPagePreview;
/**
* 静默发送消息。用户将收到没有声音的通知。
*/
private boolean disableNotification;
/**
* 如果消息是答复,则为原始消息的ID
*/
private Integer replyToMessageId;
/**
* 其他界面选项。内联键盘,自定义回复键盘,删除回复键盘或强制用户回复的说明的JSON序列化对象。
*/
private Object replyMarkup;
}
发送markdown或html消息,是根据传入的parse_mode参数来改变的。
代码实现上,markdown和html是有区别的


四:特殊消息使用
1. 引用回复功能
2024-10-10更新:
在接收机器人的Update对象信息时,拿到的message_id就是用户发送的那条消息的id,在我们向用户发送消息时带入这个message_id即可实现引用功能。

机器人引用回复–接口文档如左下,实现效果如右下:

2. 审批消息实现(弹窗/修改消息/按钮等)
①按钮消息->②按钮回调->③获取操作人角色->④展示弹窗->⑤修改这一条消息->⑥用户小卡片
①. 按钮消息
实现简单的审批功能,还是sendMessage方法入参有个rely_markup,使用InlineKeyboardMarkup内敛键盘,自定义创建绑定在消息上的键盘。

!注意传入的格式要求是下划线分割,驼峰识别不了,我在这里卡了几个小时,建议开发阶段将所有的入参出参进行打印,方便排查问题。
代码示例:
// 创建内联键盘
InlineKeyboardMarkup markup = new InlineKeyboardMarkup();
// 创建第一个按钮
Map<String, String> param1 = new HashMap<>();
param1.put("state", "1");
param1.put("myData", "123测试");
InlineKeyboardButton btn1 = new InlineKeyboardButton();
btn1.setText("✅通过");
btn1.setCallbackData(JSON.toJSONString(param1)); // 使用 JSON 作为回调数据
// 创建第二个按钮
Map<String, String> param2 = new HashMap<>();
param2.put("state", "2");
param2.put("myData", "随便填写你的参数,后面触发按钮时会将你的参数完整的返回给你");
InlineKeyboardButton btn2 = new InlineKeyboardButton();
btn2.setText("❌驳回");
btn2.setCallbackData(JSON.toJSONString(param2)); // 使用 JSON 作为回调数据
// 将按钮添加到同一行
List<InlineKeyboardButton> row = new ArrayList<>();
row.add(btn1);
row.add(btn2);
// 创建一个列表并添加行
List<List<InlineKeyboardButton>> buttons = new ArrayList<>();
buttons.add(row);
// 设置键盘
markup.setKeyboard(buttons);
// 发送消息
SendMessage sendMessage = new SendMessage();
sendMessage.setChatId(chatId);
sendMessage.setText("测试审核");
sendMessage.setReplyMarkup(markup);
try {
execute(sendMessage);
} catch (TelegramApiException e) {
e.printStackTrace();
}
JSON示例:
{
"inline_keyboard": [
[{
"callback_data": "{\"myData\":\"123测试\",\"state\":\"1\"}",
"text": "✅通过✅"
}, {
"callback_data": "{\"myData\":\"随便填写你的参数,后面触发按钮时会将你的参数完整的返回给你\",\"state\":\"2\"}",
"text": "❌拒绝❌"
}]
]
}
实现效果:

②. 消息回调
在点击按钮通过/拒绝 后tg 会向你绑定的webhook地址或者你的长轮询的接口消息的地址发送一条消息,与普通消息不同的是,普通消息是callback_query,也就是回复类消息,具体参数内容tg的接口文档描述很清楚,也可以自己断点查看想要的参数。
如图:

③.判断操作人权限 (是否是管理员或创建者)
接口:getChatMember,根据chat_id,user_id获取这个人在该群聊是什么角色。
代码示例:
String url = TelegramProperties.url
.replace("<token>", TelegramProperties.token)
.replace("<method>", PushMessageDetailDto.METHOD_NAME_MEMBER);
HttpClient client = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(url);
JSONObject json = new JSONObject();
json.put("user_id", userId);
json.put("chat_id", chatId);
try {
// 设置请求实体为 JSON 格式
StringEntity entity = new StringEntity(json.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json; charset=UTF-8");
post.setEntity(entity);
// 执行请求
HttpResponse response = client.execute(post);
String jsonResponse = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(jsonResponse);
JSONObject result = jsonObject.getJSONObject("result");
String string = result.getString("status");
// 检查用户角色
if ("administrator".equals(string) || "creator".equals(string)) {
// 用户是超级管理员或群主
System.out.println("用户是超级管理员或群主。");
return true;
} else {
// 用户不是超级管理员
System.out.println("用户不是超级管理员。");
return false;
}
} catch (Exception e) {
log.error("[Tg 获取用户角色异常 ] error", e);
return false; // 返回 false 而不是抛出异常
}
④. 选择按钮后的弹窗提示
接口:answerCallbackQuery
入参
callback_query_id 必填,就是上面点击按钮后的回调消息的id
text 文本显示内容
show_alert 是否显示弹框
如图:

代码示例:
String url = TelegramProperties.url
.replace("<token>", TelegramProperties.token)
.replace("<method>", PushMessageDetailDto.METHOD_NAME_ANSWER_CALLBACK);
// 创建 HttpPost 请求
HttpPost post = new HttpPost(url);
JSONObject json = new JSONObject();
json.put("callback_query_id", callbackQueryId);
json.put("text", text);
json.put("show_alert", showAlert);
StringEntity entity = new StringEntity(json.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json; charset=UTF-8");
post.setEntity(entity);
// 设置连接和响应超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000) // 连接超时 5 秒
.setSocketTimeout(5000) // 响应超时 5 秒
.build();
post.setConfig(requestConfig);
try {
httpClient.execute(post);
} catch (Exception e) {
log.error("[Tg 显示弹窗 ] error", e);
}
样式:

⑤. 消息修改
接口:editMessageText
在审核通过后,如何修改消息将按钮隐藏掉,使用message_id,与上面引用回复一样获取消息的id。
代码示例:
String url = TelegramProperties.url
.replace("<token>", TelegramProperties.token)
.replace("<method>", PushMessageDetailDto.METHOD_NAME_EDIT_MESSAGE);
// 创建 HttpPost 请求
HttpPost post = new HttpPost(url);
JSONObject json = new JSONObject();
json.put("message_id", messageId);
json.put("chat_id", chatId);
json.put("text", text);
json.put("parse_mode", "HTML");
StringEntity entity = new StringEntity(json.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json; charset=UTF-8");
post.setEntity(entity);
// 设置连接和响应超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000) // 连接超时 5 秒
.setSocketTimeout(5000) // 响应超时 5 秒
.build();
post.setConfig(requestConfig);
try {
httpClient.execute(post);
} catch (Exception e) {
log.error("[Tg 编辑消息 ] error", e);
}
如图:同一条消息

注意:
如果收不到按钮的回调消息,检查自己的webhook地址是否正常,接口:getWebhookInfo(查看已绑定的地址信息),如果是长轮询一般不会有问题,因为它是主动去拉取消息。
⑥. 实现用户卡片展示
点击弹出用户小卡片,实现效果图

使用 Telegram bot 发送消息的限制:
官方文档显示:每秒不超过30条接口请求,每个聊天框(chat_id)每分钟不超过20条消息,如果超出限制就会被禁用发送一段时间,所以在批量请求sendMessage的时候尽量做好限制。
例如
需求:需要批量给我绑定的所有群聊发送广播公告,短时间会下发几百上千条数据。
处理手段:使用Redisson的分布式限流器RRateLimiter,每秒放开30个令牌,保证每秒只有30个请求向tg发送sendMessage,将每个消息放入redis的List,启动一个消费者去消费,加上限流器实现广播发送。
@Component
public class RateLimiterUtil {
/**
* 限流器
*/
@Resource
private RedissonClient redissonClient;
private final Map<String, RRateLimiter> rateLimiterMap = new HashMap<>();
private static final int MAX_ATTEMPTS = 3;
private static final int WAIT_TIME_SECONDS = 20;
public static final String TG = "TG_RATE_LIMITER";// tg sendMessage 请求频次 每秒上限30
public static final String TG_GROUP = "TG_GROUP_CHAT_RATE_LIMITER";// 每个群组 上限20
//配置每秒产生30个令牌
@PostConstruct
public void init() {
RRateLimiter tgRateLimiter = redissonClient.getRateLimiter(TG);
// 删除已存在的限流器
if (tgRateLimiter.isExists()) {
tgRateLimiter.delete(); // 删除旧的配置
}
// 20/s
tgRateLimiter.trySetRate(RateType.OVERALL, 20, 1, RateIntervalUnit.SECONDS);
// RRateLimiter tgGroupChatLimiter2 = redissonClient.getRateLimiter(TG);
// tgLimiter.trySetRate(RateType.OVERALL, 30, 1, RateIntervalUnit.SECONDS);
rateLimiterMap.put(TG, tgRateLimiter);
}
// 尝试获取一个令牌
public boolean tryAcquire(String limiterName) {
RRateLimiter rateLimiter = rateLimiterMap.get(limiterName);
if (rateLimiter != null) {
return rateLimiter.tryAcquire();
}
throw new IllegalArgumentException("Limiter not found: " + limiterName);
}
// 尝试获取令牌并设置等待时间
public boolean tryAcquireWithTimeout(String limiterName, long timeout) {
RRateLimiter rateLimiter = rateLimiterMap.get(limiterName);
if (rateLimiter == null) {
throw new IllegalArgumentException("Rate limiter not found: " + limiterName);
}
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
if (rateLimiter.tryAcquire(timeout, TimeUnit.SECONDS)) {
return true; // 成功获取令牌
}
if (attempt < MAX_ATTEMPTS) {
try {
TimeUnit.SECONDS.sleep(WAIT_TIME_SECONDS); // 等待 5 秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
throw new RuntimeException("Thread was interrupted", e);
}
}
}
return false; // 三次尝试后仍未成功
}
}
@Component
@Slf4j
public class RedisListConsumer {
/**
* 消费者
*/
@Resource
private RedisUtil redisUtil;
public static final String LIST_KEY = "redis_broadcast_key_mgr";
@Autowired
private TelegramNoticeRecordServiceImpl telegramNoticeRecordService;
@Autowired
private RateLimiterUtil rateLimiterUtil;
@Autowired
private PushServiceFactory pushServiceFactory;
private static final int THREAD_COUNT = 5; // 启动的线程数量
private ExecutorService executorService;
@PostConstruct
public void startConsuming() {
executorService = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(this::consumeFromList);
}
}
private void consumeFromList() {
while (true) {
try {
// FIXME 测试限流 20/s
boolean tgRateLimiter = rateLimiterUtil.tryAcquire(RateLimiterUtil.TG);
if (!tgRateLimiter) {
// 休眠 100ms
Thread.sleep(200);
continue;
}
Object data = redisUtil.lRightPop(LIST_KEY);
if (data != null) {
log.info("tg 公告消费者 处理数据: {" + data + "} ");
PushMessageDetailDto messageDetailDto = JSON.parseObject(data.toString(), PushMessageDetailDto.class);
// 处理数据
handleData(messageDetailDto);
}
} catch (Exception e) {
log.error("Error while consuming from : ", e);
}
}
}
private void handleData(PushMessageDetailDto data) {
// 具体调用sendMessage方法的实现
}
}
以上是个人的处理手段以及方案,如果有更好的思路和方案可以一起沟通沟通。
提示
- 使用webhook,消耗资源低,不需要循环请求telegram服务器浪费资源,也不需要依赖三方jar包,通过设置WebHook地址持续获取tg发送的消息,但存在消息丢失的清空(本人生产遇到过好几次,最后换长轮询了,因为要保证消息不丢失)。
- 使用长轮询,删除webHook地址,改为我们主动轮询去查getUpdates方法,即使处理消息卡了,再次重启服务还是能查到这条消息进行处理,直接继承TelegramLongPollingBot类,重写他的一堆方法,而且本地调试很方便。
- 测试环境在国内则需要配置代理服务器做转发才能向telegram发送消息。
- 发送信息内容不允许出现非html关键字的信息,否则将接收不到消息
- 群聊与私聊的区别,在于chat中的type:private是私聊,group为群聊,supergroup也是群。
- 群聊只能识别以/开头的命令,因为默认机器人的权限不够,在群聊中只能识别到/xx这种命令,可以在群聊中给机器人设置管理员admin权限,bot就可以收到所有的消息,私聊则是能识别到用户发的任何信息。

- 一个群聊存在多个机器人,默认情况只会有一个机器人可以收到消息,也就是会抢消息,可以设置机器人的访问权限,让他收到所有的消息,没设置的机器人会继续抢消息,被设置过的会收到所有的消息。
找到Telegram Father > /mybots > 你的机器人 > bot Settings > Group Privacy > Turn off 关闭隐私权限
不建议多个机器人在同一个群聊,尽量一个群只存在一个机器人。
Telegram Bot 功能比较强大,目前只使用到了部分功能,如果后续用到别的功能或者遇到有趣的问题也会持续更新。
以上为个人总结,有错误的地方大家可以沟通指正,有什么问题可以留言沟通,Over。
1062

被折叠的 条评论
为什么被折叠?



