826 lines
25 KiB
Go
826 lines
25 KiB
Go
package openwechat
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"encoding/xml"
|
||
"errors"
|
||
"fmt"
|
||
"html"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
type Message struct {
|
||
isAt bool
|
||
AppInfo struct {
|
||
Type int
|
||
AppID string
|
||
}
|
||
AppMsgType AppMessageType
|
||
HasProductId int
|
||
ImgHeight int
|
||
ImgStatus int
|
||
ImgWidth int
|
||
ForwardFlag int
|
||
MsgType MessageType
|
||
Status int
|
||
StatusNotifyCode int
|
||
SubMsgType int
|
||
VoiceLength int
|
||
CreateTime int64
|
||
NewMsgId int64
|
||
PlayLength int64
|
||
MediaId string
|
||
MsgId string
|
||
EncryFileName string
|
||
FileName string
|
||
FileSize string
|
||
Content string
|
||
FromUserName string
|
||
OriContent string
|
||
StatusNotifyUserName string
|
||
Ticket string
|
||
ToUserName string
|
||
Url string
|
||
senderInGroupUserName string
|
||
RecommendInfo RecommendInfo
|
||
bot *Bot
|
||
mu sync.RWMutex
|
||
Context context.Context `json:"-"`
|
||
item map[string]interface{}
|
||
Raw []byte `json:"-"`
|
||
RawContent string `json:"-"` // 消息原始内容
|
||
}
|
||
|
||
// Sender 获取消息的发送者
|
||
func (m *Message) Sender() (*User, error) {
|
||
if m.FromUserName == m.bot.self.User.UserName {
|
||
return m.bot.self.User, nil
|
||
}
|
||
// 首先尝试从缓存里面查找, 如果没有找到则从服务器获取
|
||
members, err := m.bot.self.Members()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
user, exist := members.GetByUserName(m.FromUserName)
|
||
if !exist {
|
||
// 找不到, 从服务器获取
|
||
user = &User{self: m.bot.self, UserName: m.FromUserName}
|
||
err = user.Detail()
|
||
}
|
||
if m.IsSendByGroup() && len(user.MemberList) == 0 {
|
||
err = user.Detail()
|
||
}
|
||
return user, err
|
||
}
|
||
|
||
// SenderInGroup 获取消息在群里面的发送者
|
||
func (m *Message) SenderInGroup() (*User, error) {
|
||
if !m.IsComeFromGroup() {
|
||
return nil, errors.New("message is not from group")
|
||
}
|
||
// 拍一拍系列的系统消息
|
||
// https://github.com/eatmoreapple/openwechat/issues/66
|
||
if m.IsSystem() {
|
||
// 判断是否有自己发送
|
||
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")
|
||
}
|
||
user, err := m.Sender()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if user.IsFriend() {
|
||
return user, nil
|
||
}
|
||
group := &Group{user}
|
||
return group.SearchMemberByUsername(m.senderInGroupUserName)
|
||
}
|
||
|
||
// Receiver 获取消息的接收者
|
||
// 如果消息是群组消息,则返回群组
|
||
// 如果消息是好友消息,则返回好友
|
||
// 如果消息是系统消息,则返回当前用户
|
||
func (m *Message) Receiver() (*User, error) {
|
||
if m.IsSystem() || m.ToUserName == m.bot.self.UserName {
|
||
return m.bot.self.User, nil
|
||
}
|
||
// https://github.com/eatmoreapple/openwechat/issues/113
|
||
if m.ToUserName == FileHelper {
|
||
return m.bot.self.FileHelper().User, nil
|
||
}
|
||
|
||
if m.IsSendByGroup() {
|
||
groups, err := m.bot.self.Groups()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
username := m.FromUserName
|
||
if m.IsSendBySelf() {
|
||
username = m.ToUserName
|
||
}
|
||
users := groups.SearchByUserName(1, username)
|
||
if users.Count() == 0 {
|
||
return nil, ErrNoSuchUserFoundError
|
||
}
|
||
return users.First().User, nil
|
||
} else {
|
||
user, exist := m.bot.self.MemberList.GetByUserName(m.ToUserName)
|
||
if !exist {
|
||
return nil, ErrNoSuchUserFoundError
|
||
}
|
||
return user, nil
|
||
}
|
||
}
|
||
|
||
// IsSendBySelf 判断消息是否由自己发送
|
||
func (m *Message) IsSendBySelf() bool {
|
||
return m.FromUserName == m.bot.self.User.UserName
|
||
}
|
||
|
||
// IsSendByFriend 判断消息是否由好友发送
|
||
func (m *Message) IsSendByFriend() bool {
|
||
return !m.IsSendByGroup() && strings.HasPrefix(m.FromUserName, "@") && !m.IsSendBySelf()
|
||
}
|
||
|
||
// IsSendByGroup 判断消息是否由群组发送
|
||
func (m *Message) IsSendByGroup() bool {
|
||
return strings.HasPrefix(m.FromUserName, "@@") || (m.IsSendBySelf() && strings.HasPrefix(m.ToUserName, "@@"))
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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) IsText() bool {
|
||
return m.MsgType == MsgTypeText && m.Url == ""
|
||
}
|
||
|
||
func (m *Message) IsLocation() bool {
|
||
return m.MsgType == MsgTypeText && strings.Contains(m.Url, "api.map.qq.com") && strings.Contains(m.Content, "pictype=location")
|
||
}
|
||
|
||
func (m *Message) IsRealtimeLocation() bool {
|
||
return m.IsRealtimeLocationStart() || m.IsRealtimeLocationStop()
|
||
}
|
||
|
||
func (m *Message) IsRealtimeLocationStart() bool {
|
||
return m.MsgType == MsgTypeApp && m.AppMsgType == AppMsgTypeRealtimeShareLocation
|
||
}
|
||
|
||
func (m *Message) IsRealtimeLocationStop() bool {
|
||
return m.MsgType == MsgTypeSys && m.Content == "位置共享已经结束"
|
||
}
|
||
|
||
func (m *Message) IsPicture() bool {
|
||
return m.MsgType == MsgTypeImage
|
||
}
|
||
|
||
// IsEmoticon 是否为表情包消息
|
||
func (m *Message) IsEmoticon() bool {
|
||
return m.MsgType == MsgTypeEmoticon
|
||
}
|
||
|
||
func (m *Message) IsVoice() bool {
|
||
return m.MsgType == MsgTypeVoice
|
||
}
|
||
|
||
func (m *Message) IsFriendAdd() bool {
|
||
return m.MsgType == MsgTypeVerify && m.FromUserName == "fmessage"
|
||
}
|
||
|
||
func (m *Message) IsCard() bool {
|
||
return m.MsgType == MsgTypeShareCard
|
||
}
|
||
|
||
func (m *Message) IsVideo() bool {
|
||
return m.MsgType == MsgTypeVideo || m.MsgType == MsgTypeMicroVideo
|
||
}
|
||
|
||
func (m *Message) IsMedia() bool {
|
||
return m.MsgType == MsgTypeApp
|
||
}
|
||
|
||
// IsRecalled 判断是否撤回
|
||
func (m *Message) IsRecalled() bool {
|
||
return m.MsgType == MsgTypeRecalled
|
||
}
|
||
|
||
func (m *Message) IsSystem() bool {
|
||
return m.MsgType == MsgTypeSys
|
||
}
|
||
|
||
func (m *Message) IsNotify() bool {
|
||
return m.MsgType == 51 && m.StatusNotifyCode != 0
|
||
}
|
||
|
||
// IsTransferAccounts 判断当前的消息是不是微信转账
|
||
func (m *Message) IsTransferAccounts() bool {
|
||
return m.IsMedia() && m.FileName == "微信转账"
|
||
}
|
||
|
||
// IsSendRedPacket 否发出红包判断当前是
|
||
func (m *Message) IsSendRedPacket() bool {
|
||
return m.IsSystem() && m.Content == "发出红包,请在手机上查看"
|
||
}
|
||
|
||
// IsReceiveRedPacket 判断当前是否收到红包
|
||
func (m *Message) IsReceiveRedPacket() bool {
|
||
return m.IsSystem() && m.Content == "收到红包,请在手机上查看"
|
||
}
|
||
|
||
// IsRenameGroup 判断当前是否是群组重命名
|
||
func (m *Message) IsRenameGroup() bool {
|
||
return m.IsSystem() && strings.Contains(m.Content, "修改群名为")
|
||
}
|
||
|
||
func (m *Message) IsSysNotice() bool {
|
||
return m.MsgType == 9999
|
||
}
|
||
|
||
// StatusNotify 判断是否为操作通知消息
|
||
func (m *Message) StatusNotify() bool {
|
||
return m.MsgType == 51
|
||
}
|
||
|
||
// HasFile 判断消息是否为文件类型的消息
|
||
func (m *Message) HasFile() bool {
|
||
return m.IsPicture() || m.IsVoice() || m.IsVideo() || (m.IsMedia() && m.AppMsgType == AppMsgTypeAttach) || m.IsEmoticon()
|
||
}
|
||
|
||
// GetFile 获取文件消息的文件
|
||
func (m *Message) GetFile() (*http.Response, error) {
|
||
if !m.HasFile() {
|
||
return nil, errors.New("invalid message type")
|
||
}
|
||
if m.IsPicture() || m.IsEmoticon() {
|
||
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)
|
||
}
|
||
if m.IsVideo() {
|
||
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 nil, errors.New("unsupported type")
|
||
}
|
||
|
||
// GetPicture 获取图片消息的响应
|
||
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)
|
||
}
|
||
|
||
// GetVoice 获取录音消息的响应
|
||
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)
|
||
}
|
||
|
||
// GetVideo 获取视频消息的响应
|
||
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)
|
||
}
|
||
|
||
// GetMedia 获取媒体消息的响应
|
||
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)
|
||
}
|
||
|
||
// SaveFile 保存文件到指定的 io.Writer
|
||
func (m *Message) SaveFile(writer io.Writer) error {
|
||
resp, err := m.GetFile()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer func() { _ = resp.Body.Close() }()
|
||
_, err = io.Copy(writer, resp.Body)
|
||
return err
|
||
}
|
||
|
||
// SaveFileToLocal 保存文件到本地
|
||
func (m *Message) SaveFileToLocal(filename string) error {
|
||
file, err := os.Create(filename)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer func() { _ = file.Close() }()
|
||
return m.SaveFile(file)
|
||
}
|
||
|
||
// Card 获取card类型
|
||
func (m *Message) Card() (*Card, error) {
|
||
if !m.IsCard() {
|
||
return nil, errors.New("card message required")
|
||
}
|
||
var card Card
|
||
err := xml.Unmarshal(stringToByte(m.Content), &card)
|
||
return &card, err
|
||
}
|
||
|
||
// FriendAddMessageContent 获取FriendAddMessageContent内容
|
||
func (m *Message) FriendAddMessageContent() (*FriendAddMessage, error) {
|
||
if !m.IsFriendAdd() {
|
||
return nil, errors.New("friend add message required")
|
||
}
|
||
var f FriendAddMessage
|
||
err := xml.Unmarshal(stringToByte(m.Content), &f)
|
||
return &f, err
|
||
}
|
||
|
||
// RevokeMsg 获取撤回消息的内容
|
||
func (m *Message) RevokeMsg() (*RevokeMsg, error) {
|
||
if !m.IsRecalled() {
|
||
return nil, errors.New("recalled message required")
|
||
}
|
||
var r RevokeMsg
|
||
err := xml.Unmarshal(stringToByte(m.Content), &r)
|
||
return &r, err
|
||
}
|
||
|
||
// Agree 同意好友的请求
|
||
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, ""))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
friend := newFriend(m.RecommendInfo.UserName, m.bot.self)
|
||
if err = friend.Detail(); err != nil {
|
||
return nil, err
|
||
}
|
||
return friend, nil
|
||
}
|
||
|
||
// AsRead 将消息设置为已读
|
||
func (m *Message) AsRead() error {
|
||
return m.bot.Caller.WebWxStatusAsRead(m.bot.Storage.Request, m.bot.Storage.LoginInfo, m)
|
||
}
|
||
|
||
// IsArticle 判断当前的消息类型是否为文章
|
||
func (m *Message) IsArticle() bool {
|
||
return m.AppMsgType == AppMsgTypeUrl
|
||
}
|
||
|
||
// MediaData 获取当前App Message的具体内容
|
||
func (m *Message) MediaData() (*AppMessageData, error) {
|
||
if !m.IsMedia() {
|
||
return nil, errors.New("media message required")
|
||
}
|
||
var data AppMessageData
|
||
if err := xml.Unmarshal(stringToByte(m.Content), &data); err != nil {
|
||
return nil, err
|
||
}
|
||
return &data, nil
|
||
}
|
||
|
||
// Set 往消息上下文中设置值
|
||
// goroutine safe
|
||
func (m *Message) Set(key string, value interface{}) {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if m.item == nil {
|
||
m.item = make(map[string]interface{})
|
||
}
|
||
m.item[key] = value
|
||
}
|
||
|
||
// Get 从消息上下文中获取值
|
||
// goroutine safe
|
||
func (m *Message) Get(key string) (value interface{}, exist bool) {
|
||
m.mu.RLock()
|
||
defer m.mu.RUnlock()
|
||
value, exist = m.item[key]
|
||
return
|
||
}
|
||
|
||
// 消息初始化,根据不同的消息作出不同的处理
|
||
func (m *Message) init(bot *Bot) {
|
||
m.bot = bot
|
||
raw, _ := json.Marshal(m)
|
||
m.Raw = raw
|
||
m.RawContent = m.Content
|
||
// 如果是群消息
|
||
if m.IsSendByGroup() {
|
||
if !m.IsSystem() {
|
||
// 将Username和正文分开
|
||
if !m.IsSendBySelf() {
|
||
data := strings.Split(m.Content, ":<br/>")
|
||
m.Content = strings.Join(data[1:], "")
|
||
m.senderInGroupUserName = data[0]
|
||
if strings.Contains(m.Content, "@") {
|
||
sender, err := m.Sender()
|
||
if err == nil {
|
||
receiver := sender.MemberList.SearchByUserName(1, m.ToUserName)
|
||
if receiver != nil {
|
||
displayName := receiver.First().DisplayName
|
||
if displayName == "" {
|
||
displayName = receiver.First().NickName
|
||
}
|
||
var atFlag string
|
||
msgContent := FormatEmoji(m.Content)
|
||
atName := FormatEmoji(displayName)
|
||
if strings.Contains(msgContent, "\u2005") {
|
||
atFlag = "@" + atName + "\u2005"
|
||
} else {
|
||
atFlag = "@" + atName
|
||
}
|
||
m.isAt = strings.Contains(msgContent, atFlag) || strings.HasSuffix(msgContent, atFlag)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 这块不严谨,但是只能这么干了
|
||
m.isAt = strings.Contains(m.Content, "@") || strings.Contains(m.Content, "\u2005")
|
||
}
|
||
}
|
||
}
|
||
// 处理消息中的换行
|
||
m.Content = strings.Replace(m.Content, `<br/>`, "\n", -1)
|
||
// 处理html转义字符
|
||
m.Content = html.UnescapeString(m.Content)
|
||
// 处理消息中的emoji表情
|
||
m.Content = FormatEmoji(m.Content)
|
||
}
|
||
|
||
// SendMessage 发送消息的结构体
|
||
type SendMessage struct {
|
||
Type MessageType
|
||
Content string
|
||
FromUserName string
|
||
ToUserName string
|
||
LocalID string
|
||
ClientMsgId string
|
||
MediaId string `json:"MediaId,omitempty"`
|
||
}
|
||
|
||
// NewSendMessage SendMessage的构造方法
|
||
func NewSendMessage(msgType MessageType, content, fromUserName, toUserName, mediaId string) *SendMessage {
|
||
id := strconv.FormatInt(time.Now().UnixNano()/1e2, 10)
|
||
return &SendMessage{
|
||
Type: msgType,
|
||
Content: content,
|
||
FromUserName: fromUserName,
|
||
ToUserName: toUserName,
|
||
LocalID: id,
|
||
ClientMsgId: id,
|
||
MediaId: mediaId,
|
||
}
|
||
}
|
||
|
||
// NewTextSendMessage 文本消息的构造方法
|
||
func NewTextSendMessage(content, fromUserName, toUserName string) *SendMessage {
|
||
return NewSendMessage(MsgTypeText, content, fromUserName, toUserName, "")
|
||
}
|
||
|
||
// NewMediaSendMessage 媒体消息的构造方法
|
||
func NewMediaSendMessage(msgType MessageType, fromUserName, toUserName, mediaId string) *SendMessage {
|
||
return NewSendMessage(msgType, "", fromUserName, toUserName, mediaId)
|
||
}
|
||
|
||
// RecommendInfo 一些特殊类型的消息会携带该结构体信息
|
||
type RecommendInfo struct {
|
||
OpCode int
|
||
Scene int
|
||
Sex int
|
||
VerifyFlag int
|
||
AttrStatus int64
|
||
QQNum int64
|
||
Alias string
|
||
City string
|
||
Content string
|
||
NickName string
|
||
Province string
|
||
Signature string
|
||
Ticket string
|
||
UserName string
|
||
}
|
||
|
||
// Card 名片消息内容
|
||
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"`
|
||
}
|
||
|
||
// FriendAddMessage 好友添加消息信息内容
|
||
type FriendAddMessage struct {
|
||
XMLName xml.Name `xml:"msg"`
|
||
Shortpy string `xml:"shortpy,attr"`
|
||
ImageStatus int `xml:"imagestatus,attr"`
|
||
Scene int `xml:"scene,attr"`
|
||
PerCard int `xml:"percard,attr"`
|
||
Sex int `xml:"sex,attr"`
|
||
AlbumFlag int `xml:"albumflag,attr"`
|
||
AlbumStyle int `xml:"albumstyle,attr"`
|
||
SnsFlag int `xml:"snsflag,attr"`
|
||
Opcode int `xml:"opcode,attr"`
|
||
FromUserName string `xml:"fromusername,attr"`
|
||
EncryptUserName string `xml:"encryptusername,attr"`
|
||
FromNickName string `xml:"fromnickname,attr"`
|
||
Content string `xml:"content,attr"`
|
||
Country string `xml:"country,attr"`
|
||
Province string `xml:"province,attr"`
|
||
City string `xml:"city,attr"`
|
||
Sign string `xml:"sign,attr"`
|
||
Alias string `xml:"alias,attr"`
|
||
WeiBo string `xml:"weibo,attr"`
|
||
AlbumBgImgId string `xml:"albumbgimgid,attr"`
|
||
SnsBgImgId string `xml:"snsbgimgid,attr"`
|
||
SnsBgObjectId string `xml:"snsbgobjectid,attr"`
|
||
MHash string `xml:"mhash,attr"`
|
||
MFullHash string `xml:"mfullhash,attr"`
|
||
BigHeadImgUrl string `xml:"bigheadimgurl,attr"`
|
||
SmallHeadImgUrl string `xml:"smallheadimgurl,attr"`
|
||
Ticket string `xml:"ticket,attr"`
|
||
GoogleContact string `xml:"googlecontact,attr"`
|
||
QrTicket string `xml:"qrticket,attr"`
|
||
ChatRoomUserName string `xml:"chatroomusername,attr"`
|
||
SourceUserName string `xml:"sourceusername,attr"`
|
||
ShareCardUserName string `xml:"sharecardusername,attr"`
|
||
ShareCardNickName string `xml:"sharecardnickname,attr"`
|
||
CardVersion string `xml:"cardversion,attr"`
|
||
BrandList struct {
|
||
Count int `xml:"count,attr"`
|
||
Ver int64 `xml:"ver,attr"`
|
||
} `xml:"brandlist"`
|
||
}
|
||
|
||
// RevokeMsg 撤回消息Content
|
||
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"`
|
||
}
|
||
|
||
// SentMessage 已发送的信息
|
||
type SentMessage struct {
|
||
*SendMessage
|
||
self *Self
|
||
MsgId string
|
||
}
|
||
|
||
// Revoke 撤回该消息
|
||
func (s *SentMessage) Revoke() error {
|
||
return s.self.RevokeMessage(s)
|
||
}
|
||
|
||
// CanRevoke 是否可以撤回该消息
|
||
func (s *SentMessage) CanRevoke() bool {
|
||
i, err := strconv.ParseInt(s.ClientMsgId, 10, 64)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
start := time.Unix(i/10000000, 0)
|
||
return time.Since(start) < 2*time.Minute
|
||
}
|
||
|
||
// ForwardToFriends 转发该消息给好友
|
||
// 该方法会阻塞直到所有好友都接收到消息
|
||
// 这里为了兼容以前的版本,默认休眠0.5秒,如果需要更快的速度,可以使用 SentMessage.ForwardToFriendsWithDelay
|
||
func (s *SentMessage) ForwardToFriends(friends ...*Friend) error {
|
||
return s.ForwardToFriendsWithDelay(time.Second/2, friends...)
|
||
}
|
||
|
||
// ForwardToFriendsWithDelay 转发该消息给好友,延迟指定时间
|
||
func (s *SentMessage) ForwardToFriendsWithDelay(delay time.Duration, friends ...*Friend) error {
|
||
return s.self.ForwardMessageToFriends(s, delay, friends...)
|
||
}
|
||
|
||
// ForwardToGroups 转发该消息给群组
|
||
// 该方法会阻塞直到所有群组都接收到消息
|
||
// 这里为了兼容以前的版本,默认休眠0.5秒,如果需要更快的速度,可以使用 SentMessage.ForwardToGroupsDelay
|
||
func (s *SentMessage) ForwardToGroups(groups ...*Group) error {
|
||
return s.ForwardToGroupsWithDelay(time.Second/2, groups...)
|
||
}
|
||
|
||
// ForwardToGroupsWithDelay 转发该消息给群组, 延迟指定时间
|
||
func (s *SentMessage) ForwardToGroupsWithDelay(delay time.Duration, groups ...*Group) error {
|
||
return s.self.ForwardMessageToGroups(s, delay, groups...)
|
||
}
|
||
|
||
type appmsg struct {
|
||
Type int `xml:"type"`
|
||
AppId string `xml:"appid,attr"` // wxeb7ec651dd0aefa9
|
||
SdkVer string `xml:"sdkver,attr"`
|
||
Title string `xml:"title"`
|
||
Des string `xml:"des"`
|
||
Action string `xml:"action"`
|
||
Content string `xml:"content"`
|
||
Url string `xml:"url"`
|
||
LowUrl string `xml:"lowurl"`
|
||
ExtInfo string `xml:"extinfo"`
|
||
AppAttach struct {
|
||
TotalLen int64 `xml:"totallen"`
|
||
AttachId string `xml:"attachid"`
|
||
FileExt string `xml:"fileext"`
|
||
} `xml:"appattach"`
|
||
}
|
||
|
||
func (f appmsg) XmlByte() ([]byte, error) {
|
||
return xml.Marshal(f)
|
||
}
|
||
|
||
func NewFileAppMessage(stat os.FileInfo, attachId string) *appmsg {
|
||
m := &appmsg{AppId: appMessageAppId, Title: stat.Name()}
|
||
m.AppAttach.AttachId = attachId
|
||
m.AppAttach.TotalLen = stat.Size()
|
||
m.Type = 6
|
||
m.AppAttach.FileExt = getFileExt(stat.Name())
|
||
return m
|
||
}
|
||
|
||
// AppMessageData 获取APP消息的正文
|
||
// See https://github.com/eatmoreapple/openwechat/issues/62
|
||
type AppMessageData struct {
|
||
XMLName xml.Name `xml:"msg"`
|
||
AppMsg struct {
|
||
Appid string `xml:"appid,attr"`
|
||
SdkVer string `xml:"sdkver,attr"`
|
||
Title string `xml:"title"`
|
||
Des string `xml:"des"`
|
||
Action string `xml:"action"`
|
||
Type AppMessageType `xml:"type"`
|
||
ShowType string `xml:"showtype"`
|
||
Content string `xml:"content"`
|
||
URL string `xml:"url"`
|
||
DataUrl string `xml:"dataurl"`
|
||
LowUrl string `xml:"lowurl"`
|
||
LowDataUrl string `xml:"lowdataurl"`
|
||
RecordItem string `xml:"recorditem"`
|
||
ThumbUrl string `xml:"thumburl"`
|
||
MessageAction string `xml:"messageaction"`
|
||
Md5 string `xml:"md5"`
|
||
ExtInfo string `xml:"extinfo"`
|
||
SourceUsername string `xml:"sourceusername"`
|
||
SourceDisplayName string `xml:"sourcedisplayname"`
|
||
CommentUrl string `xml:"commenturl"`
|
||
AppAttach struct {
|
||
TotalLen string `xml:"totallen"`
|
||
AttachId string `xml:"attachid"`
|
||
EmoticonMd5 string `xml:"emoticonmd5"`
|
||
FileExt string `xml:"fileext"`
|
||
FileUploadToken string `xml:"fileuploadtoken"`
|
||
OverwriteNewMsgId string `xml:"overwrite_newmsgid"`
|
||
FileKey string `xml:"filekey"`
|
||
CdnAttachUrl string `xml:"cdnattachurl"`
|
||
AesKey string `xml:"aeskey"`
|
||
EncryVer string `xml:"encryver"`
|
||
} `xml:"appattach"`
|
||
WeAppInfo struct {
|
||
PagePath string `xml:"pagepath"`
|
||
Username string `xml:"username"`
|
||
Appid string `xml:"appid"`
|
||
AppServiceType string `xml:"appservicetype"`
|
||
} `xml:"weappinfo"`
|
||
WebSearch string `xml:"websearch"`
|
||
} `xml:"appmsg"`
|
||
FromUsername string `xml:"fromusername"`
|
||
Scene string `xml:"scene"`
|
||
AppInfo struct {
|
||
Version string `xml:"version"`
|
||
AppName string `xml:"appname"`
|
||
} `xml:"appinfo"`
|
||
CommentUrl string `xml:"commenturl"`
|
||
}
|
||
|
||
// IsFromApplet 判断当前的消息类型是否来自小程序
|
||
func (a *AppMessageData) IsFromApplet() bool {
|
||
return a.AppMsg.Appid != ""
|
||
}
|
||
|
||
// IsArticle 判断当前的消息类型是否为文章
|
||
func (a *AppMessageData) IsArticle() bool {
|
||
return a.AppMsg.Type == AppMsgTypeUrl
|
||
}
|
||
|
||
// IsFile 判断当前的消息类型是否为文件
|
||
func (a AppMessageData) IsFile() bool {
|
||
return a.AppMsg.Type == AppMsgTypeAttach
|
||
}
|
||
|
||
// IsComeFromGroup 判断消息是否来自群组
|
||
// 可能是自己或者别的群员发送
|
||
func (m *Message) IsComeFromGroup() bool {
|
||
return m.IsSendByGroup() || (strings.HasPrefix(m.ToUserName, "@@") && m.IsSendBySelf())
|
||
}
|
||
|
||
func (m *Message) String() string {
|
||
return fmt.Sprintf("<%s:%s>", m.MsgType, m.MsgId)
|
||
}
|
||
|
||
// IsAt 判断消息是否为@消息
|
||
func (m *Message) IsAt() bool {
|
||
return m.isAt
|
||
}
|
||
|
||
// IsPaiYiPai 判断消息是否为拍一拍
|
||
// 不要问我为什么取名为PaiYiPai,因为我也不知道取啥名字好
|
||
func (m *Message) IsPaiYiPai() bool {
|
||
return m.IsTickled()
|
||
}
|
||
|
||
// IsJoinGroup 判断是否有人加入了群聊
|
||
func (m *Message) IsJoinGroup() bool {
|
||
return m.IsSystem() && (strings.Contains(m.Content, "加入了群聊") || strings.Contains(m.Content, "分享的二维码加入群聊")) && m.IsSendByGroup()
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|