【电报机器人-小飞机TG】快速搭建Telegram Bot对话机器人 - JAVA

快速上手Telegram Bot 对话机器人

  1. 打开Telegram,添加BotFather发送/newbot创建tg bot机器人
  2. 获取tg bot的token,后续通过接口操作机器人都需要用到该token
  3. 选择通讯方式,长轮询或手动绑定webhook,是接收 tg bot 发送消息的两种方式

一:创建自己的 Telegram bot

向BotFather发送/newbot创建机器人,输入两次名称,第一次是用户名称呼,第二次是@后面的名字,后续需要使用到代码中的也是第二次输入的名称。
tg-bot创建
在这里插入图片描述
创建成功机器人后,可在BotFather输入/mybots查看或管理你的所有机器人。
下面的图片命令基本都是见名知意了
设置你的机器人
在上述图片位置 Edit commads 可以实现机器人的命令提示符
设置机器人的命令提示符

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

发送接口请求到telegram服务器
官方接口文档: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
  1. 首先 添加 依赖
        <!-- telegrambots -->
        <dependency>
            <groupId>org.telegram</groupId>
            <artifactId>telegrambots</artifactId>
            <version>5.7.1</version>
        </dependency>
  1. 继承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();
        }
    }
}
  1. 在启动类开启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发送即可。
发送http请求发送消息
我们发送消息是根据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;
}

发送markdownhtml消息,是根据传入的parse_mode参数来改变的。

代码实现上,markdown和html是有区别的
在这里插入图片描述
在这里插入图片描述

四:特殊消息使用

1. 引用回复功能

2024-10-10更新:
在接收机器人的Update对象信息时,拿到的message_id就是用户发送的那条消息的id,在我们向用户发送消息时带入这个message_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_iduser_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 是否显示弹框
如图:
tg接口文档
代码示例:

		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方法的实现
    }

}

以上是个人的处理手段以及方案,如果有更好的思路和方案可以一起沟通沟通。

提示

  1. 使用webhook,消耗资源低,不需要循环请求telegram服务器浪费资源,也不需要依赖三方jar包,通过设置WebHook地址持续获取tg发送的消息,但存在消息丢失的清空(本人生产遇到过好几次,最后换长轮询了,因为要保证消息不丢失)。
  2. 使用长轮询,删除webHook地址,改为我们主动轮询去查getUpdates方法,即使处理消息卡了,再次重启服务还是能查到这条消息进行处理,直接继承TelegramLongPollingBot类,重写他的一堆方法,而且本地调试很方便。
  3. 测试环境在国内则需要配置代理服务器做转发才能向telegram发送消息。
  4. 发送信息内容不允许出现非html关键字的信息,否则将接收不到消息
  5. 群聊与私聊的区别,在于chat中的type:private是私聊,group为群聊,supergroup也是群。
  6. 群聊只能识别以/开头的命令,因为默认机器人的权限不够,在群聊中只能识别到/xx这种命令,可以在群聊中给机器人设置管理员admin权限,bot就可以收到所有的消息,私聊则是能识别到用户发的任何信息。
    上面是长轮询方式获取到的Update消息对象
  7. 一个群聊存在多个机器人,默认情况只会有一个机器人可以收到消息,也就是会抢消息,可以设置机器人的访问权限,让他收到所有的消息,没设置的机器人会继续抢消息,被设置过的会收到所有的消息。
    找到Telegram Father > /mybots > 你的机器人 > bot Settings > Group Privacy > Turn off 关闭隐私权限
    不建议多个机器人在同一个群聊,尽量一个群只存在一个机器人。

Telegram Bot 功能比较强大,目前只使用到了部分功能,如果后续用到别的功能或者遇到有趣的问题也会持续更新。
以上为个人总结,有错误的地方大家可以沟通指正,有什么问题可以留言沟通,Over。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值