源码地址:https://github.com/ZinkLu/TGRobot
1. 背景
最近发现 TG 这个软件真的很好用,我并不关注什么隐私之类的。如果隐私是一件衣服的话,现在所有人的底裤都被扒了。
我是纯粹感觉里面功能十分清爽, SVG 动画也做得很酷很流畅;
不过最最重要的是,他对机器人的支持很友好。创建和开发都十分简单,github上也有不同语言封装的SDK,而且机器人几乎能做和人一模一样的事(甚至更多)。我看过有拿机器人来做进群校验的,有搜歌的,搜片的,也有搜图的。(为啥国内的这些聊天平台这么不开放呢。)
所以我也抽空写了一个简单的电报机器人,可以帮助来做一些事情(当然,我这里出发点是为了去管理vmshell服务器)。
PS: inline_keyboard 真的挺强大的,甚至可以搞一个简单的基于文本+选项的游戏了
2. 构建
需要用到 go 1.18
git clone git@github.com:ZinkLu/TGRobot.git && cd TGRobot && go build
3. 如何使用
3.1 启动
要启动机器人,需要一个适当的配置文件,查看config部分以获得更多细节。
命令。
./TGRobot -c config.yaml
3.2 配置
config 文件是一个有效的 yaml 格式文件或 json 格式文件。
推荐 yaml,能写注释
apiToken: xxx
debug: false
handlers:
message_handler:
vmShell: xxx
anotherMessageHandler: xxx
inline_keyboard_handler:
xxx: Xxx
picture_handler:
xxx: xxx
...
apiToken
: 机器人Token,可以从@botfather获得。debug
: 如果设置为true
,将记录更多的信息。Handlers
: 这部分应该包括不同的 handlers 的配置,详见项目结构
我也写了一些 handlers,可以直接使用
4. 内置 Handler
4.1 VmShell Handler
vmShell handler 当前可以获取服务器的信息,后面希望可以完成对服务器的简单操作(重启等,也可以执行一些指令)
4.1.1 配置
vmshell 是一个message handler,但也是一个 inline-keyboard handler,由于它处理的是原始的对话信息,我们把它放在 message_handler
中。
handlers:
message_handler:
vmShell:
username: vmshellAccount
password: vmshellAccountPassword
serverIds:
- xxx
- xxx
handers.message_handler.vmShell
:username
: vmshell 账户password
: vmshell 密码serverIds
: 想控制的服务器id
警告!
要不开启双重认证(google auth)
除非vmshell服务器可以通过apiToken访问,据他们客服说的,apiToken正在开发中。
4.1.2 如何获取serverId
打开你的服务列表
按
F12
打开开发控制台记得选择
preserve log
和Fetch/XHR
。选择一个服务器,见下图。
然后你的控制台应该会显示一个包含
serverId
的XHR请求。
4.1.3 用法
目前,vmshell handler 可以处理的信息有:
流量
:获取服务器带宽使用情况服务器
:获取服务器信息
只要向机器人发送任何带有上述关键词的信息,就可以执行对应的动作。
4.1.4 TODO
将serverId改为serverIds,这样我们就可以控制多个服务器。
如果serverIds是空的,那机器人会自动获取。
4.2 一言 Handler
一言处理程序不需要任何配置。
4.2.1 用法
发送 一句话
给机器人来获取你的一言。
特别感谢 hitokoto.cn (本网站的404也是由一言驱动的)
4.5 Tgo Handler
Tgo Handler 可以通过 gRPC 和 Tgo API 通讯,从而拿到用户信息。
当前只支持查询,不支持新增修改删除。
4.5.1 配置
Tgo Handler 也是一个 message_handler
:
handlers:
message_handler:
Tgo:
api_addr: "127.0.0.1"
api_port: 1444
use_cert: true
sni: example.com
ca_cert_path: []
cert_path: client.crt
cert_key: client.key
verify: true
handers.message_handler.Tgo
:api_addr
: grpc API’s hostapi_port
: grpc API’s portuse_cert
: 是否启用tls链接,如果启用则cert_pat
和cert_key
都必须填写sni
: 如果你使用 IP 去连接服务,则这个值会用来验证服务端提供的证书信息中的域名信息ca_cert_path
: 如果你使用的自签名的证书,则这个指定你做自签名时的 CA 证书cert_path
: 客户端的证书路径,你还需要再服务端将这本证书添加到信任列表中,详见这里cert_key
: 客户端证书的keyverify
: 是否开启服务端证书的校验,推荐一直设置为 true
当前只支持不带证书的 gRPC 链接
4.5.2 用法
给机器人发送 我的流量
,这时机器人会要求你输入密码。
选中机器人的这条信息,然后点击回复信息,再输入密码后再发送。
如果密码正确,就可以显示用户用量:
USAGE ⏳184.61MB(0.18Gb).
SPEED ⬆️0.00kb/s, ⬇️0.00kb/s
SPEED LIMIT ⬆️0.00kb/s, ⬇️0.00kb/s
ONLINE DEVICE 💻 0(current) 0(total)
查询成功后密码会被缓存,因此下次直接发送 我的流量
即可重新查询。
4.5.3 TODO
- 支持安全的 gRPC 链接
- 支持管理员密码,能对用户进行进一步管理
5. 添加自定义处理程序
在编写代码之初,我就不希望这个机器人只能处理某一种问题,比如只能控制服务器。
而是通过责任链 (Chain of Responsibility) 来串联同类的 Handler。
因此 message handler 和 inline_keyboard handler 是两条责任链。不同类型的 handler 互不影响。
如果想给机器人添加一些额外的 handler 也很方便。
(不过很多类型的 handler 还没有创建好基类)。
5.1 代码结构
由于 Telegram 有许多消息类型,源代码的结构是为了处理不同类型的消息而排布的。
每个文件夹都是一个 handler,我们称这些真正处理消息的 handler 为 app handler(应用程序)。
├── handlers
│ ├── handlers.go
| ├── register.go
│ └── message_handler
│ ├── message_handler.go
│ └── vmshell
│ ├── config.go
│ ├── server_info.go
│ ├── vmshell_client.go
│ ├── vmshell_client_test.go
│ └── vmshell_handler.go
| ├── inline_keyboard_handler
| ├── video_handler(not implement)
| ├── command_handler(not implement)
5.2 自定义 App Handler
假设现在要添加一个 message handler,用于获取本地天气。
现在 handlers/message_handler/
下创建 weather 文件夹。
5.3 定义 Config
我们希望获取本地天气,因此需要在配置里面配好所在的城市,所以我们在 handler.message_handler
下加一个 mapping 。
apiToken: xxx
debug: false
handlers:
message_handler:
weather:
city: Shanghai
别担心,一会这个配置会传到 Handler 里面,不需要额外的操作。
5.4 定义 Handler
一个 Handler 必须要符合 common.AppHandlerInterface
接口
type AppHandlerInterface interface {
Handle(*tgbotapi.Update, *tgbotapi.BotAPI) // 真正的处理方法
When(*tgbotapi.Update) bool // 如果返回true,则可以执行 Handle ,否则就 fallback 到下一个 handler
Init(*config.ConfigUnmarshaler) // 程序会调用 Init 方法,并且把配置项传进来
Order() int // 越小代表 handler 越先执行
Help() string // 帮助信息,如果所有的 handler 都无法处理消息,bot 会发送所有已经注册的应用程序的帮助信息。
Name() string // 应用程序名称,不能重名,否则会panic
}
我们来实现一个 WeatherHandler,为了能使用刚才的yaml中的配置文件,我们定义一个 struct 来与之对应
package weather
type Config struct {
City string `configKey:"city"`
}
本项目支持 json 和 yaml 两种配置文件格式,提供了新的 tag 来 unmarshal 对象,因此,我们可以来定义下 Init 方法
func (w *WeatherHandler) Init(conf *config.ConfigUnmarshaler) {
wConf := &Config{}
conf.UnmarshalConfig(wConf, w.Name())
w.City = wConf.City
}
func (w *WeatherHandler) Name() string {
return "weather"
}
func (w *WeatherHandler) Order() int {
return 999
}
*config.ConfigUnmarshaler
的 UnmarshalConfig
方法会自动将 handler.message_handler
下的所有配置传给你,只需要 unmarshal 你关心的 Key(这里是 weather)
我们顺便设置了 Order,这个方法也会影响 Help 信息显示的顺序。
现在可以设置应用程序的触发条件,我们处理所有包含天气
的信息(前提是消息没有被其他 handler处理):
func (w *WeatherHandler) When(u *tgbotapi.Update) bool {
return strings.Contains(u.Message.Text, "天气")
}
func (w *WeatherHandler) Help() string {
return "对我说, '天气'"
}
u.Message
虽然是一个指针,不过可以放心大胆的u.Message.Text
,也不用害怕空指针的问题,因为 message Handler 的触发条件必须是 message 不为空。
现在,可以定义真正的 Handle 方法了,让我们写一段伪代码
func (w *WeatherHandler) Handle(u *tgbotapi.Update, bot *tgbotapi.BotAPI) {
URL, _ := url.Parse(WEATHER_PROVIDER_URL)
URL.Query().Set("City", w.City)
resp, _ := http.Get(URL.String())
content, _ := ioutil.ReadAll(resp.Body) // 根据实际情况查询
m := tgbotapi.NewMessage(u.Message.Chat.ID, string(content))
bot.Send(m)
}
5.5 注册 Handler
为了启用 Handler,需要将 Handler 注册到他上级的 Handler 中,这里上级 handler 是 message_handler
,直接在 init 方法中直接使用 message_handler.Register
方法
func init() {
message_handler.Register(&WeatherHandler{})
}
最后,在 handlers.go
文件中导入即可,这是所有 handler 的入口
package handlers
import (
_ "github.com/ZinkLu/TGRobot/handlers/message_handler/weather"
)
这样就 OK 了,不出意外机器人就能处理你的信息了。
5.6 获取其他 Handler
有时候,一个 app handler 可以依赖其他 handler,比如一个 inline_message handler 可能是被 message handler 触发的,他需要这个 message handler 中的配置文件来处理。
在 pool.GetAppHandlerByName
中,可以获取一个已经被注册的 handler,如果这个 handler 没有被注册,则程序可能 panic。
message_handler := pool.GetAppHandlerByName[*vm_message.VmShellHandler]("vmShell")
message_handler.Config.serverIds // get info