mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-13 09:12:27 +08:00
@@ -10,11 +10,11 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 获取ticket的url https://developer.work.weixin.qq.com/document/path/90506
|
//获取ticket的url https://developer.work.weixin.qq.com/document/path/90506
|
||||||
const getQyWxTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s"
|
const getQyWxTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s"
|
||||||
const getQyAppTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=%s&type=agent_config"
|
const getQyAppTicketURL = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=%s&type=agent_config"
|
||||||
|
|
||||||
// WorkJsTicket 默认获取js ticket方法
|
//WorkJsTicket 默认获取js ticket方法
|
||||||
type WorkJsTicket struct {
|
type WorkJsTicket struct {
|
||||||
appID string
|
appID string
|
||||||
agentID string
|
agentID string
|
||||||
@@ -24,7 +24,7 @@ type WorkJsTicket struct {
|
|||||||
jsAPITicketLock *sync.Mutex
|
jsAPITicketLock *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkJsTicket new
|
//NewWorkJsTicket new
|
||||||
func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle {
|
func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle {
|
||||||
return &WorkJsTicket{
|
return &WorkJsTicket{
|
||||||
appID: appID,
|
appID: appID,
|
||||||
@@ -35,7 +35,7 @@ func NewWorkJsTicket(appID string, agentID string, cacheKeyPrefix string, cache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTicket 获取企业微信jsapi_ticket
|
//GetTicket 获取企业微信jsapi_ticket
|
||||||
func (js *WorkJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
func (js *WorkJsTicket) GetTicket(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)
|
||||||
@@ -62,7 +62,7 @@ func (js *WorkJsTicket) GetTicket(accessToken string) (ticketStr string, err err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQyWxTicketFromServer 从企业微信服务器中获取ticket
|
//GetQyWxTicketFromServer 从企业微信服务器中获取ticket
|
||||||
func GetQyWxTicketFromServer(accessToken string, isApp bool) (ticket ResTicket, err error) {
|
func GetQyWxTicketFromServer(accessToken string, isApp bool) (ticket ResTicket, err error) {
|
||||||
var response []byte
|
var response []byte
|
||||||
url := fmt.Sprintf(getQyWxTicketURL, accessToken)
|
url := fmt.Sprintf(getQyWxTicketURL, accessToken)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Encryptor struct
|
// Encryptor struct
|
||||||
@@ -108,13 +108,23 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt 解密数据
|
// Decrypt 解密数据
|
||||||
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, iv string) (*PlainData, error) {
|
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, appid string) (*PlainData, error) {
|
||||||
|
ivB := make([]byte, 16)
|
||||||
|
iv := base64.StdEncoding.EncodeToString(ivB)
|
||||||
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
length := string(cipherText[:20])
|
||||||
|
|
||||||
|
cipherTextData := strings.TrimPrefix(string(cipherText), string(cipherText[:20]))
|
||||||
|
cipherTextData = strings.TrimSuffix(cipherTextData, appid)
|
||||||
|
|
||||||
|
if len(length) != len(cipherTextData) {
|
||||||
|
return nil, fmt.Errorf("length not match, %d != %d", length, len(cipherTextData))
|
||||||
|
}
|
||||||
var plainData PlainData
|
var plainData PlainData
|
||||||
err = json.Unmarshal(cipherText, &plainData)
|
err = json.Unmarshal([]byte(cipherTextData), &plainData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -16,3 +17,16 @@ func Signature(params ...string) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CalSignature(params ...string) string {
|
||||||
|
sort.Strings(params)
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for _, value := range params {
|
||||||
|
buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := sha1.New()
|
||||||
|
sha.Write(buffer.Bytes())
|
||||||
|
signature := fmt.Sprintf("%x", sha.Sum(nil))
|
||||||
|
return string(signature)
|
||||||
|
}
|
||||||
|
|||||||
90
work/externalcontact/add_msg_template.go
Normal file
90
work/externalcontact/add_msg_template.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package externalcontact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
addMsgTemplateUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChatTypeSingle ChatType = "single"
|
||||||
|
ChatTypeGroup ChatType = "group"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReqMessage 企业群发参数
|
||||||
|
type ReqMessage struct {
|
||||||
|
ChatType ChatType `json:"chat_type"` //群发任务的类型,默认为single,表示发送给客户,group表示发送给客户群
|
||||||
|
ExternalUserid []string `json:"external_userid"` // 客户的外部联系人id列表,仅在chat_type为single时有效,不可与sender同时为空,最多可传入1万个客户
|
||||||
|
Sender string `json:"sender"` //发送企业群发消息的成员userid,当类型为发送给客户群时必填
|
||||||
|
Text struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
} `json:"text"`
|
||||||
|
Attachments []struct {
|
||||||
|
Msgtype string `json:"msgtype"`
|
||||||
|
Image MsgImage `json:"image"`
|
||||||
|
Link MsgLink `json:"link"`
|
||||||
|
Miniprogram MsgMiniprogram `json:"miniprogram"`
|
||||||
|
Video MsgVideo `json:"video"`
|
||||||
|
File MsgFile `json:"file"`
|
||||||
|
} `json:"attachments"`
|
||||||
|
}
|
||||||
|
type MsgImage struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
PicUrl string `json:"pic_url"`
|
||||||
|
}
|
||||||
|
type MsgLink struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Picurl string `json:"picurl"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
type MsgMiniprogram struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
PicMediaId string `json:"pic_media_id"`
|
||||||
|
Appid string `json:"appid"`
|
||||||
|
Page string `json:"page"`
|
||||||
|
}
|
||||||
|
type MsgVideo struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
}
|
||||||
|
type MsgFile struct {
|
||||||
|
MediaId string `json:"media_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resTemplateSend struct {
|
||||||
|
util.CommonError
|
||||||
|
FailList string `json:"fail_list"`
|
||||||
|
MsgID int64 `json:"msgid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 发送应用消息
|
||||||
|
func (r *Client) Send(msg *ReqMessage) (msgID int64, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = r.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", addMsgTemplateUrl, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resTemplateSend
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgID = result.MsgID
|
||||||
|
return
|
||||||
|
}
|
||||||
162
work/externalcontact/user.go
Normal file
162
work/externalcontact/user.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package externalcontact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
listUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list"
|
||||||
|
getUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get"
|
||||||
|
getByUserBatchUrl = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReqGetByUser struct {
|
||||||
|
UseridList []string `json:"userid_list"`
|
||||||
|
Cursor string `json:"cursor"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
type OneUser struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalContact ExternalContact `json:"external_contact"`
|
||||||
|
FollowUser []FollowInfo `json:"follow_user"` //注意,仅获取单个客户详情的时候这里返回的是跟进人列表
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
type resUserList struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalContactList []UserInfo `json:"external_contact_list"`
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
type resUserids struct {
|
||||||
|
util.CommonError
|
||||||
|
ExternalUserid []string `json:"external_userid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserInfo struct {
|
||||||
|
ExternalContact ExternalContact `json:"external_contact"`
|
||||||
|
FollowInfo FollowInfo `json:"follow_info"` //企业成员客户跟进人信息,可以参考获取客户详情,但标签信息只会返回企业标签和规则组标签的tag_id,个人标签将不再返回
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUseridList 获取我的客户列表
|
||||||
|
func (tpl *Client) GetUseridList(myUserid string) (externalUserid []string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s&userid=%s", listUrl, accessToken, myUserid)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resUserids
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
externalUserid = result.ExternalUserid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUseridList 获取我的全部客户列表及详情
|
||||||
|
func (tpl *Client) GetQyUserInfoList(qyUserid []string) ([]UserInfo, error) {
|
||||||
|
var userInfoList []UserInfo
|
||||||
|
var req ReqGetByUser
|
||||||
|
req.UseridList = qyUserid
|
||||||
|
req.Limit = 100
|
||||||
|
for {
|
||||||
|
userInfoPage, resCursor, err := tpl.GetUserInfoListByUserIds(req)
|
||||||
|
if err != nil {
|
||||||
|
return userInfoList, err
|
||||||
|
}
|
||||||
|
userInfoList = append(userInfoList, userInfoPage...)
|
||||||
|
if resCursor != "" {
|
||||||
|
req.Cursor = resCursor
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userInfoList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoAndAllFollow 获取客户详情以及全部跟进人
|
||||||
|
func (tpl *Client) GetUserInfoAndAllFollow(userid string) (OneUser, error) {
|
||||||
|
var result, res OneUser
|
||||||
|
var err error
|
||||||
|
var cursor string
|
||||||
|
for {
|
||||||
|
res, err = tpl.GetUserInfo(userid, cursor)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result.FollowUser = append(result.FollowUser, res.FollowUser...)
|
||||||
|
result.ExternalContact = res.ExternalContact
|
||||||
|
if res.NextCursor != "" {
|
||||||
|
cursor = res.NextCursor
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取客户详情
|
||||||
|
func (tpl *Client) GetUserInfo(externalUserid string, cursor ...string) (result OneUser, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var page = ""
|
||||||
|
if len(cursor) > 0 {
|
||||||
|
page = cursor[0]
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s&external_userid=%s&cursor=%s", getUrl, accessToken, externalUserid, page)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoListByUserId 批量获取客户详情
|
||||||
|
func (tpl *Client) GetUserInfoListByUserIds(req ReqGetByUser) (userList []UserInfo, nextCursor string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", getByUserBatchUrl, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resUserList
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userList = result.ExternalContactList
|
||||||
|
nextCursor = result.NextCursor
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ type Config struct {
|
|||||||
Signature string `json:"signature"`
|
Signature string `json:"signature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJs init
|
//NewJs init
|
||||||
func NewJs(context *context.Context) *Js {
|
func NewJs(context *context.Context) *Js {
|
||||||
js := new(Js)
|
js := new(Js)
|
||||||
js.Context = context
|
js.Context = context
|
||||||
@@ -32,13 +32,13 @@ func NewJs(context *context.Context) *Js {
|
|||||||
return js
|
return js
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetJsTicketHandle 自定义js ticket取值方式
|
//SetJsTicketHandle 自定义js ticket取值方式
|
||||||
func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
||||||
js.JsTicketHandle = ticketHandle
|
js.JsTicketHandle = ticketHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
config = new(Config)
|
config = new(Config)
|
||||||
var accessToken string
|
var accessToken string
|
||||||
@@ -65,8 +65,8 @@ func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAgentConfig 获取jssdk需要的配置参数
|
//GetAgentConfig 获取jssdk需要的配置参数
|
||||||
// uri 为当前网页地址
|
//uri 为当前网页地址
|
||||||
func (js *Js) GetAgentConfig(uri string) (config *Config, err error) {
|
func (js *Js) GetAgentConfig(uri string) (config *Config, err error) {
|
||||||
config = new(Config)
|
config = new(Config)
|
||||||
var accessToken string
|
var accessToken string
|
||||||
|
|||||||
0
work/message/README.md
Normal file
0
work/message/README.md
Normal file
1
work/message/group.go
Normal file
1
work/message/group.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package message
|
||||||
16
work/message/image.go
Normal file
16
work/message/image.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Image 图片消息
|
||||||
|
type Image struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Image struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
} `xml:"Image" json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewImage 回复图片消息
|
||||||
|
func NewImage(mediaID string) *Image {
|
||||||
|
image := new(Image)
|
||||||
|
image.Image.MediaID = mediaID
|
||||||
|
return image
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ type (
|
|||||||
// 消息类型,此时固定为:text
|
// 消息类型,此时固定为:text
|
||||||
MsgType string `json:"msgtype"`
|
MsgType string `json:"msgtype"`
|
||||||
// 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
// 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值
|
||||||
AgentID string `json:"agentid"`
|
AgentID int `json:"agentid"`
|
||||||
// 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
// 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0
|
||||||
Safe int `json:"safe"`
|
Safe int `json:"safe"`
|
||||||
// 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
// 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。
|
||||||
|
|||||||
41
work/message/news.go
Normal file
41
work/message/news.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//News 图文消息
|
||||||
|
type News struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
ArticleCount int `xml:"ArticleCount" json:"-"`
|
||||||
|
Articles []*Article `xml:"Articles>item,omitempty" json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewNews 初始化图文消息
|
||||||
|
func NewNews(articles []*Article) *News {
|
||||||
|
news := new(News)
|
||||||
|
news.ArticleCount = len(articles)
|
||||||
|
news.Articles = articles
|
||||||
|
return news
|
||||||
|
}
|
||||||
|
|
||||||
|
//Article 单篇文章
|
||||||
|
type Article struct {
|
||||||
|
Title string `xml:"Title,omitempty" json:"title"`
|
||||||
|
Description string `xml:"Description,omitempty" json:"description"`
|
||||||
|
PicURL string `xml:"PicUrl,omitempty" json:"picurl"`
|
||||||
|
URL string `xml:"Url,omitempty" json:"url"`
|
||||||
|
Appid string `xml:"-" json:"appid"` //仅在发送应用消息时需要
|
||||||
|
Pagepath string `xml:"-" json:"pagepath"` //仅在发送应用消息时需要
|
||||||
|
}
|
||||||
|
|
||||||
|
//MpNews 图文消息
|
||||||
|
type MpNews struct {
|
||||||
|
Articles []*MpNewsArticle `xml:"-" json:"articles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//MpNewsArticle mpnews类型的图文消息,跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信
|
||||||
|
type MpNewsArticle struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
ThumbMediaId string `json:"thumb_media_id"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
ContentSourceUrl string `json:"content_source_url"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
}
|
||||||
@@ -2,13 +2,13 @@ package message
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// ErrInvalidReply 无效的回复
|
//ErrInvalidReply 无效的回复
|
||||||
var ErrInvalidReply = errors.New("无效的回复消息")
|
var ErrInvalidReply = errors.New("无效的回复消息")
|
||||||
|
|
||||||
// ErrUnsupportReply 不支持的回复类型
|
//ErrUnsupportReply 不支持的回复类型
|
||||||
var ErrUnsupportReply = errors.New("无需回复消息")
|
var ErrUnsupportReply = errors.New("无需回复消息")
|
||||||
|
|
||||||
// Reply 消息回复
|
//Reply 消息回复
|
||||||
type Reply struct {
|
type Reply struct {
|
||||||
MsgType MsgType
|
MsgType MsgType
|
||||||
MsgData interface{}
|
MsgData interface{}
|
||||||
|
|||||||
14
work/message/text.go
Normal file
14
work/message/text.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Text 文本消息
|
||||||
|
type Text struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Content CDATA `json:"content" xml:"Content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewText 初始化文本消息
|
||||||
|
func NewText(content string) *Text {
|
||||||
|
text := new(Text)
|
||||||
|
text.Content = CDATA(content)
|
||||||
|
return text
|
||||||
|
}
|
||||||
20
work/message/video.go
Normal file
20
work/message/video.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Video 视频消息
|
||||||
|
type Video struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Video struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
Title string `xml:"Title,omitempty" json:"title"`
|
||||||
|
Description string `xml:"Description,omitempty" json:"description"`
|
||||||
|
} `xml:"Video" json:"video"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewVideo 回复图片消息
|
||||||
|
func NewVideo(mediaID, title, description string) *Video {
|
||||||
|
video := new(Video)
|
||||||
|
video.Video.MediaID = mediaID
|
||||||
|
video.Video.Title = title
|
||||||
|
video.Video.Description = description
|
||||||
|
return video
|
||||||
|
}
|
||||||
16
work/message/voice.go
Normal file
16
work/message/voice.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
//Voice 语音消息
|
||||||
|
type Voice struct {
|
||||||
|
CommonToken `json:"-"`
|
||||||
|
Voice struct {
|
||||||
|
MediaID string `xml:"MediaId" json:"media_id"`
|
||||||
|
} `xml:"Voice" json:"voice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewVoice 回复语音消息
|
||||||
|
func NewVoice(mediaID string) *Voice {
|
||||||
|
voice := new(Voice)
|
||||||
|
voice.Voice.MediaID = mediaID
|
||||||
|
return voice
|
||||||
|
}
|
||||||
64
work/oauth/user.go
Normal file
64
work/oauth/user.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
code2SessionURL = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code"
|
||||||
|
launchCode = "https://qyapi.weixin.qq.com/cgi-bin/get_launch_code?access_token=%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ctr *Oauth) Code2Session(code string) (result ResUserInfo, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = ctr.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(
|
||||||
|
fmt.Sprintf(code2SessionURL, accessToken, code),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(response, &result)
|
||||||
|
if result.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type RespLaunchCode struct {
|
||||||
|
util.CommonError
|
||||||
|
LaunchCode string `json:"launch_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLaunchCode 用于打开个人聊天窗口schema
|
||||||
|
func (ctr *Oauth) GetLaunchCode(userID, other string) (userInfo *RespLaunchCode, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = ctr.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(launchCode, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, map[string]interface{}{"operator_userid": userID, "single_chat": map[string]string{"userid": other}})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userInfo = new(RespLaunchCode)
|
||||||
|
err = json.Unmarshal(response, userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userInfo.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ const (
|
|||||||
SDKUnknownError Error = "未知错误"
|
SDKUnknownError Error = "未知错误"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error 输出错误信息
|
//Error 输出错误信息
|
||||||
func (r Error) Error() string {
|
func (r Error) Error() string {
|
||||||
return reflect.ValueOf(r).String()
|
return reflect.ValueOf(r).String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server struct
|
//Server struct
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*context.Context
|
*context.Context
|
||||||
Writer http.ResponseWriter
|
Writer http.ResponseWriter
|
||||||
@@ -36,7 +36,7 @@ type Server struct {
|
|||||||
timestamp int64
|
timestamp int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer 实例化
|
//NewServer init
|
||||||
func NewServer(context *context.Context) *Server {
|
func NewServer(context *context.Context) *Server {
|
||||||
srv := new(Server)
|
srv := new(Server)
|
||||||
srv.Context = context
|
srv.Context = context
|
||||||
@@ -64,7 +64,7 @@ func (srv *Server) SkipValidate(skip bool) {
|
|||||||
srv.skipValidate = skip
|
srv.skipValidate = skip
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve 处理企业微信的请求消息
|
//Serve 处理企业微信的请求消息
|
||||||
func (srv *Server) Serve() error {
|
func (srv *Server) Serve() error {
|
||||||
response, err := srv.handleRequest()
|
response, err := srv.handleRequest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -76,7 +76,7 @@ func (srv *Server) Serve() error {
|
|||||||
return srv.buildResponse(response)
|
return srv.buildResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate 校验请求是否合法
|
//Validate 校验请求是否合法
|
||||||
func (srv *Server) Validate() bool {
|
func (srv *Server) Validate() bool {
|
||||||
if srv.skipValidate {
|
if srv.skipValidate {
|
||||||
return true
|
return true
|
||||||
@@ -88,7 +88,7 @@ func (srv *Server) Validate() bool {
|
|||||||
return signature == util.Signature(srv.Token, timestamp, nonce)
|
return signature == util.Signature(srv.Token, timestamp, nonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleRequest 处理企业微信的请求
|
//HandleRequest 处理企业微信的请求
|
||||||
func (srv *Server) handleRequest() (reply *message.Reply, err error) {
|
func (srv *Server) handleRequest() (reply *message.Reply, err error) {
|
||||||
|
|
||||||
var msg interface{}
|
var msg interface{}
|
||||||
@@ -105,7 +105,7 @@ func (srv *Server) handleRequest() (reply *message.Reply, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMessage 解析企业微信返回的消息
|
//getMessage 解析企业微信返回的消息
|
||||||
func (srv *Server) getMessage() (interface{}, error) {
|
func (srv *Server) getMessage() (interface{}, error) {
|
||||||
var rawXMLMsgBytes []byte
|
var rawXMLMsgBytes []byte
|
||||||
var err error
|
var err error
|
||||||
@@ -145,7 +145,7 @@ func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixM
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMessageHandler 设置用户自定义的回调方法
|
//SetMessageHandler 设置用户自定义的回调方法
|
||||||
func (srv *Server) SetMessageHandler(handler func(*message.MixMessage) *message.Reply) {
|
func (srv *Server) SetMessageHandler(handler func(*message.MixMessage) *message.Reply) {
|
||||||
srv.messageHandler = handler
|
srv.messageHandler = handler
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send 将自定义的消息发送
|
//Send 将自定义的消息发送
|
||||||
func (srv *Server) Send() (err error) {
|
func (srv *Server) Send() (err error) {
|
||||||
replyMsg := srv.ResponseMsg
|
replyMsg := srv.ResponseMsg
|
||||||
log.Debugf("response msg =%+v", replyMsg)
|
log.Debugf("response msg =%+v", replyMsg)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func writeContextType(w http.ResponseWriter, value []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render render from bytes
|
//Render render from bytes
|
||||||
func (srv *Server) Render(bytes []byte) {
|
func (srv *Server) Render(bytes []byte) {
|
||||||
//debug
|
//debug
|
||||||
//fmt.Println("response msg = ", string(bytes))
|
//fmt.Println("response msg = ", string(bytes))
|
||||||
@@ -26,13 +26,13 @@ func (srv *Server) Render(bytes []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String render from string
|
//String render from string
|
||||||
func (srv *Server) String(str string) {
|
func (srv *Server) String(str string) {
|
||||||
writeContextType(srv.Writer, plainContentType)
|
writeContextType(srv.Writer, plainContentType)
|
||||||
srv.Render([]byte(str))
|
srv.Render([]byte(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
// XML render to xml
|
//XML render to xml
|
||||||
func (srv *Server) XML(obj interface{}) {
|
func (srv *Server) XML(obj interface{}) {
|
||||||
writeContextType(srv.Writer, xmlContentType)
|
writeContextType(srv.Writer, xmlContentType)
|
||||||
bytes, err := xml.Marshal(obj)
|
bytes, err := xml.Marshal(obj)
|
||||||
|
|||||||
0
work/tools/calendar.go
Normal file
0
work/tools/calendar.go
Normal file
194
work/user/user.go
Normal file
194
work/user/user.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s"
|
||||||
|
updateURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%s&fetch_child=1"
|
||||||
|
userListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User 用户管理
|
||||||
|
type User struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUser 实例化
|
||||||
|
func NewUser(context *context.Context) *User {
|
||||||
|
user := new(User)
|
||||||
|
user.Context = context
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info 用户基本信息
|
||||||
|
type Info struct {
|
||||||
|
util.CommonError
|
||||||
|
Userid string `json:"userid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Department []int `json:"department"`
|
||||||
|
Order []int `json:"order"`
|
||||||
|
Position string `json:"position"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
Gender string `json:"gender"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
ThumbAvatar string `json:"thumb_avatar"`
|
||||||
|
Telephone string `json:"telephone"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
OpenUserid string `json:"open_userid"`
|
||||||
|
MainDepartment int `json:"main_department"`
|
||||||
|
Extattr struct {
|
||||||
|
Attrs []struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Text struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"text,omitempty"`
|
||||||
|
Web struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"web,omitempty"`
|
||||||
|
} `json:"attrs"`
|
||||||
|
} `json:"extattr"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
QrCode string `json:"qr_code"`
|
||||||
|
ExternalPosition string `json:"external_position"`
|
||||||
|
ExternalProfile struct {
|
||||||
|
ExternalCorpName string `json:"external_corp_name"`
|
||||||
|
WechatChannels struct {
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"wechat_channels"`
|
||||||
|
ExternalAttr []struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Text struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"text,omitempty"`
|
||||||
|
Web struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"web,omitempty"`
|
||||||
|
Miniprogram struct {
|
||||||
|
Appid string `json:"appid"`
|
||||||
|
Pagepath string `json:"pagepath"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
} `json:"miniprogram,omitempty"`
|
||||||
|
} `json:"external_attr"`
|
||||||
|
} `json:"external_profile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenidList 用户列表
|
||||||
|
type OpenidList struct {
|
||||||
|
util.CommonError
|
||||||
|
|
||||||
|
Total int `json:"total"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Data struct {
|
||||||
|
OpenIDs []string `json:"openid"`
|
||||||
|
} `json:"data"`
|
||||||
|
NextOpenID string `json:"next_openid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户基本信息
|
||||||
|
func (user *User) GetUserInfo(userID string) (userInfo *Info, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(userInfoURL, accessToken, userID)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.HTTPGet(uri)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userInfo = new(Info)
|
||||||
|
err = json.Unmarshal(response, userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userInfo.ErrCode != 0 {
|
||||||
|
err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新员工资料
|
||||||
|
func (user *User) Update(userID, external_position string) (err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(updateURL, accessToken, userID)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, map[string]string{"userid": userID, "external_position": external_position})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.DecodeWithCommonError(response, "updateURL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserOpenIDs 返回用户列表
|
||||||
|
func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) {
|
||||||
|
accessToken, err := user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri, _ := url.Parse(userListURL)
|
||||||
|
q := uri.Query()
|
||||||
|
q.Set("access_token", accessToken)
|
||||||
|
if len(nextOpenid) > 0 && nextOpenid[0] != "" {
|
||||||
|
q.Set("next_openid", nextOpenid[0])
|
||||||
|
}
|
||||||
|
uri.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
response, err := util.HTTPGet(uri.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userlist := OpenidList{}
|
||||||
|
|
||||||
|
err = util.DecodeWithError(response, &userlist, "ListUserOpenIDs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userlist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAllUserOpenIDs 返回所有用户OpenID列表
|
||||||
|
func (user *User) ListAllUserOpenIDs() ([]string, error) {
|
||||||
|
nextOpenid := ""
|
||||||
|
openids := make([]string, 0)
|
||||||
|
count := 0
|
||||||
|
for {
|
||||||
|
ul, err := user.ListUserOpenIDs(nextOpenid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
openids = append(openids, ul.Data.OpenIDs...)
|
||||||
|
count += ul.Count
|
||||||
|
if ul.Total > count {
|
||||||
|
nextOpenid = ul.NextOpenID
|
||||||
|
} else {
|
||||||
|
return openids, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user