From 7e24cb9e8d9d3569d3b79d06c7cf0506597ca2ec Mon Sep 17 00:00:00 2001 From: hb Date: Thu, 28 Oct 2021 14:21:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BC=81=E4=B8=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/signature.go | 14 +++ work/message/message.go | 238 +++++++++++++++++++++++++++++++++++ work/message/reply.go | 15 +++ work/oauth/oauth.go | 23 ++++ work/server/error.go | 66 ++++++++++ work/server/server.go | 269 ++++++++++++++++++++++++++++++++++++++++ work/server/util.go | 58 +++++++++ work/tools/calendar.go | 21 ++++ work/user/user.go | 226 +++++++++++++++++++++++++++++++++ work/work.go | 22 ++++ 10 files changed, 952 insertions(+) create mode 100644 work/message/message.go create mode 100644 work/message/reply.go create mode 100644 work/server/error.go create mode 100644 work/server/server.go create mode 100644 work/server/util.go create mode 100644 work/tools/calendar.go create mode 100644 work/user/user.go diff --git a/util/signature.go b/util/signature.go index 22a9cc5..e9a1798 100644 --- a/util/signature.go +++ b/util/signature.go @@ -1,6 +1,7 @@ package util import ( + "bytes" "crypto/sha1" "fmt" "io" @@ -16,3 +17,16 @@ func Signature(params ...string) string { } 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) +} diff --git a/work/message/message.go b/work/message/message.go new file mode 100644 index 0000000..ecbe12a --- /dev/null +++ b/work/message/message.go @@ -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) +} diff --git a/work/message/reply.go b/work/message/reply.go new file mode 100644 index 0000000..53592f0 --- /dev/null +++ b/work/message/reply.go @@ -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{} +} diff --git a/work/oauth/oauth.go b/work/oauth/oauth.go index 296dbaf..a02c10b 100644 --- a/work/oauth/oauth.go +++ b/work/oauth/oauth.go @@ -21,6 +21,8 @@ var ( oauthUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s" //oauthQrContentTargetURL 构造独立窗口登录二维码 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 @@ -85,3 +87,24 @@ func (ctr *Oauth) UserFromCode(code string) (result ResUserInfo, err error) { } 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 +} diff --git a/work/server/error.go b/work/server/error.go new file mode 100644 index 0000000..d6dfa10 --- /dev/null +++ b/work/server/error.go @@ -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 + } +} diff --git a/work/server/server.go b/work/server/server.go new file mode 100644 index 0000000..c6844f4 --- /dev/null +++ b/work/server/server.go @@ -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 +} diff --git a/work/server/util.go b/work/server/util.go new file mode 100644 index 0000000..6c108fa --- /dev/null +++ b/work/server/util.go @@ -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 +} diff --git a/work/tools/calendar.go b/work/tools/calendar.go new file mode 100644 index 0000000..f03d2be --- /dev/null +++ b/work/tools/calendar.go @@ -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 +} diff --git a/work/user/user.go b/work/user/user.go new file mode 100644 index 0000000..506e8d8 --- /dev/null +++ b/work/user/user.go @@ -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 +} diff --git a/work/work.go b/work/work.go index b974c35..451f81a 100644 --- a/work/work.go +++ b/work/work.go @@ -7,6 +7,10 @@ import ( "github.com/silenceper/wechat/v2/work/kf" "github.com/silenceper/wechat/v2/work/msgaudit" "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 企业微信 @@ -29,6 +33,14 @@ func (wk *Work) GetContext() *context.Context { 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 func (wk *Work) GetOauth() *oauth.Oauth { return oauth.NewOauth(wk.ctx) @@ -43,3 +55,13 @@ func (wk *Work) GetMsgAudit() (*msgaudit.Client, error) { func (wk *Work) GetKF() (*kf.Client, error) { 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) +}