1 Star 0 Fork 1

linkin999/openwechat

forked from MAKOTO/openwechat 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
client.go 32.42 KB
一键复制 编辑 原始数据 按行查看 历史
多吃点苹果 提交于 2024-06-10 10:46 . add workflow (#503)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172
package openwechat
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// HttpHook 请求上下文钩子
type HttpHook interface {
// BeforeRequest 将在请求之前调用
BeforeRequest(req *http.Request)
// AfterRequest 将在请求之后调用,无论请求成功与否
AfterRequest(response *http.Response, err error)
}
// HttpHooks 请求上下文钩子列表
type HttpHooks []HttpHook
// BeforeRequest 将在请求之前调用
func (h HttpHooks) BeforeRequest(req *http.Request) {
if len(h) == 0 {
return
}
for _, hook := range h {
hook.BeforeRequest(req)
}
}
// AfterRequest 将在请求之后调用,无论请求成功与否
func (h HttpHooks) AfterRequest(response *http.Response, err error) {
if len(h) == 0 {
return
}
for _, hook := range h {
hook.AfterRequest(response, err)
}
}
type UserAgentHook struct {
UserAgent string
}
func (u UserAgentHook) BeforeRequest(req *http.Request) {
req.Header.Set("User-Agent", u.UserAgent)
}
func (u UserAgentHook) AfterRequest(_ *http.Response, _ error) {}
// defaultUserAgentHook 默认的User-Agent钩子
var defaultUserAgentHook = UserAgentHook{"Mozilla/5.0 (Linux; U; UOS x86_64; zh-cn) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 UOSBrowser/6.0.1.1001"}
// Client http请求客户端
// 客户端需要维持Session会话
type Client struct {
// 设置一些client的请求行为
// see normalMode desktopMode
mode Mode
// client http客户端
client *http.Client
// Domain 微信服务器请求域名
// 这个参数会在登录成功后被赋值
// 之后所有的请求都会使用这个域名
// 在登录热登录和扫码登录时会被重新赋值
Domain WechatDomain
// HttpHooks 请求上下文钩子
HttpHooks HttpHooks
// MaxRetryTimes 最大重试次数
MaxRetryTimes int
}
// NewClient 创建一个新的客户端
func NewClient(httpClient *http.Client) *Client {
if httpClient == nil {
panic("http client is nil")
}
client := &Client{client: httpClient}
client.MaxRetryTimes = 1
client.SetCookieJar(NewJar())
return client
}
// DefaultClient 自动存储cookie
// 设置客户端不自动跳转
func DefaultClient() *Client {
httpClient := &http.Client{
// 设置客户端不自动跳转
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
// 设置30秒超时
// 因为微信同步消息时是一个时间长达25秒的长轮训
Timeout: 30 * time.Second,
}
client := NewClient(httpClient)
client.AddHttpHook(defaultUserAgentHook)
client.MaxRetryTimes = 5
return client
}
// AddHttpHook 添加一个请求上下文钩子
func (c *Client) AddHttpHook(hooks ...HttpHook) {
c.HttpHooks = append(c.HttpHooks, hooks...)
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
// 确保请求能够被执行
if c.MaxRetryTimes <= 0 {
c.MaxRetryTimes = 1
}
var (
resp *http.Response
err error
requestBody *bytes.Reader
)
c.HttpHooks.BeforeRequest(req)
defer func() { c.HttpHooks.AfterRequest(resp, err) }()
if req.Body != nil {
rawBody, err := io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("io.ReadAll: %w", err)
}
requestBody = bytes.NewReader(rawBody)
}
for i := 0; i < c.MaxRetryTimes; i++ {
if requestBody != nil {
_, err := requestBody.Seek(0, io.SeekStart)
if err != nil {
return nil, fmt.Errorf("requestBody.Seek: %w", err)
}
req.Body = io.NopCloser(requestBody)
}
resp, err = c.client.Do(req)
if err == nil {
break
}
}
if err != nil {
err = errors.Join(NetworkErr, err)
}
return resp, err
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.do(req)
}
// 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()
}
// HTTPClient 返回http.Client
// 用于自定义http.Client的行为,如设置超时时间、设置代理、设置TLS配置等
func (c *Client) HTTPClient() *http.Client {
return c.client
}
// GetLoginUUID 获取登录的uuid
func (c *Client) GetLoginUUID(ctx context.Context) (*http.Response, error) {
req, err := c.mode.BuildGetLoginUUIDRequest(ctx)
if err != nil {
return nil, err
}
return c.Do(req)
}
// GetLoginQrcode 获取登录的二维吗
func (c *Client) GetLoginQrcode(ctx context.Context, uuid string) (*http.Response, error) {
path := qrcode + uuid
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
return c.client.Do(req)
}
// CheckLogin 检查是否登录
func (c *Client) CheckLogin(ctx context.Context, uuid, tip string) (*http.Response, error) {
path, err := url.Parse(login)
if err != nil {
return nil, err
}
now := time.Now().Unix()
params := url.Values{}
params.Add("r", strconv.FormatInt(now/1579, 10))
params.Add("_", strconv.FormatInt(now, 10))
params.Add("loginicon", "true")
params.Add("uuid", uuid)
params.Add("tip", tip)
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// GetLoginInfo 请求获取LoginInfo
func (c *Client) GetLoginInfo(ctx context.Context, path *url.URL) (*http.Response, error) {
req, err := c.mode.BuildGetLoginInfoRequest(ctx, path.String())
if err != nil {
return nil, err
}
return c.Do(req)
}
// WebInit 请求获取初始化信息
func (c *Client) WebInit(ctx context.Context, request *BaseRequest) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxinit)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("_", fmt.Sprintf("%d", time.Now().Unix()))
path.RawQuery = params.Encode()
content := struct{ BaseRequest *BaseRequest }{BaseRequest: request}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientCommonOptions struct {
BaseRequest *BaseRequest
WebInitResponse *WebInitResponse
LoginInfo *LoginInfo
}
type ClientWebWxStatusNotifyOptions ClientCommonOptions
// WebWxStatusNotify 通知手机已登录
func (c *Client) WebWxStatusNotify(ctx context.Context, opt *ClientWebWxStatusNotifyOptions) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxstatusnotify)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("lang", "zh_CN")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
username := opt.WebInitResponse.User.UserName
content := map[string]interface{}{
"BaseRequest": opt.BaseRequest,
"ClientMsgId": time.Now().Unix(),
"Code": 3,
"FromUserName": username,
"ToUserName": username,
}
path.RawQuery = params.Encode()
buffer, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), buffer)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientSyncCheckOptions ClientCommonOptions
// SyncCheck 异步检查是否有新的消息返回
func (c *Client) SyncCheck(ctx context.Context, opt *ClientSyncCheckOptions) (*http.Response, error) {
path, err := url.Parse(c.Domain.SyncHost() + synccheck)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("r", strconv.FormatInt(time.Now().UnixNano()/1e6, 10))
params.Add("skey", opt.LoginInfo.SKey)
params.Add("sid", opt.LoginInfo.WxSid)
params.Add("uin", strconv.FormatInt(opt.LoginInfo.WxUin, 10))
params.Add("deviceid", opt.BaseRequest.DeviceID)
params.Add("_", strconv.FormatInt(time.Now().UnixNano()/1e6, 10))
var syncKeyStringSlice = make([]string, opt.WebInitResponse.SyncKey.Count)
// 将SyncKey里面的元素按照特定的格式拼接起来
for index, item := range opt.WebInitResponse.SyncKey.List {
i := fmt.Sprintf("%d_%d", item.Key, item.Val)
syncKeyStringSlice[index] = i
}
syncKey := strings.Join(syncKeyStringSlice, "|")
params.Add("synckey", syncKey)
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// WebWxGetContact 获取联系人信息
func (c *Client) WebWxGetContact(ctx context.Context, sKey string, reqs int64) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxgetcontact)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("r", strconv.FormatInt(time.Now().UnixNano()/1e6, 10))
params.Add("skey", sKey)
params.Add("seq", strconv.FormatInt(reqs, 10))
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// WebWxBatchGetContact 获取联系人详情
func (c *Client) WebWxBatchGetContact(ctx context.Context, members Members, request *BaseRequest) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxbatchgetcontact)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("type", "ex")
params.Add("r", strconv.FormatInt(time.Now().UnixNano()/1e6, 10))
path.RawQuery = params.Encode()
list := NewUserDetailItemList(members)
content := map[string]interface{}{
"BaseRequest": request,
"Count": members.Count(),
"List": list,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientWebWxSyncOptions ClientCommonOptions
// WebWxSync 获取消息接口
func (c *Client) WebWxSync(ctx context.Context, opt *ClientWebWxSyncOptions) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxsync)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("sid", opt.LoginInfo.WxSid)
params.Add("skey", opt.LoginInfo.SKey)
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
path.RawQuery = params.Encode()
content := map[string]interface{}{
"BaseRequest": opt.BaseRequest,
"SyncKey": opt.WebInitResponse.SyncKey,
"rr": strconv.FormatInt(time.Now().Unix(), 10),
}
reader, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), reader)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
// 发送消息
func (c *Client) sendMessage(ctx context.Context, request *BaseRequest, url string, msg *SendMessage) (*http.Response, error) {
content := map[string]interface{}{
"BaseRequest": request,
"Msg": msg,
"Scene": 0,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientWebWxSendMsgOptions struct {
LoginInfo *LoginInfo
BaseRequest *BaseRequest
Message *SendMessage
}
// WebWxSendMsg 发送文本消息
func (c *Client) WebWxSendMsg(ctx context.Context, opt *ClientWebWxSendMsgOptions) (*http.Response, error) {
opt.Message.Type = MsgTypeText
path, err := url.Parse(c.Domain.BaseHost() + webwxsendmsg)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("lang", "zh_CN")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
path.RawQuery = params.Encode()
return c.sendMessage(ctx, opt.BaseRequest, path.String(), opt.Message)
}
// WebWxSendMsg 发送表情消息
func (c *Client) WebWxSendEmoticon(ctx context.Context, opt *ClientWebWxSendMsgOptions) (*http.Response, error) {
opt.Message.Type = MsgTypeText
path, err := url.Parse(c.Domain.BaseHost() + webwxsendemoticon)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "sys")
params.Add("lang", "zh_CN")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
path.RawQuery = params.Encode()
return c.sendMessage(ctx, opt.BaseRequest, path.String(), opt.Message)
}
// WebWxGetHeadImg 获取用户的头像
func (c *Client) WebWxGetHeadImg(ctx context.Context, user *User) (*http.Response, error) {
var path string
if user.HeadImgUrl != "" {
path = c.Domain.BaseHost() + user.HeadImgUrl
} else {
params := url.Values{}
params.Add("username", user.UserName)
params.Add("skey", user.self.bot.Storage.Request.Skey)
params.Add("type", "big")
params.Add("chatroomid", user.EncryChatRoomId)
params.Add("seq", "0")
URL, err := url.Parse(c.Domain.BaseHost() + webwxgeticon)
if err != nil {
return nil, err
}
URL.RawQuery = params.Encode()
path = URL.String()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
type ClientWebWxUploadMediaByChunkOptions struct {
FromUserName string
ToUserName string
BaseRequest *BaseRequest
LoginInfo *LoginInfo
}
// WebWxUploadMediaByChunk 分块上传文件
// TODO 优化掉这个函数
func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt *ClientWebWxUploadMediaByChunkOptions) (*http.Response, error) {
// 获取文件上传的类型
contentType, err := GetFileContentType(file)
if err != nil {
return nil, err
}
if _, err = file.Seek(0, io.SeekStart); err != nil {
return nil, err
}
// 获取文件的md5
h := md5.New()
if _, err = io.Copy(h, file); err != nil {
return nil, err
}
fileMd5 := hex.EncodeToString(h.Sum(nil))
sate, err := file.Stat()
if err != nil {
return nil, err
}
filename := sate.Name()
if ext := filepath.Ext(filename); ext == "" {
names := strings.Split(contentType, "/")
filename = filename + "." + names[len(names)-1]
}
// 获取文件的类型
mediaType := getMessageType(filename)
path, err := url.Parse(c.Domain.FileHost() + webwxuploadmedia)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("f", "json")
path.RawQuery = params.Encode()
cookies := c.Jar().Cookies(path)
webWxDataTicket, err := getWebWxDataTicket(cookies)
if err != nil {
return nil, err
}
uploadMediaRequest := map[string]interface{}{
"UploadType": 2,
"BaseRequest": opt.BaseRequest,
"ClientMediaId": time.Now().Unix() * 1e4,
"TotalLen": sate.Size(),
"StartPos": 0,
"DataLen": sate.Size(),
"MediaType": 4,
"FromUserName": opt.FromUserName,
"ToUserName": opt.ToUserName,
"FileMd5": fileMd5,
}
uploadMediaRequestByte, err := json.Marshal(uploadMediaRequest)
if err != nil {
return nil, err
}
// 计算上传文件的次数
chunks := int((sate.Size() + chunkSize - 1) / chunkSize)
content := map[string]string{
"id": "WU_FILE_0",
"name": filename,
"type": contentType,
"lastModifiedDate": sate.ModTime().Format(TimeFormat),
"size": strconv.FormatInt(sate.Size(), 10),
"mediatype": mediaType,
"webwx_data_ticket": webWxDataTicket,
"pass_ticket": opt.LoginInfo.PassTicket,
}
if chunks > 1 {
content["chunks"] = strconv.Itoa(chunks)
}
var (
resp *http.Response
formBuffer = bytes.NewBuffer(nil)
)
upload := func(chunk int, fileReader io.Reader) error {
if chunks > 1 {
content["chunk"] = strconv.Itoa(chunk)
}
formBuffer.Reset()
writer := multipart.NewWriter(formBuffer)
// write form data
{
if err = writer.WriteField("uploadmediarequest", string(uploadMediaRequestByte)); err != nil {
return err
}
for k, v := range content {
if err := writer.WriteField(k, v); err != nil {
return err
}
}
// create form file
fileWriter, err := writer.CreateFormFile("filename", file.Name())
if err != nil {
return err
}
if _, err = io.Copy(fileWriter, fileReader); err != nil {
return err
}
if err = writer.Close(); err != nil {
return err
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), formBuffer)
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err = c.Do(req)
if err != nil {
return err
}
// parse response error
{
isLastTime := chunk+1 == chunks
if !isLastTime {
defer func() { _ = resp.Body.Close() }()
parser := MessageResponseParser{Reader: resp.Body}
err = parser.Err()
}
}
return err
}
// 分块上传
for chunk := 0; chunk < chunks; chunk++ {
// chunk reader
selectionReader := io.NewSectionReader(file, int64(chunk)*chunkSize, chunkSize)
// try to upload
if err = upload(chunk, selectionReader); err != nil {
return nil, err
}
}
// 将最后一次携带文件信息的response返回
return resp, err
}
// WebWxSendMsgImg 发送图片
// 这个接口依赖上传文件的接口
// 发送的图片必须是已经成功上传的图片
func (c *Client) WebWxSendMsgImg(ctx context.Context, opt *ClientWebWxSendMsgOptions) (*http.Response, error) {
opt.Message.Type = MsgTypeImage
path, err := url.Parse(c.Domain.BaseHost() + webwxsendmsgimg)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "async")
params.Add("f", "json")
params.Add("lang", "zh_CN")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
path.RawQuery = params.Encode()
return c.sendMessage(ctx, opt.BaseRequest, path.String(), opt.Message)
}
// WebWxSendAppMsg 发送文件信息
func (c *Client) WebWxSendAppMsg(ctx context.Context, msg *SendMessage, request *BaseRequest) (*http.Response, error) {
msg.Type = AppMessage
path, err := url.Parse(c.Domain.BaseHost() + webwxsendappmsg)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "async")
params.Add("f", "json")
path.RawQuery = params.Encode()
return c.sendMessage(ctx, request, path.String(), msg)
}
type ClientWebWxOplogOption struct {
RemarkName string
UserName string
BaseRequest *BaseRequest
}
// WebWxOplog 用户重命名接口
func (c *Client) WebWxOplog(ctx context.Context, opt *ClientWebWxOplogOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxoplog)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("lang", "zh_CN")
path.RawQuery = params.Encode()
content := map[string]interface{}{
"BaseRequest": opt.BaseRequest,
"CmdId": 2,
"RemarkName": opt.RemarkName,
"UserName": opt.UserName,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientWebWxVerifyUserOption struct {
RecommendInfo RecommendInfo
VerifyContent string
BaseRequest *BaseRequest
LoginInfo *LoginInfo
}
// WebWxVerifyUser 添加用户为好友接口
func (c *Client) WebWxVerifyUser(ctx context.Context, opt *ClientWebWxVerifyUserOption) (*http.Response, error) {
loginInfo := opt.LoginInfo
path, err := url.Parse(c.Domain.BaseHost() + webwxverifyuser)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("r", strconv.FormatInt(time.Now().UnixNano()/1e6, 10))
params.Add("lang", "zh_CN")
params.Add("pass_ticket", loginInfo.PassTicket)
path.RawQuery = params.Encode()
content := map[string]interface{}{
"BaseRequest": opt.BaseRequest,
"Opcode": 3,
"SceneList": [1]int{33},
"SceneListCount": 1,
"VerifyContent": opt.VerifyContent,
"VerifyUserList": []interface{}{map[string]string{
"Value": opt.RecommendInfo.UserName,
"VerifyUserTicket": opt.RecommendInfo.Ticket,
}},
"VerifyUserListSize": 1,
"skey": opt.BaseRequest.Skey,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
// WebWxGetMsgImg 获取图片消息的图片响应
func (c *Client) WebWxGetMsgImg(ctx context.Context, msg *Message, info *LoginInfo) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxgetmsgimg)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("MsgID", msg.MsgId)
params.Add("skey", info.SKey)
// params.Add("type", "slave")
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
// WebWxGetVoice 获取语音消息的语音响应
func (c *Client) WebWxGetVoice(ctx context.Context, msg *Message, info *LoginInfo) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxgetvoice)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("msgid", msg.MsgId)
params.Add("skey", info.SKey)
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
req.Header.Add("Referer", path.String())
req.Header.Add("Range", "bytes=0-")
return c.Do(req)
}
// WebWxGetVideo 获取视频消息的视频响应
func (c *Client) WebWxGetVideo(ctx context.Context, msg *Message, info *LoginInfo) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxgetvideo)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("msgid", msg.MsgId)
params.Add("skey", info.SKey)
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
req.Header.Add("Referer", path.String())
req.Header.Add("Range", "bytes=0-")
return c.Do(req)
}
// WebWxGetMedia 获取文件消息的文件响应
func (c *Client) WebWxGetMedia(ctx context.Context, msg *Message, info *LoginInfo) (*http.Response, error) {
path, err := url.Parse(c.Domain.FileHost() + webwxgetmedia)
if err != nil {
return nil, err
}
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("pass_ticket", info.PassTicket)
params.Add("webwx_data_ticket", webWxDataTicket)
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
req.Header.Add("Referer", c.Domain.BaseHost()+"/")
return c.Do(req)
}
// Logout 用户退出
func (c *Client) Logout(ctx context.Context, info *LoginInfo) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxlogout)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("redirect", "1")
params.Add("type", "1")
params.Add("skey", info.SKey)
path.RawQuery = params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, path.String(), nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
type ClientAddMemberIntoChatRoomOption struct {
Group string
GroupLength int
InviteMemberList []string
BaseRequest *BaseRequest
LoginInfo *LoginInfo
}
// AddMemberIntoChatRoom 添加用户进群聊
func (c *Client) AddMemberIntoChatRoom(ctx context.Context, opt *ClientAddMemberIntoChatRoomOption) (*http.Response, error) {
if opt.GroupLength >= 40 {
return c.InviteMemberIntoChatRoom(ctx, opt)
}
return c.addMemberIntoChatRoom(ctx, opt)
}
// addMemberIntoChatRoom 添加用户进群聊
func (c *Client) addMemberIntoChatRoom(ctx context.Context, opt *ClientAddMemberIntoChatRoomOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "addmember")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
params.Add("lang", "zh_CN")
path.RawQuery = params.Encode()
content := map[string]interface{}{
"ChatRoomName": opt.Group,
"BaseRequest": opt.BaseRequest,
"AddMemberList": strings.Join(opt.InviteMemberList, ","),
}
buffer, err := jsonEncode(content)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), buffer)
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", jsonContentType)
return c.Do(httpReq)
}
// InviteMemberIntoChatRoom 邀请用户进群聊
func (c *Client) InviteMemberIntoChatRoom(ctx context.Context, opt *ClientAddMemberIntoChatRoomOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "invitemember")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
params.Add("lang", "zh_CN")
path.RawQuery = params.Encode()
content := map[string]interface{}{
"ChatRoomName": opt.Group,
"BaseRequest": opt.BaseRequest,
"InviteMemberList": strings.Join(opt.InviteMemberList, ","),
}
buffer, err := jsonEncode(content)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), buffer)
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", jsonContentType)
return c.Do(httpReq)
}
type ClientRemoveMemberFromChatRoomOption struct {
Group string
DelMemberList []string
BaseRequest *BaseRequest
LoginInfo *LoginInfo
}
// RemoveMemberFromChatRoom 从群聊中移除用户
func (c *Client) RemoveMemberFromChatRoom(ctx context.Context, opt *ClientRemoveMemberFromChatRoomOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "delmember")
params.Add("lang", "zh_CN")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
content := map[string]interface{}{
"ChatRoomName": opt.Group,
"BaseRequest": opt.BaseRequest,
"DelMemberList": strings.Join(opt.DelMemberList, ","),
}
buffer, err := jsonEncode(content)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), buffer)
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", jsonContentType)
return c.Do(httpReq)
}
// WebWxRevokeMsg 撤回消息
func (c *Client) WebWxRevokeMsg(ctx context.Context, msg *SentMessage, request *BaseRequest) (*http.Response, error) {
content := map[string]interface{}{
"BaseRequest": request,
"ClientMsgId": msg.ClientMsgId,
"SvrMsgId": msg.MsgId,
"ToUserName": msg.ToUserName,
}
buffer, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Domain.BaseHost()+webwxrevokemsg, buffer)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", jsonContentType)
return c.Do(req)
}
// 校验上传文件
// nolint:unused
func (c *Client) webWxCheckUpload(stat os.FileInfo, request *BaseRequest, fileMd5, fromUserName, toUserName string) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxcheckupload)
if err != nil {
return nil, err
}
content := map[string]interface{}{
"BaseRequest": request,
"FileMd5": fileMd5,
"FileName": stat.Name(),
"FileSize": stat.Size(),
"FileType": 7,
"FromUserName": fromUserName,
"ToUserName": toUserName,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientWebWxStatusAsReadOption struct {
LoginInfo *LoginInfo
Request *BaseRequest
Message *Message
}
func (c *Client) WebWxStatusAsRead(ctx context.Context, opt *ClientWebWxStatusAsReadOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxstatusnotify)
if err != nil {
return nil, err
}
content := map[string]interface{}{
"BaseRequest": opt.Request,
"DeviceID": opt.Request.DeviceID,
"Sid": opt.Request.Sid,
"Skey": opt.Request.Skey,
"Uin": opt.LoginInfo.WxUin,
"ClientMsgId": time.Now().Unix(),
"Code": 1,
"FromUserName": opt.Message.ToUserName,
"ToUserName": opt.Message.FromUserName,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientWebWxRelationPinOption struct {
Request *BaseRequest
Op uint8
RemarkName string
UserName string
}
// WebWxRelationPin 联系人置顶接口
func (c *Client) WebWxRelationPin(ctx context.Context, opt *ClientWebWxRelationPinOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxoplog)
if err != nil {
return nil, err
}
content := map[string]interface{}{
"BaseRequest": opt.Request,
"CmdId": 3,
"OP": opt.Op,
"RemarkName": opt.RemarkName,
"UserName": opt.UserName,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
// WebWxPushLogin 免扫码登陆接口
func (c *Client) WebWxPushLogin(ctx context.Context, uin int64) (*http.Response, error) {
req, err := c.mode.BuildPushLoginRequest(ctx, c.Domain.BaseHost(), uin)
if err != nil {
return nil, err
}
return c.Do(req)
}
// WebWxSendVideoMsg 发送视频消息接口
func (c *Client) WebWxSendVideoMsg(ctx context.Context, request *BaseRequest, msg *SendMessage) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxsendvideomsg)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "async")
params.Add("f", "json")
params.Add("lang", "zh_CN")
params.Add("pass_ticket", "pass_ticket")
path.RawQuery = params.Encode()
return c.sendMessage(ctx, request, path.String(), msg)
}
type ClientWebWxCreateChatRoomOption struct {
Request *BaseRequest
LoginInfo *LoginInfo
Topic string
Friends []string
}
// WebWxCreateChatRoom 创建群聊
func (c *Client) WebWxCreateChatRoom(ctx context.Context, opt *ClientWebWxCreateChatRoomOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxcreatechatroom)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
params.Add("r", fmt.Sprintf("%d", time.Now().Unix()))
path.RawQuery = params.Encode()
count := len(opt.Friends)
memberList := make([]struct{ UserName string }, count)
for index, member := range opt.Friends {
memberList[index] = struct{ UserName string }{member}
}
content := map[string]interface{}{
"BaseRequest": opt.Request,
"MemberCount": count,
"MemberList": memberList,
"Topic": opt.Topic,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
type ClientWebWxRenameChatRoomOption struct {
Request *BaseRequest
LoginInfo *LoginInfo
NewTopic string
Group string
}
// WebWxRenameChatRoom 群组重命名接口
func (c *Client) WebWxRenameChatRoom(ctx context.Context, opt *ClientWebWxRenameChatRoomOption) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxupdatechatroom)
if err != nil {
return nil, err
}
params := url.Values{}
params.Add("fun", "modtopic")
params.Add("pass_ticket", opt.LoginInfo.PassTicket)
path.RawQuery = params.Encode()
content := map[string]interface{}{
"BaseRequest": opt.Request,
"ChatRoomName": opt.Group,
"NewTopic": opt.NewTopic,
}
body, err := jsonEncode(content)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", jsonContentType)
return c.Do(req)
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Go
1
https://gitee.com/windylinkin/openwechat.git
git@gitee.com:windylinkin/openwechat.git
windylinkin
openwechat
openwechat
master

搜索帮助

0d507c66 1850385 C8b1a773 1850385