Compare commits

..

No commits in common. "master" and "v1.2.9" have entirely different histories.

24 changed files with 659 additions and 1530 deletions

View File

@ -4,16 +4,16 @@
[![Go Doc](https://pkg.go.dev/badge/github.com/eatMoreApple/openwechat)](https://godoc.org/github.com/eatMoreApple/openwechat)[![Release](https://img.shields.io/github/v/release/eatmoreapple/openwechat.svg?style=flat-square)](https://github.com/eatmoreapple/openwechat/releases)[![Go Report Card](https://goreportcard.com/badge/github.com/eatmoreapple/openwechat)](https://goreportcard.com/badge/github.com/eatmoreapple/openwechat)[![Stars](https://img.shields.io/github/stars/eatmoreapple/openwechat.svg?style=flat-square)](https://img.shields.io/github/stars/eatmoreapple/openwechat.svg?style=flat-square)[![Forks](https://img.shields.io/github/forks/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">

View File

@ -4,8 +4,6 @@ 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

276
bot.go
View File

@ -2,6 +2,7 @@ package openwechat
import (
"context"
"encoding/json"
"errors"
"io"
"log"
@ -12,26 +13,23 @@ import (
)
type Bot struct {
ScanCallBack func(body CheckLoginResponse) // 扫码回调,可获取扫码用户的头像
LoginCallBack func(body CheckLoginResponse) // 登陆回调
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则尝试继续监听
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
uuid string
loginUUID *string
deviceId string // 设备Id
loginOptionGroup BotOptionGroup
}
// Alive 判断当前用户是否正常在线
@ -51,7 +49,6 @@ func (b *Bot) Alive() bool {
// @description: 设置设备Id
// @receiver b
// @param deviceId
// TODO ADD INTO LOGIN OPTION
func (b *Bot) SetDeviceId(deviceId string) {
b.deviceId = deviceId
}
@ -70,44 +67,109 @@ func (b *Bot) GetCurrentUser() (*Self, error) {
return b.self, nil
}
// login 这里对进行一些对登录前后的hook
func (b *Bot) login(login BotLogin) (err error) {
opt := b.loginOptionGroup
opt.Prepare(b)
if err = login.Login(b); err != nil {
err = opt.OnError(b, err)
// HotLogin 热登录,可实现重复登录,
// retry设置为true可在热登录失效后进行普通登录行为
//
// Storage := NewJsonFileHotReloadStorage("Storage.json")
// err := bot.HotLogin(Storage, true)
// fmt.Println(err)
func (b *Bot) HotLogin(storage HotReloadStorage, retries ...bool) error {
err := b.hotLogin(storage)
// 判断是否为需要重新登录
if errors.Is(err, ErrInvalidStorage) {
return b.Login()
}
if err != nil {
if len(retries) > 0 && retries[0] {
retErr, ok := err.(Ret)
if !ok {
return err
}
// TODO add more error code handle here
switch retErr {
case cookieInvalid:
return b.Login()
}
return err
}
}
return err
}
func (b *Bot) hotLogin(storage HotReloadStorage) error {
b.hotReloadStorage = storage
var item HotReloadStorageItem
err := json.NewDecoder(storage).Decode(&item)
if err != nil {
return err
}
return opt.OnSuccess(b)
if err = b.hotLoginInit(&item); err != nil {
return err
}
return b.WebInit()
}
// 热登陆初始化
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
}
// Login 用户登录
func (b *Bot) Login() error {
scanLogin := &SacnLogin{UUID: b.loginUUID}
return b.login(scanLogin)
uuid, err := b.Caller.GetLoginUUID()
if err != nil {
return err
}
return b.LoginWithUUID(uuid)
}
// 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)
// 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 err = b.HandleLogin(resp.Raw); err != nil {
return err
}
if b.LoginCallBack != nil {
b.LoginCallBack(resp.Raw)
}
return nil
case StatusScanned:
// 执行扫码回调
if b.ScanCallBack != nil {
b.ScanCallBack(resp.Raw)
}
case StatusTimeout:
return ErrLoginTimeout
case StatusWait:
continue
}
}
}
// Logout 用户退出
@ -117,16 +179,16 @@ func (b *Bot) Logout() error {
if err := b.Caller.Logout(info); err != nil {
return err
}
b.Exit()
b.stopSyncCheck(errors.New("logout"))
return nil
}
return errors.New("user not login")
}
// HandleLogin 登录逻辑
func (b *Bot) HandleLogin(path *url.URL) error {
func (b *Bot) HandleLogin(data []byte) error {
// 获取登录的一些基本的信息
info, err := b.Caller.GetLoginInfo(path)
info, err := b.Caller.GetLoginInfo(data)
if err != nil {
return err
}
@ -169,10 +231,9 @@ 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
resp.ContactList.init(b.self)
b.self.Self = b.self
b.Storage.Response = resp
// 通知手机客户端已经登录
@ -184,7 +245,7 @@ func (b *Bot) WebInit() error {
// FIX: 当bot在线的情况下执行热登录,会开启多次事件监听
go b.once.Do(func() {
if b.MessageErrorHandler == nil {
b.MessageErrorHandler = defaultSyncCheckErrHandler(b)
b.MessageErrorHandler = b.stopSyncCheck
}
for {
err := b.syncCheck()
@ -193,8 +254,6 @@ func (b *Bot) WebInit() error {
}
// 判断是否继续, 如果不继续则退出
if goon := b.MessageErrorHandler(err); !goon {
b.err = err
b.Exit()
break
}
}
@ -223,8 +282,8 @@ func (b *Bot) syncCheck() error {
if !resp.Success() {
return resp.Err()
}
switch resp.Selector {
case SelectorNewMsg:
// 如果Selector不为0则获取消息
if !resp.NorMal() {
messages, err := b.syncMessage()
if err != nil {
return err
@ -237,17 +296,20 @@ 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 {
b.err = err
b.Exit()
return false
}
// 获取新的消息
func (b *Bot) syncMessage() ([]*Message, error) {
resp, err := b.Caller.WebWxSync(b.Storage.Request, b.Storage.Response, b.Storage.LoginInfo)
@ -264,7 +326,7 @@ 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
}
@ -282,6 +344,16 @@ 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 {
@ -293,87 +365,59 @@ func (b *Bot) DumpHotReloadStorage() error {
// DumpTo 将热登录需要的数据写入到指定的 io.Writer 中
// 注: 写之前最好先清空之前的数据
func (b *Bot) DumpTo(writer io.Writer) error {
jar := b.Caller.Client.Jar()
cookies := b.Caller.Client.GetCookieMap()
item := HotReloadStorageItem{
BaseRequest: b.Storage.Request,
Jar: fromCookieJar(jar),
Cookies: cookies,
LoginInfo: b.Storage.LoginInfo,
WechatDomain: b.Caller.Client.Domain,
UUID: b.uuid,
}
return b.Serializer.Encode(writer, item)
return json.NewEncoder(writer).Encode(item)
}
// IsHot returns true if is hot login otherwise false
func (b *Bot) IsHot() bool {
return b.hotReloadStorage != nil
// OnLogin is a setter for LoginCallBack
func (b *Bot) OnLogin(f func(body []byte)) {
b.LoginCallBack = f
}
// UUID returns current UUID of bot
func (b *Bot) UUID() string {
return b.uuid
// OnScanned is a setter for ScanCallBack
func (b *Bot) OnScanned(f func(body []byte)) {
b.ScanCallBack = 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
// OnLogout is a setter for LogoutCallBack
func (b *Bot) OnLogout(f func(bot *Bot)) {
b.LogoutCallBack = f
}
// NewBot Bot的构造方法
// 接收外部的 context.Context用于控制Bot的存活
func NewBot(c context.Context) *Bot {
caller := DefaultCaller()
// 默认行为为网页版微信模式
caller.Client.SetMode(normal)
// 默认行为为桌面模式
caller.Client.SetMode(Normal)
ctx, cancel := context.WithCancel(c)
return &Bot{
Caller: caller,
Storage: &Storage{},
Serializer: &JsonSerializer{},
context: ctx,
cancel: cancel,
}
return &Bot{Caller: caller, Storage: &Storage{}, context: ctx, cancel: cancel}
}
// DefaultBot 默认的Bot的构造方法,
// mode不传入默认为 openwechat.Normal,详情见mode
// mode不传入默认为 openwechat.Desktop,详情见mode
//
// bot := openwechat.DefaultBot(openwechat.Desktop)
func DefaultBot(prepares ...BotPreparer) *Bot {
func DefaultBot(modes ...Mode) *Bot {
bot := NewBot(context.Background())
if len(modes) > 0 {
bot.Caller.Client.SetMode(modes[0])
}
// 获取二维码回调
bot.UUIDCallback = PrintlnQrcodeUrl
// 扫码回调
bot.ScanCallBack = func(_ CheckLoginResponse) {
bot.ScanCallBack = func(body []byte) {
log.Println("扫码成功,请在手机上确认登录")
}
// 登录回调
bot.LoginCallBack = func(_ CheckLoginResponse) {
bot.LoginCallBack = func(body []byte) {
log.Println("登录成功")
}
// 心跳回调函数
@ -381,27 +425,9 @@ func DefaultBot(prepares ...BotPreparer) *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
@ -436,3 +462,13 @@ 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.hotReloadStorage != nil
}
// UUID returns current uuid of bot
func (b *Bot) UUID() string {
return b.uuid
}

View File

@ -1,363 +0,0 @@
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)
}

View File

@ -128,30 +128,4 @@ 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
}
}

View File

@ -50,8 +50,8 @@ func (c *Caller) GetLoginUUID() (string, error) {
}
// CheckLogin 检查是否登录成功
func (c *Caller) CheckLogin(uuid, tip string) (CheckLoginResponse, error) {
resp, err := c.Client.CheckLogin(uuid, tip)
func (c *Caller) CheckLogin(uuid string) (*CheckLoginResponse, error) {
resp, err := c.Client.CheckLogin(uuid)
if err != nil {
return nil, err
}
@ -61,13 +61,29 @@ func (c *Caller) CheckLogin(uuid, tip string) (CheckLoginResponse, error) {
if _, err := buffer.ReadFrom(resp.Body); err != nil {
return nil, err
}
return buffer.Bytes(), nil
// 正则匹配检测的code
// 具体code参考global.go
results := statusCodeRegexp.FindSubmatch(buffer.Bytes())
if len(results) != 2 {
return nil, errors.New("error status code match")
}
code := string(results[1])
return &CheckLoginResponse{Code: code, Raw: buffer.Bytes()}, nil
}
// GetLoginInfo 获取登录信息
func (c *Caller) GetLoginInfo(path *url.URL) (*LoginInfo, error) {
func (c *Caller) GetLoginInfo(body []byte) (*LoginInfo, error) {
// 从响应体里面获取需要跳转的url
resp, err := c.Client.GetLoginInfo(path)
results := redirectUriRegexp.FindSubmatch(body)
if len(results) != 2 {
return nil, errors.New("redirect url does not match")
}
path, err := url.Parse(string(results[1]))
if err != nil {
return nil, err
}
c.Client.Domain = WechatDomain(path.Host)
resp, err := c.Client.GetLoginInfo(path.String())
if err != nil {
return nil, err
}
@ -131,7 +147,7 @@ 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]), Selector(results[2])
retCode, selector := string(results[1]), string(results[2])
syncCheckResponse := &SyncCheckResponse{RetCode: retCode, Selector: selector}
return syncCheckResponse, nil
}
@ -244,12 +260,7 @@ func (c *Caller) UploadMedia(file *os.File, request *BaseRequest, info *LoginInf
}
// WebWxSendImageMsg 发送图片消息接口
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()
func (c *Caller) WebWxSendImageMsg(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
// 首先尝试上传图片
var mediaId string
{
@ -271,12 +282,7 @@ func (c *Caller) WebWxSendImageMsg(reader io.Reader, request *BaseRequest, info
return parser.SentMessage(msg)
}
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()
func (c *Caller) WebWxSendFile(file *os.File, req *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
resp, err := c.UploadMedia(file, req, info, fromUserName, toUserName)
if err != nil {
return nil, err
@ -292,12 +298,7 @@ func (c *Caller) WebWxSendFile(reader io.Reader, req *BaseRequest, info *LoginIn
return c.WebWxSendAppMsg(msg, req)
}
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()
func (c *Caller) WebWxSendVideoMsg(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
var mediaId string
{
resp, err := c.UploadMedia(file, request, info, fromUserName, toUserName)
@ -412,7 +413,7 @@ func (c *Caller) WebWxRelationPin(request *BaseRequest, user *User, op uint8) er
}
// WebWxPushLogin 免扫码登陆接口
func (c *Caller) WebWxPushLogin(uin int64) (*PushLoginResponse, error) {
func (c *Caller) WebWxPushLogin(uin int) (*PushLoginResponse, error) {
resp, err := c.Client.WebWxPushLogin(uin)
if err != nil {
return nil, err
@ -499,29 +500,3 @@ func (p *MessageResponseParser) SentMessage(msg *SendMessage) (*SentMessage, 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
}

186
client.go
View File

@ -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,44 +32,32 @@ 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(_ *http.Response, _ error) {}
func (u UserAgentHook) AfterRequest(response *http.Response, err error) {}
// Client http请求客户端
// 客户端需要维持Session会话
// 并且客户端不允许跳转
type Client struct {
// 设置一些client的请求行为
// see normalMode desktopMode
mode Mode
// client http客户端
client *http.Client
// Domain 微信服务器请求域名
// 这个参数会在登录成功后被赋值
// 之后所有的请求都会使用这个域名
// 在登录热登录和扫码登录时会被重新赋值
Domain WechatDomain
// HttpHooks 请求上下文钩子
HttpHooks HttpHooks
// MaxRetryTimes 最大重试次数
*http.Client
Domain WechatDomain
mode Mode
mu sync.Mutex
MaxRetryTimes int
cookies map[string][]*http.Cookie
}
func NewClient() *Client {
httpClient := &http.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
},
// 设置30秒超时
// 因为微信同步消息时是一个时间长达25秒的长轮训
Timeout: 30 * time.Second,
}
client := &Client{client: httpClient}
client.SetCookieJar(NewJar())
return client
Jar: jar,
Timeout: timeout,
}}
}
// DefaultClient 自动存储cookie
@ -94,7 +82,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
err error
)
for i := 0; i < c.MaxRetryTimes; i++ {
resp, err = c.client.Do(req)
resp, err = c.Client.Do(req)
if err == nil {
break
}
@ -108,23 +96,30 @@ 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) {
return c.do(req)
resp, err := c.do(req)
if err == nil {
c.setCookie(resp)
}
return resp, err
}
// 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()
// GetCookieMap 获取当前client的所有的有效的client
func (c *Client) GetCookieMap() map[string][]*http.Cookie {
return c.cookies
}
// GetLoginUUID 获取登录的uuid
@ -135,11 +130,11 @@ func (c *Client) GetLoginUUID() (*http.Response, error) {
// GetLoginQrcode 获取登录的二维吗
func (c *Client) GetLoginQrcode(uuid string) (*http.Response, error) {
path := qrcode + uuid
return c.client.Get(path)
return c.Get(path)
}
// CheckLogin 检查是否登录
func (c *Client) CheckLogin(uuid, tip string) (*http.Response, error) {
func (c *Client) CheckLogin(uuid string) (*http.Response, error) {
path, _ := url.Parse(login)
now := time.Now().Unix()
params := url.Values{}
@ -147,16 +142,15 @@ func (c *Client) CheckLogin(uuid, tip string) (*http.Response, error) {
params.Add("_", strconv.FormatInt(now, 10))
params.Add("loginicon", "true")
params.Add("uuid", uuid)
params.Add("tip", tip)
params.Add("tip", "0")
path.RawQuery = params.Encode()
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
return c.Do(req)
}
// GetLoginInfo 请求获取LoginInfo
func (c *Client) GetLoginInfo(path *url.URL) (*http.Response, error) {
c.Domain = WechatDomain(path.Host)
return c.mode.GetLoginInfo(c, path.String())
func (c *Client) GetLoginInfo(path string) (*http.Response, error) {
return c.mode.GetLoginInfo(c, path)
}
// WebInit 请求获取初始化信息
@ -302,7 +296,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")
@ -314,8 +308,6 @@ 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)
@ -336,22 +328,15 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
return nil, err
}
fileMd5 := hex.EncodeToString(h.Sum(nil))
fileMd5 := fmt.Sprintf("%x", 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(filename)
mediaType := getMessageType(sate.Name())
path, _ := url.Parse(c.Domain.FileHost() + webwxuploadmedia)
params := url.Values{}
@ -359,12 +344,8 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
path.RawQuery = params.Encode()
cookies := c.Jar().Cookies(path)
webWxDataTicket, err := getWebWxDataTicket(cookies)
if err != nil {
return nil, err
}
cookies := c.Jar.Cookies(path)
webWxDataTicket := getWebWxDataTicket(cookies)
uploadMediaRequest := map[string]interface{}{
"UploadType": 2,
@ -399,7 +380,7 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
content := map[string]string{
"id": "WU_FILE_0",
"name": filename,
"name": file.Name(),
"type": contentType,
"lastModifiedDate": sate.ModTime().Format(TimeFormat),
"size": strconv.FormatInt(sate.Size(), 10),
@ -416,17 +397,16 @@ 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)
}
formBuffer.Reset()
var formBuffer = bytes.NewBuffer(nil)
writer := multipart.NewWriter(formBuffer)
@ -441,33 +421,34 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
}
w, err := writer.CreateFormFile("filename", file.Name())
if err != nil {
return nil, err
}
n, err := file.Read(chunkBuff)
chunkData := make([]byte, chunkSize)
n, err := file.Read(chunkData)
if err != nil && err != io.EOF {
return nil, err
}
if _, err = w.Write(chunkBuff[:n]); err != nil {
if _, err = w.Write(chunkData[: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 {
parser := MessageResponseParser{Reader: resp.Body}
@ -597,21 +578,17 @@ 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", strconv.FormatInt(info.WxUin, 10))
params.Add("fromuser", fmt.Sprintf("%d", info.WxUin))
params.Add("pass_ticket", info.PassTicket)
params.Add("webwx_data_ticket", webWxDataTicket)
params.Add("webwx_data_ticket", getWebWxDataTicket(c.Jar.Cookies(path)))
path.RawQuery = params.Encode()
req, _ := http.NewRequest(http.MethodGet, path.String(), nil)
req.Header.Add("Referer", c.Domain.BaseHost()+"/")
req.Header.Add("Referer", path.String())
req.Header.Add("Range", "bytes=0-")
return c.Do(req)
}
@ -629,14 +606,6 @@ 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")
@ -658,29 +627,6 @@ 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)
@ -771,8 +717,12 @@ func (c *Client) WebWxRelationPin(request *BaseRequest, op uint8, user *User) (*
}
// WebWxPushLogin 免扫码登陆接口
func (c *Client) WebWxPushLogin(uin int64) (*http.Response, error) {
return c.mode.PushLogin(c, uin)
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)
}
// WebWxSendVideoMsg 发送视频消息接口

View File

@ -1,81 +0,0 @@
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
}

View File

@ -26,15 +26,6 @@ var (
// 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

View File

@ -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="(.*?)"`)
)
@ -98,6 +98,20 @@ const (
AppMsgTypeReaderType AppMessageType = 100001 //自定义的消息
)
// 登录状态
const (
StatusSuccess = "200"
StatusScanned = "201"
StatusTimeout = "400"
StatusWait = "408"
)
// errors
var (
ErrNoSuchUserFoundError = errors.New("no such user found")
ErrLoginTimeout = errors.New("login timeout")
)
// ALL 跟search函数搭配
//
// friends.Search(openwechat.ALL, )
@ -129,7 +143,8 @@ var imageType = map[string]struct{}{
"jpg": {},
}
const videoType = "mp4"
var videoType = "mp4"
// FileHelper 文件传输助手
const FileHelper = "filehelper"
// ZombieText 检测僵尸好友字符串
// 发送该字符给好友,能正常发送不报错的为正常好友,否则为僵尸好友
const ZombieText = "وُحfخe ̷̴̐nخg ̷̴̐cخh ̷̴̐aخo امارتيخ ̷̴̐خ\n"

View File

@ -2,8 +2,7 @@ package openwechat
import (
"errors"
"fmt"
"net/url"
"strconv"
)
/*
@ -57,9 +56,9 @@ type WebInitResponse struct {
SKey string
BaseResponse BaseResponse
SyncKey SyncKey
User *User
MPSubscribeMsgList []*MPSubscribeMsg
ContactList Members
User User
MPSubscribeMsgList []MPSubscribeMsg
ContactList []User
}
// MPSubscribeMsg 公众号的订阅信息
@ -68,14 +67,12 @@ type MPSubscribeMsg struct {
Time int64
UserName string
NickName string
MPArticleList []*MPArticle
}
type MPArticle struct {
MPArticleList []struct {
Title string
Cover string
Digest string
Url string
}
}
type UserDetailItem struct {
@ -94,6 +91,30 @@ 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"
}
func (s SyncCheckResponse) Err() error {
if s.Success() {
return nil
}
i, err := strconv.ParseInt(s.RetCode, 16, 10)
if err != nil {
return errors.New("sync check unknown error")
}
return errors.New(Ret(i).String())
}
type WebWxSyncResponse struct {
AddMsgCount int
ContinueFlag int
@ -121,49 +142,9 @@ type WebWxBatchContactResponse struct {
ContactList []*User
}
// 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 CheckLoginResponse struct {
Code string
Raw []byte
}
type MessageResponse struct {
@ -186,10 +167,3 @@ type PushLoginResponse struct {
func (p PushLoginResponse) Ok() bool {
return p.Ret == "0" && p.UUID != ""
}
func (p PushLoginResponse) Err() error {
if p.Ok() {
return nil
}
return errors.New(p.Msg)
}

View File

@ -50,7 +50,7 @@ type Message struct {
Url string
senderInGroupUserName string
RecommendInfo RecommendInfo
bot *Bot
Bot *Bot `json:"-"`
mu sync.RWMutex
Context context.Context `json:"-"`
item map[string]interface{}
@ -60,18 +60,18 @@ type Message struct {
// 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()
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}
user = &User{Self: m.Bot.self, UserName: m.FromUserName}
err = user.Detail()
}
if m.IsSendByGroup() && len(user.MemberList) == 0 {
@ -89,8 +89,8 @@ 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")
}
@ -110,16 +110,15 @@ func (m *Message) SenderInGroup() (*User, error) {
// 如果消息是好友消息,则返回好友
// 如果消息是系统消息,则返回当前用户
func (m *Message) Receiver() (*User, error) {
if m.IsSystem() || m.ToUserName == m.bot.self.UserName {
return m.bot.self.User, nil
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.ToUserName == m.Bot.self.fileHelper.UserName {
return m.Bot.self.fileHelper.User, nil
}
if m.IsSendByGroup() {
groups, err := m.bot.self.Groups()
groups, err := m.Bot.self.Groups()
if err != nil {
return nil, err
}
@ -133,7 +132,7 @@ func (m *Message) Receiver() (*User, error) {
}
return users.First().User, nil
} else {
user, exist := m.bot.self.MemberList.GetByUserName(m.ToUserName)
user, exist := m.Bot.self.MemberList.GetByUserName(m.ToUserName)
if !exist {
return nil, ErrNoSuchUserFoundError
}
@ -143,7 +142,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 判断消息是否由好友发送
@ -158,35 +157,35 @@ func (m *Message) IsSendByGroup() bool {
// ReplyText 回复文本消息
func (m *Message) ReplyText(content string) (*SentMessage, error) {
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)
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 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)
func (m *Message) ReplyImage(file *os.File) (*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 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)
func (m *Message) ReplyVideo(file *os.File) (*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 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) ReplyFile(file *os.File) (*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 {
@ -291,16 +290,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")
}
@ -310,7 +309,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 获取录音消息的响应
@ -318,7 +317,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 获取视频消息的响应
@ -326,7 +325,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 获取媒体消息的响应
@ -334,7 +333,7 @@ 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
@ -393,11 +392,11 @@ func (m *Message) Agree(verifyContents ...string) (*Friend, error) {
if !m.IsFriendAdd() {
return nil, errors.New("friend add message required")
}
err := 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)
friend := newFriend(m.RecommendInfo.UserName, m.Bot.self)
if err = friend.Detail(); err != nil {
return nil, err
}
@ -406,7 +405,7 @@ func (m *Message) Agree(verifyContents ...string) (*Friend, error) {
// 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 判断当前的消息类型是否为文章
@ -448,7 +447,7 @@ 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.RawContent = m.Content
@ -630,13 +629,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 是否可以撤回该消息
@ -658,7 +657,7 @@ func (s *SentMessage) ForwardToFriends(friends ...*Friend) error {
// ForwardToFriendsWithDelay 转发该消息给好友,延迟指定时间
func (s *SentMessage) ForwardToFriendsWithDelay(delay time.Duration, friends ...*Friend) error {
return s.self.ForwardMessageToFriends(s, delay, friends...)
return s.Self.ForwardMessageToFriends(s, delay, friends...)
}
// ForwardToGroups 转发该消息给群组
@ -670,7 +669,7 @@ func (s *SentMessage) ForwardToGroups(groups ...*Group) error {
// ForwardToGroupsWithDelay 转发该消息给群组, 延迟指定时间
func (s *SentMessage) ForwardToGroupsWithDelay(delay time.Duration, groups ...*Group) error {
return s.self.ForwardMessageToGroups(s, delay, groups...)
return s.Self.ForwardMessageToGroups(s, delay, groups...)
}
type appmsg struct {
@ -791,7 +790,7 @@ func (m *Message) IsAt() bool {
// IsPaiYiPai 判断消息是否为拍一拍
// 不要问我为什么取名为PaiYiPai因为我也不知道取啥名字好
func (m *Message) IsPaiYiPai() bool {
return m.IsTickled()
return m.IsSystem() && strings.Contains(m.Content, "拍了拍")
}
// IsJoinGroup 判断是否有人加入了群聊
@ -801,25 +800,10 @@ func (m *Message) IsJoinGroup() bool {
// IsTickled 判断消息是否为拍一拍
func (m *Message) IsTickled() bool {
return m.IsSystem() && strings.Contains(m.Content, "拍了拍")
}
// IsTickledMe 判断消息是否拍了拍自己
func (m *Message) IsTickledMe() bool {
return m.IsSystem() && strings.Count(m.Content, "拍了拍我") == 1
return m.IsPaiYiPai()
}
// 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
}

View File

@ -11,6 +11,14 @@ type MessageDispatcher interface {
Dispatch(msg *Message)
}
// DispatchMessage 跟 MessageDispatcher 结合封装成 MessageHandler
// Deprecated: use MessageMatchDispatcher.AsMessageHandler instead
func DispatchMessage(dispatcher MessageDispatcher) func(msg *Message) {
return func(msg *Message) { dispatcher.Dispatch(msg) }
}
// MessageDispatcher impl
// MessageContextHandler 消息处理函数
type MessageContextHandler func(ctx *MessageContext)

26
mode.go
View File

@ -10,28 +10,16 @@ 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 网页版模式
normal Mode = normalMode{}
Normal Mode = normalMode{}
// desktop 桌面模式uos electron套壳
desktop Mode = desktopMode{}
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{}
@ -77,13 +65,3 @@ 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)
}

View File

@ -38,6 +38,15 @@ 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 ""
}
// GetFileContentType 获取文件上传的类型
func GetFileContentType(file multipart.File) (string, error) {
data := make([]byte, 512)

View File

@ -2,49 +2,45 @@ package openwechat
import (
"fmt"
"io"
"os"
"time"
)
type Friend struct{ *User }
// implement fmt.Stringer
func (f *Friend) String() string {
display := f.NickName
if f.RemarkName != "" {
display = f.RemarkName
}
return fmt.Sprintf("<Friend:%s>", display)
func (f Friend) String() string {
return fmt.Sprintf("<Friend:%s>", f.NickName)
}
// SetRemarkName 重命名当前好友
func (f *Friend) SetRemarkName(name string) error {
return f.self.SetRemarkNameToFriend(f, name)
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 io.Reader) (*SentMessage, error) {
return f.self.SendImageToFriend(f, file)
func (f *Friend) SendImage(file *os.File) (*SentMessage, error) {
return f.Self.SendImageToFriend(f, file)
}
// SendVideo 发送视频消息
func (f *Friend) SendVideo(file io.Reader) (*SentMessage, error) {
return f.self.SendVideoToFriend(f, file)
func (f *Friend) SendVideo(file *os.File) (*SentMessage, error) {
return f.Self.SendVideoToFriend(f, file)
}
// SendFile 发送文件消息
func (f *Friend) SendFile(file io.Reader) (*SentMessage, error) {
return f.self.SendFileToFriend(f, file)
func (f *Friend) SendFile(file *os.File) (*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
@ -57,7 +53,7 @@ func (f Friends) Count() int {
// First 获取第一个好友
func (f Friends) First() *Friend {
if f.Count() > 0 {
return f.Sort()[0]
return f[0]
}
return nil
}
@ -65,7 +61,7 @@ func (f Friends) First() *Friend {
// Last 获取最后一个好友
func (f Friends) Last() *Friend {
if f.Count() > 0 {
return f.Sort()[f.Count()-1]
return f[f.Count()-1]
}
return nil
}
@ -107,16 +103,6 @@ func (f Friends) AsMembers() Members {
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, delays ...time.Duration) error {
if f.Count() == 0 {
@ -126,12 +112,12 @@ func (f Friends) SendText(text string, delays ...time.Duration) error {
if len(delays) > 0 {
delay = delays[0]
}
self := f.First().self
self := f.First().Self
return self.SendTextToFriends(text, delay, f...)
}
// SendImage 向slice的好友依次发送图片消息
func (f Friends) SendImage(file io.Reader, delays ...time.Duration) error {
func (f Friends) SendImage(file *os.File, delays ...time.Duration) error {
if f.Count() == 0 {
return nil
}
@ -139,12 +125,12 @@ func (f Friends) SendImage(file io.Reader, delays ...time.Duration) error {
if len(delays) > 0 {
delay = delays[0]
}
self := f.First().self
self := f.First().Self
return self.SendImageToFriends(file, delay, f...)
}
// SendFile 群发文件
func (f Friends) SendFile(file io.Reader, delay ...time.Duration) error {
func (f Friends) SendFile(file *os.File, delay ...time.Duration) error {
if f.Count() == 0 {
return nil
}
@ -152,35 +138,35 @@ func (f Friends) SendFile(file io.Reader, delay ...time.Duration) error {
if len(delay) > 0 {
d = delay[0]
}
self := f.First().self
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)
}
// 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 io.Reader) (*SentMessage, error) {
return g.self.SendImageToGroup(g, file)
func (g *Group) SendImage(file *os.File) (*SentMessage, error) {
return g.Self.SendImageToGroup(g, file)
}
// SendVideo 发行视频消息给当前的群组
func (g *Group) SendVideo(file io.Reader) (*SentMessage, error) {
return g.self.SendVideoToGroup(g, file)
func (g *Group) SendVideo(file *os.File) (*SentMessage, error) {
return g.Self.SendVideoToGroup(g, file)
}
// SendFile 发送文件给当前的群组
func (g *Group) SendFile(file io.Reader) (*SentMessage, error) {
return g.self.SendFileToGroup(g, file)
func (g *Group) SendFile(file *os.File) (*SentMessage, error) {
return g.Self.SendFileToGroup(g, file)
}
// Members 获取所有的群成员
@ -188,26 +174,25 @@ 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 {
friends = Friends(friends).Uniq()
return g.self.AddFriendsIntoGroup(g, friends...)
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 根据用户名查找群成员
@ -242,7 +227,7 @@ func (g Groups) Count() int {
// First 获取第一个群组
func (g Groups) First() *Group {
if g.Count() > 0 {
return g.Sort()[0]
return g[0]
}
return nil
}
@ -250,7 +235,7 @@ func (g Groups) First() *Group {
// Last 获取最后一个群组
func (g Groups) Last() *Group {
if g.Count() > 0 {
return g.Sort()[g.Count()-1]
return g[g.Count()-1]
}
return nil
}
@ -264,12 +249,12 @@ func (g Groups) SendText(text string, delay ...time.Duration) error {
if len(delay) > 0 {
d = delay[0]
}
self := g.First().self
self := g.First().Self
return self.SendTextToGroups(text, d, g...)
}
// SendImage 向群组依次发送图片消息, 支持发送延迟
func (g Groups) SendImage(file io.Reader, delay ...time.Duration) error {
func (g Groups) SendImage(file *os.File, delay ...time.Duration) error {
if g.Count() == 0 {
return nil
}
@ -277,12 +262,12 @@ func (g Groups) SendImage(file io.Reader, delay ...time.Duration) error {
if len(delay) > 0 {
d = delay[0]
}
self := g.First().self
self := g.First().Self
return self.SendImageToGroups(file, d, g...)
}
// SendFile 向群组依次发送文件消息, 支持发送延迟
func (g Groups) SendFile(file io.Reader, delay ...time.Duration) error {
func (g Groups) SendFile(file *os.File, delay ...time.Duration) error {
if g.Count() == 0 {
return nil
}
@ -290,7 +275,7 @@ func (g Groups) SendFile(file io.Reader, delay ...time.Duration) error {
if len(delay) > 0 {
d = delay[0]
}
self := g.First().self
self := g.First().Self
return self.SendFileToGroups(file, d, g...)
}
@ -304,6 +289,11 @@ 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, searchFuncList ...func(group *Group) bool) (results Groups) {
return g.AsMembers().Search(limit, func(user *User) bool {
@ -326,20 +316,10 @@ func (g Groups) AsMembers() Members {
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)
}
@ -354,7 +334,7 @@ func (m Mps) Count() int {
// First 获取第一个
func (m Mps) First() *Mp {
if m.Count() > 0 {
return m.Sort()[0]
return m[0]
}
return nil
}
@ -362,7 +342,7 @@ func (m Mps) First() *Mp {
// Last 获取最后一个
func (m Mps) Last() *Mp {
if m.Count() > 0 {
return m.Sort()[m.Count()-1]
return m[m.Count()-1]
}
return nil
}
@ -389,16 +369,6 @@ func (m Mps) AsMembers() Members {
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 根据用户名查找
func (m Mps) SearchByUserName(limit int, userName string) (results Mps) {
return m.Search(limit, func(group *Mp) bool { return group.UserName == userName })
@ -411,17 +381,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 io.Reader) (*SentMessage, error) {
return m.self.SendImageToMp(m, file)
func (m *Mp) SendImage(file *os.File) (*SentMessage, error) {
return m.Self.SendImageToMp(m, file)
}
// SendFile 发送文件消息给公众号
func (m *Mp) SendFile(file io.Reader) (*SentMessage, error) {
return m.self.SendFileToMp(m, file)
func (m *Mp) SendFile(file *os.File) (*SentMessage, error) {
return m.Self.SendFileToMp(m, file)
}
// GetByUsername 根据username查询一个Friend
@ -444,6 +414,11 @@ 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()

View File

@ -1,25 +0,0 @@
package openwechat
import (
"encoding/json"
"io"
)
// Serializer is an interface for encoding and decoding data.
type Serializer interface {
Encode(writer io.Writer, v interface{}) error
Decode(reader io.Reader, v interface{}) error
}
// JsonSerializer is a serializer for json.
type JsonSerializer struct{}
// Encode encodes v to writer.
func (j JsonSerializer) Encode(writer io.Writer, v interface{}) error {
return json.NewEncoder(writer).Encode(v)
}
// Decode decodes data from reader to v.
func (j JsonSerializer) Decode(reader io.Reader, v interface{}) error {
return json.NewDecoder(reader).Decode(v)
}

View File

@ -60,7 +60,7 @@ func main() {
#### 扫码登录
#### 普通登录
上面的准备工作做完了,下面就可以登录,直接调用`Bot.Login`即可。
@ -68,8 +68,6 @@ func main() {
bot.Login()
```
`Login`方法会阻塞当前 goroutine直到登录成功或者失败。
登录会返回一个`error`,即登录失败的原因。
@ -84,25 +82,12 @@ 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.ReadWriter
@ -112,46 +97,6 @@ type HotReloadStorage io.ReadWriter
实现这个接口,实现你自己的存储方式。
#### 免扫码登录
目前热登录有一点缺点就是它的有效期很短(具体多久我也不知道)。
我们平常在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`了,就会往手机上发送确认登录的请求。
### 扫码回调
@ -161,21 +106,13 @@ err = bot.PushLogin(reloadStorage, openwechat.NewRetryLoginOption())
通过对`bot`对象绑定扫码回调即可实现对应的功能。
```go
bot.ScanCallBack = func(body openwechat.CheckLoginResponse) { fmt.Println(string(body)) }
bot.ScanCallBack = func(body []byte) { fmt.Println(string(body)) }
```
用户扫码后body里面会携带用户的头像信息。
**注**:绑定扫码回调须在登录前执行。
`CheckLoginResponse` 是一个`[]byte`包装类型, 扫码成功后可以通过该类型获取用户的头像信息。
```go
type CheckLoginResponse []byte
func (c CheckLoginResponse) Avatar() (string, error)
```
### 登录回调
@ -183,13 +120,13 @@ func (c CheckLoginResponse) Avatar() (string, error)
`bot`对象绑定登录
```go
bot.LoginCallBack = func(body openwechat.CheckLoginResponse) {
bot.LoginCallBack = func(body []byte) {
fmt.Println(string(body))
// to do your business
}
```
登录回调的参数就是当前客户端需要跳转的链接,用户可以不用关心它。(其实可以拿来做一些骚操作😈)
登录回调的参数就是当前客户端需要跳转的链接,可以不用关心它。
登录回调函数可以当做一个信号处理,表示当前扫码登录的用户已经确认登录。
@ -241,7 +178,7 @@ dispatcher.OnText(func(ctx *openwechat.MessageContext){
})
// 注册消息回调函数
bot.MessageHandler = dispatcher.AsMessageHandler()
bot.MessageHandler = openwechat.DispatchMessage(dispatcher)
```
`openwechat.DispatchMessage`会将消息转发给`dispatcher`对象处理
@ -325,29 +262,5 @@ 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`
该方法会一直阻塞,直到用户主动退出或者网络请求发生错误

View File

@ -1,5 +1,7 @@
# 消息
### 接受消息
被动接受的消息对象,由微信服务器发出
@ -7,15 +9,17 @@
消息对象通过绑定在`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
@ -26,6 +30,8 @@ msg.Content // 获取消息内容
由于消息分为很多种类型,它们都共用`Content`属性。一般当消息类型为文本类型的时候,我们才会去访问`Content`属性。
#### 消息类型判断
下面的判断消息类型的方法均返回`bool`
@ -111,17 +117,13 @@ msg.IsIsPaiYiPai() // 拍一拍消息
msg.IsTickled()
```
##### 判断是否拍了拍自己
```go
msg.IsTickledMe()
```
##### 判断是否有新人加入群聊
```go
msg.IsJoinGroup()
```
#### 获取消息的发送者
```go
@ -130,12 +132,16 @@ sender, err := msg.Sender()
如果是群聊消息,该方法返回的是群聊对象(需要自己将`User`转换为`Group`对象)
#### 获取消息的接受者
```go
receiver, err := msg.Receiver()
```
#### 获取消息在群里面的发送者
```go
@ -144,17 +150,15 @@ sender, err := msg.SenderInGroup()
获取群聊中具体发消息的用户,前提该消息必须来自群聊。
#### 是否由自己发送
```go
msg.IsSendBySelf()
```
#### 是否为拍一拍
```go
msg.IsTickled()
```
#### 消息是否由好友发出
@ -162,18 +166,24 @@ msg.IsTickled()
msg.IsSendByFriend()
```
#### 消息是否由群聊发出
```go
msg.IsSendByGroup()
```
#### 回复文本消息
```go
msg.ReplyText("hello")
```
#### 回复图片消息
```go
@ -182,6 +192,8 @@ defer img.Close()
msg.ReplyImage(img)
```
#### 回复文件消息
```go
@ -190,12 +202,14 @@ defer file.Close()
msg.ReplyFile(file)
```
#### 获取消息里的其他信息
##### 名片消息
```go
card, err := msg.Card()
card, err := msg. Card()
```
该方法调用的前提为`msg.IsCard()`返回为`true`
@ -211,29 +225,31 @@ 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
@ -246,17 +262,19 @@ 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
@ -268,6 +286,8 @@ friend, err := msg.Agree()
该方法调用成功的前提是`msg.IsFriendAdd()`返回为`true`
#### 设置为已读
```go
@ -276,6 +296,10 @@ msg.AsRead()
该当前消息设置为已读
#### 设置消息的上下文
用于多个消息处理函数之间的通信,并且是协程安全的。
@ -292,6 +316,10 @@ msg.Set("hello", "world")
value, exist := msg.Get("hello")
```
### 已发送消息
已发送消息指当前用户发送出去的消息
@ -306,6 +334,8 @@ sentMsg, err := msg.ReplyText("hello") // 通过回复消息获取
// and so on
```
#### 撤回消息
撤回刚刚发送的消息撤回消息的有效时间为2分钟超过了这个时间则无法撤回
@ -314,12 +344,16 @@ sentMsg, err := msg.ReplyText("hello") // 通过回复消息获取
sentMsg.Revoke()
```
#### 判断是否可以撤回
```go
sentMsg.CanRevoke()
```
#### 转发给好友
```go
@ -328,6 +362,8 @@ sentMsg.ForwardToFriends(friend1, friend2)
将刚发送的消息转发给好友
#### 转发给群聊
```go
@ -336,6 +372,10 @@ sentMsg.ForwardToGroups(group1, group2)
将刚发送的消息转发给群聊
### Emoji表情
openwechat提供了微信全套`emoji`表情的支持

View File

@ -400,10 +400,10 @@ func (f Friends) SendText(text string, delay ...time.Duration) error
#### 群发图片
```go
func (f Friends) SendImage(file io.Reader, delay ...time.Duration) error
func (f Friends) SendImage(file *os.File, delay ...time.Duration) error
```
* `file``io.Reader`类型。
* `file``os.file`类型,即发送图片的文件指针
* `delay`每次发送消息的间隔发送消息过快可能会被wx检测到最好加上间隔时间
@ -411,10 +411,10 @@ func (f Friends) SendImage(file io.Reader, delay ...time.Duration) error
#### 群发文件
```go
func (f Friends) SendFile(file io.Reader, delay ...time.Duration) error
func (f Friends) SendFile(file *os.File, delay ...time.Duration) error
```
* `file``io.Reader`类型。
* `file``os.file`类型,即发送文件的文件指针
* `delay`每次发送消息的间隔发送消息过快可能会被wx检测到最好加上间隔时间
@ -545,10 +545,10 @@ func (g Groups) SendText(text string, delay ...time.Duration) error
#### 群发图片
```go
func (g Groups) SendImage(file io.Reader, delay ...time.Duration) error
func (g Groups) SendImage(file *os.File, delay ...time.Duration) error
```
* `file``io.Reader`类型。
* `file``os.file`类型,即发送文件的文件指针
* `delay`每次发送消息的间隔发送消息过快可能会被wx检测到最好加上间隔时间
@ -556,10 +556,10 @@ func (g Groups) SendImage(file io.Reader, delay ...time.Duration) error
#### 群发文件
```go
func (g Groups) SendFile(file io.Reader, delay ...time.Duration) error
func (g Groups) SendFile(file *os.File, delay ...time.Duration) error
```
* `file``io.Reader`类型。
* `file``os.file`类型,即发送文件的文件指针
* `delay`每次发送消息的间隔发送消息过快可能会被wx检测到最好加上间隔时间

View File

@ -58,8 +58,6 @@ 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]
@ -70,36 +68,31 @@ func _() {
const (
_Ret_name_0 = "ticket error"
_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"
_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"
)
var (
_Ret_index_1 = [...]uint8{0, 11, 20}
_Ret_index_3 = [...]uint8{0, 17, 35, 49}
_Ret_index_2 = [...]uint8{0, 17, 35, 49}
)
func (i Ret) String() string {
func (r Ret) String() string {
switch {
case i == -14:
case r == -14:
return _Ret_name_0
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:
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:
return _Ret_name_4
case i == 1205:
return _Ret_name_5
default:
return "Ret(" + strconv.FormatInt(int64(i), 10) + ")"
return "Ret(" + strconv.FormatInt(int64(r), 10) + ")"
}
}
func _() {

View File

@ -2,9 +2,8 @@ package openwechat
import (
"io"
"net/http"
"os"
"sync"
"time"
)
// Storage 身份信息, 维持整个登陆的Session会话
@ -15,7 +14,7 @@ type Storage struct {
}
type HotReloadStorageItem struct {
Jar *Jar
Cookies map[string][]*http.Cookie
BaseRequest *BaseRequest
LoginInfo *LoginInfo
WechatDomain WechatDomain
@ -25,17 +24,14 @@ type HotReloadStorageItem struct {
// HotReloadStorage 热登陆存储接口
type HotReloadStorage io.ReadWriter
// fileHotReloadStorage 实现HotReloadStorage接口
// 以文件的形式存储
type fileHotReloadStorage struct {
// jsonFileHotReloadStorage 实现HotReloadStorage接口
// 默认json文件的形式存储
type jsonFileHotReloadStorage struct {
filename string
file *os.File
lock sync.Mutex
}
func (j *fileHotReloadStorage) Read(p []byte) (n int, err error) {
j.lock.Lock()
defer j.lock.Unlock()
func (j *jsonFileHotReloadStorage) Read(p []byte) (n int, err error) {
if j.file == nil {
j.file, err = os.OpenFile(j.filename, os.O_RDWR, 0600)
if os.IsNotExist(err) {
@ -48,74 +44,35 @@ func (j *fileHotReloadStorage) Read(p []byte) (n int, err error) {
return j.file.Read(p)
}
func (j *fileHotReloadStorage) Write(p []byte) (n int, err error) {
j.lock.Lock()
defer j.lock.Unlock()
func (j *jsonFileHotReloadStorage) Write(p []byte) (n int, err error) {
if j.file == nil {
j.file, err = os.Create(j.filename)
if err != nil {
return 0, err
}
}
// reset offset and truncate file
// 为什么这里要对文件进行Truncate操作呢?
// 这是为了方便每次Dump的时候对文件进行重新写入, 而不是追加
// json序列化写入只会调用一次Write方法, 所以不要把这个方法当成io.Writer的Write方法
if _, err = j.file.Seek(0, io.SeekStart); err != nil {
return
}
if err = j.file.Truncate(0); err != nil {
return
}
// json decode only write once
return j.file.Write(p)
}
func (j *fileHotReloadStorage) Close() error {
j.lock.Lock()
defer j.lock.Unlock()
func (j *jsonFileHotReloadStorage) Close() error {
if j.file == nil {
return nil
}
return j.file.Close()
}
// Deprecated: use NewFileHotReloadStorage instead
// 不再单纯以json的格式存储支持了用户自定义序列化方式
// NewJsonFileHotReloadStorage 创建JsonFileHotReloadStorage
func NewJsonFileHotReloadStorage(filename string) io.ReadWriteCloser {
return NewFileHotReloadStorage(filename)
return &jsonFileHotReloadStorage{filename: 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}
}
var _ HotReloadStorage = (*jsonFileHotReloadStorage)(nil)

View File

@ -1,44 +0,0 @@
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)
}

298
user.go
View File

@ -3,13 +3,10 @@ package openwechat
import (
"errors"
"fmt"
"html"
"io"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
@ -52,28 +49,18 @@ type User struct {
MemberList Members
self *Self
Self *Self `json:"-"`
}
// implement fmt.Stringer
func (u *User) String() string {
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)
return fmt.Sprintf("<User:%s>", u.NickName)
}
// GetAvatarResponse 获取用户头像
func (u *User) GetAvatarResponse() (resp *http.Response, err error) {
for i := 0; i < 3; i++ {
resp, err = u.self.bot.Caller.Client.WebWxGetHeadImg(u)
resp, err = u.Self.Bot.Caller.Client.WebWxGetHeadImg(u)
if err != nil {
return nil, err
}
@ -112,19 +99,19 @@ func (u *User) SaveAvatarWithWriter(writer io.Writer) error {
// 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
}
@ -133,50 +120,26 @@ 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 判断当前联系人(好友、群组、公众号)是否为置顶状态
@ -207,35 +170,6 @@ func (u *User) ID() string {
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)
@ -246,7 +180,7 @@ func (u *User) formatEmoji() {
// Self 自己,当前登录用户对象
type Self struct {
*User
bot *Bot
Bot *Bot
fileHelper *Friend
members Members
friends Friends
@ -263,14 +197,13 @@ 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
}
@ -288,18 +221,13 @@ func (s *Self) FileHelper() *Friend {
}
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 (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
if s.friends == nil || (len(update) > 0 && update[0]) {
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
@ -307,14 +235,10 @@ func (s *Self) Friends(update ...bool) (Friends, error) {
// Groups 获取所有的群组
func (s *Self) Groups(update ...bool) (Groups, error) {
if (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
if s.groups == nil || (len(update) > 0 && update[0]) {
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
@ -322,12 +246,10 @@ func (s *Self) Groups(update ...bool) (Groups, error) {
// Mps 获取所有的公众号
func (s *Self) Mps(update ...bool) (Mps, error) {
if (len(update) > 0 && update[0]) || s.ChkFrdGrpMpNil() {
if s.mps == nil || (len(update) > 0 && update[0]) {
if _, err := s.Members(true); err != nil {
return nil, err
}
}
if s.mps == nil || (len(update) > 0 && update[0]) {
s.mps = s.members.MPs()
}
return s.mps, nil
@ -347,30 +269,30 @@ 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
sentMessage, err := s.bot.Caller.WebWxSendMsg(msg, info, request)
info := s.Bot.Storage.LoginInfo
request := s.Bot.Storage.Request
sentMessage, err := s.Bot.Caller.WebWxSendMsg(msg, info, request)
return s.sendMessageWrapper(sentMessage, err)
}
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)
func (s *Self) sendImageToUser(user *User, file *os.File) (*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)
func (s *Self) sendVideoToUser(user *User, file *os.File) (*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)
func (s *Self) sendFileToUser(user *User, file *os.File) (*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)
}
@ -380,17 +302,17 @@ func (s *Self) SendTextToFriend(friend *Friend, text string) (*SentMessage, erro
}
// SendImageToFriend 发送图片消息给好友
func (s *Self) SendImageToFriend(friend *Friend, file io.Reader) (*SentMessage, error) {
func (s *Self) SendImageToFriend(friend *Friend, file *os.File) (*SentMessage, error) {
return s.sendImageToUser(friend.User, file)
}
// SendVideoToFriend 发送视频给好友
func (s *Self) SendVideoToFriend(friend *Friend, file io.Reader) (*SentMessage, error) {
func (s *Self) SendVideoToFriend(friend *Friend, file *os.File) (*SentMessage, error) {
return s.sendVideoToUser(friend.User, file)
}
// SendFileToFriend 发送文件给好友
func (s *Self) SendFileToFriend(friend *Friend, file io.Reader) (*SentMessage, error) {
func (s *Self) SendFileToFriend(friend *Friend, file *os.File) (*SentMessage, error) {
return s.sendFileToUser(friend.User, file)
}
@ -398,25 +320,24 @@ func (s *Self) SendFileToFriend(friend *Friend, file io.Reader) (*SentMessage, e
//
// 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
}
@ -427,7 +348,6 @@ 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 {
@ -441,9 +361,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 从群聊中移除用户
@ -472,15 +392,14 @@ 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
@ -491,9 +410,9 @@ 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)
req := s.Bot.Storage.Request
info := s.Bot.Storage.LoginInfo
return s.Bot.Caller.WebWxRenameChatRoom(req, info, newName, group)
}
// SendTextToGroup 发送文本消息给群组
@ -502,17 +421,17 @@ func (s *Self) SendTextToGroup(group *Group, text string) (*SentMessage, error)
}
// SendImageToGroup 发送图片消息给群组
func (s *Self) SendImageToGroup(group *Group, file io.Reader) (*SentMessage, error) {
func (s *Self) SendImageToGroup(group *Group, file *os.File) (*SentMessage, error) {
return s.sendImageToUser(group.User, file)
}
// SendVideoToGroup 发送视频给群组
func (s *Self) SendVideoToGroup(group *Group, file io.Reader) (*SentMessage, error) {
func (s *Self) SendVideoToGroup(group *Group, file *os.File) (*SentMessage, error) {
return s.sendVideoToUser(group.User, file)
}
// SendFileToGroup 发送文件给群组
func (s *Self) SendFileToGroup(group *Group, file io.Reader) (*SentMessage, error) {
func (s *Self) SendFileToGroup(group *Group, file *os.File) (*SentMessage, error) {
return s.sendFileToUser(group.User, file)
}
@ -523,19 +442,19 @@ func (s *Self) SendFileToGroup(group *Group, file io.Reader) (*SentMessage, erro
// 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, delay time.Duration, users ...*User) error {
info := s.bot.Storage.LoginInfo
req := s.bot.Storage.Request
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)
@ -544,7 +463,7 @@ func (s *Self) forwardMessage(msg *SentMessage, delay time.Duration, users ...*U
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)
@ -553,7 +472,7 @@ func (s *Self) forwardMessage(msg *SentMessage, delay time.Duration, users ...*U
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)
@ -591,7 +510,7 @@ func (s *Self) sendTextToMembers(text string, delay time.Duration, members ...*U
}
// sendImageToMembers 发送图片消息给群组或者好友
func (s *Self) sendImageToMembers(img io.Reader, delay time.Duration, members ...*User) error {
func (s *Self) sendImageToMembers(img *os.File, delay time.Duration, members ...*User) error {
if len(members) == 0 {
return nil
}
@ -605,7 +524,7 @@ func (s *Self) sendImageToMembers(img io.Reader, delay time.Duration, members ..
}
// sendVideoToMembers 发送视频消息给群组或者好友
func (s *Self) sendVideoToMembers(video io.Reader, delay time.Duration, members ...*User) error {
func (s *Self) sendVideoToMembers(video *os.File, delay time.Duration, members ...*User) error {
if len(members) == 0 {
return nil
}
@ -618,7 +537,7 @@ func (s *Self) sendVideoToMembers(video io.Reader, delay time.Duration, members
return s.forwardMessage(msg, delay, members[1:]...)
}
func (s *Self) sendFileToMembers(file io.Reader, delay time.Duration, members ...*User) error {
func (s *Self) sendFileToMembers(file *os.File, delay time.Duration, members ...*User) error {
if len(members) == 0 {
return nil
}
@ -638,19 +557,19 @@ func (s *Self) SendTextToFriends(text string, delay time.Duration, friends ...*F
}
// SendImageToFriends 发送图片消息给好友
func (s *Self) SendImageToFriends(img io.Reader, delay time.Duration, friends ...*Friend) error {
func (s *Self) SendImageToFriends(img *os.File, 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 {
func (s *Self) SendFileToFriends(file *os.File, 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 {
func (s *Self) SendVideoToFriends(video *os.File, delay time.Duration, friends ...*Friend) error {
members := Friends(friends).AsMembers()
return s.sendVideoToMembers(video, delay, members...)
}
@ -662,68 +581,26 @@ func (s *Self) SendTextToGroups(text string, delay time.Duration, groups ...*Gro
}
// SendImageToGroups 发送图片消息给群组
func (s *Self) SendImageToGroups(img io.Reader, delay time.Duration, groups ...*Group) error {
func (s *Self) SendImageToGroups(img *os.File, 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 {
func (s *Self) SendFileToGroups(file *os.File, 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 {
func (s *Self) SendVideoToGroups(video *os.File, 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)
@ -798,8 +675,8 @@ func (m Members) GetByNickName(nickname string) (*User, bool) {
func (m Members) Friends() Friends {
friends := make(Friends, 0)
for _, mb := range m {
friend, ok := mb.AsFriend()
if ok {
if mb.IsFriend() {
friend := &Friend{mb}
friends = append(friends, friend)
}
}
@ -809,8 +686,8 @@ func (m Members) Friends() Friends {
func (m Members) Groups() Groups {
groups := make(Groups, 0)
for _, mb := range m {
group, ok := mb.AsGroup()
if ok {
if mb.IsGroup() {
group := &Group{mb}
groups = append(groups, group)
}
}
@ -820,8 +697,8 @@ func (m Members) Groups() Groups {
func (m Members) MPs() Mps {
mps := make(Mps, 0)
for _, mb := range m {
mp, ok := mb.AsMP()
if ok {
if mb.IsMP() {
mp := &Mp{mb}
mps = append(mps, mp)
}
}
@ -844,7 +721,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++ {
@ -853,7 +730,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
}
@ -865,7 +742,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
}
@ -880,19 +757,19 @@ 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()
}
}
func newFriend(username string, self *Self) *Friend {
return &Friend{&User{UserName: username, self: self}}
return &Friend{&User{UserName: username, Self: self}}
}
// NewFriendHelper 创建一个文件传输助手
// 文件传输助手的微信身份标识符永远是filehelper
func NewFriendHelper(self *Self) *Friend {
return newFriend(FileHelper, self)
return newFriend("filehelper", self)
}
// SendTextToMp 发送文本消息给公众号
@ -901,17 +778,17 @@ func (s *Self) SendTextToMp(mp *Mp, text string) (*SentMessage, error) {
}
// SendImageToMp 发送图片消息给公众号
func (s *Self) SendImageToMp(mp *Mp, file io.Reader) (*SentMessage, error) {
func (s *Self) SendImageToMp(mp *Mp, file *os.File) (*SentMessage, error) {
return s.sendImageToUser(mp.User, file)
}
// SendFileToMp 发送文件给公众号
func (s *Self) SendFileToMp(mp *Mp, file io.Reader) (*SentMessage, error) {
func (s *Self) SendFileToMp(mp *Mp, file *os.File) (*SentMessage, error) {
return s.sendFileToUser(mp.User, file)
}
// SendVideoToMp 发送视频消息给公众号
func (s *Self) SendVideoToMp(mp *Mp, file io.Reader) (*SentMessage, error) {
func (s *Self) SendVideoToMp(mp *Mp, file *os.File) (*SentMessage, error) {
return s.sendVideoToUser(mp.User, file)
}
@ -919,11 +796,6 @@ func (s *Self) sendMessageWrapper(message *SentMessage, err error) (*SentMessage
if err != nil {
return nil, err
}
message.self = s
message.Self = s
return message, nil
}
// Bot 获取当前用户的机器人
func (s *Self) Bot() *Bot {
return s.bot
}