mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-07 14:12:27 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a810837a4 | ||
|
|
c51d41ee8a | ||
|
|
24f812d187 | ||
|
|
dd43b7baa3 | ||
|
|
2dfd2ff608 | ||
|
|
23bb10b0c9 | ||
|
|
b639d2235d | ||
|
|
26d2093bd7 | ||
|
|
cf42cd8d54 | ||
|
|
85ee45580b | ||
|
|
208d5c528a | ||
|
|
b5f9a8933e | ||
|
|
52fb5596d3 | ||
|
|
44150c557e | ||
|
|
635a0c640d |
33
cache/redis.go
vendored
33
cache/redis.go
vendored
@@ -2,6 +2,8 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
@@ -15,25 +17,38 @@ type Redis struct {
|
|||||||
|
|
||||||
// RedisOpts redis 连接属性
|
// RedisOpts redis 连接属性
|
||||||
type RedisOpts struct {
|
type RedisOpts struct {
|
||||||
Host string `yml:"host" json:"host"`
|
Host string `json:"host" yml:"host"`
|
||||||
Username string `yaml:"username" json:"username"`
|
Username string `json:"username" yaml:"username"`
|
||||||
Password string `yml:"password" json:"password"`
|
Password string `json:"password" yml:"password"`
|
||||||
Database int `yml:"database" json:"database"`
|
Database int `json:"database" yml:"database"`
|
||||||
MaxIdle int `yml:"max_idle" json:"max_idle"`
|
MaxIdle int `json:"max_idle" yml:"max_idle"`
|
||||||
MaxActive int `yml:"max_active" json:"max_active"`
|
MaxActive int `json:"max_active" yml:"max_active"`
|
||||||
IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` // second
|
IdleTimeout int `json:"idle_timeout" yml:"idle_timeout"` // second
|
||||||
|
UseTLS bool `json:"use_tls" yml:"use_tls"` // 是否使用TLS
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRedis 实例化
|
// NewRedis 实例化
|
||||||
func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
||||||
conn := redis.NewUniversalClient(&redis.UniversalOptions{
|
uniOpt := &redis.UniversalOptions{
|
||||||
Addrs: []string{opts.Host},
|
Addrs: []string{opts.Host},
|
||||||
DB: opts.Database,
|
DB: opts.Database,
|
||||||
Username: opts.Username,
|
Username: opts.Username,
|
||||||
Password: opts.Password,
|
Password: opts.Password,
|
||||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||||
MinIdleConns: opts.MaxIdle,
|
MinIdleConns: opts.MaxIdle,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if opts.UseTLS {
|
||||||
|
h, _, err := net.SplitHostPort(opts.Host)
|
||||||
|
if err != nil {
|
||||||
|
h = opts.Host
|
||||||
|
}
|
||||||
|
uniOpt.TLSConfig = &tls.Config{
|
||||||
|
ServerName: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := redis.NewUniversalClient(uniOpt)
|
||||||
return &Redis{ctx: ctx, conn: conn}
|
return &Redis{ctx: ctx, conn: conn}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package credential
|
package credential
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context2 "context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -42,6 +43,16 @@ type ResTicket struct {
|
|||||||
|
|
||||||
// GetTicket 获取jsapi_ticket
|
// GetTicket 获取jsapi_ticket
|
||||||
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
||||||
|
return js.GetTicketContext(context2.Background(), accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTicketFromServer 从服务器中获取ticket
|
||||||
|
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
||||||
|
return GetTicketFromServerContext(context2.Background(), accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTicketContext 获取jsapi_ticket
|
||||||
|
func (js *DefaultJsTicket) GetTicketContext(ctx context2.Context, accessToken string) (ticketStr string, err error) {
|
||||||
// 先从cache中取
|
// 先从cache中取
|
||||||
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
@@ -57,7 +68,7 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ticket ResTicket
|
var ticket ResTicket
|
||||||
ticket, err = GetTicketFromServer(accessToken)
|
ticket, err = GetTicketFromServerContext(ctx, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,11 +78,11 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTicketFromServer 从服务器中获取ticket
|
// GetTicketFromServerContext 从服务器中获取ticket
|
||||||
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
func GetTicketFromServerContext(ctx context2.Context, accessToken string) (ticket ResTicket, err error) {
|
||||||
var response []byte
|
var response []byte
|
||||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||||
response, err = util.HTTPGet(url)
|
response, err = util.HTTPGetContext(ctx, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
22
credential/default_js_ticket_test.go
Normal file
22
credential/default_js_ticket_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package credential
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/h2non/gock.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetTicketFromServerContext 测试 GetTicketFromServerContext 函数
|
||||||
|
func TestGetTicketFromServerContext(t *testing.T) {
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New(fmt.Sprintf(getTicketURL, "arg-ak")).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10})
|
||||||
|
|
||||||
|
ticket, err := GetTicketFromServerContext(context.Background(), "arg-ak")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(0), ticket.ErrCode)
|
||||||
|
assert.Equal(t, "mock-ticket", ticket.Ticket, "they should be equal")
|
||||||
|
assert.Equal(t, int64(10), ticket.ExpiresIn, "they should be equal")
|
||||||
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
package credential
|
package credential
|
||||||
|
|
||||||
|
import context2 "context"
|
||||||
|
|
||||||
// JsTicketHandle js ticket获取
|
// JsTicketHandle js ticket获取
|
||||||
type JsTicketHandle interface {
|
type JsTicketHandle interface {
|
||||||
// GetTicket 获取ticket
|
// GetTicket 获取ticket
|
||||||
GetTicket(accessToken string) (ticket string, err error)
|
GetTicket(accessToken string) (ticket string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JsTicketContextHandle js ticket获取
|
||||||
|
type JsTicketContextHandle interface {
|
||||||
|
JsTicketHandle
|
||||||
|
GetTicketContext(ctx context2.Context, accessToken string) (ticket string, err error)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// code2SessionURL 小程序登录
|
||||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
||||||
|
// checkEncryptedDataURL 检查加密信息
|
||||||
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
|
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
|
||||||
|
// getPhoneNumber 获取手机号
|
||||||
getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
|
getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
|
||||||
|
// checkSessionURL 检验登录态
|
||||||
|
checkSessionURL = "https://api.weixin.qq.com/wxa/checksession?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||||
|
// resetUserSessionKeyURL 重置登录态
|
||||||
|
resetUserSessionKeyURL = "https://api.weixin.qq.com/wxa/resetusersessionkey?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||||
|
// getPluginOpenPIDURL 获取插件用户openPID
|
||||||
|
getPluginOpenPIDURL = "https://api.weixin.qq.com/wxa/getpluginopenpid?access_token=%s"
|
||||||
|
// getPaidUnionIDURL 支付后获取 UnionID
|
||||||
|
getPaidUnionIDURL = "https://api.weixin.qq.com/wxa/getpaidunionid"
|
||||||
|
// getUserEncryptKeyURL 获取用户encryptKey
|
||||||
|
getUserEncryptKeyURL = "https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auth 登录/用户信息
|
// Auth 登录/用户信息
|
||||||
@@ -65,9 +76,45 @@ func (auth *Auth) Code2SessionContext(ctx context2.Context, jsCode string) (resu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// GetPaidUnionIDRequest 支付后获取UnionID请求
|
||||||
|
GetPaidUnionIDRequest struct {
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
TransactionID string `json:"transaction_id,omitempty"`
|
||||||
|
MchID string `json:"mch_id,omitempty"`
|
||||||
|
OutTradeNo string `json:"out_trade_no,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPaidUnionIDResponse 支付后获取UnionID响应
|
||||||
|
GetPaidUnionIDResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
UnionID string `json:"unionid"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
||||||
func (auth *Auth) GetPaidUnionID() {
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPaidUnionid.html
|
||||||
// TODO
|
func (auth *Auth) GetPaidUnionID(req *GetPaidUnionIDRequest) (string, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var url string
|
||||||
|
if req.TransactionID != "" {
|
||||||
|
url = fmt.Sprintf("%s?access_token=%s&openid=%s&transaction_id=%s", getPaidUnionIDURL, accessToken, req.OpenID, req.TransactionID)
|
||||||
|
} else {
|
||||||
|
url = fmt.Sprintf("%s?access_token=%s&openid=%s&mch_id=%s&out_trade_no=%s", getPaidUnionIDURL, accessToken, req.OpenID, req.MchID, req.OutTradeNo)
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(url); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result := &GetPaidUnionIDResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetPaidUnionID")
|
||||||
|
return result.UnionID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
||||||
@@ -81,7 +128,7 @@ func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHa
|
|||||||
var (
|
var (
|
||||||
at string
|
at string
|
||||||
)
|
)
|
||||||
if at, err = auth.GetAccessToken(); err != nil {
|
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +167,7 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
|||||||
at string
|
at string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if at, err = auth.GetAccessToken(); err != nil {
|
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
@@ -146,3 +193,115 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
|||||||
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
||||||
return auth.GetPhoneNumberContext(context2.Background(), code)
|
return auth.GetPhoneNumberContext(context2.Background(), code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckSession 检验登录态
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/checkSessionKey.html
|
||||||
|
func (auth *Auth) CheckSession(signature, openID string) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(checkSessionURL, accessToken, signature, openID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "CheckSession")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserSessionKeyResponse 重置登录态响应
|
||||||
|
type ResetUserSessionKeyResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
SessionKey string `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserSessionKey 重置登录态
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/ResetUserSessionKey.html
|
||||||
|
func (auth *Auth) ResetUserSessionKey(signature, openID string) (*ResetUserSessionKeyResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(resetUserSessionKeyURL, accessToken, signature, openID)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &ResetUserSessionKeyResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "ResetUserSessionKey")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// GetPluginOpenPIDRequest 获取插件用户openPID请求
|
||||||
|
GetPluginOpenPIDRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginOpenPIDResponse 获取插件用户openPID响应
|
||||||
|
GetPluginOpenPIDResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
OpenPID string `json:"openpid"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPluginOpenPID 获取插件用户openPID
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPluginOpenPId.html
|
||||||
|
func (auth *Auth) GetPluginOpenPID(code string) (string, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req := &GetPluginOpenPIDRequest{
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(getPluginOpenPIDURL, accessToken), req); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result := &GetPluginOpenPIDResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetPluginOpenPID")
|
||||||
|
return result.OpenPID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserEncryptKeyResponse 获取用户encryptKey响应
|
||||||
|
type GetUserEncryptKeyResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
KeyInfoList []KeyInfo `json:"key_info_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyInfo 用户最近三次的加密key
|
||||||
|
type KeyInfo struct {
|
||||||
|
EncryptKey string `json:"encrypt_key"`
|
||||||
|
Version int64 `json:"version"`
|
||||||
|
ExpireIn int64 `json:"expire_in"`
|
||||||
|
Iv string `json:"iv"`
|
||||||
|
CreateTime int64 `json:"create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserEncryptKey 获取用户encryptKey
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/internet/getUserEncryptKey.html
|
||||||
|
func (auth *Auth) GetUserEncryptKey(signature, openID string) (*GetUserEncryptKeyResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(getUserEncryptKeyURL, accessToken, signature, openID)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetUserEncryptKeyResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetUserEncryptKey")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// createActivityURL 创建activity_id
|
// createActivityIDURL 创建activity_id
|
||||||
createActivityURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s"
|
createActivityIDURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/activityid/create?access_token=%s&unionid=%s&openid=%s"
|
||||||
// SendUpdatableMsgURL 修改动态消息
|
// SendUpdatableMsgURL 修改动态消息
|
||||||
setUpdatableMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/updatablemsg/send?access_token=%s"
|
setUpdatableMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/updatablemsg/send?access_token=%s"
|
||||||
|
// setChatToolMsgURL 修改小程序聊天工具的动态卡片消息
|
||||||
|
setChatToolMsgURL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/chattoolmsg/send?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdatableTargetState 动态消息状态
|
// UpdatableTargetState 动态消息状态
|
||||||
@@ -38,15 +40,26 @@ func NewUpdatableMessage(ctx *context.Context) *UpdatableMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateActivityIDRequest 创建activity_id请求
|
||||||
|
type CreateActivityIDRequest struct {
|
||||||
|
UnionID string
|
||||||
|
OpenID string
|
||||||
|
}
|
||||||
|
|
||||||
// CreateActivityID 创建activity_id
|
// CreateActivityID 创建activity_id
|
||||||
func (updatableMessage *UpdatableMessage) CreateActivityID() (res CreateActivityIDResponse, err error) {
|
func (updatableMessage *UpdatableMessage) CreateActivityID() (CreateActivityIDResponse, error) {
|
||||||
|
return updatableMessage.CreateActivityIDWithReq(&CreateActivityIDRequest{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateActivityIDWithReq 创建activity_id
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/updatable-message/createActivityId.html
|
||||||
|
func (updatableMessage *UpdatableMessage) CreateActivityIDWithReq(req *CreateActivityIDRequest) (res CreateActivityIDResponse, err error) {
|
||||||
accessToken, err := updatableMessage.GetAccessToken()
|
accessToken, err := updatableMessage.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
url := fmt.Sprintf(createActivityIDURL, accessToken, req.UnionID, req.OpenID)
|
||||||
uri := fmt.Sprintf(createActivityURL, accessToken)
|
response, err := util.HTTPGet(url)
|
||||||
response, err := util.HTTPGet(uri)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,3 +113,35 @@ type SendUpdatableMsgReq struct {
|
|||||||
TemplateInfo UpdatableMsgTemplate `json:"template_info"`
|
TemplateInfo UpdatableMsgTemplate `json:"template_info"`
|
||||||
TargetState UpdatableTargetState `json:"target_state"`
|
TargetState UpdatableTargetState `json:"target_state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetChatToolMsgRequest 修改小程序聊天工具的动态卡片消息请求
|
||||||
|
type SetChatToolMsgRequest struct {
|
||||||
|
VersionType int64 `json:"version_type"`
|
||||||
|
TargetState UpdatableTargetState `json:"target_state"`
|
||||||
|
ActivityID string `json:"activity_id"`
|
||||||
|
TemplateID string `json:"template_id"`
|
||||||
|
ParticipatorInfoList []ParticipatorInfo `json:"participator_info_list,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParticipatorInfo 更新后的聊天室成员状态
|
||||||
|
type ParticipatorInfo struct {
|
||||||
|
State int `json:"state"`
|
||||||
|
GroupOpenID string `json:"group_openid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChatToolMsg 修改小程序聊天工具的动态卡片消息
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/updatable-message/setChatToolMsg.html
|
||||||
|
func (updatableMessage *UpdatableMessage) SetChatToolMsg(req *SetChatToolMsgRequest) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = updatableMessage.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(setChatToolMsgURL, accessToken), req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "SetChatToolMsg")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package security
|
package security
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context2 "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -64,7 +65,12 @@ type MediaCheckAsyncRequest struct {
|
|||||||
|
|
||||||
// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容
|
// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容
|
||||||
func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) {
|
func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) {
|
||||||
accessToken, err := security.GetAccessToken()
|
return security.MediaCheckAsyncContext(context2.Background(), in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaCheckAsyncContext 异步校验图片/音频是否含有违法违规内容
|
||||||
|
func (security *Security) MediaCheckAsyncContext(ctx context2.Context, in *MediaCheckAsyncRequest) (traceID string, err error) {
|
||||||
|
accessToken, err := security.GetAccessTokenContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -77,7 +83,7 @@ func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID s
|
|||||||
req.Version = 2
|
req.Version = 2
|
||||||
|
|
||||||
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
|
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
|
||||||
response, err := util.PostJSON(uri, req)
|
response, err := util.PostJSONContext(ctx, uri, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -222,7 +228,12 @@ func (security *Security) MsgCheckV1(content string) (res MsgCheckResponse, err
|
|||||||
|
|
||||||
// MsgCheck 检查一段文本是否含有违法违规内容
|
// MsgCheck 检查一段文本是否含有违法违规内容
|
||||||
func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
||||||
accessToken, err := security.GetAccessToken()
|
return security.MsgCheckContext(context2.Background(), in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MsgCheckContext 检查一段文本是否含有违法违规内容
|
||||||
|
func (security *Security) MsgCheckContext(ctx context2.Context, in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
||||||
|
accessToken, err := security.GetAccessTokenContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -235,7 +246,7 @@ func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, e
|
|||||||
req.Version = 2
|
req.Version = 2
|
||||||
|
|
||||||
uri := fmt.Sprintf(msgCheckURL, accessToken)
|
uri := fmt.Sprintf(msgCheckURL, accessToken)
|
||||||
response, err := util.PostJSON(uri, req)
|
response, err := util.PostJSONContext(ctx, uri, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,22 +12,30 @@ const (
|
|||||||
// 发送订阅消息
|
// 发送订阅消息
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
||||||
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"
|
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"
|
||||||
|
|
||||||
// 获取当前帐号下的个人模板列表
|
// 获取当前帐号下的个人模板列表
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
||||||
getTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
getTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
||||||
|
|
||||||
// 添加订阅模板
|
// 添加订阅模板
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html
|
||||||
addTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate"
|
addTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate"
|
||||||
|
|
||||||
// 删除私有模板
|
// 删除私有模板
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.deleteTemplate.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.deleteTemplate.html
|
||||||
delTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate"
|
delTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate"
|
||||||
|
|
||||||
// 统一服务消息
|
// 统一服务消息
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
|
||||||
uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send"
|
uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send"
|
||||||
|
// getCategoryURL 获取类目
|
||||||
|
getCategoryURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getcategory?access_token=%s"
|
||||||
|
// getPubTemplateKeyWordsByIDURL 获取关键词列表
|
||||||
|
getPubTemplateKeyWordsByIDURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatekeywords?access_token=%s&tid=%s"
|
||||||
|
// getPubTemplateTitleListURL 获取所属类目下的公共模板
|
||||||
|
getPubTemplateTitleListURL = "https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatetitles?access_token=%s&ids=%s&start=%d&limit=%d"
|
||||||
|
// setUserNotifyURL 激活与更新服务卡片
|
||||||
|
setUserNotifyURL = "https://api.weixin.qq.com/wxa/set_user_notify?access_token=%s"
|
||||||
|
// setUserNotifyExtURL 更新服务卡片扩展信息
|
||||||
|
setUserNotifyExtURL = "https://api.weixin.qq.com/wxa/set_user_notifyext?access_token=%s"
|
||||||
|
// getUserNotifyURL 查询服务卡片状态
|
||||||
|
getUserNotifyURL = "https://api.weixin.qq.com/wxa/get_user_notify?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Subscribe 订阅消息
|
// Subscribe 订阅消息
|
||||||
@@ -58,11 +66,18 @@ type DataItem struct {
|
|||||||
|
|
||||||
// TemplateItem template item
|
// TemplateItem template item
|
||||||
type TemplateItem struct {
|
type TemplateItem struct {
|
||||||
PriTmplID string `json:"priTmplId"`
|
PriTmplID string `json:"priTmplId"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Example string `json:"example"`
|
Example string `json:"example"`
|
||||||
Type int64 `json:"type"`
|
Type int64 `json:"type"`
|
||||||
|
KeywordEnumValueList []KeywordEnumValue `json:"keywordEnumValueList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeywordEnumValue 枚举参数值范围
|
||||||
|
type KeywordEnumValue struct {
|
||||||
|
EnumValueList []string `json:"enumValueList"`
|
||||||
|
KeywordCode string `json:"keywordCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateList template list
|
// TemplateList template list
|
||||||
@@ -224,3 +239,200 @@ func (s *Subscribe) Delete(templateID string) (err error) {
|
|||||||
}
|
}
|
||||||
return util.DecodeWithCommonError(response, "DeleteSubscribe")
|
return util.DecodeWithCommonError(response, "DeleteSubscribe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCategoryResponse 获取类目响应
|
||||||
|
type GetCategoryResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
Data []Category `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category 类目
|
||||||
|
type Category struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategory 获取类目
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getCategory.html
|
||||||
|
func (s *Subscribe) GetCategory() ([]Category, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(getCategoryURL, accessToken)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetCategoryResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetCategory")
|
||||||
|
return result.Data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPubTemplateKeywordsByIDResponse 获取关键词列表响应
|
||||||
|
type GetPubTemplateKeywordsByIDResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
Data []PubTemplateKeywords `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubTemplateKeywords 关键词
|
||||||
|
type PubTemplateKeywords struct {
|
||||||
|
KID int64 `json:"kid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Example string `json:"example"`
|
||||||
|
Rule string `json:"rule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPubTemplateKeywordsByID 获取关键词列表
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getPubTemplateKeyWordsById.html
|
||||||
|
func (s *Subscribe) GetPubTemplateKeywordsByID(tid string) (*GetPubTemplateKeywordsByIDResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(getPubTemplateKeyWordsByIDURL, accessToken, tid)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetPubTemplateKeywordsByIDResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetPubTemplateKeywordsByID")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPubTemplateTitleListRequest 获取所属类目下的公共模板请求
|
||||||
|
type GetPubTemplateTitleListRequest struct {
|
||||||
|
Start int64
|
||||||
|
Limit int64
|
||||||
|
IDs string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPubTemplateTitleListResponse 获取所属类目下的公共模板响应
|
||||||
|
type GetPubTemplateTitleListResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
Data []PubTemplateTitle `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubTemplateTitle 模板标题
|
||||||
|
type PubTemplateTitle struct {
|
||||||
|
Type int64 `json:"type"`
|
||||||
|
TID string `json:"tid"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
CategoryID string `json:"categoryId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPubTemplateTitleList 获取所属类目下的公共模板
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getPubTemplateTitleList.html
|
||||||
|
func (s *Subscribe) GetPubTemplateTitleList(req *GetPubTemplateTitleListRequest) (*GetPubTemplateTitleListResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(getPubTemplateTitleListURL, accessToken, req.IDs, req.Start, req.Limit)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetPubTemplateTitleListResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetPubTemplateTitleList")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserNotifyRequest 激活与更新服务卡片请求
|
||||||
|
type SetUserNotifyRequest struct {
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
NotifyType int64 `json:"notify_type"`
|
||||||
|
NotifyCode string `json:"notify_code"`
|
||||||
|
ContentJSON string `json:"content_json"`
|
||||||
|
CheckJSON string `json:"check_json,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserNotify 激活与更新服务卡片
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/setUserNotify.html
|
||||||
|
func (s *Subscribe) SetUserNotify(req *SetUserNotifyRequest) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(setUserNotifyURL, accessToken), req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "SetUserNotify")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserNotifyExtRequest 更新服务卡片扩展信息请求
|
||||||
|
type SetUserNotifyExtRequest struct {
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
NotifyType int64 `json:"notify_type"`
|
||||||
|
NotifyCode string `json:"notify_code"`
|
||||||
|
ExtJSON string `json:"ext_json"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserNotifyExt 更新服务卡片扩展信息
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/setUserNotifyExt.html
|
||||||
|
func (s *Subscribe) SetUserNotifyExt(req *SetUserNotifyExtRequest) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(setUserNotifyExtURL, accessToken), req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "SetUserNotifyExt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserNotifyRequest 查询服务卡片状态请求
|
||||||
|
type GetUserNotifyRequest struct {
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
NotifyType int64 `json:"notify_type"`
|
||||||
|
NotifyCode string `json:"notify_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserNotifyResponse 查询服务卡片状态响应
|
||||||
|
type GetUserNotifyResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
NotifyInfo NotifyInfo `json:"notify_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyInfo 卡片状态
|
||||||
|
type NotifyInfo struct {
|
||||||
|
NotifyType int64 `json:"notify_type"`
|
||||||
|
ContentJSON string `json:"content_json"`
|
||||||
|
CodeState int64 `json:"code_state"`
|
||||||
|
CodeExpireTime int64 `json:"code_expire_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserNotify 查询服务卡片状态
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/getUserNotify.html
|
||||||
|
func (s *Subscribe) GetUserNotify(req *GetUserNotifyRequest) (*GetUserNotifyResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = s.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(getUserNotifyURL, accessToken), req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetUserNotifyResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetUserNotify")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink"
|
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink?access_token=%s"
|
||||||
|
|
||||||
|
// ULQueryRequest 查询加密URLLink请求
|
||||||
|
type ULQueryRequest struct {
|
||||||
|
URLLink string `json:"url_link"`
|
||||||
|
QueryType int `json:"query_type"`
|
||||||
|
}
|
||||||
|
|
||||||
// ULQueryResult 返回的结果
|
// ULQueryResult 返回的结果
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值
|
||||||
@@ -28,25 +34,35 @@ type ULQueryResult struct {
|
|||||||
ResourceAppid string `json:"resource_appid"`
|
ResourceAppid string `json:"resource_appid"`
|
||||||
} `json:"cloud_base"`
|
} `json:"cloud_base"`
|
||||||
} `json:"url_link_info"`
|
} `json:"url_link_info"`
|
||||||
VisitOpenid string `json:"visit_openid"`
|
VisitOpenid string `json:"visit_openid"`
|
||||||
|
QuotaInfo QuotaInfo `json:"quota_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaInfo quota 配置
|
||||||
|
type QuotaInfo struct {
|
||||||
|
RemainVisitQuota int64 `json:"remain_visit_quota"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query 查询小程序 url_link 配置。
|
// Query 查询小程序 url_link 配置。
|
||||||
func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) {
|
func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) {
|
||||||
accessToken, err := u.GetAccessToken()
|
return u.QueryWithType(&ULQueryRequest{URLLink: urlLink})
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
// QueryWithType 查询加密URLLink
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-link/queryUrlLink.html
|
||||||
uri := fmt.Sprintf("%s?access_token=%s", queryURL, accessToken)
|
func (u *URLLink) QueryWithType(req *ULQueryRequest) (*ULQueryResult, error) {
|
||||||
response, err := util.PostJSON(uri, map[string]string{"url_link": urlLink})
|
var (
|
||||||
if err != nil {
|
accessToken string
|
||||||
return nil, err
|
err error
|
||||||
}
|
)
|
||||||
var resp ULQueryResult
|
if accessToken, err = u.GetAccessToken(); err != nil {
|
||||||
err = util.DecodeWithError(response, &resp, "URLLink.Query")
|
return nil, err
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
var response []byte
|
||||||
}
|
if response, err = util.PostJSON(fmt.Sprintf(queryURL, accessToken), req); err != nil {
|
||||||
return &resp, nil
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &ULQueryResult{}
|
||||||
|
err = util.DecodeWithError(response, result, "URLLink.Query")
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ const (
|
|||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||||
type QueryScheme struct {
|
type QueryScheme struct {
|
||||||
// 小程序 scheme 码
|
// 小程序 scheme 码
|
||||||
Scheme string `json:"scheme"`
|
Scheme string `json:"scheme"`
|
||||||
|
QueryType int `json:"query_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SchemeInfo scheme 配置
|
// SchemeInfo scheme 配置
|
||||||
@@ -33,34 +34,47 @@ type SchemeInfo struct {
|
|||||||
EnvVersion EnvVersion `json:"env_version"`
|
EnvVersion EnvVersion `json:"env_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// resQueryScheme 返回结构体
|
// QuotaInfo quota 配置
|
||||||
|
type QuotaInfo struct {
|
||||||
|
RemainVisitQuota int64 `json:"remain_visit_quota"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResQueryScheme 返回结构体
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||||
type resQueryScheme struct {
|
type ResQueryScheme struct {
|
||||||
// 通用错误
|
// 通用错误
|
||||||
util.CommonError
|
util.CommonError
|
||||||
// scheme 配置
|
// scheme 配置
|
||||||
SchemeInfo SchemeInfo `json:"scheme_info"`
|
SchemeInfo SchemeInfo `json:"scheme_info"`
|
||||||
// 访问该链接的openid,没有用户访问过则为空字符串
|
// 访问该链接的openid,没有用户访问过则为空字符串
|
||||||
VisitOpenid string `json:"visit_openid"`
|
VisitOpenid string `json:"visit_openid"`
|
||||||
|
QuotaInfo QuotaInfo `json:"quota_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryScheme 查询小程序 scheme 码
|
// QueryScheme 查询小程序 scheme 码
|
||||||
func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) {
|
func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) {
|
||||||
var accessToken string
|
res, err := u.QuerySchemeWithRes(querySchemeParams)
|
||||||
accessToken, err = u.GetAccessToken()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
urlStr := fmt.Sprintf(querySchemeURL, accessToken)
|
|
||||||
var response []byte
|
|
||||||
response, err = util.PostJSON(urlStr, querySchemeParams)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用通用方法返回错误
|
|
||||||
var res resQueryScheme
|
|
||||||
err = util.DecodeWithError(response, &res, "QueryScheme")
|
|
||||||
return res.SchemeInfo, res.VisitOpenid, err
|
return res.SchemeInfo, res.VisitOpenid, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuerySchemeWithRes 查询scheme码
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/queryScheme.html
|
||||||
|
func (u *URLScheme) QuerySchemeWithRes(req QueryScheme) (*ResQueryScheme, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = u.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(querySchemeURL, accessToken), req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &ResQueryScheme{}
|
||||||
|
err = util.DecodeWithError(response, result, "QueryScheme")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ func NewURLScheme(ctx *context.Context) *URLScheme {
|
|||||||
return &URLScheme{Context: ctx}
|
return &URLScheme{Context: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
|
const (
|
||||||
|
// generateURL 获取加密scheme码
|
||||||
|
generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
|
||||||
|
// generateNFCURL 获取 NFC 的小程序 scheme
|
||||||
|
generateNFCURL = "https://api.weixin.qq.com/wxa/generatenfcscheme?access_token=%s"
|
||||||
|
)
|
||||||
|
|
||||||
// TExpireType 失效类型 (指定时间戳/指定间隔)
|
// TExpireType 失效类型 (指定时间戳/指定间隔)
|
||||||
type TExpireType int
|
type TExpireType int
|
||||||
@@ -50,11 +55,13 @@ type JumpWxa struct {
|
|||||||
// USParams 请求参数
|
// USParams 请求参数
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
|
||||||
type USParams struct {
|
type USParams struct {
|
||||||
JumpWxa *JumpWxa `json:"jump_wxa"`
|
JumpWxa *JumpWxa `json:"jump_wxa,omitempty"`
|
||||||
ExpireType TExpireType `json:"expire_type"`
|
ExpireType TExpireType `json:"expire_type,omitempty"`
|
||||||
ExpireTime int64 `json:"expire_time"`
|
ExpireTime int64 `json:"expire_time,omitempty"`
|
||||||
ExpireInterval int `json:"expire_interval"`
|
ExpireInterval int `json:"expire_interval,omitempty"`
|
||||||
IsExpire bool `json:"is_expire,omitempty"`
|
IsExpire bool `json:"is_expire,omitempty"`
|
||||||
|
ModelID string `json:"model_id,omitempty"`
|
||||||
|
Sn string `json:"sn,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// USResult 返回的结果
|
// USResult 返回的结果
|
||||||
@@ -81,3 +88,22 @@ func (u *URLScheme) Generate(params *USParams) (string, error) {
|
|||||||
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
||||||
return resp.OpenLink, err
|
return resp.OpenLink, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateNFC 获取 NFC 的小程序 scheme
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/generateNFCScheme.html
|
||||||
|
func (u *URLScheme) GenerateNFC(params *USParams) (string, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = u.GetAccessToken(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(generateNFCURL, accessToken), params); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result := &USResult{}
|
||||||
|
err = util.DecodeWithError(response, result, "URLScheme.GenerateNFC")
|
||||||
|
return result.OpenLink, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package js
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context2 "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/credential"
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
@@ -39,20 +40,40 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
|||||||
// GetConfig 获取jssdk需要的配置参数
|
// GetConfig 获取jssdk需要的配置参数
|
||||||
// uri 为当前网页地址
|
// uri 为当前网页地址
|
||||||
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||||
|
return js.GetConfigContext(context2.Background(), uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
||||||
|
func (js *Js) GetConfigContext(ctx context2.Context, uri string) (config *Config, err error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
accessToken, err = js.GetAccessToken()
|
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
||||||
|
if ctxHandle, ok := js.Context.AccessTokenHandle.(credential.AccessTokenContextHandle); ok {
|
||||||
|
accessToken, err = ctxHandle.GetAccessTokenContext(ctx)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 AccessTokenContextHandle 接口,调用旧的 GetAccessToken 方法
|
||||||
|
accessToken, err = js.Context.GetAccessToken()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticketStr string
|
var ticketStr string
|
||||||
ticketStr, err = js.GetTicket(accessToken)
|
// 类型断言 jsTicket
|
||||||
|
if ticketCtxHandle, ok := js.JsTicketHandle.(credential.JsTicketContextHandle); ok {
|
||||||
|
ticketStr, err = ticketCtxHandle.GetTicketContext(ctx, accessToken)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 JsTicketContextHandle 接口,调用旧的 GetTicket 方法
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nonceStr := util.RandomStr(16)
|
nonceStr := util.RandomStr(16)
|
||||||
timestamp := util.GetCurrTS()
|
timestamp := util.GetCurrTS()
|
||||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
sigStr := util.Signature(str)
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
config = new(Config)
|
config = new(Config)
|
||||||
config.AppID = js.AppID
|
config.AppID = js.AppID
|
||||||
config.NonceStr = nonceStr
|
config.NonceStr = nonceStr
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package account
|
|||||||
|
|
||||||
import "github.com/silenceper/wechat/v2/openplatform/context"
|
import "github.com/silenceper/wechat/v2/openplatform/context"
|
||||||
|
|
||||||
// Account 开放平台张哈管理
|
// Account 开放平台帐号管理
|
||||||
// TODO 实现方法
|
// TODO 实现方法
|
||||||
type Account struct {
|
type Account struct {
|
||||||
*context.Context
|
*context.Context
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const (
|
|||||||
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
|
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
|
||||||
componentLoginURL = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=%d&biz_appid=%s"
|
componentLoginURL = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=%d&biz_appid=%s"
|
||||||
bindComponentURL = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
|
bindComponentURL = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
|
||||||
|
bindComponentURLV2 = "https://open.weixin.qq.com/wxaopen/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
|
||||||
// TODO 获取授权方选项信息
|
// TODO 获取授权方选项信息
|
||||||
// getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
// getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
||||||
// TODO 获取已授权的账号信息
|
// TODO 获取已授权的账号信息
|
||||||
@@ -137,6 +138,20 @@ func (ctx *Context) GetBindComponentURL(redirectURI string, authType int, bizApp
|
|||||||
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
|
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBindComponentURLV2Context 获取新版本第三方公众号授权链接(链接跳转,适用移动端)
|
||||||
|
func (ctx *Context) GetBindComponentURLV2Context(stdCtx context.Context, redirectURI string, authType int, bizAppID string) (string, error) {
|
||||||
|
code, err := ctx.GetPreCodeContext(stdCtx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(bindComponentURLV2, authType, ctx.AppID, code, url.QueryEscape(redirectURI), bizAppID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBindComponentURLV2 获取新版本第三方公众号授权链接(链接跳转,适用移动端)
|
||||||
|
func (ctx *Context) GetBindComponentURLV2(redirectURI string, authType int, bizAppID string) (string, error) {
|
||||||
|
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
|
||||||
|
}
|
||||||
|
|
||||||
// ID 微信返回接口中各种类型字段
|
// ID 微信返回接口中各种类型字段
|
||||||
type ID struct {
|
type ID struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
@@ -225,6 +240,10 @@ func (ctx *Context) RefreshAuthrTokenContext(stdCtx context.Context, appid, refr
|
|||||||
if err := cache.SetContext(stdCtx, ctx.Cache, authrTokenKey, ret.AccessToken, time.Second*time.Duration(ret.ExpiresIn-30)); err != nil {
|
if err := cache.SetContext(stdCtx, ctx.Cache, authrTokenKey, ret.AccessToken, time.Second*time.Duration(ret.ExpiresIn-30)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
refreshTokenKey := "authorizer_refresh_token_" + appid
|
||||||
|
if err := cache.SetContext(stdCtx, ctx.Cache, refreshTokenKey, ret.RefreshToken, 10*365*24*60*60*time.Second); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +257,18 @@ func (ctx *Context) GetAuthrAccessTokenContext(stdCtx context.Context, appid str
|
|||||||
authrTokenKey := "authorizer_access_token_" + appid
|
authrTokenKey := "authorizer_access_token_" + appid
|
||||||
val := cache.GetContext(stdCtx, ctx.Cache, authrTokenKey)
|
val := cache.GetContext(stdCtx, ctx.Cache, authrTokenKey)
|
||||||
if val == nil {
|
if val == nil {
|
||||||
return "", fmt.Errorf("cannot get authorizer %s access token", appid)
|
refreshTokenKey := "authorizer_refresh_token_" + appid
|
||||||
|
val := cache.GetContext(stdCtx, ctx.Cache, refreshTokenKey)
|
||||||
|
if val == nil {
|
||||||
|
return "", fmt.Errorf("cannot get authorizer %s refresh token", appid)
|
||||||
|
}
|
||||||
|
token, err := ctx.RefreshAuthrTokenContext(stdCtx, appid, val.(string))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return val.(string), nil
|
return val.(string), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package js
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context2 "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/credential"
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
@@ -32,14 +33,31 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
|||||||
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
|
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
|
||||||
// uri 为当前网页地址
|
// uri 为当前网页地址
|
||||||
func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) {
|
func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) {
|
||||||
config = new(officialJs.Config)
|
return js.GetConfigContext(context2.Background(), uri, appid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
||||||
|
func (js *Js) GetConfigContext(ctx context2.Context, uri, appid string) (config *officialJs.Config, err error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
accessToken, err = js.GetAccessToken()
|
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
||||||
|
if ctxHandle, ok := js.Context.AccessTokenHandle.(credential.AccessTokenContextHandle); ok {
|
||||||
|
accessToken, err = ctxHandle.GetAccessTokenContext(ctx)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 AccessTokenContextHandle 接口,调用旧的 GetAccessToken 方法
|
||||||
|
accessToken, err = js.Context.GetAccessToken()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticketStr string
|
var ticketStr string
|
||||||
ticketStr, err = js.GetTicket(accessToken)
|
// 类型断言 jsTicket
|
||||||
|
if ticketCtxHandle, ok := js.JsTicketHandle.(credential.JsTicketContextHandle); ok {
|
||||||
|
ticketStr, err = ticketCtxHandle.GetTicketContext(ctx, accessToken)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 JsTicketContextHandle 接口,调用旧的 GetTicket 方法
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -49,6 +67,7 @@ func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error
|
|||||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
sigStr := util.Signature(str)
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
|
config = new(officialJs.Config)
|
||||||
config.AppID = appid
|
config.AppID = appid
|
||||||
config.NonceStr = nonceStr
|
config.NonceStr = nonceStr
|
||||||
config.Timestamp = timestamp
|
config.Timestamp = timestamp
|
||||||
|
|||||||
147
openplatform/officialaccount/js/js_test.go
Normal file
147
openplatform/officialaccount/js/js_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// 验证 js.GetConfigContext 是否能正确传递上下文到 HTTP 请求,确保上下文正确传播,防止在获取 JSSDK 配置时发生协程泄露。
|
||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
context2 "context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/cache"
|
||||||
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
|
"github.com/silenceper/wechat/v2/officialaccount/config"
|
||||||
|
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockAccessTokenHandle 模拟 AccessTokenHandle
|
||||||
|
type mockAccessTokenHandle struct{}
|
||||||
|
|
||||||
|
func (m *mockAccessTokenHandle) GetAccessToken() (string, error) {
|
||||||
|
return "mock-access-token", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAccessTokenHandle) GetAccessTokenContext(_ context2.Context) (string, error) {
|
||||||
|
return "mock-access-token", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextCheckingRoundTripper 自定义 RoundTripper 用于检查 context
|
||||||
|
type contextCheckingRoundTripper struct {
|
||||||
|
originalCtx context2.Context
|
||||||
|
t *testing.T
|
||||||
|
key interface{}
|
||||||
|
expectedVal interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *contextCheckingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
// 获取请求中的 context
|
||||||
|
reqCtx := req.Context()
|
||||||
|
|
||||||
|
// 打印 context 比较结果
|
||||||
|
rt.t.Logf("比较上下文的内存地址:\n")
|
||||||
|
if reqCtx == rt.originalCtx {
|
||||||
|
rt.t.Logf("上下文具有相同的内存地址。原始上下文: %p, 请求上下文: %p\n", rt.originalCtx, reqCtx)
|
||||||
|
} else {
|
||||||
|
rt.t.Logf("上下文具有不同的内存地址。原始上下文: %p, 请求上下文: %p\n", rt.originalCtx, reqCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 context 中的键值对
|
||||||
|
if rt.key != nil {
|
||||||
|
value := reqCtx.Value(rt.key)
|
||||||
|
rt.t.Logf("检查请求上下文中的键 %v:\n", rt.key)
|
||||||
|
if value != rt.expectedVal {
|
||||||
|
rt.t.Errorf("上下文键 %v 的值不匹配: 预期 %v, 实际 %v\n", rt.key, rt.expectedVal, value)
|
||||||
|
} else {
|
||||||
|
rt.t.Logf("上下文键 %v 的值匹配: 预期 %v, 实际 %v\n", rt.key, rt.expectedVal, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查上下文是否已取消
|
||||||
|
select {
|
||||||
|
case <-reqCtx.Done():
|
||||||
|
return nil, reqCtx.Err() // 返回上下文取消错误
|
||||||
|
default:
|
||||||
|
// 返回模拟的 HTTP 响应,包含有效的 JSON
|
||||||
|
responseBody := `{"ticket":"mock-ticket","expires_in":7200}`
|
||||||
|
response := &http.Response{
|
||||||
|
Status: "200 OK",
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 1,
|
||||||
|
Body: io.NopCloser(bytes.NewReader([]byte(responseBody))),
|
||||||
|
ContentLength: int64(len(responseBody)),
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
response.Header.Set("Content-Type", "application/json")
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextKey 定义自定义上下文键类型,避免使用内置 string 类型
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
// setupJsInstance 初始化 Js 实例和 HTTP 客户端
|
||||||
|
func setupJsInstance(t *testing.T, ctx context2.Context, key, val interface{}) (*Js, func()) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
AppID: "test-app-id",
|
||||||
|
AppSecret: "test-app-secret",
|
||||||
|
Cache: cache.NewMemory(),
|
||||||
|
}
|
||||||
|
cacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", credential.CacheKeyOfficialAccountPrefix, cfg.AppID)
|
||||||
|
if err := cfg.Cache.Delete(cacheKey); err != nil {
|
||||||
|
t.Fatalf("清除缓存失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Log("清除 jsapi_ticket 的缓存:", cacheKey)
|
||||||
|
|
||||||
|
ctxHandle := &context.Context{Config: cfg, AccessTokenHandle: &mockAccessTokenHandle{}}
|
||||||
|
jsInstance := NewJs(ctxHandle, cfg.AppID)
|
||||||
|
jsInstance.SetJsTicketHandle(credential.NewDefaultJsTicket(cfg.AppID, credential.CacheKeyOfficialAccountPrefix, cfg.Cache))
|
||||||
|
|
||||||
|
originalClient := util.DefaultHTTPClient
|
||||||
|
util.DefaultHTTPClient = &http.Client{
|
||||||
|
Transport: &contextCheckingRoundTripper{originalCtx: ctx, t: t, key: key, expectedVal: val},
|
||||||
|
}
|
||||||
|
return jsInstance, func() { util.DefaultHTTPClient = originalClient }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetConfigContext 测试GetConfigContext的上下文传递和取消行为。
|
||||||
|
func TestGetConfigContext(t *testing.T) {
|
||||||
|
t.Run("ContextPassing", func(t *testing.T) {
|
||||||
|
ctxKey := contextKey("testKey111") // 使用自定义类型 contextKey
|
||||||
|
ctxValue := "testValue222"
|
||||||
|
ctx := context2.WithValue(context2.Background(), ctxKey, ctxValue)
|
||||||
|
t.Logf("创建的测试上下文: %p, 添加的键值对: %v=%v\n", ctx, ctxKey, ctxValue)
|
||||||
|
|
||||||
|
jsInstance, cleanup := setupJsInstance(t, ctx, ctxKey, ctxValue)
|
||||||
|
defer cleanup()
|
||||||
|
t.Log("调用 GetConfigContext")
|
||||||
|
config2, err := jsInstance.GetConfigContext(ctx, "https://www.baidu.com", "test-app-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetConfigContext 失败: %v", err)
|
||||||
|
}
|
||||||
|
if config2.AppID != "test-app-id" {
|
||||||
|
t.Errorf("预期 AppID 为 %s,实际为 %s", "test-app-id", config2.AppID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ContextCancellation", func(t *testing.T) {
|
||||||
|
ctx, cancel := context2.WithCancel(context2.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
jsInstance, cleanup := setupJsInstance(t, ctx, nil, nil)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
t.Log("调用 GetConfigContext(已取消上下文)")
|
||||||
|
_, err := jsInstance.GetConfigContext(ctx, "https://www.baidu.com", "test-app-id")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("预期上下文取消错误,但 GetConfigContext 未返回错误")
|
||||||
|
} else if !errors.Is(err, context2.Canceled) {
|
||||||
|
t.Errorf("预期错误为 context.Canceled,实际为: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -166,6 +166,7 @@ func PostFile(fieldName, filePath, uri string) ([]byte, error) {
|
|||||||
IsFile: true,
|
IsFile: true,
|
||||||
Fieldname: fieldName,
|
Fieldname: fieldName,
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
|
Filename: filePath,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return PostMultipartForm(fields, uri)
|
return PostMultipartForm(fields, uri)
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ type UserUpdateRequest struct {
|
|||||||
Gender int `json:"gender"`
|
Gender int `json:"gender"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
BizMail string `json:"biz_mail"`
|
BizMail string `json:"biz_mail"`
|
||||||
|
BizMailAlias string `json:"biz_mail_alias"`
|
||||||
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
||||||
DirectLeader []string `json:"direct_leader"`
|
DirectLeader []string `json:"direct_leader"`
|
||||||
Enable int `json:"enable"`
|
Enable int `json:"enable"`
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const (
|
|||||||
clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s"
|
clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s"
|
||||||
// delOptionURL 删除打卡规则
|
// delOptionURL 删除打卡规则
|
||||||
delOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/del_checkin_option?access_token=%s"
|
delOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/del_checkin_option?access_token=%s"
|
||||||
|
// addRecordURL 添加打卡记录
|
||||||
|
addRecordURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/add_checkin_record?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetScheduleListRequest 为打卡人员排班请求
|
// SetScheduleListRequest 为打卡人员排班请求
|
||||||
@@ -140,6 +142,7 @@ type OptionGroupRule struct {
|
|||||||
SyncOutCheckin bool `json:"sync_out_checkin,omitempty"`
|
SyncOutCheckin bool `json:"sync_out_checkin,omitempty"`
|
||||||
BukaRemind OptionGroupBukaRemind `json:"buka_remind,omitempty"`
|
BukaRemind OptionGroupBukaRemind `json:"buka_remind,omitempty"`
|
||||||
BukaRestriction int64 `json:"buka_restriction,omitempty"`
|
BukaRestriction int64 `json:"buka_restriction,omitempty"`
|
||||||
|
CheckinMethodType int64 `json:"checkin_method_type,omitempty"`
|
||||||
SpanDayTime int64 `json:"span_day_time,omitempty"`
|
SpanDayTime int64 `json:"span_day_time,omitempty"`
|
||||||
StandardWorkDuration int64 `json:"standard_work_duration,omitempty"`
|
StandardWorkDuration int64 `json:"standard_work_duration,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -385,3 +388,41 @@ func (r *Client) DelOption(req *DelOptionRequest) error {
|
|||||||
}
|
}
|
||||||
return util.DecodeWithCommonError(response, "DelOption")
|
return util.DecodeWithCommonError(response, "DelOption")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddRecordRequest 添加打卡记录请求
|
||||||
|
type AddRecordRequest struct {
|
||||||
|
Records []Record `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record 打卡记录
|
||||||
|
type Record struct {
|
||||||
|
UserID string `json:"userid"`
|
||||||
|
CheckinTime int64 `json:"checkin_time"`
|
||||||
|
LocationTitle string `json:"location_title"`
|
||||||
|
LocationDetail string `json:"location_detail"`
|
||||||
|
MediaIDS []string `json:"mediaids"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
DeviceType int `json:"device_type"`
|
||||||
|
Lat int64 `json:"lat"`
|
||||||
|
Lng int64 `json:"lng"`
|
||||||
|
DeviceDetail string `json:"device_detail"`
|
||||||
|
WifiName string `json:"wifiname"`
|
||||||
|
WifiMac string `json:"wifimac"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRecord 添加打卡记录
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/99647
|
||||||
|
func (r *Client) AddRecord(req *AddRecordRequest) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(addRecordURL, accessToken), req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "AddRecord")
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,9 +173,15 @@ type (
|
|||||||
|
|
||||||
// OtInfo 加班信息
|
// OtInfo 加班信息
|
||||||
OtInfo struct {
|
OtInfo struct {
|
||||||
OtStatus int64 `json:"ot_status"`
|
OtStatus int64 `json:"ot_status"`
|
||||||
OtDuration int64 `json:"ot_duration"`
|
OtDuration int64 `json:"ot_duration"`
|
||||||
ExceptionDuration []uint64 `json:"exception_duration"`
|
ExceptionDuration []uint64 `json:"exception_duration"`
|
||||||
|
WorkdayOverAsVacation int64 `json:"workday_over_as_vacation"`
|
||||||
|
WorkdayOverAsMoney int64 `json:"workday_over_as_money"`
|
||||||
|
RestdayOverAsVacation int64 `json:"restday_over_as_vacation"`
|
||||||
|
RestdayOverAsMoney int64 `json:"restday_over_as_money"`
|
||||||
|
HolidayOverAsVacation int64 `json:"holiday_over_as_vacation"`
|
||||||
|
HolidayOverAsMoney int64 `json:"holiday_over_as_money"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -237,13 +243,20 @@ type (
|
|||||||
RegularDays int64 `json:"regular_days"`
|
RegularDays int64 `json:"regular_days"`
|
||||||
RegularWorkSec int64 `json:"regular_work_sec"`
|
RegularWorkSec int64 `json:"regular_work_sec"`
|
||||||
StandardWorkSec int64 `json:"standard_work_sec"`
|
StandardWorkSec int64 `json:"standard_work_sec"`
|
||||||
|
RestDays int64 `json:"rest_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OverWorkInfo 加班情况
|
// OverWorkInfo 加班情况
|
||||||
OverWorkInfo struct {
|
OverWorkInfo struct {
|
||||||
WorkdayOverSec int64 `json:"workday_over_sec"`
|
WorkdayOverSec int64 `json:"workday_over_sec"`
|
||||||
HolidayOverSec int64 `json:"holidays_over_sec"`
|
HolidayOverSec int64 `json:"holidays_over_sec"`
|
||||||
RestDayOverSec int64 `json:"restdays_over_sec"`
|
RestDayOverSec int64 `json:"restdays_over_sec"`
|
||||||
|
WorkdaysOverAsVacation int64 `json:"workdays_over_as_vacation"`
|
||||||
|
WorkdaysOverAsMoney int64 `json:"workdays_over_as_money"`
|
||||||
|
RestdaysOverAsVacation int64 `json:"restdays_over_as_vacation"`
|
||||||
|
RestdaysOverAsMoney int64 `json:"restdays_over_as_money"`
|
||||||
|
HolidaysOverAsVacation int64 `json:"holidays_over_as_vacation"`
|
||||||
|
HolidaysOverAsMoney int64 `json:"holidays_over_as_money"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -304,6 +317,10 @@ type CorpOptionGroup struct {
|
|||||||
BukaRestriction int64 `json:"buka_restriction"`
|
BukaRestriction int64 `json:"buka_restriction"`
|
||||||
ScheduleList []ScheduleList `json:"schedulelist"`
|
ScheduleList []ScheduleList `json:"schedulelist"`
|
||||||
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
|
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
|
||||||
|
SpanDayTime int64 `json:"span_day_time"`
|
||||||
|
StandardWorkDuration int64 `json:"standard_work_duration"`
|
||||||
|
OpenSpCheckin bool `json:"open_sp_checkin"`
|
||||||
|
CheckinMethodType int64 `json:"checkin_method_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupCheckinDate 打卡时间,当规则类型为排班时没有意义
|
// GroupCheckinDate 打卡时间,当规则类型为排班时没有意义
|
||||||
@@ -505,6 +522,7 @@ type OptionInfo struct {
|
|||||||
type OptionGroup struct {
|
type OptionGroup struct {
|
||||||
GroupType int64 `json:"grouptype"`
|
GroupType int64 `json:"grouptype"`
|
||||||
GroupID int64 `json:"groupid"`
|
GroupID int64 `json:"groupid"`
|
||||||
|
OpenSpCheckin bool `json:"open_sp_checkin"`
|
||||||
GroupName string `json:"groupname"`
|
GroupName string `json:"groupname"`
|
||||||
CheckinDate []OptionCheckinDate `json:"checkindate"`
|
CheckinDate []OptionCheckinDate `json:"checkindate"`
|
||||||
SpeWorkdays []SpeWorkdays `json:"spe_workdays"`
|
SpeWorkdays []SpeWorkdays `json:"spe_workdays"`
|
||||||
@@ -518,6 +536,10 @@ type OptionGroup struct {
|
|||||||
LocInfos []LocInfos `json:"loc_infos"`
|
LocInfos []LocInfos `json:"loc_infos"`
|
||||||
ScheduleList []ScheduleList `json:"schedulelist"`
|
ScheduleList []ScheduleList `json:"schedulelist"`
|
||||||
BukaRestriction int64 `json:"buka_restriction"`
|
BukaRestriction int64 `json:"buka_restriction"`
|
||||||
|
SpanDayTime int64 `json:"span_day_time"`
|
||||||
|
StandardWorkDuration int64 `json:"standard_work_duration"`
|
||||||
|
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
|
||||||
|
CheckinMethodType int64 `json:"checkin_method_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionCheckinDate 打卡时间配置
|
// OptionCheckinDate 打卡时间配置
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const (
|
|||||||
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
|
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
|
||||||
// customerAcquisitionStatistic 查询链接使用详情
|
// customerAcquisitionStatistic 查询链接使用详情
|
||||||
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
||||||
|
// customerAcquisitionGetChatInfo 获取成员多次收消息详情
|
||||||
|
customerAcquisitionGetChatInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/get_chat_info?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -308,3 +310,42 @@ func (r *Client) CustomerAcquisitionStatistic(req *CustomerAcquisitionStatisticR
|
|||||||
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
|
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// GetChatInfoRequest 获取成员多次收消息详情请求
|
||||||
|
GetChatInfoRequest struct {
|
||||||
|
ChatKey string `json:"chat_key"`
|
||||||
|
}
|
||||||
|
// GetChatInfoResponse 获取成员多次收消息详情响应
|
||||||
|
GetChatInfoResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
UserID string `json:"userid"`
|
||||||
|
ExternalUserID string `json:"external_userid"`
|
||||||
|
ChatInfo ChatInfo `json:"chat_info"`
|
||||||
|
}
|
||||||
|
// ChatInfo 聊天信息
|
||||||
|
ChatInfo struct {
|
||||||
|
RecvMsgCnt int64 `json:"recv_msg_cnt"` // 成员收到的此客户的消息次数
|
||||||
|
LinkID string `json:"link_id"` // 成员添加客户的获客链接id
|
||||||
|
State string `json:"state"` // 成员添加客户的state
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetChatInfo 获取成员多次收消息详情
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/100130
|
||||||
|
func (r *Client) GetChatInfo(req *GetChatInfoRequest) (*GetChatInfoResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(customerAcquisitionGetChatInfoURL, accessToken), req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetChatInfoResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetChatInfo")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user