Java实现Telegram Bot教程
Joel
发布于 2022-07-26 / 7563 阅读 / 7 评论 / 37 点赞

Java实现Telegram Bot教程

引言

最近工作中因为遇到了几个实现Telegram Bot的需求,从一开始的时候无从下手到慢慢的上手以及到最后实现以及优化,这编博客整理出Telegram Bot机器人Java的实现以及期间遇到的一些坑。教程通过springboot+maven方式来实现,因为Telegram在国内无法访问,这个教程的实现必须在外网或者搭梯子的前提下进行。这里不对Telegram Bot进行介绍了,详细的介绍可以查看官网的介绍:https://core.telegram.org/bots

进入主题

Telegram Bot API是通过HTTP请求的方式与机器人进行通信,Java的实现需要用到GitHub上一个开源的java sdk包(https://github.com/rubenlagus/TelegramBots),通过maven导进来:

<dependency>
        <groupId>org.telegram</groupId>
        <artifactId>telegrambots</artifactId>
        <version>6.1.0</version>
    </dependency>

首先所以在Telegram的客户端上申请一个机器人拿到机器人的token才能对机器人进行开发,在telegram上搜索BotFather,
Snipaste_2022-07-27_21-24-23

发送/newbot根据指引创建自己的bot

Snipaste_2022-07-27_21-27-53

Telegram提供两种与机器人通信的方式getUpdateswebhooks
getUpdates的原理是通过长轮询去请求bot获取别人发送给bot的消息,webhooks的原理是每次有人发送消息给bot的时候telegram会第一时间推送到你的服务器上,webhooks的时效性原理上要比getUpdates的方式高,但是webhooks方式去实现机器人需要你的服务器有一个域名和https证书,通过实践观察getUpdates的方式基本上也会第一时间能拿到别人发送给bot的消息,时效性不会太差,所以这次教程用getUpdates的方式去实现。

getUpdates方式实现

在java中新建一个MyBot.java,getUpdates的方式通过继承org.telegram.telegrambots.bots.TelegramLongPollingBot这个类来实现(如果想通过webhooks就继承org.telegram.telegrambots.bots.TelegramWebhookBot)

import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
 * @author wucode
 * @email wucode@163.com
 * @date 2022/7/26
 */
public class MyBot extends TelegramLongPollingBot {

    private String token = "你机器人的token";
    private String botUsername = "你机器人的名字";


    @Override
    public String getBotUsername() {
        return this.botUsername;
    }

    @Override
    public String getBotToken() {
        return this.token;
    }

    @Override
    public void onUpdateReceived(Update update) {
        System.out.println(update);
    }
}

WebHook方式实现

import org.telegram.telegrambots.bots.TelegramWebhookBot;  
import org.telegram.telegrambots.meta.api.methods.BotApiMethod;  
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;  
import org.telegram.telegrambots.meta.api.objects.Update;  
  
/**  
 * @author wucode
 * @email wucode@163.com
 * @date 2022/7/26
*/
public class MyWebHookBot extends TelegramWebhookBot {  
  
    private String token = "你机器人的token";  
    private String botUsername = "你机器人的名字";  
    @Override  
  public String getBotUsername() {  
        return this.botUsername;  
    }  
  
    @Override  
  public String getBotToken() {  
        return this.token;  
    }  
  
    @Override  
  public BotApiMethod<?> onWebhookUpdateReceived(Update update) {  
        return SendMessage.builder().chatId(update.getMessage().getChatId()).text("Halo!").build();  
    }  
  
    @Override  
  public String getBotPath() {  
        return this.botUsername;  
    }  
}

在springboot启动类中注册MyBot

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.telegram.telegrambots.meta.TelegramBotsApi;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;

@SpringBootApplication
public class MyBotApplication {

    public static void main(String[] args) {
        try {
            TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class);
            telegramBotsApi.registerBot(new MyBot());
            /*  Webhook方式
			Webhook webhook = new DefaultWebhook();  
			//Webhooks_目前支持的端口:443, 80, 88, 8443
			webhook.setInternalUrl("http://0.0.0.0:8443");  
			TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class, webhook);  
			MyWebHookBot myWebHookBot = new MyWebHookBot();  
			//需要一个可以https访问的域名并转发到上面端口
			SetWebhook setWebhook = SetWebhook.builder().url("https://joelwublog.com/").build();  
			telegramBotsApi.registerBot(myWebHookBot, setWebhook);  
			*/
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
        SpringApplication.run(MyBotApplication.class, args);
    }

}

代理设置

本地调试的时候需要加代理可以这样设置(普通的System.setProperty方法无效)

            TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class);
            DefaultBotOptions botOptions = new DefaultBotOptions();
            botOptions.setProxyHost("127.0.0.1");
            botOptions.setProxyPort(7890);
            // Select proxy type: [HTTP|SOCKS4|SOCKS5] (default: NO_PROXY)
            botOptions.setProxyType(DefaultBotOptions.ProxyType.HTTP);
            MyBot myBot = new MyBot(botOptions);
            telegramBotsApi.registerBot(myBot);

MyBot类需要重载构造方法

public class MyBot extends TelegramLongPollingBot {
    private String token = "你机器人的token";
    private String botUsername = "你机器人的名字";
    public MyBot(DefaultBotOptions botOptions) {
        super(botOptions);
    }
    @Override
    public String getBotUsername() {
        return this.botUsername;
    }
    @Override
    public String getBotToken() {
        return this.token;
    }
    @Override
    public void onUpdateReceived(Update update) {}
}

运行程序之后发条消息给刚才创建的机器人,这个时候就能看到有消息进来了

Snipaste_2022-07-27_22-22-12

Snipaste_2022-07-27_22-24-59

给别人回复文字

Snipaste_2022-07-27_22-42-12

@Override
    public void onUpdateReceived(Update update) {
        SendMessage message = SendMessage.builder()
                .text("hello!")
                .chatId(update.getMessage().getChatId().toString())
                .build();
        try {
            execute(message);
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }

Snipaste_2022-07-27_22-39-54

常用的功能

内联菜单

@Override
    public void onUpdateReceived(Update update) {
        InlineKeyboardButton button1 = InlineKeyboardButton.builder().text("A").callbackData("opt1").build();
        InlineKeyboardButton button2 = InlineKeyboardButton.builder().text("B").callbackData("opt2").build();
        InlineKeyboardButton button3 = InlineKeyboardButton.builder().text("C").callbackData("opt3").build();
        InlineKeyboardButton button4 = InlineKeyboardButton.builder().text("D").callbackData("opt4").build();
        List<InlineKeyboardButton> list1 = new ArrayList<>();
        List<InlineKeyboardButton> list2 = new ArrayList<>();
        List<InlineKeyboardButton> list3 = new ArrayList<>();
        List<InlineKeyboardButton> list4 = new ArrayList<>();
        list1.add(button1);
        list2.add(button2);
        list3.add(button3);
        list4.add(button4);
        List<List<InlineKeyboardButton>> rowList = new ArrayList<>();
        Collections.addAll(rowList, list1, list2, list3, list4);
        InlineKeyboardMarkup inlineKeyboardMarkup = InlineKeyboardMarkup.builder().keyboard(rowList).build();
        SendMessage message = SendMessage.builder()
                .text("请选择一个选项!")
                .chatId(update.getMessage().getChatId().toString())
                .replyMarkup(inlineKeyboardMarkup)
                .build();
        try {
            execute(message);
        } catch (TelegramApiException e) {
            e.printStackTrace();
        }
    }

Snipaste_2022-07-27_22-57-08

键盘

@Override
    public void onUpdateReceived(Update update) {
        if (update.getCallbackQuery().getData().equals("opt1")) {
            try {
                KeyboardButton button1 = KeyboardButton.builder().text("1").build();
                KeyboardButton button2 = KeyboardButton.builder().text("2").build();
                KeyboardButton button3 = KeyboardButton.builder().text("3").build();
                KeyboardButton button4 = KeyboardButton.builder().text("4").build();
                KeyboardRow keyboardRow1 = new KeyboardRow();
                KeyboardRow keyboardRow2 = new KeyboardRow();
                keyboardRow1.add(button1);
                keyboardRow1.add(button2);
                keyboardRow2.add(button3);
                keyboardRow2.add(button4);
                List<KeyboardRow> keyboardRows = new ArrayList<>();
                Collections.addAll(keyboardRows, keyboardRow1, keyboardRow2);
                ReplyKeyboardMarkup replyKeyboardMarkup = ReplyKeyboardMarkup.builder().keyboard(keyboardRows).build();
                SendMessage message = SendMessage.builder().replyMarkup(replyKeyboardMarkup).text("你选择了A").chatId(update.getCallbackQuery().getFrom().getId().toString()).build();
                execute(message);
            } catch (TelegramApiException e) {
                e.printStackTrace();
            }
        }
    }

Snipaste_2022-07-27_23-16-01

设置Markdown&html语法

Snipaste_2023-02-16_21-26-12

SendMessage message = SendMessage.builder()
                .text("点击可以跳到我的博客:[joelwublog](https://www.joelwublog.com)")
                .chatId(update.getMessage().getChatId().toString())
                .parseMode("MarkdownV2")//设置为MarkdownV2语法
                //.parseMode("HTML")//设置为html语法
                .build();
        execute(message);

注意:如果使用Markdown语法消息中有特殊字符需要对特殊字符转义,不然会解析报错

public String escape(String s) {
        if (s == null) return null;
        List<String> list = Arrays.asList("#", "!", "*", "_", "~", "+", "-", ">", "<", "[", "]", "`", ".", "^", "=");
        for (String l : list) {
            s = s.replace(l, "\\" + l);
        }
        return s;
    }

获取电话以及位置

Snipaste_2023-02-16_21-46-59
Snipaste_2023-02-16_21-50-59
Snipaste_2023-02-16_21-52-04

	List<KeyboardRow> list = new ArrayList<>();
        KeyboardRow keyboardRow = new KeyboardRow();
        KeyboardButton keyboardButton1 = KeyboardButton.builder().text("分享电话").requestContact(true).build();
        KeyboardButton keyboardButton2 = KeyboardButton.builder().text("分享位置").requestLocation(true).build();
        keyboardRow.add(keyboardButton1);
        keyboardRow.add(keyboardButton2);
        list.add(keyboardRow);
        ReplyKeyboardMarkup keyboardMarkup = ReplyKeyboardMarkup.builder().keyboardRow(keyboardRow).oneTimeKeyboard(true).build();
        execute(SendMessage.builder().chatId(update.getMessage().getChatId().toString()).text("点击下面按钮分享电话以及位置").replyMarkup(keyboardMarkup).build())

发送图片

	//发送url地址图片
        SendPhoto sendPhoto = SendPhoto.builder().chatId(update.getMessage().getChatId().toString()).photo(new InputFile("https://joelwublog.com/upload/2022/07/bot-2.png")).build();
        //通过fileId发送telegram服务器上的图片
        //SendPhoto sendPhoto = SendPhoto.builder().chatId(update.getMessage().getChatId().toString()).photo(new InputFile("12313131")).build();
        //发送本地图片
        //SendPhoto sendPhoto = SendPhoto.builder().chatId(update.getMessage().getChatId().toString()).photo(new InputFile(new File("F:\\bot-1.png"))).build();
        execute(sendPhoto);

发送视频

//发送url地址视频
        SendVideo sendVideo = SendVideo.builder().chatId(update.getMessage().getChatId().toString()).video(new InputFile("https://joelwublog.com/upload/2022/07/bot-test.mp4")).build();
        //通过fileId发送telegram服务器上的视频文件
        //SendVideo sendVideo =SendVideo.builder().chatId(update.getMessage().getChatId().toString()).video(new InputFile("12313131")).build();
        //发送本地视频文件
        //SendVideo sendVideo =SendVideo.builder().chatId(update.getMessage().getChatId().toString()).video(new InputFile(new File("F:\\bot-test.mp4"))).build();
        execute(sendVideo);

这里就演示几种功能,其他的功能也一样是通过execute这个方法进行发送,其他的功能可以对应Telegram bot api中的方法名来进行发送。
https://core.telegram.org/bots/api#available-methods
Snipaste_2022-07-27_23-22-05

授权登录(Telegram Login Widget)

telegram的授权登录是利用官方提供的授权登录按钮实现的(javascript组件),官方文档:https://core.telegram.org/widgets/login
前提条件:
1.需要可以公网访问的域名或者IP地址
2.需要申请一个bot并且通过@BotFather设置这个bot的允许授权白名单域(只有在个该白名单域下才允许进行授权)
通过@BotFather发送/setdomain设置白名单域
Snipaste_2023-07-23_14-14-26.png
在官网文档给的按钮控件设置好自己的botname并按钮控件放入自己项目的页面中
Snipaste_2023-07-23_14-05-32.png

<script async src="https://telegram.org/js/telegram-widget.js?22" data-telegram-login="botname" data-size="large" data-onauth="onTelegramAuth(user)" data-request-access="write"></script>

从回调信息中拿到下面参数auth_date、first_name、hash、id、last_name、photo_url、username,拿到这些信息之后通过哈希计算后校验哈希值是否与回调信息中的hash字段匹配,如果匹配则说明校验成功,之后就可以保存用户信息以及其他业务逻辑。
点击按钮效果
Snipaste_2023-07-23_13-28-06.png
Snipaste_2023-07-23_13-29-08.png
Snipaste_2023-07-23_13-29-59.png
下面是用Java写的校验哈希的方法

<!--这里需要引用hutool类包-->
<dependency>  
<groupId>cn.hutool</groupId>  
<artifactId>hutool-all</artifactId>  
</dependency>
String sb = "auth_date=" + auth_date + "\nfirst_name=" + first_name + "\nid=" + id;
//last_name、photo_url、username没有值的时候不要拼接上去
	if (StringUtils.isNotBlank(last_name)) {
		sb = sb + "\nlast_name=" + last_name;
	}
	if (StringUtils.isNotBlank(photo_url)){
		sb = sb + "\nphoto_url=" + photo_url;
	}
	if (StringUtils.isNotBlank(username)) {
		sb = sb + "\nusername=" + username;
	}
//youbotToken替换成你bot的token信息
byte[] secretKey = DigestUtil.sha256("youbotToken");
HMac hmac = new HMac(HmacAlgorithm.HmacSHA256, secretKey);
String hex = hmac.digestHex(sb);
//校验通过
	if (hex.equals(hash)) {
		return Boolean.TRUE;
	} else {
	return Boolean.FALSE;
	}
//TODO 保存用户信息&其他业务逻辑

常见错误:
页面按钮提示Bot Domain invalid
代码打开地址域名和设置的白名单域不一致

尾声

第一次写这么长的教程,因为接触telegram bot的时间也不长这个教程只是简单的运行一个机器人,当然Telegram bot可以做到更高级的玩法,所以后面有机会的话会继续更新;文中有任何错误或者有疑问的欢迎邮件联系我。


评论