mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-09 15:12:26 +08:00
完善企业微信API
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
238
work/message/message.go
Normal file
238
work/message/message.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/officialaccount/device"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MsgType 基本消息类型
|
||||||
|
type MsgType string
|
||||||
|
|
||||||
|
// EventType 事件类型
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
// InfoType 第三方平台授权事件类型
|
||||||
|
type InfoType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
//MsgTypeText 表示文本消息
|
||||||
|
MsgTypeText MsgType = "text"
|
||||||
|
//MsgTypeImage 表示图片消息
|
||||||
|
MsgTypeImage = "image"
|
||||||
|
//MsgTypeVoice 表示语音消息
|
||||||
|
MsgTypeVoice = "voice"
|
||||||
|
//MsgTypeVideo 表示视频消息
|
||||||
|
MsgTypeVideo = "video"
|
||||||
|
//MsgTypeMiniprogrampage 表示小程序卡片消息
|
||||||
|
MsgTypeMiniprogrampage = "miniprogrampage"
|
||||||
|
//MsgTypeShortVideo 表示短视频消息[限接收]
|
||||||
|
MsgTypeShortVideo = "shortvideo"
|
||||||
|
//MsgTypeLocation 表示坐标消息[限接收]
|
||||||
|
MsgTypeLocation = "location"
|
||||||
|
//MsgTypeLink 表示链接消息[限接收]
|
||||||
|
MsgTypeLink = "link"
|
||||||
|
//MsgTypeMusic 表示音乐消息[限回复]
|
||||||
|
MsgTypeMusic = "music"
|
||||||
|
//MsgTypeNews 表示图文消息[限回复]
|
||||||
|
MsgTypeNews = "news"
|
||||||
|
//MsgTypeTransfer 表示消息消息转发到客服
|
||||||
|
MsgTypeTransfer = "transfer_customer_service"
|
||||||
|
//MsgTypeEvent 表示事件推送消息
|
||||||
|
MsgTypeEvent = "event"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//EventSubscribe 订阅
|
||||||
|
EventSubscribe EventType = "subscribe"
|
||||||
|
//EventUnsubscribe 取消订阅
|
||||||
|
EventUnsubscribe = "unsubscribe"
|
||||||
|
//EventScan 用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者
|
||||||
|
EventScan = "SCAN"
|
||||||
|
//EventLocation 上报地理位置事件
|
||||||
|
EventLocation = "LOCATION"
|
||||||
|
//EventClick 点击菜单拉取消息时的事件推送
|
||||||
|
EventClick = "CLICK"
|
||||||
|
//EventView 点击菜单跳转链接时的事件推送
|
||||||
|
EventView = "VIEW"
|
||||||
|
//EventScancodePush 扫码推事件的事件推送
|
||||||
|
EventScancodePush = "scancode_push"
|
||||||
|
//EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送
|
||||||
|
EventScancodeWaitmsg = "scancode_waitmsg"
|
||||||
|
//EventPicSysphoto 弹出系统拍照发图的事件推送
|
||||||
|
EventPicSysphoto = "pic_sysphoto"
|
||||||
|
//EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送
|
||||||
|
EventPicPhotoOrAlbum = "pic_photo_or_album"
|
||||||
|
//EventPicWeixin 弹出微信相册发图器的事件推送
|
||||||
|
EventPicWeixin = "pic_weixin"
|
||||||
|
//EventLocationSelect 弹出地理位置选择器的事件推送
|
||||||
|
EventLocationSelect = "location_select"
|
||||||
|
//EventTemplateSendJobFinish 发送模板消息推送通知
|
||||||
|
EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH"
|
||||||
|
//EventMassSendJobFinish 群发消息推送通知
|
||||||
|
EventMassSendJobFinish = "MASSSENDJOBFINISH"
|
||||||
|
//EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件
|
||||||
|
EventWxaMediaCheck = "wxa_media_check"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//微信开放平台需要用到
|
||||||
|
|
||||||
|
// InfoTypeVerifyTicket 返回ticket
|
||||||
|
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
|
||||||
|
// InfoTypeAuthorized 授权
|
||||||
|
InfoTypeAuthorized = "authorized"
|
||||||
|
// InfoTypeUnauthorized 取消授权
|
||||||
|
InfoTypeUnauthorized = "unauthorized"
|
||||||
|
// InfoTypeUpdateAuthorized 更新授权
|
||||||
|
InfoTypeUpdateAuthorized = "updateauthorized"
|
||||||
|
)
|
||||||
|
|
||||||
|
//MixMessage 存放所有微信发送过来的消息和事件
|
||||||
|
type MixMessage struct {
|
||||||
|
CommonToken
|
||||||
|
|
||||||
|
//基本消息
|
||||||
|
MsgID int64 `xml:"MsgId"` //其他消息推送过来是MsgId
|
||||||
|
TemplateMsgID int64 `xml:"MsgID"` //模板消息推送成功的消息是MsgID
|
||||||
|
Content string `xml:"Content"`
|
||||||
|
Recognition string `xml:"Recognition"`
|
||||||
|
PicURL string `xml:"PicUrl"`
|
||||||
|
MediaID string `xml:"MediaId"`
|
||||||
|
Format string `xml:"Format"`
|
||||||
|
ThumbMediaID string `xml:"ThumbMediaId"`
|
||||||
|
LocationX float64 `xml:"Location_X"`
|
||||||
|
LocationY float64 `xml:"Location_Y"`
|
||||||
|
Scale float64 `xml:"Scale"`
|
||||||
|
Label string `xml:"Label"`
|
||||||
|
Title string `xml:"Title"`
|
||||||
|
Description string `xml:"Description"`
|
||||||
|
URL string `xml:"Url"`
|
||||||
|
|
||||||
|
//事件相关
|
||||||
|
Event EventType `xml:"Event"`
|
||||||
|
EventKey string `xml:"EventKey"`
|
||||||
|
Ticket string `xml:"Ticket"`
|
||||||
|
Latitude string `xml:"Latitude"`
|
||||||
|
Longitude string `xml:"Longitude"`
|
||||||
|
Precision string `xml:"Precision"`
|
||||||
|
MenuID string `xml:"MenuId"`
|
||||||
|
Status string `xml:"Status"`
|
||||||
|
SessionFrom string `xml:"SessionFrom"`
|
||||||
|
TotalCount int64 `xml:"TotalCount"`
|
||||||
|
FilterCount int64 `xml:"FilterCount"`
|
||||||
|
SentCount int64 `xml:"SentCount"`
|
||||||
|
ErrorCount int64 `xml:"ErrorCount"`
|
||||||
|
|
||||||
|
ScanCodeInfo struct {
|
||||||
|
ScanType string `xml:"ScanType"`
|
||||||
|
ScanResult string `xml:"ScanResult"`
|
||||||
|
} `xml:"ScanCodeInfo"`
|
||||||
|
|
||||||
|
SendPicsInfo struct {
|
||||||
|
Count int32 `xml:"Count"`
|
||||||
|
PicList []EventPic `xml:"PicList>item"`
|
||||||
|
} `xml:"SendPicsInfo"`
|
||||||
|
|
||||||
|
SendLocationInfo struct {
|
||||||
|
LocationX float64 `xml:"Location_X"`
|
||||||
|
LocationY float64 `xml:"Location_Y"`
|
||||||
|
Scale float64 `xml:"Scale"`
|
||||||
|
Label string `xml:"Label"`
|
||||||
|
Poiname string `xml:"Poiname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三方平台相关
|
||||||
|
InfoType InfoType `xml:"InfoType"`
|
||||||
|
AppID string `xml:"AppId"`
|
||||||
|
ComponentVerifyTicket string `xml:"ComponentVerifyTicket"`
|
||||||
|
AuthorizerAppid string `xml:"AuthorizerAppid"`
|
||||||
|
AuthorizationCode string `xml:"AuthorizationCode"`
|
||||||
|
AuthorizationCodeExpiredTime int64 `xml:"AuthorizationCodeExpiredTime"`
|
||||||
|
PreAuthCode string `xml:"PreAuthCode"`
|
||||||
|
|
||||||
|
// 卡券相关
|
||||||
|
CardID string `xml:"CardId"`
|
||||||
|
RefuseReason string `xml:"RefuseReason"`
|
||||||
|
IsGiveByFriend int32 `xml:"IsGiveByFriend"`
|
||||||
|
FriendUserName string `xml:"FriendUserName"`
|
||||||
|
UserCardCode string `xml:"UserCardCode"`
|
||||||
|
OldUserCardCode string `xml:"OldUserCardCode"`
|
||||||
|
OuterStr string `xml:"OuterStr"`
|
||||||
|
IsRestoreMemberCard int32 `xml:"IsRestoreMemberCard"`
|
||||||
|
UnionID string `xml:"UnionId"`
|
||||||
|
|
||||||
|
// 内容审核相关
|
||||||
|
IsRisky bool `xml:"isrisky"`
|
||||||
|
ExtraInfoJSON string `xml:"extra_info_json"`
|
||||||
|
TraceID string `xml:"trace_id"`
|
||||||
|
StatusCode int `xml:"status_code"`
|
||||||
|
|
||||||
|
//设备相关
|
||||||
|
device.MsgDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
//EventPic 发图事件推送
|
||||||
|
type EventPic struct {
|
||||||
|
PicMd5Sum string `xml:"PicMd5Sum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//EncryptedXMLMsg 安全模式下的消息体
|
||||||
|
type EncryptedXMLMsg struct {
|
||||||
|
XMLName struct{} `xml:"xml" json:"-"`
|
||||||
|
ToUserName string `xml:"ToUserName" json:"ToUserName"`
|
||||||
|
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//ResponseEncryptedXMLMsg 需要返回的消息体
|
||||||
|
type ResponseEncryptedXMLMsg struct {
|
||||||
|
XMLName struct{} `xml:"xml" json:"-"`
|
||||||
|
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
|
||||||
|
MsgSignature string `xml:"MsgSignature" json:"MsgSignature"`
|
||||||
|
Timestamp int64 `xml:"TimeStamp" json:"TimeStamp"`
|
||||||
|
Nonce string `xml:"Nonce" json:"Nonce"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略
|
||||||
|
type CDATA string
|
||||||
|
|
||||||
|
// MarshalXML 实现自己的序列化方法
|
||||||
|
func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return e.EncodeElement(struct {
|
||||||
|
string `xml:",cdata"`
|
||||||
|
}{string(c)}, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonToken 消息中通用的结构
|
||||||
|
type CommonToken struct {
|
||||||
|
XMLName xml.Name `xml:"xml"`
|
||||||
|
ToUserName CDATA `xml:"ToUserName"`
|
||||||
|
FromUserName CDATA `xml:"FromUserName"`
|
||||||
|
CreateTime int64 `xml:"CreateTime"`
|
||||||
|
MsgType MsgType `xml:"MsgType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetToUserName set ToUserName
|
||||||
|
func (msg *CommonToken) SetToUserName(toUserName CDATA) {
|
||||||
|
msg.ToUserName = toUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetFromUserName set FromUserName
|
||||||
|
func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
|
||||||
|
msg.FromUserName = fromUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetCreateTime set createTime
|
||||||
|
func (msg *CommonToken) SetCreateTime(createTime int64) {
|
||||||
|
msg.CreateTime = createTime
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetMsgType set MsgType
|
||||||
|
func (msg *CommonToken) SetMsgType(msgType MsgType) {
|
||||||
|
msg.MsgType = msgType
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetOpenID get the FromUserName value
|
||||||
|
func (msg *CommonToken) GetOpenID() string {
|
||||||
|
return string(msg.FromUserName)
|
||||||
|
}
|
||||||
15
work/message/reply.go
Normal file
15
work/message/reply.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
//ErrInvalidReply 无效的回复
|
||||||
|
var ErrInvalidReply = errors.New("无效的回复消息")
|
||||||
|
|
||||||
|
//ErrUnsupportReply 不支持的回复类型
|
||||||
|
var ErrUnsupportReply = errors.New("不支持的回复消息")
|
||||||
|
|
||||||
|
//Reply 消息回复
|
||||||
|
type Reply struct {
|
||||||
|
MsgType MsgType
|
||||||
|
MsgData interface{}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ var (
|
|||||||
oauthUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s"
|
oauthUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s"
|
||||||
//oauthQrContentTargetURL 构造独立窗口登录二维码
|
//oauthQrContentTargetURL 构造独立窗口登录二维码
|
||||||
oauthQrContentTargetURL = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s"
|
oauthQrContentTargetURL = "https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s"
|
||||||
|
//code2Session 获取用户信息地址
|
||||||
|
code2SessionURL = "https://qyapi.weixin.qq.com/cgi-bin/miniprogram/jscode2session?access_token=%s&js_code=%s&grant_type=authorization_code"
|
||||||
)
|
)
|
||||||
|
|
||||||
//NewOauth new init oauth
|
//NewOauth new init oauth
|
||||||
@@ -85,3 +87,24 @@ func (ctr *Oauth) UserFromCode(code string) (result ResUserInfo, err error) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
66
work/server/error.go
Normal file
66
work/server/error.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error 错误
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SDKValidateSignatureError Error = "签名验证错误" //-40001
|
||||||
|
SDKParseJsonError Error = "xml/json解析失败" //-40002
|
||||||
|
SDKComputeSignatureError Error = "sha加密生成签名失败" //-40003
|
||||||
|
SDKIllegalAesKey Error = "AESKey 非法" //-40004
|
||||||
|
SDKValidateCorpidError Error = "ReceiveId 校验错误" //-40005
|
||||||
|
SDKEncryptAESError Error = "AES 加密失败" //-40006
|
||||||
|
SDKDecryptAESError Error = "AES 解密失败" //-40007
|
||||||
|
SDKIllegalBuffer Error = "解密后得到的buffer非法" //-40008
|
||||||
|
SDKEncodeBase64Error Error = "base64加密失败" //-40009
|
||||||
|
SDKDecodeBase64Error Error = "base64解密失败" //-40010
|
||||||
|
SDKGenJsonError Error = "生成xml/json失败" //-40011
|
||||||
|
SDKIllegalProtocolType Error = "协议类型非法" //-40012
|
||||||
|
SDKUnknownError Error = "未知错误"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Error 输出错误信息
|
||||||
|
func (r Error) Error() string {
|
||||||
|
return reflect.ValueOf(r).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSDKErr 初始化SDK实例错误信息
|
||||||
|
func NewSDKErr(code int64, msgList ...string) Error {
|
||||||
|
switch code {
|
||||||
|
case 40001:
|
||||||
|
return SDKValidateSignatureError
|
||||||
|
case 40002:
|
||||||
|
return SDKParseJsonError
|
||||||
|
case 40003:
|
||||||
|
return SDKComputeSignatureError
|
||||||
|
case 40004:
|
||||||
|
return SDKIllegalAesKey
|
||||||
|
case 40005:
|
||||||
|
return SDKValidateCorpidError
|
||||||
|
case 40006:
|
||||||
|
return SDKEncryptAESError
|
||||||
|
case 40007:
|
||||||
|
return SDKDecryptAESError
|
||||||
|
case 40008:
|
||||||
|
return SDKIllegalBuffer
|
||||||
|
case 40009:
|
||||||
|
return SDKEncodeBase64Error
|
||||||
|
case 40010:
|
||||||
|
return SDKDecodeBase64Error
|
||||||
|
case 40011:
|
||||||
|
return SDKGenJsonError
|
||||||
|
case 40012:
|
||||||
|
return SDKIllegalProtocolType
|
||||||
|
default:
|
||||||
|
//返回未知的自定义错误
|
||||||
|
if len(msgList) > 0 {
|
||||||
|
return Error(strings.Join(msgList, ","))
|
||||||
|
}
|
||||||
|
return SDKUnknownError
|
||||||
|
}
|
||||||
|
}
|
||||||
269
work/server/server.go
Normal file
269
work/server/server.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
"github.com/silenceper/wechat/v2/work/message"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Server struct
|
||||||
|
type Server struct {
|
||||||
|
*context.Context
|
||||||
|
Writer http.ResponseWriter
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
|
skipValidate bool
|
||||||
|
|
||||||
|
openID string
|
||||||
|
|
||||||
|
messageHandler func(*message.MixMessage) *message.Reply
|
||||||
|
|
||||||
|
RequestRawXMLMsg []byte
|
||||||
|
RequestMsg *message.MixMessage
|
||||||
|
ResponseRawXMLMsg []byte
|
||||||
|
ResponseMsg interface{}
|
||||||
|
|
||||||
|
isSafeMode bool
|
||||||
|
random []byte
|
||||||
|
nonce string
|
||||||
|
timestamp int64
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewServer init
|
||||||
|
func NewServer(context *context.Context) *Server {
|
||||||
|
srv := new(Server)
|
||||||
|
srv.Context = context
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) VerifyURL() (string, error) {
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
signature := srv.Query("msg_signature")
|
||||||
|
echoStr := srv.Query("echostr")
|
||||||
|
log.Info("Signature----", util.CalSignature(srv.Token, timestamp, nonce))
|
||||||
|
log.Info("Signature----", util.Signature(srv.Token, timestamp, nonce, echoStr))
|
||||||
|
log.Info("srv.Token---", srv.Token)
|
||||||
|
if signature != util.Signature(srv.Token, timestamp, nonce, echoStr) {
|
||||||
|
return "", NewSDKErr(40001)
|
||||||
|
}
|
||||||
|
_, bData, err := util.DecryptMsg(srv.CorpID, echoStr, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", NewSDKErr(40002)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(bData), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipValidate set skip validate
|
||||||
|
func (srv *Server) SkipValidate(skip bool) {
|
||||||
|
srv.skipValidate = skip
|
||||||
|
}
|
||||||
|
|
||||||
|
//Serve 处理微信的请求消息
|
||||||
|
func (srv *Server) Serve() error {
|
||||||
|
if !srv.Validate() {
|
||||||
|
log.Error("Validate Signature Failed.")
|
||||||
|
return fmt.Errorf("请求校验失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
echostr, exists := srv.GetQuery("echostr")
|
||||||
|
if exists {
|
||||||
|
srv.String(echostr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := srv.handleRequest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//debug print request msg
|
||||||
|
log.Debugf("request msg =%s", string(srv.RequestRawXMLMsg))
|
||||||
|
|
||||||
|
return srv.buildResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Validate 校验请求是否合法
|
||||||
|
func (srv *Server) Validate() bool {
|
||||||
|
if srv.skipValidate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
signature := srv.Query("msg_signature")
|
||||||
|
log.Debugf("validate signature, timestamp=%s, nonce=%s", timestamp, nonce)
|
||||||
|
return signature == util.Signature(srv.Token, timestamp, nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
//HandleRequest 处理微信的请求
|
||||||
|
func (srv *Server) handleRequest() (reply *message.Reply, err error) {
|
||||||
|
//set isSafeMode
|
||||||
|
srv.isSafeMode = false
|
||||||
|
encryptType := srv.Query("encrypt_type")
|
||||||
|
if encryptType == "aes" {
|
||||||
|
srv.isSafeMode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
//set openID
|
||||||
|
srv.openID = srv.Query("openid")
|
||||||
|
|
||||||
|
var msg interface{}
|
||||||
|
msg, err = srv.getMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mixMessage, success := msg.(*message.MixMessage)
|
||||||
|
if !success {
|
||||||
|
err = errors.New("消息类型转换失败")
|
||||||
|
}
|
||||||
|
srv.RequestMsg = mixMessage
|
||||||
|
reply = srv.messageHandler(mixMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetOpenID return openID
|
||||||
|
func (srv *Server) GetOpenID() string {
|
||||||
|
return srv.openID
|
||||||
|
}
|
||||||
|
|
||||||
|
//getMessage 解析微信返回的消息
|
||||||
|
func (srv *Server) getMessage() (interface{}, error) {
|
||||||
|
var rawXMLMsgBytes []byte
|
||||||
|
var err error
|
||||||
|
if srv.isSafeMode {
|
||||||
|
var encryptedXMLMsg message.EncryptedXMLMsg
|
||||||
|
if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil {
|
||||||
|
return nil, fmt.Errorf("从body中解析xml失败,err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//验证消息签名
|
||||||
|
timestamp := srv.Query("timestamp")
|
||||||
|
srv.timestamp, err = strconv.ParseInt(timestamp, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce := srv.Query("nonce")
|
||||||
|
srv.nonce = nonce
|
||||||
|
msgSignature := srv.Query("msg_signature")
|
||||||
|
msgSignatureGen := util.Signature(srv.Token, timestamp, nonce, encryptedXMLMsg.EncryptedMsg)
|
||||||
|
if msgSignature != msgSignatureGen {
|
||||||
|
return nil, fmt.Errorf("消息不合法,验证签名失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
//解密
|
||||||
|
srv.random, rawXMLMsgBytes, err = util.DecryptMsg(srv.CorpID, encryptedXMLMsg.EncryptedMsg, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("消息解密失败, err=%v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rawXMLMsgBytes, err = ioutil.ReadAll(srv.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("从body中解析xml失败, err=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.RequestRawXMLMsg = rawXMLMsgBytes
|
||||||
|
|
||||||
|
return srv.parseRequestMessage(rawXMLMsgBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixMessage, err error) {
|
||||||
|
msg = &message.MixMessage{}
|
||||||
|
err = xml.Unmarshal(rawXMLMsgBytes, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetMessageHandler 设置用户自定义的回调方法
|
||||||
|
func (srv *Server) SetMessageHandler(handler func(*message.MixMessage) *message.Reply) {
|
||||||
|
srv.messageHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) buildResponse(reply *message.Reply) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if reply == nil {
|
||||||
|
//do nothing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msgType := reply.MsgType
|
||||||
|
switch msgType {
|
||||||
|
case message.MsgTypeText:
|
||||||
|
case message.MsgTypeImage:
|
||||||
|
case message.MsgTypeVoice:
|
||||||
|
case message.MsgTypeVideo:
|
||||||
|
case message.MsgTypeMusic:
|
||||||
|
case message.MsgTypeNews:
|
||||||
|
case message.MsgTypeTransfer:
|
||||||
|
default:
|
||||||
|
err = message.ErrUnsupportReply
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgData := reply.MsgData
|
||||||
|
value := reflect.ValueOf(msgData)
|
||||||
|
//msgData must be a ptr
|
||||||
|
kind := value.Kind().String()
|
||||||
|
if kind != "ptr" {
|
||||||
|
return message.ErrUnsupportReply
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make([]reflect.Value, 1)
|
||||||
|
params[0] = reflect.ValueOf(srv.RequestMsg.FromUserName)
|
||||||
|
value.MethodByName("SetToUserName").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(srv.RequestMsg.ToUserName)
|
||||||
|
value.MethodByName("SetFromUserName").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(msgType)
|
||||||
|
value.MethodByName("SetMsgType").Call(params)
|
||||||
|
|
||||||
|
params[0] = reflect.ValueOf(util.GetCurrTS())
|
||||||
|
value.MethodByName("SetCreateTime").Call(params)
|
||||||
|
|
||||||
|
srv.ResponseMsg = msgData
|
||||||
|
srv.ResponseRawXMLMsg, err = xml.Marshal(msgData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Send 将自定义的消息发送
|
||||||
|
func (srv *Server) Send() (err error) {
|
||||||
|
replyMsg := srv.ResponseMsg
|
||||||
|
log.Debugf("response msg =%+v", replyMsg)
|
||||||
|
if srv.isSafeMode {
|
||||||
|
//安全模式下对消息进行加密
|
||||||
|
var encryptedMsg []byte
|
||||||
|
encryptedMsg, err = util.EncryptMsg(srv.random, srv.ResponseRawXMLMsg, srv.CorpID, srv.EncodingAESKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//TODO 如果获取不到timestamp nonce 则自己生成
|
||||||
|
timestamp := srv.timestamp
|
||||||
|
timestampStr := strconv.FormatInt(timestamp, 10)
|
||||||
|
msgSignature := util.Signature(srv.Token, timestampStr, srv.nonce, string(encryptedMsg))
|
||||||
|
replyMsg = message.ResponseEncryptedXMLMsg{
|
||||||
|
EncryptedMsg: string(encryptedMsg),
|
||||||
|
MsgSignature: msgSignature,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Nonce: srv.nonce,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if replyMsg != nil {
|
||||||
|
srv.XML(replyMsg)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
58
work/server/util.go
Normal file
58
work/server/util.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||||
|
var plainContentType = []string{"text/plain; charset=utf-8"}
|
||||||
|
|
||||||
|
func writeContextType(w http.ResponseWriter, value []string) {
|
||||||
|
header := w.Header()
|
||||||
|
if val := header["Content-Type"]; len(val) == 0 {
|
||||||
|
header["Content-Type"] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Render render from bytes
|
||||||
|
func (srv *Server) Render(bytes []byte) {
|
||||||
|
//debug
|
||||||
|
//fmt.Println("response msg = ", string(bytes))
|
||||||
|
srv.Writer.WriteHeader(200)
|
||||||
|
_, err := srv.Writer.Write(bytes)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//String render from string
|
||||||
|
func (srv *Server) String(str string) {
|
||||||
|
writeContextType(srv.Writer, plainContentType)
|
||||||
|
srv.Render([]byte(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
//XML render to xml
|
||||||
|
func (srv *Server) XML(obj interface{}) {
|
||||||
|
writeContextType(srv.Writer, xmlContentType)
|
||||||
|
bytes, err := xml.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
srv.Render(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns the keyed url query value if it exists
|
||||||
|
func (srv *Server) Query(key string) string {
|
||||||
|
value, _ := srv.GetQuery(key)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuery is like Query(), it returns the keyed url query value
|
||||||
|
func (srv *Server) GetQuery(key string) (string, bool) {
|
||||||
|
req := srv.Request
|
||||||
|
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
|
||||||
|
return values[0], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
21
work/tools/calendar.go
Normal file
21
work/tools/calendar.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/silenceper/wechat/v2/work/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
calendarURL = "https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/get?access_token=%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Calendar 日历管理
|
||||||
|
type Calendar struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewCalendar 实例化
|
||||||
|
func NewCalendar(context *context.Context) *Calendar {
|
||||||
|
calendar := new(Calendar)
|
||||||
|
calendar.Context = context
|
||||||
|
return calendar
|
||||||
|
}
|
||||||
226
work/user/user.go
Normal file
226
work/user/user.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
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"
|
||||||
|
launchCode = "https://qyapi.weixin.qq.com/cgi-bin/get_launch_code"
|
||||||
|
)
|
||||||
|
|
||||||
|
//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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RespLaunchCode struct {
|
||||||
|
util.CommonError
|
||||||
|
LaunchCode string `json:"launch_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetLaunchCode 用于打开个人聊天窗口schema
|
||||||
|
func (user *User) GetLaunchCode(userID, other string) (userInfo *RespLaunchCode, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = user.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf(launchCode, accessToken, userID)
|
||||||
|
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
|
||||||
|
}
|
||||||
22
work/work.go
22
work/work.go
@@ -7,6 +7,10 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/work/kf"
|
"github.com/silenceper/wechat/v2/work/kf"
|
||||||
"github.com/silenceper/wechat/v2/work/msgaudit"
|
"github.com/silenceper/wechat/v2/work/msgaudit"
|
||||||
"github.com/silenceper/wechat/v2/work/oauth"
|
"github.com/silenceper/wechat/v2/work/oauth"
|
||||||
|
"github.com/silenceper/wechat/v2/work/server"
|
||||||
|
"github.com/silenceper/wechat/v2/work/tools"
|
||||||
|
"github.com/silenceper/wechat/v2/work/user"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Work 企业微信
|
// Work 企业微信
|
||||||
@@ -29,6 +33,14 @@ func (wk *Work) GetContext() *context.Context {
|
|||||||
return wk.ctx
|
return wk.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetServer 消息管理:接收事件,被动回复消息管理
|
||||||
|
func (wk *Work) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server {
|
||||||
|
srv := server.NewServer(wk.ctx)
|
||||||
|
srv.Request = req
|
||||||
|
srv.Writer = writer
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
//GetOauth get oauth
|
//GetOauth get oauth
|
||||||
func (wk *Work) GetOauth() *oauth.Oauth {
|
func (wk *Work) GetOauth() *oauth.Oauth {
|
||||||
return oauth.NewOauth(wk.ctx)
|
return oauth.NewOauth(wk.ctx)
|
||||||
@@ -43,3 +55,13 @@ func (wk *Work) GetMsgAudit() (*msgaudit.Client, error) {
|
|||||||
func (wk *Work) GetKF() (*kf.Client, error) {
|
func (wk *Work) GetKF() (*kf.Client, error) {
|
||||||
return kf.NewClient(wk.ctx.Config)
|
return kf.NewClient(wk.ctx.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//GetUser get user
|
||||||
|
func (wk *Work) GetUser() *user.User {
|
||||||
|
return user.NewUser(wk.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetCalendar get calendar
|
||||||
|
func (wk *Work) GetCalendar() *tools.Calendar {
|
||||||
|
return tools.NewCalendar(wk.ctx)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user