Compare commits
138 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3e6fc41298 | ||
|
3c7ac0cc75 | ||
|
99af4a2685 | ||
|
0c57ab1ed5 | ||
|
a72c165c59 | ||
|
35a348f0af | ||
|
66c4bebd1f | ||
|
fbfd691cb4 | ||
|
5194ad4965 | ||
|
eccc25e66e | ||
|
d77bb0a4cb | ||
|
e9c89f9ac8 | ||
|
6629e77fd5 | ||
|
76bd0a5648 | ||
|
6be6359e34 | ||
|
7cbb2ae1bb | ||
|
b9bbaed369 | ||
|
2d11a7b95a | ||
|
e38b4258f0 | ||
|
26fc0bb366 | ||
|
71d8221cad | ||
|
1a13e73355 | ||
|
fc38bfb401 | ||
|
30da2df475 | ||
|
63143f9364 | ||
|
53478bad19 | ||
|
1eadbf8be6 | ||
|
0f10aa4508 | ||
|
f8461401a7 | ||
|
0af4b51d93 | ||
|
26abfd6e49 | ||
|
dc3669dcad | ||
|
cc4d650796 | ||
|
71fe8b87ec | ||
|
fb20b70ebf | ||
|
87036e2c94 | ||
|
102af677ff | ||
|
200c5be4ff | ||
|
2681b51702 | ||
|
f49c0367ad | ||
|
93c73833e0 | ||
|
88b6a34307 | ||
|
9b5590f708 | ||
|
da3b267965 | ||
|
721f56314e | ||
|
953c1ce0be | ||
|
b509a11a6d | ||
|
00e99ed293 | ||
|
fc438ab83f | ||
|
17fbbd350e | ||
|
46e6fb1afb | ||
|
e171c1fefa | ||
|
326f200976 | ||
|
5f14a13c62 | ||
|
806085b712 | ||
|
869912fa7a | ||
|
ae02ab70e1 | ||
|
d316f2a533 | ||
|
8c9d36ec16 | ||
|
df01bfefe5 | ||
|
f07652e6c6 | ||
|
e6fcd53bd4 | ||
|
659bf1e522 | ||
|
270e290472 | ||
|
caa0ed64b6 | ||
|
eb3d45a1f3 | ||
|
443b3ea38c | ||
|
144b147606 | ||
|
a7ca046e03 | ||
|
25ba0a61f4 | ||
|
efdac13d91 | ||
|
03aa3c1633 | ||
|
967e85ad9d | ||
|
94743ec582 | ||
|
277f499c56 | ||
|
1223ed8346 | ||
|
5df5a54190 | ||
|
7728a1517d | ||
|
c9e4e842d9 | ||
|
4063e369bd | ||
|
8a201f1461 | ||
|
8ba44602ae | ||
|
c223141ad1 | ||
|
c75e60e717 | ||
|
3d22c239d2 | ||
|
01233fa544 | ||
|
5b35d2014a | ||
|
b18242699a | ||
|
7b4a856cea | ||
|
dd0d6603a7 | ||
|
fe2f73b5a2 | ||
|
bc7d6c752e | ||
|
6709249cd2 | ||
|
854cafcc44 | ||
|
2a8ce91534 | ||
|
68d965c390 | ||
|
9a1a5b0bb9 | ||
|
8fc242b794 | ||
|
21eaf0c137 | ||
|
28652aa6ba | ||
|
b3f579b752 | ||
|
a286c63146 | ||
|
80efb966db | ||
|
1649ee68b8 | ||
|
f32f905b55 | ||
|
3fc3e44d78 | ||
|
3b8019afa2 | ||
|
193ed72366 | ||
|
28e1fded99 | ||
|
bf8bed01d2 | ||
|
1cab9bf5a8 | ||
|
6a0f787925 | ||
|
decdc4a29c | ||
|
91f26a5f64 | ||
|
5a8e0611dd | ||
|
f84c06f81f | ||
|
84e2dc175b | ||
|
4feda379ce | ||
|
e3847f2e74 | ||
|
a4341e24d6 | ||
|
9e388b323f | ||
|
d51ad8cdff | ||
|
6d7541a8ed | ||
|
bb0e909654 | ||
|
fda999473a | ||
|
d2a2ffc119 | ||
|
9b1bc9189b | ||
|
1b926f3ee4 | ||
|
8b7d66a534 | ||
|
879be7aed8 | ||
|
3553d8ea85 | ||
|
6ed620f5dc | ||
|
0aa3dc3361 | ||
|
7411d180f1 | ||
|
b5832e624d | ||
|
6484a80b37 | ||
|
3605ad01b7 | ||
|
e8bf2443a9 |
@ -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)
|
||||
|
||||
> golang版个人微信号API, 突破网页版限制,类似开发公众号一样,开发个人微信号
|
||||
> golang版个人微信号API, 突破登录限制,类似开发公众号一样,开发个人微信号
|
||||
|
||||
|
||||
|
||||
微信机器人:smiling_imp:,利用微信号完成一些功能的定制化开发⭐
|
||||
|
||||
|
||||
|
||||
* 模块简单易用,易于扩展
|
||||
* 支持定制化开发,如日志记录,自动回复
|
||||
* 突破网页版登录限制📣
|
||||
* 突破登录限制📣
|
||||
* 无需重复扫码登录
|
||||
* 支持多个微信号同时登陆
|
||||
|
||||
@ -115,7 +115,7 @@ func main() {
|
||||
|
||||
### 添加微信(EatMoreApple):apple:(备注: openwechat),进群交流:smiling_imp:
|
||||
|
||||
**如果二维码图片没显示出来,请添加微信号 EatMoreApple**
|
||||
**如果二维码图片没显示出来,请添加微信号 eatmoreapple**
|
||||
|
||||
<img width="210px" src="https://raw.githubusercontent.com/eatmoreapple/eatMoreApple/main/img/wechat.jpg" align="left">
|
||||
|
||||
|
@ -4,6 +4,8 @@ type Ret int
|
||||
|
||||
const (
|
||||
ticketError Ret = -14 // ticket error
|
||||
logicError Ret = -2 // logic error
|
||||
sysError Ret = -1 // sys error
|
||||
paramError Ret = 1 // param error
|
||||
failedLoginWarn Ret = 1100 // failed login warn
|
||||
failedLoginCheck Ret = 1101 // failed login check
|
||||
@ -22,6 +24,9 @@ func (b BaseResponse) Ok() bool {
|
||||
return b.Ret == 0
|
||||
}
|
||||
|
||||
func (b BaseResponse) Error() string {
|
||||
return b.Ret.String()
|
||||
func (b BaseResponse) Err() error {
|
||||
if b.Ok() {
|
||||
return nil
|
||||
}
|
||||
return b.Ret
|
||||
}
|
||||
|
341
bot.go
341
bot.go
@ -2,8 +2,8 @@ package openwechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
@ -12,23 +12,26 @@ import (
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
ScanCallBack func(body []byte) // 扫码回调,可获取扫码用户的头像
|
||||
LoginCallBack func(body []byte) // 登陆回调
|
||||
LogoutCallBack func(bot *Bot) // 退出回调
|
||||
UUIDCallback func(uuid string) // 获取UUID的回调函数
|
||||
SyncCheckCallback func(resp SyncCheckResponse) // 心跳回调
|
||||
MessageHandler MessageHandler // 获取消息成功的handle
|
||||
MessageErrorHandler func(err error) bool // 获取消息发生错误的handle, 返回true则尝试继续监听
|
||||
isHot bool // 是否为热登录模式
|
||||
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
|
||||
Caller *Caller
|
||||
self *Self
|
||||
Storage *Storage
|
||||
HotReloadStorage HotReloadStorage
|
||||
hotReloadStorage HotReloadStorage
|
||||
uuid string
|
||||
loginUUID *string
|
||||
deviceId string // 设备Id
|
||||
loginOptionGroup BotOptionGroup
|
||||
}
|
||||
|
||||
// Alive 判断当前用户是否正常在线
|
||||
@ -44,12 +47,22 @@ func (b *Bot) Alive() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
//
|
||||
// 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")
|
||||
@ -57,137 +70,87 @@ func (b *Bot) GetCurrentUser() (*Self, error) {
|
||||
return b.self, nil
|
||||
}
|
||||
|
||||
// HotLogin 热登录,可实现重复登录,
|
||||
// retry设置为true可在热登录失效后进行普通登录行为
|
||||
// Storage := NewJsonFileHotReloadStorage("Storage.json")
|
||||
// err := bot.HotLogin(Storage, true)
|
||||
// fmt.Println(err)
|
||||
func (b *Bot) HotLogin(storage HotReloadStorage, retry ...bool) error {
|
||||
b.isHot = true
|
||||
b.HotReloadStorage = storage
|
||||
|
||||
var err error
|
||||
|
||||
// 如果load出错了,就执行正常登陆逻辑
|
||||
// 第一次没有数据load都会出错的
|
||||
item, err := NewHotReloadStorageItem(storage)
|
||||
|
||||
if err != nil {
|
||||
return b.Login()
|
||||
// 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 = b.hotLoginInit(*item); err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果webInit出错,则说明可能身份信息已经失效
|
||||
// 如果retry为True的话,则进行正常登陆
|
||||
if err = b.WebInit(); err != nil && (len(retry) > 0 && retry[0]) {
|
||||
err = b.Login()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 热登陆初始化
|
||||
func (b *Bot) hotLoginInit(item HotReloadStorageItem) error {
|
||||
cookies := item.Cookies
|
||||
for u, ck := range cookies {
|
||||
path, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Caller.Client.Jar.SetCookies(path, ck)
|
||||
}
|
||||
b.Storage.LoginInfo = item.LoginInfo
|
||||
b.Storage.Request = item.BaseRequest
|
||||
b.Caller.Client.Domain = item.WechatDomain
|
||||
b.uuid = item.UUID
|
||||
return nil
|
||||
return opt.OnSuccess(b)
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (b *Bot) Login() error {
|
||||
uuid, err := b.Caller.GetLoginUUID()
|
||||
b.uuid = uuid
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.LoginWithUUID(uuid)
|
||||
scanLogin := &SacnLogin{UUID: b.loginUUID}
|
||||
return b.login(scanLogin)
|
||||
}
|
||||
|
||||
// LoginWithUUID 用户登录
|
||||
// 该方法会一直阻塞,直到用户扫码登录,或者二维码过期
|
||||
func (b *Bot) LoginWithUUID(uuid string) error {
|
||||
b.uuid = uuid
|
||||
// 二维码获取回调
|
||||
if b.UUIDCallback != nil {
|
||||
b.UUIDCallback(uuid)
|
||||
}
|
||||
for {
|
||||
// 长轮询检查是否扫码登录
|
||||
resp, err := b.Caller.CheckLogin(uuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch resp.Code {
|
||||
case StatusSuccess:
|
||||
// 判断是否有登录回调,如果有执行它
|
||||
if b.LoginCallBack != nil {
|
||||
b.LoginCallBack(resp.Raw)
|
||||
}
|
||||
return b.HandleLogin(resp.Raw)
|
||||
case StatusScanned:
|
||||
// 执行扫码回调
|
||||
if b.ScanCallBack != nil {
|
||||
b.ScanCallBack(resp.Raw)
|
||||
}
|
||||
case StatusTimeout:
|
||||
return ErrLoginTimeout
|
||||
case StatusWait:
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 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() {
|
||||
if b.LogoutCallBack != nil {
|
||||
b.LogoutCallBack(b)
|
||||
}
|
||||
info := b.Storage.LoginInfo
|
||||
if err := b.Caller.Logout(info); err != nil {
|
||||
return err
|
||||
}
|
||||
b.stopSyncCheck(errors.New("logout"))
|
||||
b.Exit()
|
||||
return nil
|
||||
}
|
||||
return errors.New("user not login")
|
||||
}
|
||||
|
||||
// HandleLogin 登录逻辑
|
||||
func (b *Bot) HandleLogin(data []byte) error {
|
||||
func (b *Bot) HandleLogin(path *url.URL) error {
|
||||
// 获取登录的一些基本的信息
|
||||
info, err := b.Caller.GetLoginInfo(data)
|
||||
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: GetRandomDeviceId(),
|
||||
DeviceID: b.deviceId,
|
||||
}
|
||||
|
||||
// 将BaseRequest存到storage里面方便后续调用
|
||||
b.Storage.Request = request
|
||||
|
||||
// 如果是热登陆,则将当前的重要信息写入hotReloadStorage
|
||||
if b.isHot {
|
||||
if b.hotReloadStorage != nil {
|
||||
if err = b.DumpHotReloadStorage(); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -206,9 +169,10 @@ func (b *Bot) WebInit() error {
|
||||
return err
|
||||
}
|
||||
// 设置当前的用户
|
||||
b.self = &Self{Bot: b, User: &resp.User}
|
||||
b.self = &Self{bot: b, User: resp.User}
|
||||
b.self.formatEmoji()
|
||||
b.self.Self = b.self
|
||||
b.self.self = b.self
|
||||
resp.ContactList.init(b.self)
|
||||
b.Storage.Response = resp
|
||||
|
||||
// 通知手机客户端已经登录
|
||||
@ -220,7 +184,7 @@ func (b *Bot) WebInit() error {
|
||||
// FIX: 当bot在线的情况下执行热登录,会开启多次事件监听
|
||||
go b.once.Do(func() {
|
||||
if b.MessageErrorHandler == nil {
|
||||
b.MessageErrorHandler = b.stopSyncCheck
|
||||
b.MessageErrorHandler = defaultSyncCheckErrHandler(b)
|
||||
}
|
||||
for {
|
||||
err := b.syncCheck()
|
||||
@ -229,6 +193,8 @@ func (b *Bot) WebInit() error {
|
||||
}
|
||||
// 判断是否继续, 如果不继续则退出
|
||||
if goon := b.MessageErrorHandler(err); !goon {
|
||||
b.err = err
|
||||
b.Exit()
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -255,10 +221,10 @@ func (b *Bot) syncCheck() error {
|
||||
}
|
||||
// 如果不是正常的状态码返回,发生了错误,直接退出
|
||||
if !resp.Success() {
|
||||
return resp
|
||||
return resp.Err()
|
||||
}
|
||||
// 如果Selector不为0,则获取消息
|
||||
if !resp.NorMal() {
|
||||
switch resp.Selector {
|
||||
case SelectorNewMsg:
|
||||
messages, err := b.syncMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -271,26 +237,17 @@ func (b *Bot) syncCheck() error {
|
||||
// 默认同步调用
|
||||
// 如果异步调用则需自行处理
|
||||
// 如配合 openwechat.MessageMatchDispatcher 使用
|
||||
// NOTE: 请确保 MessageHandler 不会阻塞,否则会导致收不到后续的消息
|
||||
b.MessageHandler(message)
|
||||
}
|
||||
case SelectorModContact:
|
||||
case SelectorAddOrDelContact:
|
||||
case SelectorModChatRoom:
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 当获取消息发生错误时, 默认的错误处理行为
|
||||
func (b *Bot) stopSyncCheck(err error) bool {
|
||||
if IsNetworkError(err) {
|
||||
log.Println(err)
|
||||
// 继续监听
|
||||
return true
|
||||
}
|
||||
b.err = err
|
||||
b.Exit()
|
||||
log.Printf("exit with : %s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取新的消息
|
||||
func (b *Bot) syncMessage() ([]*Message, error) {
|
||||
resp, err := b.Caller.WebWxSync(b.Storage.Request, b.Storage.Response, b.Storage.LoginInfo)
|
||||
@ -307,12 +264,15 @@ func (b *Bot) Block() error {
|
||||
if b.self == nil {
|
||||
return errors.New("`Block` must be called after user login")
|
||||
}
|
||||
<-b.context.Done()
|
||||
<-b.Context().Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exit 主动退出,让 Block 不在阻塞
|
||||
func (b *Bot) Exit() {
|
||||
if b.LogoutCallBack != nil {
|
||||
b.LogoutCallBack(b)
|
||||
}
|
||||
b.self = nil
|
||||
b.cancel()
|
||||
}
|
||||
@ -322,73 +282,98 @@ func (b *Bot) CrashReason() error {
|
||||
return b.err
|
||||
}
|
||||
|
||||
// MessageOnSuccess setter for Bot.MessageHandler
|
||||
func (b *Bot) MessageOnSuccess(h func(msg *Message)) {
|
||||
b.MessageHandler = h
|
||||
}
|
||||
|
||||
// MessageOnError setter for Bot.GetMessageErrorHandler
|
||||
func (b *Bot) MessageOnError(h func(err error) bool) {
|
||||
b.MessageErrorHandler = h
|
||||
}
|
||||
|
||||
// DumpHotReloadStorage 写入HotReloadStorage
|
||||
func (b *Bot) DumpHotReloadStorage() error {
|
||||
if b.HotReloadStorage == nil {
|
||||
if b.hotReloadStorage == nil {
|
||||
return errors.New("HotReloadStorage can not be nil")
|
||||
}
|
||||
cookies := b.Caller.Client.GetCookieMap()
|
||||
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,
|
||||
Cookies: cookies,
|
||||
Jar: fromCookieJar(jar),
|
||||
LoginInfo: b.Storage.LoginInfo,
|
||||
WechatDomain: b.Caller.Client.Domain,
|
||||
UUID: b.uuid,
|
||||
}
|
||||
|
||||
return json.NewEncoder(b.HotReloadStorage).Encode(item)
|
||||
return b.Serializer.Encode(writer, item)
|
||||
}
|
||||
|
||||
// OnLogin is a setter for LoginCallBack
|
||||
func (b *Bot) OnLogin(f func(body []byte)) {
|
||||
b.LoginCallBack = f
|
||||
// IsHot returns true if is hot login otherwise false
|
||||
func (b *Bot) IsHot() bool {
|
||||
return b.hotReloadStorage != nil
|
||||
}
|
||||
|
||||
// OnScanned is a setter for ScanCallBack
|
||||
func (b *Bot) OnScanned(f func(body []byte)) {
|
||||
b.ScanCallBack = f
|
||||
// UUID returns current UUID of bot
|
||||
func (b *Bot) UUID() string {
|
||||
return b.uuid
|
||||
}
|
||||
|
||||
// OnLogout is a setter for LogoutCallBack
|
||||
func (b *Bot) OnLogout(f func(bot *Bot)) {
|
||||
b.LogoutCallBack = f
|
||||
// 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的构造方法
|
||||
func NewBot() *Bot {
|
||||
// 接收外部的 context.Context,用于控制Bot的存活
|
||||
func NewBot(c context.Context) *Bot {
|
||||
caller := DefaultCaller()
|
||||
// 默认行为为桌面模式
|
||||
caller.Client.SetMode(Normal)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Bot{Caller: caller, Storage: &Storage{}, context: ctx, cancel: cancel}
|
||||
// 默认行为为网页版微信模式
|
||||
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.Desktop,详情见mode
|
||||
// bot := openwechat.DefaultBot(openwechat.Desktop)
|
||||
func DefaultBot(modes ...Mode) *Bot {
|
||||
bot := NewBot()
|
||||
if len(modes) > 0 {
|
||||
bot.Caller.Client.SetMode(modes[0])
|
||||
}
|
||||
// mode不传入默认为 openwechat.Normal,详情见mode
|
||||
//
|
||||
// bot := openwechat.DefaultBot(openwechat.Desktop)
|
||||
func DefaultBot(prepares ...BotPreparer) *Bot {
|
||||
bot := NewBot(context.Background())
|
||||
// 获取二维码回调
|
||||
bot.UUIDCallback = PrintlnQrcodeUrl
|
||||
// 扫码回调
|
||||
bot.ScanCallBack = func(body []byte) {
|
||||
bot.ScanCallBack = func(_ CheckLoginResponse) {
|
||||
log.Println("扫码成功,请在手机上确认登录")
|
||||
}
|
||||
// 登录回调
|
||||
bot.LoginCallBack = func(body []byte) {
|
||||
bot.LoginCallBack = func(_ CheckLoginResponse) {
|
||||
log.Println("登录成功")
|
||||
}
|
||||
// 心跳回调函数
|
||||
@ -396,9 +381,27 @@ func DefaultBot(modes ...Mode) *Bot {
|
||||
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
|
||||
@ -433,13 +436,3 @@ func open(url string) error {
|
||||
args = append(args, url)
|
||||
return exec.Command(cmd, args...).Start()
|
||||
}
|
||||
|
||||
// IsHot returns true if is hot login otherwise false
|
||||
func (b *Bot) IsHot() bool {
|
||||
return b.isHot
|
||||
}
|
||||
|
||||
// UUID returns current uuid of bot
|
||||
func (b *Bot) UUID() string {
|
||||
return b.uuid
|
||||
}
|
||||
|
363
bot_login.go
Normal file
363
bot_login.go
Normal file
@ -0,0 +1,363 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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 {
|
||||
Prepare(*Bot)
|
||||
}
|
||||
|
||||
type BotLoginOption interface {
|
||||
BotPreparer
|
||||
OnError(*Bot, error) error
|
||||
OnSuccess(*Bot) error
|
||||
}
|
||||
|
||||
// BotOptionGroup 是一个 BotLoginOption 的集合
|
||||
// 用于将多个 BotLoginOption 组合成一个 BotLoginOption
|
||||
type BotOptionGroup []BotLoginOption
|
||||
|
||||
// Prepare 实现了 BotLoginOption 接口
|
||||
func (g BotOptionGroup) Prepare(bot *Bot) {
|
||||
for _, option := range g {
|
||||
option.Prepare(bot)
|
||||
}
|
||||
}
|
||||
|
||||
// OnError 实现了 BotLoginOption 接口
|
||||
func (g BotOptionGroup) OnError(b *Bot, err error) error {
|
||||
// 当有一个 BotLoginOption 的 OnError 返回的 error 等于 nil 时,就会停止执行后续的 BotLoginOption
|
||||
for _, option := range g {
|
||||
currentErr := option.OnError(b, err)
|
||||
if currentErr == nil {
|
||||
return nil
|
||||
}
|
||||
if currentErr != err {
|
||||
return currentErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// OnSuccess 实现了 BotLoginOption 接口
|
||||
func (g BotOptionGroup) OnSuccess(b *Bot) error {
|
||||
for _, option := range g {
|
||||
if err := option.OnSuccess(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BaseBotLoginOption struct{}
|
||||
|
||||
func (BaseBotLoginOption) Prepare(_ *Bot) {}
|
||||
|
||||
func (BaseBotLoginOption) OnError(_ *Bot, err error) error { return err }
|
||||
|
||||
func (BaseBotLoginOption) OnSuccess(_ *Bot) error { return nil }
|
||||
|
||||
// DoNothingBotLoginOption 是一个空的 BotLoginOption,表示不做任何操作
|
||||
var DoNothingBotLoginOption = &BaseBotLoginOption{}
|
||||
|
||||
// RetryLoginOption 在登录失败后进行扫码登录
|
||||
type RetryLoginOption struct {
|
||||
BaseBotLoginOption
|
||||
MaxRetryCount int
|
||||
currentRetryTime int
|
||||
}
|
||||
|
||||
// OnError 实现了 BotLoginOption 接口
|
||||
// 当登录失败后,会调用此方法进行扫码登录
|
||||
func (r *RetryLoginOption) OnError(bot *Bot, err error) error {
|
||||
if r.currentRetryTime >= r.MaxRetryCount {
|
||||
return err
|
||||
}
|
||||
r.currentRetryTime++
|
||||
return bot.Login()
|
||||
}
|
||||
|
||||
func NewRetryLoginOption() BotLoginOption {
|
||||
return &RetryLoginOption{MaxRetryCount: 1}
|
||||
}
|
||||
|
||||
// SyncReloadDataLoginOption 在登录成功后进行数据定时同步到指定的storage中
|
||||
type SyncReloadDataLoginOption struct {
|
||||
BaseBotLoginOption
|
||||
SyncLoopDuration time.Duration
|
||||
}
|
||||
|
||||
// OnSuccess 实现了 BotLoginOption 接口
|
||||
// 当登录成功后,会调用此方法进行数据定时同步到指定的storage中
|
||||
func (s SyncReloadDataLoginOption) OnSuccess(bot *Bot) error {
|
||||
if s.SyncLoopDuration <= 0 {
|
||||
return nil
|
||||
}
|
||||
syncer := NewHotReloadStorageSyncer(bot, s.SyncLoopDuration)
|
||||
go func() { _ = syncer.Sync() }()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSyncReloadDataLoginOption(duration time.Duration) BotLoginOption {
|
||||
return &SyncReloadDataLoginOption{SyncLoopDuration: duration}
|
||||
}
|
||||
|
||||
// withModeOption 指定使用哪种客户端模式
|
||||
type withModeOption struct {
|
||||
mode Mode
|
||||
}
|
||||
|
||||
// Prepare 实现了 BotLoginOption 接口
|
||||
func (w withModeOption) Prepare(b *Bot) { b.Caller.Client.SetMode(w.mode) }
|
||||
|
||||
func withMode(mode Mode) BotPreparer {
|
||||
return withModeOption{mode: mode}
|
||||
}
|
||||
|
||||
// btw, 这两个变量已经变了4回了, 但是为了兼容以前的代码, 还是得想着法儿让用户无感知的更新
|
||||
var (
|
||||
// Normal 网页版微信模式
|
||||
Normal = withMode(normal)
|
||||
|
||||
// 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 (
|
||||
defaultHotStorageSyncDuration = time.Minute * 5
|
||||
)
|
||||
|
||||
// BotLogin 定义了一个Login的接口
|
||||
type BotLogin interface {
|
||||
Login(bot *Bot) error
|
||||
}
|
||||
|
||||
// SacnLogin 扫码登录
|
||||
type SacnLogin struct {
|
||||
UUID *string
|
||||
}
|
||||
|
||||
// Login 实现了 BotLogin 接口
|
||||
func (s *SacnLogin) Login(bot *Bot) error {
|
||||
var uuid string
|
||||
if s.UUID == nil {
|
||||
var err error
|
||||
uuid, err = bot.Caller.GetLoginUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
uuid = *s.UUID
|
||||
}
|
||||
return s.checkLogin(bot, uuid)
|
||||
}
|
||||
|
||||
// checkLogin 该方法会一直阻塞,直到用户扫码登录,或者二维码过期
|
||||
func (s *SacnLogin) checkLogin(bot *Bot, uuid string) error {
|
||||
bot.uuid = uuid
|
||||
loginChecker := &LoginChecker{
|
||||
Bot: bot,
|
||||
Tip: "0",
|
||||
UUIDCallback: bot.UUIDCallback,
|
||||
LoginCallBack: bot.LoginCallBack,
|
||||
ScanCallBack: bot.ScanCallBack,
|
||||
}
|
||||
return loginChecker.CheckLogin()
|
||||
}
|
||||
|
||||
var (
|
||||
hotLoginDefaultOptions = [...]BotLoginOption{
|
||||
NewSyncReloadDataLoginOption(defaultHotStorageSyncDuration),
|
||||
}
|
||||
pushLoginDefaultOptions = hotLoginDefaultOptions
|
||||
)
|
||||
|
||||
// HotLogin 热登录模式
|
||||
type HotLogin struct {
|
||||
storage HotReloadStorage
|
||||
}
|
||||
|
||||
// Login 实现了 BotLogin 接口
|
||||
func (h *HotLogin) Login(bot *Bot) error {
|
||||
if err := h.hotLoginInit(bot); err != nil {
|
||||
return err
|
||||
}
|
||||
return bot.WebInit()
|
||||
}
|
||||
|
||||
func (h *HotLogin) hotLoginInit(bot *Bot) error {
|
||||
bot.hotReloadStorage = h.storage
|
||||
return bot.reload()
|
||||
}
|
||||
|
||||
// PushLogin 免扫码登录模式
|
||||
type PushLogin struct {
|
||||
storage HotReloadStorage
|
||||
}
|
||||
|
||||
// Login 实现了 BotLogin 接口
|
||||
func (p *PushLogin) Login(bot *Bot) error {
|
||||
if err := p.pushLoginInit(bot); err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := bot.Caller.WebWxPushLogin(bot.Storage.LoginInfo.WxUin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = resp.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.checkLogin(bot, resp.UUID)
|
||||
}
|
||||
|
||||
func (p *PushLogin) pushLoginInit(bot *Bot) error {
|
||||
bot.hotReloadStorage = p.storage
|
||||
return bot.reload()
|
||||
}
|
||||
|
||||
// checkLogin 登录检查
|
||||
func (p *PushLogin) checkLogin(bot *Bot, uuid string) error {
|
||||
bot.uuid = uuid
|
||||
// 为什么把 UUIDCallback 和 ScanCallBack 置为nil呢?
|
||||
// 因为这两个对用户是无感知的。
|
||||
loginChecker := &LoginChecker{
|
||||
Bot: bot,
|
||||
Tip: "1",
|
||||
LoginCallBack: bot.LoginCallBack,
|
||||
}
|
||||
return loginChecker.CheckLogin()
|
||||
}
|
||||
|
||||
type LoginChecker struct {
|
||||
Bot *Bot
|
||||
Tip string
|
||||
UUIDCallback func(uuid string)
|
||||
LoginCallBack func(body CheckLoginResponse)
|
||||
ScanCallBack func(body CheckLoginResponse)
|
||||
}
|
||||
|
||||
func (l *LoginChecker) CheckLogin() error {
|
||||
uuid := l.Bot.UUID()
|
||||
// 二维码获取回调
|
||||
if cb := l.UUIDCallback; cb != nil {
|
||||
cb(uuid)
|
||||
}
|
||||
var tip = l.Tip
|
||||
for {
|
||||
// 长轮询检查是否扫码登录
|
||||
resp, err := l.Bot.Caller.CheckLogin(uuid, tip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
code, err := resp.Code()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tip == "1" {
|
||||
tip = "0"
|
||||
}
|
||||
switch code {
|
||||
case LoginCodeSuccess:
|
||||
// 判断是否有登录回调,如果有执行它
|
||||
redirectURL, err := resp.RedirectURL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = l.Bot.HandleLogin(redirectURL); err != nil {
|
||||
return err
|
||||
}
|
||||
if cb := l.LoginCallBack; cb != nil {
|
||||
cb(resp)
|
||||
}
|
||||
return nil
|
||||
case LoginCodeScanned:
|
||||
// 执行扫码回调
|
||||
if cb := l.ScanCallBack; cb != nil {
|
||||
cb(resp)
|
||||
}
|
||||
case LoginCodeTimeout:
|
||||
return ErrLoginTimeout
|
||||
case LoginCodeWait:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// # 下面都是即将废弃的函数。
|
||||
// # 为了兼容老版本暂时留了下来, 但是它的函数签名已经发生了改变。
|
||||
// # 如果你是使用的是openwechat提供的api来调用这些函数,那么你是感知不到变动的。
|
||||
// # openwechat内部对这些函数的调用做了兼容处理, 如果你的代码中调用了这些函数, 请尽快修改。
|
||||
|
||||
// Deprecated: 请使用 NewRetryLoginOption 代替
|
||||
// HotLoginWithRetry 热登录模式,如果登录失败会重试
|
||||
func HotLoginWithRetry(flag bool) BotLoginOption {
|
||||
if flag {
|
||||
return NewRetryLoginOption()
|
||||
}
|
||||
return DoNothingBotLoginOption
|
||||
}
|
||||
|
||||
// Deprecated: 请使用 NewRetryLoginOption 代替
|
||||
// HotLoginWithSyncReloadData 定时同步热登录数据
|
||||
func HotLoginWithSyncReloadData(duration time.Duration) BotLoginOption {
|
||||
return NewSyncReloadDataLoginOption(duration)
|
||||
}
|
||||
|
||||
// Deprecated: 请使用 NewRetryLoginOption 代替
|
||||
// PushLoginWithRetry 免扫码登录模式,如果登录失败会重试
|
||||
func PushLoginWithRetry(flag bool) BotLoginOption {
|
||||
if !flag {
|
||||
return DoNothingBotLoginOption
|
||||
}
|
||||
return NewRetryLoginOption()
|
||||
}
|
||||
|
||||
// Deprecated: 请使用 NewSyncReloadDataLoginOption 代替
|
||||
// PushLoginWithSyncReloadData 定时同步热登录数据
|
||||
func PushLoginWithSyncReloadData(duration time.Duration) BotLoginOption {
|
||||
return NewSyncReloadDataLoginOption(duration)
|
||||
}
|
26
bot_test.go
26
bot_test.go
@ -128,4 +128,30 @@ func TestSender(t *testing.T) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
297
caller.go
297
caller.go
@ -4,6 +4,8 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -13,7 +15,6 @@ import (
|
||||
// 上层模块可以直接获取封装后的请求结果
|
||||
type Caller struct {
|
||||
Client *Client
|
||||
path *url.URL
|
||||
}
|
||||
|
||||
// NewCaller Constructor for Caller
|
||||
@ -33,7 +34,7 @@ func (c *Caller) GetLoginUUID() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
||||
@ -49,52 +50,40 @@ func (c *Caller) GetLoginUUID() (string, error) {
|
||||
}
|
||||
|
||||
// CheckLogin 检查是否登录成功
|
||||
func (c *Caller) CheckLogin(uuid string) (*CheckLoginResponse, error) {
|
||||
resp, err := c.Client.CheckLogin(uuid)
|
||||
func (c *Caller) CheckLogin(uuid, tip string) (CheckLoginResponse, error) {
|
||||
resp, err := c.Client.CheckLogin(uuid, tip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 正则匹配检测的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
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetLoginInfo 获取登录信息
|
||||
func (c *Caller) GetLoginInfo(body []byte) (*LoginInfo, error) {
|
||||
func (c *Caller) GetLoginInfo(path *url.URL) (*LoginInfo, error) {
|
||||
// 从响应体里面获取需要跳转的url
|
||||
results := redirectUriRegexp.FindSubmatch(body)
|
||||
if len(results) != 2 {
|
||||
return nil, errors.New("redirect url does not match")
|
||||
}
|
||||
path, err := url.Parse(string(results[1]))
|
||||
resp, err := c.Client.GetLoginInfo(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Client.Domain = WechatDomain(path.Host)
|
||||
resp, err := c.Client.GetLoginInfo(path.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 判断是否重定向
|
||||
if resp.StatusCode != http.StatusMovedPermanently {
|
||||
return nil, fmt.Errorf("%w: try to login with Desktop Mode", ErrForbidden)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
var loginInfo LoginInfo
|
||||
// xml结构体序列化储存
|
||||
if err := scanXml(resp, &loginInfo); err != nil {
|
||||
if err := scanXml(resp.Body, &loginInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !loginInfo.Ok() {
|
||||
return nil, loginInfo
|
||||
return nil, loginInfo.Err()
|
||||
}
|
||||
return &loginInfo, nil
|
||||
}
|
||||
@ -106,10 +95,13 @@ func (c *Caller) WebInit(request *BaseRequest) (*WebInitResponse, error) {
|
||||
return nil, err
|
||||
}
|
||||
var webInitResponse WebInitResponse
|
||||
defer resp.Body.Close()
|
||||
if err := scanJson(resp, &webInitResponse); err != nil {
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if err := scanJson(resp.Body, &webInitResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !webInitResponse.BaseResponse.Ok() {
|
||||
return nil, webInitResponse.BaseResponse.Err()
|
||||
}
|
||||
return &webInitResponse, nil
|
||||
}
|
||||
|
||||
@ -119,15 +111,9 @@ func (c *Caller) WebWxStatusNotify(request *BaseRequest, response *WebInitRespon
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var item struct{ BaseResponse BaseResponse }
|
||||
defer resp.Body.Close()
|
||||
if err := scanJson(resp, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
if !item.BaseResponse.Ok() {
|
||||
return item.BaseResponse
|
||||
}
|
||||
return nil
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// SyncCheck 异步获取是否有新的消息
|
||||
@ -136,7 +122,7 @@ func (c *Caller) SyncCheck(request *BaseRequest, info *LoginInfo, response *WebI
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
var buffer bytes.Buffer
|
||||
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
||||
return nil, err
|
||||
@ -145,26 +131,39 @@ func (c *Caller) SyncCheck(request *BaseRequest, info *LoginInfo, response *WebI
|
||||
if len(results) != 3 {
|
||||
return nil, errors.New("parse sync key failed")
|
||||
}
|
||||
retCode, selector := string(results[1]), string(results[2])
|
||||
retCode, selector := string(results[1]), Selector(results[2])
|
||||
syncCheckResponse := &SyncCheckResponse{RetCode: retCode, Selector: selector}
|
||||
return syncCheckResponse, nil
|
||||
}
|
||||
|
||||
// WebWxGetContact 获取所有的联系人
|
||||
func (c *Caller) WebWxGetContact(info *LoginInfo) (Members, error) {
|
||||
resp, err := c.Client.WebWxGetContact(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var members Members
|
||||
var reqs int64
|
||||
for {
|
||||
resp, err := c.Client.WebWxGetContact(info, reqs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var item WebWxContactResponse
|
||||
if err = scanJson(resp.Body, &item); err != nil {
|
||||
_ = resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !item.BaseResponse.Ok() {
|
||||
return nil, item.BaseResponse.Err()
|
||||
}
|
||||
members = append(members, item.MemberList...)
|
||||
|
||||
if item.Seq == 0 || item.Seq == reqs {
|
||||
break
|
||||
}
|
||||
reqs = item.Seq
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var item WebWxContactResponse
|
||||
if err := scanJson(resp, &item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !item.BaseResponse.Ok() {
|
||||
return nil, item.BaseResponse
|
||||
}
|
||||
return item.MemberList, nil
|
||||
return members, nil
|
||||
}
|
||||
|
||||
// WebWxBatchGetContact 获取联系人的详情
|
||||
@ -174,13 +173,13 @@ func (c *Caller) WebWxBatchGetContact(members Members, request *BaseRequest) (Me
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
var item WebWxBatchContactResponse
|
||||
if err := scanJson(resp, &item); err != nil {
|
||||
if err := scanJson(resp.Body, &item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !item.BaseResponse.Ok() {
|
||||
return nil, item.BaseResponse
|
||||
return nil, item.BaseResponse.Err()
|
||||
}
|
||||
return item.ContactList, nil
|
||||
}
|
||||
@ -191,9 +190,9 @@ func (c *Caller) WebWxSync(request *BaseRequest, response *WebInitResponse, info
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
var webWxSyncResponse WebWxSyncResponse
|
||||
if err := scanJson(resp, &webWxSyncResponse); err != nil {
|
||||
if err := scanJson(resp.Body, &webWxSyncResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &webWxSyncResponse, nil
|
||||
@ -202,7 +201,12 @@ func (c *Caller) WebWxSync(request *BaseRequest, response *WebInitResponse, info
|
||||
// WebWxSendMsg 发送消息接口
|
||||
func (c *Caller) WebWxSendMsg(msg *SendMessage, info *LoginInfo, request *BaseRequest) (*SentMessage, error) {
|
||||
resp, err := c.Client.WebWxSendMsg(msg, info, request)
|
||||
return getSuccessSentMessage(msg, resp, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.SentMessage(msg)
|
||||
}
|
||||
|
||||
// WebWxOplog 修改用户备注接口
|
||||
@ -211,7 +215,9 @@ func (c *Caller) WebWxOplog(request *BaseRequest, remarkName, toUserName string)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
func (c *Caller) UploadMedia(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*UploadResponse, error) {
|
||||
@ -221,15 +227,15 @@ func (c *Caller) UploadMedia(file *os.File, request *BaseRequest, info *LoginInf
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
var item UploadResponse
|
||||
|
||||
if err := scanJson(resp, &item); err != nil {
|
||||
if err := scanJson(resp.Body, &item); err != nil {
|
||||
return &item, err
|
||||
}
|
||||
if !item.BaseResponse.Ok() {
|
||||
return &item, item.BaseResponse
|
||||
return &item, item.BaseResponse.Err()
|
||||
}
|
||||
if len(item.MediaId) == 0 {
|
||||
return &item, errors.New("upload failed")
|
||||
@ -238,20 +244,39 @@ func (c *Caller) UploadMedia(file *os.File, request *BaseRequest, info *LoginInf
|
||||
}
|
||||
|
||||
// WebWxSendImageMsg 发送图片消息接口
|
||||
func (c *Caller) WebWxSendImageMsg(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
|
||||
// 首先尝试上传图片
|
||||
resp, err := c.UploadMedia(file, request, info, fromUserName, toUserName)
|
||||
func (c *Caller) WebWxSendImageMsg(reader io.Reader, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
|
||||
file, cb, err := readerToFile(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cb()
|
||||
// 首先尝试上传图片
|
||||
var mediaId string
|
||||
{
|
||||
resp, err := c.UploadMedia(file, request, info, fromUserName, toUserName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mediaId = resp.MediaId
|
||||
}
|
||||
// 构造新的图片类型的信息
|
||||
msg := NewMediaSendMessage(MsgTypeImage, fromUserName, toUserName, resp.MediaId)
|
||||
msg := NewMediaSendMessage(MsgTypeImage, fromUserName, toUserName, mediaId)
|
||||
// 发送图片信息
|
||||
resp1, err := c.Client.WebWxSendMsgImg(msg, request, info)
|
||||
return getSuccessSentMessage(msg, resp1, err)
|
||||
resp, err := c.Client.WebWxSendMsgImg(msg, request, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.SentMessage(msg)
|
||||
}
|
||||
|
||||
func (c *Caller) WebWxSendFile(file *os.File, req *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
|
||||
func (c *Caller) WebWxSendFile(reader io.Reader, req *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
|
||||
file, cb, err := readerToFile(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cb()
|
||||
resp, err := c.UploadMedia(file, req, info, fromUserName, toUserName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -267,21 +292,40 @@ func (c *Caller) WebWxSendFile(file *os.File, req *BaseRequest, info *LoginInfo,
|
||||
return c.WebWxSendAppMsg(msg, req)
|
||||
}
|
||||
|
||||
func (c *Caller) WebWxSendVideoMsg(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
|
||||
resp, err := c.UploadMedia(file, request, info, fromUserName, toUserName)
|
||||
func (c *Caller) WebWxSendVideoMsg(reader io.Reader, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
|
||||
file, cb, err := readerToFile(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cb()
|
||||
var mediaId string
|
||||
{
|
||||
resp, err := c.UploadMedia(file, request, info, fromUserName, toUserName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mediaId = resp.MediaId
|
||||
}
|
||||
// 构造新的图片类型的信息
|
||||
msg := NewMediaSendMessage(MsgTypeVideo, fromUserName, toUserName, resp.MediaId)
|
||||
resp2, err := c.Client.WebWxSendVideoMsg(request, msg)
|
||||
return getSuccessSentMessage(msg, resp2, err)
|
||||
msg := NewMediaSendMessage(MsgTypeVideo, fromUserName, toUserName, mediaId)
|
||||
resp, err := c.Client.WebWxSendVideoMsg(request, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.SentMessage(msg)
|
||||
}
|
||||
|
||||
// WebWxSendAppMsg 发送媒体消息
|
||||
func (c *Caller) WebWxSendAppMsg(msg *SendMessage, req *BaseRequest) (*SentMessage, error) {
|
||||
resp, err := c.Client.WebWxSendAppMsg(msg, req)
|
||||
return getSuccessSentMessage(msg, resp, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.SentMessage(msg)
|
||||
}
|
||||
|
||||
// Logout 用户退出
|
||||
@ -290,7 +334,9 @@ func (c *Caller) Logout(info *LoginInfo) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// AddFriendIntoChatRoom 拉好友入群
|
||||
@ -302,7 +348,9 @@ func (c *Caller) AddFriendIntoChatRoom(req *BaseRequest, info *LoginInfo, group
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// RemoveFriendFromChatRoom 从群聊中移除用户
|
||||
@ -314,7 +362,9 @@ func (c *Caller) RemoveFriendFromChatRoom(req *BaseRequest, info *LoginInfo, gro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// WebWxVerifyUser 同意加好友请求
|
||||
@ -323,7 +373,9 @@ func (c *Caller) WebWxVerifyUser(storage *Storage, info RecommendInfo, verifyCon
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// WebWxRevokeMsg 撤回消息操作
|
||||
@ -332,7 +384,9 @@ func (c *Caller) WebWxRevokeMsg(msg *SentMessage, request *BaseRequest) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// WebWxStatusAsRead 将消息设置为已读
|
||||
@ -341,7 +395,9 @@ func (c *Caller) WebWxStatusAsRead(request *BaseRequest, info *LoginInfo, msg *M
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// WebWxRelationPin 将联系人是否置顶
|
||||
@ -350,16 +406,18 @@ func (c *Caller) WebWxRelationPin(request *BaseRequest, user *User, op uint8) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// WebWxPushLogin 免扫码登陆接口
|
||||
func (c *Caller) WebWxPushLogin(uin int) (*PushLoginResponse, error) {
|
||||
func (c *Caller) WebWxPushLogin(uin int64) (*PushLoginResponse, error) {
|
||||
resp, err := c.Client.WebWxPushLogin(uin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
var item PushLoginResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&item); err != nil {
|
||||
return nil, err
|
||||
@ -373,7 +431,7 @@ func (c *Caller) WebWxCreateChatRoom(request *BaseRequest, info *LoginInfo, topi
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
var item struct {
|
||||
BaseResponse BaseResponse
|
||||
ChatRoomName string
|
||||
@ -382,7 +440,7 @@ func (c *Caller) WebWxCreateChatRoom(request *BaseRequest, info *LoginInfo, topi
|
||||
return nil, err
|
||||
}
|
||||
if !item.BaseResponse.Ok() {
|
||||
return nil, item.BaseResponse
|
||||
return nil, item.BaseResponse.Err()
|
||||
}
|
||||
group := Group{User: &User{UserName: item.ChatRoomName}}
|
||||
return &group, nil
|
||||
@ -394,7 +452,9 @@ func (c *Caller) WebWxRenameChatRoom(request *BaseRequest, info *LoginInfo, newT
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return parseBaseResponseError(resp)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
parser := MessageResponseParser{resp.Body}
|
||||
return parser.Err()
|
||||
}
|
||||
|
||||
// SetMode 设置Client的模式
|
||||
@ -402,41 +462,66 @@ func (c *Client) SetMode(mode Mode) {
|
||||
c.mode = mode
|
||||
}
|
||||
|
||||
// 处理响应返回的结果是否正常
|
||||
func parseBaseResponseError(resp *http.Response) error {
|
||||
defer resp.Body.Close()
|
||||
// MessageResponseParser 消息响应解析器
|
||||
type MessageResponseParser struct {
|
||||
Reader io.Reader
|
||||
}
|
||||
|
||||
// Err 解析错误
|
||||
func (p *MessageResponseParser) Err() error {
|
||||
var item struct{ BaseResponse BaseResponse }
|
||||
if err := scanJson(resp, &item); err != nil {
|
||||
if err := scanJson(p.Reader, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
if !item.BaseResponse.Ok() {
|
||||
return item.BaseResponse
|
||||
return item.BaseResponse.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMessageResponseError(resp *http.Response, msg *SentMessage) error {
|
||||
defer resp.Body.Close()
|
||||
|
||||
// MsgID 解析消息ID
|
||||
func (p *MessageResponseParser) MsgID() (string, error) {
|
||||
var messageResp MessageResponse
|
||||
|
||||
if err := scanJson(resp, &messageResp); err != nil {
|
||||
return err
|
||||
if err := scanJson(p.Reader, &messageResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !messageResp.BaseResponse.Ok() {
|
||||
return messageResp.BaseResponse
|
||||
return "", messageResp.BaseResponse.Err()
|
||||
}
|
||||
// 发送成功之后将msgId赋值给SendMessage
|
||||
msg.MsgId = messageResp.MsgID
|
||||
return nil
|
||||
return messageResp.MsgID, nil
|
||||
}
|
||||
|
||||
func getSuccessSentMessage(msg *SendMessage, resp *http.Response, err error) (*SentMessage, error) {
|
||||
// SentMessage 返回 SentMessage
|
||||
func (p *MessageResponseParser) SentMessage(msg *SendMessage) (*SentMessage, error) {
|
||||
msgID, err := p.MsgID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sendSuccessMsg := &SentMessage{SendMessage: msg}
|
||||
err = parseMessageResponseError(resp, sendSuccessMsg)
|
||||
return sendSuccessMsg, err
|
||||
return &SentMessage{MsgId: msgID, SendMessage: msg}, nil
|
||||
}
|
||||
|
||||
func readerToFile(reader io.Reader) (file *os.File, cb func(), err error) {
|
||||
var ok bool
|
||||
if file, ok = reader.(*os.File); ok {
|
||||
return file, func() {}, nil
|
||||
}
|
||||
file, err = os.CreateTemp("", "*")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cb = func() {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(file.Name())
|
||||
}
|
||||
_, err = io.Copy(file, reader)
|
||||
if err != nil {
|
||||
cb()
|
||||
return nil, nil, err
|
||||
}
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
cb()
|
||||
return nil, nil, err
|
||||
}
|
||||
return file, cb, nil
|
||||
}
|
||||
|
216
client.go
216
client.go
@ -4,17 +4,17 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -32,31 +32,44 @@ 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")
|
||||
}
|
||||
|
||||
func (u UserAgentHook) AfterRequest(response *http.Response, err error) {}
|
||||
func (u UserAgentHook) AfterRequest(_ *http.Response, _ error) {}
|
||||
|
||||
// Client http请求客户端
|
||||
// 客户端需要维持Session会话
|
||||
// 并且客户端不允许跳转
|
||||
type Client struct {
|
||||
// 设置一些client的请求行为
|
||||
// see normalMode desktopMode
|
||||
mode Mode
|
||||
|
||||
// client http客户端
|
||||
client *http.Client
|
||||
|
||||
// Domain 微信服务器请求域名
|
||||
// 这个参数会在登录成功后被赋值
|
||||
// 之后所有的请求都会使用这个域名
|
||||
// 在登录热登录和扫码登录时会被重新赋值
|
||||
Domain WechatDomain
|
||||
|
||||
// HttpHooks 请求上下文钩子
|
||||
HttpHooks HttpHooks
|
||||
*http.Client
|
||||
Domain WechatDomain
|
||||
mode Mode
|
||||
mu sync.Mutex
|
||||
cookies map[string][]*http.Cookie
|
||||
|
||||
// MaxRetryTimes 最大重试次数
|
||||
MaxRetryTimes int
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
jar, _ := cookiejar.New(nil)
|
||||
timeout := 30 * time.Second
|
||||
return &Client{
|
||||
Client: &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Jar: jar,
|
||||
Timeout: timeout,
|
||||
}}
|
||||
httpClient := &http.Client{
|
||||
// 设置客户端不自动跳转
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
// 设置30秒超时
|
||||
// 因为微信同步消息时是一个时间长达25秒的长轮训
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
client := &Client{client: httpClient}
|
||||
client.SetCookieJar(NewJar())
|
||||
return client
|
||||
}
|
||||
|
||||
// DefaultClient 自动存储cookie
|
||||
@ -64,6 +77,7 @@ func NewClient() *Client {
|
||||
func DefaultClient() *Client {
|
||||
client := NewClient()
|
||||
client.AddHttpHook(UserAgentHook{})
|
||||
client.MaxRetryTimes = 5
|
||||
return client
|
||||
}
|
||||
|
||||
@ -75,9 +89,18 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||
for _, hook := range c.HttpHooks {
|
||||
hook.BeforeRequest(req)
|
||||
}
|
||||
resp, err := c.Client.Do(req)
|
||||
var (
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
for i := 0; i < c.MaxRetryTimes; i++ {
|
||||
resp, err = c.client.Do(req)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
err = NetworkErr{error: err}
|
||||
err = fmt.Errorf("%w: %s", NetworkErr, err.Error())
|
||||
}
|
||||
for _, hook := range c.HttpHooks {
|
||||
hook.AfterRequest(resp, err)
|
||||
@ -85,30 +108,23 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *Client) setCookie(resp *http.Response) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
cookies := resp.Cookies()
|
||||
if c.cookies == nil {
|
||||
c.cookies = make(map[string][]*http.Cookie)
|
||||
}
|
||||
path := fmt.Sprintf("%s://%s%s", resp.Request.URL.Scheme, resp.Request.URL.Host, resp.Request.URL.Path)
|
||||
c.cookies[path] = cookies
|
||||
}
|
||||
|
||||
// Do 抽象Do方法,将所有的有效的cookie存入Client.cookies
|
||||
// 方便热登陆时获取
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
resp, err := c.do(req)
|
||||
if err == nil {
|
||||
c.setCookie(resp)
|
||||
}
|
||||
return resp, err
|
||||
return c.do(req)
|
||||
}
|
||||
|
||||
// GetCookieMap 获取当前client的所有的有效的client
|
||||
func (c *Client) GetCookieMap() map[string][]*http.Cookie {
|
||||
return c.cookies
|
||||
// Jar 返回当前client的 http.CookieJar
|
||||
// this http.CookieJar must be *Jar type
|
||||
func (c *Client) Jar() http.CookieJar {
|
||||
return c.client.Jar
|
||||
}
|
||||
|
||||
// SetCookieJar 设置cookieJar
|
||||
// 这里限制了cookieJar必须是Jar类型
|
||||
// 否则进行cookie序列化的时候因为字段的私有性无法进行所有字段的导出
|
||||
func (c *Client) SetCookieJar(jar *Jar) {
|
||||
c.client.Jar = jar.AsCookieJar()
|
||||
}
|
||||
|
||||
// GetLoginUUID 获取登录的uuid
|
||||
@ -119,11 +135,11 @@ func (c *Client) GetLoginUUID() (*http.Response, error) {
|
||||
// GetLoginQrcode 获取登录的二维吗
|
||||
func (c *Client) GetLoginQrcode(uuid string) (*http.Response, error) {
|
||||
path := qrcode + uuid
|
||||
return c.Get(path)
|
||||
return c.client.Get(path)
|
||||
}
|
||||
|
||||
// CheckLogin 检查是否登录
|
||||
func (c *Client) CheckLogin(uuid string) (*http.Response, error) {
|
||||
func (c *Client) CheckLogin(uuid, tip string) (*http.Response, error) {
|
||||
path, _ := url.Parse(login)
|
||||
now := time.Now().Unix()
|
||||
params := url.Values{}
|
||||
@ -131,15 +147,16 @@ func (c *Client) CheckLogin(uuid string) (*http.Response, error) {
|
||||
params.Add("_", strconv.FormatInt(now, 10))
|
||||
params.Add("loginicon", "true")
|
||||
params.Add("uuid", uuid)
|
||||
params.Add("tip", "0")
|
||||
params.Add("tip", tip)
|
||||
path.RawQuery = params.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// GetLoginInfo 请求获取LoginInfo
|
||||
func (c *Client) GetLoginInfo(path string) (*http.Response, error) {
|
||||
return c.mode.GetLoginInfo(c, path)
|
||||
func (c *Client) GetLoginInfo(path *url.URL) (*http.Response, error) {
|
||||
c.Domain = WechatDomain(path.Host)
|
||||
return c.mode.GetLoginInfo(c, path.String())
|
||||
}
|
||||
|
||||
// WebInit 请求获取初始化信息
|
||||
@ -203,12 +220,12 @@ func (c *Client) SyncCheck(request *BaseRequest, info *LoginInfo, response *WebI
|
||||
}
|
||||
|
||||
// WebWxGetContact 获取联系人信息
|
||||
func (c *Client) WebWxGetContact(info *LoginInfo) (*http.Response, error) {
|
||||
func (c *Client) WebWxGetContact(info *LoginInfo, reqs int64) (*http.Response, error) {
|
||||
path, _ := url.Parse(c.Domain.BaseHost() + webwxgetcontact)
|
||||
params := url.Values{}
|
||||
params.Add("r", strconv.FormatInt(time.Now().UnixNano()/1e6, 10))
|
||||
params.Add("skey", info.SKey)
|
||||
params.Add("req", "0")
|
||||
params.Add("seq", strconv.FormatInt(reqs, 10))
|
||||
path.RawQuery = params.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
return c.Do(req)
|
||||
@ -285,7 +302,7 @@ func (c *Client) WebWxGetHeadImg(user *User) (*http.Response, error) {
|
||||
} else {
|
||||
params := url.Values{}
|
||||
params.Add("username", user.UserName)
|
||||
params.Add("skey", user.Self.Bot.Storage.Request.Skey)
|
||||
params.Add("skey", user.self.bot.Storage.Request.Skey)
|
||||
params.Add("type", "big")
|
||||
params.Add("chatroomid", user.EncryChatRoomId)
|
||||
params.Add("seq", "0")
|
||||
@ -297,6 +314,8 @@ func (c *Client) WebWxGetHeadImg(user *User) (*http.Response, error) {
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
// WebWxUploadMediaByChunk 分块上传文件
|
||||
// TODO 优化掉这个函数
|
||||
func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, info *LoginInfo, forUserName, toUserName string) (*http.Response, error) {
|
||||
// 获取文件上传的类型
|
||||
contentType, err := GetFileContentType(file)
|
||||
@ -317,15 +336,22 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileMd5 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
fileMd5 := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
sate, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filename := sate.Name()
|
||||
|
||||
if ext := filepath.Ext(filename); ext == "" {
|
||||
names := strings.Split(contentType, "/")
|
||||
filename = filename + "." + names[len(names)-1]
|
||||
}
|
||||
|
||||
// 获取文件的类型
|
||||
mediaType := getMessageType(sate.Name())
|
||||
mediaType := getMessageType(filename)
|
||||
|
||||
path, _ := url.Parse(c.Domain.FileHost() + webwxuploadmedia)
|
||||
params := url.Values{}
|
||||
@ -333,8 +359,12 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
||||
|
||||
path.RawQuery = params.Encode()
|
||||
|
||||
cookies := c.Jar.Cookies(path)
|
||||
webWxDataTicket := getWebWxDataTicket(cookies)
|
||||
cookies := c.Jar().Cookies(path)
|
||||
|
||||
webWxDataTicket, err := getWebWxDataTicket(cookies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uploadMediaRequest := map[string]interface{}{
|
||||
"UploadType": 2,
|
||||
@ -369,7 +399,7 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
||||
|
||||
content := map[string]string{
|
||||
"id": "WU_FILE_0",
|
||||
"name": file.Name(),
|
||||
"name": filename,
|
||||
"type": contentType,
|
||||
"lastModifiedDate": sate.ModTime().Format(TimeFormat),
|
||||
"size": strconv.FormatInt(sate.Size(), 10),
|
||||
@ -386,16 +416,17 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chunkBuff = make([]byte, chunkSize)
|
||||
|
||||
var formBuffer = bytes.NewBuffer(nil)
|
||||
|
||||
// 分块上传
|
||||
for chunk := 0; int64(chunk) < chunks; chunk++ {
|
||||
|
||||
isLastTime := int64(chunk)+1 == chunks
|
||||
|
||||
if chunks > 1 {
|
||||
content["chunk"] = strconv.Itoa(chunk)
|
||||
}
|
||||
|
||||
var formBuffer = bytes.NewBuffer(nil)
|
||||
formBuffer.Reset()
|
||||
|
||||
writer := multipart.NewWriter(formBuffer)
|
||||
|
||||
@ -410,37 +441,41 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
|
||||
}
|
||||
|
||||
w, err := writer.CreateFormFile("filename", file.Name())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chunkData := make([]byte, chunkSize)
|
||||
|
||||
n, err := file.Read(chunkData)
|
||||
n, err := file.Read(chunkBuff)
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = w.Write(chunkData[:n]); err != nil {
|
||||
if _, err = w.Write(chunkBuff[:n]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ct := writer.FormDataContentType()
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, path.String(), formBuffer)
|
||||
req.Header.Set("Content-Type", ct)
|
||||
|
||||
// 发送数据
|
||||
resp, err = c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isLastTime := int64(chunk)+1 == chunks
|
||||
// 如果不是最后一次, 解析有没有错误
|
||||
if !isLastTime {
|
||||
if err := parseBaseResponseError(resp); err != nil {
|
||||
parser := MessageResponseParser{Reader: resp.Body}
|
||||
if err = parser.Err(); err != nil {
|
||||
_ = resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -562,17 +597,21 @@ func (c *Client) WebWxGetVideo(msg *Message, info *LoginInfo) (*http.Response, e
|
||||
// WebWxGetMedia 获取文件消息的文件响应
|
||||
func (c *Client) WebWxGetMedia(msg *Message, info *LoginInfo) (*http.Response, error) {
|
||||
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.Add("sender", msg.FromUserName)
|
||||
params.Add("mediaid", msg.MediaId)
|
||||
params.Add("encryfilename", msg.EncryFileName)
|
||||
params.Add("fromuser", fmt.Sprintf("%d", info.WxUin))
|
||||
params.Add("fromuser", strconv.FormatInt(info.WxUin, 10))
|
||||
params.Add("pass_ticket", info.PassTicket)
|
||||
params.Add("webwx_data_ticket", getWebWxDataTicket(c.Jar.Cookies(path)))
|
||||
params.Add("webwx_data_ticket", webWxDataTicket)
|
||||
path.RawQuery = params.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
req.Header.Add("Referer", path.String())
|
||||
req.Header.Add("Range", "bytes=0-")
|
||||
req.Header.Add("Referer", c.Domain.BaseHost()+"/")
|
||||
return c.Do(req)
|
||||
}
|
||||
|
||||
@ -590,6 +629,14 @@ func (c *Client) Logout(info *LoginInfo) (*http.Response, error) {
|
||||
|
||||
// AddMemberIntoChatRoom 添加用户进群聊
|
||||
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)
|
||||
params := url.Values{}
|
||||
params.Add("fun", "addmember")
|
||||
@ -611,6 +658,29 @@ func (c *Client) AddMemberIntoChatRoom(req *BaseRequest, info *LoginInfo, group
|
||||
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 从群聊中移除用户
|
||||
func (c *Client) RemoveMemberFromChatRoom(req *BaseRequest, info *LoginInfo, group *Group, friends ...*User) (*http.Response, error) {
|
||||
path, _ := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
|
||||
@ -701,12 +771,8 @@ func (c *Client) WebWxRelationPin(request *BaseRequest, op uint8, user *User) (*
|
||||
}
|
||||
|
||||
// WebWxPushLogin 免扫码登陆接口
|
||||
func (c *Client) WebWxPushLogin(uin int) (*http.Response, error) {
|
||||
path, _ := url.Parse(c.Domain.BaseHost() + webwxpushloginurl)
|
||||
params := url.Values{"uin": {strconv.Itoa(uin)}}
|
||||
path.RawQuery = params.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
return c.Do(req)
|
||||
func (c *Client) WebWxPushLogin(uin int64) (*http.Response, error) {
|
||||
return c.mode.PushLogin(c, uin)
|
||||
}
|
||||
|
||||
// WebWxSendVideoMsg 发送视频消息接口
|
||||
|
81
cookiejar.go
Normal file
81
cookiejar.go
Normal file
@ -0,0 +1,81 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
PsList cookiejar.PublicSuffixList
|
||||
|
||||
// mu locks the remaining fields.
|
||||
mu sync.Mutex
|
||||
|
||||
// Entries is a set of entries, keyed by their eTLD+1 and subkeyed by
|
||||
// their name/Domain/path.
|
||||
Entries map[string]map[string]entry
|
||||
|
||||
// nextSeqNum is the next sequence number assigned to a new cookie
|
||||
// created SetCookies.
|
||||
NextSeqNum uint64
|
||||
}
|
||||
|
||||
// AsCookieJar unsafe convert to http.CookieJar
|
||||
func (j *Jar) AsCookieJar() http.CookieJar {
|
||||
return (*cookiejar.Jar)(unsafe.Pointer(j))
|
||||
}
|
||||
|
||||
func fromCookieJar(jar http.CookieJar) *Jar {
|
||||
return (*Jar)(unsafe.Pointer(jar.(*cookiejar.Jar)))
|
||||
}
|
||||
|
||||
func NewJar() *Jar {
|
||||
jar, _ := cookiejar.New(nil)
|
||||
return fromCookieJar(jar)
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
Name string
|
||||
Value string
|
||||
Domain string
|
||||
Path string
|
||||
SameSite string
|
||||
Secure bool
|
||||
HttpOnly bool
|
||||
Persistent bool
|
||||
HostOnly bool
|
||||
Expires time.Time
|
||||
Creation time.Time
|
||||
LastAccess time.Time
|
||||
|
||||
// seqNum is a sequence number so that Jar returns cookies in a
|
||||
// deterministic order, even for cookies that have equal Path length and
|
||||
// equal Creation time. This simplifies testing.
|
||||
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
|
||||
}
|
2
emoji.go
2
emoji.go
@ -11,7 +11,7 @@ var (
|
||||
emojiRegexp = regexp.MustCompile(`<span class="emoji emoji(.*?)"></span>`)
|
||||
)
|
||||
|
||||
// emoji 表情
|
||||
// Emoji 表情
|
||||
// 字段太多了,污染命名空间,封装成struct返回
|
||||
var Emoji = struct {
|
||||
Smile string
|
||||
|
@ -1,7 +1,9 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -23,8 +25,11 @@ func (l LoginInfo) Ok() bool {
|
||||
return l.Ret == 0
|
||||
}
|
||||
|
||||
func (l LoginInfo) Error() string {
|
||||
return l.Message
|
||||
func (l LoginInfo) Err() error {
|
||||
if l.Ok() {
|
||||
return nil
|
||||
}
|
||||
return errors.New(l.Message)
|
||||
}
|
||||
|
||||
// BaseRequest 初始的请求信息
|
||||
@ -52,9 +57,9 @@ type WebInitResponse struct {
|
||||
SKey string
|
||||
BaseResponse BaseResponse
|
||||
SyncKey SyncKey
|
||||
User User
|
||||
MPSubscribeMsgList []MPSubscribeMsg
|
||||
ContactList []User
|
||||
User *User
|
||||
MPSubscribeMsgList []*MPSubscribeMsg
|
||||
ContactList Members
|
||||
}
|
||||
|
||||
// MPSubscribeMsg 公众号的订阅信息
|
||||
@ -63,12 +68,14 @@ type MPSubscribeMsg struct {
|
||||
Time int64
|
||||
UserName string
|
||||
NickName string
|
||||
MPArticleList []struct {
|
||||
Title string
|
||||
Cover string
|
||||
Digest string
|
||||
Url string
|
||||
}
|
||||
MPArticleList []*MPArticle
|
||||
}
|
||||
|
||||
type MPArticle struct {
|
||||
Title string
|
||||
Cover string
|
||||
Digest string
|
||||
Url string
|
||||
}
|
||||
|
||||
type UserDetailItem struct {
|
||||
@ -87,28 +94,6 @@ func NewUserDetailItemList(members Members) UserDetailItemList {
|
||||
return list
|
||||
}
|
||||
|
||||
type SyncCheckResponse struct {
|
||||
RetCode string
|
||||
Selector string
|
||||
}
|
||||
|
||||
func (s SyncCheckResponse) Success() bool {
|
||||
return s.RetCode == "0"
|
||||
}
|
||||
|
||||
func (s SyncCheckResponse) NorMal() bool {
|
||||
return s.Success() && s.Selector == "0"
|
||||
}
|
||||
|
||||
// 实现error接口
|
||||
func (s SyncCheckResponse) Error() string {
|
||||
i, err := strconv.ParseInt(s.RetCode, 16, 10)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return Ret(i).String()
|
||||
}
|
||||
|
||||
type WebWxSyncResponse struct {
|
||||
AddMsgCount int
|
||||
ContinueFlag int
|
||||
@ -125,7 +110,7 @@ type WebWxSyncResponse struct {
|
||||
|
||||
type WebWxContactResponse struct {
|
||||
MemberCount int
|
||||
Seq int
|
||||
Seq int64
|
||||
BaseResponse BaseResponse
|
||||
MemberList []*User
|
||||
}
|
||||
@ -136,9 +121,49 @@ type WebWxBatchContactResponse struct {
|
||||
ContactList []*User
|
||||
}
|
||||
|
||||
type CheckLoginResponse struct {
|
||||
Code string
|
||||
Raw []byte
|
||||
// CheckLoginResponse 检查登录状态的响应body
|
||||
type CheckLoginResponse []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 {
|
||||
@ -162,22 +187,9 @@ func (p PushLoginResponse) Ok() bool {
|
||||
return p.Ret == "0" && p.UUID != ""
|
||||
}
|
||||
|
||||
type NetworkErr struct{ error }
|
||||
|
||||
func (n NetworkErr) Unwrap() error {
|
||||
return n.error
|
||||
}
|
||||
|
||||
func IsNetworkError(err error) bool {
|
||||
_, ok := err.(NetworkErr)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IgnoreNetworkError 忽略网络请求的错误
|
||||
func IgnoreNetworkError(errHandler func(err error)) func(error) {
|
||||
return func(err error) {
|
||||
if !IsNetworkError(err) {
|
||||
errHandler(err)
|
||||
}
|
||||
func (p PushLoginResponse) Err() error {
|
||||
if p.Ok() {
|
||||
return nil
|
||||
}
|
||||
return errors.New(p.Msg)
|
||||
}
|
43
errors.go
Normal file
43
errors.go
Normal file
@ -0,0 +1,43 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func IsNetworkError(err error) bool {
|
||||
return errors.Is(err, NetworkErr)
|
||||
}
|
||||
|
||||
// IgnoreNetworkError 忽略网络请求的错误
|
||||
func IgnoreNetworkError(errHandler func(err error)) func(error) {
|
||||
return func(err error) {
|
||||
if !IsNetworkError(err) {
|
||||
errHandler(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrForbidden 禁止当前账号登录
|
||||
ErrForbidden = errors.New("login forbidden")
|
||||
|
||||
// ErrInvalidStorage define invalid storage error
|
||||
ErrInvalidStorage = errors.New("invalid storage")
|
||||
|
||||
// NetworkErr define wechat network error
|
||||
NetworkErr = errors.New("wechat network error")
|
||||
|
||||
// ErrNoSuchUserFoundError define no such user found error
|
||||
ErrNoSuchUserFoundError = errors.New("no such user found")
|
||||
|
||||
// ErrLoginTimeout define login timeout error
|
||||
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
|
||||
func (r Ret) Error() string {
|
||||
return r.String()
|
||||
}
|
15
errors_test.go
Normal file
15
errors_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsNetworkError(t *testing.T) {
|
||||
var err = errors.New("test error")
|
||||
err = fmt.Errorf("%w: %s", NetworkErr, err.Error())
|
||||
if !IsNetworkError(err) {
|
||||
t.Error("err is not network error")
|
||||
}
|
||||
}
|
60
global.go
60
global.go
@ -1,13 +1,13 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
uuidRegexp = regexp.MustCompile(`uuid = "(.*?)";`)
|
||||
statusCodeRegexp = regexp.MustCompile(`window.code=(\d+);`)
|
||||
avatarRegexp = regexp.MustCompile(`window.userAvatar = '(.*)';`)
|
||||
syncCheckRegexp = regexp.MustCompile(`window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`)
|
||||
redirectUriRegexp = regexp.MustCompile(`window.redirect_uri="(.*?)"`)
|
||||
)
|
||||
@ -18,19 +18,15 @@ const (
|
||||
|
||||
jsonContentType = "application/json; charset=utf-8"
|
||||
uosPatchClientVersion = "2.0.0"
|
||||
uosPatchExtspam = "Gp8ICJkIEpkICggwMDAwMDAwMRAGGoAI1GiJSIpeO1RZTq9QBKsRbPJdi84ropi16EYI10WB6g74sGmRwSNXjPQnYU" +
|
||||
"KYotKkvLGpshucCaeWZMOylnc6o2AgDX9grhQQx7fm2DJRTyuNhUlwmEoWhjoG3F0ySAWUsEbH3bJMsEBwoB//0qmFJob74ffdaslqL+IrSy7L" +
|
||||
"J76/G5TkvNC+J0VQkpH1u3iJJs0uUYyLDzdBIQ6Ogd8LDQ3VKnJLm4g/uDLe+G7zzzkOPzCjXL+70naaQ9medzqmh+/SmaQ6uFWLDQLcRln++w" +
|
||||
"BwoEibNpG4uOJvqXy+ql50DjlNchSuqLmeadFoo9/mDT0q3G7o/80P15ostktjb7h9bfNc+nZVSnUEJXbCjTeqS5UYuxn+HTS5nZsPVxJA2O5G" +
|
||||
"dKCYK4x8lTTKShRstqPfbQpplfllx2fwXcSljuYi3YipPyS3GCAqf5A7aYYwJ7AvGqUiR2SsVQ9Nbp8MGHET1GxhifC692APj6SJxZD3i1drSY" +
|
||||
"ZPMMsS9rKAJTGz2FEupohtpf2tgXm6c16nDk/cw+C7K7me5j5PLHv55DFCS84b06AytZPdkFZLj7FHOkcFGJXitHkX5cgww7vuf6F3p0yM/W73" +
|
||||
"SoXTx6GX4G6Hg2rYx3O/9VU2Uq8lvURB4qIbD9XQpzmyiFMaytMnqxcZJcoXCtfkTJ6pI7a92JpRUvdSitg967VUDUAQnCXCM/m0snRkR9LtoX" +
|
||||
"AO1FUGpwlp1EfIdCZFPKNnXMeqev0j9W9ZrkEs9ZWcUEexSj5z+dKYQBhIICviYUQHVqBTZSNy22PlUIeDeIs11j7q4t8rD8LPvzAKWVqXE+5l" +
|
||||
"S1JPZkjg4y5hfX1Dod3t96clFfwsvDP6xBSe1NBcoKbkyGxYK0UvPGtKQEE0Se2zAymYDv41klYE9s+rxp8e94/H8XhrL9oGm8KWb2RmYnAE7r" +
|
||||
"y9gd6e8ZuBRIsISlJAE/e8y8xFmP031S6Lnaet6YXPsFpuFsdQs535IjcFd75hh6DNMBYhSfjv456cvhsb99+fRw/KVZLC3yzNSCbLSyo9d9BI" +
|
||||
"45Plma6V8akURQA/qsaAzU0VyTIqZJkPDTzhuCl92vD2AD/QOhx6iwRSVPAxcRFZcWjgc2wCKh+uCYkTVbNQpB9B90YlNmI3fWTuUOUjwOzQRx" +
|
||||
"JZj11NsimjOJ50qQwTTFj6qQvQ1a/I+MkTx5UO+yNHl718JWcR3AXGmv/aa9rD1eNP8ioTGlOZwPgmr2sor2iBpKTOrB83QgZXP+xRYkb4zVC+" +
|
||||
"LoAXEoIa1+zArywlgREer7DLePukkU6wHTkuSaF+ge5Of1bXuU4i938WJHj0t3D8uQxkJvoFi/EYN/7u2P1zGRLV4dHVUsZMGCCtnO6BBigFMAA="
|
||||
uosPatchExtspam = "Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykC" +
|
||||
"yNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R" +
|
||||
"3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5p" +
|
||||
"M7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHa" +
|
||||
"GGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYSc" +
|
||||
"W8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8" +
|
||||
"wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn" +
|
||||
"2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYIT" +
|
||||
"IqItIKjD35IGKAUwAA=="
|
||||
)
|
||||
|
||||
// 消息类型
|
||||
@ -102,24 +98,9 @@ const (
|
||||
AppMsgTypeReaderType AppMessageType = 100001 //自定义的消息
|
||||
)
|
||||
|
||||
// 登录状态
|
||||
const (
|
||||
StatusSuccess = "200"
|
||||
StatusScanned = "201"
|
||||
StatusTimeout = "400"
|
||||
StatusWait = "408"
|
||||
)
|
||||
|
||||
// errors
|
||||
var (
|
||||
ErrNoSuchUserFoundError = errors.New("no such user found")
|
||||
ErrMissLocationHeader = errors.New("301 response missing Location header")
|
||||
ErrLoginForbiddenError = errors.New("login forbidden")
|
||||
ErrLoginTimeout = errors.New("login timeout")
|
||||
)
|
||||
|
||||
// ALL 跟search函数搭配
|
||||
// friends.Search(openwechat.ALL, )
|
||||
//
|
||||
// friends.Search(openwechat.ALL, )
|
||||
const ALL = 0
|
||||
|
||||
// 性别
|
||||
@ -141,15 +122,14 @@ const (
|
||||
|
||||
const TimeFormat = "Mon Jan 02 2006 15:04:05 GMT+0800 (中国标准时间)"
|
||||
|
||||
var imageType = map[string]bool{
|
||||
"bmp": true,
|
||||
"png": true,
|
||||
"jpeg": true,
|
||||
"jpg": true,
|
||||
var imageType = map[string]struct{}{
|
||||
"bmp": {},
|
||||
"png": {},
|
||||
"jpeg": {},
|
||||
"jpg": {},
|
||||
}
|
||||
|
||||
var videoType = "mp4"
|
||||
const videoType = "mp4"
|
||||
|
||||
// ZombieText 检测僵尸好友字符串
|
||||
// 发送该字符给好友,能正常发送不报错的为正常好友,否则为僵尸好友
|
||||
const ZombieText = "وُحfخe ̷̴̐nخg ̷̴̐cخh ̷̴̐aخo امارتيخ ̷̴̐خ\n"
|
||||
// FileHelper 文件传输助手
|
||||
const FileHelper = "filehelper"
|
||||
|
301
message.go
301
message.go
@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -49,21 +50,33 @@ type Message struct {
|
||||
Url string
|
||||
senderInGroupUserName string
|
||||
RecommendInfo RecommendInfo
|
||||
Bot *Bot `json:"-"`
|
||||
bot *Bot
|
||||
mu sync.RWMutex
|
||||
Context context.Context `json:"-"`
|
||||
item map[string]interface{}
|
||||
Raw []byte `json:"-"`
|
||||
RowContent string `json:"-"` // 消息原始内容
|
||||
RawContent string `json:"-"` // 消息原始内容
|
||||
}
|
||||
|
||||
// Sender 获取消息的发送者
|
||||
func (m *Message) Sender() (*User, error) {
|
||||
if m.FromUserName == m.Bot.self.User.UserName {
|
||||
return m.Bot.self.User, nil
|
||||
if m.FromUserName == m.bot.self.User.UserName {
|
||||
return m.bot.self.User, nil
|
||||
}
|
||||
// 首先尝试从缓存里面查找, 如果没有找到则从服务器获取
|
||||
members, err := m.bot.self.Members()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, exist := members.GetByUserName(m.FromUserName)
|
||||
if !exist {
|
||||
// 找不到, 从服务器获取
|
||||
user = &User{self: m.bot.self, UserName: m.FromUserName}
|
||||
err = user.Detail()
|
||||
}
|
||||
if m.IsSendByGroup() && len(user.MemberList) == 0 {
|
||||
err = user.Detail()
|
||||
}
|
||||
user := &User{Self: m.Bot.self, UserName: m.FromUserName}
|
||||
err := user.Detail()
|
||||
return user, err
|
||||
}
|
||||
|
||||
@ -76,43 +89,51 @@ func (m *Message) SenderInGroup() (*User, error) {
|
||||
// https://github.com/eatmoreapple/openwechat/issues/66
|
||||
if m.IsSystem() {
|
||||
// 判断是否有自己发送
|
||||
if m.FromUserName == m.Bot.self.User.UserName {
|
||||
return m.Bot.self.User, nil
|
||||
if m.FromUserName == m.bot.self.User.UserName {
|
||||
return m.bot.self.User, nil
|
||||
}
|
||||
return nil, errors.New("can not found sender from system message")
|
||||
}
|
||||
group, err := m.Sender()
|
||||
user, err := m.Sender()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := group.Detail(); err != nil {
|
||||
return nil, err
|
||||
if user.IsFriend() {
|
||||
return user, nil
|
||||
}
|
||||
if group.IsFriend() {
|
||||
return group, nil
|
||||
}
|
||||
users := group.MemberList.SearchByUserName(1, m.senderInGroupUserName)
|
||||
if users == nil {
|
||||
return nil, ErrNoSuchUserFoundError
|
||||
}
|
||||
users.init(m.Bot.self)
|
||||
return users.First(), nil
|
||||
group := &Group{user}
|
||||
return group.SearchMemberByUsername(m.senderInGroupUserName)
|
||||
}
|
||||
|
||||
// Receiver 获取消息的接收者
|
||||
// 如果消息是群组消息,则返回群组
|
||||
// 如果消息是好友消息,则返回好友
|
||||
// 如果消息是系统消息,则返回当前用户
|
||||
func (m *Message) Receiver() (*User, error) {
|
||||
if m.IsSystem() || m.ToUserName == m.bot.self.UserName {
|
||||
return m.bot.self.User, nil
|
||||
}
|
||||
// https://github.com/eatmoreapple/openwechat/issues/113
|
||||
if m.ToUserName == FileHelper {
|
||||
return m.bot.self.FileHelper().User, nil
|
||||
}
|
||||
|
||||
if m.IsSendByGroup() {
|
||||
if sender, err := m.Sender(); err != nil {
|
||||
groups, err := m.bot.self.Groups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
user, exist := sender.MemberList.GetByUserName(m.ToUserName)
|
||||
if !exist {
|
||||
return nil, ErrNoSuchUserFoundError
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
username := m.FromUserName
|
||||
if m.IsSendBySelf() {
|
||||
username = m.ToUserName
|
||||
}
|
||||
users := groups.SearchByUserName(1, username)
|
||||
if users.Count() == 0 {
|
||||
return nil, ErrNoSuchUserFoundError
|
||||
}
|
||||
return users.First().User, nil
|
||||
} else {
|
||||
user, exist := m.Bot.self.MemberList.GetByRemarkName(m.ToUserName)
|
||||
user, exist := m.bot.self.MemberList.GetByUserName(m.ToUserName)
|
||||
if !exist {
|
||||
return nil, ErrNoSuchUserFoundError
|
||||
}
|
||||
@ -122,7 +143,7 @@ func (m *Message) Receiver() (*User, error) {
|
||||
|
||||
// IsSendBySelf 判断消息是否由自己发送
|
||||
func (m *Message) IsSendBySelf() bool {
|
||||
return m.FromUserName == m.Bot.self.User.UserName
|
||||
return m.FromUserName == m.bot.self.User.UserName
|
||||
}
|
||||
|
||||
// IsSendByFriend 判断消息是否由好友发送
|
||||
@ -132,54 +153,60 @@ func (m *Message) IsSendByFriend() bool {
|
||||
|
||||
// IsSendByGroup 判断消息是否由群组发送
|
||||
func (m *Message) IsSendByGroup() bool {
|
||||
return strings.HasPrefix(m.FromUserName, "@@")
|
||||
}
|
||||
|
||||
// Reply 回复消息
|
||||
func (m *Message) Reply(msgType MessageType, content, mediaId string) (*SentMessage, error) {
|
||||
msg := NewSendMessage(msgType, content, m.Bot.self.User.UserName, m.FromUserName, mediaId)
|
||||
info := m.Bot.Storage.LoginInfo
|
||||
request := m.Bot.Storage.Request
|
||||
sentMessage, err := m.Bot.Caller.WebWxSendMsg(msg, info, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sentMessage.Self = m.Bot.self
|
||||
return sentMessage, nil
|
||||
return strings.HasPrefix(m.FromUserName, "@@") || (m.IsSendBySelf() && strings.HasPrefix(m.ToUserName, "@@"))
|
||||
}
|
||||
|
||||
// ReplyText 回复文本消息
|
||||
func (m *Message) ReplyText(content string) (*SentMessage, error) {
|
||||
return m.Reply(MsgTypeText, content, "")
|
||||
msg := NewSendMessage(MsgTypeText, content, m.bot.self.User.UserName, m.FromUserName, "")
|
||||
info := m.bot.Storage.LoginInfo
|
||||
request := m.bot.Storage.Request
|
||||
sentMessage, err := m.bot.Caller.WebWxSendMsg(msg, info, request)
|
||||
return m.bot.self.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
// ReplyImage 回复图片消息
|
||||
func (m *Message) ReplyImage(file *os.File) (*SentMessage, error) {
|
||||
info := m.Bot.Storage.LoginInfo
|
||||
request := m.Bot.Storage.Request
|
||||
return m.Bot.Caller.WebWxSendImageMsg(file, request, info, m.Bot.self.UserName, m.FromUserName)
|
||||
func (m *Message) ReplyImage(file io.Reader) (*SentMessage, error) {
|
||||
info := m.bot.Storage.LoginInfo
|
||||
request := m.bot.Storage.Request
|
||||
sentMessage, err := m.bot.Caller.WebWxSendImageMsg(file, request, info, m.bot.self.UserName, m.FromUserName)
|
||||
return m.bot.self.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
// ReplyVideo 回复视频消息
|
||||
func (m *Message) ReplyVideo(file *os.File) (*SentMessage, error) {
|
||||
info := m.Bot.Storage.LoginInfo
|
||||
request := m.Bot.Storage.Request
|
||||
return m.Bot.Caller.WebWxSendVideoMsg(file, request, info, m.Bot.self.UserName, m.FromUserName)
|
||||
func (m *Message) ReplyVideo(file io.Reader) (*SentMessage, error) {
|
||||
info := m.bot.Storage.LoginInfo
|
||||
request := m.bot.Storage.Request
|
||||
sentMessage, err := m.bot.Caller.WebWxSendVideoMsg(file, request, info, m.bot.self.UserName, m.FromUserName)
|
||||
return m.bot.self.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
// ReplyFile 回复文件消息
|
||||
func (m *Message) ReplyFile(file *os.File) (*SentMessage, error) {
|
||||
info := m.Bot.Storage.LoginInfo
|
||||
request := m.Bot.Storage.Request
|
||||
return m.Bot.Caller.WebWxSendFile(file, request, info, m.Bot.self.UserName, m.FromUserName)
|
||||
func (m *Message) ReplyFile(file io.Reader) (*SentMessage, error) {
|
||||
info := m.bot.Storage.LoginInfo
|
||||
request := m.bot.Storage.Request
|
||||
sentMessage, err := m.bot.Caller.WebWxSendFile(file, request, info, m.bot.self.UserName, m.FromUserName)
|
||||
return m.bot.self.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
func (m *Message) IsText() bool {
|
||||
return m.MsgType == MsgTypeText && m.Url == ""
|
||||
}
|
||||
|
||||
func (m *Message) IsMap() bool {
|
||||
return m.MsgType == MsgTypeText && m.Url != ""
|
||||
func (m *Message) IsLocation() bool {
|
||||
return m.MsgType == MsgTypeText && strings.Contains(m.Url, "api.map.qq.com") && strings.Contains(m.Content, "pictype=location")
|
||||
}
|
||||
|
||||
func (m *Message) IsRealtimeLocation() bool {
|
||||
return m.IsRealtimeLocationStart() || m.IsRealtimeLocationStop()
|
||||
}
|
||||
|
||||
func (m *Message) IsRealtimeLocationStart() bool {
|
||||
return m.MsgType == MsgTypeApp && m.AppMsgType == AppMsgTypeRealtimeShareLocation
|
||||
}
|
||||
|
||||
func (m *Message) IsRealtimeLocationStop() bool {
|
||||
return m.MsgType == MsgTypeSys && m.Content == "位置共享已经结束"
|
||||
}
|
||||
|
||||
func (m *Message) IsPicture() bool {
|
||||
@ -239,6 +266,11 @@ func (m *Message) IsReceiveRedPacket() bool {
|
||||
return m.IsSystem() && m.Content == "收到红包,请在手机上查看"
|
||||
}
|
||||
|
||||
// IsRenameGroup 判断当前是否是群组重命名
|
||||
func (m *Message) IsRenameGroup() bool {
|
||||
return m.IsSystem() && strings.Contains(m.Content, "修改群名为")
|
||||
}
|
||||
|
||||
func (m *Message) IsSysNotice() bool {
|
||||
return m.MsgType == 9999
|
||||
}
|
||||
@ -259,16 +291,16 @@ func (m *Message) GetFile() (*http.Response, error) {
|
||||
return nil, errors.New("invalid message type")
|
||||
}
|
||||
if m.IsPicture() || m.IsEmoticon() {
|
||||
return m.Bot.Caller.Client.WebWxGetMsgImg(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetMsgImg(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
if m.IsVoice() {
|
||||
return m.Bot.Caller.Client.WebWxGetVoice(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetVoice(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
if m.IsVideo() {
|
||||
return m.Bot.Caller.Client.WebWxGetVideo(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetVideo(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
if m.IsMedia() {
|
||||
return m.Bot.Caller.Client.WebWxGetMedia(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetMedia(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
return nil, errors.New("unsupported type")
|
||||
}
|
||||
@ -278,7 +310,7 @@ func (m *Message) GetPicture() (*http.Response, error) {
|
||||
if !(m.IsPicture() || m.IsEmoticon()) {
|
||||
return nil, errors.New("picture message required")
|
||||
}
|
||||
return m.Bot.Caller.Client.WebWxGetMsgImg(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetMsgImg(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
|
||||
// GetVoice 获取录音消息的响应
|
||||
@ -286,7 +318,7 @@ func (m *Message) GetVoice() (*http.Response, error) {
|
||||
if !m.IsVoice() {
|
||||
return nil, errors.New("voice message required")
|
||||
}
|
||||
return m.Bot.Caller.Client.WebWxGetVoice(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetVoice(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
|
||||
// GetVideo 获取视频消息的响应
|
||||
@ -294,7 +326,7 @@ func (m *Message) GetVideo() (*http.Response, error) {
|
||||
if !m.IsVideo() {
|
||||
return nil, errors.New("video message required")
|
||||
}
|
||||
return m.Bot.Caller.Client.WebWxGetVideo(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetVideo(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
|
||||
// GetMedia 获取媒体消息的响应
|
||||
@ -302,7 +334,28 @@ func (m *Message) GetMedia() (*http.Response, error) {
|
||||
if !m.IsMedia() {
|
||||
return nil, errors.New("media message required")
|
||||
}
|
||||
return m.Bot.Caller.Client.WebWxGetMedia(m, m.Bot.Storage.LoginInfo)
|
||||
return m.bot.Caller.Client.WebWxGetMedia(m, m.bot.Storage.LoginInfo)
|
||||
}
|
||||
|
||||
// SaveFile 保存文件到指定的 io.Writer
|
||||
func (m *Message) SaveFile(writer io.Writer) error {
|
||||
resp, err := m.GetFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
_, err = io.Copy(writer, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveFileToLocal 保存文件到本地
|
||||
func (m *Message) SaveFileToLocal(filename string) error {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
return m.SaveFile(file)
|
||||
}
|
||||
|
||||
// Card 获取card类型
|
||||
@ -336,16 +389,24 @@ func (m *Message) RevokeMsg() (*RevokeMsg, error) {
|
||||
}
|
||||
|
||||
// Agree 同意好友的请求
|
||||
func (m *Message) Agree(verifyContents ...string) error {
|
||||
func (m *Message) Agree(verifyContents ...string) (*Friend, error) {
|
||||
if !m.IsFriendAdd() {
|
||||
return fmt.Errorf("friend add message required")
|
||||
return nil, errors.New("friend add message required")
|
||||
}
|
||||
return m.Bot.Caller.WebWxVerifyUser(m.Bot.Storage, m.RecommendInfo, strings.Join(verifyContents, ""))
|
||||
err := m.bot.Caller.WebWxVerifyUser(m.bot.Storage, m.RecommendInfo, strings.Join(verifyContents, ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
friend := newFriend(m.RecommendInfo.UserName, m.bot.self)
|
||||
if err = friend.Detail(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return friend, nil
|
||||
}
|
||||
|
||||
// AsRead 将消息设置为已读
|
||||
func (m *Message) AsRead() error {
|
||||
return m.Bot.Caller.WebWxStatusAsRead(m.Bot.Storage.Request, m.Bot.Storage.LoginInfo, m)
|
||||
return m.bot.Caller.WebWxStatusAsRead(m.bot.Storage.Request, m.bot.Storage.LoginInfo, m)
|
||||
}
|
||||
|
||||
// IsArticle 判断当前的消息类型是否为文章
|
||||
@ -387,32 +448,42 @@ func (m *Message) Get(key string) (value interface{}, exist bool) {
|
||||
|
||||
// 消息初始化,根据不同的消息作出不同的处理
|
||||
func (m *Message) init(bot *Bot) {
|
||||
m.Bot = bot
|
||||
m.bot = bot
|
||||
raw, _ := json.Marshal(m)
|
||||
m.Raw = raw
|
||||
m.RowContent = m.Content
|
||||
m.RawContent = m.Content
|
||||
// 如果是群消息
|
||||
if m.IsSendByGroup() {
|
||||
if !m.IsSystem() {
|
||||
// 将Username和正文分开
|
||||
data := strings.Split(m.Content, ":<br/>")
|
||||
m.Content = strings.Join(data[1:], "")
|
||||
m.senderInGroupUserName = data[0]
|
||||
receiver, err := m.Receiver()
|
||||
if err == nil {
|
||||
displayName := receiver.DisplayName
|
||||
if displayName == "" {
|
||||
displayName = receiver.NickName
|
||||
}
|
||||
// 判断是不是@消息
|
||||
atFlag := "@" + displayName + "\u2005"
|
||||
// mac客户端的@是空格非\u2005
|
||||
macAtFlag := "@" + displayName + " "
|
||||
if strings.Contains(m.Content, atFlag) || strings.Contains(m.Content, macAtFlag) {
|
||||
m.isAt = true
|
||||
m.Content = strings.Replace(m.Content, atFlag, "", -1)
|
||||
m.Content = strings.Replace(m.Content, macAtFlag, "", -1)
|
||||
if !m.IsSendBySelf() {
|
||||
data := strings.Split(m.Content, ":<br/>")
|
||||
m.Content = strings.Join(data[1:], "")
|
||||
m.senderInGroupUserName = data[0]
|
||||
if strings.Contains(m.Content, "@") {
|
||||
sender, err := m.Sender()
|
||||
if err == nil {
|
||||
receiver := sender.MemberList.SearchByUserName(1, m.ToUserName)
|
||||
if receiver != nil {
|
||||
displayName := receiver.First().DisplayName
|
||||
if displayName == "" {
|
||||
displayName = receiver.First().NickName
|
||||
}
|
||||
var atFlag string
|
||||
msgContent := FormatEmoji(m.Content)
|
||||
atName := FormatEmoji(displayName)
|
||||
if strings.Contains(msgContent, "\u2005") {
|
||||
atFlag = "@" + atName + "\u2005"
|
||||
} else {
|
||||
atFlag = "@" + atName
|
||||
}
|
||||
m.isAt = strings.Contains(msgContent, atFlag) || strings.HasSuffix(msgContent, atFlag)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 这块不严谨,但是只能这么干了
|
||||
m.isAt = strings.Contains(m.Content, "@") || strings.Contains(m.Content, "\u2005")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -504,7 +575,7 @@ type Card struct {
|
||||
// FriendAddMessage 好友添加消息信息内容
|
||||
type FriendAddMessage struct {
|
||||
XMLName xml.Name `xml:"msg"`
|
||||
Shortpy int `xml:"shortpy,attr"`
|
||||
Shortpy string `xml:"shortpy,attr"`
|
||||
ImageStatus int `xml:"imagestatus,attr"`
|
||||
Scene int `xml:"scene,attr"`
|
||||
PerCard int `xml:"percard,attr"`
|
||||
@ -559,13 +630,13 @@ type RevokeMsg struct {
|
||||
// SentMessage 已发送的信息
|
||||
type SentMessage struct {
|
||||
*SendMessage
|
||||
Self *Self
|
||||
self *Self
|
||||
MsgId string
|
||||
}
|
||||
|
||||
// Revoke 撤回该消息
|
||||
func (s *SentMessage) Revoke() error {
|
||||
return s.Self.RevokeMessage(s)
|
||||
return s.self.RevokeMessage(s)
|
||||
}
|
||||
|
||||
// CanRevoke 是否可以撤回该消息
|
||||
@ -575,17 +646,31 @@ func (s *SentMessage) CanRevoke() bool {
|
||||
return false
|
||||
}
|
||||
start := time.Unix(i/10000000, 0)
|
||||
return time.Now().Sub(start) < time.Minute*2
|
||||
return time.Since(start) < 2*time.Minute
|
||||
}
|
||||
|
||||
// ForwardToFriends 转发该消息给好友
|
||||
// 该方法会阻塞直到所有好友都接收到消息
|
||||
// 这里为了兼容以前的版本,默认休眠0.5秒,如果需要更快的速度,可以使用 SentMessage.ForwardToFriendsWithDelay
|
||||
func (s *SentMessage) ForwardToFriends(friends ...*Friend) error {
|
||||
return s.Self.ForwardMessageToFriends(s, friends...)
|
||||
return s.ForwardToFriendsWithDelay(time.Second/2, friends...)
|
||||
}
|
||||
|
||||
// ForwardToFriendsWithDelay 转发该消息给好友,延迟指定时间
|
||||
func (s *SentMessage) ForwardToFriendsWithDelay(delay time.Duration, friends ...*Friend) error {
|
||||
return s.self.ForwardMessageToFriends(s, delay, friends...)
|
||||
}
|
||||
|
||||
// ForwardToGroups 转发该消息给群组
|
||||
// 该方法会阻塞直到所有群组都接收到消息
|
||||
// 这里为了兼容以前的版本,默认休眠0.5秒,如果需要更快的速度,可以使用 SentMessage.ForwardToGroupsDelay
|
||||
func (s *SentMessage) ForwardToGroups(groups ...*Group) error {
|
||||
return s.Self.ForwardMessageToGroups(s, groups...)
|
||||
return s.ForwardToGroupsWithDelay(time.Second/2, groups...)
|
||||
}
|
||||
|
||||
// ForwardToGroupsWithDelay 转发该消息给群组, 延迟指定时间
|
||||
func (s *SentMessage) ForwardToGroupsWithDelay(delay time.Duration, groups ...*Group) error {
|
||||
return s.self.ForwardMessageToGroups(s, delay, groups...)
|
||||
}
|
||||
|
||||
type appmsg struct {
|
||||
@ -706,15 +791,35 @@ func (m *Message) IsAt() bool {
|
||||
// IsPaiYiPai 判断消息是否为拍一拍
|
||||
// 不要问我为什么取名为PaiYiPai,因为我也不知道取啥名字好
|
||||
func (m *Message) IsPaiYiPai() bool {
|
||||
return m.IsSystem() && strings.Contains(m.Content, "拍了拍")
|
||||
return m.IsTickled()
|
||||
}
|
||||
|
||||
// IsJoinGroup 判断是否有人加入了群聊
|
||||
func (m *Message) IsJoinGroup() bool {
|
||||
return m.IsSystem() && strings.Contains(m.Content, "加入了群聊") && m.IsSendByGroup()
|
||||
return m.IsSystem() && (strings.Contains(m.Content, "加入了群聊") || strings.Contains(m.Content, "分享的二维码加入群聊")) && m.IsSendByGroup()
|
||||
}
|
||||
|
||||
// IsTickled 判断消息是否为拍一拍
|
||||
func (m *Message) IsTickled() bool {
|
||||
return m.IsPaiYiPai()
|
||||
return m.IsSystem() && strings.Contains(m.Content, "拍了拍")
|
||||
}
|
||||
|
||||
// IsTickledMe 判断消息是否拍了拍自己
|
||||
func (m *Message) IsTickledMe() bool {
|
||||
return m.IsSystem() && strings.Count(m.Content, "拍了拍我") == 1
|
||||
}
|
||||
|
||||
// IsVoipInvite 判断消息是否为语音或视频通话邀请
|
||||
func (m *Message) IsVoipInvite() bool {
|
||||
return m.MsgType == MsgTypeVoipInvite
|
||||
}
|
||||
|
||||
// Bot 返回当前消息所属的Bot
|
||||
func (m *Message) Bot() *Bot {
|
||||
return m.bot
|
||||
}
|
||||
|
||||
// Owner 返回当前消息的拥有者
|
||||
func (m *Message) Owner() *Self {
|
||||
return m.Bot().self
|
||||
}
|
||||
|
@ -11,13 +11,6 @@ type MessageDispatcher interface {
|
||||
Dispatch(msg *Message)
|
||||
}
|
||||
|
||||
// DispatchMessage 跟 MessageDispatcher 结合封装成 MessageHandler
|
||||
func DispatchMessage(dispatcher MessageDispatcher) func(msg *Message) {
|
||||
return func(msg *Message) { dispatcher.Dispatch(msg) }
|
||||
}
|
||||
|
||||
// MessageDispatcher impl
|
||||
|
||||
// MessageContextHandler 消息处理函数
|
||||
type MessageContextHandler func(ctx *MessageContext)
|
||||
|
||||
@ -85,12 +78,13 @@ type matchNode struct {
|
||||
type matchNodes []*matchNode
|
||||
|
||||
// MessageMatchDispatcher impl MessageDispatcher interface
|
||||
// dispatcher := NewMessageMatchDispatcher()
|
||||
// dispatcher.OnText(func(msg *Message){
|
||||
// msg.ReplyText("hello")
|
||||
// })
|
||||
// bot := DefaultBot()
|
||||
// bot.MessageHandler = DispatchMessage(dispatcher)
|
||||
//
|
||||
// dispatcher := NewMessageMatchDispatcher()
|
||||
// dispatcher.OnText(func(msg *Message){
|
||||
// msg.ReplyText("hello")
|
||||
// })
|
||||
// bot := DefaultBot()
|
||||
// bot.MessageHandler = DispatchMessage(dispatcher)
|
||||
type MessageMatchDispatcher struct {
|
||||
async bool
|
||||
matchNodes matchNodes
|
||||
@ -224,14 +218,22 @@ func (m *MessageMatchDispatcher) OnGroupByGroupName(groupName string, handlers .
|
||||
m.OnUser(f, handlers...)
|
||||
}
|
||||
|
||||
// AsMessageHandler 将MessageMatchDispatcher转换为MessageHandler
|
||||
func (m *MessageMatchDispatcher) AsMessageHandler() MessageHandler {
|
||||
return func(msg *Message) {
|
||||
m.Dispatch(msg)
|
||||
}
|
||||
}
|
||||
|
||||
type MessageSenderMatchFunc func(user *User) bool
|
||||
|
||||
// SenderMatchFunc 抽象的匹配发送者特征的处理函数
|
||||
// dispatcher := NewMessageMatchDispatcher()
|
||||
// matchFuncList := MatchFuncList(SenderFriendRequired(), SenderNickNameContainsMatchFunc("多吃点苹果"))
|
||||
// dispatcher.RegisterHandler(matchFuncList, func(ctx *MessageContext) {
|
||||
// do your own business
|
||||
// })
|
||||
//
|
||||
// dispatcher := NewMessageMatchDispatcher()
|
||||
// matchFuncList := MatchFuncList(SenderFriendRequired(), SenderNickNameContainsMatchFunc("多吃点苹果"))
|
||||
// dispatcher.RegisterHandler(matchFuncList, func(ctx *MessageContext) {
|
||||
// do your own business
|
||||
// })
|
||||
func SenderMatchFunc(matchFuncs ...MessageSenderMatchFunc) MatchFunc {
|
||||
return func(message *Message) bool {
|
||||
sender, err := message.Sender()
|
||||
|
@ -3,6 +3,7 @@ package openwechat
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ExampleMessageType_output() {
|
||||
@ -43,3 +44,19 @@ func ExampleMessageType_output() {
|
||||
// 收到一条未知消息(type 6): MessageType(6)
|
||||
// 收到一条未知消息(type 51): MessageType(51)
|
||||
}
|
||||
|
||||
func TestMessage_FriendAddMessageContent(t *testing.T) {
|
||||
m := &Message{
|
||||
MsgType: 37,
|
||||
FromUserName: "fmessage",
|
||||
Content: "<msg fromusername=\"user1\" encryptusername=\"123123131@stranger\" fromnickname=\"nickname1\" content=\"test11111\" shortpy=\"MMHDNLXX\" imagestatus=\"3\" scene=\"3\" country=\"CN\" province=\"Zhejiang\" city=\"Hangzhou\" sign=\"Life is a struggle.\" percard=\"1\" sex=\"1\" alias=\"1111\" weibo=\"\" albumflag=\"0\" albumstyle=\"0\" albumbgimgid=\"\" snsflag=\"273\" snsbgimgid=\"http://shmmsns.qpic.cn/mbsFs/0\" snsbgobjectid=\"123123131\" mhash=\"1231231\" mfullhash=\"1231231\" bigheadimgurl=\"http://wx.qlogo.cn/mmhead/ver_1/1231231/0\" smallheadimgurl=\"http://wx.qlogo.cn/mmhead/ver_1/1231231/132\" ticket=\"1231231@stranger\" opcode=\"2\" googlecontact=\"\" qrticket=\"\" chatroomusername=\"123@chatroom\" sourceusername=\"\" sourcenickname=\"\" sharecardusername=\"\" sharecardnickname=\"\" cardversion=\"\" extflag=\"0\"><brandlist count=\"0\" ver=\"759291380\"></brandlist></msg>",
|
||||
}
|
||||
fm, err := m.FriendAddMessageContent()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if fm.Shortpy != "MMHDNLXX" {
|
||||
t.Error("unexpected shorty:", fm.Shortpy)
|
||||
}
|
||||
}
|
||||
|
28
mode.go
28
mode.go
@ -10,18 +10,28 @@ import (
|
||||
type Mode interface {
|
||||
GetLoginUUID(client *Client) (*http.Response, error)
|
||||
GetLoginInfo(client *Client, path string) (*http.Response, error)
|
||||
PushLogin(client *Client, uin int64) (*http.Response, error)
|
||||
}
|
||||
|
||||
var (
|
||||
Normal Mode = normalMode{}
|
||||
// normal 网页版模式
|
||||
normal Mode = normalMode{}
|
||||
|
||||
// Desktop
|
||||
// Deprecated
|
||||
Desktop Mode = desktopMode{}
|
||||
// desktop 桌面模式,uos electron套壳
|
||||
desktop Mode = desktopMode{}
|
||||
)
|
||||
|
||||
type normalMode struct{}
|
||||
|
||||
func (n normalMode) PushLogin(client *Client, uin int64) (*http.Response, error) {
|
||||
path, _ := url.Parse(client.Domain.BaseHost() + webwxpushloginurl)
|
||||
params := url.Values{}
|
||||
params.Add("uin", strconv.FormatInt(uin, 10))
|
||||
path.RawQuery = params.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func (n normalMode) GetLoginUUID(client *Client) (*http.Response, error) {
|
||||
path, _ := url.Parse(jslogin)
|
||||
params := url.Values{}
|
||||
@ -67,3 +77,13 @@ func (n desktopMode) GetLoginInfo(client *Client, path string) (*http.Response,
|
||||
req.Header.Add("extspam", uosPatchExtspam)
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func (n desktopMode) PushLogin(client *Client, uin int64) (*http.Response, error) {
|
||||
path, _ := url.Parse(client.Domain.BaseHost() + webwxpushloginurl)
|
||||
params := url.Values{}
|
||||
params.Add("uin", strconv.FormatInt(uin, 10))
|
||||
params.Add("mod", "desktop")
|
||||
path.RawQuery = params.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
return client.Do(req)
|
||||
}
|
||||
|
31
parser.go
31
parser.go
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"math/rand"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@ -37,23 +38,6 @@ func GetRandomDeviceId() string {
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func getWebWxDataTicket(cookies []*http.Cookie) string {
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "webwx_data_ticket" {
|
||||
return cookie.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getTotalDuration(delay ...time.Duration) time.Duration {
|
||||
var total time.Duration
|
||||
for _, d := range delay {
|
||||
total += d
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// GetFileContentType 获取文件上传的类型
|
||||
func GetFileContentType(file multipart.File) (string, error) {
|
||||
data := make([]byte, 512)
|
||||
@ -80,20 +64,21 @@ const (
|
||||
// 微信匹配文件类型策略
|
||||
func getMessageType(filename string) string {
|
||||
ext := getFileExt(filename)
|
||||
if imageType[ext] {
|
||||
if _, ok := imageType[ext]; ok {
|
||||
return pic
|
||||
} else if ext == videoType {
|
||||
}
|
||||
if ext == videoType {
|
||||
return video
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
func scanXml(resp *http.Response, v interface{}) error {
|
||||
return xml.NewDecoder(resp.Body).Decode(v)
|
||||
func scanXml(reader io.Reader, v interface{}) error {
|
||||
return xml.NewDecoder(reader).Decode(v)
|
||||
}
|
||||
|
||||
func scanJson(resp *http.Response, v interface{}) error {
|
||||
return json.NewDecoder(resp.Body).Decode(v)
|
||||
func scanJson(reader io.Reader, v interface{}) error {
|
||||
return json.NewDecoder(reader).Decode(v)
|
||||
}
|
||||
|
||||
func stringToByte(s string) []byte {
|
||||
|
454
relations.go
454
relations.go
@ -2,50 +2,49 @@ package openwechat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Friend struct{ *User }
|
||||
|
||||
// implement fmt.Stringer
|
||||
func (f Friend) String() string {
|
||||
return fmt.Sprintf("<Friend:%s>", f.NickName)
|
||||
func (f *Friend) String() string {
|
||||
display := f.NickName
|
||||
if f.RemarkName != "" {
|
||||
display = f.RemarkName
|
||||
}
|
||||
return fmt.Sprintf("<Friend:%s>", display)
|
||||
}
|
||||
|
||||
// SetRemarkName 重命名当前好友
|
||||
func (f *Friend) SetRemarkName(name string) error {
|
||||
return f.Self.SetRemarkNameToFriend(f, name)
|
||||
}
|
||||
|
||||
// SendMsg 发送自定义消息
|
||||
func (f *Friend) SendMsg(msg *SendMessage) (*SentMessage, error) {
|
||||
return f.Self.SendMessageToFriend(f, msg)
|
||||
return f.self.SetRemarkNameToFriend(f, name)
|
||||
}
|
||||
|
||||
// SendText 发送文本消息
|
||||
func (f *Friend) SendText(content string) (*SentMessage, error) {
|
||||
return f.Self.SendTextToFriend(f, content)
|
||||
return f.self.SendTextToFriend(f, content)
|
||||
}
|
||||
|
||||
// SendImage 发送图片消息
|
||||
func (f *Friend) SendImage(file *os.File) (*SentMessage, error) {
|
||||
return f.Self.SendImageToFriend(f, file)
|
||||
func (f *Friend) SendImage(file io.Reader) (*SentMessage, error) {
|
||||
return f.self.SendImageToFriend(f, file)
|
||||
}
|
||||
|
||||
// SendVideo 发送视频消息
|
||||
func (f *Friend) SendVideo(file *os.File) (*SentMessage, error) {
|
||||
return f.Self.SendVideoToFriend(f, file)
|
||||
func (f *Friend) SendVideo(file io.Reader) (*SentMessage, error) {
|
||||
return f.self.SendVideoToFriend(f, file)
|
||||
}
|
||||
|
||||
// SendFile 发送文件消息
|
||||
func (f *Friend) SendFile(file *os.File) (*SentMessage, error) {
|
||||
return f.Self.SendFileToFriend(f, file)
|
||||
func (f *Friend) SendFile(file io.Reader) (*SentMessage, error) {
|
||||
return f.self.SendFileToFriend(f, file)
|
||||
}
|
||||
|
||||
// AddIntoGroup 拉该好友入群
|
||||
func (f *Friend) AddIntoGroup(groups ...*Group) error {
|
||||
return f.Self.AddFriendIntoManyGroups(f, groups...)
|
||||
return f.self.AddFriendIntoManyGroups(f, groups...)
|
||||
}
|
||||
|
||||
type Friends []*Friend
|
||||
@ -58,7 +57,7 @@ func (f Friends) Count() int {
|
||||
// First 获取第一个好友
|
||||
func (f Friends) First() *Friend {
|
||||
if f.Count() > 0 {
|
||||
return f[0]
|
||||
return f.Sort()[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -66,7 +65,7 @@ func (f Friends) First() *Friend {
|
||||
// Last 获取最后一个好友
|
||||
func (f Friends) Last() *Friend {
|
||||
if f.Count() > 0 {
|
||||
return f[f.Count()-1]
|
||||
return f.Sort()[f.Count()-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -87,148 +86,101 @@ func (f Friends) SearchByRemarkName(limit int, remarkName string) (results Frien
|
||||
}
|
||||
|
||||
// Search 根据自定义条件查找好友
|
||||
func (f Friends) Search(limit int, condFuncList ...func(friend *Friend) bool) (results Friends) {
|
||||
if condFuncList == nil {
|
||||
return f
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = f.Count()
|
||||
}
|
||||
for _, member := range f {
|
||||
if results.Count() == limit {
|
||||
break
|
||||
}
|
||||
var passCount int
|
||||
for _, condFunc := range condFuncList {
|
||||
if condFunc(member) {
|
||||
passCount++
|
||||
func (f Friends) Search(limit int, searchFuncList ...func(friend *Friend) bool) (results Friends) {
|
||||
return f.AsMembers().Search(limit, func(user *User) bool {
|
||||
var friend = &Friend{user}
|
||||
for _, searchFunc := range searchFuncList {
|
||||
if !searchFunc(friend) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if passCount == len(condFuncList) {
|
||||
results = append(results, member)
|
||||
}
|
||||
}
|
||||
return
|
||||
return true
|
||||
}).Friends()
|
||||
}
|
||||
|
||||
// SendMsg 向slice的好友依次发送消息
|
||||
func (f Friends) SendMsg(msg *SendMessage, delay ...time.Duration) error {
|
||||
total := getTotalDuration(delay...)
|
||||
var (
|
||||
sentMessage *SentMessage
|
||||
err error
|
||||
self *Self
|
||||
)
|
||||
// AsMembers 将群组转换为用户列表
|
||||
func (f Friends) AsMembers() Members {
|
||||
var members = make(Members, 0, f.Count())
|
||||
for _, friend := range f {
|
||||
self = friend.Self
|
||||
time.Sleep(total)
|
||||
if sentMessage != nil {
|
||||
err = self.ForwardMessageToFriends(sentMessage, f...)
|
||||
return err
|
||||
}
|
||||
if sentMessage, err = friend.SendMsg(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
members = append(members, friend.User)
|
||||
}
|
||||
return nil
|
||||
return members
|
||||
}
|
||||
|
||||
// Sort 对好友进行排序
|
||||
func (f Friends) Sort() Friends {
|
||||
return f.AsMembers().Sort().Friends()
|
||||
}
|
||||
|
||||
// Uniq 对好友进行去重
|
||||
func (f Friends) Uniq() Friends {
|
||||
return f.AsMembers().Uniq().Friends()
|
||||
}
|
||||
|
||||
// SendText 向slice的好友依次发送文本消息
|
||||
func (f Friends) SendText(text string, delay ...time.Duration) error {
|
||||
total := getTotalDuration(delay...)
|
||||
var (
|
||||
sentMessage *SentMessage
|
||||
err error
|
||||
self *Self
|
||||
)
|
||||
for _, friend := range f {
|
||||
self = friend.Self
|
||||
time.Sleep(total)
|
||||
if sentMessage != nil {
|
||||
err = self.ForwardMessageToFriends(sentMessage, f...)
|
||||
return err
|
||||
}
|
||||
if sentMessage, err = friend.SendText(text); err != nil {
|
||||
return err
|
||||
}
|
||||
func (f Friends) SendText(text string, delays ...time.Duration) error {
|
||||
if f.Count() == 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
var delay time.Duration
|
||||
if len(delays) > 0 {
|
||||
delay = delays[0]
|
||||
}
|
||||
self := f.First().self
|
||||
return self.SendTextToFriends(text, delay, f...)
|
||||
}
|
||||
|
||||
// SendImage 向slice的好友依次发送图片消息
|
||||
func (f Friends) SendImage(file *os.File, delay ...time.Duration) error {
|
||||
total := getTotalDuration(delay...)
|
||||
var (
|
||||
sentMessage *SentMessage
|
||||
err error
|
||||
self *Self
|
||||
)
|
||||
for _, friend := range f {
|
||||
self = friend.Self
|
||||
time.Sleep(total)
|
||||
if sentMessage != nil {
|
||||
err = self.ForwardMessageToFriends(sentMessage, f...)
|
||||
return err
|
||||
}
|
||||
if sentMessage, err = friend.SendImage(file); err != nil {
|
||||
return err
|
||||
}
|
||||
func (f Friends) SendImage(file io.Reader, delays ...time.Duration) error {
|
||||
if f.Count() == 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
var delay time.Duration
|
||||
if len(delays) > 0 {
|
||||
delay = delays[0]
|
||||
}
|
||||
self := f.First().self
|
||||
return self.SendImageToFriends(file, delay, f...)
|
||||
}
|
||||
|
||||
// SendFile 群发文件
|
||||
func (f Friends) SendFile(file *os.File, delay ...time.Duration) error {
|
||||
total := getTotalDuration(delay...)
|
||||
var (
|
||||
sentMessage *SentMessage
|
||||
err error
|
||||
self *Self
|
||||
)
|
||||
for _, friend := range f {
|
||||
self = friend.Self
|
||||
time.Sleep(total)
|
||||
if sentMessage != nil {
|
||||
err = self.ForwardMessageToFriends(sentMessage, f...)
|
||||
return err
|
||||
}
|
||||
if sentMessage, err = friend.SendFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
func (f Friends) SendFile(file io.Reader, delay ...time.Duration) error {
|
||||
if f.Count() == 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
var d time.Duration
|
||||
if len(delay) > 0 {
|
||||
d = delay[0]
|
||||
}
|
||||
self := f.First().self
|
||||
return self.SendFileToFriends(file, d, f...)
|
||||
}
|
||||
|
||||
type Group struct{ *User }
|
||||
|
||||
// implement fmt.Stringer
|
||||
func (g Group) String() string {
|
||||
func (g *Group) String() string {
|
||||
return fmt.Sprintf("<Group:%s>", g.NickName)
|
||||
}
|
||||
|
||||
// SendMsg 发行消息给当前的群组
|
||||
func (g *Group) SendMsg(msg *SendMessage) (*SentMessage, error) {
|
||||
return g.Self.SendMessageToGroup(g, msg)
|
||||
}
|
||||
|
||||
// SendText 发行文本消息给当前的群组
|
||||
func (g *Group) SendText(content string) (*SentMessage, error) {
|
||||
return g.Self.SendTextToGroup(g, content)
|
||||
return g.self.SendTextToGroup(g, content)
|
||||
}
|
||||
|
||||
// SendImage 发行图片消息给当前的群组
|
||||
func (g *Group) SendImage(file *os.File) (*SentMessage, error) {
|
||||
return g.Self.SendImageToGroup(g, file)
|
||||
func (g *Group) SendImage(file io.Reader) (*SentMessage, error) {
|
||||
return g.self.SendImageToGroup(g, file)
|
||||
}
|
||||
|
||||
// SendVideo 发行视频消息给当前的群组
|
||||
func (g *Group) SendVideo(file *os.File) (*SentMessage, error) {
|
||||
return g.Self.SendVideoToGroup(g, file)
|
||||
func (g *Group) SendVideo(file io.Reader) (*SentMessage, error) {
|
||||
return g.self.SendVideoToGroup(g, file)
|
||||
}
|
||||
|
||||
// SendFile 发送文件给当前的群组
|
||||
func (g *Group) SendFile(file *os.File) (*SentMessage, error) {
|
||||
return g.Self.SendFileToGroup(g, file)
|
||||
func (g *Group) SendFile(file io.Reader) (*SentMessage, error) {
|
||||
return g.self.SendFileToGroup(g, file)
|
||||
}
|
||||
|
||||
// Members 获取所有的群成员
|
||||
@ -236,25 +188,48 @@ func (g *Group) Members() (Members, error) {
|
||||
if err := g.Detail(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.MemberList.init(g.Self)
|
||||
g.MemberList.init(g.self)
|
||||
return g.MemberList, nil
|
||||
}
|
||||
|
||||
// AddFriendsIn 拉好友入群
|
||||
func (g *Group) AddFriendsIn(friends ...*Friend) error {
|
||||
return g.Self.AddFriendsIntoGroup(g, friends...)
|
||||
friends = Friends(friends).Uniq()
|
||||
return g.self.AddFriendsIntoGroup(g, friends...)
|
||||
}
|
||||
|
||||
// RemoveMembers 从群聊中移除用户
|
||||
// Deprecated
|
||||
// 无论是网页版,还是程序上都不起作用
|
||||
func (g *Group) RemoveMembers(members Members) error {
|
||||
return g.Self.RemoveMemberFromGroup(g, members)
|
||||
return g.self.RemoveMemberFromGroup(g, members)
|
||||
}
|
||||
|
||||
// Rename 群组重命名
|
||||
func (g *Group) Rename(name string) error {
|
||||
return g.Self.RenameGroup(g, name)
|
||||
return g.self.RenameGroup(g, name)
|
||||
}
|
||||
|
||||
// SearchMemberByUsername 根据用户名查找群成员
|
||||
func (g *Group) SearchMemberByUsername(username string) (*User, error) {
|
||||
if g.MemberList.Count() == 0 {
|
||||
if _, err := g.Members(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
members := g.MemberList.SearchByUserName(1, username)
|
||||
// 如果此时本地查不到, 那么该成员可能是新加入的
|
||||
if members.Count() == 0 {
|
||||
if _, err := g.Members(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 再次尝试获取
|
||||
members = g.MemberList.SearchByUserName(1, username)
|
||||
if members.Count() == 0 {
|
||||
return nil, ErrNoSuchUserFoundError
|
||||
}
|
||||
return members.First(), nil
|
||||
}
|
||||
|
||||
type Groups []*Group
|
||||
@ -267,7 +242,7 @@ func (g Groups) Count() int {
|
||||
// First 获取第一个群组
|
||||
func (g Groups) First() *Group {
|
||||
if g.Count() > 0 {
|
||||
return g[0]
|
||||
return g.Sort()[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -275,75 +250,48 @@ func (g Groups) First() *Group {
|
||||
// Last 获取最后一个群组
|
||||
func (g Groups) Last() *Group {
|
||||
if g.Count() > 0 {
|
||||
return g[g.Count()-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendMsg 向群组依次发送消息, 支持发送延迟
|
||||
func (g Groups) SendMsg(msg *SendMessage, delay ...time.Duration) error {
|
||||
total := getTotalDuration(delay...)
|
||||
var (
|
||||
sentMessage *SentMessage
|
||||
err error
|
||||
self *Self
|
||||
)
|
||||
for _, group := range g {
|
||||
self = group.Self
|
||||
time.Sleep(total)
|
||||
if sentMessage != nil {
|
||||
err = self.ForwardMessageToGroups(sentMessage, g...)
|
||||
return err
|
||||
}
|
||||
if sentMessage, err = group.SendMsg(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return g.Sort()[g.Count()-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendText 向群组依次发送文本消息, 支持发送延迟
|
||||
func (g Groups) SendText(text string, delay ...time.Duration) error {
|
||||
total := getTotalDuration(delay...)
|
||||
var (
|
||||
sentMessage *SentMessage
|
||||
err error
|
||||
self *Self
|
||||
)
|
||||
for _, group := range g {
|
||||
self = group.Self
|
||||
time.Sleep(total)
|
||||
if sentMessage != nil {
|
||||
err = self.ForwardMessageToGroups(sentMessage, g...)
|
||||
return err
|
||||
}
|
||||
if sentMessage, err = group.SendText(text); err != nil {
|
||||
return err
|
||||
}
|
||||
if g.Count() == 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
var d time.Duration
|
||||
if len(delay) > 0 {
|
||||
d = delay[0]
|
||||
}
|
||||
self := g.First().self
|
||||
return self.SendTextToGroups(text, d, g...)
|
||||
}
|
||||
|
||||
// SendImage 向群组依次发送图片消息, 支持发送延迟
|
||||
func (g Groups) SendImage(file *os.File, delay ...time.Duration) error {
|
||||
total := getTotalDuration(delay...)
|
||||
var (
|
||||
sentMessage *SentMessage
|
||||
err error
|
||||
self *Self
|
||||
)
|
||||
for _, group := range g {
|
||||
self = group.Self
|
||||
time.Sleep(total)
|
||||
if sentMessage != nil {
|
||||
err = self.ForwardMessageToGroups(sentMessage, g...)
|
||||
return err
|
||||
}
|
||||
if sentMessage, err = group.SendImage(file); err != nil {
|
||||
return err
|
||||
}
|
||||
func (g Groups) SendImage(file io.Reader, delay ...time.Duration) error {
|
||||
if g.Count() == 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
var d time.Duration
|
||||
if len(delay) > 0 {
|
||||
d = delay[0]
|
||||
}
|
||||
self := g.First().self
|
||||
return self.SendImageToGroups(file, d, g...)
|
||||
}
|
||||
|
||||
// SendFile 向群组依次发送文件消息, 支持发送延迟
|
||||
func (g Groups) SendFile(file io.Reader, delay ...time.Duration) error {
|
||||
if g.Count() == 0 {
|
||||
return nil
|
||||
}
|
||||
var d time.Duration
|
||||
if len(delay) > 0 {
|
||||
d = delay[0]
|
||||
}
|
||||
self := g.First().self
|
||||
return self.SendFileToGroups(file, d, g...)
|
||||
}
|
||||
|
||||
// SearchByUserName 根据用户名查找群组
|
||||
@ -356,40 +304,42 @@ func (g Groups) SearchByNickName(limit int, nickName string) (results Groups) {
|
||||
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 根据自定义条件查找群组
|
||||
func (g Groups) Search(limit int, condFuncList ...func(group *Group) bool) (results Groups) {
|
||||
if condFuncList == nil {
|
||||
return g
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = g.Count()
|
||||
}
|
||||
for _, member := range g {
|
||||
if results.Count() == limit {
|
||||
break
|
||||
}
|
||||
var passCount int
|
||||
for _, condFunc := range condFuncList {
|
||||
if condFunc(member) {
|
||||
passCount++
|
||||
func (g Groups) Search(limit int, searchFuncList ...func(group *Group) bool) (results Groups) {
|
||||
return g.AsMembers().Search(limit, func(user *User) bool {
|
||||
var group = &Group{user}
|
||||
for _, searchFunc := range searchFuncList {
|
||||
if !searchFunc(group) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if passCount == len(condFuncList) {
|
||||
results = append(results, member)
|
||||
}
|
||||
return true
|
||||
}).Groups()
|
||||
}
|
||||
|
||||
// AsMembers 将群组列表转换为用户列表
|
||||
func (g Groups) AsMembers() Members {
|
||||
var members = make(Members, 0, g.Count())
|
||||
for _, group := range g {
|
||||
members = append(members, group.User)
|
||||
}
|
||||
return
|
||||
return members
|
||||
}
|
||||
|
||||
// Sort 对群组进行排序
|
||||
func (g Groups) Sort() Groups {
|
||||
return g.AsMembers().Sort().Groups()
|
||||
}
|
||||
|
||||
// Uniq 对群组进行去重
|
||||
func (g Groups) Uniq() Groups {
|
||||
return g.AsMembers().Uniq().Groups()
|
||||
}
|
||||
|
||||
// Mp 公众号对象
|
||||
type Mp struct{ *User }
|
||||
|
||||
func (m Mp) String() string {
|
||||
func (m *Mp) String() string {
|
||||
return fmt.Sprintf("<Mp:%s>", m.NickName)
|
||||
}
|
||||
|
||||
@ -404,7 +354,7 @@ func (m Mps) Count() int {
|
||||
// First 获取第一个
|
||||
func (m Mps) First() *Mp {
|
||||
if m.Count() > 0 {
|
||||
return m[0]
|
||||
return m.Sort()[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -412,34 +362,41 @@ func (m Mps) First() *Mp {
|
||||
// Last 获取最后一个
|
||||
func (m Mps) Last() *Mp {
|
||||
if m.Count() > 0 {
|
||||
return m[m.Count()-1]
|
||||
return m.Sort()[m.Count()-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search 根据自定义条件查找
|
||||
func (m Mps) Search(limit int, condFuncList ...func(group *Mp) bool) (results Mps) {
|
||||
if condFuncList == nil {
|
||||
return m
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = m.Count()
|
||||
}
|
||||
for _, member := range m {
|
||||
if results.Count() == limit {
|
||||
break
|
||||
}
|
||||
var passCount int
|
||||
for _, condFunc := range condFuncList {
|
||||
if condFunc(member) {
|
||||
passCount++
|
||||
func (m Mps) Search(limit int, searchFuncList ...func(group *Mp) bool) (results Mps) {
|
||||
return m.AsMembers().Search(limit, func(user *User) bool {
|
||||
var mp = &Mp{user}
|
||||
for _, searchFunc := range searchFuncList {
|
||||
if !searchFunc(mp) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if passCount == len(condFuncList) {
|
||||
results = append(results, member)
|
||||
}
|
||||
return true
|
||||
}).MPs()
|
||||
}
|
||||
|
||||
// AsMembers 将公众号列表转换为用户列表
|
||||
func (m Mps) AsMembers() Members {
|
||||
var members = make(Members, 0, m.Count())
|
||||
for _, mp := range m {
|
||||
members = append(members, mp.User)
|
||||
}
|
||||
return
|
||||
return members
|
||||
}
|
||||
|
||||
// Sort 对公众号进行排序
|
||||
func (m Mps) Sort() Mps {
|
||||
return m.AsMembers().Sort().MPs()
|
||||
}
|
||||
|
||||
// Uniq 对公众号进行去重
|
||||
func (m Mps) Uniq() Mps {
|
||||
return m.AsMembers().Uniq().MPs()
|
||||
}
|
||||
|
||||
// SearchByUserName 根据用户名查找
|
||||
@ -454,17 +411,17 @@ func (m Mps) SearchByNickName(limit int, nickName string) (results Mps) {
|
||||
|
||||
// SendText 发送文本消息给公众号
|
||||
func (m *Mp) SendText(content string) (*SentMessage, error) {
|
||||
return m.Self.SendTextToMp(m, content)
|
||||
return m.self.SendTextToMp(m, content)
|
||||
}
|
||||
|
||||
// SendImage 发送图片消息给公众号
|
||||
func (m *Mp) SendImage(file *os.File) (*SentMessage, error) {
|
||||
return m.Self.SendImageToMp(m, file)
|
||||
func (m *Mp) SendImage(file io.Reader) (*SentMessage, error) {
|
||||
return m.self.SendImageToMp(m, file)
|
||||
}
|
||||
|
||||
// SendFile 发送文件消息给公众号
|
||||
func (m *Mp) SendFile(file *os.File) (*SentMessage, error) {
|
||||
return m.Self.SendFileToMp(m, file)
|
||||
func (m *Mp) SendFile(file io.Reader) (*SentMessage, error) {
|
||||
return m.self.SendFileToMp(m, file)
|
||||
}
|
||||
|
||||
// GetByUsername 根据username查询一个Friend
|
||||
@ -487,11 +444,6 @@ func (g Groups) GetByUsername(username string) *Group {
|
||||
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
|
||||
func (g Groups) GetByNickName(nickname string) *Group {
|
||||
return g.SearchByNickName(1, nickname).First()
|
||||
@ -506,3 +458,19 @@ func (m Mps) GetByNickName(nickname string) *Mp {
|
||||
func (m Mps) GetByUserName(username string) *Mp {
|
||||
return m.SearchByUserName(1, username).First()
|
||||
}
|
||||
|
||||
// search 根据自定义条件查找
|
||||
func search(searchList Members, limit int, searchFunc func(group *User) bool) (results Members) {
|
||||
if limit <= 0 {
|
||||
limit = searchList.Count()
|
||||
}
|
||||
for _, member := range searchList {
|
||||
if results.Count() == limit {
|
||||
break
|
||||
}
|
||||
if searchFunc(member) {
|
||||
results = append(results, member)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
25
serializer.go
Normal file
25
serializer.go
Normal file
@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
105
source/bot.md
105
source/bot.md
@ -60,7 +60,7 @@ func main() {
|
||||
|
||||
|
||||
|
||||
#### 普通登录
|
||||
#### 扫码登录
|
||||
|
||||
上面的准备工作做完了,下面就可以登录,直接调用`Bot.Login`即可。
|
||||
|
||||
@ -68,6 +68,8 @@ func main() {
|
||||
bot.Login()
|
||||
```
|
||||
|
||||
`Login`方法会阻塞当前 goroutine,直到登录成功或者失败。
|
||||
|
||||
登录会返回一个`error`,即登录失败的原因。
|
||||
|
||||
|
||||
@ -82,21 +84,74 @@ bot.Login()
|
||||
// 创建热存储容器对象
|
||||
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
|
||||
|
||||
defer reloadStorage.Close()
|
||||
|
||||
// 执行热登录
|
||||
bot.HotLogin(reloadStorage)
|
||||
```
|
||||
|
||||
`HotLogin`需要接受一个`热存储容器对象`来调用。`热存储容器`用来保存登录的会话信息,本质是一个接口类型
|
||||
|
||||
我们第一次进行热登录的时候,因为我们的`热存储容器`是空的,所以这时候会发生错误。
|
||||
|
||||
我们只需要在`HotLogin`增加一个参数,让它在失败后执行扫码登录即可
|
||||
|
||||
```go
|
||||
bot.HotLogin(reloadStorage, openwechat.NewRetryLoginOption())
|
||||
```
|
||||
|
||||
当扫码登录成功后,会将会话信息写入到`热存储容器`中,下次再执行热登录的时候就会从`热存储容器`中读取会话信息,直接登录成功。
|
||||
|
||||
|
||||
```go
|
||||
// 热登陆存储接口
|
||||
type HotReloadStorage io.ReadWriteCloser
|
||||
type HotReloadStorage io.ReadWriter
|
||||
```
|
||||
|
||||
`NewJsonFileHotReloadStorage`简单实现了该接口,它采用`JSON`的方式存储会话信息。
|
||||
|
||||
实现这个接口,实现你自己的存储方式。
|
||||
|
||||
#### 免扫码登录
|
||||
|
||||
目前热登录有一点缺点就是它的有效期很短(具体多久我也不知道)。
|
||||
|
||||
我们平常在pc上登录微信的时候,通常只需要登录一次,第二次就会在微信上有一个确认登录的按钮,点击确认就会往手机上发送一个确认登录的请求,这样就可以免扫码登录了。
|
||||
|
||||
openwechat也提供了这样的功能。
|
||||
|
||||
```go
|
||||
bot.PushLogin(storage HotReloadStorage, opts ...openwechat.BotLoginOption) error
|
||||
```
|
||||
|
||||
`PushLogin`需要传入一个`热存储容器`,和一些可选参数。
|
||||
|
||||
`HotReloadStorage` 跟上面一样,用来保存会话信息,必要参数。
|
||||
|
||||
`openwechat.BotLoginOption`是一个可选参数,用来设置一些额外的行为。
|
||||
|
||||
目前有下面几个可选参数:
|
||||
|
||||
```go
|
||||
// NewSyncReloadDataLoginOption 登录成功后定时同步热存储容器数据
|
||||
func NewSyncReloadDataLoginOption(duration time.Duration) BotLoginOption
|
||||
|
||||
// NewRetryLoginOption 登录失败后进行扫码登录
|
||||
func NewRetryLoginOption() BotLoginOption
|
||||
```
|
||||
|
||||
注意:如果是第一次登录,``PushLogin`` 一定会失败的,因为我们的`HotReloadStorage`里面没有会话信息,你需要设置失败会进行扫码登录。
|
||||
|
||||
```go
|
||||
bot := openwechat.DefaultBot()
|
||||
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
|
||||
defer reloadStorage.Close()
|
||||
err = bot.PushLogin(reloadStorage, openwechat.NewRetryLoginOption())
|
||||
```
|
||||
|
||||
这样当第一次登录失败的时候,会自动执行扫码登录。
|
||||
|
||||
扫码登录成功后,会自动保存会话信息到`HotReloadStorage`,下次登录就可以直接使用`PushLogin`了,就会往手机上发送确认登录的请求。
|
||||
|
||||
|
||||
### 扫码回调
|
||||
@ -106,13 +161,21 @@ type HotReloadStorage io.ReadWriteCloser
|
||||
通过对`bot`对象绑定扫码回调即可实现对应的功能。
|
||||
|
||||
```go
|
||||
bot.ScanCallBack = func(body []byte) { fmt.Println(string(body)) }
|
||||
bot.ScanCallBack = func(body openwechat.CheckLoginResponse) { fmt.Println(string(body)) }
|
||||
```
|
||||
|
||||
用户扫码后,body里面会携带用户的头像信息。
|
||||
|
||||
**注**:绑定扫码回调须在登录前执行。
|
||||
|
||||
`CheckLoginResponse` 是一个`[]byte`包装类型, 扫码成功后可以通过该类型获取用户的头像信息。
|
||||
|
||||
```go
|
||||
type CheckLoginResponse []byte
|
||||
|
||||
func (c CheckLoginResponse) Avatar() (string, error)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 登录回调
|
||||
@ -120,13 +183,13 @@ bot.ScanCallBack = func(body []byte) { fmt.Println(string(body)) }
|
||||
对`bot`对象绑定登录
|
||||
|
||||
```go
|
||||
bot.LoginCallBack = func(body []byte) {
|
||||
bot.LoginCallBack = func(body openwechat.CheckLoginResponse) {
|
||||
fmt.Println(string(body))
|
||||
// to do your business
|
||||
}
|
||||
```
|
||||
|
||||
登录回调的参数就是当前客户端需要跳转的链接,可以不用关心它。
|
||||
登录回调的参数就是当前客户端需要跳转的链接,用户可以不用关心它。(其实可以拿来做一些骚操作😈)
|
||||
|
||||
登录回调函数可以当做一个信号处理,表示当前扫码登录的用户已经确认登录。
|
||||
|
||||
@ -171,12 +234,14 @@ bot.MessageHandler = func(msg *openwechat.Message) {
|
||||
dispatcher := openwechat.NewMessageMatchDispatcher()
|
||||
|
||||
// 只处理消息类型为文本类型的消息
|
||||
dispatcher.OnText(func(msg *Message){
|
||||
dispatcher.OnText(func(ctx *openwechat.MessageContext){
|
||||
msg := ctx.Message
|
||||
fmt.Println("Text: ", msg.Content)
|
||||
msg.ReplyText("hello")
|
||||
})
|
||||
|
||||
// 注册消息回调函数
|
||||
bot.MessageHandler = openwechat.DispatchMessage(dispatcher)
|
||||
bot.MessageHandler = dispatcher.AsMessageHandler()
|
||||
```
|
||||
|
||||
`openwechat.DispatchMessage`会将消息转发给`dispatcher`对象处理
|
||||
@ -260,5 +325,29 @@ self, err := bot.GetCurrentUser()
|
||||
bot.Block()
|
||||
```
|
||||
|
||||
该方法会一直阻塞,直到用户主动退出或者网络请求发生错误
|
||||
该方法会一直阻塞,直到用户主动退出或者网络请求发生错误。
|
||||
|
||||
|
||||
### 控制Bot存活
|
||||
|
||||
判断当前的`Bot`是否存活。
|
||||
|
||||
```go
|
||||
func (b *Bot) Alive() bool
|
||||
```
|
||||
|
||||
当返回为`true`则表示`Bot`存活。
|
||||
|
||||
如何控制`Bot`存活呢?
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
bot := openwechat.DefaultBot(openwechat.WithContext(ctx))
|
||||
```
|
||||
|
||||
`WithContext`接受一个`context.Context`对象,当`context`对象被取消时,`Bot`也会被取消。
|
||||
|
||||
当前我们也可以调用`bot.Logout`来主动退出当前的`Bot`,当`Bot`退出后,`bot.Alive()`会返回`false`。
|
||||
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
# 消息
|
||||
|
||||
|
||||
|
||||
### 接受消息
|
||||
|
||||
被动接受的消息对象,由微信服务器发出
|
||||
@ -9,28 +7,24 @@
|
||||
消息对象通过绑定在`bot`上的消息回调函数获取
|
||||
|
||||
```go
|
||||
bot.MessageHandler = func(msg *openwechat.Message) {
|
||||
if msg.IsText() && msg.Content == "ping" {
|
||||
msg.ReplyText("pong")
|
||||
}
|
||||
bot.MessageHandler = func (msg *openwechat.Message) {
|
||||
if msg.IsText() && msg.Content == "ping" {
|
||||
msg.ReplyText("pong")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以下简写为`msg`
|
||||
|
||||
|
||||
|
||||
#### 消息内容
|
||||
|
||||
```go
|
||||
msg.Content // 获取消息内容
|
||||
msg.Content // 获取消息内容
|
||||
```
|
||||
|
||||
通过访问`Content`属性可直接获取消息内容
|
||||
|
||||
由于消息分为很多种类型,它们都共用`Content`属性。一般当消息类型问文本类型的时候,我们才会去访问`Content`属性。
|
||||
|
||||
|
||||
由于消息分为很多种类型,它们都共用`Content`属性。一般当消息类型为文本类型的时候,我们才会去访问`Content`属性。
|
||||
|
||||
#### 消息类型判断
|
||||
|
||||
@ -51,7 +45,7 @@ msg.IsPicture()
|
||||
##### 位置消息
|
||||
|
||||
```go
|
||||
msg.IsMap()
|
||||
msg.IsLocation()
|
||||
```
|
||||
|
||||
##### 语音消息
|
||||
@ -60,7 +54,7 @@ msg.IsMap()
|
||||
msg.IsVoice()
|
||||
```
|
||||
|
||||
##### 添加好友请求
|
||||
##### 是否为好友添加请求
|
||||
|
||||
```go
|
||||
msg.IsFriendAdd()
|
||||
@ -110,7 +104,23 @@ msg.IsReceiveRedPacket()
|
||||
|
||||
但是不能领取!
|
||||
|
||||
##### 判断是否为拍一拍
|
||||
|
||||
```go
|
||||
msg.IsIsPaiYiPai() // 拍一拍消息
|
||||
msg.IsTickled()
|
||||
```
|
||||
|
||||
##### 判断是否拍了拍自己
|
||||
```go
|
||||
msg.IsTickledMe()
|
||||
```
|
||||
|
||||
##### 判断是否有新人加入群聊
|
||||
|
||||
```go
|
||||
msg.IsJoinGroup()
|
||||
```
|
||||
|
||||
#### 获取消息的发送者
|
||||
|
||||
@ -120,16 +130,12 @@ sender, err := msg.Sender()
|
||||
|
||||
如果是群聊消息,该方法返回的是群聊对象(需要自己将`User`转换为`Group`对象)
|
||||
|
||||
|
||||
|
||||
#### 获取消息的接受者
|
||||
|
||||
```go
|
||||
receiver, err := msg.Receiver()
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 获取消息在群里面的发送者
|
||||
|
||||
```go
|
||||
@ -138,15 +144,17 @@ sender, err := msg.SenderInGroup()
|
||||
|
||||
获取群聊中具体发消息的用户,前提该消息必须来自群聊。
|
||||
|
||||
|
||||
|
||||
#### 是否由自己发送
|
||||
|
||||
```go
|
||||
msg.IsSendBySelf()
|
||||
```
|
||||
|
||||
#### 是否为拍一拍
|
||||
|
||||
```go
|
||||
msg.IsTickled()
|
||||
```
|
||||
|
||||
#### 消息是否由好友发出
|
||||
|
||||
@ -154,24 +162,18 @@ msg.IsSendBySelf()
|
||||
msg.IsSendByFriend()
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 消息是否由群聊发出
|
||||
|
||||
```go
|
||||
msg.IsSendByGroup()
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 回复文本消息
|
||||
|
||||
```go
|
||||
msg.ReplyText("hello")
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 回复图片消息
|
||||
|
||||
```go
|
||||
@ -180,8 +182,6 @@ defer img.Close()
|
||||
msg.ReplyImage(img)
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 回复文件消息
|
||||
|
||||
```go
|
||||
@ -190,14 +190,12 @@ defer file.Close()
|
||||
msg.ReplyFile(file)
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 获取消息里的其他信息
|
||||
|
||||
##### 名片消息
|
||||
|
||||
```go
|
||||
card, err := msg. Card()
|
||||
card, err := msg.Card()
|
||||
```
|
||||
|
||||
该方法调用的前提为`msg.IsCard()`返回为`true`
|
||||
@ -213,31 +211,29 @@ alias := card.Alias
|
||||
```go
|
||||
// 名片消息内容
|
||||
type Card struct {
|
||||
XMLName xml.Name `xml:"msg"`
|
||||
ImageStatus int `xml:"imagestatus,attr"`
|
||||
Scene int `xml:"scene,attr"`
|
||||
Sex int `xml:"sex,attr"`
|
||||
Certflag int `xml:"certflag,attr"`
|
||||
BigHeadImgUrl string `xml:"bigheadimgurl,attr"`
|
||||
SmallHeadImgUrl string `xml:"smallheadimgurl,attr"`
|
||||
UserName string `xml:"username,attr"`
|
||||
NickName string `xml:"nickname,attr"`
|
||||
ShortPy string `xml:"shortpy,attr"`
|
||||
Alias string `xml:"alias,attr"` // Note: 这个是名片用户的微信号
|
||||
Province string `xml:"province,attr"`
|
||||
City string `xml:"city,attr"`
|
||||
Sign string `xml:"sign,attr"`
|
||||
Certinfo string `xml:"certinfo,attr"`
|
||||
BrandIconUrl string `xml:"brandIconUrl,attr"`
|
||||
BrandHomeUr string `xml:"brandHomeUr,attr"`
|
||||
BrandSubscriptConfigUrl string `xml:"brandSubscriptConfigUrl,attr"`
|
||||
BrandFlags string `xml:"brandFlags,attr"`
|
||||
RegionCode string `xml:"regionCode,attr"`
|
||||
XMLName xml.Name `xml:"msg"`
|
||||
ImageStatus int `xml:"imagestatus,attr"`
|
||||
Scene int `xml:"scene,attr"`
|
||||
Sex int `xml:"sex,attr"`
|
||||
Certflag int `xml:"certflag,attr"`
|
||||
BigHeadImgUrl string `xml:"bigheadimgurl,attr"`
|
||||
SmallHeadImgUrl string `xml:"smallheadimgurl,attr"`
|
||||
UserName string `xml:"username,attr"`
|
||||
NickName string `xml:"nickname,attr"`
|
||||
ShortPy string `xml:"shortpy,attr"`
|
||||
Alias string `xml:"alias,attr"` // Note: 这个是名片用户的微信号
|
||||
Province string `xml:"province,attr"`
|
||||
City string `xml:"city,attr"`
|
||||
Sign string `xml:"sign,attr"`
|
||||
Certinfo string `xml:"certinfo,attr"`
|
||||
BrandIconUrl string `xml:"brandIconUrl,attr"`
|
||||
BrandHomeUr string `xml:"brandHomeUr,attr"`
|
||||
BrandSubscriptConfigUrl string `xml:"brandSubscriptConfigUrl,attr"`
|
||||
BrandFlags string `xml:"brandFlags,attr"`
|
||||
RegionCode string `xml:"regionCode,attr"`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
##### 获取已撤回的消息
|
||||
|
||||
```go
|
||||
@ -250,30 +246,28 @@ revokeMsg, err := msg.RevokeMsg()
|
||||
|
||||
```go
|
||||
type RevokeMsg struct {
|
||||
SysMsg xml.Name `xml:"sysmsg"`
|
||||
Type string `xml:"type,attr"`
|
||||
RevokeMsg struct {
|
||||
OldMsgId int64 `xml:"oldmsgid"`
|
||||
MsgId int64 `xml:"msgid"`
|
||||
Session string `xml:"session"`
|
||||
ReplaceMsg string `xml:"replacemsg"`
|
||||
} `xml:"revokemsg"`
|
||||
SysMsg xml.Name `xml:"sysmsg"`
|
||||
Type string `xml:"type,attr"`
|
||||
RevokeMsg struct {
|
||||
OldMsgId int64 `xml:"oldmsgid"`
|
||||
MsgId int64 `xml:"msgid"`
|
||||
Session string `xml:"session"`
|
||||
ReplaceMsg string `xml:"replacemsg"`
|
||||
} `xml:"revokemsg"`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 同意好友请求
|
||||
|
||||
```go
|
||||
msg.Agree()
|
||||
friend, err := msg.Agree()
|
||||
// msg.Agree("我同意了")
|
||||
```
|
||||
|
||||
返回的friend即刚添加的好友对象
|
||||
|
||||
该方法调用成功的前提是`msg.IsFriendAdd()`返回为`true`
|
||||
|
||||
|
||||
|
||||
#### 设置为已读
|
||||
|
||||
```go
|
||||
@ -282,10 +276,6 @@ msg.AsRead()
|
||||
|
||||
该当前消息设置为已读
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 设置消息的上下文
|
||||
|
||||
用于多个消息处理函数之间的通信,并且是协程安全的。
|
||||
@ -302,10 +292,6 @@ msg.Set("hello", "world")
|
||||
value, exist := msg.Get("hello")
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 已发送消息
|
||||
|
||||
已发送消息指当前用户发送出去的消息
|
||||
@ -320,8 +306,6 @@ sentMsg, err := msg.ReplyText("hello") // 通过回复消息获取
|
||||
// and so on
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 撤回消息
|
||||
|
||||
撤回刚刚发送的消息,撤回消息的有效时间为2分钟,超过了这个时间则无法撤回
|
||||
@ -330,16 +314,12 @@ sentMsg, err := msg.ReplyText("hello") // 通过回复消息获取
|
||||
sentMsg.Revoke()
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 判断是否可以撤回
|
||||
|
||||
```go
|
||||
sentMsg.CanRevoke()
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### 转发给好友
|
||||
|
||||
```go
|
||||
@ -348,8 +328,6 @@ sentMsg.ForwardToFriends(friend1, friend2)
|
||||
|
||||
将刚发送的消息转发给好友
|
||||
|
||||
|
||||
|
||||
#### 转发给群聊
|
||||
|
||||
```go
|
||||
@ -358,10 +336,6 @@ sentMsg.ForwardToGroups(group1, group2)
|
||||
|
||||
将刚发送的消息转发给群聊
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Emoji表情
|
||||
|
||||
openwechat提供了微信全套`emoji`表情的支持
|
||||
@ -373,7 +347,7 @@ emoji表情可以通过发送`Text`类型的函数发送
|
||||
如
|
||||
|
||||
```go
|
||||
firend.SendText(openwechat.Emoji.Doge) // 发送狗头表情
|
||||
firend.SendText(openwechat.Emoji.Doge) // 发送狗头表情
|
||||
msg.ReplyText(openwechat.Emoji.Awesome) // 发送666的表情
|
||||
```
|
||||
|
||||
|
@ -43,7 +43,7 @@ type User struct {
|
||||
}
|
||||
```
|
||||
|
||||
`User`结构体的属性,部门信息可以通过它的英文名知道它所描述的意思。
|
||||
`User`结构体的属性,部分信息可以通过它的英文名知道它所描述的意思。
|
||||
|
||||
其中要注意的是`UserName`这个属性。
|
||||
|
||||
@ -52,7 +52,13 @@ type User struct {
|
||||
不同用户的`UserName`的值是不一样的,可以通过该字段来区分不同的用户。
|
||||
|
||||
|
||||
#### 获取用户唯一标识
|
||||
|
||||
不同于`UserName`,`ID`是用户的唯一标识,且不会随着登录而改变。
|
||||
|
||||
```go
|
||||
func (u *User) ID() string
|
||||
```
|
||||
|
||||
|
||||
#### 获取头像
|
||||
@ -188,7 +194,7 @@ self.SendImageToFriend(friend, img)
|
||||
|
||||
```go
|
||||
file, _ := os.Open("your file path")
|
||||
defer img.Close()
|
||||
defer file.Close()
|
||||
self.SendFileToFriend(friend, file)
|
||||
// 或者
|
||||
// friend.SendFile(img)
|
||||
@ -394,10 +400,10 @@ func (f Friends) SendText(text string, delay ...time.Duration) error
|
||||
#### 群发图片
|
||||
|
||||
```go
|
||||
func (f Friends) SendImage(file *os.File, delay ...time.Duration) error
|
||||
func (f Friends) SendImage(file io.Reader, delay ...time.Duration) error
|
||||
```
|
||||
|
||||
* `file`:`os.file`类型,即发送图片的文件指针
|
||||
* `file`:`io.Reader`类型。
|
||||
* `delay`:每次发送消息的间隔(发送消息过快可能会被wx检测到,最好加上间隔时间)
|
||||
|
||||
|
||||
@ -405,10 +411,10 @@ func (f Friends) SendImage(file *os.File, delay ...time.Duration) error
|
||||
#### 群发文件
|
||||
|
||||
```go
|
||||
func (f Friends) SendFile(file *os.File, delay ...time.Duration) error
|
||||
func (f Friends) SendFile(file io.Reader, delay ...time.Duration) error
|
||||
```
|
||||
|
||||
* `file`:`os.file`类型,即发送文件的文件指针
|
||||
* `file`:`io.Reader`类型。
|
||||
* `delay`:每次发送消息的间隔(发送消息过快可能会被wx检测到,最好加上间隔时间)
|
||||
|
||||
|
||||
@ -539,10 +545,10 @@ func (g Groups) SendText(text string, delay ...time.Duration) error
|
||||
#### 群发图片
|
||||
|
||||
```go
|
||||
func (g Groups) SendImage(file *os.File, delay ...time.Duration) error
|
||||
func (g Groups) SendImage(file io.Reader, delay ...time.Duration) error
|
||||
```
|
||||
|
||||
* `file`:`os.file`类型,即发送文件的文件指针
|
||||
* `file`:`io.Reader`类型。
|
||||
* `delay`:每次发送消息的间隔(发送消息过快可能会被wx检测到,最好加上间隔时间)
|
||||
|
||||
|
||||
@ -550,10 +556,10 @@ func (g Groups) SendImage(file *os.File, delay ...time.Duration) error
|
||||
#### 群发文件
|
||||
|
||||
```go
|
||||
func (g Groups) SendFile(file *os.File, delay ...time.Duration) error
|
||||
func (g Groups) SendFile(file io.Reader, delay ...time.Duration) error
|
||||
```
|
||||
|
||||
* `file`:`os.file`类型,即发送文件的文件指针
|
||||
* `file`:`io.Reader`类型。
|
||||
* `delay`:每次发送消息的间隔(发送消息过快可能会被wx检测到,最好加上间隔时间)
|
||||
|
||||
|
||||
|
39
stringer.go
39
stringer.go
@ -58,6 +58,8 @@ func _() {
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[ticketError - -14]
|
||||
_ = x[logicError - -2]
|
||||
_ = x[sysError - -1]
|
||||
_ = x[paramError-1]
|
||||
_ = x[failedLoginWarn-1100]
|
||||
_ = x[failedLoginCheck-1101]
|
||||
@ -68,31 +70,36 @@ func _() {
|
||||
|
||||
const (
|
||||
_Ret_name_0 = "ticket error"
|
||||
_Ret_name_1 = "param error"
|
||||
_Ret_name_2 = "failed login warnfailed login Checkcookie invalid"
|
||||
_Ret_name_3 = "login environmental abnormality"
|
||||
_Ret_name_4 = "operate too often"
|
||||
_Ret_name_1 = "logic errorsys error"
|
||||
_Ret_name_2 = "param error"
|
||||
_Ret_name_3 = "failed login warnfailed login checkcookie invalid"
|
||||
_Ret_name_4 = "login environmental abnormality"
|
||||
_Ret_name_5 = "operate too often"
|
||||
)
|
||||
|
||||
var (
|
||||
_Ret_index_2 = [...]uint8{0, 17, 35, 49}
|
||||
_Ret_index_1 = [...]uint8{0, 11, 20}
|
||||
_Ret_index_3 = [...]uint8{0, 17, 35, 49}
|
||||
)
|
||||
|
||||
func (r Ret) String() string {
|
||||
func (i Ret) String() string {
|
||||
switch {
|
||||
case r == -14:
|
||||
case i == -14:
|
||||
return _Ret_name_0
|
||||
case r == 1:
|
||||
return _Ret_name_1
|
||||
case 1100 <= r && r <= 1102:
|
||||
r -= 1100
|
||||
return _Ret_name_2[_Ret_index_2[r]:_Ret_index_2[r+1]]
|
||||
case r == 1203:
|
||||
return _Ret_name_3
|
||||
case r == 1205:
|
||||
case -2 <= i && i <= -1:
|
||||
i -= -2
|
||||
return _Ret_name_1[_Ret_index_1[i]:_Ret_index_1[i+1]]
|
||||
case i == 1:
|
||||
return _Ret_name_2
|
||||
case 1100 <= i && i <= 1102:
|
||||
i -= 1100
|
||||
return _Ret_name_3[_Ret_index_3[i]:_Ret_index_3[i+1]]
|
||||
case i == 1203:
|
||||
return _Ret_name_4
|
||||
case i == 1205:
|
||||
return _Ret_name_5
|
||||
default:
|
||||
return "Ret(" + strconv.FormatInt(int64(r), 10) + ")"
|
||||
return "Ret(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
|
111
stroage.go
111
stroage.go
@ -1,11 +1,10 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Storage 身份信息, 维持整个登陆的Session会话
|
||||
@ -16,7 +15,7 @@ type Storage struct {
|
||||
}
|
||||
|
||||
type HotReloadStorageItem struct {
|
||||
Cookies map[string][]*http.Cookie
|
||||
Jar *Jar
|
||||
BaseRequest *BaseRequest
|
||||
LoginInfo *LoginInfo
|
||||
WechatDomain WechatDomain
|
||||
@ -26,17 +25,22 @@ type HotReloadStorageItem struct {
|
||||
// HotReloadStorage 热登陆存储接口
|
||||
type HotReloadStorage io.ReadWriter
|
||||
|
||||
// JsonFileHotReloadStorage 实现HotReloadStorage接口
|
||||
// 默认以json文件的形式存储
|
||||
// Deprecated
|
||||
type JsonFileHotReloadStorage struct {
|
||||
FileName string
|
||||
// fileHotReloadStorage 实现HotReloadStorage接口
|
||||
// 以文件的形式存储
|
||||
type fileHotReloadStorage struct {
|
||||
filename string
|
||||
file *os.File
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (j *JsonFileHotReloadStorage) Read(p []byte) (n int, err error) {
|
||||
func (j *fileHotReloadStorage) Read(p []byte) (n int, err error) {
|
||||
j.lock.Lock()
|
||||
defer j.lock.Unlock()
|
||||
if j.file == nil {
|
||||
j.file, err = os.Open(j.FileName)
|
||||
j.file, err = os.OpenFile(j.filename, os.O_RDWR, 0600)
|
||||
if os.IsNotExist(err) {
|
||||
return 0, ErrInvalidStorage
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -44,31 +48,74 @@ func (j *JsonFileHotReloadStorage) Read(p []byte) (n int, err error) {
|
||||
return j.file.Read(p)
|
||||
}
|
||||
|
||||
func (j *JsonFileHotReloadStorage) Write(p []byte) (n int, err error) {
|
||||
j.file, err = os.Create(j.FileName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
func (j *fileHotReloadStorage) Write(p []byte) (n int, err error) {
|
||||
j.lock.Lock()
|
||||
defer j.lock.Unlock()
|
||||
if j.file == nil {
|
||||
j.file, err = os.Create(j.filename)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
// reset offset and truncate file
|
||||
if _, err = j.file.Seek(0, io.SeekStart); err != nil {
|
||||
return
|
||||
}
|
||||
if err = j.file.Truncate(0); err != nil {
|
||||
return
|
||||
}
|
||||
// json decode only write once
|
||||
return j.file.Write(p)
|
||||
}
|
||||
|
||||
// NewJsonFileHotReloadStorage 创建JsonFileHotReloadStorage
|
||||
// Deprecated
|
||||
// use os.File instead
|
||||
func NewJsonFileHotReloadStorage(filename string) HotReloadStorage {
|
||||
return &JsonFileHotReloadStorage{FileName: filename}
|
||||
func (j *fileHotReloadStorage) Close() error {
|
||||
j.lock.Lock()
|
||||
defer j.lock.Unlock()
|
||||
if j.file == nil {
|
||||
return nil
|
||||
}
|
||||
return j.file.Close()
|
||||
}
|
||||
|
||||
var _ HotReloadStorage = &JsonFileHotReloadStorage{}
|
||||
|
||||
func NewHotReloadStorageItem(storage HotReloadStorage) (*HotReloadStorageItem, error) {
|
||||
if storage == nil {
|
||||
return nil, errors.New("storage can't be nil")
|
||||
}
|
||||
var item HotReloadStorageItem
|
||||
|
||||
if err := json.NewDecoder(storage).Decode(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
// Deprecated: use NewFileHotReloadStorage instead
|
||||
// 不再单纯以json的格式存储,支持了用户自定义序列化方式
|
||||
func NewJsonFileHotReloadStorage(filename string) io.ReadWriteCloser {
|
||||
return NewFileHotReloadStorage(filename)
|
||||
}
|
||||
|
||||
// NewFileHotReloadStorage implements HotReloadStorage
|
||||
func NewFileHotReloadStorage(filename string) io.ReadWriteCloser {
|
||||
return &fileHotReloadStorage{filename: filename}
|
||||
}
|
||||
|
||||
var _ HotReloadStorage = (*fileHotReloadStorage)(nil)
|
||||
|
||||
type HotReloadStorageSyncer struct {
|
||||
duration time.Duration
|
||||
bot *Bot
|
||||
}
|
||||
|
||||
// Sync 定时同步数据到登陆存储中
|
||||
func (h *HotReloadStorageSyncer) Sync() error {
|
||||
if h.duration <= 0 {
|
||||
return nil
|
||||
}
|
||||
// 定时器
|
||||
ticker := time.NewTicker(h.duration)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// 每隔一段时间, 将数据同步到storage中
|
||||
if err := h.bot.DumpHotReloadStorage(); err != nil {
|
||||
return err
|
||||
}
|
||||
case <-h.bot.Context().Done():
|
||||
// 当Bot关闭的时候, 退出循环
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewHotReloadStorageSyncer(bot *Bot, duration time.Duration) *HotReloadStorageSyncer {
|
||||
return &HotReloadStorageSyncer{duration: duration, bot: bot}
|
||||
}
|
||||
|
44
sync_check.go
Normal file
44
sync_check.go
Normal file
@ -0,0 +1,44 @@
|
||||
package openwechat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Selector string
|
||||
|
||||
const (
|
||||
SelectorNormal Selector = "0" // 正常
|
||||
SelectorNewMsg Selector = "2" // 有新消息
|
||||
SelectorModContact Selector = "4" // 联系人信息变更
|
||||
SelectorAddOrDelContact Selector = "6" // 添加或删除联系人
|
||||
SelectorModChatRoom Selector = "7" // 进入或退出聊天室
|
||||
)
|
||||
|
||||
type SyncCheckResponse struct {
|
||||
RetCode string
|
||||
Selector Selector
|
||||
}
|
||||
|
||||
func (s SyncCheckResponse) Success() bool {
|
||||
return s.RetCode == "0"
|
||||
}
|
||||
|
||||
func (s SyncCheckResponse) NorMal() bool {
|
||||
return s.Success() && s.Selector == SelectorNormal
|
||||
}
|
||||
|
||||
func (s SyncCheckResponse) HasNewMessage() bool {
|
||||
return s.Success() && s.Selector == SelectorNewMsg
|
||||
}
|
||||
|
||||
func (s SyncCheckResponse) Err() error {
|
||||
if s.Success() {
|
||||
return nil
|
||||
}
|
||||
i, err := strconv.Atoi(s.RetCode)
|
||||
if err != nil {
|
||||
return errors.New("sync check unknown error")
|
||||
}
|
||||
return Ret(i)
|
||||
}
|
596
user.go
596
user.go
@ -3,10 +3,16 @@ package openwechat
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// User 抽象的用户结构: 好友 群组 公众号
|
||||
@ -46,17 +52,37 @@ type User struct {
|
||||
|
||||
MemberList Members
|
||||
|
||||
Self *Self
|
||||
self *Self
|
||||
}
|
||||
|
||||
// implement fmt.Stringer
|
||||
func (u *User) String() string {
|
||||
return fmt.Sprintf("<User:%s>", u.NickName)
|
||||
format := "User"
|
||||
if u.IsSelf() {
|
||||
format = "Self"
|
||||
} else if u.IsFriend() {
|
||||
format = "Friend"
|
||||
} else if u.IsGroup() {
|
||||
format = "Group"
|
||||
} else if u.IsMP() {
|
||||
format = "MP"
|
||||
}
|
||||
return fmt.Sprintf("<%s:%s>", format, u.NickName)
|
||||
}
|
||||
|
||||
// GetAvatarResponse 获取用户头像
|
||||
func (u *User) GetAvatarResponse() (*http.Response, error) {
|
||||
return u.Self.Bot.Caller.Client.WebWxGetHeadImg(u)
|
||||
func (u *User) GetAvatarResponse() (resp *http.Response, err error) {
|
||||
for i := 0; i < 3; i++ {
|
||||
resp, err = u.self.bot.Caller.Client.WebWxGetHeadImg(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 这里存在 ContentLength 为0的情况,需要重试
|
||||
if resp.ContentLength > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// SaveAvatar 下载用户头像
|
||||
@ -65,7 +91,7 @@ func (u *User) SaveAvatar(filename string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
return u.SaveAvatarWithWriter(file)
|
||||
}
|
||||
|
||||
@ -74,40 +100,31 @@ func (u *User) SaveAvatarWithWriter(writer io.Writer) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 这里获取头像的响应有时可能会异常
|
||||
// 一般为网路原因
|
||||
// 再去请求一次即可解决
|
||||
if resp.ContentLength == 0 && resp.Header.Get("Content-Type") == "image/jpeg" {
|
||||
resp, err = u.GetAvatarResponse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 写文件前判断下 content length 是否是 0,不然保存的头像会出现
|
||||
// image not loaded try to open it externally to fix format problem 问题
|
||||
if resp.ContentLength == 0 {
|
||||
return fmt.Errorf("get avatar response content length is 0")
|
||||
return errors.New("get avatar response content length is 0")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
_, err = io.Copy(writer, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// Detail 获取用户的详情
|
||||
func (u *User) Detail() error {
|
||||
if u.UserName == u.Self.UserName {
|
||||
if u.UserName == u.self.UserName {
|
||||
return nil
|
||||
}
|
||||
members := Members{u}
|
||||
request := u.Self.Bot.Storage.Request
|
||||
newMembers, err := u.Self.Bot.Caller.WebWxBatchGetContact(members, request)
|
||||
request := u.self.bot.Storage.Request
|
||||
newMembers, err := u.self.bot.Caller.WebWxBatchGetContact(members, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newMembers.init(u.Self)
|
||||
newMembers.init(u.self)
|
||||
user := newMembers.First()
|
||||
*u = *user
|
||||
u.MemberList.init(u.Self)
|
||||
u.MemberList.init(u.self)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -116,26 +133,50 @@ func (u *User) IsFriend() bool {
|
||||
return !u.IsGroup() && strings.HasPrefix(u.UserName, "@") && u.VerifyFlag == 0
|
||||
}
|
||||
|
||||
// AsFriend 将当前用户转换为好友类型
|
||||
func (u *User) AsFriend() (*Friend, bool) {
|
||||
if u.IsFriend() {
|
||||
return &Friend{User: u}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// IsGroup 判断是否为群组
|
||||
func (u *User) IsGroup() bool {
|
||||
return strings.HasPrefix(u.UserName, "@@") && u.VerifyFlag == 0
|
||||
}
|
||||
|
||||
// AsGroup 将当前用户转换为群组类型
|
||||
func (u *User) AsGroup() (*Group, bool) {
|
||||
if u.IsGroup() {
|
||||
return &Group{User: u}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// IsMP 判断是否为公众号
|
||||
func (u *User) IsMP() bool {
|
||||
return u.VerifyFlag == 8 || u.VerifyFlag == 24 || u.VerifyFlag == 136
|
||||
}
|
||||
|
||||
// AsMP 将当前用户转换为公众号类型
|
||||
func (u *User) AsMP() (*Mp, bool) {
|
||||
if u.IsMP() {
|
||||
return &Mp{User: u}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Pin 将联系人置顶
|
||||
func (u *User) Pin() error {
|
||||
req := u.Self.Bot.Storage.Request
|
||||
return u.Self.Bot.Caller.WebWxRelationPin(req, u, 1)
|
||||
req := u.self.bot.Storage.Request
|
||||
return u.self.bot.Caller.WebWxRelationPin(req, u, 1)
|
||||
}
|
||||
|
||||
// UnPin 将联系人取消置顶
|
||||
func (u *User) UnPin() error {
|
||||
req := u.Self.Bot.Storage.Request
|
||||
return u.Self.Bot.Caller.WebWxRelationPin(req, u, 0)
|
||||
req := u.self.bot.Storage.Request
|
||||
return u.self.bot.Caller.WebWxRelationPin(req, u, 0)
|
||||
}
|
||||
|
||||
// IsPin 判断当前联系人(好友、群组、公众号)是否为置顶状态
|
||||
@ -143,6 +184,58 @@ func (u *User) IsPin() bool {
|
||||
return u.ContactFlag == 2051
|
||||
}
|
||||
|
||||
// ID 获取用户的唯一标识 只对当前登录的用户有效
|
||||
// ID 和 UserName 的区别是 ID 多次登录不会变化,而 UserName 只针对当前登录会话有效
|
||||
func (u *User) ID() string {
|
||||
// 首先尝试获取uid
|
||||
if u.Uin != 0 {
|
||||
return strconv.FormatInt(u.Uin, 10)
|
||||
}
|
||||
// 如果uid不存在,尝试从头像url中获取
|
||||
if u.HeadImgUrl != "" {
|
||||
index := strings.Index(u.HeadImgUrl, "?") + 1
|
||||
if len(u.HeadImgUrl) > index {
|
||||
query := u.HeadImgUrl[index:]
|
||||
params, err := url.ParseQuery(query)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return params.Get("seq")
|
||||
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Self 返回当前用户
|
||||
func (u *User) Self() *Self {
|
||||
return u.self
|
||||
}
|
||||
|
||||
// IsSelf 判断是否为当前用户
|
||||
func (u *User) IsSelf() bool {
|
||||
return u.UserName == u.Self().UserName
|
||||
}
|
||||
|
||||
// OrderSymbol 获取用户的排序标识
|
||||
func (u *User) OrderSymbol() string {
|
||||
var symbol string
|
||||
if u.RemarkPYQuanPin != "" {
|
||||
symbol = u.RemarkPYQuanPin
|
||||
} else if u.PYQuanPin != "" {
|
||||
symbol = u.PYQuanPin
|
||||
} else {
|
||||
symbol = u.NickName
|
||||
}
|
||||
symbol = html.UnescapeString(symbol)
|
||||
symbol = strings.ToUpper(symbol)
|
||||
symbol = regexp.MustCompile("/\\W/ig").ReplaceAllString(symbol, "")
|
||||
if len(symbol) > 0 && symbol[0] < 'A' {
|
||||
return "~"
|
||||
}
|
||||
return symbol
|
||||
}
|
||||
|
||||
// 格式化emoji表情
|
||||
func (u *User) formatEmoji() {
|
||||
u.NickName = FormatEmoji(u.NickName)
|
||||
@ -153,7 +246,7 @@ func (u *User) formatEmoji() {
|
||||
// Self 自己,当前登录用户对象
|
||||
type Self struct {
|
||||
*User
|
||||
Bot *Bot
|
||||
bot *Bot
|
||||
fileHelper *Friend
|
||||
members Members
|
||||
friends Friends
|
||||
@ -170,13 +263,14 @@ func (s *Self) Members(update ...bool) (Members, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s.members.Sort()
|
||||
return s.members, nil
|
||||
}
|
||||
|
||||
// 更新联系人处理
|
||||
func (s *Self) updateMembers() error {
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
members, err := s.Bot.Caller.WebWxGetContact(info)
|
||||
info := s.bot.Storage.LoginInfo
|
||||
members, err := s.bot.Caller.WebWxGetContact(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -186,31 +280,26 @@ func (s *Self) updateMembers() error {
|
||||
}
|
||||
|
||||
// FileHelper 获取文件传输助手对象,封装成Friend返回
|
||||
// fh, err := self.FileHelper() // or fh := openwechat.NewFriendHelper(self)
|
||||
func (s *Self) FileHelper() (*Friend, error) {
|
||||
// 如果缓存里有,直接返回,否则去联系人里面找
|
||||
if s.fileHelper != nil {
|
||||
return s.fileHelper, nil
|
||||
}
|
||||
members, err := s.Members()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users := members.SearchByUserName(1, "filehelper")
|
||||
if users == nil {
|
||||
//
|
||||
// fh := self.FileHelper() // or fh := openwechat.NewFriendHelper(self)
|
||||
func (s *Self) FileHelper() *Friend {
|
||||
if s.fileHelper == nil {
|
||||
s.fileHelper = NewFriendHelper(s)
|
||||
} else {
|
||||
s.fileHelper = &Friend{users.First()}
|
||||
}
|
||||
return s.fileHelper, nil
|
||||
return s.fileHelper
|
||||
}
|
||||
func (s *Self) ChkFrdGrpMpNil() bool {
|
||||
return s.friends == nil && s.groups == nil && s.mps == nil
|
||||
}
|
||||
|
||||
// Friends 获取所有的好友
|
||||
func (s *Self) Friends(update ...bool) (Friends, error) {
|
||||
if s.friends == nil || (len(update) > 0 && update[0]) {
|
||||
if (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
|
||||
if _, err := s.Members(true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if s.friends == nil || (len(update) > 0 && update[0]) {
|
||||
s.friends = s.members.Friends()
|
||||
}
|
||||
return s.friends, nil
|
||||
@ -218,10 +307,14 @@ func (s *Self) Friends(update ...bool) (Friends, error) {
|
||||
|
||||
// Groups 获取所有的群组
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
if s.groups == nil || (len(update) > 0 && update[0]) {
|
||||
s.groups = s.members.Groups()
|
||||
}
|
||||
return s.groups, nil
|
||||
@ -229,10 +322,12 @@ func (s *Self) Groups(update ...bool) (Groups, error) {
|
||||
|
||||
// Mps 获取所有的公众号
|
||||
func (s *Self) Mps(update ...bool) (Mps, error) {
|
||||
if s.mps == nil || (len(update) > 0 && update[0]) {
|
||||
if (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
|
||||
if _, err := s.Members(true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if s.mps == nil || (len(update) > 0 && update[0]) {
|
||||
s.mps = s.members.MPs()
|
||||
}
|
||||
return s.mps, nil
|
||||
@ -248,73 +343,80 @@ func (s *Self) UpdateMembersDetail() error {
|
||||
return members.detail(s)
|
||||
}
|
||||
|
||||
// 抽象发送消息接口
|
||||
func (s *Self) sendMessageToUser(user *User, msg *SendMessage) (*SentMessage, error) {
|
||||
func (s *Self) sendTextToUser(user *User, text string) (*SentMessage, error) {
|
||||
msg := NewTextSendMessage(text, s.UserName, user.UserName)
|
||||
msg.FromUserName = s.UserName
|
||||
msg.ToUserName = user.UserName
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
request := s.Bot.Storage.Request
|
||||
successSendMessage, err := s.Bot.Caller.WebWxSendMsg(msg, info, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
successSendMessage.Self = s
|
||||
return successSendMessage, nil
|
||||
info := s.bot.Storage.LoginInfo
|
||||
request := s.bot.Storage.Request
|
||||
sentMessage, err := s.bot.Caller.WebWxSendMsg(msg, info, request)
|
||||
return s.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
// SendMessageToFriend 发送消息给好友
|
||||
func (s *Self) SendMessageToFriend(friend *Friend, msg *SendMessage) (*SentMessage, error) {
|
||||
return s.sendMessageToUser(friend.User, msg)
|
||||
func (s *Self) sendImageToUser(user *User, file io.Reader) (*SentMessage, error) {
|
||||
req := s.bot.Storage.Request
|
||||
info := s.bot.Storage.LoginInfo
|
||||
sentMessage, err := s.bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, user.UserName)
|
||||
return s.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
func (s *Self) sendVideoToUser(user *User, file io.Reader) (*SentMessage, error) {
|
||||
req := s.bot.Storage.Request
|
||||
info := s.bot.Storage.LoginInfo
|
||||
sentMessage, err := s.bot.Caller.WebWxSendVideoMsg(file, req, info, s.UserName, user.UserName)
|
||||
return s.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
func (s *Self) sendFileToUser(user *User, file io.Reader) (*SentMessage, error) {
|
||||
req := s.bot.Storage.Request
|
||||
info := s.bot.Storage.LoginInfo
|
||||
sentMessage, err := s.bot.Caller.WebWxSendFile(file, req, info, s.UserName, user.UserName)
|
||||
return s.sendMessageWrapper(sentMessage, err)
|
||||
}
|
||||
|
||||
// SendTextToFriend 发送文本消息给好友
|
||||
func (s *Self) SendTextToFriend(friend *Friend, text string) (*SentMessage, error) {
|
||||
msg := NewTextSendMessage(text, s.UserName, friend.UserName)
|
||||
return s.SendMessageToFriend(friend, msg)
|
||||
return s.sendTextToUser(friend.User, text)
|
||||
}
|
||||
|
||||
// SendImageToFriend 发送图片消息给好友
|
||||
func (s *Self) SendImageToFriend(friend *Friend, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, friend.UserName)
|
||||
func (s *Self) SendImageToFriend(friend *Friend, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendImageToUser(friend.User, file)
|
||||
}
|
||||
|
||||
// SendVideoToFriend 发送视频给好友
|
||||
func (s *Self) SendVideoToFriend(friend *Friend, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendVideoMsg(file, req, info, s.UserName, friend.UserName)
|
||||
func (s *Self) SendVideoToFriend(friend *Friend, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendVideoToUser(friend.User, file)
|
||||
}
|
||||
|
||||
// SendFileToFriend 发送文件给好友
|
||||
func (s *Self) SendFileToFriend(friend *Friend, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendFile(file, req, info, s.UserName, friend.UserName)
|
||||
func (s *Self) SendFileToFriend(friend *Friend, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendFileToUser(friend.User, file)
|
||||
}
|
||||
|
||||
// SetRemarkNameToFriend 设置好友备注
|
||||
// self.SetRemarkNameToFriend(friend, "remark") // or friend.SetRemarkName("remark")
|
||||
//
|
||||
// self.SetRemarkNameToFriend(friend, "remark") // or friend.SetRemarkName("remark")
|
||||
func (s *Self) SetRemarkNameToFriend(friend *Friend, remarkName string) error {
|
||||
req := s.Bot.Storage.Request
|
||||
return s.Bot.Caller.WebWxOplog(req, remarkName, friend.UserName)
|
||||
req := s.bot.Storage.Request
|
||||
return s.bot.Caller.WebWxOplog(req, remarkName, friend.UserName)
|
||||
}
|
||||
|
||||
// CreateGroup 创建群聊
|
||||
// topic 群昵称,可以传递字符串
|
||||
// friends 群员,最少为2个,加上自己3个,三人才能成群
|
||||
func (s *Self) CreateGroup(topic string, friends ...*Friend) (*Group, error) {
|
||||
friends = Friends(friends).Uniq()
|
||||
if len(friends) < 2 {
|
||||
return nil, errors.New("a group must be at least 2 members")
|
||||
}
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
group, err := s.Bot.Caller.WebWxCreateChatRoom(req, info, topic, friends)
|
||||
req := s.bot.Storage.Request
|
||||
info := s.bot.Storage.LoginInfo
|
||||
group, err := s.bot.Caller.WebWxCreateChatRoom(req, info, topic, friends)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
group.Self = s
|
||||
group.self = s
|
||||
err = group.Detail()
|
||||
return group, err
|
||||
}
|
||||
@ -325,6 +427,7 @@ func (s *Self) AddFriendsIntoGroup(group *Group, friends ...*Friend) error {
|
||||
if len(friends) == 0 {
|
||||
return nil
|
||||
}
|
||||
friends = Friends(friends).Uniq()
|
||||
// 获取群的所有的群员
|
||||
groupMembers, err := group.Members()
|
||||
if err != nil {
|
||||
@ -338,9 +441,9 @@ func (s *Self) AddFriendsIntoGroup(group *Group, friends ...*Friend) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.AddFriendIntoChatRoom(req, info, group, friends...)
|
||||
req := s.bot.Storage.Request
|
||||
info := s.bot.Storage.LoginInfo
|
||||
return s.bot.Caller.AddFriendIntoChatRoom(req, info, group, friends...)
|
||||
}
|
||||
|
||||
// RemoveMemberFromGroup 从群聊中移除用户
|
||||
@ -369,14 +472,15 @@ func (s *Self) RemoveMemberFromGroup(group *Group, members Members) error {
|
||||
if count != len(members) {
|
||||
return errors.New("invalid members")
|
||||
}
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.RemoveFriendFromChatRoom(req, info, group, members...)
|
||||
req := s.bot.Storage.Request
|
||||
info := s.bot.Storage.LoginInfo
|
||||
return s.bot.Caller.RemoveFriendFromChatRoom(req, info, group, members...)
|
||||
}
|
||||
|
||||
// AddFriendIntoManyGroups 拉好友进多个群聊
|
||||
// AddFriendIntoGroups, 名字和上面的有点像
|
||||
func (s *Self) AddFriendIntoManyGroups(friend *Friend, groups ...*Group) error {
|
||||
groups = Groups(groups).Uniq()
|
||||
for _, group := range groups {
|
||||
if err := s.AddFriendsIntoGroup(group, friend); err != nil {
|
||||
return err
|
||||
@ -387,106 +491,239 @@ func (s *Self) AddFriendIntoManyGroups(friend *Friend, groups ...*Group) error {
|
||||
|
||||
// RenameGroup 群组重命名
|
||||
func (s *Self) RenameGroup(group *Group, newName string) error {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxRenameChatRoom(req, info, newName, group)
|
||||
}
|
||||
|
||||
// SendMessageToGroup 发送消息给群组
|
||||
func (s *Self) SendMessageToGroup(group *Group, msg *SendMessage) (*SentMessage, error) {
|
||||
return s.sendMessageToUser(group.User, msg)
|
||||
req := s.bot.Storage.Request
|
||||
info := s.bot.Storage.LoginInfo
|
||||
return s.bot.Caller.WebWxRenameChatRoom(req, info, newName, group)
|
||||
}
|
||||
|
||||
// SendTextToGroup 发送文本消息给群组
|
||||
func (s *Self) SendTextToGroup(group *Group, text string) (*SentMessage, error) {
|
||||
msg := NewTextSendMessage(text, s.UserName, group.UserName)
|
||||
return s.SendMessageToGroup(group, msg)
|
||||
return s.sendTextToUser(group.User, text)
|
||||
}
|
||||
|
||||
// SendImageToGroup 发送图片消息给群组
|
||||
func (s *Self) SendImageToGroup(group *Group, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, group.UserName)
|
||||
func (s *Self) SendImageToGroup(group *Group, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendImageToUser(group.User, file)
|
||||
}
|
||||
|
||||
// SendVideoToGroup 发送视频给群组
|
||||
func (s *Self) SendVideoToGroup(group *Group, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendVideoMsg(file, req, info, s.UserName, group.UserName)
|
||||
func (s *Self) SendVideoToGroup(group *Group, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendVideoToUser(group.User, file)
|
||||
}
|
||||
|
||||
// SendFileToGroup 发送文件给群组
|
||||
func (s *Self) SendFileToGroup(group *Group, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendFile(file, req, info, s.UserName, group.UserName)
|
||||
func (s *Self) SendFileToGroup(group *Group, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendFileToUser(group.User, file)
|
||||
}
|
||||
|
||||
// RevokeMessage 撤回消息
|
||||
// sentMessage, err := friend.SendText("message")
|
||||
// if err == nil {
|
||||
// self.RevokeMessage(sentMessage) // or sentMessage.Revoke()
|
||||
// }
|
||||
//
|
||||
// sentMessage, err := friend.SendText("message")
|
||||
// if err == nil {
|
||||
// self.RevokeMessage(sentMessage) // or sentMessage.Revoke()
|
||||
// }
|
||||
func (s *Self) RevokeMessage(msg *SentMessage) error {
|
||||
return s.Bot.Caller.WebWxRevokeMsg(msg, s.Bot.Storage.Request)
|
||||
return s.bot.Caller.WebWxRevokeMsg(msg, s.bot.Storage.Request)
|
||||
}
|
||||
|
||||
// 转发消息接口
|
||||
func (s *Self) forwardMessage(msg *SentMessage, users ...*User) error {
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
req := s.Bot.Storage.Request
|
||||
func (s *Self) forwardMessage(msg *SentMessage, delay time.Duration, users ...*User) error {
|
||||
info := s.bot.Storage.LoginInfo
|
||||
req := s.bot.Storage.Request
|
||||
switch msg.Type {
|
||||
case MsgTypeText:
|
||||
for _, user := range users {
|
||||
msg.FromUserName = s.UserName
|
||||
msg.ToUserName = user.UserName
|
||||
if _, err := s.Self.Bot.Caller.WebWxSendMsg(msg.SendMessage, info, req); err != nil {
|
||||
if _, err := s.self.bot.Caller.WebWxSendMsg(msg.SendMessage, info, req); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
case MsgTypeImage:
|
||||
for _, user := range users {
|
||||
msg.FromUserName = s.UserName
|
||||
msg.ToUserName = user.UserName
|
||||
if _, err := s.Self.Bot.Caller.Client.WebWxSendMsgImg(msg.SendMessage, req, info); err != nil {
|
||||
if _, err := s.self.bot.Caller.Client.WebWxSendMsgImg(msg.SendMessage, req, info); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
case AppMessage:
|
||||
for _, user := range users {
|
||||
msg.FromUserName = s.UserName
|
||||
msg.ToUserName = user.UserName
|
||||
if _, err := s.Self.Bot.Caller.Client.WebWxSendAppMsg(msg.SendMessage, req); err != nil {
|
||||
if _, err := s.self.bot.Caller.Client.WebWxSendAppMsg(msg.SendMessage, req); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported message type: %s", msg.Type)
|
||||
}
|
||||
return errors.New("unsupport message")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForwardMessageToFriends 转发给好友
|
||||
func (s *Self) ForwardMessageToFriends(msg *SentMessage, friends ...*Friend) error {
|
||||
var users = make([]*User, len(friends))
|
||||
for index, friend := range friends {
|
||||
users[index] = friend.User
|
||||
}
|
||||
return s.forwardMessage(msg, users...)
|
||||
func (s *Self) ForwardMessageToFriends(msg *SentMessage, delay time.Duration, friends ...*Friend) error {
|
||||
members := Friends(friends).AsMembers()
|
||||
return s.forwardMessage(msg, delay, members...)
|
||||
}
|
||||
|
||||
// ForwardMessageToGroups 转发给群组
|
||||
func (s *Self) ForwardMessageToGroups(msg *SentMessage, groups ...*Group) error {
|
||||
var users = make([]*User, len(groups))
|
||||
for index, group := range groups {
|
||||
users[index] = group.User
|
||||
func (s *Self) ForwardMessageToGroups(msg *SentMessage, delay time.Duration, groups ...*Group) error {
|
||||
members := Groups(groups).AsMembers()
|
||||
return s.forwardMessage(msg, delay, members...)
|
||||
}
|
||||
|
||||
// sendTextToMembers 发送文本消息给群组或者好友
|
||||
func (s *Self) sendTextToMembers(text string, delay time.Duration, members ...*User) error {
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.forwardMessage(msg, users...)
|
||||
user := members[0]
|
||||
msg, err := s.sendTextToUser(user, text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
return s.forwardMessage(msg, delay, members[1:]...)
|
||||
}
|
||||
|
||||
// sendImageToMembers 发送图片消息给群组或者好友
|
||||
func (s *Self) sendImageToMembers(img io.Reader, delay time.Duration, members ...*User) error {
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
user := members[0]
|
||||
msg, err := s.sendImageToUser(user, img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
return s.forwardMessage(msg, delay, members[1:]...)
|
||||
}
|
||||
|
||||
// sendVideoToMembers 发送视频消息给群组或者好友
|
||||
func (s *Self) sendVideoToMembers(video io.Reader, delay time.Duration, members ...*User) error {
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
user := members[0]
|
||||
msg, err := s.sendVideoToUser(user, video)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
return s.forwardMessage(msg, delay, members[1:]...)
|
||||
}
|
||||
|
||||
func (s *Self) sendFileToMembers(file io.Reader, delay time.Duration, members ...*User) error {
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
user := members[0]
|
||||
msg, err := s.sendFileToUser(user, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
return s.forwardMessage(msg, delay, members[1:]...)
|
||||
}
|
||||
|
||||
// SendTextToFriends 发送文本消息给好友
|
||||
func (s *Self) SendTextToFriends(text string, delay time.Duration, friends ...*Friend) error {
|
||||
members := Friends(friends).AsMembers()
|
||||
return s.sendTextToMembers(text, delay, members...)
|
||||
}
|
||||
|
||||
// SendImageToFriends 发送图片消息给好友
|
||||
func (s *Self) SendImageToFriends(img io.Reader, delay time.Duration, friends ...*Friend) error {
|
||||
members := Friends(friends).AsMembers()
|
||||
return s.sendImageToMembers(img, delay, members...)
|
||||
}
|
||||
|
||||
// SendFileToFriends 发送文件给好友
|
||||
func (s *Self) SendFileToFriends(file io.Reader, delay time.Duration, friends ...*Friend) error {
|
||||
members := Friends(friends).AsMembers()
|
||||
return s.sendFileToMembers(file, delay, members...)
|
||||
}
|
||||
|
||||
// SendVideoToFriends 发送视频给好友
|
||||
func (s *Self) SendVideoToFriends(video io.Reader, delay time.Duration, friends ...*Friend) error {
|
||||
members := Friends(friends).AsMembers()
|
||||
return s.sendVideoToMembers(video, delay, members...)
|
||||
}
|
||||
|
||||
// SendTextToGroups 发送文本消息给群组
|
||||
func (s *Self) SendTextToGroups(text string, delay time.Duration, groups ...*Group) error {
|
||||
members := Groups(groups).AsMembers()
|
||||
return s.sendTextToMembers(text, delay, members...)
|
||||
}
|
||||
|
||||
// SendImageToGroups 发送图片消息给群组
|
||||
func (s *Self) SendImageToGroups(img io.Reader, delay time.Duration, groups ...*Group) error {
|
||||
members := Groups(groups).AsMembers()
|
||||
return s.sendImageToMembers(img, delay, members...)
|
||||
}
|
||||
|
||||
// SendFileToGroups 发送文件给群组
|
||||
func (s *Self) SendFileToGroups(file io.Reader, delay time.Duration, groups ...*Group) error {
|
||||
members := Groups(groups).AsMembers()
|
||||
return s.sendFileToMembers(file, delay, members...)
|
||||
}
|
||||
|
||||
// SendVideoToGroups 发送视频给群组
|
||||
func (s *Self) SendVideoToGroups(video io.Reader, delay time.Duration, groups ...*Group) error {
|
||||
members := Groups(groups).AsMembers()
|
||||
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 抽象的用户组
|
||||
type Members []*User
|
||||
|
||||
func (m Members) Len() int {
|
||||
return len(m)
|
||||
}
|
||||
|
||||
// Less 按照微信的规则排序
|
||||
func (m Members) Less(i, j int) bool {
|
||||
return m[i].OrderSymbol() < m[j].OrderSymbol()
|
||||
}
|
||||
|
||||
func (m Members) Swap(i, j int) {
|
||||
m[i], m[j] = m[j], m[i]
|
||||
}
|
||||
|
||||
// Uniq Members 去重
|
||||
func (m Members) Uniq() Members {
|
||||
var uniqMembers = make(map[string]*User)
|
||||
for _, member := range m {
|
||||
uniqMembers[member.UserName] = member
|
||||
}
|
||||
var members Members
|
||||
for _, member := range uniqMembers {
|
||||
members = append(members, member)
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
// Sort 对联系人进行排序
|
||||
func (m Members) Sort() Members {
|
||||
sort.Sort(m)
|
||||
return m
|
||||
}
|
||||
|
||||
// Count 统计数量
|
||||
func (m Members) Count() int {
|
||||
return len(m)
|
||||
@ -526,30 +763,15 @@ func (m Members) SearchByRemarkName(limit int, remarkName string) (results Membe
|
||||
}
|
||||
|
||||
// Search 根据自定义条件查找
|
||||
func (m Members) Search(limit int, condFuncList ...func(user *User) bool) (results Members) {
|
||||
if condFuncList == nil {
|
||||
return m
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = m.Count()
|
||||
}
|
||||
for _, member := range m {
|
||||
if count := len(results); count == limit {
|
||||
break
|
||||
}
|
||||
var passCount int
|
||||
for _, condFunc := range condFuncList {
|
||||
if condFunc(member) {
|
||||
passCount++
|
||||
} else {
|
||||
break
|
||||
func (m Members) Search(limit int, searchFuncList ...func(user *User) bool) (results Members) {
|
||||
return search(m, limit, func(group *User) bool {
|
||||
for _, searchFunc := range searchFuncList {
|
||||
if !searchFunc(group) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if passCount == len(condFuncList) {
|
||||
results = append(results, member)
|
||||
}
|
||||
}
|
||||
return
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// GetByUserName 根据username查找用户
|
||||
@ -576,8 +798,8 @@ func (m Members) GetByNickName(nickname string) (*User, bool) {
|
||||
func (m Members) Friends() Friends {
|
||||
friends := make(Friends, 0)
|
||||
for _, mb := range m {
|
||||
if mb.IsFriend() {
|
||||
friend := &Friend{mb}
|
||||
friend, ok := mb.AsFriend()
|
||||
if ok {
|
||||
friends = append(friends, friend)
|
||||
}
|
||||
}
|
||||
@ -587,8 +809,8 @@ func (m Members) Friends() Friends {
|
||||
func (m Members) Groups() Groups {
|
||||
groups := make(Groups, 0)
|
||||
for _, mb := range m {
|
||||
if mb.IsGroup() {
|
||||
group := &Group{mb}
|
||||
group, ok := mb.AsGroup()
|
||||
if ok {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
@ -598,8 +820,8 @@ func (m Members) Groups() Groups {
|
||||
func (m Members) MPs() Mps {
|
||||
mps := make(Mps, 0)
|
||||
for _, mb := range m {
|
||||
if mb.IsMP() {
|
||||
mp := &Mp{mb}
|
||||
mp, ok := mb.AsMP()
|
||||
if ok {
|
||||
mps = append(mps, mp)
|
||||
}
|
||||
}
|
||||
@ -622,7 +844,7 @@ func (m Members) detail(self *Self) error {
|
||||
times = count / 50
|
||||
}
|
||||
var newMembers Members
|
||||
request := self.Bot.Storage.Request
|
||||
request := self.bot.Storage.Request
|
||||
var pMembers Members
|
||||
// 分情况依次更新
|
||||
for i := 1; i <= times; i++ {
|
||||
@ -631,7 +853,7 @@ func (m Members) detail(self *Self) error {
|
||||
} else {
|
||||
pMembers = members[(i-1)*50 : i*50]
|
||||
}
|
||||
nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request)
|
||||
nMembers, err := self.bot.Caller.WebWxBatchGetContact(pMembers, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -643,7 +865,7 @@ func (m Members) detail(self *Self) error {
|
||||
// 将全部剩余的更新完毕
|
||||
left := count - total
|
||||
pMembers = members[total : total+left]
|
||||
nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request)
|
||||
nMembers, err := self.bot.Caller.WebWxBatchGetContact(pMembers, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -658,40 +880,50 @@ func (m Members) detail(self *Self) error {
|
||||
|
||||
func (m Members) init(self *Self) {
|
||||
for _, member := range m {
|
||||
member.Self = self
|
||||
member.self = self
|
||||
member.formatEmoji()
|
||||
}
|
||||
}
|
||||
|
||||
// NewFriendHelper 这里为了兼容Desktop版本找不到文件传输助手的问题
|
||||
// 文件传输助手的微信身份标识符永远是filehelper
|
||||
// 这种形式的对象可能缺少一些其他属性
|
||||
// 但是不影响发送信息的功能
|
||||
func NewFriendHelper(self *Self) *Friend {
|
||||
return &Friend{&User{UserName: "filehelper", Self: self}}
|
||||
func newFriend(username string, self *Self) *Friend {
|
||||
return &Friend{&User{UserName: username, self: self}}
|
||||
}
|
||||
|
||||
// SendMessageToMp 发送消息给公众号
|
||||
func (s *Self) SendMessageToMp(mp *Mp, msg *SendMessage) (*SentMessage, error) {
|
||||
return s.sendMessageToUser(mp.User, msg)
|
||||
// NewFriendHelper 创建一个文件传输助手
|
||||
// 文件传输助手的微信身份标识符永远是filehelper
|
||||
func NewFriendHelper(self *Self) *Friend {
|
||||
return newFriend(FileHelper, self)
|
||||
}
|
||||
|
||||
// SendTextToMp 发送文本消息给公众号
|
||||
func (s *Self) SendTextToMp(mp *Mp, text string) (*SentMessage, error) {
|
||||
msg := NewTextSendMessage(text, s.UserName, mp.UserName)
|
||||
return s.SendMessageToMp(mp, msg)
|
||||
return s.sendTextToUser(mp.User, text)
|
||||
}
|
||||
|
||||
// SendImageToMp 发送图片消息给公众号
|
||||
func (s *Self) SendImageToMp(mp *Mp, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, mp.UserName)
|
||||
func (s *Self) SendImageToMp(mp *Mp, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendImageToUser(mp.User, file)
|
||||
}
|
||||
|
||||
// SendFileToMp 发送文件给公众号
|
||||
func (s *Self) SendFileToMp(mp *Mp, file *os.File) (*SentMessage, error) {
|
||||
req := s.Bot.Storage.Request
|
||||
info := s.Bot.Storage.LoginInfo
|
||||
return s.Bot.Caller.WebWxSendFile(file, req, info, s.UserName, mp.UserName)
|
||||
func (s *Self) SendFileToMp(mp *Mp, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendFileToUser(mp.User, file)
|
||||
}
|
||||
|
||||
// SendVideoToMp 发送视频消息给公众号
|
||||
func (s *Self) SendVideoToMp(mp *Mp, file io.Reader) (*SentMessage, error) {
|
||||
return s.sendVideoToUser(mp.User, file)
|
||||
}
|
||||
|
||||
func (s *Self) sendMessageWrapper(message *SentMessage, err error) (*SentMessage, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message.self = s
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// Bot 获取当前用户的机器人
|
||||
func (s *Self) Bot() *Bot {
|
||||
return s.bot
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user