From 4a4339fc322139109c2be2f55e914d91c81ebf04 Mon Sep 17 00:00:00 2001 From: wind Date: Fri, 27 Sep 2024 15:44:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E4=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/encryptor/encryptor.go | 16 +- work/externalcontact/user.go | 65 +------ work/message/README.md | 3 - work/message/message.go | 2 +- work/message/mix_message.go | 285 +++++++++++++++++++++++++++++ work/message/template_card.go | 169 +---------------- work/oauth/user.go | 64 +++++++ work/tools/calendar.go | 21 --- work/user/user.go | 40 +--- work/work.go | 16 ++ 10 files changed, 394 insertions(+), 287 deletions(-) create mode 100644 work/message/mix_message.go create mode 100644 work/oauth/user.go diff --git a/miniprogram/encryptor/encryptor.go b/miniprogram/encryptor/encryptor.go index af3c4c2..a5dffb4 100644 --- a/miniprogram/encryptor/encryptor.go +++ b/miniprogram/encryptor/encryptor.go @@ -7,8 +7,8 @@ import ( "encoding/json" "errors" "fmt" - "github.com/silenceper/wechat/v2/miniprogram/context" + "strings" ) // Encryptor struct @@ -108,13 +108,23 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) { } // 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) if err != nil { 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 - err = json.Unmarshal(cipherText, &plainData) + err = json.Unmarshal([]byte(cipherTextData), &plainData) if err != nil { return nil, err } diff --git a/work/externalcontact/user.go b/work/externalcontact/user.go index 34282ed..60b29f8 100644 --- a/work/externalcontact/user.go +++ b/work/externalcontact/user.go @@ -38,59 +38,10 @@ type UserInfo struct { FollowInfo FollowInfo `json:"follow_info"` //企业成员客户跟进人信息,可以参考获取客户详情,但标签信息只会返回企业标签和规则组标签的tag_id,个人标签将不再返回 } -type ExternalContact struct { - ExternalUserid string `json:"external_userid"` - Name string `json:"name"` - Position string `json:"position"` - Avatar string `json:"avatar"` - CorpName string `json:"corp_name"` - CorpFullName string `json:"corp_full_name"` - Type int `json:"type"` - Gender int `json:"gender"` - Unionid string `json:"unionid"` - ExternalProfile struct { - 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,omitempty"` -} - -type FollowInfo struct { - Userid string `json:"userid"` - Remark string `json:"remark"` - Description string `json:"description"` - Createtime int `json:"createtime"` - TagId []string `json:"tag_id"` //批量获取时才有 - Tags []struct { - GroupName string `json:"group_name"` - TagName string `json:"tag_name"` - TagId string `json:"tag_id,omitempty"` - Type int `json:"type"` - } `json:"tags,omitempty"` //单独获取时才有 - RemarkCorpName string `json:"remark_corp_name,omitempty"` - RemarkMobiles []string `json:"remark_mobiles,omitempty"` - OperUserid string `json:"oper_userid"` - AddWay int `json:"add_way"` - State string `json:"state,omitempty"` -} - -//GetUseridList 获取我的客户列表 +// GetUseridList 获取我的客户列表 func (tpl *Client) GetUseridList(myUserid string) (externalUserid []string, err error) { var accessToken string - accessToken, err = tpl.ctx.GetAccessToken() + accessToken, err = tpl.GetAccessToken() if err != nil { return } @@ -113,7 +64,7 @@ func (tpl *Client) GetUseridList(myUserid string) (externalUserid []string, err return } -//GetUseridList 获取我的全部客户列表及详情 +// GetUseridList 获取我的全部客户列表及详情 func (tpl *Client) GetQyUserInfoList(qyUserid []string) ([]UserInfo, error) { var userInfoList []UserInfo var req ReqGetByUser @@ -134,7 +85,7 @@ func (tpl *Client) GetQyUserInfoList(qyUserid []string) ([]UserInfo, error) { return userInfoList, nil } -//GetUserInfoAndAllFollow 获取客户详情以及全部跟进人 +// GetUserInfoAndAllFollow 获取客户详情以及全部跟进人 func (tpl *Client) GetUserInfoAndAllFollow(userid string) (OneUser, error) { var result, res OneUser var err error @@ -155,10 +106,10 @@ func (tpl *Client) GetUserInfoAndAllFollow(userid string) (OneUser, error) { return result, nil } -//GetUserInfo 获取客户详情 +// GetUserInfo 获取客户详情 func (tpl *Client) GetUserInfo(externalUserid string, cursor ...string) (result OneUser, err error) { var accessToken string - accessToken, err = tpl.ctx.GetAccessToken() + accessToken, err = tpl.GetAccessToken() if err != nil { return } @@ -183,10 +134,10 @@ func (tpl *Client) GetUserInfo(externalUserid string, cursor ...string) (result return } -//GetUserInfoListByUserId 批量获取客户详情 +// GetUserInfoListByUserId 批量获取客户详情 func (tpl *Client) GetUserInfoListByUserIds(req ReqGetByUser) (userList []UserInfo, nextCursor string, err error) { var accessToken string - accessToken, err = tpl.ctx.GetAccessToken() + accessToken, err = tpl.GetAccessToken() if err != nil { return } diff --git a/work/message/README.md b/work/message/README.md index 20bc498..e69de29 100644 --- a/work/message/README.md +++ b/work/message/README.md @@ -1,3 +0,0 @@ -### 企业微信消息推送SDK - -相关文档正在梳理中... \ No newline at end of file diff --git a/work/message/message.go b/work/message/message.go index ef6d24d..db4a8aa 100644 --- a/work/message/message.go +++ b/work/message/message.go @@ -24,7 +24,7 @@ type ( // 消息类型,此时固定为:text MsgType string `json:"msgtype"` // 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值 - AgentID string `json:"agentid"` + AgentID int `json:"agentid"` // 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0 Safe int `json:"safe"` // 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。 diff --git a/work/message/mix_message.go b/work/message/mix_message.go new file mode 100644 index 0000000..74fc023 --- /dev/null +++ b/work/message/mix_message.go @@ -0,0 +1,285 @@ +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 ( + //MsgTypeEvent 表示事件推送消息 [限接收] + MsgTypeEvent = "event" + + //MsgTypeText 表示文本消息 + MsgTypeText MsgType = "text" + //MsgTypeImage 表示图片消息 + MsgTypeImage MsgType = "image" + //MsgTypeVoice 表示语音消息 + MsgTypeVoice MsgType = "voice" + //MsgTypeVideo 表示视频消息 + MsgTypeVideo MsgType = "video" + //MsgTypeNews 表示图文消息[限回复与发送应用消息] + MsgTypeNews MsgType = "news" + + //MsgTypeLink 表示链接消息[限接收] + MsgTypeLink MsgType = "link" + //MsgTypeLocation 表示坐标消息[限接收] + MsgTypeLocation MsgType = "location" + + //MsgTypeUpdateButton 更新点击用户的按钮文案[限回复应用消息] + MsgTypeUpdateButton MsgType = "update_button" + //MsgTypeUpdateTemplateCard 更新点击用户的整张卡片[限回复应用消息] + MsgTypeUpdateTemplateCard MsgType = "update_template_card" + + //MsgTypeFile 文件消息[限发送应用消息] + MsgTypeFile MsgType = "file" + //MsgTypeTextCard 文本卡片消息[限发送应用消息] + MsgTypeTextCard MsgType = "textcard" + //MsgTypeMpNews 图文消息[限发送应用消息] 跟普通的图文消息一致,唯一的差异是图文内容存储在企业微信 + MsgTypeMpNews MsgType = "mpnews" + //MsgTypeMarkdown markdown消息[限发送应用消息] + MsgTypeMarkdown MsgType = "markdown" + //MsgTypeMiniprogramNotice 小程序通知消息[限发送应用消息] + MsgTypeMiniprogramNotice MsgType = "miniprogram_notice" + //MsgTypeTemplateCard 模板卡片消息[限发送应用消息] + MsgTypeTemplateCard MsgType = "template_card" +) + +const ( + //EventSubscribe 成员关注,成员已经加入企业,管理员添加成员到应用可见范围(或移除可见范围)时 + EventSubscribe EventType = "subscribe" + //EventUnsubscribe 成员取消关注,成员已经在应用可见范围,成员加入(或退出)企业时 + EventUnsubscribe EventType = "unsubscribe" + //EventEnterAgent 本事件在成员进入企业微信的应用时触发 + EventEnterAgent EventType = "enter_agent" + //EventLocation 上报地理位置事件 + EventLocation EventType = "LOCATION" + //EventBatchJobResult 异步任务完成事件推送 + EventBatchJobResult EventType = "batch_job_result" + //EventClick 点击菜单拉取消息时的事件推送 + EventClick EventType = "click" + //EventView 点击菜单跳转链接时的事件推送 + EventView EventType = "view" + //EventScancodePush 扫码推事件的事件推送 + EventScancodePush EventType = "scancode_push" + //EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送 + EventScancodeWaitmsg EventType = "scancode_waitmsg" + //EventPicSysphoto 弹出系统拍照发图的事件推送 + EventPicSysphoto EventType = "pic_sysphoto" + //EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送 + EventPicPhotoOrAlbum EventType = "pic_photo_or_album" + //EventPicWeixin 弹出微信相册发图器的事件推送 + EventPicWeixin EventType = "pic_weixin" + //EventLocationSelect 弹出地理位置选择器的事件推送 + EventLocationSelect EventType = "location_select" + + //EventOpenApprovalChange 审批状态通知事件推送 + EventOpenApprovalChange EventType = "open_approval_change" + + //EventShareAgentChange 共享应用事件回调 + EventShareAgentChange EventType = "share_agent_change" + + //EventTemplateCard 模板卡片事件推送 + EventTemplateCard EventType = "template_card_event" + + //EventTemplateCardMenu 通用模板卡片右上角菜单事件推送 + EventTemplateCardMenu EventType = "template_card_menu_event" + + //EventChangeExternalContact 企业客户事件推送 + //add_external_contact 添加 + //edit_external_contact 编辑 + //add_half_external_contact 免验证添加 + //del_external_contact 员工删除客户 + //del_follow_user 客户删除跟进员工 + //transfer_fail 企业将客户分配给新的成员接替后,客户添加失败 + //change_external_chat 客户群创建事件 + EventChangeExternalContact EventType = "change_external_contact" + + //EventChangeExternalChat 企业客户群变更事件推送 + //create 客户群创建 + //update 客户群变更 + //dismiss 客户群解散 + EventChangeExternalChat EventType = "change_external_chat" + + //EventChangeExternalTag 企业客户标签创建事件推送 + //create 创建标签 + //update 变更标签 + //delete 删除标签 + //shuffle 重新排序 + EventChangeExternalTag EventType = "change_external_tag" + + //EventKfMsg 企业微信客服回调事件 + EventKfMsg EventType = "kf_msg_or_event" + //EventLivingStatusChange 直播回调事件 + EventLivingStatusChange EventType = "living_status_change" + + //EventMsgauditNotify 会话内容存档开启后,产生会话回调事件 + EventMsgauditNotify EventType = "msgaudit_notify" +) + +//todo 第三方应用开发 +/*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 + AgentID int `xml:"AgentID"` //企业应用的id,整型。可在应用的设置页面查看 + + Content string `xml:"Content,omitempty"` //文本消息内容 + Format string `xml:"Format,omitempty"` //语音消息格式,如amr,speex等 + ThumbMediaID string `xml:"ThumbMediaId,omitempty"` //视频消息缩略图的媒体id,可以调用获取媒体文件接口拉取数据,仅三天内有效 + + Title string `xml:"Title,omitempty"` //链接消息,标题 + Description string `xml:"Description,omitempty"` //链接消息,描述 + URL string `xml:"Url,omitempty"` //链接消息,链接跳转的url + + PicURL string `xml:"PicUrl,omitempty"` ////图片消息或者链接消息,封面缩略图的url + MediaID string `xml:"MediaId,omitempty"` //图片媒体文件id//语音媒体文件id//视频消息缩略图的媒体id,可以调用获取媒体文件接口拉取,仅三天内有效 + + LocationX float64 `xml:"Location_X,omitempty"` //位置消息,地理位置纬度 + LocationY float64 `xml:"Location_Y,omitempty"` //位置消息,地理位置经度 + Scale float64 `xml:"Scale,omitempty"` //位置消息,地图缩放大小 + Label string `xml:"Label,omitempty"` //位置消息,地理位置信息 + + AppType string `xml:"AppType,omitempty"` //接收地理位置时存在,app类型,在企业微信固定返回wxwork,在微信不返回该字段 + + //TemplateMsgID int64 `xml:"MsgID"` //模板消息推送成功的消息是MsgID + ///Recognition string `xml:"Recognition"` + + //事件相关 + Event EventType `xml:"Event,omitempty"` + EventKey string `xml:"EventKey,omitempty"` + ChangeType string `xml:"ChangeType,omitempty"` + + //模板卡片事件推送 https://developer.work.weixin.qq.com/document/path/90240#%E6%A8%A1%E6%9D%BF%E5%8D%A1%E7%89%87%E4%BA%8B%E4%BB%B6%E6%8E%A8%E9%80%81 + TaskId string `xml:"TaskId,omitempty"` //与发送模板卡片消息时指定的task_id相同 + CardType string `xml:"CardType,omitempty"` //通用模板卡片的类型,类型有"text_notice", "news_notice", "button_interaction", "vote_interaction", "multiple_interaction"五种 + ResponseCode string `xml:"ResponseCode,omitempty"` //用于调用更新卡片接口的ResponseCode,24小时内有效,且只能使用一次 + SelectedItems struct { + SelectedItem struct { + QuestionKey string `xml:"QuestionKey"` //问题的key值 + OptionIds struct { //对应问题的选项列表 + OptionId string `xml:"OptionId"` + } `xml:"OptionIds"` + } `xml:"SelectedItem"` + } `xml:"SelectedItems,omitempty"` + + //仅上报地理位置事件 + Latitude string `xml:"Latitude,omitempty"` //地理位置纬度 + Longitude string `xml:"Longitude,omitempty"` //地理位置经度 + Precision string `xml:"Precision,omitempty"` //地理位置精度 + + //仅异步任务完成事件 + JobId string `xml:"JobId,omitempty"` //异步任务id,最大长度为64字符 + JobType string `xml:"JobType,omitempty"` //异步任务,操作类型,字符串,目前分别有:sync_user(增量更新成员)、 replace_user(全量覆盖成员)、invite_user(邀请成员关注)、replace_party(全量覆盖部门) + ErrCode int `xml:"ErrCode,omitempty"` //异步任务,返回码 + ErrMsg string `xml:"ErrMsg,omitempty"` //异步任务,对返回码的文本描述内容 + + //开启通讯录回调通知 https://open.work.weixin.qq.com/api/doc/90000/90135/90967 + UserID string `xml:"UserID,omitempty"` //用户userid + ExternalUserID string `xml:"ExternalUserID,omitempty"` //外部联系人userid + State string `xml:"State,omitempty"` //添加此用户的「联系我」方式配置的state参数,可用于识别添加此用户的渠道 + WelcomeCode string `xml:"WelcomeCode,omitempty"` //欢迎码,当state为1时,该值有效 + Source string `xml:"Source,omitempty"` //删除客户的操作来源,DELETE_BY_TRANSFER表示此客户是因在职继承自动被转接成员删除 + + // todo 第三方平台相关 字段名可能不准确 + /*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"`*/ + + //设备相关 + 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"` + AgentID string `xml:"AgentID" json:"AgentID"` + 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/template_card.go b/work/message/template_card.go index c91bdef..6b0e0de 100644 --- a/work/message/template_card.go +++ b/work/message/template_card.go @@ -4,19 +4,17 @@ import ( "encoding/json" "fmt" "github.com/silenceper/wechat/v2/util" - "strconv" ) const ( - messageSendURL = "https://qyapi.weixin.qq.com/cgi-bin/message/send" messageUpdateTemplateCardURL = "https://api.weixin.qq.com/cgi-bin/message/update_template_card" messageDelURL = "https://api.weixin.qq.com/cgi-bin/message/recall" ) // UpdateButton 模板卡片按钮 type UpdateButton struct { - CommonToken `json:"-"` - Button struct { + //CommonToken `json:"-"` + Button struct { ReplaceName string `xml:"ReplaceName" json:"replace_name"` } `xml:"Button" json:"button"` } @@ -31,7 +29,7 @@ func NewUpdateButton(replaceName string) *UpdateButton { // TemplateCard 被动回复模板卡片 // https://open.work.weixin.qq.com/api/doc/90000/90135/90241 type TemplateCard struct { - CommonToken `json:"-"` + //CommonToken `json:"-"` TemplateCard interface{} `xml:"TemplateCard" json:"template_card"` } @@ -42,137 +40,6 @@ func NewTemplateCard(cardXml interface{}) *TemplateCard { return card } -// AppMessage 发送的模板消息内容 -type AppMessage struct { - ToUser string `json:"touser"` // 必须, 成员ID列表(多个接收者用‘|’分隔,最多支持1000个 ,指定为”@all”,则向该企业应用的全部成员发送 - Toparty string `json:"toparty,omitempty"` //部门ID列表,当touser为”@all”时忽略本参数 - Totag string `json:"totag,omitempty"` //标签ID列表,当touser为”@all”时忽略本参数 - Msgtype MsgType `json:"msgtype"` - Agentid int `json:"agentid"` - Safe int `json:"safe,omitempty"` - EnableIdTrans int `json:"enable_id_trans,omitempty"` - EnableDuplicateCheck int `json:"enable_duplicate_check,omitempty"` - DuplicateCheckInterval int `json:"duplicate_check_interval,omitempty"` - Text *Text `json:"text,omitempty"` - *Image `json:"image,omitempty"` - *Voice `json:"voice,omitempty"` - *Video `json:"video,omitempty"` - File *PushFile `json:"file,omitempty"` - TextCard *PushTextCard `json:"textcard,omitempty"` - News *News `json:"news,omitempty"` - MpNews *MpNews `json:"mpnews,omitempty"` - Markdown *Text `json:"markdown,omitempty"` - //todo(wind) 可能会发生变化的字段直接用interface{}了 - MiniprogramNotice interface{} `json:"miniprogram_notice,omitempty"` - TemplateCard *TemplateCardButton `json:"template_card,omitempty"` -} -type Action struct { - Text string `json:"text"` - Key string `json:"key"` -} -type HorizontalContent struct { - Keyname string `json:"keyname"` - Value string `json:"value,omitempty"` - Type int `json:"type,omitempty"` - Url string `json:"url,omitempty"` - MediaId string `json:"media_id,omitempty"` - Userid string `json:"userid,omitempty"` -} -type VerticalContent struct { - Title string `json:"title"` - Desc string `json:"desc"` -} -type Jump struct { - Type int `json:"type,omitempty"` - Title string `json:"title"` - Url string `json:"url,omitempty"` - Appid string `json:"appid,omitempty"` - Pagepath string `json:"pagepath,omitempty"` -} -type OptionCheckBox struct { - Id string `json:"id"` - Text string `json:"text"` - IsChecked bool `json:"is_checked"` -} -type Option struct { - Id string `json:"id"` - Text string `json:"text"` -} -type Select struct { - QuestionKey string `json:"question_key"` - Title string `json:"title"` - SelectedId string `json:"selected_id"` - OptionList []Option `json:"option_list"` -} -type Button struct { - Text string `json:"text"` - Style int `json:"style,omitempty"` - Key string `json:"key,omitempty"` -} - -type TemplateCardButton struct { - CardType string `json:"card_type"` - Source struct { - IconUrl string `json:"icon_url,omitempty"` - Desc string `json:"desc,omitempty"` - DescColor int `json:"desc_color,omitempty"` - } `json:"source,omitempty"` - - ActionMenu struct { - Desc string `json:"desc,omitempty"` - ActionList []Action `json:"action_list"` - } `json:"action_menu,omitempty"` - MainTitle struct { - Title string `json:"title"` - Desc string `json:"desc,omitempty"` - } `json:"main_title,omitempty"` - QuoteArea struct { - Type int `json:"type,omitempty"` - Url string `json:"url,omitempty"` - Title string `json:"title,omitempty"` - QuoteText string `json:"quote_text,omitempty"` - } `json:"quote_area"` - SubTitleText string `json:"sub_title_text,omitempty"` - HorizontalContentList []HorizontalContent `json:"horizontal_content_list"` - VerticalContentList []VerticalContent `json:"vertical_content_list,omitempty"` - CardAction struct { - Type int `json:"type,omitempty"` - Url string `json:"url,omitempty"` - Appid string `json:"appid,omitempty"` - Pagepath string `json:"pagepath,omitempty"` - } `json:"card_action"` - JumpList []Jump `json:"jump_list,omitempty"` - EmphasisContent struct { - Title string `json:"title,omitempty"` - Desc string `json:"desc,omitempty"` - } `json:"emphasis_content,omitempty"` - ImageTextArea struct { - Type int `json:"type"` - Url string `json:"url"` - Title string `json:"title"` - Desc string `json:"desc"` - ImageUrl string `json:"image_url"` - } `json:"image_text_area,omitempty"` - CardImage struct { - Url string `json:"url"` - AspectRatio float64 `json:"aspect_ratio"` - } `json:"card_image,omitempty"` - Checkbox struct { - QuestionKey string `json:"question_key"` - OptionList []OptionCheckBox `json:"option_list"` - Mode int `json:"mode"` - } `json:"checkbox,omitempty"` - SelectList []Select `json:"select_list"` - TaskId string `json:"task_id"` - ButtonSelection struct { - QuestionKey string `json:"question_key"` - Title string `json:"title,omitempty"` - OptionList []Option `json:"option_list,omitempty"` - SelectedId string `json:"selected_id,omitempty"` - } `json:"button_selection,omitempty"` - ButtonList []Button `json:"button_list"` -} - type PushFile struct { MediaID string `json:"media_id"` } @@ -192,36 +59,6 @@ type resTemplateSend struct { ResponseCode string `json:"response_code"` //仅消息类型为“按钮交互型”,“投票选择型”和“多项选择型”的模板卡片消息返回,应用可使用response_code调用更新模版卡片消息接口,24小时内有效,且只能使用一次 } -// Send 发送应用消息 -func (r *Client) Send(msg *AppMessage) (msgID string, responseCode string, err error) { - var accessToken string - accessToken, err = r.GetAccessToken() - if err != nil { - return - } - uri := fmt.Sprintf("%s?access_token=%s", messageSendURL, accessToken) - var response []byte - if msg.Agentid == 0 { - msg.Agentid, _ = strconv.Atoi(r.Context.AgentID) - } - 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 - responseCode = result.ResponseCode - return -} - // TemplateUpdate 更新模版卡片消息内容 type TemplateUpdate struct { Userids []string `json:"userids"` diff --git a/work/oauth/user.go b/work/oauth/user.go new file mode 100644 index 0000000..581c758 --- /dev/null +++ b/work/oauth/user.go @@ -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 +} diff --git a/work/tools/calendar.go b/work/tools/calendar.go index f03d2be..e69de29 100644 --- a/work/tools/calendar.go +++ b/work/tools/calendar.go @@ -1,21 +0,0 @@ -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 index 66eda7c..93d9012 100644 --- a/work/user/user.go +++ b/work/user/user.go @@ -13,22 +13,21 @@ 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?access_token=%s" ) -//User 用户管理 +// User 用户管理 type User struct { *context.Context } -//NewUser 实例化 +// NewUser 实例化 func NewUser(context *context.Context) *User { user := new(User) user.Context = context return user } -//Info 用户基本信息 +// Info 用户基本信息 type Info struct { util.CommonError Userid string `json:"userid"` @@ -100,7 +99,7 @@ type OpenidList struct { NextOpenID string `json:"next_openid"` } -//GetUserInfo 获取用户基本信息 +// GetUserInfo 获取用户基本信息 func (user *User) GetUserInfo(userID string) (userInfo *Info, err error) { var accessToken string accessToken, err = user.GetAccessToken() @@ -193,34 +192,3 @@ func (user *User) ListAllUserOpenIDs() ([]string, error) { } } } - -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) - 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 24c5773..1c79a89 100644 --- a/work/work.go +++ b/work/work.go @@ -9,12 +9,15 @@ import ( "github.com/silenceper/wechat/v2/work/context" "github.com/silenceper/wechat/v2/work/externalcontact" "github.com/silenceper/wechat/v2/work/invoice" + "github.com/silenceper/wechat/v2/work/js" "github.com/silenceper/wechat/v2/work/kf" "github.com/silenceper/wechat/v2/work/material" "github.com/silenceper/wechat/v2/work/message" "github.com/silenceper/wechat/v2/work/msgaudit" "github.com/silenceper/wechat/v2/work/oauth" "github.com/silenceper/wechat/v2/work/robot" + "github.com/silenceper/wechat/v2/work/server" + "net/http" ) // Work 企业微信 @@ -91,3 +94,16 @@ func (wk *Work) GetInvoice() *invoice.Client { func (wk *Work) GetCheckin() *checkin.Client { return checkin.NewClient(wk.ctx) } + +// GetJs js-sdk配置 +func (wk *Work) GetJs() *js.Js { + return js.NewJs(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 +}