diff --git a/doc/api/work.md b/doc/api/work.md index 43061b6..a188268 100644 --- a/doc/api/work.md +++ b/doc/api/work.md @@ -74,7 +74,9 @@ host: https://qyapi.weixin.qq.com/ | 删除企业已配置的「联系我」方式 | POST | /cgi-bin/externalcontact/del_contact_way | YES | (r *Client) DelContactWay | MARKWANG | ## 通讯录管理 -[官方文档](https://developer.work.weixin.qq.com/document/path/95350/90200) +[官方文档](https://developer.work.weixin.qq.com/document/path/90193) + +### 部门管理 | 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | |:---------:|------|:----------------------------------------| ---------- | ------------------------------- |----------| @@ -82,6 +84,22 @@ host: https://qyapi.weixin.qq.com/ | 获取部门成员 | GET | /cgi-bin/user/simplelist | YES | (r *Client) UserSimpleList | MARKWANG | ======= +### 成员管理 + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +| -------- | -------- | ----------------- | ---------- | ------------------- | -------- | +| 读取成员 | GET | /cgi-bin/user/get | YES | (r *Client) UserGet | chcthink | + + + +## 群机器人 + +[官方文档](https://developer.work.weixin.qq.com/document/path/91770) + +| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 | +| ---------------- | -------- | --------------------- | ---------- | -------------------------- | -------- | +| 群机器人发送消息 | POST | /cgi-bin/webhook/send | YES | (r *Client) RobotBroadcast | chcthink | + ## 应用管理 TODO diff --git a/work/addresslist/user.go b/work/addresslist/user.go index 2847a1c..f5f7c6b 100644 --- a/work/addresslist/user.go +++ b/work/addresslist/user.go @@ -9,6 +9,8 @@ import ( const ( // UserSimpleListURL 获取部门成员 UserSimpleListURL = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%d" + // UserGetURL 读取成员 + UserGetURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s" ) type ( @@ -47,3 +49,87 @@ func (r *Client) UserSimpleList(departmentID int) ([]*UserList, error) { } return result.UserList, nil } + +// UserGetResponse 获取部门成员响应 +type UserGetResponse struct { + util.CommonError + UserID string `json:"userid"` // 成员UserID。对应管理端的帐号,企业内必须唯一。不区分大小写,长度为1~64个字节;第三方应用返回的值为open_userid + Name string `json:"name"` // 成员名称;第三方不可获取,调用时返回userid以代替name;代开发自建应用需要管理员授权才返回;对于非第三方创建的成员,第三方通讯录应用也不可获取;未返回name的情况需要通过通讯录展示组件来展示名字 + Department []int `json:"department"` // 成员所属部门id列表,仅返回该应用有查看权限的部门id;成员授权模式下,固定返回根部门id,即固定为1。对授权了“组织架构信息”权限的第三方应用,返回成员所属的全部部门id + Order []int `json:"order"` // 部门内的排序值,默认为0。数量必须和department一致,数值越大排序越前面。值范围是[0, 2^32)。成员授权模式下不返回该字段 + Position string `json:"position"` // 职务信息;代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Mobile string `json:"mobile"` // 手机号码,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Gender string `json:"gender"` // 性别。0表示未定义,1表示男性,2表示女性。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段。注:不可获取指返回值0 + Email string `json:"email"` // 邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + BizMail string `json:"biz_mail"` // 企业邮箱,代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + IsLeaderInDept []int `json:"is_leader_in_dept"` // 表示在所在的部门内是否为部门负责人,数量与department一致;第三方通讯录应用或者授权了“组织架构信息-应用可获取企业的部门组织架构信息-部门负责人”权限的第三方应用可获取;对于非第三方创建的成员,第三方通讯录应用不可获取;上游企业不可获取下游企业成员该字段 + DirectLeader []string `json:"direct_leader"` // 直属上级UserID,返回在应用可见范围内的直属上级列表,最多有五个直属上级;第三方通讯录应用或者授权了“组织架构信息-应用可获取可见范围内成员组织架构信息-直属上级”权限的第三方应用可获取;对于非第三方创建的成员,第三方通讯录应用不可获取;上游企业不可获取下游企业成员该字段;代开发自建应用不可获取该字段 + Avatar string `json:"avatar"` // 头像url。 代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + ThumbAvatar string `json:"thumb_avatar"` // 头像缩略图url。第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Telephone string `json:"telephone"` // 座机。代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Alias string `json:"alias"` // 别名;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + Address string `json:"address"` // 地址。代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + OpenUserid string `json:"open_userid"` // 全局唯一。对于同一个服务商,不同应用获取到企业内同一个成员的open_userid是相同的,最多64个字节。仅第三方应用可获取 + 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"` // 激活状态: 1=已激活,2=已禁用,4=未激活,5=退出企业。 已激活代表已激活企业微信或已关注微信插件(原企业号)。未激活代表既未激活企业微信又未关注微信插件(原企业号)。 + QrCode string `json:"qr_code"` // 员工个人二维码,扫描可添加为外部联系人(注意返回的是一个url,可在浏览器上打开该url以展示二维码);代开发自建应用需要管理员授权且成员oauth2授权获取;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 + ExternalPosition string `json:"external_position"` // 对外职务,如果设置了该值,则以此作为对外展示的职务,否则以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"` // 成员对外属性,字段详情见对外属性;代开发自建应用需要管理员授权才返回;第三方仅通讯录应用可获取;对于非第三方创建的成员,第三方通讯录应用也不可获取;上游企业不可获取下游企业成员该字段 +} + +// UserGet 获取部门成员 +// @see https://developer.work.weixin.qq.com/document/path/90196 +func (r *Client) UserGet(UserID string) (*UserGetResponse, error) { + var ( + accessToken string + err error + ) + if accessToken, err = r.GetAccessToken(); err != nil { + return nil, err + } + var response []byte + if response, err = util.HTTPGet(fmt.Sprintf(UserGetURL, accessToken, UserID)); err != nil { + return nil, err + } + result := &UserGetResponse{} + err = util.DecodeWithError(response, result, "UserGet") + if err != nil { + return nil, err + } + return result, nil +} diff --git a/work/oauth/oauth.go b/work/oauth/oauth.go index 06074b6..3f475d9 100644 --- a/work/oauth/oauth.go +++ b/work/oauth/oauth.go @@ -17,6 +17,8 @@ type Oauth struct { var ( // oauthTargetURL 企业微信内跳转地址 oauthTargetURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect" + // oauthTargetURL 企业微信内跳转地址(获取成员的详细信息) + oauthTargetPrivateURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&agentid=%s&state=STATE#wechat_redirect" // oauthUserInfoURL 获取用户信息地址 oauthUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s" // oauthQrContentTargetURL 构造独立窗口登录二维码 @@ -40,6 +42,17 @@ func (ctr *Oauth) GetTargetURL(callbackURL string) string { ) } +// GetTargetPrivateURL 获取个人信息授权地址 +func (ctr *Oauth) GetTargetPrivateURL(callbackURL string, agentID string) string { + // url encode + return fmt.Sprintf( + oauthTargetPrivateURL, + ctr.CorpID, + url.QueryEscape(callbackURL), + agentID, + ) +} + // GetQrContentTargetURL 构造独立窗口登录二维码 func (ctr *Oauth) GetQrContentTargetURL(callbackURL string) string { // url encode diff --git a/work/robot/client.go b/work/robot/client.go new file mode 100644 index 0000000..faaaa3e --- /dev/null +++ b/work/robot/client.go @@ -0,0 +1,17 @@ +package robot + +import ( + "github.com/silenceper/wechat/v2/work/context" +) + +// Client 群聊机器人接口实例 +type Client struct { + *context.Context +} + +// NewClient 初始化实例 +func NewClient(ctx *context.Context) *Client { + return &Client{ + ctx, + } +} diff --git a/work/robot/robot.go b/work/robot/robot.go new file mode 100644 index 0000000..145f63d --- /dev/null +++ b/work/robot/robot.go @@ -0,0 +1,29 @@ +package robot + +import ( + "encoding/json" + "fmt" + + "github.com/silenceper/wechat/v2/util" +) + +const ( + // WebhookSendURL 机器人发送群组消息 + WebhookSendURL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s" +) + +// RobotBroadcast 群机器人消息发送 +// @see https://developer.work.weixin.qq.com/document/path/91770 +func (r *Client) RobotBroadcast(webhookKey string, options interface{}) (info util.CommonError, err error) { + var data []byte + if data, err = util.PostJSON(fmt.Sprintf(WebhookSendURL, webhookKey), options); err != nil { + return + } + if err = json.Unmarshal(data, &info); err != nil { + return + } + if info.ErrCode != 0 { + return info, err + } + return info, nil +} diff --git a/work/robot/send_option.go b/work/robot/send_option.go new file mode 100644 index 0000000..39efebe --- /dev/null +++ b/work/robot/send_option.go @@ -0,0 +1,126 @@ +package robot + +import "github.com/silenceper/wechat/v2/util" + +// WebhookSendResponse 机器人发送群组消息响应 +type WebhookSendResponse struct { + util.CommonError +} + +// WebhookSendTextOption 机器人发送文本消息请求参数 +type WebhookSendTextOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为text + Text struct { + Content string `json:"content"` // 文本内容,最长不超过2048个字节,必须是utf8编码 + MentionedList []string `json:"mentioned_list"` // userid的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userid,可以使用mentioned_mobile_list + MentionedMobileList []string `json:"mentioned_mobile_list"` // 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人 + } `json:"text"` // 文本消息内容 +} + +// WebhookSendMarkdownOption 机器人发送markdown消息请求参数 +// 支持语法参考 https://developer.work.weixin.qq.com/document/path/91770 +type WebhookSendMarkdownOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为markdown + Markdown struct { + Content string `json:"content"` // markdown内容,最长不超过4096个字节,必须是utf8编码 + } `json:"markdown"` // markdown消息内容 +} + +// WebhookSendImageOption 机器人发送图片消息请求参数 +type WebhookSendImageOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为image + Image struct { + Base64 string `json:"base64"` // 图片内容的base64编码 + MD5 string `json:"md5"` // 图片内容(base64编码前)的md5值 + } `json:"image"` // 图片消息内容 +} + +// WebhookSendNewsOption 机器人发送图文消息请求参数 +type WebhookSendNewsOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为news + News struct { + Articles []struct { + Title string `json:"title"` // 标题,不超过128个字节,超过会自动截断 + Description string `json:"description"` // 描述,不超过512个字节,超过会自动截断 + URL string `json:"url"` // 点击后跳转的链接 + PicURL string `json:"picurl"` // 图文消息的图片链接,支持JPG、PNG格式,较好的效果为大图 1068*455,小图150*150 + } `json:"articles"` // 图文消息列表 一个图文消息支持1到8条图文 + } `json:"news"` // 图文消息内容 +} + +// WebhookSendFileOption 机器人发送文件消息请求参数 +type WebhookSendFileOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时固定为file + File struct { + MediaID string `json:"media_id"` // 文件id,通过下文的文件上传接口获取 + } `json:"file"` // 文件类型 +} + +// WebHookSendTempNoticeOption 机器人发送文本通知模版消息请求参数 +type WebHookSendTempNoticeOption struct { + MsgType string `json:"msgtype"` // 消息类型,此时的消息类型固定为template_card + TemplateCard TemplateCard `json:"template_card"` // 具体的模版卡片参数 +} + +// TemplateCard 具体的模版卡片参数 +type TemplateCard struct { + CardType string `json:"card_type"` // 模版卡片的模版类型,文本通知模版卡片的类型为text_notice + Source CardSource `json:"source"` // 卡片来源样式信息,不需要来源样式可不填写 + MainTitle CardTitle `json:"main_title"` // 模版卡片的主要内容,包括一级标题和标题辅助信息 + EmphasisContent CardTitle `json:"emphasis_content"` // 关键数据样式 + QuoteArea CardQuoteArea `json:"quote_area"` // 引用文献样式,建议不与关键数据共用 + SubTitleText string `json:"sub_title_text"` // 二级普通文本,建议不超过112个字。模版卡片主要内容的一级标题main_title.title和二级普通文本sub_title_text必须有一项填写 + HorizontalContentList []CardContent `json:"horizontal_content_list"` // 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6 + JumpList []JumpContent `json:"jump_list"` // 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3 + CardAction CardAction `json:"card_action"` // 整体卡片的点击跳转事件,text_notice模版卡片中该字段为必填项 +} + +// CardSource 卡片来源样式信息,不需要来源样式可不填写 +type CardSource struct { + IconURL string `json:"icon_url"` // 来源图片的url + Desc string `json:"desc"` // 来源图片的描述,建议不超过13个字 + DescColor int `json:"desc_color"` // 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色 +} + +// CardTitle 标题和标题辅助信息 +type CardTitle struct { + Title string `json:"title"` // 标题,建议不超过26个字。模版卡片主要内容的一级标题main_title.title和二级普通文本sub_title_text必须有一项填写 + Desc string `json:"desc"` // 标题辅助信息,建议不超过30个字 +} + +// CardQuoteArea 引用文献样式,建议不与关键数据共用 +type CardQuoteArea struct { + Type int `json:"type"` // 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序 + URL string `json:"url,omitempty"` // 点击跳转的url,quote_area.type是1时必填 + Appid string `json:"appid,omitempty"` // 点击跳转的小程序的appid,quote_area.type是2时必填 + Pagepath string `json:"pagepath,omitempty"` // 点击跳转的小程序的pagepath,quote_area.type是2时选填 + Title string `json:"title"` // 引用文献样式的标题 + QuoteText string `json:"quote_text"` // 引用文献样式的引用文案 +} + +// CardContent 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6 +type CardContent struct { + KeyName string `json:"keyname"` // 链接类型,0或不填代表是普通文本,1 代表跳转url,2 代表下载附件,3 代表@员工 + Value string `json:"value"` // 二级标题,建议不超过5个字 + Type int `json:"type,omitempty"` // 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过26个字 + URL string `json:"url,omitempty"` // 链接跳转的url,horizontal_content_list.type是1时必填 + MediaID string `json:"media_id,omitempty"` // 附件的media_id,horizontal_content_list.type是2时必填 + UserID string `json:"userid,omitempty"` // 被@的成员的userid,horizontal_content_list.type是3时必填 +} + +// JumpContent 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3 +type JumpContent struct { + Type int `json:"type"` // 跳转链接类型,0或不填代表不是链接,1 代表跳转url,2 代表跳转小程序 + URL string `json:"url,omitempty"` // 跳转链接的url,jump_list.type是1时必填 + Title string `json:"title"` // 跳转链接样式的文案内容,建议不超过13个字 + AppID string `json:"appid,omitempty"` // 跳转链接的小程序的appid,jump_list.type是2时必填 + PagePath string `json:"pagepath,omitempty"` // 跳转链接的小程序的pagepath,jump_list.type是2时选填 +} + +// CardAction 整体卡片的点击跳转事件,text_notice模版卡片中该字段为必填项 +type CardAction struct { + Type int `json:"type"` // 卡片跳转类型,1 代表跳转url,2 代表打开小程序。text_notice模版卡片中该字段取值范围为[1,2] + URL string `json:"url,omitempty"` // 跳转事件的url,card_action.type是1时必填 + Appid string `json:"appid,omitempty"` // 跳转事件的小程序的appid,card_action.type是2时必填 + PagePath string `json:"pagepath,omitempty"` // 跳转事件的小程序的pagepath,card_action.type是2时选填 +} diff --git a/work/work.go b/work/work.go index dd463bf..2921db3 100644 --- a/work/work.go +++ b/work/work.go @@ -9,6 +9,7 @@ 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/robot" ) // Work 企业微信 @@ -55,3 +56,8 @@ func (wk *Work) GetExternalContact() *externalcontact.Client { func (wk *Work) GetAddressList() *addresslist.Client { return addresslist.NewClient(wk.ctx) } + +// GetRobot get robot +func (wk *Work) GetRobot() *robot.Client { + return robot.NewClient(wk.ctx) +}