diff --git a/README.md b/README.md index 077a04a..7ee7f56 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ CI 会在 `go1.17` 和 Go 的当前稳定版本、上一个稳定版本上跑测 * [x] OA (**大部分支持**,见下) * [x] 会话内容存档 (**大部分支持**,见下) * [x] 企业微信登录接口 (code2Session) +* [x] 获取访问用户身份 (code2UserInfo)
通讯录管理 API @@ -193,9 +194,9 @@ CI 会在 `go1.17` 和 Go 的当前稳定版本、上一个稳定版本上跑测 - [x] 获取接待人员列表 * [x] 会话分配与消息收发 - [x] 分配客服会话 - - [ ] 接收消息和事件 - - [ ] 发送消息 - - [ ] 发送欢迎语等事件响应消息 + - [x] 接收消息和事件 + - [x] 发送消息 + - [x] 发送欢迎语等事件响应消息 * [ ] 「升级服务」配置 * [ ] 其他基础信息获取 - [ ] 获取客户基础信息 diff --git a/apis.md.go b/apis.md.go index 555e6fc..a399c00 100644 --- a/apis.md.go +++ b/apis.md.go @@ -58,6 +58,20 @@ func (c *WorkwxApp) execJSCode2Session(req reqJSCode2Session) (respJSCode2Sessio return resp, nil } +// execAuthCode2UserInfo 获取访问用户身份 +func (c *WorkwxApp) execAuthCode2UserInfo(req reqAuthCode2UserInfo) (respAuthCode2UserInfo, error) { + var resp respAuthCode2UserInfo + err := c.executeQyapiGet("/cgi-bin/auth/getuserinfo", req, &resp, true) + if err != nil { + return respAuthCode2UserInfo{}, err + } + if bizErr := resp.TryIntoErr(); bizErr != nil { + return respAuthCode2UserInfo{}, bizErr + } + + return resp, nil +} + // execUserGet 读取成员 func (c *WorkwxApp) execUserGet(req reqUserGet) (respUserGet, error) { var resp respUserGet @@ -929,7 +943,7 @@ func (c *WorkwxApp) execKfAccountUpdate(req reqKfAccountUpdate) (respKfAccountUp // execKfAccountDelete 删除客服账号 func (c *WorkwxApp) execKfAccountDelete(req reqKfAccountDelete) (respKfAccountDelete, error) { var resp respKfAccountDelete - err := c.executeQyapiGet("/cgi-bin/kf/account/del", req, &resp, true) + err := c.executeQyapiJSONPost("/cgi-bin/kf/account/del", req, &resp, true) if err != nil { return respKfAccountDelete{}, err } @@ -1037,3 +1051,45 @@ func (c *WorkwxApp) execKfServiceStateTrans(req reqKfServiceStateTrans) (respKfS return resp, nil } + +// execKfSyncMsg 读取消息 +func (c *WorkwxApp) execKfSyncMsg(req reqKfSyncMsg) (respKfSyncMsg, error) { + var resp respKfSyncMsg + err := c.executeQyapiJSONPost("/cgi-bin/kf/sync_msg", req, &resp, true) + if err != nil { + return respKfSyncMsg{}, err + } + if bizErr := resp.TryIntoErr(); bizErr != nil { + return respKfSyncMsg{}, bizErr + } + + return resp, nil +} + +// execKfSend 发送消息 +func (c *WorkwxApp) execKfSend(req reqMessage) (respMessageSend, error) { + var resp respMessageSend + err := c.executeQyapiJSONPost("/cgi-bin/kf/send_msg", req, &resp, true) + if err != nil { + return respMessageSend{}, err + } + if bizErr := resp.TryIntoErr(); bizErr != nil { + return respMessageSend{}, bizErr + } + + return resp, nil +} + +// execKfOnEventSend 发送欢迎语等事件响应消息 +func (c *WorkwxApp) execKfOnEventSend(req reqMessage) (respMessageSend, error) { + var resp respMessageSend + err := c.executeQyapiJSONPost("/cgi-bin/kf/send_msg_on_event", req, &resp, true) + if err != nil { + return respMessageSend{}, err + } + if bizErr := resp.TryIntoErr(); bizErr != nil { + return respMessageSend{}, bizErr + } + + return resp, nil +} diff --git a/docs/apis.md b/docs/apis.md index dda9288..f70358e 100644 --- a/docs/apis.md +++ b/docs/apis.md @@ -8,6 +8,7 @@ Name|Request Type|Response Type|Access Token|URL|Doc `execGetJSAPITicket`|`reqJSAPITicket`|`respJSAPITicket`|+|`GET /cgi-bin/get_jsapi_ticket`|[获取企业的jsapi_ticket](https://open.work.weixin.qq.com/api/doc/90000/90136/90506) `execGetJSAPITicketAgentConfig`|`reqJSAPITicketAgentConfig`|`respJSAPITicket`|+|`GET /cgi-bin/ticket/get`|[获取应用的jsapi_ticket](https://open.work.weixin.qq.com/api/doc/90000/90136/90506) `execJSCode2Session`|`reqJSCode2Session`|`respJSCode2Session`|+|`GET /cgi-bin/miniprogram/jscode2session`|[临时登录凭证校验code2Session](https://open.work.weixin.qq.com/api/doc/90000/90136/91507) +`execAuthCode2UserInfo`|`reqAuthCode2UserInfo`|`respAuthCode2UserInfo`|+|`GET /cgi-bin/auth/getuserinfo`|[获取访问用户身份](https://developer.work.weixin.qq.com/document/path/91023) # 成员管理 @@ -247,7 +248,7 @@ Name|Request Type|Response Type|Access Token|URL|Doc :---|------------|-------------|------------|:--|:-- `execKfAccountCreate`|`reqKfAccountCreate`|`respKfAccountCreate`|+|`POST /cgi-bin/kf/account/add`|[添加客服账号](https://developer.work.weixin.qq.com/document/path/94662) `execKfAccountUpdate`|`reqKfAccountUpdate`|`respKfAccountUpdate`|+|`POST /cgi-bin/kf/account/update`|[修改客服账号](https://developer.work.weixin.qq.com/document/path/94664) -`execKfAccountDelete`|`reqKfAccountDelete`|`respKfAccountDelete`|+|`GET /cgi-bin/kf/account/del`|[删除客服账号](https://developer.work.weixin.qq.com/document/path/94663) +`execKfAccountDelete`|`reqKfAccountDelete`|`respKfAccountDelete`|+|`POST /cgi-bin/kf/account/del`|[删除客服账号](https://developer.work.weixin.qq.com/document/path/94663) `execKfAccountList`|`reqKfAccountList`|`respKfAccountList`|+|`GET /cgi-bin/kf/account/list`|[获取客服账号列表](https://developer.work.weixin.qq.com/document/path/94661) `execAddKfContact`|`reqAddKfContact`|`respAddKfContact`|+|`POST /cgi-bin/kf/add_contact_way`|[获取客服账号链接](https://developer.work.weixin.qq.com/document/path/94665) @@ -269,3 +270,6 @@ Name|Request Type|Response Type|Access Token|URL|Doc :---|------------|-------------|------------|:--|:-- `execKfServiceStateGet`|`reqKfServiceStateGet`|`respKfServiceStateGet`|+|`POST /cgi-bin/kf/service_state/get`|[获取会话状态](https://developer.work.weixin.qq.com/document/path/94669) `execKfServiceStateTrans`|`reqKfServiceStateTrans`|`respKfServiceStateTrans`|+|`POST /cgi-bin/kf/service_state/trans`|[变更会话状态](https://developer.work.weixin.qq.com/document/path/94669) +`execKfSyncMsg`|`reqKfSyncMsg`|`respKfSyncMsg`|+|`POST /cgi-bin/kf/sync_msg`|[读取消息](https://developer.work.weixin.qq.com/document/path/94670) +`execKfSend`|`reqMessage`|`respMessageSend`|+|`POST /cgi-bin/kf/send_msg`|[发送消息](https://developer.work.weixin.qq.com/document/path/94677) +`execKfOnEventSend`|`reqMessage`|`respMessageSend`|+|`POST /cgi-bin/kf/send_msg_on_event`|[发送欢迎语等事件响应消息](https://developer.work.weixin.qq.com/document/path/95122) diff --git a/docs/kf.md b/docs/kf.md index 493e6f8..9018fd0 100644 --- a/docs/kf.md +++ b/docs/kf.md @@ -20,7 +20,7 @@ `StopType` | `stop_type` | `int` | 接待人员的接待状态为「停止接待」的子类型。0:停止接待,1:暂时挂起 `DepartmentID` | `department_id,omitempty` | `int64` | 接待人员部门的id -### `KfServicerResult` 客户群列表数据 +### `KfServicerResult` 接待人员数据 Name | JSON | Type | Doc :---------------|:--------------------------|:---------|:------------ @@ -29,7 +29,75 @@ `ErrCode` | `errcode` | `int64` | 该条记录的结果 `ErrMsg` | `errmsg` | `string` | 结果信息 +### `KfMsg` 客服消息数据 + + Name | JSON | Type | Doc +:---------------|:----------------------------|:--------------|:------------ +`MsgID` | `msgid,omitempty` | `string` | 消息ID +`OpenKfID`| `open_kfid,omitempty` | `string` |客服账号ID(msgtype为event,该字段不返回) +`ExternalUserID`| `external_userid,omitempty` | `string` |客客户UserID(msgtype为event,该字段不返回) +`SendTime` | `send_time,omitempty` | `int64` | 消息发送时间 +`Origin` | `origin,omitempty` | `int` | 消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息 +`ServicerUserID`| `servicer_userid,omitempty` | `string` |从企业微信给客户发消息的接待人员userid(即仅origin为5才返回;msgtype为event,该字段不返回) +`MsgType` | `msgtype` | `MessageType` | 消息类型 +`Text` | `text,omitempty` | `Text` | 文本消息 +`Image` | `image,omitempty` | `Image` | 图片消息 +`Link` | `link,omitempty` | `Link` | 链接消息 +`MiniProgram` | `mini_program,omitempty` | `MiniProgram` | 小程序消息 +`Event` | `event,omitempty` | `KfEvent` | 事件类型 + +### `KfEvent` 客服会话事件 + +Name|JSON|Type|Doc +:---|:--|:---|:-- +`EventType`|`event_type`|`KfEventType`|事件类型 +`OpenKfID`|`open_kfid`|`string`|客服账号ID +`ExternalUserID`|`external_userid,omitempty`|`string`|客户UserID,注意不是企业成员的帐号 +`ServicerUserID`|`servicer_userid,omitempty`|`string`|接待人员userid +`Scene`|`scene,omitempty`|`string`|用户进入会话事件特有。进入会话的场景值,获取客服账号链接开发者自定义的场景值 +`SceneParam`|`scene_param,omitempty`|`string`|用户进入会话事件特有。进入会话的自定义参数,获取客服账号链接返回的url,开发者按规范拼接的scene_param参数 +`WelcomeCode`|`welcome_code,omitempty`|`string`|用户进入会话事件特有。如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。 +`WechatChannels`|`wechat_channels,omitempty`|`KfWechatChannels`|用户进入会话事件特有。进入会话的视频号信息,从视频号进入会话才有值 +`FailMsgID`|`fail_msgid,omitempty`|`string`|消息发送失败事件特有。发送失败的消息msgid +`FailType`|`fail_type,omitempty`|`int`|消息发送失败事件特有。失败类型。0-未知原因 1-客服账号已删除 2-应用已关闭 4-会话已过期,超过48小时 5-会话已关闭 6-超过5条限制 8-主体未验证 10-用户拒收 11-企业未有成员登录企业微信App(排查方法:企业至少一个成员通过手机号验证/微信授权登录企业微信App即可)12-发送的消息为客服组件禁发的消息类型 +`Status`|`status,omitempty`|`int`|接待人员接待状态变更事件特有。状态类型。1-接待中 2-停止接待 +`StopType`|`stop_type,omitempty`|`int`|接待人员接待状态变更事件特有。接待人员的状态为「停止接待」的子类型。0:停止接待,1:暂时挂起 +`ChangeType`|`change_type,omitempty`|`KfServiceState`|会话状态变更事件特有。变更类型,均为接待人员在企业微信客户端操作触发。1-从接待池接入会话 2-转接会话 3-结束会话 4-重新接入已结束/已转接会话 +`OldServicerUserID`|`old_servicer_userid,omitempty`|`string`|会话状态变更事件特有。老的接待人员userid。仅change_type为2、3和4有值 +`NewServicerUserid`|`new_servicer_userid,omitempty`|`string`|会话状态变更事件特有。新的接待人员userid。仅change_type为1、2和4有值 +`MsgCode`|`msg_code,omitempty`|`string`|会话状态变更事件特有。用于发送事件响应消息的code,仅change_type为1和3时,会返回该字段。可用该msg_code调用发送事件响应消息接口给客户发送回复语或结束语。 +`RecallMsgID`|`recall_msgid,omitempty`|`string`|撤回消息事件特有。 撤回的消息msgid +`RejectSwitch`|`reject_switch,omitempty`|`int`|拒收客户消息变更事件特有。 拒收客户消息,1表示接待人员拒收了客户消息,0表示接待人员取消拒收客户消息 + +### `KfWechatChannels` 进入会话的视频号信息,从视频号进入会话才有值 + + Name | JSON | Type | Doc +:---------------|:--------------------------|:---------|:------------ + `NickName` | `nickname,omitempty` | `string` | 视频号名称,视频号场景值为1、2、3时返回此项 + `ShopNickName` | `shop_nickname,omitempty` | `string` | 视频号小店名称,视频号场景值为4、5时返回此项 + `Scene` | `scene` | `int64` | 视频号场景值。1:视频号主页,2:视频号直播间商品列表页,3:视频号商品橱窗页,4:视频号小店商品详情页,5:视频号小店订单页 + ```go +// KfEventType 事件类型 +type KfEventType string + +const ( + // KfEventTypeEnterSession 用户进入会话事件 + KfEventTypeEnterSession KfEventType = "enter_session" + // KfEventTypeMsgSendFail 消息发送失败事件 + KfEventTypeMsgSendFail KfEventType = "msg_send_fail" + // KfEventTypeServicerStatusChange 接待人员接待状态变更事件 + KfEventTypeServicerStatusChange KfEventType = "servicer_status_change" + // KfEventTypeSessionStatusChange 会话状态变更事件 + KfEventTypeSessionStatusChange KfEventType = "session_status_change" + // KfEventTypeUserRecallMsg 用户撤回消息事件 + KfEventTypeUserRecallMsg KfEventType = "user_recall_msg" + // KfEventTypeServicerRecallMsg 接待人员撤回消息事件 + KfEventTypeServicerRecallMsg KfEventType = "servicer_recall_msg" + // KfEventTypeRejectCustomerMsgSwitchChange 拒收客户消息变更事件 + KfEventTypeRejectCustomerMsgSwitchChange KfEventType = "reject_customer_msg_switch_change" +) + // KfServiceState 客服会话状态 // // 0 未处理 新会话接入 diff --git a/docs/rx_msg.md b/docs/rx_msg.md index c4d72e9..bb48b59 100644 --- a/docs/rx_msg.md +++ b/docs/rx_msg.md @@ -55,6 +55,9 @@ const EventTypeSysApprovalChange EventType = "sys_approval_change" // EventTypeChangeContact 通讯录回调通知 const EventTypeChangeContact EventType = "change_contact" +// EventTypeKfMsgOrEvent 客服回调通知 +const EventTypeKfMsgOrEvent EventType = "kf_msg_or_event" + // ChangeType 变更类型 type ChangeType string @@ -297,6 +300,13 @@ Name|XML|Type|Doc :---|:--|:---|:-- `EventKey`|`EventKey`|`string`|事件key +### `rxEventKfMsgOrEvent` 接受的事件消息,客服接收消息和事件 + +Name|XML|Type|Doc +:---|:--|:---|:-- +`OpenKfID`|`OpenKfId`|`string`|有新消息的客服账号。可通过sync_msg接口指定open_kfid获取此客服账号的消息 +`Token`|`Token`|`string`|调用拉取消息接口时,需要传此token,用于校验请求的合法性 + ### `rxEventUnknown` 接受的事件消息,未定义的事件类型 Name|XML|Type|Doc diff --git a/errcodes/mod.go b/errcodes/mod.go index 761d06e..46bafe9 100644 --- a/errcodes/mod.go +++ b/errcodes/mod.go @@ -5,7 +5,7 @@ package errcodes // ErrCode 错误码类型 // // 全局错误码文档: https://developer.work.weixin.qq.com/document/path/90313 -// 文档爬取时间: 2024-01-24 16:57:05 +0800 +// 文档爬取时间: 2024-01-30 16:57:13 +0800 // // NOTE: 关于错误码的名字为何如此无聊: // @@ -1041,6 +1041,16 @@ const ErrCode41095 ErrCode = 41095 // 排查方法: - const ErrCode41096 ErrCode = 41096 +// ErrCode41098 组件关联应用创建的获客链接授权给了其他组件 +// +// 排查方法: - +const ErrCode41098 ErrCode = 41098 + +// ErrCode41099 不是服务商代支付模式 +// +// 排查方法: - +const ErrCode41099 ErrCode = 41099 + // ErrCode41102 缺少菜单名 // // 排查方法: - @@ -4180,6 +4190,11 @@ const ErrCode90707 ErrCode = 90707 // 排查方法: - const ErrCode90708 ErrCode = 90708 +// ErrCode90710 日程关联的会议已经结束,无法修改 +// +// 排查方法: - +const ErrCode90710 ErrCode = 90710 + // ErrCode91040 获取ticket的类型无效 // // 排查方法: [查看帮助] diff --git a/kf.go b/kf.go index 6d09f5a..96e7a73 100644 --- a/kf.go +++ b/kf.go @@ -124,3 +124,18 @@ func (c *WorkwxApp) TransKfServiceState(openKfID, externalUserID, servicerUserID } return resp.MsgCode, nil } + +// KfSyncMsg 微信客服获取消息列表 +func (c *WorkwxApp) KfSyncMsg(openKfID, token, cursor string, limit int64, voiceFormat int) ([]KfMsg, int, string, error) { + resp, err := c.execKfSyncMsg(reqKfSyncMsg{ + OpenKfID: openKfID, + Cursor: cursor, + Token: token, + Limit: limit, + VoiceFormat: voiceFormat, + }) + if err != nil { + return nil, 0, "", err + } + return resp.MsgList, resp.HasMore, resp.NextCursor, nil +} diff --git a/kf.md.go b/kf.md.go index f9a1474..8003c69 100644 --- a/kf.md.go +++ b/kf.md.go @@ -26,7 +26,7 @@ type KfServicer struct { DepartmentID int64 `json:"department_id,omitempty"` } -// KfServicerResult 客户群列表数据 +// KfServicerResult 接待人员数据 type KfServicerResult struct { // UserID 接待人员的userid UserID string `json:"userid,omitempty"` @@ -38,6 +38,104 @@ type KfServicerResult struct { ErrMsg string `json:"errmsg"` } +// KfMsg 客服消息数据 +type KfMsg struct { + // MsgID 消息ID + MsgID string `json:"msgid,omitempty"` + // OpenKfID 客服账号ID(msgtype为event,该字段不返回) + OpenKfID string `json:"open_kfid,omitempty"` + // ExternalUserID 客客户UserID(msgtype为event,该字段不返回) + ExternalUserID string `json:"external_userid,omitempty"` + // SendTime 消息发送时间 + SendTime int64 `json:"send_time,omitempty"` + // Origin 消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息 + Origin int `json:"origin,omitempty"` + // ServicerUserID 从企业微信给客户发消息的接待人员userid(即仅origin为5才返回;msgtype为event,该字段不返回) + ServicerUserID string `json:"servicer_userid,omitempty"` + // MsgType 消息类型 + MsgType MessageType `json:"msgtype"` + // Text 文本消息 + Text Text `json:"text,omitempty"` + // Image 图片消息 + Image Image `json:"image,omitempty"` + // Link 链接消息 + Link Link `json:"link,omitempty"` + // MiniProgram 小程序消息 + MiniProgram MiniProgram `json:"mini_program,omitempty"` + // Event 事件类型 + Event KfEvent `json:"event,omitempty"` +} + +// KfEvent 客服会话事件 +type KfEvent struct { + // EventType 事件类型 + EventType KfEventType `json:"event_type"` + // OpenKfID 客服账号ID + OpenKfID string `json:"open_kfid"` + // ExternalUserID 客户UserID,注意不是企业成员的帐号 + ExternalUserID string `json:"external_userid,omitempty"` + // ServicerUserID 接待人员userid + ServicerUserID string `json:"servicer_userid,omitempty"` + // Scene 用户进入会话事件特有。进入会话的场景值,获取客服账号链接开发者自定义的场景值 + Scene string `json:"scene,omitempty"` + // SceneParam 用户进入会话事件特有。进入会话的自定义参数,获取客服账号链接返回的url,开发者按规范拼接的scene_param参数 + SceneParam string `json:"scene_param,omitempty"` + // WelcomeCode 用户进入会话事件特有。如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。 + WelcomeCode string `json:"welcome_code,omitempty"` + // WechatChannels 用户进入会话事件特有。进入会话的视频号信息,从视频号进入会话才有值 + WechatChannels KfWechatChannels `json:"wechat_channels,omitempty"` + // FailMsgID 消息发送失败事件特有。发送失败的消息msgid + FailMsgID string `json:"fail_msgid,omitempty"` + // FailType 消息发送失败事件特有。失败类型。0-未知原因 1-客服账号已删除 2-应用已关闭 4-会话已过期,超过48小时 5-会话已关闭 6-超过5条限制 8-主体未验证 10-用户拒收 11-企业未有成员登录企业微信App(排查方法:企业至少一个成员通过手机号验证/微信授权登录企业微信App即可)12-发送的消息为客服组件禁发的消息类型 + FailType int `json:"fail_type,omitempty"` + // Status 接待人员接待状态变更事件特有。状态类型。1-接待中 2-停止接待 + Status int `json:"status,omitempty"` + // StopType 接待人员接待状态变更事件特有。接待人员的状态为「停止接待」的子类型。0:停止接待,1:暂时挂起 + StopType int `json:"stop_type,omitempty"` + // ChangeType 会话状态变更事件特有。变更类型,均为接待人员在企业微信客户端操作触发。1-从接待池接入会话 2-转接会话 3-结束会话 4-重新接入已结束/已转接会话 + ChangeType KfServiceState `json:"change_type,omitempty"` + // OldServicerUserID 会话状态变更事件特有。老的接待人员userid。仅change_type为2、3和4有值 + OldServicerUserID string `json:"old_servicer_userid,omitempty"` + // NewServicerUserid 会话状态变更事件特有。新的接待人员userid。仅change_type为1、2和4有值 + NewServicerUserid string `json:"new_servicer_userid,omitempty"` + // MsgCode 会话状态变更事件特有。用于发送事件响应消息的code,仅change_type为1和3时,会返回该字段。可用该msg_code调用发送事件响应消息接口给客户发送回复语或结束语。 + MsgCode string `json:"msg_code,omitempty"` + // RecallMsgID 撤回消息事件特有。 撤回的消息msgid + RecallMsgID string `json:"recall_msgid,omitempty"` + // RejectSwitch 拒收客户消息变更事件特有。 拒收客户消息,1表示接待人员拒收了客户消息,0表示接待人员取消拒收客户消息 + RejectSwitch int `json:"reject_switch,omitempty"` +} + +// KfWechatChannels 进入会话的视频号信息,从视频号进入会话才有值 +type KfWechatChannels struct { + // NickName 视频号名称,视频号场景值为1、2、3时返回此项 + NickName string `json:"nickname,omitempty"` + // ShopNickName 视频号小店名称,视频号场景值为4、5时返回此项 + ShopNickName string `json:"shop_nickname,omitempty"` + // Scene 视频号场景值。1:视频号主页,2:视频号直播间商品列表页,3:视频号商品橱窗页,4:视频号小店商品详情页,5:视频号小店订单页 + Scene int64 `json:"scene"` +} + +// KfEventType 事件类型 +type KfEventType string + +const ( + // KfEventTypeEnterSession 用户进入会话事件 + KfEventTypeEnterSession KfEventType = "enter_session" + // KfEventTypeMsgSendFail 消息发送失败事件 + KfEventTypeMsgSendFail KfEventType = "msg_send_fail" + // KfEventTypeServicerStatusChange 接待人员接待状态变更事件 + KfEventTypeServicerStatusChange KfEventType = "servicer_status_change" + // KfEventTypeSessionStatusChange 会话状态变更事件 + KfEventTypeSessionStatusChange KfEventType = "session_status_change" + // KfEventTypeUserRecallMsg 用户撤回消息事件 + KfEventTypeUserRecallMsg KfEventType = "user_recall_msg" + // KfEventTypeServicerRecallMsg 接待人员撤回消息事件 + KfEventTypeServicerRecallMsg KfEventType = "servicer_recall_msg" + // KfEventTypeRejectCustomerMsgSwitchChange 拒收客户消息变更事件 + KfEventTypeRejectCustomerMsgSwitchChange KfEventType = "reject_customer_msg_switch_change" +) + // KfServiceState 客服会话状态 // // 0 未处理 新会话接入 diff --git a/message.go b/message.go index aac213d..a51729e 100644 --- a/message.go +++ b/message.go @@ -207,6 +207,8 @@ func (c *WorkwxApp) SendTemplateCardMessage( // sendMessage 发送消息底层接口 // // 收件人参数如果仅设置了 `ChatID` 字段,则为【发送消息到群聊会话】接口调用; +// 收件人参数如果仅设置了 `OpenKfID` 字段,则为【客服发送消息】接口调用; +// 收件人参数如果仅设置了 `Code` 字段,则为【发送欢迎语等事件响应消息】接口调用; // 否则为单纯的【发送应用消息】接口调用。 func (c *WorkwxApp) sendMessage( recipient *Recipient, @@ -214,15 +216,18 @@ func (c *WorkwxApp) sendMessage( content map[string]interface{}, isSafe bool, ) error { - isApichatSendRequest := false + sendRequestFunc := c.execMessageSend if !recipient.isValidForMessageSend() { - if !recipient.isValidForAppchatSend() { + if recipient.isValidForAppchatSend() { + sendRequestFunc = c.execAppchatSend + } else if recipient.isValidForKfSend() { + sendRequestFunc = c.execKfSend + } else if recipient.isValidForKfOnEventSend() { + sendRequestFunc = c.execKfOnEventSend + } else { // TODO: better error return errors.New("recipient invalid for message sending") } - - // 发送给群聊 - isApichatSendRequest = true } req := reqMessage{ @@ -236,13 +241,7 @@ func (c *WorkwxApp) sendMessage( IsSafe: isSafe, } - var resp respMessageSend - var err error - if isApichatSendRequest { - resp, err = c.execAppchatSend(req) - } else { - resp, err = c.execMessageSend(req) - } + resp, err := sendRequestFunc(req) if err != nil { return err diff --git a/models.go b/models.go index 0cab206..fab42e0 100644 --- a/models.go +++ b/models.go @@ -739,6 +739,33 @@ type JSCodeSession struct { SessionKey string `json:"session_key"` } +// reqAuthCode2UserInfo 获取访问用户身份 +type reqAuthCode2UserInfo struct { + Code string +} + +var _ urlValuer = reqAuthCode2UserInfo{} + +func (x reqAuthCode2UserInfo) intoURLValues() url.Values { + return url.Values{ + "code": {x.Code}, + } +} + +// respAuthCode2UserInfo 获取访问用户身份响应 +type respAuthCode2UserInfo struct { + respCommon + AuthCodeUserInfo +} + +// AuthCodeUserInfo 访问用户身份 +type AuthCodeUserInfo struct { + UserID string `json:"userid,omitempty"` + UserTicket string `json:"user_ticket,omitempty"` + OpenID string `json:"openid,omitempty"` + ExternalUserID string `json:"external_userid,omitempty"` +} + type reqMsgAuditListPermitUser struct { MsgAuditEdition MsgAuditEdition `json:"type"` } @@ -1690,12 +1717,10 @@ type reqKfAccountDelete struct { OpenKfID string `json:"open_kfid"` } -var _ urlValuer = reqKfAccountDelete{} +var _ bodyer = reqKfAccountDelete{} -func (x reqKfAccountDelete) intoURLValues() url.Values { - return url.Values{ - "open_kfid": {x.OpenKfID}, - } +func (x reqKfAccountDelete) intoBody() ([]byte, error) { + return marshalIntoJSONBody(x) } // respKfAccountDelete 删除客服账号 响应 @@ -1862,3 +1887,26 @@ type respKfServiceStateTrans struct { MsgCode string `json:"msg_code"` } + +// reqKfSyncMsg 读取消息 +type reqKfSyncMsg struct { + OpenKfID string `json:"open_kfid"` + Cursor string `json:"cursor"` + Token string `json:"token"` + Limit int64 `json:"limit"` + VoiceFormat int `json:"voice_format"` +} + +var _ bodyer = reqKfSyncMsg{} + +func (x reqKfSyncMsg) intoBody() ([]byte, error) { + return marshalIntoJSONBody(x) +} + +// respKfSyncMsg 读取消息 响应 +type respKfSyncMsg struct { + respCommon + NextCursor string `json:"next_cursor"` + HasMore int `json:"has_more"` + MsgList []KfMsg `json:"msg_list"` +} diff --git a/recipient.go b/recipient.go index f1ae146..9446157 100644 --- a/recipient.go +++ b/recipient.go @@ -10,6 +10,10 @@ type Recipient struct { TagIDs []string // ChatID 应用关联群聊ID,仅用于【发送消息到群聊会话】 ChatID string + // OpenKfID 应用关联客服ID,仅用于【客服发送消息】 + OpenKfID string + // Code 仅用于【客服发送欢迎语等事件响应消息】 + Code string } // isIndividualTargetsEmpty 对非群发收件人字段而言,是否全为空 @@ -23,6 +27,16 @@ func (x *Recipient) isIndividualTargetsEmpty() bool { // isValidForMessageSend 本结构体是否对【发送应用消息】请求有效 func (x *Recipient) isValidForMessageSend() bool { + if x.OpenKfID != "" { + // 这时候你应该用 KfSend 接口 + return false + } + + if x.Code != "" { + // 这时候你应该用 KfOnEventSend 接口 + return false + } + if x.ChatID != "" { // 这时候你应该用 AppchatSend 接口 return false @@ -43,9 +57,38 @@ func (x *Recipient) isValidForMessageSend() bool { // isValidForAppchatSend 本结构体是否对【发送消息到群聊会话】请求有效 func (x *Recipient) isValidForAppchatSend() bool { + if x.OpenKfID != "" { + // 这时候你应该用 KfSend 接口 + return false + } + + if x.Code != "" { + // 这时候你应该用 KfOnEventSend 接口 + return false + } + if !x.isIndividualTargetsEmpty() { return false } return x.ChatID != "" } + +// isValidForKfSend 本结构体是否对【客服发送消息】请求有效 +func (x *Recipient) isValidForKfSend() bool { + if x.Code != "" { + // 这时候你应该用 KfOnEventSend 接口 + return false + } + + if !x.isIndividualTargetsEmpty() { + return false + } + + return x.OpenKfID != "" +} + +// isValidForKfOnEventSend 本结构体是否对【客服发送欢迎语等事件响应消息】请求有效 +func (x *Recipient) isValidForKfOnEventSend() bool { + return x.Code != "" +} diff --git a/rx_msg.go b/rx_msg.go index 6bd73fb..d3be45d 100644 --- a/rx_msg.go +++ b/rx_msg.go @@ -206,6 +206,12 @@ func (m *RxMessage) EventAppUnsubscribe() (*rxEventAppUnsubscribe, bool) { return y, ok } +// EventKfMsgOrEvent 如果消息为客服接收消息和事件,则拿出相应消息参数,否则返回 nil, false +func (m *RxMessage) EventKfMsgOrEvent() (*rxEventKfMsgOrEvent, bool) { + y, ok := m.extras.(*rxEventKfMsgOrEvent) + return y, ok +} + // EventUnknown 未定义的event类型 func (m *RxMessage) EventUnknown() (*rxEventUnknown, bool) { y, ok := m.extras.(*rxEventUnknown) diff --git a/rx_msg.md.go b/rx_msg.md.go index 6c89300..0719b13 100644 --- a/rx_msg.md.go +++ b/rx_msg.md.go @@ -61,6 +61,9 @@ const EventTypeSysApprovalChange EventType = "sys_approval_change" // EventTypeChangeContact 通讯录回调通知 const EventTypeChangeContact EventType = "change_contact" +// EventTypeKfMsgOrEvent 客服回调通知 +const EventTypeKfMsgOrEvent EventType = "kf_msg_or_event" + // ChangeType 变更类型 type ChangeType string @@ -366,6 +369,14 @@ type rxEventAppUnsubscribe struct { EventKey string `xml:"EventKey"` } +// rxEventKfMsgOrEvent 接受的事件消息,客服接收消息和事件 +type rxEventKfMsgOrEvent struct { + // OpenKfID 有新消息的客服账号。可通过sync_msg接口指定open_kfid获取此客服账号的消息 + OpenKfID string `xml:"OpenKfId"` + // Token 调用拉取消息接口时,需要传此token,用于校验请求的合法性 + Token string `xml:"Token"` +} + // rxEventUnknown 接受的事件消息,未定义的事件类型 type rxEventUnknown struct { // EventType 事件类型 diff --git a/rx_msg_extras.go b/rx_msg_extras.go index f089b94..ca22b75 100644 --- a/rx_msg_extras.go +++ b/rx_msg_extras.go @@ -178,6 +178,14 @@ func extractMessageExtras(common rxMessageCommon, body []byte) (messageKind, err return nil, err } return &x, nil + case EventTypeKfMsgOrEvent: + var x rxEventKfMsgOrEvent + err := xml.Unmarshal(body, &x) + if err != nil { + return nil, err + } + return &x, nil + default: // 返回一个未定义的事件类型 return &rxEventUnknown{EventType: string(common.Event), Raw: string(body)}, nil @@ -707,6 +715,36 @@ func (r rxEventAppUnsubscribe) formatInto(w io.Writer) { _, _ = fmt.Fprintf(w, "EventKey: %#v", r.EventKey) } +// EventKfMsgOrEvent 客服接收消息和事件 +type EventKfMsgOrEvent interface { + messageKind + + // GetOpenKfID 客服账号ID + GetOpenKfID() string + + // GetToken 调用拉取消息接口时,需要传此token,用于校验请求的合法性 + GetToken() string +} + +var _ EventKfMsgOrEvent = (*rxEventKfMsgOrEvent)(nil) + +func (r *rxEventKfMsgOrEvent) formatInto(w io.Writer) { + _, _ = fmt.Fprintf( + w, + "OpenKfID: %#v, Token: %#v", + r.OpenKfID, + r.Token, + ) +} + +func (r *rxEventKfMsgOrEvent) GetOpenKfID() string { + return r.OpenKfID +} + +func (r *rxEventKfMsgOrEvent) GetToken() string { + return r.Token +} + func (r rxEventUnknown) formatInto(w io.Writer) { _, _ = fmt.Fprintf(w, "Raw: %#v", r.Raw) } diff --git a/token.go b/token.go index f388aee..0a943c3 100644 --- a/token.go +++ b/token.go @@ -234,3 +234,12 @@ func (c *WorkwxApp) JSCode2Session(jscode string) (*JSCodeSession, error) { } return &resp.JSCodeSession, nil } + +// AuthCode2UserInfo 获取访问用户身份 +func (c *WorkwxApp) AuthCode2UserInfo(code string) (*AuthCodeUserInfo, error) { + resp, err := c.execAuthCode2UserInfo(reqAuthCode2UserInfo{Code: code}) + if err != nil { + return nil, err + } + return &resp.AuthCodeUserInfo, nil +}