openwechat/bot.go
2023-02-05 11:23:05 +08:00

439 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package openwechat
import (
"context"
"errors"
"io"
"log"
"net/url"
"os/exec"
"runtime"
"sync"
)
type Bot struct {
ScanCallBack func(body CheckLoginResponse) // 扫码回调,可获取扫码用户的头像
LoginCallBack func(body CheckLoginResponse) // 登陆回调
LogoutCallBack func(bot *Bot) // 退出回调
UUIDCallback func(uuid string) // 获取UUID的回调函数
SyncCheckCallback func(resp SyncCheckResponse) // 心跳回调
MessageHandler MessageHandler // 获取消息成功的handle
MessageErrorHandler func(err error) bool // 获取消息发生错误的handle, 返回true则尝试继续监听
Serializer Serializer // 序列化器, 默认为json
Storage *Storage
Caller *Caller
once sync.Once
err error
context context.Context
cancel context.CancelFunc
self *Self
hotReloadStorage HotReloadStorage
uuid string
loginUUID *string
deviceId string // 设备Id
loginOptionGroup BotOptionGroup
}
// Alive 判断当前用户是否正常在线
func (b *Bot) Alive() bool {
if b.self == nil {
return false
}
select {
case <-b.context.Done():
return false
default:
return true
}
}
// SetDeviceId
// @description: 设置设备Id
// @receiver b
// @param deviceId
// TODO ADD INTO LOGIN OPTION
func (b *Bot) SetDeviceId(deviceId string) {
b.deviceId = deviceId
}
// GetCurrentUser 获取当前的用户
//
// self, err := bot.GetCurrentUser()
// if err != nil {
// return
// }
// fmt.Println(self.NickName)
func (b *Bot) GetCurrentUser() (*Self, error) {
if b.self == nil {
return nil, errors.New("user not login")
}
return b.self, nil
}
// login 这里对进行一些对登录前后的hook
func (b *Bot) login(login BotLogin) (err error) {
opt := b.loginOptionGroup
opt.Prepare(b)
if err = login.Login(b); err != nil {
err = opt.OnError(b, err)
}
if err != nil {
return err
}
return opt.OnSuccess(b)
}
// Login 用户登录
func (b *Bot) Login() error {
scanLogin := &SacnLogin{UUID: b.loginUUID}
return b.login(scanLogin)
}
// HotLogin 热登录,可实现在单位时间内免重复扫码登录
func (b *Bot) HotLogin(storage HotReloadStorage, opts ...BotLoginOption) error {
hotLogin := &HotLogin{storage: storage}
// 进行相关设置。
// 如果相对默认的行为进行修改在opts里面进行追加即可。
b.loginOptionGroup = append(hotLoginDefaultOptions[:], opts...)
return b.login(hotLogin)
}
// PushLogin 免扫码登录
// 免扫码登录需要先扫码登录一次才可以进行扫码登录
// 扫码登录成功后需要利用微信号发送一条消息,然后在手机上进行主动退出。
// 这时候在进行一次 PushLogin 即可。
func (b *Bot) PushLogin(storage HotReloadStorage, opts ...BotLoginOption) error {
pushLogin := &PushLogin{storage: storage}
// 进行相关设置。
// 如果相对默认的行为进行修改在opts里面进行追加即可。
b.loginOptionGroup = append(pushLoginDefaultOptions[:], opts...)
return b.login(pushLogin)
}
// Logout 用户退出
func (b *Bot) Logout() error {
if b.Alive() {
info := b.Storage.LoginInfo
if err := b.Caller.Logout(info); err != nil {
return err
}
b.Exit()
return nil
}
return errors.New("user not login")
}
// HandleLogin 登录逻辑
func (b *Bot) HandleLogin(path *url.URL) error {
// 获取登录的一些基本的信息
info, err := b.Caller.GetLoginInfo(path)
if err != nil {
return err
}
// 将LoginInfo存到storage里面
b.Storage.LoginInfo = info
// 处理设备Id
if b.deviceId == "" {
b.deviceId = GetRandomDeviceId()
}
// 构建BaseRequest
request := &BaseRequest{
Uin: info.WxUin,
Sid: info.WxSid,
Skey: info.SKey,
DeviceID: b.deviceId,
}
// 将BaseRequest存到storage里面方便后续调用
b.Storage.Request = request
// 如果是热登陆,则将当前的重要信息写入hotReloadStorage
if b.hotReloadStorage != nil {
if err = b.DumpHotReloadStorage(); err != nil {
return err
}
}
return b.WebInit()
}
// WebInit 根据有效凭证获取和初始化用户信息
func (b *Bot) WebInit() error {
req := b.Storage.Request
info := b.Storage.LoginInfo
// 获取初始化的用户信息和一些必要的参数
resp, err := b.Caller.WebInit(req)
if err != nil {
return err
}
// 设置当前的用户
b.self = &Self{bot: b, User: resp.User}
b.self.formatEmoji()
b.self.self = b.self
resp.ContactList.init(b.self)
b.Storage.Response = resp
// 通知手机客户端已经登录
if err = b.Caller.WebWxStatusNotify(req, resp, info); err != nil {
return err
}
// 开启协程,轮询获取是否有新的消息返回
// FIX: 当bot在线的情况下执行热登录,会开启多次事件监听
go b.once.Do(func() {
if b.MessageErrorHandler == nil {
b.MessageErrorHandler = defaultSyncCheckErrHandler(b)
}
for {
err := b.syncCheck()
if err == nil {
continue
}
// 判断是否继续, 如果不继续则退出
if goon := b.MessageErrorHandler(err); !goon {
b.err = err
b.Exit()
break
}
}
})
return nil
}
// 轮询请求
// 根据状态码判断是否有新的请求
func (b *Bot) syncCheck() error {
var (
err error
resp *SyncCheckResponse
)
for b.Alive() {
// 长轮询检查是否有消息返回
resp, err = b.Caller.SyncCheck(b.Storage.Request, b.Storage.LoginInfo, b.Storage.Response)
if err != nil {
return err
}
// 执行心跳回调
if b.SyncCheckCallback != nil {
b.SyncCheckCallback(*resp)
}
// 如果不是正常的状态码返回,发生了错误,直接退出
if !resp.Success() {
return resp.Err()
}
switch resp.Selector {
case SelectorNewMsg:
messages, err := b.syncMessage()
if err != nil {
return err
}
if b.MessageHandler == nil {
continue
}
for _, message := range messages {
message.init(b)
// 默认同步调用
// 如果异步调用则需自行处理
// 如配合 openwechat.MessageMatchDispatcher 使用
// NOTE: 请确保 MessageHandler 不会阻塞,否则会导致收不到后续的消息
b.MessageHandler(message)
}
case SelectorModContact:
case SelectorAddOrDelContact:
case SelectorModChatRoom:
}
}
return err
}
// 获取新的消息
func (b *Bot) syncMessage() ([]*Message, error) {
resp, err := b.Caller.WebWxSync(b.Storage.Request, b.Storage.Response, b.Storage.LoginInfo)
if err != nil {
return nil, err
}
// 更新SyncKey并且重新存入storage
b.Storage.Response.SyncKey = resp.SyncKey
return resp.AddMsgList, nil
}
// Block 当消息同步发生了错误或者用户主动在手机上退出,该方法会立即返回,否则会一直阻塞
func (b *Bot) Block() error {
if b.self == nil {
return errors.New("`Block` must be called after user login")
}
<-b.Context().Done()
return nil
}
// Exit 主动退出,让 Block 不在阻塞
func (b *Bot) Exit() {
if b.LogoutCallBack != nil {
b.LogoutCallBack(b)
}
b.self = nil
b.cancel()
}
// CrashReason 获取当前Bot崩溃的原因
func (b *Bot) CrashReason() error {
return b.err
}
// DumpHotReloadStorage 写入HotReloadStorage
func (b *Bot) DumpHotReloadStorage() error {
if b.hotReloadStorage == nil {
return errors.New("HotReloadStorage can not be nil")
}
return b.DumpTo(b.hotReloadStorage)
}
// DumpTo 将热登录需要的数据写入到指定的 io.Writer 中
// 注: 写之前最好先清空之前的数据
func (b *Bot) DumpTo(writer io.Writer) error {
jar := b.Caller.Client.Jar()
item := HotReloadStorageItem{
BaseRequest: b.Storage.Request,
Jar: fromCookieJar(jar),
LoginInfo: b.Storage.LoginInfo,
WechatDomain: b.Caller.Client.Domain,
UUID: b.uuid,
}
return b.Serializer.Encode(writer, item)
}
// IsHot returns true if is hot login otherwise false
func (b *Bot) IsHot() bool {
return b.hotReloadStorage != nil
}
// UUID returns current UUID of bot
func (b *Bot) UUID() string {
return b.uuid
}
// SetUUID
// @description: 设置UUID可以用来手动登录用
// @receiver b
// @param UUID
// TODO ADD INTO LOGIN OPTION
func (b *Bot) SetUUID(uuid string) {
b.loginUUID = &uuid
}
// Context returns current context of bot
func (b *Bot) Context() context.Context {
return b.context
}
func (b *Bot) reload() error {
if b.hotReloadStorage == nil {
return errors.New("hotReloadStorage is nil")
}
var item HotReloadStorageItem
if err := b.Serializer.Decode(b.hotReloadStorage, &item); err != nil {
return err
}
b.Caller.Client.SetCookieJar(item.Jar)
b.Storage.LoginInfo = item.LoginInfo
b.Storage.Request = item.BaseRequest
b.Caller.Client.Domain = item.WechatDomain
b.uuid = item.UUID
return nil
}
// NewBot Bot的构造方法
// 接收外部的 context.Context用于控制Bot的存活
func NewBot(c context.Context) *Bot {
caller := DefaultCaller()
// 默认行为为网页版微信模式
caller.Client.SetMode(normal)
ctx, cancel := context.WithCancel(c)
return &Bot{
Caller: caller,
Storage: &Storage{},
Serializer: &JsonSerializer{},
context: ctx,
cancel: cancel,
}
}
// DefaultBot 默认的Bot的构造方法,
// mode不传入默认为 openwechat.Normal,详情见mode
//
// bot := openwechat.DefaultBot(openwechat.Desktop)
func DefaultBot(prepares ...BotPreparer) *Bot {
bot := NewBot(context.Background())
// 获取二维码回调
bot.UUIDCallback = PrintlnQrcodeUrl
// 扫码回调
bot.ScanCallBack = func(_ CheckLoginResponse) {
log.Println("扫码成功,请在手机上确认登录")
}
// 登录回调
bot.LoginCallBack = func(_ CheckLoginResponse) {
log.Println("登录成功")
}
// 心跳回调函数
// 默认的行为打印SyncCheckResponse
bot.SyncCheckCallback = func(resp SyncCheckResponse) {
log.Printf("RetCode:%s Selector:%s", resp.RetCode, resp.Selector)
}
for _, prepare := range prepares {
prepare.Prepare(bot)
}
return bot
}
// defaultSyncCheckErrHandler 默认的SyncCheck错误处理函数
func defaultSyncCheckErrHandler(bot *Bot) func(error) bool {
return func(err error) bool {
var ret Ret
if errors.As(err, &ret) {
switch ret {
case failedLoginCheck, cookieInvalid, failedLoginWarn:
_ = bot.Logout()
return false
}
}
return true
}
}
// GetQrcodeUrl 通过uuid获取登录二维码的url
func GetQrcodeUrl(uuid string) string {
return qrcode + uuid
}
// PrintlnQrcodeUrl 打印登录二维码
func PrintlnQrcodeUrl(uuid string) {
println("访问下面网址扫描二维码登录")
qrcodeUrl := GetQrcodeUrl(uuid)
println(qrcodeUrl)
// browser open the login url
_ = open(qrcodeUrl)
}
// open opens the specified URL in the default browser of the user.
func open(url string) error {
var (
cmd string
args []string
)
switch runtime.GOOS {
case "windows":
cmd, args = "cmd", []string{"/c", "start"}
case "darwin":
cmd = "open"
default:
// "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}