Compare commits
No commits in common. "master" and "v1.3.5" have entirely different histories.
@ -4,16 +4,16 @@
|
|||||||
|
|
||||||
[](https://godoc.org/github.com/eatMoreApple/openwechat)[](https://github.com/eatmoreapple/openwechat/releases)[](https://goreportcard.com/badge/github.com/eatmoreapple/openwechat)[](https://img.shields.io/github/stars/eatmoreapple/openwechat.svg?style=flat-square)[](https://img.shields.io/github/forks/eatmoreapple/openwechat.svg?style=flat-square)
|
[](https://godoc.org/github.com/eatMoreApple/openwechat)[](https://github.com/eatmoreapple/openwechat/releases)[](https://goreportcard.com/badge/github.com/eatmoreapple/openwechat)[](https://img.shields.io/github/stars/eatmoreapple/openwechat.svg?style=flat-square)[](https://img.shields.io/github/forks/eatmoreapple/openwechat.svg?style=flat-square)
|
||||||
|
|
||||||
> golang版个人微信号API, 突破登录限制,类似开发公众号一样,开发个人微信号
|
> golang版个人微信号API, 突破网页版限制,类似开发公众号一样,开发个人微信号
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
微信机器人:smiling_imp:,利用微信号完成一些功能的定制化开发⭐
|
微信机器人:smiling_imp:,利用微信号完成一些功能的定制化开发⭐
|
||||||
|
|
||||||
|
|
||||||
* 模块简单易用,易于扩展
|
|
||||||
* 支持定制化开发,如日志记录,自动回复
|
* 支持定制化开发,如日志记录,自动回复
|
||||||
* 突破登录限制📣
|
* 突破网页版登录限制📣
|
||||||
* 无需重复扫码登录
|
* 无需重复扫码登录
|
||||||
* 支持多个微信号同时登陆
|
* 支持多个微信号同时登陆
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ func main() {
|
|||||||
|
|
||||||
### 添加微信(EatMoreApple):apple:(备注: openwechat),进群交流:smiling_imp:
|
### 添加微信(EatMoreApple):apple:(备注: openwechat),进群交流:smiling_imp:
|
||||||
|
|
||||||
**如果二维码图片没显示出来,请添加微信号 eatmoreapple**
|
**如果二维码图片没显示出来,请添加微信号 EatMoreApple**
|
||||||
|
|
||||||
<img width="210px" src="https://raw.githubusercontent.com/eatmoreapple/eatMoreApple/main/img/wechat.jpg" align="left">
|
<img width="210px" src="https://raw.githubusercontent.com/eatmoreapple/eatMoreApple/main/img/wechat.jpg" align="left">
|
||||||
|
|
||||||
|
65
bot.go
65
bot.go
@ -2,34 +2,32 @@ package openwechat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bot struct {
|
type Bot struct {
|
||||||
ScanCallBack func(body CheckLoginResponse) // 扫码回调,可获取扫码用户的头像
|
ScanCallBack func(body []byte) // 扫码回调,可获取扫码用户的头像
|
||||||
LoginCallBack func(body CheckLoginResponse) // 登陆回调
|
LoginCallBack func(body []byte) // 登陆回调
|
||||||
LogoutCallBack func(bot *Bot) // 退出回调
|
LogoutCallBack func(bot *Bot) // 退出回调
|
||||||
UUIDCallback func(uuid string) // 获取UUID的回调函数
|
UUIDCallback func(uuid string) // 获取UUID的回调函数
|
||||||
SyncCheckCallback func(resp SyncCheckResponse) // 心跳回调
|
SyncCheckCallback func(resp SyncCheckResponse) // 心跳回调
|
||||||
MessageHandler MessageHandler // 获取消息成功的handle
|
MessageHandler MessageHandler // 获取消息成功的handle
|
||||||
MessageErrorHandler func(err error) bool // 获取消息发生错误的handle, 返回true则尝试继续监听
|
MessageErrorHandler func(err error) bool // 获取消息发生错误的handle, 返回true则尝试继续监听
|
||||||
Serializer Serializer // 序列化器, 默认为json
|
|
||||||
Storage *Storage
|
|
||||||
Caller *Caller
|
|
||||||
once sync.Once
|
once sync.Once
|
||||||
err error
|
err error
|
||||||
context context.Context
|
context context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
Caller *Caller
|
||||||
self *Self
|
self *Self
|
||||||
|
Storage *Storage
|
||||||
hotReloadStorage HotReloadStorage
|
hotReloadStorage HotReloadStorage
|
||||||
uuid string
|
uuid string
|
||||||
loginUUID *string
|
|
||||||
deviceId string // 设备Id
|
deviceId string // 设备Id
|
||||||
loginOptionGroup BotOptionGroup
|
loginOptionGroup BotOptionGroup
|
||||||
}
|
}
|
||||||
@ -51,7 +49,6 @@ func (b *Bot) Alive() bool {
|
|||||||
// @description: 设置设备Id
|
// @description: 设置设备Id
|
||||||
// @receiver b
|
// @receiver b
|
||||||
// @param deviceId
|
// @param deviceId
|
||||||
// TODO ADD INTO LOGIN OPTION
|
|
||||||
func (b *Bot) SetDeviceId(deviceId string) {
|
func (b *Bot) SetDeviceId(deviceId string) {
|
||||||
b.deviceId = deviceId
|
b.deviceId = deviceId
|
||||||
}
|
}
|
||||||
@ -85,7 +82,7 @@ func (b *Bot) login(login BotLogin) (err error) {
|
|||||||
|
|
||||||
// Login 用户登录
|
// Login 用户登录
|
||||||
func (b *Bot) Login() error {
|
func (b *Bot) Login() error {
|
||||||
scanLogin := &SacnLogin{UUID: b.loginUUID}
|
scanLogin := &SacnLogin{}
|
||||||
return b.login(scanLogin)
|
return b.login(scanLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,9 +121,9 @@ func (b *Bot) Logout() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleLogin 登录逻辑
|
// HandleLogin 登录逻辑
|
||||||
func (b *Bot) HandleLogin(path *url.URL) error {
|
func (b *Bot) HandleLogin(data []byte) error {
|
||||||
// 获取登录的一些基本的信息
|
// 获取登录的一些基本的信息
|
||||||
info, err := b.Caller.GetLoginInfo(path)
|
info, err := b.Caller.GetLoginInfo(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -169,10 +166,9 @@ func (b *Bot) WebInit() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 设置当前的用户
|
// 设置当前的用户
|
||||||
b.self = &Self{bot: b, User: resp.User}
|
b.self = &Self{bot: b, User: &resp.User}
|
||||||
b.self.formatEmoji()
|
b.self.formatEmoji()
|
||||||
b.self.self = b.self
|
b.self.self = b.self
|
||||||
resp.ContactList.init(b.self)
|
|
||||||
b.Storage.Response = resp
|
b.Storage.Response = resp
|
||||||
|
|
||||||
// 通知手机客户端已经登录
|
// 通知手机客户端已经登录
|
||||||
@ -194,7 +190,6 @@ func (b *Bot) WebInit() error {
|
|||||||
// 判断是否继续, 如果不继续则退出
|
// 判断是否继续, 如果不继续则退出
|
||||||
if goon := b.MessageErrorHandler(err); !goon {
|
if goon := b.MessageErrorHandler(err); !goon {
|
||||||
b.err = err
|
b.err = err
|
||||||
b.Exit()
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,8 +218,8 @@ func (b *Bot) syncCheck() error {
|
|||||||
if !resp.Success() {
|
if !resp.Success() {
|
||||||
return resp.Err()
|
return resp.Err()
|
||||||
}
|
}
|
||||||
switch resp.Selector {
|
// 如果Selector不为0,则获取消息
|
||||||
case SelectorNewMsg:
|
if !resp.NorMal() {
|
||||||
messages, err := b.syncMessage()
|
messages, err := b.syncMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -237,12 +232,8 @@ func (b *Bot) syncCheck() error {
|
|||||||
// 默认同步调用
|
// 默认同步调用
|
||||||
// 如果异步调用则需自行处理
|
// 如果异步调用则需自行处理
|
||||||
// 如配合 openwechat.MessageMatchDispatcher 使用
|
// 如配合 openwechat.MessageMatchDispatcher 使用
|
||||||
// NOTE: 请确保 MessageHandler 不会阻塞,否则会导致收不到后续的消息
|
|
||||||
b.MessageHandler(message)
|
b.MessageHandler(message)
|
||||||
}
|
}
|
||||||
case SelectorModContact:
|
|
||||||
case SelectorAddOrDelContact:
|
|
||||||
case SelectorModChatRoom:
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@ -293,15 +284,15 @@ func (b *Bot) DumpHotReloadStorage() error {
|
|||||||
// DumpTo 将热登录需要的数据写入到指定的 io.Writer 中
|
// DumpTo 将热登录需要的数据写入到指定的 io.Writer 中
|
||||||
// 注: 写之前最好先清空之前的数据
|
// 注: 写之前最好先清空之前的数据
|
||||||
func (b *Bot) DumpTo(writer io.Writer) error {
|
func (b *Bot) DumpTo(writer io.Writer) error {
|
||||||
jar := b.Caller.Client.Jar()
|
cookies := b.Caller.Client.GetCookieJar()
|
||||||
item := HotReloadStorageItem{
|
item := HotReloadStorageItem{
|
||||||
BaseRequest: b.Storage.Request,
|
BaseRequest: b.Storage.Request,
|
||||||
Jar: fromCookieJar(jar),
|
Jar: cookies,
|
||||||
LoginInfo: b.Storage.LoginInfo,
|
LoginInfo: b.Storage.LoginInfo,
|
||||||
WechatDomain: b.Caller.Client.Domain,
|
WechatDomain: b.Caller.Client.Domain,
|
||||||
UUID: b.uuid,
|
UUID: b.uuid,
|
||||||
}
|
}
|
||||||
return b.Serializer.Encode(writer, item)
|
return json.NewEncoder(writer).Encode(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHot returns true if is hot login otherwise false
|
// IsHot returns true if is hot login otherwise false
|
||||||
@ -309,20 +300,11 @@ func (b *Bot) IsHot() bool {
|
|||||||
return b.hotReloadStorage != nil
|
return b.hotReloadStorage != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UUID returns current UUID of bot
|
// UUID returns current uuid of bot
|
||||||
func (b *Bot) UUID() string {
|
func (b *Bot) UUID() string {
|
||||||
return b.uuid
|
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
|
// Context returns current context of bot
|
||||||
func (b *Bot) Context() context.Context {
|
func (b *Bot) Context() context.Context {
|
||||||
return b.context
|
return b.context
|
||||||
@ -333,10 +315,11 @@ func (b *Bot) reload() error {
|
|||||||
return errors.New("hotReloadStorage is nil")
|
return errors.New("hotReloadStorage is nil")
|
||||||
}
|
}
|
||||||
var item HotReloadStorageItem
|
var item HotReloadStorageItem
|
||||||
if err := b.Serializer.Decode(b.hotReloadStorage, &item); err != nil {
|
err := json.NewDecoder(b.hotReloadStorage).Decode(&item)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.Caller.Client.SetCookieJar(item.Jar)
|
b.Caller.Client.Jar = item.Jar.AsCookieJar()
|
||||||
b.Storage.LoginInfo = item.LoginInfo
|
b.Storage.LoginInfo = item.LoginInfo
|
||||||
b.Storage.Request = item.BaseRequest
|
b.Storage.Request = item.BaseRequest
|
||||||
b.Caller.Client.Domain = item.WechatDomain
|
b.Caller.Client.Domain = item.WechatDomain
|
||||||
@ -351,13 +334,7 @@ func NewBot(c context.Context) *Bot {
|
|||||||
// 默认行为为网页版微信模式
|
// 默认行为为网页版微信模式
|
||||||
caller.Client.SetMode(normal)
|
caller.Client.SetMode(normal)
|
||||||
ctx, cancel := context.WithCancel(c)
|
ctx, cancel := context.WithCancel(c)
|
||||||
return &Bot{
|
return &Bot{Caller: caller, Storage: &Storage{}, context: ctx, cancel: cancel}
|
||||||
Caller: caller,
|
|
||||||
Storage: &Storage{},
|
|
||||||
Serializer: &JsonSerializer{},
|
|
||||||
context: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultBot 默认的Bot的构造方法,
|
// DefaultBot 默认的Bot的构造方法,
|
||||||
@ -369,11 +346,11 @@ func DefaultBot(prepares ...BotPreparer) *Bot {
|
|||||||
// 获取二维码回调
|
// 获取二维码回调
|
||||||
bot.UUIDCallback = PrintlnQrcodeUrl
|
bot.UUIDCallback = PrintlnQrcodeUrl
|
||||||
// 扫码回调
|
// 扫码回调
|
||||||
bot.ScanCallBack = func(_ CheckLoginResponse) {
|
bot.ScanCallBack = func(body []byte) {
|
||||||
log.Println("扫码成功,请在手机上确认登录")
|
log.Println("扫码成功,请在手机上确认登录")
|
||||||
}
|
}
|
||||||
// 登录回调
|
// 登录回调
|
||||||
bot.LoginCallBack = func(_ CheckLoginResponse) {
|
bot.LoginCallBack = func(body []byte) {
|
||||||
log.Println("登录成功")
|
log.Println("登录成功")
|
||||||
}
|
}
|
||||||
// 心跳回调函数
|
// 心跳回调函数
|
||||||
|
91
bot_login.go
91
bot_login.go
@ -1,39 +1,9 @@
|
|||||||
package openwechat
|
package openwechat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoginCode 定义登录状态码
|
|
||||||
type LoginCode string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// LoginCodeSuccess 登录成功
|
|
||||||
LoginCodeSuccess LoginCode = "200"
|
|
||||||
// LoginCodeScanned 已扫码
|
|
||||||
LoginCodeScanned LoginCode = "201"
|
|
||||||
// LoginCodeTimeout 登录超时
|
|
||||||
LoginCodeTimeout LoginCode = "400"
|
|
||||||
// LoginCodeWait 等待扫码
|
|
||||||
LoginCodeWait LoginCode = "408"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (l LoginCode) String() string {
|
|
||||||
switch l {
|
|
||||||
case LoginCodeSuccess:
|
|
||||||
return "登录成功"
|
|
||||||
case LoginCodeScanned:
|
|
||||||
return "已扫码"
|
|
||||||
case LoginCodeTimeout:
|
|
||||||
return "登录超时"
|
|
||||||
case LoginCodeWait:
|
|
||||||
return "等待扫码"
|
|
||||||
default:
|
|
||||||
return "未知状态"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BotPreparer interface {
|
type BotPreparer interface {
|
||||||
Prepare(*Bot)
|
Prepare(*Bot)
|
||||||
}
|
}
|
||||||
@ -133,16 +103,16 @@ func NewSyncReloadDataLoginOption(duration time.Duration) BotLoginOption {
|
|||||||
return &SyncReloadDataLoginOption{SyncLoopDuration: duration}
|
return &SyncReloadDataLoginOption{SyncLoopDuration: duration}
|
||||||
}
|
}
|
||||||
|
|
||||||
// withModeOption 指定使用哪种客户端模式
|
// WithModeOption 指定使用哪种客户端模式
|
||||||
type withModeOption struct {
|
type WithModeOption struct {
|
||||||
mode Mode
|
mode Mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare 实现了 BotLoginOption 接口
|
// Prepare 实现了 BotLoginOption 接口
|
||||||
func (w withModeOption) Prepare(b *Bot) { b.Caller.Client.SetMode(w.mode) }
|
func (w WithModeOption) Prepare(b *Bot) { b.Caller.Client.SetMode(w.mode) }
|
||||||
|
|
||||||
func withMode(mode Mode) BotPreparer {
|
func withMode(mode Mode) BotPreparer {
|
||||||
return withModeOption{mode: mode}
|
return WithModeOption{mode: mode}
|
||||||
}
|
}
|
||||||
|
|
||||||
// btw, 这两个变量已经变了4回了, 但是为了兼容以前的代码, 还是得想着法儿让用户无感知的更新
|
// btw, 这两个变量已经变了4回了, 但是为了兼容以前的代码, 还是得想着法儿让用户无感知的更新
|
||||||
@ -154,19 +124,6 @@ var (
|
|||||||
Desktop = withMode(desktop)
|
Desktop = withMode(desktop)
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithContextOption 指定一个 context.Context 用于Bot的生命周期
|
|
||||||
type WithContextOption struct {
|
|
||||||
Ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare 实现了 BotLoginOption 接口
|
|
||||||
func (w WithContextOption) Prepare(b *Bot) {
|
|
||||||
if w.Ctx == nil {
|
|
||||||
panic("context is nil")
|
|
||||||
}
|
|
||||||
b.context, b.cancel = context.WithCancel(w.Ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultHotStorageSyncDuration = time.Minute * 5
|
defaultHotStorageSyncDuration = time.Minute * 5
|
||||||
)
|
)
|
||||||
@ -177,22 +134,14 @@ type BotLogin interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SacnLogin 扫码登录
|
// SacnLogin 扫码登录
|
||||||
type SacnLogin struct {
|
type SacnLogin struct{}
|
||||||
UUID *string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login 实现了 BotLogin 接口
|
// Login 实现了 BotLogin 接口
|
||||||
func (s *SacnLogin) Login(bot *Bot) error {
|
func (s *SacnLogin) Login(bot *Bot) error {
|
||||||
var uuid string
|
uuid, err := bot.Caller.GetLoginUUID()
|
||||||
if s.UUID == nil {
|
|
||||||
var err error
|
|
||||||
uuid, err = bot.Caller.GetLoginUUID()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
uuid = *s.UUID
|
|
||||||
}
|
|
||||||
return s.checkLogin(bot, uuid)
|
return s.checkLogin(bot, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,8 +225,8 @@ type LoginChecker struct {
|
|||||||
Bot *Bot
|
Bot *Bot
|
||||||
Tip string
|
Tip string
|
||||||
UUIDCallback func(uuid string)
|
UUIDCallback func(uuid string)
|
||||||
LoginCallBack func(body CheckLoginResponse)
|
LoginCallBack func(body []byte)
|
||||||
ScanCallBack func(body CheckLoginResponse)
|
ScanCallBack func(body []byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LoginChecker) CheckLogin() error {
|
func (l *LoginChecker) CheckLogin() error {
|
||||||
@ -293,35 +242,27 @@ func (l *LoginChecker) CheckLogin() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
code, err := resp.Code()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tip == "1" {
|
if tip == "1" {
|
||||||
tip = "0"
|
tip = "0"
|
||||||
}
|
}
|
||||||
switch code {
|
switch resp.Code {
|
||||||
case LoginCodeSuccess:
|
case StatusSuccess:
|
||||||
// 判断是否有登录回调,如果有执行它
|
// 判断是否有登录回调,如果有执行它
|
||||||
redirectURL, err := resp.RedirectURL()
|
if err = l.Bot.HandleLogin(resp.Raw); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = l.Bot.HandleLogin(redirectURL); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cb := l.LoginCallBack; cb != nil {
|
if cb := l.LoginCallBack; cb != nil {
|
||||||
cb(resp)
|
cb(resp.Raw)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case LoginCodeScanned:
|
case StatusScanned:
|
||||||
// 执行扫码回调
|
// 执行扫码回调
|
||||||
if cb := l.ScanCallBack; cb != nil {
|
if cb := l.ScanCallBack; cb != nil {
|
||||||
cb(resp)
|
cb(resp.Raw)
|
||||||
}
|
}
|
||||||
case LoginCodeTimeout:
|
case StatusTimeout:
|
||||||
return ErrLoginTimeout
|
return ErrLoginTimeout
|
||||||
case LoginCodeWait:
|
case StatusWait:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
bot_test.go
26
bot_test.go
@ -128,30 +128,4 @@ func TestSender(t *testing.T) {
|
|||||||
bot.Block()
|
bot.Block()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetUUID
|
|
||||||
// @description: 获取登录二维码(UUID)
|
|
||||||
// @param t
|
|
||||||
func TestGetUUID(t *testing.T) {
|
|
||||||
bot := DefaultBot(Desktop)
|
|
||||||
|
|
||||||
uuid, err := bot.Caller.GetLoginUUID()
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Log(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLoginWithUUID
|
|
||||||
// @description: 使用UUID登录
|
|
||||||
// @param t
|
|
||||||
func TestLoginWithUUID(t *testing.T) {
|
|
||||||
uuid := "oZZsO0Qv8Q=="
|
|
||||||
bot := DefaultBot(Desktop)
|
|
||||||
bot.SetUUID(uuid)
|
|
||||||
err := bot.Login()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("登录失败: %v", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
47
caller.go
47
caller.go
@ -50,7 +50,7 @@ func (c *Caller) GetLoginUUID() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckLogin 检查是否登录成功
|
// CheckLogin 检查是否登录成功
|
||||||
func (c *Caller) CheckLogin(uuid, tip string) (CheckLoginResponse, error) {
|
func (c *Caller) CheckLogin(uuid, tip string) (*CheckLoginResponse, error) {
|
||||||
resp, err := c.Client.CheckLogin(uuid, tip)
|
resp, err := c.Client.CheckLogin(uuid, tip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -61,13 +61,29 @@ func (c *Caller) CheckLogin(uuid, tip string) (CheckLoginResponse, error) {
|
|||||||
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return buffer.Bytes(), nil
|
// 正则匹配检测的code
|
||||||
|
// 具体code参考global.go
|
||||||
|
results := statusCodeRegexp.FindSubmatch(buffer.Bytes())
|
||||||
|
if len(results) != 2 {
|
||||||
|
return nil, errors.New("error status code match")
|
||||||
|
}
|
||||||
|
code := string(results[1])
|
||||||
|
return &CheckLoginResponse{Code: code, Raw: buffer.Bytes()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLoginInfo 获取登录信息
|
// GetLoginInfo 获取登录信息
|
||||||
func (c *Caller) GetLoginInfo(path *url.URL) (*LoginInfo, error) {
|
func (c *Caller) GetLoginInfo(body []byte) (*LoginInfo, error) {
|
||||||
// 从响应体里面获取需要跳转的url
|
// 从响应体里面获取需要跳转的url
|
||||||
resp, err := c.Client.GetLoginInfo(path)
|
results := redirectUriRegexp.FindSubmatch(body)
|
||||||
|
if len(results) != 2 {
|
||||||
|
return nil, errors.New("redirect url does not match")
|
||||||
|
}
|
||||||
|
path, err := url.Parse(string(results[1]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.Client.Domain = WechatDomain(path.Host)
|
||||||
|
resp, err := c.Client.GetLoginInfo(path.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -501,27 +517,30 @@ func (p *MessageResponseParser) SentMessage(msg *SendMessage) (*SentMessage, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readerToFile(reader io.Reader) (file *os.File, cb func(), err error) {
|
func readerToFile(reader io.Reader) (file *os.File, cb func(), err error) {
|
||||||
var ok bool
|
if file, ok := reader.(*os.File); ok {
|
||||||
if file, ok = reader.(*os.File); ok {
|
|
||||||
return file, func() {}, nil
|
return file, func() {}, nil
|
||||||
}
|
}
|
||||||
file, err = os.CreateTemp("", "*")
|
file, err = os.CreateTemp("", "*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
cb = func() {
|
|
||||||
_ = file.Close()
|
|
||||||
_ = os.Remove(file.Name())
|
|
||||||
}
|
|
||||||
_, err = io.Copy(file, reader)
|
_, err = io.Copy(file, reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cb()
|
_ = file.Close()
|
||||||
|
_ = os.Remove(file.Name())
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
_, err = file.Seek(0, io.SeekStart)
|
if err = file.Close(); err != nil {
|
||||||
|
_ = os.Remove(file.Name())
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
file, err = os.Open(file.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cb()
|
_ = os.Remove(file.Name())
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return file, cb, nil
|
return file, func() {
|
||||||
|
_ = file.Close()
|
||||||
|
_ = os.Remove(file.Name())
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
132
client.go
132
client.go
@ -15,6 +15,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,44 +33,30 @@ func (u UserAgentHook) BeforeRequest(req *http.Request) {
|
|||||||
req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36")
|
req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u UserAgentHook) AfterRequest(_ *http.Response, _ error) {}
|
func (u UserAgentHook) AfterRequest(response *http.Response, err error) {}
|
||||||
|
|
||||||
// Client http请求客户端
|
// Client http请求客户端
|
||||||
// 客户端需要维持Session会话
|
// 客户端需要维持Session会话
|
||||||
|
// 并且客户端不允许跳转
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// 设置一些client的请求行为
|
|
||||||
// see normalMode desktopMode
|
|
||||||
mode Mode
|
|
||||||
|
|
||||||
// client http客户端
|
|
||||||
client *http.Client
|
|
||||||
|
|
||||||
// Domain 微信服务器请求域名
|
|
||||||
// 这个参数会在登录成功后被赋值
|
|
||||||
// 之后所有的请求都会使用这个域名
|
|
||||||
// 在登录热登录和扫码登录时会被重新赋值
|
|
||||||
Domain WechatDomain
|
|
||||||
|
|
||||||
// HttpHooks 请求上下文钩子
|
|
||||||
HttpHooks HttpHooks
|
HttpHooks HttpHooks
|
||||||
|
*http.Client
|
||||||
// MaxRetryTimes 最大重试次数
|
Domain WechatDomain
|
||||||
|
mode Mode
|
||||||
|
mu sync.Mutex
|
||||||
MaxRetryTimes int
|
MaxRetryTimes int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient() *Client {
|
func NewClient() *Client {
|
||||||
httpClient := &http.Client{
|
timeout := 30 * time.Second
|
||||||
// 设置客户端不自动跳转
|
return &Client{
|
||||||
|
Client: &http.Client{
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
},
|
},
|
||||||
// 设置30秒超时
|
Jar: newCookieJar(),
|
||||||
// 因为微信同步消息时是一个时间长达25秒的长轮训
|
Timeout: timeout,
|
||||||
Timeout: 30 * time.Second,
|
}}
|
||||||
}
|
|
||||||
client := &Client{client: httpClient}
|
|
||||||
client.SetCookieJar(NewJar())
|
|
||||||
return client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultClient 自动存储cookie
|
// DefaultClient 自动存储cookie
|
||||||
@ -94,7 +81,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
for i := 0; i < c.MaxRetryTimes; i++ {
|
for i := 0; i < c.MaxRetryTimes; i++ {
|
||||||
resp, err = c.client.Do(req)
|
resp, err = c.Client.Do(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -114,17 +101,9 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|||||||
return c.do(req)
|
return c.do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jar 返回当前client的 http.CookieJar
|
// GetCookieJar 获取当前client的所有的有效的client
|
||||||
// this http.CookieJar must be *Jar type
|
func (c *Client) GetCookieJar() *Jar {
|
||||||
func (c *Client) Jar() http.CookieJar {
|
return fromCookieJar(c.Client.Jar)
|
||||||
return c.client.Jar
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCookieJar 设置cookieJar
|
|
||||||
// 这里限制了cookieJar必须是Jar类型
|
|
||||||
// 否则进行cookie序列化的时候因为字段的私有性无法进行所有字段的导出
|
|
||||||
func (c *Client) SetCookieJar(jar *Jar) {
|
|
||||||
c.client.Jar = jar.AsCookieJar()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLoginUUID 获取登录的uuid
|
// GetLoginUUID 获取登录的uuid
|
||||||
@ -135,7 +114,7 @@ func (c *Client) GetLoginUUID() (*http.Response, error) {
|
|||||||
// GetLoginQrcode 获取登录的二维吗
|
// GetLoginQrcode 获取登录的二维吗
|
||||||
func (c *Client) GetLoginQrcode(uuid string) (*http.Response, error) {
|
func (c *Client) GetLoginQrcode(uuid string) (*http.Response, error) {
|
||||||
path := qrcode + uuid
|
path := qrcode + uuid
|
||||||
return c.client.Get(path)
|
return c.Get(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckLogin 检查是否登录
|
// CheckLogin 检查是否登录
|
||||||
@ -154,9 +133,8 @@ func (c *Client) CheckLogin(uuid, tip string) (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLoginInfo 请求获取LoginInfo
|
// GetLoginInfo 请求获取LoginInfo
|
||||||
func (c *Client) GetLoginInfo(path *url.URL) (*http.Response, error) {
|
func (c *Client) GetLoginInfo(path string) (*http.Response, error) {
|
||||||
c.Domain = WechatDomain(path.Host)
|
return c.mode.GetLoginInfo(c, path)
|
||||||
return c.mode.GetLoginInfo(c, path.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebInit 请求获取初始化信息
|
// WebInit 请求获取初始化信息
|
||||||
@ -314,8 +292,6 @@ func (c *Client) WebWxGetHeadImg(user *User) (*http.Response, error) {
|
|||||||
return c.Do(req)
|
return c.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebWxUploadMediaByChunk 分块上传文件
|
|
||||||
// TODO 优化掉这个函数
|
|
||||||
func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, info *LoginInfo, forUserName, toUserName string) (*http.Response, error) {
|
func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, info *LoginInfo, forUserName, toUserName string) (*http.Response, error) {
|
||||||
// 获取文件上传的类型
|
// 获取文件上传的类型
|
||||||
contentType, err := GetFileContentType(file)
|
contentType, err := GetFileContentType(file)
|
||||||
@ -359,12 +335,8 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
|||||||
|
|
||||||
path.RawQuery = params.Encode()
|
path.RawQuery = params.Encode()
|
||||||
|
|
||||||
cookies := c.Jar().Cookies(path)
|
cookies := c.Jar.Cookies(path)
|
||||||
|
webWxDataTicket := getWebWxDataTicket(cookies)
|
||||||
webWxDataTicket, err := getWebWxDataTicket(cookies)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadMediaRequest := map[string]interface{}{
|
uploadMediaRequest := map[string]interface{}{
|
||||||
"UploadType": 2,
|
"UploadType": 2,
|
||||||
@ -416,17 +388,16 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunkBuff = make([]byte, chunkSize)
|
|
||||||
|
|
||||||
var formBuffer = bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
// 分块上传
|
// 分块上传
|
||||||
for chunk := 0; int64(chunk) < chunks; chunk++ {
|
for chunk := 0; int64(chunk) < chunks; chunk++ {
|
||||||
|
|
||||||
|
isLastTime := int64(chunk)+1 == chunks
|
||||||
|
|
||||||
if chunks > 1 {
|
if chunks > 1 {
|
||||||
content["chunk"] = strconv.Itoa(chunk)
|
content["chunk"] = strconv.Itoa(chunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
formBuffer.Reset()
|
var formBuffer = bytes.NewBuffer(nil)
|
||||||
|
|
||||||
writer := multipart.NewWriter(formBuffer)
|
writer := multipart.NewWriter(formBuffer)
|
||||||
|
|
||||||
@ -441,33 +412,34 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
|||||||
}
|
}
|
||||||
|
|
||||||
w, err := writer.CreateFormFile("filename", file.Name())
|
w, err := writer.CreateFormFile("filename", file.Name())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := file.Read(chunkBuff)
|
chunkData := make([]byte, chunkSize)
|
||||||
|
|
||||||
|
n, err := file.Read(chunkData)
|
||||||
|
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err = w.Write(chunkBuff[:n]); err != nil {
|
|
||||||
|
if _, err = w.Write(chunkData[:n]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ct := writer.FormDataContentType()
|
ct := writer.FormDataContentType()
|
||||||
if err = writer.Close(); err != nil {
|
if err = writer.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, _ := http.NewRequest(http.MethodPost, path.String(), formBuffer)
|
req, _ := http.NewRequest(http.MethodPost, path.String(), formBuffer)
|
||||||
req.Header.Set("Content-Type", ct)
|
req.Header.Set("Content-Type", ct)
|
||||||
|
|
||||||
// 发送数据
|
// 发送数据
|
||||||
resp, err = c.Do(req)
|
resp, err = c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
isLastTime := int64(chunk)+1 == chunks
|
|
||||||
// 如果不是最后一次, 解析有没有错误
|
// 如果不是最后一次, 解析有没有错误
|
||||||
if !isLastTime {
|
if !isLastTime {
|
||||||
parser := MessageResponseParser{Reader: resp.Body}
|
parser := MessageResponseParser{Reader: resp.Body}
|
||||||
@ -597,18 +569,13 @@ func (c *Client) WebWxGetVideo(msg *Message, info *LoginInfo) (*http.Response, e
|
|||||||
// WebWxGetMedia 获取文件消息的文件响应
|
// WebWxGetMedia 获取文件消息的文件响应
|
||||||
func (c *Client) WebWxGetMedia(msg *Message, info *LoginInfo) (*http.Response, error) {
|
func (c *Client) WebWxGetMedia(msg *Message, info *LoginInfo) (*http.Response, error) {
|
||||||
path, _ := url.Parse(c.Domain.FileHost() + webwxgetmedia)
|
path, _ := url.Parse(c.Domain.FileHost() + webwxgetmedia)
|
||||||
cookies := c.Jar().Cookies(path)
|
|
||||||
webWxDataTicket, err := getWebWxDataTicket(cookies)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("sender", msg.FromUserName)
|
params.Add("sender", msg.FromUserName)
|
||||||
params.Add("mediaid", msg.MediaId)
|
params.Add("mediaid", msg.MediaId)
|
||||||
params.Add("encryfilename", msg.EncryFileName)
|
params.Add("encryfilename", msg.EncryFileName)
|
||||||
params.Add("fromuser", strconv.FormatInt(info.WxUin, 10))
|
params.Add("fromuser", strconv.FormatInt(info.WxUin, 10))
|
||||||
params.Add("pass_ticket", info.PassTicket)
|
params.Add("pass_ticket", info.PassTicket)
|
||||||
params.Add("webwx_data_ticket", webWxDataTicket)
|
params.Add("webwx_data_ticket", getWebWxDataTicket(c.Jar.Cookies(path)))
|
||||||
path.RawQuery = params.Encode()
|
path.RawQuery = params.Encode()
|
||||||
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||||
req.Header.Add("Referer", c.Domain.BaseHost()+"/")
|
req.Header.Add("Referer", c.Domain.BaseHost()+"/")
|
||||||
@ -629,14 +596,6 @@ func (c *Client) Logout(info *LoginInfo) (*http.Response, error) {
|
|||||||
|
|
||||||
// AddMemberIntoChatRoom 添加用户进群聊
|
// AddMemberIntoChatRoom 添加用户进群聊
|
||||||
func (c *Client) AddMemberIntoChatRoom(req *BaseRequest, info *LoginInfo, group *Group, friends ...*Friend) (*http.Response, error) {
|
func (c *Client) AddMemberIntoChatRoom(req *BaseRequest, info *LoginInfo, group *Group, friends ...*Friend) (*http.Response, error) {
|
||||||
if len(group.MemberList) >= 40 {
|
|
||||||
return c.InviteMemberIntoChatRoom(req, info, group, friends...)
|
|
||||||
}
|
|
||||||
return c.addMemberIntoChatRoom(req, info, group, friends...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addMemberIntoChatRoom 添加用户进群聊
|
|
||||||
func (c *Client) addMemberIntoChatRoom(req *BaseRequest, info *LoginInfo, group *Group, friends ...*Friend) (*http.Response, error) {
|
|
||||||
path, _ := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
|
path, _ := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("fun", "addmember")
|
params.Add("fun", "addmember")
|
||||||
@ -658,29 +617,6 @@ func (c *Client) addMemberIntoChatRoom(req *BaseRequest, info *LoginInfo, group
|
|||||||
return c.Do(requ)
|
return c.Do(requ)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InviteMemberIntoChatRoom 邀请用户进群聊
|
|
||||||
func (c *Client) InviteMemberIntoChatRoom(req *BaseRequest, info *LoginInfo, group *Group, friends ...*Friend) (*http.Response, error) {
|
|
||||||
path, _ := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
|
|
||||||
params := url.Values{}
|
|
||||||
params.Add("fun", "invitemember")
|
|
||||||
params.Add("pass_ticket", info.PassTicket)
|
|
||||||
params.Add("lang", "zh_CN")
|
|
||||||
path.RawQuery = params.Encode()
|
|
||||||
addMemberList := make([]string, len(friends))
|
|
||||||
for index, friend := range friends {
|
|
||||||
addMemberList[index] = friend.UserName
|
|
||||||
}
|
|
||||||
content := map[string]interface{}{
|
|
||||||
"ChatRoomName": group.UserName,
|
|
||||||
"BaseRequest": req,
|
|
||||||
"InviteMemberList": strings.Join(addMemberList, ","),
|
|
||||||
}
|
|
||||||
buffer, _ := ToBuffer(content)
|
|
||||||
requ, _ := http.NewRequest(http.MethodPost, path.String(), buffer)
|
|
||||||
requ.Header.Set("Content-Type", jsonContentType)
|
|
||||||
return c.Do(requ)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveMemberFromChatRoom 从群聊中移除用户
|
// RemoveMemberFromChatRoom 从群聊中移除用户
|
||||||
func (c *Client) RemoveMemberFromChatRoom(req *BaseRequest, info *LoginInfo, group *Group, friends ...*User) (*http.Response, error) {
|
func (c *Client) RemoveMemberFromChatRoom(req *BaseRequest, info *LoginInfo, group *Group, friends ...*User) (*http.Response, error) {
|
||||||
path, _ := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
|
path, _ := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
|
||||||
|
@ -32,9 +32,6 @@ var (
|
|||||||
|
|
||||||
// ErrLoginTimeout define login timeout error
|
// ErrLoginTimeout define login timeout error
|
||||||
ErrLoginTimeout = errors.New("login timeout")
|
ErrLoginTimeout = errors.New("login timeout")
|
||||||
|
|
||||||
// ErrWebWxDataTicketNotFound define webwx_data_ticket not found error
|
|
||||||
ErrWebWxDataTicketNotFound = errors.New("webwx_data_ticket not found")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error impl error interface
|
// Error impl error interface
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
uuidRegexp = regexp.MustCompile(`uuid = "(.*?)";`)
|
uuidRegexp = regexp.MustCompile(`uuid = "(.*?)";`)
|
||||||
statusCodeRegexp = regexp.MustCompile(`window.code=(\d+);`)
|
statusCodeRegexp = regexp.MustCompile(`window.code=(\d+);`)
|
||||||
avatarRegexp = regexp.MustCompile(`window.userAvatar = '(.*)';`)
|
|
||||||
syncCheckRegexp = regexp.MustCompile(`window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`)
|
syncCheckRegexp = regexp.MustCompile(`window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`)
|
||||||
redirectUriRegexp = regexp.MustCompile(`window.redirect_uri="(.*?)"`)
|
redirectUriRegexp = regexp.MustCompile(`window.redirect_uri="(.*?)"`)
|
||||||
)
|
)
|
||||||
@ -98,6 +97,14 @@ const (
|
|||||||
AppMsgTypeReaderType AppMessageType = 100001 //自定义的消息
|
AppMsgTypeReaderType AppMessageType = 100001 //自定义的消息
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 登录状态
|
||||||
|
const (
|
||||||
|
StatusSuccess = "200"
|
||||||
|
StatusScanned = "201"
|
||||||
|
StatusTimeout = "400"
|
||||||
|
StatusWait = "408"
|
||||||
|
)
|
||||||
|
|
||||||
// ALL 跟search函数搭配
|
// ALL 跟search函数搭配
|
||||||
//
|
//
|
||||||
// friends.Search(openwechat.ALL, )
|
// friends.Search(openwechat.ALL, )
|
||||||
|
@ -2,8 +2,6 @@ package openwechat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -57,9 +55,9 @@ type WebInitResponse struct {
|
|||||||
SKey string
|
SKey string
|
||||||
BaseResponse BaseResponse
|
BaseResponse BaseResponse
|
||||||
SyncKey SyncKey
|
SyncKey SyncKey
|
||||||
User *User
|
User User
|
||||||
MPSubscribeMsgList []*MPSubscribeMsg
|
MPSubscribeMsgList []MPSubscribeMsg
|
||||||
ContactList Members
|
ContactList []User
|
||||||
}
|
}
|
||||||
|
|
||||||
// MPSubscribeMsg 公众号的订阅信息
|
// MPSubscribeMsg 公众号的订阅信息
|
||||||
@ -68,15 +66,13 @@ type MPSubscribeMsg struct {
|
|||||||
Time int64
|
Time int64
|
||||||
UserName string
|
UserName string
|
||||||
NickName string
|
NickName string
|
||||||
MPArticleList []*MPArticle
|
MPArticleList []struct {
|
||||||
}
|
|
||||||
|
|
||||||
type MPArticle struct {
|
|
||||||
Title string
|
Title string
|
||||||
Cover string
|
Cover string
|
||||||
Digest string
|
Digest string
|
||||||
Url string
|
Url string
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type UserDetailItem struct {
|
type UserDetailItem struct {
|
||||||
UserName string
|
UserName string
|
||||||
@ -121,49 +117,9 @@ type WebWxBatchContactResponse struct {
|
|||||||
ContactList []*User
|
ContactList []*User
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckLoginResponse 检查登录状态的响应body
|
type CheckLoginResponse struct {
|
||||||
type CheckLoginResponse []byte
|
Code string
|
||||||
|
Raw []byte
|
||||||
// RedirectURL 重定向的URL
|
|
||||||
func (c CheckLoginResponse) RedirectURL() (*url.URL, error) {
|
|
||||||
code, err := c.Code()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if code != LoginCodeSuccess {
|
|
||||||
return nil, fmt.Errorf("expect status code %s, but got %s", LoginCodeSuccess, code)
|
|
||||||
}
|
|
||||||
results := redirectUriRegexp.FindSubmatch(c)
|
|
||||||
if len(results) != 2 {
|
|
||||||
return nil, errors.New("redirect url does not match")
|
|
||||||
}
|
|
||||||
return url.Parse(string(results[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Code 获取当前的登录检查状态的代码
|
|
||||||
func (c CheckLoginResponse) Code() (LoginCode, error) {
|
|
||||||
results := statusCodeRegexp.FindSubmatch(c)
|
|
||||||
if len(results) != 2 {
|
|
||||||
return "", errors.New("error status code match")
|
|
||||||
}
|
|
||||||
code := string(results[1])
|
|
||||||
return LoginCode(code), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avatar 获取扫码后的用户头像, base64编码
|
|
||||||
func (c CheckLoginResponse) Avatar() (string, error) {
|
|
||||||
code, err := c.Code()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if code != LoginCodeScanned {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
results := avatarRegexp.FindSubmatch(c)
|
|
||||||
if len(results) != 2 {
|
|
||||||
return "", errors.New("avatar does not match")
|
|
||||||
}
|
|
||||||
return string(results[1]), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageResponse struct {
|
type MessageResponse struct {
|
@ -9,7 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Jar is a struct which as same as cookiejar.Jar
|
// Jar is a struct which as same as cookiejar.Jar
|
||||||
// cookiejar.Jar's fields are private, so we can't use it directly
|
|
||||||
type Jar struct {
|
type Jar struct {
|
||||||
PsList cookiejar.PublicSuffixList
|
PsList cookiejar.PublicSuffixList
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ type Jar struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
// Entries is a set of entries, keyed by their eTLD+1 and subkeyed by
|
// Entries is a set of entries, keyed by their eTLD+1 and subkeyed by
|
||||||
// their name/Domain/path.
|
// their name/domain/path.
|
||||||
Entries map[string]map[string]entry
|
Entries map[string]map[string]entry
|
||||||
|
|
||||||
// nextSeqNum is the next sequence number assigned to a new cookie
|
// nextSeqNum is the next sequence number assigned to a new cookie
|
||||||
@ -30,13 +29,13 @@ func (j *Jar) AsCookieJar() http.CookieJar {
|
|||||||
return (*cookiejar.Jar)(unsafe.Pointer(j))
|
return (*cookiejar.Jar)(unsafe.Pointer(j))
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromCookieJar(jar http.CookieJar) *Jar {
|
func newCookieJar() http.CookieJar {
|
||||||
return (*Jar)(unsafe.Pointer(jar.(*cookiejar.Jar)))
|
jar, _ := cookiejar.New(nil)
|
||||||
|
return jar
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJar() *Jar {
|
func fromCookieJar(jar http.CookieJar) *Jar {
|
||||||
jar, _ := cookiejar.New(nil)
|
return (*Jar)(unsafe.Pointer(jar.(*cookiejar.Jar)))
|
||||||
return fromCookieJar(jar)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type entry struct {
|
type entry struct {
|
||||||
@ -58,24 +57,3 @@ type entry struct {
|
|||||||
// equal Creation time. This simplifies testing.
|
// equal Creation time. This simplifies testing.
|
||||||
seqNum uint64
|
seqNum uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// CookieGroup is a group of cookies
|
|
||||||
type CookieGroup []*http.Cookie
|
|
||||||
|
|
||||||
func (c CookieGroup) GetByName(cookieName string) (cookie *http.Cookie, exist bool) {
|
|
||||||
for _, cookie := range c {
|
|
||||||
if cookie.Name == cookieName {
|
|
||||||
return cookie, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWebWxDataTicket(cookies []*http.Cookie) (string, error) {
|
|
||||||
cookieGroup := CookieGroup(cookies)
|
|
||||||
cookie, exist := cookieGroup.GetByName("webwx_data_ticket")
|
|
||||||
if !exist {
|
|
||||||
return "", ErrWebWxDataTicketNotFound
|
|
||||||
}
|
|
||||||
return cookie.Value, nil
|
|
||||||
}
|
|
@ -791,7 +791,7 @@ func (m *Message) IsAt() bool {
|
|||||||
// IsPaiYiPai 判断消息是否为拍一拍
|
// IsPaiYiPai 判断消息是否为拍一拍
|
||||||
// 不要问我为什么取名为PaiYiPai,因为我也不知道取啥名字好
|
// 不要问我为什么取名为PaiYiPai,因为我也不知道取啥名字好
|
||||||
func (m *Message) IsPaiYiPai() bool {
|
func (m *Message) IsPaiYiPai() bool {
|
||||||
return m.IsTickled()
|
return m.IsSystem() && strings.Contains(m.Content, "拍了拍")
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsJoinGroup 判断是否有人加入了群聊
|
// IsJoinGroup 判断是否有人加入了群聊
|
||||||
@ -801,12 +801,7 @@ func (m *Message) IsJoinGroup() bool {
|
|||||||
|
|
||||||
// IsTickled 判断消息是否为拍一拍
|
// IsTickled 判断消息是否为拍一拍
|
||||||
func (m *Message) IsTickled() bool {
|
func (m *Message) IsTickled() bool {
|
||||||
return m.IsSystem() && strings.Contains(m.Content, "拍了拍")
|
return m.IsPaiYiPai()
|
||||||
}
|
|
||||||
|
|
||||||
// IsTickledMe 判断消息是否拍了拍自己
|
|
||||||
func (m *Message) IsTickledMe() bool {
|
|
||||||
return m.IsSystem() && strings.Count(m.Content, "拍了拍我") == 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsVoipInvite 判断消息是否为语音或视频通话邀请
|
// IsVoipInvite 判断消息是否为语音或视频通话邀请
|
||||||
|
@ -11,6 +11,14 @@ type MessageDispatcher interface {
|
|||||||
Dispatch(msg *Message)
|
Dispatch(msg *Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DispatchMessage 跟 MessageDispatcher 结合封装成 MessageHandler
|
||||||
|
// Deprecated: use MessageMatchDispatcher.AsMessageHandler instead
|
||||||
|
func DispatchMessage(dispatcher MessageDispatcher) func(msg *Message) {
|
||||||
|
return func(msg *Message) { dispatcher.Dispatch(msg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageDispatcher impl
|
||||||
|
|
||||||
// MessageContextHandler 消息处理函数
|
// MessageContextHandler 消息处理函数
|
||||||
type MessageContextHandler func(ctx *MessageContext)
|
type MessageContextHandler func(ctx *MessageContext)
|
||||||
|
|
||||||
|
@ -38,6 +38,15 @@ func GetRandomDeviceId() string {
|
|||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getWebWxDataTicket(cookies []*http.Cookie) string {
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if cookie.Name == "webwx_data_ticket" {
|
||||||
|
return cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// GetFileContentType 获取文件上传的类型
|
// GetFileContentType 获取文件上传的类型
|
||||||
func GetFileContentType(file multipart.File) (string, error) {
|
func GetFileContentType(file multipart.File) (string, error) {
|
||||||
data := make([]byte, 512)
|
data := make([]byte, 512)
|
||||||
|
34
relations.go
34
relations.go
@ -9,12 +9,8 @@ import (
|
|||||||
type Friend struct{ *User }
|
type Friend struct{ *User }
|
||||||
|
|
||||||
// implement fmt.Stringer
|
// implement fmt.Stringer
|
||||||
func (f *Friend) String() string {
|
func (f Friend) String() string {
|
||||||
display := f.NickName
|
return fmt.Sprintf("<Friend:%s>", f.NickName)
|
||||||
if f.RemarkName != "" {
|
|
||||||
display = f.RemarkName
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("<Friend:%s>", display)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRemarkName 重命名当前好友
|
// SetRemarkName 重命名当前好友
|
||||||
@ -57,7 +53,7 @@ func (f Friends) Count() int {
|
|||||||
// First 获取第一个好友
|
// First 获取第一个好友
|
||||||
func (f Friends) First() *Friend {
|
func (f Friends) First() *Friend {
|
||||||
if f.Count() > 0 {
|
if f.Count() > 0 {
|
||||||
return f.Sort()[0]
|
return f[0]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -65,7 +61,7 @@ func (f Friends) First() *Friend {
|
|||||||
// Last 获取最后一个好友
|
// Last 获取最后一个好友
|
||||||
func (f Friends) Last() *Friend {
|
func (f Friends) Last() *Friend {
|
||||||
if f.Count() > 0 {
|
if f.Count() > 0 {
|
||||||
return f.Sort()[f.Count()-1]
|
return f[f.Count()-1]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -159,7 +155,7 @@ func (f Friends) SendFile(file io.Reader, delay ...time.Duration) error {
|
|||||||
type Group struct{ *User }
|
type Group struct{ *User }
|
||||||
|
|
||||||
// implement fmt.Stringer
|
// implement fmt.Stringer
|
||||||
func (g *Group) String() string {
|
func (g Group) String() string {
|
||||||
return fmt.Sprintf("<Group:%s>", g.NickName)
|
return fmt.Sprintf("<Group:%s>", g.NickName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +238,7 @@ func (g Groups) Count() int {
|
|||||||
// First 获取第一个群组
|
// First 获取第一个群组
|
||||||
func (g Groups) First() *Group {
|
func (g Groups) First() *Group {
|
||||||
if g.Count() > 0 {
|
if g.Count() > 0 {
|
||||||
return g.Sort()[0]
|
return g[0]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -250,7 +246,7 @@ func (g Groups) First() *Group {
|
|||||||
// Last 获取最后一个群组
|
// Last 获取最后一个群组
|
||||||
func (g Groups) Last() *Group {
|
func (g Groups) Last() *Group {
|
||||||
if g.Count() > 0 {
|
if g.Count() > 0 {
|
||||||
return g.Sort()[g.Count()-1]
|
return g[g.Count()-1]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -304,6 +300,11 @@ func (g Groups) SearchByNickName(limit int, nickName string) (results Groups) {
|
|||||||
return g.Search(limit, func(group *Group) bool { return group.NickName == nickName })
|
return g.Search(limit, func(group *Group) bool { return group.NickName == nickName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchByRemarkName 根据备注查找群组
|
||||||
|
func (g Groups) SearchByRemarkName(limit int, remarkName string) (results Groups) {
|
||||||
|
return g.Search(limit, func(group *Group) bool { return group.RemarkName == remarkName })
|
||||||
|
}
|
||||||
|
|
||||||
// Search 根据自定义条件查找群组
|
// Search 根据自定义条件查找群组
|
||||||
func (g Groups) Search(limit int, searchFuncList ...func(group *Group) bool) (results Groups) {
|
func (g Groups) Search(limit int, searchFuncList ...func(group *Group) bool) (results Groups) {
|
||||||
return g.AsMembers().Search(limit, func(user *User) bool {
|
return g.AsMembers().Search(limit, func(user *User) bool {
|
||||||
@ -339,7 +340,7 @@ func (g Groups) Uniq() Groups {
|
|||||||
// Mp 公众号对象
|
// Mp 公众号对象
|
||||||
type Mp struct{ *User }
|
type Mp struct{ *User }
|
||||||
|
|
||||||
func (m *Mp) String() string {
|
func (m Mp) String() string {
|
||||||
return fmt.Sprintf("<Mp:%s>", m.NickName)
|
return fmt.Sprintf("<Mp:%s>", m.NickName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,7 +355,7 @@ func (m Mps) Count() int {
|
|||||||
// First 获取第一个
|
// First 获取第一个
|
||||||
func (m Mps) First() *Mp {
|
func (m Mps) First() *Mp {
|
||||||
if m.Count() > 0 {
|
if m.Count() > 0 {
|
||||||
return m.Sort()[0]
|
return m[0]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -362,7 +363,7 @@ func (m Mps) First() *Mp {
|
|||||||
// Last 获取最后一个
|
// Last 获取最后一个
|
||||||
func (m Mps) Last() *Mp {
|
func (m Mps) Last() *Mp {
|
||||||
if m.Count() > 0 {
|
if m.Count() > 0 {
|
||||||
return m.Sort()[m.Count()-1]
|
return m[m.Count()-1]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -444,6 +445,11 @@ func (g Groups) GetByUsername(username string) *Group {
|
|||||||
return g.SearchByUserName(1, username).First()
|
return g.SearchByUserName(1, username).First()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByRemarkName 根据remarkName查询一个Group
|
||||||
|
func (g Groups) GetByRemarkName(remarkName string) *Group {
|
||||||
|
return g.SearchByRemarkName(1, remarkName).First()
|
||||||
|
}
|
||||||
|
|
||||||
// GetByNickName 根据nickname查询一个Group
|
// GetByNickName 根据nickname查询一个Group
|
||||||
func (g Groups) GetByNickName(nickname string) *Group {
|
func (g Groups) GetByNickName(nickname string) *Group {
|
||||||
return g.SearchByNickName(1, nickname).First()
|
return g.SearchByNickName(1, nickname).First()
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package openwechat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Serializer is an interface for encoding and decoding data.
|
|
||||||
type Serializer interface {
|
|
||||||
Encode(writer io.Writer, v interface{}) error
|
|
||||||
Decode(reader io.Reader, v interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// JsonSerializer is a serializer for json.
|
|
||||||
type JsonSerializer struct{}
|
|
||||||
|
|
||||||
// Encode encodes v to writer.
|
|
||||||
func (j JsonSerializer) Encode(writer io.Writer, v interface{}) error {
|
|
||||||
return json.NewEncoder(writer).Encode(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode decodes data from reader to v.
|
|
||||||
func (j JsonSerializer) Decode(reader io.Reader, v interface{}) error {
|
|
||||||
return json.NewDecoder(reader).Decode(v)
|
|
||||||
}
|
|
@ -84,8 +84,6 @@ bot.Login()
|
|||||||
// 创建热存储容器对象
|
// 创建热存储容器对象
|
||||||
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
|
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
|
||||||
|
|
||||||
defer reloadStorage.Close()
|
|
||||||
|
|
||||||
// 执行热登录
|
// 执行热登录
|
||||||
bot.HotLogin(reloadStorage)
|
bot.HotLogin(reloadStorage)
|
||||||
```
|
```
|
||||||
@ -97,7 +95,7 @@ bot.HotLogin(reloadStorage)
|
|||||||
我们只需要在`HotLogin`增加一个参数,让它在失败后执行扫码登录即可
|
我们只需要在`HotLogin`增加一个参数,让它在失败后执行扫码登录即可
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bot.HotLogin(reloadStorage, openwechat.NewRetryLoginOption())
|
bot.HotLogin(reloadStorage, openwechat.HotLoginWithRetry(true))
|
||||||
```
|
```
|
||||||
|
|
||||||
当扫码登录成功后,会将会话信息写入到`热存储容器`中,下次再执行热登录的时候就会从`热存储容器`中读取会话信息,直接登录成功。
|
当扫码登录成功后,会将会话信息写入到`热存储容器`中,下次再执行热登录的时候就会从`热存储容器`中读取会话信息,直接登录成功。
|
||||||
@ -121,23 +119,29 @@ type HotReloadStorage io.ReadWriter
|
|||||||
openwechat也提供了这样的功能。
|
openwechat也提供了这样的功能。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bot.PushLogin(storage HotReloadStorage, opts ...openwechat.BotLoginOption) error
|
bot.PushLogin(storage HotReloadStorage, opts ...PushLoginOptionFunc) error
|
||||||
```
|
```
|
||||||
|
|
||||||
`PushLogin`需要传入一个`热存储容器`,和一些可选参数。
|
`PushLogin`需要传入一个`热存储容器`,和一些可选参数。
|
||||||
|
|
||||||
`HotReloadStorage` 跟上面一样,用来保存会话信息,必要参数。
|
`HotReloadStorage` 跟上面一样,用来保存会话信息,必要参数。
|
||||||
|
|
||||||
`openwechat.BotLoginOption`是一个可选参数,用来设置一些额外的行为。
|
`PushLoginOptionFunc`是一个可选参数,用来设置一些额外的行为。
|
||||||
|
|
||||||
目前有下面几个可选参数:
|
目前有下面几个可选参数:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// NewSyncReloadDataLoginOption 登录成功后定时同步热存储容器数据
|
// PushLoginWithoutUUIDCallback 设置 PushLogin 不执行二维码回调, 默认为 true
|
||||||
func NewSyncReloadDataLoginOption(duration time.Duration) BotLoginOption
|
func PushLoginWithoutUUIDCallback(flag bool) PushLoginOptionFunc
|
||||||
|
|
||||||
// NewRetryLoginOption 登录失败后进行扫码登录
|
// PushLoginWithoutScanCallback 设置 PushLogin 不执行扫码回调, 默认为true
|
||||||
func NewRetryLoginOption() BotLoginOption
|
func PushLoginWithoutScanCallback(flag bool) PushLoginOptionFunc
|
||||||
|
|
||||||
|
// PushLoginWithoutLoginCallback 设置 PushLogin 不执行登录回调,默认为false
|
||||||
|
func PushLoginWithoutLoginCallback(flag bool) PushLoginOptionFunc
|
||||||
|
|
||||||
|
// PushLoginWithRetry 设置 PushLogin 失败后执行扫码登录,默认为false
|
||||||
|
func PushLoginWithRetry(flag bool) PushLoginOptionFunc
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:如果是第一次登录,``PushLogin`` 一定会失败的,因为我们的`HotReloadStorage`里面没有会话信息,你需要设置失败会进行扫码登录。
|
注意:如果是第一次登录,``PushLogin`` 一定会失败的,因为我们的`HotReloadStorage`里面没有会话信息,你需要设置失败会进行扫码登录。
|
||||||
@ -145,8 +149,7 @@ func NewRetryLoginOption() BotLoginOption
|
|||||||
```go
|
```go
|
||||||
bot := openwechat.DefaultBot()
|
bot := openwechat.DefaultBot()
|
||||||
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
|
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
|
||||||
defer reloadStorage.Close()
|
err = bot.PushLogin(reloadStorage, openwechat.PushLoginWithRetry(true))
|
||||||
err = bot.PushLogin(reloadStorage, openwechat.NewRetryLoginOption())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
这样当第一次登录失败的时候,会自动执行扫码登录。
|
这样当第一次登录失败的时候,会自动执行扫码登录。
|
||||||
@ -161,21 +164,13 @@ err = bot.PushLogin(reloadStorage, openwechat.NewRetryLoginOption())
|
|||||||
通过对`bot`对象绑定扫码回调即可实现对应的功能。
|
通过对`bot`对象绑定扫码回调即可实现对应的功能。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bot.ScanCallBack = func(body openwechat.CheckLoginResponse) { fmt.Println(string(body)) }
|
bot.ScanCallBack = func(body []byte) { fmt.Println(string(body)) }
|
||||||
```
|
```
|
||||||
|
|
||||||
用户扫码后,body里面会携带用户的头像信息。
|
用户扫码后,body里面会携带用户的头像信息。
|
||||||
|
|
||||||
**注**:绑定扫码回调须在登录前执行。
|
**注**:绑定扫码回调须在登录前执行。
|
||||||
|
|
||||||
`CheckLoginResponse` 是一个`[]byte`包装类型, 扫码成功后可以通过该类型获取用户的头像信息。
|
|
||||||
|
|
||||||
```go
|
|
||||||
type CheckLoginResponse []byte
|
|
||||||
|
|
||||||
func (c CheckLoginResponse) Avatar() (string, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 登录回调
|
### 登录回调
|
||||||
@ -183,13 +178,13 @@ func (c CheckLoginResponse) Avatar() (string, error)
|
|||||||
对`bot`对象绑定登录
|
对`bot`对象绑定登录
|
||||||
|
|
||||||
```go
|
```go
|
||||||
bot.LoginCallBack = func(body openwechat.CheckLoginResponse) {
|
bot.LoginCallBack = func(body []byte) {
|
||||||
fmt.Println(string(body))
|
fmt.Println(string(body))
|
||||||
// to do your business
|
// to do your business
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
登录回调的参数就是当前客户端需要跳转的链接,用户可以不用关心它。(其实可以拿来做一些骚操作😈)
|
登录回调的参数就是当前客户端需要跳转的链接,可以不用关心它。
|
||||||
|
|
||||||
登录回调函数可以当做一个信号处理,表示当前扫码登录的用户已经确认登录。
|
登录回调函数可以当做一个信号处理,表示当前扫码登录的用户已经确认登录。
|
||||||
|
|
||||||
@ -241,7 +236,7 @@ dispatcher.OnText(func(ctx *openwechat.MessageContext){
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 注册消息回调函数
|
// 注册消息回调函数
|
||||||
bot.MessageHandler = dispatcher.AsMessageHandler()
|
bot.MessageHandler = openwechat.DispatchMessage(dispatcher)
|
||||||
```
|
```
|
||||||
|
|
||||||
`openwechat.DispatchMessage`会将消息转发给`dispatcher`对象处理
|
`openwechat.DispatchMessage`会将消息转发给`dispatcher`对象处理
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# 消息
|
# 消息
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 接受消息
|
### 接受消息
|
||||||
|
|
||||||
被动接受的消息对象,由微信服务器发出
|
被动接受的消息对象,由微信服务器发出
|
||||||
@ -16,6 +18,8 @@ msg.ReplyText("pong")
|
|||||||
|
|
||||||
以下简写为`msg`
|
以下简写为`msg`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 消息内容
|
#### 消息内容
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -26,6 +30,8 @@ msg.Content // 获取消息内容
|
|||||||
|
|
||||||
由于消息分为很多种类型,它们都共用`Content`属性。一般当消息类型为文本类型的时候,我们才会去访问`Content`属性。
|
由于消息分为很多种类型,它们都共用`Content`属性。一般当消息类型为文本类型的时候,我们才会去访问`Content`属性。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 消息类型判断
|
#### 消息类型判断
|
||||||
|
|
||||||
下面的判断消息类型的方法均返回`bool`值
|
下面的判断消息类型的方法均返回`bool`值
|
||||||
@ -111,17 +117,13 @@ msg.IsIsPaiYiPai() // 拍一拍消息
|
|||||||
msg.IsTickled()
|
msg.IsTickled()
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 判断是否拍了拍自己
|
|
||||||
```go
|
|
||||||
msg.IsTickledMe()
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 判断是否有新人加入群聊
|
##### 判断是否有新人加入群聊
|
||||||
|
|
||||||
```go
|
```go
|
||||||
msg.IsJoinGroup()
|
msg.IsJoinGroup()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 获取消息的发送者
|
#### 获取消息的发送者
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -130,12 +132,16 @@ sender, err := msg.Sender()
|
|||||||
|
|
||||||
如果是群聊消息,该方法返回的是群聊对象(需要自己将`User`转换为`Group`对象)
|
如果是群聊消息,该方法返回的是群聊对象(需要自己将`User`转换为`Group`对象)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 获取消息的接受者
|
#### 获取消息的接受者
|
||||||
|
|
||||||
```go
|
```go
|
||||||
receiver, err := msg.Receiver()
|
receiver, err := msg.Receiver()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 获取消息在群里面的发送者
|
#### 获取消息在群里面的发送者
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -144,6 +150,8 @@ sender, err := msg.SenderInGroup()
|
|||||||
|
|
||||||
获取群聊中具体发消息的用户,前提该消息必须来自群聊。
|
获取群聊中具体发消息的用户,前提该消息必须来自群聊。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 是否由自己发送
|
#### 是否由自己发送
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -156,24 +164,31 @@ msg.IsSendBySelf()
|
|||||||
msg.IsTickled()
|
msg.IsTickled()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### 消息是否由好友发出
|
#### 消息是否由好友发出
|
||||||
|
|
||||||
```go
|
```go
|
||||||
msg.IsSendByFriend()
|
msg.IsSendByFriend()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 消息是否由群聊发出
|
#### 消息是否由群聊发出
|
||||||
|
|
||||||
```go
|
```go
|
||||||
msg.IsSendByGroup()
|
msg.IsSendByGroup()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 回复文本消息
|
#### 回复文本消息
|
||||||
|
|
||||||
```go
|
```go
|
||||||
msg.ReplyText("hello")
|
msg.ReplyText("hello")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 回复图片消息
|
#### 回复图片消息
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -182,6 +197,8 @@ defer img.Close()
|
|||||||
msg.ReplyImage(img)
|
msg.ReplyImage(img)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 回复文件消息
|
#### 回复文件消息
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -190,6 +207,8 @@ defer file.Close()
|
|||||||
msg.ReplyFile(file)
|
msg.ReplyFile(file)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 获取消息里的其他信息
|
#### 获取消息里的其他信息
|
||||||
|
|
||||||
##### 名片消息
|
##### 名片消息
|
||||||
@ -234,6 +253,8 @@ RegionCode string `xml:"regionCode,attr"`
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### 获取已撤回的消息
|
##### 获取已撤回的消息
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -257,6 +278,8 @@ ReplaceMsg string `xml:"replacemsg"`
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 同意好友请求
|
#### 同意好友请求
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -268,6 +291,8 @@ friend, err := msg.Agree()
|
|||||||
|
|
||||||
该方法调用成功的前提是`msg.IsFriendAdd()`返回为`true`
|
该方法调用成功的前提是`msg.IsFriendAdd()`返回为`true`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 设置为已读
|
#### 设置为已读
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -276,6 +301,10 @@ msg.AsRead()
|
|||||||
|
|
||||||
该当前消息设置为已读
|
该当前消息设置为已读
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 设置消息的上下文
|
#### 设置消息的上下文
|
||||||
|
|
||||||
用于多个消息处理函数之间的通信,并且是协程安全的。
|
用于多个消息处理函数之间的通信,并且是协程安全的。
|
||||||
@ -292,6 +321,10 @@ msg.Set("hello", "world")
|
|||||||
value, exist := msg.Get("hello")
|
value, exist := msg.Get("hello")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 已发送消息
|
### 已发送消息
|
||||||
|
|
||||||
已发送消息指当前用户发送出去的消息
|
已发送消息指当前用户发送出去的消息
|
||||||
@ -306,6 +339,8 @@ sentMsg, err := msg.ReplyText("hello") // 通过回复消息获取
|
|||||||
// and so on
|
// and so on
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 撤回消息
|
#### 撤回消息
|
||||||
|
|
||||||
撤回刚刚发送的消息,撤回消息的有效时间为2分钟,超过了这个时间则无法撤回
|
撤回刚刚发送的消息,撤回消息的有效时间为2分钟,超过了这个时间则无法撤回
|
||||||
@ -314,12 +349,16 @@ sentMsg, err := msg.ReplyText("hello") // 通过回复消息获取
|
|||||||
sentMsg.Revoke()
|
sentMsg.Revoke()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 判断是否可以撤回
|
#### 判断是否可以撤回
|
||||||
|
|
||||||
```go
|
```go
|
||||||
sentMsg.CanRevoke()
|
sentMsg.CanRevoke()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 转发给好友
|
#### 转发给好友
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -328,6 +367,8 @@ sentMsg.ForwardToFriends(friend1, friend2)
|
|||||||
|
|
||||||
将刚发送的消息转发给好友
|
将刚发送的消息转发给好友
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### 转发给群聊
|
#### 转发给群聊
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@ -336,6 +377,10 @@ sentMsg.ForwardToGroups(group1, group2)
|
|||||||
|
|
||||||
将刚发送的消息转发给群聊
|
将刚发送的消息转发给群聊
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Emoji表情
|
### Emoji表情
|
||||||
|
|
||||||
openwechat提供了微信全套`emoji`表情的支持
|
openwechat提供了微信全套`emoji`表情的支持
|
||||||
|
37
stroage.go
37
stroage.go
@ -3,7 +3,6 @@ package openwechat
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,17 +24,14 @@ type HotReloadStorageItem struct {
|
|||||||
// HotReloadStorage 热登陆存储接口
|
// HotReloadStorage 热登陆存储接口
|
||||||
type HotReloadStorage io.ReadWriter
|
type HotReloadStorage io.ReadWriter
|
||||||
|
|
||||||
// fileHotReloadStorage 实现HotReloadStorage接口
|
// jsonFileHotReloadStorage 实现HotReloadStorage接口
|
||||||
// 以文件的形式存储
|
// 默认以json文件的形式存储
|
||||||
type fileHotReloadStorage struct {
|
type jsonFileHotReloadStorage struct {
|
||||||
filename string
|
filename string
|
||||||
file *os.File
|
file *os.File
|
||||||
lock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *fileHotReloadStorage) Read(p []byte) (n int, err error) {
|
func (j *jsonFileHotReloadStorage) Read(p []byte) (n int, err error) {
|
||||||
j.lock.Lock()
|
|
||||||
defer j.lock.Unlock()
|
|
||||||
if j.file == nil {
|
if j.file == nil {
|
||||||
j.file, err = os.OpenFile(j.filename, os.O_RDWR, 0600)
|
j.file, err = os.OpenFile(j.filename, os.O_RDWR, 0600)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@ -48,47 +44,38 @@ func (j *fileHotReloadStorage) Read(p []byte) (n int, err error) {
|
|||||||
return j.file.Read(p)
|
return j.file.Read(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *fileHotReloadStorage) Write(p []byte) (n int, err error) {
|
func (j *jsonFileHotReloadStorage) Write(p []byte) (n int, err error) {
|
||||||
j.lock.Lock()
|
|
||||||
defer j.lock.Unlock()
|
|
||||||
if j.file == nil {
|
if j.file == nil {
|
||||||
j.file, err = os.Create(j.filename)
|
j.file, err = os.Create(j.filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// reset offset and truncate file
|
// 为什么这里要对文件进行Truncate操作呢?
|
||||||
|
// 这是为了方便每次Dump的时候对文件进行重新写入, 而不是追加
|
||||||
|
// json序列化写入只会调用一次Write方法, 所以不要把这个方法当成io.Writer的Write方法
|
||||||
if _, err = j.file.Seek(0, io.SeekStart); err != nil {
|
if _, err = j.file.Seek(0, io.SeekStart); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = j.file.Truncate(0); err != nil {
|
if err = j.file.Truncate(0); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// json decode only write once
|
|
||||||
return j.file.Write(p)
|
return j.file.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *fileHotReloadStorage) Close() error {
|
func (j *jsonFileHotReloadStorage) Close() error {
|
||||||
j.lock.Lock()
|
|
||||||
defer j.lock.Unlock()
|
|
||||||
if j.file == nil {
|
if j.file == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return j.file.Close()
|
return j.file.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: use NewFileHotReloadStorage instead
|
// NewJsonFileHotReloadStorage 创建JsonFileHotReloadStorage
|
||||||
// 不再单纯以json的格式存储,支持了用户自定义序列化方式
|
|
||||||
func NewJsonFileHotReloadStorage(filename string) io.ReadWriteCloser {
|
func NewJsonFileHotReloadStorage(filename string) io.ReadWriteCloser {
|
||||||
return NewFileHotReloadStorage(filename)
|
return &jsonFileHotReloadStorage{filename: filename}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileHotReloadStorage implements HotReloadStorage
|
var _ HotReloadStorage = (*jsonFileHotReloadStorage)(nil)
|
||||||
func NewFileHotReloadStorage(filename string) io.ReadWriteCloser {
|
|
||||||
return &fileHotReloadStorage{filename: filename}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ HotReloadStorage = (*fileHotReloadStorage)(nil)
|
|
||||||
|
|
||||||
type HotReloadStorageSyncer struct {
|
type HotReloadStorageSyncer struct {
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
|
@ -25,11 +25,7 @@ func (s SyncCheckResponse) Success() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s SyncCheckResponse) NorMal() bool {
|
func (s SyncCheckResponse) NorMal() bool {
|
||||||
return s.Success() && s.Selector == SelectorNormal
|
return s.Success() && s.Selector == "0"
|
||||||
}
|
|
||||||
|
|
||||||
func (s SyncCheckResponse) HasNewMessage() bool {
|
|
||||||
return s.Success() && s.Selector == SelectorNewMsg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SyncCheckResponse) Err() error {
|
func (s SyncCheckResponse) Err() error {
|
||||||
|
46
user.go
46
user.go
@ -58,14 +58,14 @@ type User struct {
|
|||||||
// implement fmt.Stringer
|
// implement fmt.Stringer
|
||||||
func (u *User) String() string {
|
func (u *User) String() string {
|
||||||
format := "User"
|
format := "User"
|
||||||
if u.IsSelf() {
|
if u.IsFriend() {
|
||||||
format = "Self"
|
|
||||||
} else if u.IsFriend() {
|
|
||||||
format = "Friend"
|
format = "Friend"
|
||||||
} else if u.IsGroup() {
|
} else if u.IsGroup() {
|
||||||
format = "Group"
|
format = "Group"
|
||||||
} else if u.IsMP() {
|
} else if u.IsMP() {
|
||||||
format = "MP"
|
format = "MP"
|
||||||
|
} else if u.IsSelf() {
|
||||||
|
format = "Self"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("<%s:%s>", format, u.NickName)
|
return fmt.Sprintf("<%s:%s>", format, u.NickName)
|
||||||
}
|
}
|
||||||
@ -263,7 +263,6 @@ func (s *Self) Members(update ...bool) (Members, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.members.Sort()
|
|
||||||
return s.members, nil
|
return s.members, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,18 +287,13 @@ func (s *Self) FileHelper() *Friend {
|
|||||||
}
|
}
|
||||||
return s.fileHelper
|
return s.fileHelper
|
||||||
}
|
}
|
||||||
func (s *Self) ChkFrdGrpMpNil() bool {
|
|
||||||
return s.friends == nil && s.groups == nil && s.mps == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Friends 获取所有的好友
|
// Friends 获取所有的好友
|
||||||
func (s *Self) Friends(update ...bool) (Friends, error) {
|
func (s *Self) Friends(update ...bool) (Friends, error) {
|
||||||
if (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
|
if s.friends == nil || (len(update) > 0 && update[0]) {
|
||||||
if _, err := s.Members(true); err != nil {
|
if _, err := s.Members(true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if s.friends == nil || (len(update) > 0 && update[0]) {
|
|
||||||
s.friends = s.members.Friends()
|
s.friends = s.members.Friends()
|
||||||
}
|
}
|
||||||
return s.friends, nil
|
return s.friends, nil
|
||||||
@ -307,14 +301,10 @@ func (s *Self) Friends(update ...bool) (Friends, error) {
|
|||||||
|
|
||||||
// Groups 获取所有的群组
|
// Groups 获取所有的群组
|
||||||
func (s *Self) Groups(update ...bool) (Groups, error) {
|
func (s *Self) Groups(update ...bool) (Groups, error) {
|
||||||
|
if s.groups == nil || (len(update) > 0 && update[0]) {
|
||||||
if (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
|
|
||||||
if _, err := s.Members(true); err != nil {
|
if _, err := s.Members(true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
if s.groups == nil || (len(update) > 0 && update[0]) {
|
|
||||||
s.groups = s.members.Groups()
|
s.groups = s.members.Groups()
|
||||||
}
|
}
|
||||||
return s.groups, nil
|
return s.groups, nil
|
||||||
@ -322,12 +312,10 @@ func (s *Self) Groups(update ...bool) (Groups, error) {
|
|||||||
|
|
||||||
// Mps 获取所有的公众号
|
// Mps 获取所有的公众号
|
||||||
func (s *Self) Mps(update ...bool) (Mps, error) {
|
func (s *Self) Mps(update ...bool) (Mps, error) {
|
||||||
if (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
|
if s.mps == nil || (len(update) > 0 && update[0]) {
|
||||||
if _, err := s.Members(true); err != nil {
|
if _, err := s.Members(true); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if s.mps == nil || (len(update) > 0 && update[0]) {
|
|
||||||
s.mps = s.members.MPs()
|
s.mps = s.members.MPs()
|
||||||
}
|
}
|
||||||
return s.mps, nil
|
return s.mps, nil
|
||||||
@ -679,16 +667,6 @@ func (s *Self) SendVideoToGroups(video io.Reader, delay time.Duration, groups ..
|
|||||||
return s.sendVideoToMembers(video, delay, members...)
|
return s.sendVideoToMembers(video, delay, members...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContactList 获取最近的联系人列表
|
|
||||||
func (s *Self) ContactList() Members {
|
|
||||||
return s.Bot().Storage.Response.ContactList
|
|
||||||
}
|
|
||||||
|
|
||||||
// MPSubscribeList 获取部分公众号文章列表
|
|
||||||
func (s *Self) MPSubscribeList() []*MPSubscribeMsg {
|
|
||||||
return s.Bot().Storage.Response.MPSubscribeMsgList
|
|
||||||
}
|
|
||||||
|
|
||||||
// Members 抽象的用户组
|
// Members 抽象的用户组
|
||||||
type Members []*User
|
type Members []*User
|
||||||
|
|
||||||
@ -798,8 +776,8 @@ func (m Members) GetByNickName(nickname string) (*User, bool) {
|
|||||||
func (m Members) Friends() Friends {
|
func (m Members) Friends() Friends {
|
||||||
friends := make(Friends, 0)
|
friends := make(Friends, 0)
|
||||||
for _, mb := range m {
|
for _, mb := range m {
|
||||||
friend, ok := mb.AsFriend()
|
if mb.IsFriend() {
|
||||||
if ok {
|
friend := &Friend{mb}
|
||||||
friends = append(friends, friend)
|
friends = append(friends, friend)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -809,8 +787,8 @@ func (m Members) Friends() Friends {
|
|||||||
func (m Members) Groups() Groups {
|
func (m Members) Groups() Groups {
|
||||||
groups := make(Groups, 0)
|
groups := make(Groups, 0)
|
||||||
for _, mb := range m {
|
for _, mb := range m {
|
||||||
group, ok := mb.AsGroup()
|
if mb.IsGroup() {
|
||||||
if ok {
|
group := &Group{mb}
|
||||||
groups = append(groups, group)
|
groups = append(groups, group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -820,8 +798,8 @@ func (m Members) Groups() Groups {
|
|||||||
func (m Members) MPs() Mps {
|
func (m Members) MPs() Mps {
|
||||||
mps := make(Mps, 0)
|
mps := make(Mps, 0)
|
||||||
for _, mb := range m {
|
for _, mb := range m {
|
||||||
mp, ok := mb.AsMP()
|
if mb.IsMP() {
|
||||||
if ok {
|
mp := &Mp{mb}
|
||||||
mps = append(mps, mp)
|
mps = append(mps, mp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user