439 lines
11 KiB
Go
439 lines
11 KiB
Go
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()
|
||
}
|