Ver código fonte

feat(work): 企业微信接口增加 (#617)

* feat(work): 企业微信接口增加

群机器人 查询成员信息

* 变更

* fix(fix): 修复lint 报错

* fix: 删除注释多余字符
chcthink 3 anos atrás
pai
commit
5e0c31bfa9
7 arquivos alterados com 296 adições e 1 exclusões
  1. 19 1
      doc/api/work.md
  2. 86 0
      work/addresslist/user.go
  3. 13 0
      work/oauth/oauth.go
  4. 17 0
      work/robot/client.go
  5. 29 0
      work/robot/robot.go
  6. 126 0
      work/robot/send_option.go
  7. 6 0
      work/work.go

+ 19 - 1
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

+ 86 - 0
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
+}

+ 13 - 0
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

+ 17 - 0
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,
+	}
+}

+ 29 - 0
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
+}

+ 126 - 0
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时选填
+}

+ 6 - 0
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)
+}