From 533ede4f6643373b6ce4f858c55546a488c2af95 Mon Sep 17 00:00:00 2001 From: loser <1711788888@qq.com> Date: Tue, 27 Sep 2022 11:31:23 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 27 +- README.md | 1014 ++++++++++++++++++++- account/api.go | 305 +++++++ account/enum.go | 16 + account/types.go | 117 +++ callback/callback.go | 289 ++++++ callback/types.go | 324 +++++++ example/main.go | 63 ++ go.mod | 7 + group/api.go | 1315 +++++++++++++++++++++++++++ group/filter.go | 210 +++++ group/group.go | 383 ++++++++ group/member.go | 160 ++++ group/message.go | 221 +++++ group/types.go | 508 +++++++++++ im.go | 212 +++++ im_test.go | 1511 +++++++++++++++++++++++++++++++ internal/conv/conv.go | 108 +++ internal/core/client.go | 146 +++ internal/core/error.go | 38 + internal/entity/mesage.go | 170 ++++ internal/entity/offline_push.go | 150 +++ internal/entity/user.go | 330 +++++++ internal/enum/code.go | 16 + internal/enum/enum.go | 95 ++ internal/random/random.go | 63 ++ internal/sign/base64.go | 30 + internal/sign/sign.go | 178 ++++ internal/types/interface.go | 30 + internal/types/types.go | 171 ++++ mute/api.go | 83 ++ mute/types.go | 37 + operation/api.go | 104 +++ operation/enum.go | 51 ++ operation/types.go | 91 ++ private/api.go | 354 ++++++++ private/enum.go | 42 + private/message.go | 183 ++++ private/types.go | 192 ++++ profile/api.go | 106 +++ profile/enum.go | 54 ++ profile/profile.go | 39 + profile/types.go | 39 + push/api.go | 398 ++++++++ push/enum.go | 38 + push/message.go | 152 ++++ push/types.go | 126 +++ recentcontact/api.go | 148 +++ recentcontact/enum.go | 16 + recentcontact/types.go | 78 ++ sns/api.go | 901 ++++++++++++++++++ sns/enum.go | 90 ++ sns/friend.go | 206 +++++ sns/types.go | 301 ++++++ 54 files changed, 12011 insertions(+), 25 deletions(-) create mode 100644 account/api.go create mode 100644 account/enum.go create mode 100644 account/types.go create mode 100644 callback/callback.go create mode 100644 callback/types.go create mode 100644 example/main.go create mode 100644 go.mod create mode 100644 group/api.go create mode 100644 group/filter.go create mode 100644 group/group.go create mode 100644 group/member.go create mode 100644 group/message.go create mode 100644 group/types.go create mode 100644 im.go create mode 100644 im_test.go create mode 100644 internal/conv/conv.go create mode 100644 internal/core/client.go create mode 100644 internal/core/error.go create mode 100644 internal/entity/mesage.go create mode 100644 internal/entity/offline_push.go create mode 100644 internal/entity/user.go create mode 100644 internal/enum/code.go create mode 100644 internal/enum/enum.go create mode 100644 internal/random/random.go create mode 100644 internal/sign/base64.go create mode 100644 internal/sign/sign.go create mode 100644 internal/types/interface.go create mode 100644 internal/types/types.go create mode 100644 mute/api.go create mode 100644 mute/types.go create mode 100644 operation/api.go create mode 100644 operation/enum.go create mode 100644 operation/types.go create mode 100644 private/api.go create mode 100644 private/enum.go create mode 100644 private/message.go create mode 100644 private/types.go create mode 100644 profile/api.go create mode 100644 profile/enum.go create mode 100644 profile/profile.go create mode 100644 profile/types.go create mode 100644 push/api.go create mode 100644 push/enum.go create mode 100644 push/message.go create mode 100644 push/types.go create mode 100644 recentcontact/api.go create mode 100644 recentcontact/enum.go create mode 100644 recentcontact/types.go create mode 100644 sns/api.go create mode 100644 sns/enum.go create mode 100644 sns/friend.go create mode 100644 sns/types.go diff --git a/.gitignore b/.gitignore index adf8f72..4f8c0c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,4 @@ -# ---> Go -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work - +.idea/ +*/.DS_Store +.vscode +go.sum \ No newline at end of file diff --git a/README.md b/README.md index dc95b13..983a197 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,1013 @@ -# tencent-im +## 腾讯云IM官方API文档 -腾讯云即时IM封装SDK \ No newline at end of file +点击查看 [官方文档](https://cloud.tencent.com/product/im/developer) + +## 如何使用 + +```shell script +go get git.echol.cn/loser/tencent-im +``` + +## 调用方法 + +```go +package main + +import ( + "fmt" + + "git.echol.cn/loser/tencent-im" + "git.echol.cn/loser/tencent-im/account" +) + +func main() { + tim := im.NewIM(&im.Options{ + AppId: 1400579830, // 无效的AppId,请勿直接使用 + AppSecret: "0d2a321b087fdb8fd5ed5ea14fe0489139086eb1b03541283fc9feeab8f2bfd3", // 无效的AppSecret,请勿直接使用 + UserId: "administrator", // 管理员用户账号,请在腾讯云IM后台设置管理账号 + }) + + // 导入账号 + if err := tim.Account().ImportAccount(&account.Account{ + UserId: "test1", + Nickname: "测试账号1", + FaceUrl: "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png", + }); err != nil { + if e, ok := err.(im.Error); ok { + fmt.Println(fmt.Sprintf("import accout failed, code:%d, message:%s.", e.Code(), e.Message())) + } else { + fmt.Println(fmt.Sprintf("import accout failed:%s.", err.Error())) + } + } + + fmt.Println("import account success.") + + // 注册回调事件 + tim.Callback().Register(callback.EventAfterFriendAdd, func(ack callback.Ack, data interface{}) { + fmt.Printf("%+v", data.(callback.AfterFriendAdd)) + _ = ack.AckSuccess(0) + }) + + // 注册回调事件 + tim.Callback().Register(callback.EventAfterFriendDelete, func(ack callback.Ack, data interface{}) { + fmt.Printf("%+v", data.(callback.AfterFriendDelete)) + _ = ack.AckSuccess(0) + }) + + // 开启监听 + http.HandleFunc("/callback", func(writer http.ResponseWriter, request *http.Request) { + tim.Callback().Listen(writer, request) + }) + + // 启动服务器 + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatal("ListenAndServe: ", err) + } +} +``` + +## SDK列表 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模块名称方法说明master
账号管理 + 导入单个帐号 + Account.ImportAccount本接口用于将 App 自有帐号导入即时通信 IM 帐号系统,为该帐号创建一个对应的内部 ID,使该帐号能够使用即时通信 IM 服务。
+ 导入多个帐号 + Account.ImportAccounts本接口用于批量将 App 自有帐号导入即时通信 IM 帐号系统,为该帐号创建一个对应的内部 ID,使该帐号能够使用即时通信 IM 服务。
+ 删除单个帐号 + Account.DeleteAccount +
    +
  • 本方法拓展于“删除多个帐号(DeleteAccounts)”方法。
  • +
  • 仅支持删除套餐包类型为 IM 体验版的帐号,其他类型的账号(如:TRTC、白板、专业版、旗舰版)无法删除。
  • +
+
+ 删除多个帐号 + Account.DeleteAccounts仅支持删除套餐包类型为 IM 体验版的帐号,其他类型的账号(如:TRTC、白板、专业版、旗舰版)无法删除。
+ 查询单个帐号导入状态 + Account.CheckAccount +
    +
  • 本方法拓展于“查询多个帐号导入状态(CheckAccounts)”方法。
  • +
  • 用于查询自有帐号是否已导入即时通信 IM。
  • +
+
+ 查询多个帐号导入状态 + Account.CheckAccounts用于查询自有帐号是否已导入即时通信 IM,支持批量查询。
+ 失效帐号登录状态 + Account.KickAccount本接口适用于将 App 用户帐号的登录状态(例如 UserSig)失效。
+ 查询单个帐号在线状态 + Account.GetAccountOnlineState +
    +
  • 本方法拓展于“查询多个帐号在线状态(GetAccountsOnlineState)”方法。
  • +
  • 获取用户当前的登录状态。
  • +
+
+ 查询多个帐号在线状态 + Account.GetAccountsOnlineState获取用户当前的登录状态。
资料管理 + 设置资料 + Profile.SetProfile支持 标配资料字段自定义资料字段 的设置。
+ 拉取资料 + Profile.GetProfiles +
    +
  • 支持拉取好友和非好友的资料字段。
  • +
  • 支持拉取 标配资料字段自定义资料字段
  • +
  • 建议每次拉取的用户数不超过100,避免因回包数据量太大导致回包失败。
  • +
  • 请确保请求中的所有帐号都已导入即时通信 IM,如果请求中含有未导入即时通信 IM 的帐号,即时通信 IM 后台将会提示错误。
  • +
+
关系链管理 + 添加单个好友 + SNS.AddFriend +
    +
  • 本方法拓展于“添加多个好友(AddFriends)”方法。
  • +
  • 添加好友,仅支持添加单个好友
  • +
+
+ 添加多个好友 + SNS.AddFriends添加好友,支持批量添加好友。
+ 导入单个好友 + SNS.ImportFriend本方法拓展于“添加多个好友(ImportFriends)”方法。
+ 导入多个好友 + SNS.ImportFriends +
    +
  • 支持批量导入单向好友。
  • +
  • 往同一个用户导入好友时建议采用批量导入的方式,避免并发写导致的写冲突。
  • +
+
+ 更新单个好友 + SNS.UpdateFriend +
    +
  • 本方法拓展于“更新多个好友(UpdateFriends)”方法。
  • +
+
+ 更新多个好友 + SNS.UpdateFriends +
    +
  • 支持批量更新同一用户的多个好友的关系链数据。
  • +
  • 更新一个用户多个好友时,建议采用批量方式,避免并发写导致的写冲突。
  • +
+
+ 删除单个好友 + SNS.DeleteFriend本方法拓展于“删除多个好友(DeleteFriends)”方法。
+ 删除多个好友 + SNS.DeleteFriends删除好友,支持单向删除好友和双向删除好友。
+ 删除所有好友 + SNS.DeleteAllFriends清除指定用户的标配好友数据和自定义好友数据。
+ 校验单个好友 + SNS.CheckFriend本方法拓展于“校验多个好友(CheckFriends)”方法。
+ 校验多个好友 + SNS.CheckFriends支持批量校验好友关系。
+ 拉取好友 + SNS.FetchFriends +
    +
  • 分页拉取全量好友数据。
  • +
  • 不支持资料数据的拉取。
  • +
  • 不需要指定请求拉取的字段,默认返回全量的标配好友数据和自定义好友数据。
  • +
+
+ 拉取好友 + SNS.PullFriends +
    +
  • 本API是借助"拉取好友"API进行扩展实现
  • +
  • 分页拉取全量好友数据。
  • +
  • 不支持资料数据的拉取。
  • +
  • 不需要指定请求拉取的字段,默认返回全量的标配好友数据和自定义好友数据。
  • +
+
+ 拉取单个指定好友 + SNS.GetFriend +
    +
  • 本方法拓展于“拉取多个指定好友(GetFriends)”方法。
  • +
  • 支持拉取指定好友的好友数据和资料数据。
  • +
+
+ 拉取多个指定好友 + SNS.GetFriends +
    +
  • 支持拉取指定好友的好友数据和资料数据。
  • +
  • 建议每次拉取的好友数不超过100,避免因数据量太大导致回包失败。
  • +
+
+ 添加黑名单 + SNS.AddBlacklist添加黑名单,支持批量添加黑名单。
+ 删除黑名单 + SNS.DeleteBlacklist删除指定黑名单。
+ 拉取黑名单 + SNS.FetchBlacklist支持分页拉取所有黑名单。
+ 拉取黑名单 + SNS.PullBlacklist +
    +
  • 本API是借助"拉取黑名单"API进行扩展实现
  • +
  • 支持分页拉取所有黑名单。
  • +
+
+ 校验黑名单 + SNS.CheckBlacklist支持批量校验黑名单。
+ 添加分组 + SNS.AddGroups添加分组,支持批量添加分组,并将指定好友加入到新增分组中。
+ 删除分组 + SNS.DeleteGroups删除指定分组。
+ 拉取分组 + SNS.GetGroups拉取分组,支持指定分组以及拉取分组下的好友列表。
私聊消息 + 单发单聊消息 + Private.SendMessage +
    +
  • 管理员向帐号发消息,接收方看到消息发送者是管理员。
  • +
  • 管理员指定某一帐号向其他帐号发消息,接收方看到发送者不是管理员,而是管理员指定的帐号。
  • +
  • 该接口不会检查发送者和接收者的好友关系(包括黑名单),同时不会检查接收者是否被禁言。
  • +
  • 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。
  • +
+
+ 批量发单聊消息 + Private.SendMessages +
    +
  • 支持一次对最多500个用户进行单发消息。
  • +
  • 与单发消息相比,该接口更适用于营销类消息、系统通知 tips 等时效性较强的消息。
  • +
  • 管理员指定某一帐号向目标帐号批量发消息,接收方看到发送者不是管理员,而是管理员指定的帐号。
  • +
  • 该接口不触发回调请求。
  • +
  • 该接口不会检查发送者和接收者的好友关系(包括黑名单),同时不会检查接收者是否被禁言。
  • +
  • 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。
  • +
+
+ 导入单聊消息 + Private.ImportMessage +
    +
  • 导入历史单聊消息到即时通信 IM。
  • +
  • 平滑过渡期间,将原有即时通信实时单聊消息导入到即时通信 IM。
  • +
  • 该接口不会触发回调。
  • +
  • 该接口会根据 From_Account , To_Account ,MsgSeq , MsgRandom , MsgTimeStamp 字段的值对导入的消息进行去重。仅当这五个字段的值都对应相同时,才判定消息是重复的,消息是否重复与消息内容本身无关。
  • +
  • 重复导入的消息不会覆盖之前已导入的消息(即消息内容以首次导入的为准)。
  • +
  • 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。
  • +
+
+ 查询单聊消息 + Private.FetchMessages +
    +
  • 管理员按照时间范围查询某单聊会话的消息记录。
  • +
  • 查询的单聊会话由请求中的 From_Account 和 To_Account 指定。查询结果包含会话双方互相发送的消息,具体每条消息的发送方和接收方由每条消息里的 From_Account 和 To_Account 指定。
  • +
  • 一般情况下,请求中的 From_Account 和 To_Account 字段值互换,查询结果不变。但通过 单发单聊消息 或 批量发单聊消息 接口发送的消息,如果指定 SyncOtherMachine 值为2,则需要指定正确的 From_Account 和 To_Account 字段值才能查询到该消息。
  • +
  • 查询结果包含被撤回的消息,由消息里的 MsgFlagBits 字段标识。
  • +
  • 若想通过 REST API 撤回单聊消息 接口撤回某条消息,可先用本接口查询出该消息的 MsgKey,然后再调用撤回接口进行撤回。
  • +
  • 可查询的消息记录的时间范围取决于漫游消息存储时长,默认是7天。支持在控制台修改消息漫游时长,延长消息漫游时长是增值服务。具体请参考 漫游消息存储。
  • +
  • 若请求时间段内的消息总大小超过应答包体大小限制(目前为13K)时,则需要续拉。您可以通过应答中的 Complete 字段查看是否已拉取请求的全部消息。
  • +
+
+ 续拉取单聊消息 + Private.PullMessages +
    +
  • 本API是借助"查询单聊消息"API进行扩展实现。
  • +
  • 管理员按照时间范围查询某单聊会话的消息记录。
  • +
  • 查询的单聊会话由请求中的 From_Account 和 To_Account 指定。查询结果包含会话双方互相发送的消息,具体每条消息的发送方和接收方由每条消息里的 From_Account 和 To_Account 指定。
  • +
  • 一般情况下,请求中的 From_Account 和 To_Account 字段值互换,查询结果不变。但通过 单发单聊消息 或 批量发单聊消息 接口发送的消息,如果指定 SyncOtherMachine 值为2,则需要指定正确的 From_Account 和 To_Account 字段值才能查询到该消息。
  • +
  • 查询结果包含被撤回的消息,由消息里的 MsgFlagBits 字段标识。
  • +
  • 若想通过 REST API 撤回单聊消息 接口撤回某条消息,可先用本接口查询出该消息的 MsgKey,然后再调用撤回接口进行撤回。
  • +
  • 可查询的消息记录的时间范围取决于漫游消息存储时长,默认是7天。支持在控制台修改消息漫游时长,延长消息漫游时长是增值服务。具体请参考 漫游消息存储。
  • +
  • 若请求时间段内的消息总大小超过应答包体大小限制(目前为13K)时,则需要续拉。您可以通过应答中的 Complete 字段查看是否已拉取请求的全部消息。
  • +
+
+ 撤回单聊消息 + Private.RevokeMessage +
    +
  • 管理员撤回单聊消息。
  • +
  • 该接口可以撤回所有单聊消息,包括客户端发出的单聊消息,由 REST API 单发 和 批量发 接口发出的单聊消息。
  • +
  • 若需要撤回由客户端发出的单聊消息,您可以开通 发单聊消息之前回调 或 发单聊消息之后回调 ,通过该回调接口记录每条单聊消息的 MsgKey ,然后填在本接口的 MsgKey 字段进行撤回。您也可以通过 查询单聊消息 查询出待撤回的单聊消息的 MsgKey 后,填在本接口的 MsgKey 字段进行撤回。
  • +
  • 若需要撤回由 REST API 单发 和 批量发 接口发出的单聊消息,需要记录这些接口回包里的 MsgKey 字段以进行撤回。
  • +
  • 调用该接口撤回消息后,该条消息的离线、漫游存储,以及消息发送方和接收方的客户端的本地缓存都会被撤回。
  • +
  • 该接口可撤回的单聊消息没有时间限制,即可以撤回任何时间的单聊消息。
  • +
+
+ 设置单聊消息已读 + Private.SetMessageRead设置用户的某个单聊会话的消息全部已读。
+ 查询单聊未读消息计数 + Private.GetUnreadMessageNumApp 后台可以通过该接口查询特定账号的单聊总未读数(包含所有的单聊会话)或者单个单聊会话的未读数。
全员推送 + 设置应用属性名称 + Push.PushMessage +
    +
  • 支持全员推送。
  • +
  • 支持按用户属性推送。
  • +
  • 支持按用户标签推送。
  • +
  • 管理员推送消息,接收方看到消息发送者是管理员。
  • +
  • 管理员指定某一帐号向其他帐号推送消息,接收方看到发送者不是管理员,而是管理员指定的帐号。
  • +
  • 支持消息离线存储,不支持漫游。
  • +
  • 由于全员推送需要下发的帐号数量巨大,下发完全部帐号需要一定时间(根据帐号总数而定,一般在一分钟内)。
  • +
+
+ 设置应用属性名称 + Push.SetAttrNames每个应用可以设置自定义的用户属性,最多可以有10个。通过本接口可以设置每个属性的名称,设置完成后,即可用于按用户属性推送等。
+ 获取应用属性名称 + Push.GetAttrNames管理员获取应用属性名称。使用前请先 设置应用属性名称 。
+ 获取用户属性 + Push.GetUserAttrs获取用户属性(必须以管理员帐号调用);每次最多只能获取100个用户的属性。使用前请先 设置应用属性名称 。
+ 设置用户属性 + Push.SetUserAttrs管理员给用户设置属性。每次最多只能给100个用户设置属性。使用前请先 设置应用属性名称 。
+ 删除用户属性 + Push.DeleteUserAttrs管理员给用户删除属性。注意每次最多只能给100个用户删除属性。使用前请先 设置应用属性名称。
+ 获取用户标签 + Push.GetUserTags获取用户标签(必须以管理员帐号调用)。每次最多只能获取100个用户的标签。
+ 添加用户标签 + Push.AddUserTags +
    +
  • 管理员给用户添加标签。
  • +
  • 每次请求最多只能给100个用户添加标签,请求体中单个用户添加标签数最多为10个。
  • +
  • 单个用户可设置最大标签数为100个,若用户当前标签超过100,则添加新标签之前请先删除旧标签。
  • +
  • 单个标签最大长度为50字节。
  • +
+
+ 删除用户标签 + Push.DeleteUserTags管理员给用户删除标签。注意每次最多只能给100个用户删除标签。
+ 删除用户所有标签 + Push.DeleteUserAllTags管理员给用户删除所有标签。注意每次最多只能给100个用户删除所有标签。
全局禁言管理 + 设置全局禁言 + Mute.SetNoSpeaking +
    +
  • 设置帐号的单聊消息全局禁言。
  • +
  • 设置帐号的群组消息全局禁言。
  • +
+
+ 查询全局禁言 + Mute.GetNoSpeaking +
    +
  • 查询帐号的单聊消息全局禁言。
  • +
  • 查询帐号的群组消息全局禁言。
  • +
+
运营管理 + 拉取运营数据 + Operation.GetOperationDataApp 管理员可以通过该接口拉取最近30天的运营数据,可拉取的字段见下文可拉取的运营字段。
+ 下载最近消息记录 + Operation.GetHistoryDataApp 管理员可以通过该接口获取 App 中最近7天中某天某小时的所有单发或群组消息记录的下载地址。
+ 获取服务器IP地址 + Operation.GetIPList基于安全等考虑,您可能需要获知服务器的 IP 地址列表,以便进行相关限制。App 管理员可以通过该接口获得 SDK、第三方回调所使用到的服务器 IP 地址列表或 IP 网段信息。
群组管理 + 拉取App中的所有群组ID + Group.FetchGroupIdsApp 管理员可以通过该接口获取App中所有群组的ID。
+ 拉取App中的所有群组 + Group.FetchGroups本方法由“拉取App中的所有群组ID(FetchGroupIds)”拓展而来
+ 续拉取App中的所有群组 + Group.PullGroups本方法由“拉取App中的所有群组(FetchGroups)”拓展而来
+ 创建群组 + Group.CreateGroupApp 管理员可以通过该接口创建群组。
+ 获取单个群详细资料 + Group.GetGroup本方法由“获取多个群详细资料(GetGroups)”拓展而来
+ 获取多个群详细资料 + Group.GetGroupsApp 管理员可以根据群组 ID 获取群组的详细信息。
+ 拉取群成员详细资料 + Group.FetchMembersApp管理员可以根据群组ID获取群组成员的资料。
+ 拉取群成员详细资料 + Group.PullMembers本方法由“拉取群成员详细资料(FetchMembers)”拓展而来
+ 修改群基础资料 + Group.UpdateGroupApp管理员可以通过该接口修改指定群组的基础信息。
+ 增加群成员 + Group.AddMembersApp管理员可以通过该接口向指定的群中添加新成员。
+ 删除群成员 + Group.DeleteMembersApp管理员可以通过该接口删除群成员。
+ 修改群成员资料 + Group.UpdateMemberApp管理员可以通过该接口修改群成员资料。
+ 解散群组 + Group.DestroyGroupApp管理员通过该接口解散群。
+ 拉取用户所加入的群组 + Group.FetchMemberGroupsApp管理员可以通过本接口获取某一用户加入的群信息。默认不获取用户已加入但未激活好友工作群(Work)以及直播群(AVChatRoom)群信息。
+ 拉取用户所加入的群组 + Group.PullMemberGroups本方法由“拉取用户所加入的群组(FetchMemberGroups)”拓展而来
+ 查询用户在群组中的身份 + Group.GetRolesInGroupApp管理员可以通过该接口获取一批用户在群内的身份,即“成员角色”。
+ 批量禁言 + Group.ForbidSendMessage +
    +
  • App 管理员禁止指定群组中某些用户在一段时间内发言。
  • +
  • App 管理员取消对某些用户的禁言。
  • +
  • 被禁言用户退出群组之后再进入同一群组,禁言仍然有效。
  • +
+
+ 取消禁言 + Group.AllowSendMessage本方法由“批量禁言(ForbidSendMessage)”拓展而来
+ 获取被禁言群成员列表 + Group.GetShuttedUpMembersApp管理员可以根据群组ID获取群组中被禁言的用户列表。
+ 在群组中发送普通消息 + Group.SendMessageApp管理员可以通过该接口在群组中发送普通消息。
+ 在群组中发送系统通知 + Group.SendNotificationApp 管理员可以通过该接口在群组中发送系统通知。
+ 转让群主 + Group.ChangeGroupOwner +
    +
  • App 管理员可以通过该接口将群主身份转移给他人。
  • +
  • 没有群主的群,App 管理员可以通过此接口指定他人作为群主。
  • +
  • 新群主必须为群内成员。
  • +
+
+ 撤回单条群消息 + Group.RevokeMessage本方法由“撤回多条群消息(RevokeMessages)”拓展而来
+ 撤回多条群消息 + Group.RevokeMessagesApp 管理员通过该接口撤回指定群组的消息,消息需要在漫游有效期以内。
+ 导入群基础资料 + Group.ImportGroupApp 管理员可以通过该接口导入群组,不会触发回调、不会下发通知;当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群组数据。
+ 导入群消息 + Group.ImportMembers +
    +
  • 该 API 接口的作用是导入群组的消息,不会触发回调、不会下发通知。
  • +
  • 当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群消息数据。
  • +
+
+ 导入多个群成员 + Group.ImportMembers +
    +
  • 该 API 接口的作用是导入群组成员,不会触发回调、不会下发通知。
  • +
  • 当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群成员数据。
  • +
+
+ 设置成员未读消息计数 + Group.SetMemberUnreadMsgNum +
    +
  • App管理员使用该接口设置群组成员未读消息数,不会触发回调、不会下发通知。
  • +
  • 当App需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议设置群成员的未读消息计数。
  • +
+
+ 撤回指定用户发送的消息 + Group.RevokeMemberMessages该API接口的作用是撤回最近1000条消息中指定用户发送的消息。
+ 拉取群历史消息 + Group.FetchMessages +
    +
  • 即时通信 IM 的群消息是按 Seq 排序的,按照 server 收到群消息的顺序分配 Seq,先发的群消息 Seq 小,后发的 Seq 大。
  • +
  • 如果用户想拉取一个群的全量消息,首次拉取时不用填拉取 Seq,Server 会自动返回最新的消息,以后拉取时拉取 Seq 填上次返回的最小 Seq 减1。
  • +
  • 如果返回消息的 IsPlaceMsg 为1,表示这个 Seq 的消息或者过期、或者存储失败、或者被删除了。
  • +
+
+ 续拉取群历史消息 + Group.PullMessages本方法由“拉取群历史消息(FetchMessages)”拓展而来
+ 获取直播群在线人数 + Group.GetOnlineMemberNumApp 管理员可以根据群组 ID 获取直播群在线人数。
最近联系人 + 拉取会话列表 + RecentContact.FetchSessions支持分页拉取会话列表。
+ 拉取会话列表 + RecentContact.PullSessions +
    +
  • 本API是借助"拉取会话列表"API进行扩展实现
  • +
  • 支持分页拉取会话列表。
  • +
+
+ 删除单个会话 + RecentContact.DeleteSession删除指定会话,支持同步清理漫游消息。
\ No newline at end of file diff --git a/account/api.go b/account/api.go new file mode 100644 index 0000000..07bf09b --- /dev/null +++ b/account/api.go @@ -0,0 +1,305 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 20:07 + * @Desc: 账号管理 + */ + +package account + +import ( + "fmt" + + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + serviceAccount = "im_open_login_svc" + serviceOpenIM = "openim" + commandImportAccount = "account_import" + commandImportAccounts = "multiaccount_import" + commandDeleteAccounts = "account_delete" + commandCheckAccounts = "account_check" + commandKickAccount = "kick" + commandQueryAccountsOnlineStatus = "query_online_status" + + batchImportAccountsLimit = 100 // 导入账号限制 + batchDeleteAccountsLimit = 100 // 删除账号限制 + batchCheckAccountsLimit = 100 // 查询账号限制 +) + +type API interface { + // ImportAccount 导入单个帐号 + // 本接口用于将 App 自有帐号导入即时通信 IM 帐号系统, + // 为该帐号创建一个对应的内部 ID,使该帐号能够使用即时通信 IM 服务。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1608 + ImportAccount(account *Account) (err error) + + // ImportAccounts 导入多个帐号 + // 本接口用于批量将 App 自有帐号导入即时通信 IM 帐号系统, + // 为该帐号创建一个对应的内部 ID,使该帐号能够使用即时通信 IM 服务。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/4919 + ImportAccounts(userIds ...string) (failUserIds []string, err error) + + // DeleteAccount 删除账号 + // 本方法拓展于“删除多个帐号(DeleteAccounts)”方法。 + // 仅支持删除套餐包类型为 IM 体验版的帐号,其他类型的账号(如:TRTC、白板、专业版、旗舰版)无法删除。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/36443 + DeleteAccount(userId string) (err error) + + // DeleteAccounts 删除多个帐号 + // 仅支持删除套餐包类型为 IM 体验版的帐号,其他类型的账号(如:TRTC、白板、专业版、旗舰版)无法删除。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/36443 + DeleteAccounts(userIds ...string) (results []*DeleteResult, err error) + + // CheckAccount 查询帐号导入状态 + // 本方法拓展于“查询多个帐号导入状态(CheckAccounts)”方法。 + // 用于查询自有帐号是否已导入即时通信 IM。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/38417 + CheckAccount(userId string) (bool, error) + + // CheckAccounts 查询多个帐号导入状态 + // 用于查询自有帐号是否已导入即时通信 IM,支持批量查询。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/38417 + CheckAccounts(userIds ...string) (results []*CheckResult, err error) + + // KickAccount 使帐号登录状态失效 + // 本接口适用于将 App 用户帐号的登录状态(例如 UserSig)失效。 + // 例如,开发者判断一个用户为恶意帐号后,可以调用本接口将该用户当前的登录状态失效,这样用户使用历史 UserSig 登录即时通信 IM 会失败。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/3853 + KickAccount(userId string) (err error) + + // GetAccountOnlineState 查询帐号在线状态 + // 本方法拓展于“查询多个帐号在线状态(GetAccountsOnlineState)”方法。 + // 获取用户当前的登录状态。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2566 + GetAccountOnlineState(userId string, isNeedDetail ...bool) (*OnlineStatusResult, error) + + // GetAccountsOnlineState 查询多个帐号在线状态 + // 获取用户当前的登录状态。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2566 + GetAccountsOnlineState(userIds []string, isNeedDetail ...bool) (ret *OnlineStatusRet, err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// ImportAccount 导入单个帐号 +// 本接口用于将 App 自有帐号导入即时通信 IM 帐号系统, +// 为该帐号创建一个对应的内部 ID,使该帐号能够使用即时通信 IM 服务。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1608 +func (a *api) ImportAccount(account *Account) (err error) { + if err = a.client.Post(serviceAccount, commandImportAccount, account, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// ImportAccounts 导入多个帐号 +// 本接口用于批量将 App 自有帐号导入即时通信 IM 帐号系统, +// 为该帐号创建一个对应的内部 ID,使该帐号能够使用即时通信 IM 服务。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/4919 +func (a *api) ImportAccounts(userIds ...string) (failUserIds []string, err error) { + if c := len(userIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the userid is not set") + return + } else if c > batchImportAccountsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of imported accounts cannot exceed %d", batchImportAccountsLimit)) + return + } + + req := &importAccountsReq{UserIds: userIds} + resp := &importAccountsResp{} + + if err = a.client.Post(serviceAccount, commandImportAccounts, req, resp); err != nil { + return + } + + failUserIds = resp.FailUserIds + + return +} + +// DeleteAccount 删除账号 +// 本方法拓展于“删除多个帐号(DeleteAccounts)”方法。 +// 仅支持删除套餐包类型为 IM 体验版的帐号,其他类型的账号(如:TRTC、白板、专业版、旗舰版)无法删除。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/36443 +func (a *api) DeleteAccount(userId string) (err error) { + results, err := a.DeleteAccounts(userId) + if err != nil { + return + } + + for _, result := range results { + if result.UserId == userId && result.ResultCode != enum.SuccessCode { + return core.NewError(result.ResultCode, result.ResultInfo) + } + } + + return +} + +// DeleteAccounts 删除多个帐号 +// 仅支持删除套餐包类型为 IM 体验版的帐号,其他类型的账号(如:TRTC、白板、专业版、旗舰版)无法删除。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/36443 +func (a *api) DeleteAccounts(userIds ...string) (results []*DeleteResult, err error) { + if c := len(userIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the userid is not set") + return + } else if c > batchDeleteAccountsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of deleted accounts cannot exceed %d", batchDeleteAccountsLimit)) + return + } + + req := &deleteAccountsReq{} + resp := &deleteAccountsResp{} + + for _, userId := range userIds { + req.Deletes = append(req.Deletes, &accountItem{userId}) + } + + if err = a.client.Post(serviceAccount, commandDeleteAccounts, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// CheckAccount 查询帐号导入状态. +// 本方法拓展于“查询多个帐号导入状态(CheckAccounts)”方法。 +// 用于查询自有帐号是否已导入即时通信 IM。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/38417 +func (a *api) CheckAccount(userId string) (bool, error) { + results, err := a.CheckAccounts(userId) + if err != nil { + return false, err + } + + for _, result := range results { + if result.UserId == userId { + if result.ResultCode != enum.SuccessCode { + return false, core.NewError(result.ResultCode, result.ResultInfo) + } else { + return result.Status == ImportedStatusYes, nil + } + } + } + + return false, nil +} + +// CheckAccounts 查询多个帐号导入状态. +// 用于查询自有帐号是否已导入即时通信 IM,支持批量查询。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/38417 +func (a *api) CheckAccounts(userIds ...string) (results []*CheckResult, err error) { + if c := len(userIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the account is not set") + return + } else if c > batchCheckAccountsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of checked accounts cannot exceed %d", batchCheckAccountsLimit)) + return + } + + req := &checkAccountsReq{} + resp := &checkAccountsResp{} + + for _, userId := range userIds { + req.Checks = append(req.Checks, &accountItem{userId}) + } + + if err = a.client.Post(serviceAccount, commandCheckAccounts, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// KickAccount 失效帐号登录状态 +// 本接口适用于将 App 用户帐号的登录状态(例如 UserSig)失效。 +// 例如,开发者判断一个用户为恶意帐号后,可以调用本接口将该用户当前的登录状态失效,这样用户使用历史 UserSig 登录即时通信 IM 会失败。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/3853 +func (a *api) KickAccount(userId string) (err error) { + if err = a.client.Post(serviceAccount, commandKickAccount, &kickAccountReq{userId}, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// GetAccountOnlineState 查询帐号在线状态 +// 本方法拓展于“查询多个帐号在线状态(GetAccountsOnlineState)”方法。 +// 获取用户当前的登录状态。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2566 +func (a *api) GetAccountOnlineState(userId string, isNeedDetail ...bool) (*OnlineStatusResult, error) { + ret, err := a.GetAccountsOnlineState([]string{userId}, isNeedDetail...) + if err != nil { + return nil, err + } + + for _, item := range ret.Errors { + if item.UserId == userId && item.ErrorCode != enum.SuccessCode { + return nil, core.NewError(item.ErrorCode, "account exception") + } + } + + for _, item := range ret.Results { + if item.UserId == userId { + return &item, nil + } + } + + return nil, nil +} + +// GetAccountsOnlineState 查询多个帐号在线状态 +// 获取用户当前的登录状态。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2566 +func (a *api) GetAccountsOnlineState(userIds []string, isNeedDetail ...bool) (ret *OnlineStatusRet, err error) { + req := &queryAccountsOnlineStatusReq{UserIds: userIds} + resp := &queryAccountsOnlineStatusResp{} + + if len(isNeedDetail) > 0 && isNeedDetail[0] { + req.IsNeedDetail = 1 + } + + if err = a.client.Post(serviceOpenIM, commandQueryAccountsOnlineStatus, req, resp); err != nil { + return + } + + ret = &OnlineStatusRet{ + Results: resp.Results, + Errors: resp.Errors, + } + + return +} diff --git a/account/enum.go b/account/enum.go new file mode 100644 index 0000000..64de044 --- /dev/null +++ b/account/enum.go @@ -0,0 +1,16 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 11:09 + * @Desc: TODO + */ + +package account + +// ImportedStatusType 导入状态 +type ImportedStatusType string + +const ( + ImportedStatusNo ImportedStatusType = "NotImported" // 未导入 + ImportedStatusYes ImportedStatusType = "Imported" // 已导入 +) diff --git a/account/types.go b/account/types.go new file mode 100644 index 0000000..6b3e951 --- /dev/null +++ b/account/types.go @@ -0,0 +1,117 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 17:36 + * @Desc: 账号管理数据类型 + */ + +package account + +import ( + "git.echol.cn/loser/tencent-im/internal/types" +) + +type ( + // Account 导入单个账号 + Account struct { + UserId string `json:"Identifier"` // (必填)用户名,长度不超过32字节 + Nickname string `json:"Nick"` // (选填)用户昵称 + FaceUrl string `json:"FaceUrl"` // (选填)用户头像 URL + } + + // 批量导入账号(参数) + importAccountsReq struct { + UserIds []string `json:"Accounts"` // (必填)用户名,单个用户名长度不超过32字节,单次最多导入100个用户名 + } + + // 批量导入账号(响应) + importAccountsResp struct { + types.ActionBaseResp + FailUserIds []string `json:"FailAccounts"` // 导入失败的帐号列表 + } + + // 账号项 + accountItem struct { + UserId string `json:"UserID"` // 帐号的UserID + } + + // 删除多个帐号(请求) + deleteAccountsReq struct { + Deletes []*accountItem `json:"DeleteItem"` // 请求删除的帐号对象数组,单次请求最多支持100个帐号 + } + + // 删除多个账号(响应) + deleteAccountsResp struct { + types.ActionBaseResp + Results []*DeleteResult `json:"ResultItem"` + } + + // DeleteResult 删除多个账号结果项 + DeleteResult struct { + ResultCode int `json:"ResultCode"` // 单个帐号的错误码,0表示成功,非0表示失败 + ResultInfo string `json:"ResultInfo"` // 单个帐号删除失败时的错误描述信息 + UserId string `json:"UserID"` // 请求删除的帐号的 UserID + } + + // 查询多个帐号(请求) + checkAccountsReq struct { + Checks []*accountItem `json:"CheckItem"` // (必填)请求检查的帐号对象数组,单次请求最多支持100个帐号 + } + + // 查询多个帐号(响应) + checkAccountsResp struct { + types.ActionBaseResp + Results []*CheckResult `json:"ResultItem"` // 检测结果 + } + + // CheckResult 检测结果 + CheckResult struct { + UserId string `json:"UserID"` // 请求检查的帐号的 UserID + Status ImportedStatusType `json:"AccountStatus"` // 单个帐号的导入状态:Imported 表示已导入,NotImported 表示未导入 + ResultCode int `json:"ResultCode"` // 单个帐号的检查结果:0表示成功,非0表示失败 + ResultInfo string `json:"ResultInfo"` // 单个帐号检查失败时的错误描述信息 + } + + // 失效帐号登录状态(请求) + kickAccountReq struct { + UserId string `json:"Identifier"` // (必填)用户名 + } + + // 查询帐号在线状态(请求) + queryAccountsOnlineStatusReq struct { + UserIds []string `json:"To_Account"` // (必填)需要查询这些 UserID 的登录状态,一次最多查询500个 UserID 的状态 + IsNeedDetail int `json:"IsNeedDetail"` // (选填)是否需要返回详细的登录平台信息。0表示不需要,1表示需要 + } + + // 查询帐号在线状态(响应) + queryAccountsOnlineStatusResp struct { + types.ActionBaseResp + Results []OnlineStatusResult `json:"QueryResult"` // 用户在线状态结构化信息 + Errors []OnlineStatusError `json:"ErrorList"` // 状态查询失败的帐号列表,在此列表中的目标帐号,状态查询失败或目标帐号不存在。若状态全部查询成功,则 ErrorList 为空 + } + + // OnlineStatusRet 在线状态结果 + OnlineStatusRet struct { + Results []OnlineStatusResult // 用户在线状态结构化信息 + Errors []OnlineStatusError // 状态查询失败的帐号列表,在此列表中的目标帐号,状态查询失败或目标帐号不存在。若状态全部查询成功,则 ErrorList 为空 + } + + // OnlineStatusPlatform 详细的登录平台信息 + OnlineStatusPlatform struct { + Platform string `json:"Platform"` // 登录的平台类型。可能的返回值有:"iPhone", "Android", "Web", "PC", "iPad", "Mac"。 + Status string `json:"Status"` // 该登录平台的状态 + } + + // OnlineStatusResult 用户在线状态结构化信息项 + OnlineStatusResult struct { + UserId string `json:"To_Account"` // 用户的 UserID + Status string `json:"Status"` // 用户状态,前台运行状态(Online)、后台运行状态(PushOnline)、未登录状态(Offline) + Detail []OnlineStatusPlatform `json:"Detail"` // 详细的登录平台信息 + } + + // OnlineStatusError 状态查询失败的帐号项 + OnlineStatusError struct { + UserId string `json:"To_Account"` // 状态查询失败的目标帐号 + ErrorCode int `json:"ErrorCode"` // 状态查询失败的错误码,若目标帐号的错误码为70107,表示该帐号不存在 + } +) diff --git a/callback/callback.go b/callback/callback.go new file mode 100644 index 0000000..6433307 --- /dev/null +++ b/callback/callback.go @@ -0,0 +1,289 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 14:24 + * @Desc: TODO + */ + +package callback + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strconv" + "sync" +) + +const ( + commandStateChange = "State.StateChange" + commandBeforeFriendAdd = "Sns.CallbackPrevFriendAdd" + commandBeforeFriendResponse = "Sns.CallbackPrevFriendResponse" + commandAfterFriendAdd = "Sns.CallbackFriendAdd" + commandAfterFriendDelete = "Sns.CallbackFriendDelete" + commandAfterBlacklistAdd = "Sns.CallbackBlackListAdd" + commandAfterBlacklistDelete = "Sns.CallbackBlackListDelete" + commandBeforePrivateMessageSend = "C2C.CallbackBeforeSendMsg" + commandAfterPrivateMessageSend = "C2C.CallbackAfterSendMsg" + commandAfterPrivateMessageReport = "C2C.CallbackAfterMsgReport" + commandAfterPrivateMessageRevoke = "C2C.CallbackAfterMsgWithDraw" + commandBeforeGroupCreate = "Group.CallbackBeforeCreateGroup" + commandAfterGroupCreate = "Group.CallbackAfterCreateGroup" + commandBeforeApplyJoinGroup = "Group.CallbackBeforeApplyJoinGroup" + commandBeforeInviteJoinGroup = "Group.CallbackBeforeInviteJoinGroup" + commandAfterNewMemberJoinGroup = "Group.CallbackAfterNewMemberJoin" + commandAfterMemberExitGroup = "Group.CallbackAfterMemberExit" + commandBeforeGroupMessageSend = "Group.CallbackBeforeSendMsg" + commandAfterGroupMessageSend = "Group.CallbackAfterSendMsg" + commandAfterGroupFull = "Group.CallbackAfterGroupFull" + commandAfterGroupDestroyed = "Group.CallbackAfterGroupDestroyed" + commandAfterGroupInfoChanged = "Group.CallbackAfterGroupInfoChanged" +) + +const ( + EventStateChange Event = iota + 1 + EventBeforeFriendAdd + EventBeforeFriendResponse + EventAfterFriendAdd + EventAfterFriendDelete + EventAfterBlacklistAdd + EventAfterBlacklistDelete + EventBeforePrivateMessageSend + EventAfterPrivateMessageSend + EventAfterPrivateMessageReport + EventAfterPrivateMessageRevoke + EventBeforeGroupCreate + EventAfterGroupCreate + EventBeforeApplyJoinGroup + EventBeforeInviteJoinGroup + EventAfterNewMemberJoinGroup + EventAfterMemberExitGroup + EventBeforeGroupMessageSend + EventAfterGroupMessageSend + EventAfterGroupFull + EventAfterGroupDestroyed + EventAfterGroupInfoChanged +) + +const ( + ackSuccessStatus = "OK" + ackFailureStatus = "FAIL" + + ackSuccessCode = 0 + ackFailureCode = 1 + + queryAppId = "SdkAppid" + queryCommand = "CallbackCommand" + queryClientId = "ClientIP" + queryOptPlatform = "OptPlatform" + queryContentType = "contenttype" +) + +type ( + Event int + EventHandlerFunc func(ack Ack, data interface{}) + Options struct { + SdkAppId int + } + + Callback interface { + // Register 注册事件 + Register(event Event, handler EventHandlerFunc) + // Listen 监听事件 + Listen(w http.ResponseWriter, r *http.Request) + } + + callback struct { + appId int + mu sync.Mutex + handlers map[Event]EventHandlerFunc + } + + Ack interface { + // Ack 应答 + Ack(resp interface{}) error + // AckFailure 失败应答 + AckFailure(message ...string) error + // AckSuccess 成功应答 + AckSuccess(code int, message ...string) error + } + + ack struct { + w http.ResponseWriter + } +) + +func NewCallback(appId int) Callback { + return &callback{ + appId: appId, + handlers: make(map[Event]EventHandlerFunc), + } +} + +// Register 注册事件 +func (c *callback) Register(event Event, handler EventHandlerFunc) { + c.mu.Lock() + c.handlers[event] = handler + c.mu.Unlock() +} + +// Listen 监听事件 +func (c *callback) Listen(w http.ResponseWriter, r *http.Request) { + a := newAck(w) + + appId, ok := c.GetQuery(r, queryAppId) + if !ok || appId != strconv.Itoa(c.appId) { + _ = a.AckFailure("invalid sdk appId") + return + } + + command, ok := c.GetQuery(r, queryCommand) + if !ok { + _ = a.AckFailure("invalid callback command") + return + } + + body, err := ioutil.ReadAll(r.Body) + _ = r.Body.Close() + if err != nil { + _ = a.AckFailure(err.Error()) + return + } + + if event, data, err := c.parseCommand(command, body); err != nil { + _ = a.AckFailure(err.Error()) + } else { + if fn, ok := c.handlers[event]; ok { + fn(a, data) + return + } else { + _ = a.AckSuccess(ackSuccessCode) + } + } +} + +// parseCommand parse command and body package. +func (c *callback) parseCommand(command string, body []byte) (event Event, data interface{}, err error) { + switch command { + case commandStateChange: + event = EventStateChange + data = &StateChange{} + case commandBeforeFriendAdd: + event = EventBeforeFriendAdd + data = &BeforeFriendAdd{} + case commandBeforeFriendResponse: + event = EventBeforeFriendResponse + data = &BeforeFriendResponse{} + case commandAfterFriendAdd: + event = EventAfterFriendAdd + data = &AfterFriendAdd{} + case commandAfterFriendDelete: + event = EventAfterFriendDelete + data = &AfterFriendDelete{} + case commandAfterBlacklistAdd: + event = EventAfterBlacklistAdd + data = &AfterBlacklistAdd{} + case commandAfterBlacklistDelete: + event = EventAfterBlacklistDelete + data = &AfterBlacklistDelete{} + case commandBeforePrivateMessageSend: + event = EventBeforePrivateMessageSend + data = &BeforePrivateMessageSend{} + case commandAfterPrivateMessageSend: + event = EventAfterPrivateMessageSend + data = &AfterPrivateMessageSend{} + case commandAfterPrivateMessageReport: + event = EventAfterPrivateMessageReport + data = &AfterPrivateMessageReport{} + case commandAfterPrivateMessageRevoke: + event = EventAfterPrivateMessageRevoke + data = &AfterPrivateMessageRevoke{} + case commandBeforeGroupCreate: + event = EventBeforeGroupCreate + data = &BeforeGroupCreate{} + case commandAfterGroupCreate: + event = EventAfterGroupCreate + data = &AfterGroupCreate{} + case commandBeforeApplyJoinGroup: + event = EventBeforeApplyJoinGroup + data = &BeforeApplyJoinGroup{} + case commandBeforeInviteJoinGroup: + event = EventBeforeInviteJoinGroup + data = &BeforeInviteJoinGroup{} + case commandAfterNewMemberJoinGroup: + event = EventAfterNewMemberJoinGroup + data = &AfterNewMemberJoinGroup{} + case commandAfterMemberExitGroup: + event = EventAfterMemberExitGroup + data = &AfterMemberExitGroup{} + case commandBeforeGroupMessageSend: + event = EventBeforeGroupMessageSend + data = &BeforeGroupMessageSend{} + case commandAfterGroupMessageSend: + event = EventAfterGroupMessageSend + data = &AfterGroupMessageSend{} + case commandAfterGroupFull: + event = EventAfterGroupFull + data = &AfterGroupFull{} + case commandAfterGroupDestroyed: + event = EventAfterGroupDestroyed + data = &AfterGroupDestroyed{} + case commandAfterGroupInfoChanged: + event = EventAfterGroupInfoChanged + data = &AfterGroupInfoChanged{} + default: + return 0, nil, errors.New("invalid callback command") + } + + if err = json.Unmarshal(body, &data); err != nil { + return 0, nil, err + } + + return event, data, nil +} + +// GetQuery 获取查询参数 +func (c *callback) GetQuery(r *http.Request, key string) (string, bool) { + if values, ok := r.URL.Query()[key]; ok { + return values[0], ok + } else { + return "", false + } +} + +func newAck(w http.ResponseWriter) Ack { + return &ack{w} +} + +// Ack 应答 +func (a *ack) Ack(resp interface{}) error { + b, _ := json.Marshal(resp) + a.w.WriteHeader(http.StatusOK) + _, err := a.w.Write(b) + return err +} + +// AckFailure 应答失败 +func (a *ack) AckFailure(message ...string) error { + resp := BaseResp{} + resp.ActionStatus = ackFailureStatus + resp.ErrorCode = ackFailureCode + if len(message) > 0 { + resp.ErrorInfo = message[0] + } + + return a.Ack(resp) +} + +// AckSuccess 应答成功 +func (a *ack) AckSuccess(code int, message ...string) error { + resp := BaseResp{} + resp.ActionStatus = ackSuccessStatus + resp.ErrorCode = code + if len(message) > 0 { + resp.ErrorInfo = message[0] + } + + return a.Ack(resp) +} diff --git a/callback/types.go b/callback/types.go new file mode 100644 index 0000000..2c131e7 --- /dev/null +++ b/callback/types.go @@ -0,0 +1,324 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 16:36 + * @Desc: Callback request struct defined. + */ + +package callback + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + BaseResp struct { + ErrorCode int `json:"ErrorCode"` + ErrorInfo string `json:"ErrorInfo"` + ActionStatus string `json:"ActionStatus"` + } + + // StateChange 状态变更回调 + StateChange struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + EventTime int64 `json:"EventTime"` // 触发本次回调的时间戳,单位为毫秒 + Info struct { + UserId string `json:"To_Account"` // 用户 UserID + Action string `json:"Action"` // 用户上线或者下线的动作,Login 表示上线(TCP 建立),Logout 表示下线(TCP 断开),Disconnect 表示网络断开(TCP 断开) + Reason string `json:"Reason"` // 用户上下线触发的原因 + } `json:"Info"` // 用户上下线的信息 + KickedDevice []struct { + Platform string `json:"Platform"` // 被踢下线的设备的平台类型,可能的取值有"iOS", "Android", "Web", "Windows", "iPad", "Mac", "Linux"。 + } `json:"KickedDevice"` // 此字段表示其他被踢下线的设备的信息 + } + + // BeforeFriendAdd 添加好友之前回调 + BeforeFriendAdd struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + EventTime int64 `json:"EventTime"` // 触发本次回调的时间戳,单位为毫秒 + RequesterUserId string `json:"Requester_Account"` // 请求发起方的 UserID + FromUserId string `json:"From_Account"` // 请求添加好友的用户的 UserID(A添加B为好友中的A) + AddType string `json:"AddType"` // 加好友方式(默认双向加好友方式,Add_Type_Single:表示单向加好友 Add_Type_Both:表示双向加好友) + ForceAddFlags int `json:"ForceAddFlags"` // 管理员强制加好友标记(1:表示强制加好友 0:表示常规加好友方式) + Friends []struct { + ToAccount string `json:"To_Account"` // 请求添加的用户的 UserID + Remark string `json:"Remark"` // From_Account 对 To_Account 设置的好友备注 + GroupName string `json:"GroupName"` // From_Account 对 To_Account 设置的好友分组 + AddSource string `json:"AddSource"` // 加好友来源 + AddWording string `json:"AddWording"` // 加好友附言 + } `json:"FriendItem"` // 加好友请求的参数 + } + + // BeforeFriendAddResp 添加好友之前回调应答 + BeforeFriendAddResp struct { + BaseResp + Results []*BeforeFriendAddResult `json:"ResultItem"` // App 后台的处理结果 + } + + // BeforeFriendAddResult App后台的处理结果 + BeforeFriendAddResult struct { + UserId string `json:"To_Account"` // (必填)请求添加的用户的 UserID + ResultCode int `json:"ResultCode"` // (必填)错误码:0表示允许加好友; 非0值表示不允许加好友; 如果不允许加好友,请将错误码设置在[38000, 39000] + ResultInfo string `json:"ResultInfo"` // (必填)错误信息 + } + + // BeforeFriendResponse 添加好友回应之前回调 + BeforeFriendResponse struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + EventTime int64 `json:"EventTime"` // 触发本次回调的时间戳,单位为毫秒 + RequesterUserId string `json:"Requester_Account"` // 请求发起方的 UserID + FromUserId string `json:"From_Account"` // 请求加好友回应的用户的 UserID + Friends []struct { + ToAccount string `json:"To_Account"` // 请求回应的用户的 UserID + Remark string `json:"Remark"` // From_Account 对 To_Account 设置的好友备注 + TagName string `json:"TagName"` // From_Account 对 To_Account 设置的好友分组 + ResponseAction string `json:"ResponseAction"` // 加好友回应方式,Response_Action_AgreeAndAdd 表示同意且添加对方为好友;Response_Action_Agree 表示同意对方加自己为好友;Response_Action_Reject 表示拒绝对方的加好友请求 + } `json:"ResponseFriendItem"` // 加好友回应请求的参数 + } + + // BeforeFriendResponseResp 添加好友之前回调应答 + BeforeFriendResponseResp struct { + BaseResp + Results []*BeforeFriendAddResult `json:"ResultItem"` // App 后台的处理结果 + } + + // BeforeFriendResponseResult App后台的处理结果 + BeforeFriendResponseResult struct { + UserId string `json:"To_Account"` // (必填)请求添加的用户的 UserID + ResultCode int `json:"ResultCode"` // (必填)错误码:0表示允许加好友; 非0值表示不允许加好友; 如果不允许加好友,请将错误码设置在[38000, 39000] + ResultInfo string `json:"ResultInfo"` // (必填)错误信息 + } + + // AfterFriendAdd 添加好友之后 + AfterFriendAdd struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + ClientCmd string `json:"ClientCmd"` // 触发回调的命令字:加好友请求,合理的取值如下:friend_add、FriendAdd; 加好友回应,合理的取值如下:friend_response、FriendResponse + AdminUserId string `json:"Admin_Account"` // 如果当前请求是后台触发的加好友请求,则该字段被赋值为管理员帐号;否则为空 + ForceFlag int `json:"ForceFlag"` // 管理员强制加好友标记:1 表示强制加好友;0 表示常规加好友方式 + PairList []struct { + FromUserId string `json:"From_Account"` // From_Account 的好友表中增加了 To_Account + ToUserId string `json:"To_Account"` // To_Account 被增加到了 From_Account 的好友表中 + InitiatorUserId string `json:"Initiator_Account"` // 发起加好友请求的用户的 UserID + } `json:"PairList"` // 成功添加的好友对 + } + + // AfterFriendDelete 删除好友之后回调 + AfterFriendDelete struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + PairList []struct { + FromUserId string `json:"From_Account"` // From_Account 的好友表中删除了 To_Account + ToUserId string `json:"To_Account"` // To_Account 从 From_Account 的好友表中删除 + } `json:"PairList"` // 成功删除的好友 + } + + // AfterBlacklistAdd 添加黑名单之后回调 + AfterBlacklistAdd struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + PairList []struct { + FromUserId string `json:"From_Account"` // From_Account 的黑名单列表中添加了 To_Account + ToUserId string `json:"To_Account"` // To_Account 被加入到 From_Account 的黑名单列表中 + } `json:"PairList"` // 成功添加的黑名单关系链对 + } + + // AfterBlacklistDelete 删除黑名单之后回调 + AfterBlacklistDelete struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + PairList []struct { + FromUserId string `json:"From_Account"` // From_Account 的黑名单列表中删除了 To_Account + ToUserId string `json:"To_Account"` // To_Account 从 From_Account 的黑名单列表中删除 + } `json:"PairList"` // 成功删除的黑名单对 + } + + // BeforePrivateMessageSend 发单聊消息之前回调 + BeforePrivateMessageSend struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + FromUserId string `json:"From_Account"` // 消息发送者 UserID + ToUserId string `json:"To_Account"` // 消息接收者 UserID + MsgSeq int `json:"MsgSeq"` // 消息序列号,用于标记该条消息(32位无符号整数) + MsgRandom int `json:"MsgRandom"` // 消息随机数,用于标记该条消息(32位无符号整数) + MsgTime int64 `json:"MsgTime"` // 消息的发送时间戳,单位为秒,单聊消息优先使用 MsgTime 进行排序,同一秒发送的消息则按 MsgSeq 排序,MsgSeq 值越大消息越靠后 + MsgKey string `json:"MsgKey"` // 该条消息的唯一标识,可根据该标识进行 REST API 撤回单聊消息 + OnlineOnlyFlag int `json:"OnlineOnlyFlag"` // 在线消息,为1,否则为0 + MsgBody []*types.MsgBody `json:"MsgBody"` // 消息体 + CloudCustomData string `json:"CloudCustomData"` // 消息自定义数据(云端保存,会发送到对端,程序卸载重装后还能拉取到) + } + + // BeforePrivateMessageSendResp 发单聊消息之前回调应答 + BeforePrivateMessageSendResp struct { + BaseResp + MsgBody []*types.MsgBody `json:"MsgBody,omitempty"` // (选填)App 修改之后的消息,如果没有,则默认使用用户发送的消息 + CloudCustomData string `json:"CloudCustomData,omitempty"` // (选填)经过 App 修改之后的消息自定义数据(云端保存,会发送到对端,程序卸载重装后还能拉取到),即时通信 IM 后台将把修改后的消息发送给接收方 + } + + // AfterPrivateMessageSend 发单聊消息之后回调 + AfterPrivateMessageSend struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + FromUserId string `json:"From_Account"` // 消息发送者 UserID + ToUserId string `json:"To_Account"` // 消息接收者 UserID + MsgSeq int `json:"MsgSeq"` // 消息序列号,用于标记该条消息(32位无符号整数) + MsgRandom int `json:"MsgRandom"` // 消息随机数,用于标记该条消息(32位无符号整数) + MsgTime int64 `json:"MsgTime"` // 消息的发送时间戳,单位为秒,单聊消息优先使用 MsgTime 进行排序,同一秒发送的消息则按 MsgSeq 排序,MsgSeq 值越大消息越靠后 + MsgKey string `json:"MsgKey"` // 该条消息的唯一标识,可根据该标识进行 REST API 撤回单聊消息 + OnlineOnlyFlag int `json:"OnlineOnlyFlag"` // 在线消息,为1,否则为0 + MsgBody []*types.MsgBody `json:"MsgBody"` // 消息体 + CloudCustomData string `json:"CloudCustomData"` // 消息自定义数据(云端保存,会发送到对端,程序卸载重装后还能拉取到) + SendMsgResult int `json:"SendMsgResult"` // 该条消息的下发结果,0表示下发成功,非0表示下发失败 + ErrorInfo string `json:"ErrorInfo"` // 该条消息下发失败的错误信息,若消息发送成功,则为"send msg succeed" + UnreadMsgNum int `json:"UnreadMsgNum"` // To_Account 未读的单聊消息总数量(包含所有的单聊会话)。若该条消息下发失败(例如被脏字过滤),该字段值为-1 + } + + // AfterPrivateMessageReport 单聊消息已读上报后回调 + AfterPrivateMessageReport struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + ReportUserId string `json:"Report_Account"` // 已读上报方 UserID + PeerUserId string `json:"Peer_Account"` // 会话对端 UserID + LastReadTime int64 `json:"LastReadTime"` // 已读时间 + UnreadMsgNum int `json:"UnreadMsgNum"` // Report_Account 未读的单聊消息总数量(包含所有的单聊会话) + } + + // AfterPrivateMessageRevoke 单聊消息撤回后回调 + AfterPrivateMessageRevoke struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + FromUserId string `json:"From_Account"` // 消息发送者 UserID + ToUserId string `json:"To_Account"` // 消息接收者 UserID + MsgKey string `json:"MsgKey"` // 消息的唯一标识 + UnreadMsgNum int `json:"UnreadMsgNum"` // To_Account 未读的单聊消息总数量(包含所有的单聊会话) + } + + // BeforeGroupCreate 创建群组之前回调 + BeforeGroupCreate struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + OperatorUserId string `json:"Operator_Account"` // 操作者 + OwnerUserId string `json:"Owner_Account"` // 群主 + Type string `json:"Type"` // 群组类型 + Name string `json:"Name"` // 请求创建的群组的名称 + CreateGroupNum int `json:"CreateGroupNum"` // 该用户已创建的同类的群组个数 + MemberList []struct { + UserId string `json:"Member_Account"` // 成员 UserID + } `json:"MemberList"` // 请求创建的群组的初始化成员列表 + } + + // AfterGroupCreate 创建群组之后回调 + AfterGroupCreate struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + OperatorUserId string `json:"Operator_Account"` // 操作者 + OwnerUserId string `json:"Owner_Account"` // 群主 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + Name string `json:"Name"` // 请求创建的群组的名称 + CreateGroupNum int `json:"CreateGroupNum"` // 该用户已创建的同类的群组个数 + MemberList []struct { + UserId string `json:"Member_Account"` // 成员 UserID + } `json:"MemberList"` // 请求创建的群组的初始化成员列表 + UserDefinedDataList []struct { + Key string `json:"Key"` + Value string `json:"Value"` + } `json:"UserDefinedDataList"` // 用户建群时的自定义字段 + } + + // BeforeApplyJoinGroup 申请入群之前回调 + BeforeApplyJoinGroup struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + RequestorUserId string `json:"Requestor_Account"` // 申请者 + } + + // BeforeInviteJoinGroup 拉人入群之前回调 + BeforeInviteJoinGroup struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + OperatorUserId string `json:"Operator_Account"` // 操作者 + MemberList []struct { + UserId string `json:"Member_Account"` // 成员 UserID + } `json:"DestinationMembers"` // 要拉入群组的 UserID 集合 + } + + // BeforeInviteJoinGroupResp 拉人入群之前回调应答 + BeforeInviteJoinGroupResp struct { + BaseResp + RefusedMemberUserIds []string `json:"RefusedMembers_Account,omitempty"` // 拒绝加入的用户列表 + } + + // AfterNewMemberJoinGroup 新成员入群之后回调 + AfterNewMemberJoinGroup struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + JoinType string `json:"JoinType"` // 入群方式:Apply(申请入群);Invited(邀请入群) + OperatorUserId string `json:"Operator_Account"` // 操作者 + MemberList []struct { + UserId string `json:"Member_Account"` // 成员 UserID + } `json:"NewMemberList"` // 新入群成员列表 + } + + // AfterMemberExitGroup 群成员离开之后回调 + AfterMemberExitGroup struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + ExitType string `json:"ExitType"` // 成员离开方式:Kicked-被踢;Quit-主动退群 + OperatorUserId string `json:"Operator_Account"` // 操作者 + MemberList []struct { + UserId string `json:"Member_Account"` // 成员 UserID + } `json:"ExitMemberList"` // 离开群的成员列表 + } + + // BeforeGroupMessageSend 群内发言之前回调 + BeforeGroupMessageSend struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + FromUserId string `json:"From_Account"` // 发送者 + OperatorUserId string `json:"Operator_Account"` // 请求的发起者 + OnlineOnlyFlag int `json:"OnlineOnlyFlag"` // 在线消息,为1,否则为0;直播群忽略此属性,为默认值0。 + MsgRandom int `json:"Random"` // 随机数 + MsgBody []*types.MsgBody `json:"MsgBody"` // 消息体 + } + + // BeforeGroupMessageSendResp 群内发言之前回调应答 + BeforeGroupMessageSendResp struct { + BaseResp + MsgBody []*types.MsgBody `json:"MsgBody,omitempty"` // (选填)App 修改之后的消息,如果没有,则默认使用用户发送的消息 + } + + // AfterGroupMessageSend 群内发言之后回调 + AfterGroupMessageSend struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + FromUserId string `json:"From_Account"` // 发送者 + OperatorUserId string `json:"Operator_Account"` // 请求的发起者 + OnlineOnlyFlag int `json:"OnlineOnlyFlag"` // 在线消息,为1,否则为0;直播群忽略此属性,为默认值0。 + MsgSeq int `json:"MsgSeq"` // 消息的序列号 + MsgRandom int `json:"Random"` // 随机数 + MsgTime int64 `json:"MsgTime"` // 消息的时间 + MsgBody []*types.MsgBody `json:"MsgBody"` // 消息体 + } + + // AfterGroupFull 群组满员之后回调 + AfterGroupFull struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + } + + // AfterGroupDestroyed 群组解散之后回调 + AfterGroupDestroyed struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + Name string `json:"Name"` // 群组名称 + OwnerUserId string `json:"Owner_Account"` // 操作者 + MemberList []struct { + UserId string `json:"Member_Account"` // 成员 UserID + } `json:"MemberList"` // 被解散的群组中的成员 + } + + // AfterGroupInfoChanged 群组资料修改之后回调 + AfterGroupInfoChanged struct { + CallbackCommand string `json:"CallbackCommand"` // 回调命令 + GroupId string `json:"GroupId"` // 群ID + Type string `json:"Type"` // 群组类型 + Notification string `json:"Notification"` // 修改后的群公告 + OperatorUserId string `json:"Operator_Account"` // 请求的发起者 + } +) diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..d9038f0 --- /dev/null +++ b/example/main.go @@ -0,0 +1,63 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/8/31 15:14 + * @Desc: TODO + */ + +package main + +import ( + "fmt" + "log" + "net/http" + + "git.echol.cn/loser/tencent-im" + "git.echol.cn/loser/tencent-im/account" + "git.echol.cn/loser/tencent-im/callback" +) + +func main() { + tim := im.NewIM(&im.Options{ + AppId: 1400579830, // 无效的AppId,请勿直接使用 + AppSecret: "0d2a321b087fdb8fd5ed5ea14fe0489139086eb1b03541283fc9feeab8f2bfd3", // 无效的AppSecret,请勿直接使用 + UserId: "administrator", // 管理员用户账号,请在腾讯云IM后台设置管理账号 + }) + + // 导入账号 + if err := tim.Account().ImportAccount(&account.Account{ + UserId: "test1", + Nickname: "测试账号1", + FaceUrl: "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png", + }); err != nil { + if e, ok := err.(im.Error); ok { + fmt.Println(fmt.Sprintf("import account failed, code:%d, message:%s.", e.Code(), e.Message())) + } else { + fmt.Println(fmt.Sprintf("import account failed:%s.", err.Error())) + } + } + + fmt.Println("import account success.") + + // 注册回调事件 + tim.Callback().Register(callback.EventAfterFriendAdd, func(ack callback.Ack, data interface{}) { + fmt.Printf("%+v", data.(callback.AfterFriendAdd)) + _ = ack.AckSuccess(0) + }) + + // 注册回调事件 + tim.Callback().Register(callback.EventAfterFriendDelete, func(ack callback.Ack, data interface{}) { + fmt.Printf("%+v", data.(callback.AfterFriendDelete)) + _ = ack.AckSuccess(0) + }) + + // 开启监听 + http.HandleFunc("/callback", func(writer http.ResponseWriter, request *http.Request) { + tim.Callback().Listen(writer, request) + }) + + // 启动服务器 + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatal("ListenAndServe: ", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7450fc8 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module git.echol.cn/loser/tencent-im + +go 1.18 + +require git.echol.cn/loser/http v0.0.3 + +require github.com/dobyte/http v0.0.2 // indirect diff --git a/group/api.go b/group/api.go new file mode 100644 index 0000000..9cf81ea --- /dev/null +++ b/group/api.go @@ -0,0 +1,1315 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 20:46 + * @Desc: Group Api Implementation. + */ + +package group + +import ( + "fmt" + + "git.echol.cn/loser/tencent-im/internal/conv" + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + serviceGroup = "group_open_http_svc" + commandFetchGroupIds = "get_appid_group_list" + commandCreateGroup = "create_group" + commandDestroyGroup = "destroy_group" + commandGetGroups = "get_group_info" + commandFetchGroupMembers = "get_group_member_info" + commandUpdateGroup = "modify_group_base_info" + commandAddGroupMembers = "add_group_member" + commandDeleteGroupMember = "delete_group_member" + commandModifyGroupMemberInfo = "modify_group_member_info" + commandFetchMemberGroups = "get_joined_group_list" + commandGetRoleInGroup = "get_role_in_group" + commandForbidSendMsg = "forbid_send_msg" + commandGetGroupShuttedUin = "get_group_shutted_uin" + commandSendGroupMsg = "send_group_msg" + commandSendGroupSystemNotification = "send_group_system_notification" + commandChangeGroupOwner = "change_group_owner" + commandRecallGroupMsg = "group_msg_recall" + commandImportGroup = "import_group" + commandImportGroupMsg = "import_group_msg" + commandImportGroupMember = "import_group_member" + commandSetUnreadMsgNum = "set_unread_msg_num" + commandDeleteGroupMsgBySender = "delete_group_msg_by_sender" + commandGetGroupSimpleMsg = "group_msg_get_simple" + commandGetOnlineMemberNum = "get_online_member_num" + + batchGetGroupsLimit = 50 // 批量获取群组限制 +) + +type API interface { + // FetchGroupIds 拉取App中的所有群组ID + // App 管理员可以通过该接口获取App中所有群组的ID。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1614 + FetchGroupIds(limit int, next int, groupType ...Type) (ret *FetchGroupIdsRet, err error) + + // FetchGroups 拉取App中的所有群组 + // 本方法由“拉取App中的所有群组ID(FetchGroupIds)”拓展而来 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1614 + FetchGroups(limit int, next int, groupTypeAndFilter ...interface{}) (ret *FetchGroupsRet, err error) + + // PullGroups 续拉取App中的所有群组 + // 本方法由“拉取App中的所有群组(FetchGroups)”拓展而来 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1614 + PullGroups(arg *PullGroupsArg, fn func(ret *FetchGroupsRet)) (err error) + + // CreateGroup 创建群组 + // App 管理员可以通过该接口创建群组。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1615 + CreateGroup(group *Group) (groupId string, err error) + + // GetGroup 获取单个群详细资料 + // 本方法由“获取多个群详细资料(GetGroups)”拓展而来 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1616 + GetGroup(groupId string, filter ...*Filter) (group *Group, err error) + + // GetGroups 获取多个群详细资料 + // App 管理员可以根据群组 ID 获取群组的详细信息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1616 + GetGroups(groupIds []string, filter ...*Filter) (groups []*Group, err error) + + // FetchMembers 拉取群成员详细资料 + // App管理员可以根据群组ID获取群组成员的资料。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1617 + FetchMembers(groupId string, limit, offset int, filter ...*Filter) (ret *FetchMembersRet, err error) + + // PullMembers 续拉取群成员详细资料 + // 本方法由“拉取群成员详细资料(FetchMembers)”拓展而来 + // App管理员可以根据群组ID获取群组成员的资料。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1617 + PullMembers(arg *PullMembersArg, fn func(ret *FetchMembersRet)) (err error) + + // UpdateGroup 修改群基础资料 + // App管理员可以通过该接口修改指定群组的基础信息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1620 + UpdateGroup(group *Group) (err error) + + // AddMembers 增加群成员 + // App管理员可以通过该接口向指定的群中添加新成员。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1621 + AddMembers(groupId string, userIds []string, silence ...bool) (results []AddMembersResult, err error) + + // DeleteMembers 删除群成员 + // App管理员可以通过该接口删除群成员。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1622 + DeleteMembers(groupId string, userIds []string, reasonAndSilence ...interface{}) (err error) + + // UpdateMember 修改群成员资料 + // App管理员可以通过该接口修改群成员资料。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1623 + UpdateMember(groupId string, member *Member) (err error) + + // DestroyGroup 解散群组 + // App管理员通过该接口解散群。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1624 + DestroyGroup(groupId string) (err error) + + // FetchMemberGroups 拉取用户所加入的群组 + // App管理员可以通过本接口获取某一用户加入的群信息。默认不获取用户已加入但未激活好友工作群(Work)以及直播群(AVChatRoom)群信息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1625 + FetchMemberGroups(arg *FetchMemberGroupsArg) (ret *FetchMemberGroupsRet, err error) + + // PullMemberGroups 续拉取用户所加入的群组 + // 本方法由“拉取用户所加入的群组(FetchMemberGroups)”拓展而来 + // App管理员可以通过本接口获取某一用户加入的群信息。默认不获取用户已加入但未激活好友工作群(Work)以及直播群(AVChatRoom)群信息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1625 + PullMemberGroups(arg *PullMemberGroupsArg, fn func(ret *FetchMemberGroupsRet)) (err error) + + // GetRolesInGroup 查询用户在群组中的身份 + // App管理员可以通过该接口获取一批用户在群内的身份,即“成员角色”。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1626 + GetRolesInGroup(groupId string, userIds []string) (memberRoles map[string]string, err error) + + // ForbidSendMessage 批量禁言 + // App 管理员禁止指定群组中某些用户在一段时间内发言。 + // App 管理员取消对某些用户的禁言。 + // 被禁言用户退出群组之后再进入同一群组,禁言仍然有效。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1627 + ForbidSendMessage(groupId string, userIds []string, shutUpTime int64) (err error) + + // AllowSendMessage 取消禁言 + // 本方法由“批量禁言(ForbidSendMessage)”拓展而来 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1627 + AllowSendMessage(groupId string, userIds []string) (err error) + + // GetShuttedUpMembers 获取被禁言群成员列表 + // App管理员可以根据群组ID获取群组中被禁言的用户列表。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2925 + GetShuttedUpMembers(groupId string) (shuttedUps map[string]int64, err error) + + // SendMessage 在群组中发送普通消息 + // App管理员可以通过该接口在群组中发送普通消息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1629 + SendMessage(groupId string, message *Message) (ret *SendMessageRet, err error) + + // SendNotification 在群组中发送系统通知 + // App 管理员可以通过该接口在群组中发送系统通知。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1630 + SendNotification(groupId, content string, userId ...string) (err error) + + // ChangeGroupOwner 转让群主 + // App 管理员可以通过该接口将群主身份转移给他人。 + // 没有群主的群,App 管理员可以通过此接口指定他人作为群主。 + // 新群主必须为群内成员。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1633 + ChangeGroupOwner(groupId, userId string) (err error) + + // RevokeMessage 撤回单条群消息 + // 本方法由“撤回多条群消息(RevokeMessages)”拓展而来 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/12341 + RevokeMessage(groupId string, msgSeq int) (err error) + + // RevokeMessages 撤回多条群消息 + // App 管理员通过该接口撤回指定群组的消息,消息需要在漫游有效期以内。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/12341 + RevokeMessages(groupId string, msgSeq ...int) (results map[int]int, err error) + + // ImportGroup 导入群基础资料 + // App 管理员可以通过该接口导入群组,不会触发回调、不会下发通知;当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群组数据。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1634 + ImportGroup(group *Group) (groupId string, err error) + + // ImportMessages 导入群消息 + // 该 API 接口的作用是导入群组的消息,不会触发回调、不会下发通知。 + // 当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群消息数据。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1635 + ImportMessages(groupId string, messages ...*Message) (results []ImportMessagesResult, err error) + + // ImportMembers 导入多个群成员 + // 该 API 接口的作用是导入群组成员,不会触发回调、不会下发通知。 + // 当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群成员数据。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1636 + ImportMembers(groupId string, members ...*Member) (results []ImportMemberResult, err error) + + // SetMemberUnreadMsgNum 设置成员未读消息计数 + // App管理员使用该接口设置群组成员未读消息数,不会触发回调、不会下发通知。 + // 当App需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议设置群成员的未读消息计数。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1637 + SetMemberUnreadMsgNum(groupId, userId string, unreadMsgNum int) (err error) + + // RevokeMemberMessages 撤回指定用户发送的消息 + // 该API接口的作用是撤回最近1000条消息中指定用户发送的消息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2359 + RevokeMemberMessages(groupId, userId string) (err error) + + // FetchMessages 拉取群历史消息 + // 即时通信 IM 的群消息是按 Seq 排序的,按照 server 收到群消息的顺序分配 Seq,先发的群消息 Seq 小,后发的 Seq 大。 + // 如果用户想拉取一个群的全量消息,首次拉取时不用填拉取 Seq,Server 会自动返回最新的消息,以后拉取时拉取 Seq 填上次返回的最小 Seq 减1。 + // 如果返回消息的 IsPlaceMsg 为1,表示这个 Seq 的消息或者过期、或者存储失败、或者被删除了。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2738 + FetchMessages(groupId string, limit int, msgSeq ...int) (ret *FetchMessagesRet, err error) + + // PullMessages 续拉取群历史消息 + // 本方法由“拉取群历史消息(FetchMessages)”拓展而来 + // 即时通信 IM 的群消息是按 Seq 排序的,按照 server 收到群消息的顺序分配 Seq,先发的群消息 Seq 小,后发的 Seq 大。 + // 如果用户想拉取一个群的全量消息,首次拉取时不用填拉取 Seq,Server 会自动返回最新的消息,以后拉取时拉取 Seq 填上次返回的最小 Seq 减1。 + // 如果返回消息的 IsPlaceMsg 为1,表示这个 Seq 的消息或者过期、或者存储失败、或者被删除了。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2738 + PullMessages(groupId string, limit int, fn func(ret *FetchMessagesRet)) (err error) + + // GetOnlineMemberNum 获取直播群在线人数 + // App 管理员可以根据群组 ID 获取直播群在线人数。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/49180 + GetOnlineMemberNum(groupId string) (num int, err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// FetchGroupIds 拉取App中的所有群组ID +// App 管理员可以通过该接口获取App中所有群组的ID。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1614 +func (a *api) FetchGroupIds(limit int, next int, groupType ...Type) (ret *FetchGroupIdsRet, err error) { + req := &fetchGroupIdsReq{Limit: limit, Next: next} + + if len(groupType) > 0 { + req.Type = string(groupType[0]) + } + + resp := &fetchGroupIdsResp{} + + if err = a.client.Post(serviceGroup, commandFetchGroupIds, req, resp); err != nil { + return + } + + ret = &FetchGroupIdsRet{} + ret.Next = resp.Next + ret.Total = resp.TotalCount + ret.HasMore = ret.Next != 0 + ret.List = make([]string, 0, len(resp.GroupIdList)) + + for _, item := range resp.GroupIdList { + ret.List = append(ret.List, item.GroupId) + } + + return +} + +// FetchGroups 拉取App中的所有群组 +// 本方法由“拉取App中的所有群组ID(FetchGroupIds)”拓展而来 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1614 +func (a *api) FetchGroups(limit int, next int, groupTypeAndFilter ...interface{}) (ret *FetchGroupsRet, err error) { + if limit > batchGetGroupsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of groups id cannot exceed %d", batchGetGroupsLimit)) + return + } + + var ( + resp *FetchGroupIdsRet + filter *Filter + groupType Type + ) + + if len(groupTypeAndFilter) > 0 { + for i, val := range groupTypeAndFilter { + if i > 1 { + break + } + switch v := val.(type) { + case Type: + groupType = v + case *Filter: + filter = v + } + } + } + + if resp, err = a.FetchGroupIds(limit, next, groupType); err != nil { + return + } + + ret = &FetchGroupsRet{Next: resp.Next, Total: resp.Total, HasMore: resp.HasMore} + + if len(resp.List) > 0 { + if ret.List, err = a.GetGroups(resp.List, filter); err != nil { + return + } + } + + return +} + +// PullGroups 续拉取App中的所有群组 +// 本方法由“拉取App中的所有群组(FetchGroups)”拓展而来 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1614 +func (a *api) PullGroups(arg *PullGroupsArg, fn func(ret *FetchGroupsRet)) (err error) { + var ( + limit = arg.Limit + groupType = arg.Type + filter = arg.Filter + next int + ret *FetchGroupsRet + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchGroups(limit, next, groupType, filter) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + next = ret.Next + break + } + } + + return +} + +// CreateGroup 创建群组 +// App管理员可以通过该接口创建群组。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1615 +func (a *api) CreateGroup(group *Group) (groupId string, err error) { + if err = group.checkCreateError(); err != nil { + return + } + + req := &createGroupReq{} + req.GroupId = group.id + req.OwnerUserId = group.owner + req.Type = group.groupType + req.Name = group.name + req.FaceUrl = group.avatar + req.Introduction = group.introduction + req.Notification = group.notification + req.MaxMemberNum = group.maxMemberNum + req.ApplyJoinOption = group.applyJoinOption + + if data := group.GetAllCustomData(); data != nil { + req.AppDefinedData = make([]*customDataItem, 0, len(data)) + for key, val := range data { + req.AppDefinedData = append(req.AppDefinedData, &customDataItem{ + Key: key, + Value: val, + }) + } + } + + if c := len(group.members); c > 0 { + req.MemberList = make([]*memberItem, 0, c) + + var item *memberItem + for _, member := range group.members { + if err = member.checkError(); err != nil { + return + } + + item = &memberItem{ + UserId: member.userId, + Role: member.role, + JoinTime: member.joinTime, + NameCard: member.nameCard, + AppMemberDefinedData: make([]*customDataItem, 0, len(member.GetAllCustomData())), + } + + for k, v := range member.GetAllCustomData() { + item.AppMemberDefinedData = append(item.AppMemberDefinedData, &customDataItem{ + Key: k, + Value: v, + }) + } + + req.MemberList = append(req.MemberList, item) + } + } + + resp := &createGroupResp{} + + if err = a.client.Post(serviceGroup, commandCreateGroup, req, resp); err != nil { + return + } else { + groupId = resp.GroupId + } + + return +} + +// GetGroup 获取单个群详细资料 +// 本方法由“获取多个群详细资料(GetGroups)”拓展而来 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1616 +func (a *api) GetGroup(groupId string, filter ...*Filter) (group *Group, err error) { + var groups []*Group + + if groups, err = a.GetGroups([]string{groupId}, filter...); err != nil { + return + } + + if len(groups) > 0 { + if err = groups[0].err; err != nil { + return + } + + group = groups[0] + } + + return +} + +// GetGroups 获取多个群详细资料 +// App 管理员可以根据群组 ID 获取群组的详细信息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1616 +func (a *api) GetGroups(groupIds []string, filters ...*Filter) (groups []*Group, err error) { + if c := len(groupIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the group's id is not set") + return + } else if c > batchGetGroupsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of group's id cannot exceed %d", batchGetGroupsLimit)) + return + } + + req := &getGroupsReq{GroupIds: groupIds} + resp := &getGroupsResp{} + + if len(filters) > 0 { + if filter := filters[0]; filter != nil { + req.ResponseFilter = &responseFilter{ + GroupBaseInfoFilter: filter.GetAllBaseInfoFilterFields(), + MemberInfoFilter: filter.GetAllMemberInfoFilterFields(), + GroupCustomDataFilter: filter.GetAllGroupCustomDataFilterFields(), + MemberCustomDataFilter: filter.GetAllMemberCustomDataFilterFields(), + } + } + } + + if err = a.client.Post(serviceGroup, commandGetGroups, req, resp); err != nil { + return + } + + groups = make([]*Group, 0, len(resp.GroupInfos)) + for _, item := range resp.GroupInfos { + group := NewGroup() + group.setError(item.ErrorCode, item.ErrorInfo) + if group.err == nil { + group.id = item.GroupId + group.name = item.Name + group.groupType = item.Type + group.owner = item.OwnerUserId + group.avatar = item.FaceUrl + group.memberNum = item.MemberNum + group.maxMemberNum = item.MaxMemberNum + group.applyJoinOption = item.ApplyJoinOption + group.createTime = item.CreateTime + group.lastInfoTime = item.LastInfoTime + group.lastMsgTime = item.LastMsgTime + group.shutUpStatus = item.ShutUpAllMember + group.nextMsgSeq = item.NextMsgSeq + + if item.AppDefinedData != nil && len(item.AppDefinedData) > 0 { + for _, v := range item.AppDefinedData { + group.SetCustomData(v.Key, v.Value) + } + } + + if item.MemberList != nil && len(item.MemberList) > 0 { + for _, m := range item.MemberList { + member := &Member{ + userId: m.UserId, + role: m.Role, + joinTime: m.JoinTime, + nameCard: m.NameCard, + msgSeq: m.MsgSeq, + msgFlag: MsgFlag(m.MsgFlag), + lastSendMsgTime: m.LastSendMsgTime, + } + + if m.AppMemberDefinedData != nil && len(m.AppMemberDefinedData) > 0 { + for _, v := range m.AppMemberDefinedData { + member.SetCustomData(v.Key, v.Value) + } + } + + group.AddMembers(member) + } + } + + groups = append(groups, group) + } + } + + return +} + +// FetchMembers 拉取群成员详细资料 +// App管理员可以根据群组ID获取群组成员的资料。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1617 +func (a *api) FetchMembers(groupId string, limit, offset int, filters ...*Filter) (ret *FetchMembersRet, err error) { + req := &fetchMembersReq{GroupId: groupId, Limit: limit, Offset: offset} + + if len(filters) > 0 { + if filter := filters[0]; filter != nil { + req.MemberInfoFilter = filter.GetAllMemberInfoFilterFields() + req.MemberRoleFilter = filter.GetAllMemberRoleFilterValues() + req.MemberCustomDataFilter = filter.GetAllMemberCustomDataFilterFields() + } + } + + resp := &fetchMembersResp{} + + if err = a.client.Post(serviceGroup, commandFetchGroupMembers, req, resp); err != nil { + return + } + + ret = &FetchMembersRet{} + ret.Total = resp.MemberNum + ret.List = make([]*Member, 0, len(resp.MemberList)) + ret.HasMore = resp.MemberNum > limit+offset + + for _, m := range resp.MemberList { + member := &Member{ + userId: m.UserId, + role: m.Role, + joinTime: m.JoinTime, + nameCard: m.NameCard, + msgSeq: m.MsgSeq, + msgFlag: MsgFlag(m.MsgFlag), + lastSendMsgTime: m.LastSendMsgTime, + } + + if m.AppMemberDefinedData != nil && len(m.AppMemberDefinedData) > 0 { + for _, v := range m.AppMemberDefinedData { + member.SetCustomData(v.Key, v.Value) + } + } + + ret.List = append(ret.List, member) + } + + return +} + +// PullMembers 续拉取群成员详细资料 +// 本方法由“拉取群成员详细资料(FetchMembers)”拓展而来 +// App管理员可以根据群组ID获取群组成员的资料。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1617 +func (a *api) PullMembers(arg *PullMembersArg, fn func(ret *FetchMembersRet)) (err error) { + var ( + offset int + ret *FetchMembersRet + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchMembers(arg.GroupId, arg.Limit, offset, arg.Filter) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + offset += arg.Limit + } + } + + return +} + +// UpdateGroup 修改群基础资料 +// App管理员可以通过该接口修改指定群组的基础信息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1620 +func (a *api) UpdateGroup(group *Group) (err error) { + if err = group.checkUpdateError(); err != nil { + return + } + + req := &updateGroupReq{} + req.GroupId = group.id + req.Name = group.name + req.FaceUrl = group.avatar + req.Introduction = group.introduction + req.Notification = group.notification + req.MaxMemberNum = group.maxMemberNum + req.ApplyJoinOption = group.applyJoinOption + req.ShutUpAllMember = group.shutUpStatus + + if data := group.GetAllCustomData(); data != nil { + req.AppDefinedData = make([]customDataItem, 0, len(data)) + for key, val := range data { + req.AppDefinedData = append(req.AppDefinedData, customDataItem{ + Key: key, + Value: val, + }) + } + } + + if err = a.client.Post(serviceGroup, commandUpdateGroup, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// AddMembers 增加群成员 +// App管理员可以通过该接口向指定的群中添加新成员。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1621 +func (a *api) AddMembers(groupId string, userIds []string, silence ...bool) (results []AddMembersResult, err error) { + req := &addMembersReq{} + req.GroupId = groupId + req.MemberList = make([]addMemberItem, 0, len(userIds)) + for _, userId := range userIds { + req.MemberList = append(req.MemberList, addMemberItem{ + UserId: userId, + }) + } + if len(silence) > 0 && silence[0] { + req.Silence = 1 + } + + resp := &addMembersResp{} + + if err = a.client.Post(serviceGroup, commandAddGroupMembers, req, resp); err != nil { + return + } + + results = resp.MemberList + + return +} + +// DeleteMembers 删除群成员 +// App管理员可以通过该接口删除群成员。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1622 +func (a *api) DeleteMembers(groupId string, userIds []string, reasonAndSilence ...interface{}) (err error) { + req := &deleteMembersReq{} + req.GroupId = groupId + req.UserIds = userIds + + if len(reasonAndSilence) > 0 { + for i, val := range reasonAndSilence { + if i > 1 { + break + } + + switch v := val.(type) { + case string: + req.Reason = v + case bool: + if v { + req.Silence = 1 + } + } + } + } + + if err = a.client.Post(serviceGroup, commandDeleteGroupMember, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// UpdateMember 修改群成员资料 +// App管理员可以通过该接口修改群成员资料。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1623 +func (a *api) UpdateMember(groupId string, member *Member) (err error) { + if err = member.checkError(); err != nil { + return + } + + req := &updateMemberReq{} + req.GroupId = groupId + req.UserId = member.userId + req.Role = member.role + req.MsgFlag = string(member.msgFlag) + req.NameCard = member.nameCard + req.ShutUpUntil = member.shutUpUntil + + if data := member.GetAllCustomData(); data != nil { + req.AppMemberDefinedData = make([]customDataItem, 0, len(data)) + for key, val := range data { + req.AppMemberDefinedData = append(req.AppMemberDefinedData, customDataItem{ + Key: key, + Value: val, + }) + } + } + + if err = a.client.Post(serviceGroup, commandModifyGroupMemberInfo, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// DestroyGroup 解散群组 +// App管理员通过该接口解散群。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1624 +func (a *api) DestroyGroup(groupId string) (err error) { + req := &destroyGroupReq{GroupId: groupId} + + if err = a.client.Post(serviceGroup, commandDestroyGroup, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// FetchMemberGroups 拉取用户所加入的群组 +// App管理员可以通过本接口获取某一用户加入的群信息。默认不获取用户已加入但未激活好友工作群(Work)以及直播群(AVChatRoom)群信息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1625 +func (a *api) FetchMemberGroups(arg *FetchMemberGroupsArg) (ret *FetchMemberGroupsRet, err error) { + req := &fetchMemberGroupsReq{UserId: arg.UserId, Limit: arg.Limit, Offset: arg.Offset, Type: arg.Type} + + if arg.Filter != nil { + req.ResponseFilter = &responseFilter{ + GroupBaseInfoFilter: arg.Filter.GetAllBaseInfoFilterFields(), + SelfInfoFilter: arg.Filter.GetAllMemberInfoFilterFields(), + } + } + + if arg.IsWithNoActiveGroups { + req.WithNoActiveGroups = 1 + } + + if arg.IsWithLiveRoomGroups { + req.WithHugeGroups = 1 + } + + resp := &fetchMemberGroupsResp{} + + if err = a.client.Post(serviceGroup, commandFetchMemberGroups, req, resp); err != nil { + return + } + + ret = &FetchMemberGroupsRet{} + ret.Total = resp.TotalCount + ret.List = make([]*Group, 0, len(resp.GroupList)) + + if arg.Limit == 0 { + ret.HasMore = false + } else { + ret.HasMore = arg.Limit+arg.Offset < resp.TotalCount + } + + for _, item := range resp.GroupList { + group := NewGroup() + group.id = item.GroupId + group.name = item.Name + group.groupType = item.Type + group.owner = item.OwnerUserId + group.avatar = item.FaceUrl + group.memberNum = item.MemberNum + group.maxMemberNum = item.MaxMemberNum + group.applyJoinOption = item.ApplyJoinOption + group.createTime = item.CreateTime + group.lastInfoTime = item.LastInfoTime + group.lastMsgTime = item.LastMsgTime + group.shutUpStatus = item.ShutUpAllMember + group.nextMsgSeq = item.NextMsgSeq + + if item.AppDefinedData != nil && len(item.AppDefinedData) > 0 { + for _, v := range item.AppDefinedData { + group.SetCustomData(v.Key, v.Value) + } + } + + if item.MemberInfo != nil { + member := &Member{ + userId: arg.UserId, + role: item.MemberInfo.Role, + joinTime: item.MemberInfo.JoinTime, + nameCard: item.MemberInfo.NameCard, + msgSeq: item.MemberInfo.MsgSeq, + msgFlag: MsgFlag(item.MemberInfo.MsgFlag), + lastSendMsgTime: item.MemberInfo.LastSendMsgTime, + } + + if item.MemberInfo.AppMemberDefinedData != nil && len(item.MemberInfo.AppMemberDefinedData) > 0 { + for _, v := range item.MemberInfo.AppMemberDefinedData { + member.SetCustomData(v.Key, v.Value) + } + } + + group.AddMembers(member) + } + + ret.List = append(ret.List, group) + } + + return +} + +// PullMemberGroups 续拉取用户所加入的群组 +// 本方法由“拉取用户所加入的群组(FetchMemberGroups)”拓展而来 +// App管理员可以通过本接口获取某一用户加入的群信息。默认不获取用户已加入但未激活好友工作群(Work)以及直播群(AVChatRoom)群信息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1625 +func (a *api) PullMemberGroups(arg *PullMemberGroupsArg, fn func(ret *FetchMemberGroupsRet)) (err error) { + var ( + ret *FetchMemberGroupsRet + req = &FetchMemberGroupsArg{ + UserId: arg.UserId, + Limit: arg.Limit, + Type: arg.Type, + Filter: arg.Filter, + IsWithNoActiveGroups: arg.IsWithNoActiveGroups, + IsWithLiveRoomGroups: arg.IsWithLiveRoomGroups, + } + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchMemberGroups(req) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + req.Offset += arg.Limit + } + } + + return +} + +// GetRolesInGroup 查询用户在群组中的身份 +// App管理员可以通过该接口获取一批用户在群内的身份,即“成员角色”。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1626 +func (a *api) GetRolesInGroup(groupId string, userIds []string) (roles map[string]string, err error) { + req := &getRolesInGroupReq{GroupId: groupId, UserIds: userIds} + resp := &getRolesInGroupResp{} + + if err = a.client.Post(serviceGroup, commandGetRoleInGroup, req, resp); err != nil { + return + } + + roles = make(map[string]string, len(resp.MemberRoleList)) + for _, item := range resp.MemberRoleList { + roles[item.UserId] = item.Role + } + + return +} + +// ForbidSendMessage 批量禁言 +// App 管理员禁止指定群组中某些用户在一段时间内发言。 +// App 管理员取消对某些用户的禁言。 +// 被禁言用户退出群组之后再进入同一群组,禁言仍然有效。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1627 +func (a *api) ForbidSendMessage(groupId string, userIds []string, shutUpTime int64) (err error) { + req := &forbidSendMessageReq{ + GroupId: groupId, + UserIds: userIds, + ShutUpTime: shutUpTime, + } + + if err = a.client.Post(serviceGroup, commandForbidSendMsg, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// AllowSendMessage 取消禁言 +// 本方法由“批量禁言(ForbidSendMessage)”拓展而来 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1627 +func (a *api) AllowSendMessage(groupId string, userIds []string) (err error) { + return a.ForbidSendMessage(groupId, userIds, 0) +} + +// GetShuttedUpMembers 获取被禁言群成员列表 +// App管理员可以根据群组ID获取群组中被禁言的用户列表。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2925 +func (a *api) GetShuttedUpMembers(groupId string) (shuttedUps map[string]int64, err error) { + req := &getShuttedUpMembersReq{GroupId: groupId} + resp := &getShuttedUpMembersResp{} + + if err = a.client.Post(serviceGroup, commandGetGroupShuttedUin, req, resp); err != nil { + return + } + + shuttedUps = make(map[string]int64) + for _, item := range resp.ShuttedUpList { + shuttedUps[item.UserId] = item.ShuttedUntil + } + + return +} + +// SendMessage 在群组中发送普通消息 +// App管理员可以通过该接口在群组中发送普通消息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1629 +func (a *api) SendMessage(groupId string, message *Message) (ret *SendMessageRet, err error) { + if err = message.checkSendError(); err != nil { + return + } + + req := &sendMessageReq{} + req.GroupId = groupId + req.FromUserId = message.GetSender() + req.OfflinePushInfo = message.GetOfflinePushInfo() + req.MsgPriority = string(message.GetPriority()) + req.MsgBody = message.GetBody() + req.Random = message.GetRandom() + req.CloudCustomData = conv.String(message.GetCustomData()) + req.SendMsgControl = message.GetSendMsgControl() + req.ForbidCallbackControl = message.GetForbidCallbackControl() + req.OnlineOnlyFlag = int(message.GetOnlineOnlyFlag()) + + if message.atMembers != nil && len(message.atMembers) > 0 { + req.GroupAtInfo = make([]atInfo, 0, len(message.atMembers)) + + for userId, _ := range message.atMembers { + if userId == AtAllMembersFlag { + req.GroupAtInfo = append(req.GroupAtInfo, atInfo{ + GroupAtAllFlag: 1, + }) + } else { + req.GroupAtInfo = append(req.GroupAtInfo, atInfo{ + GroupAtAllFlag: 0, + GroupAtUserId: userId, + }) + } + } + } + + resp := &sendMessageResp{} + + if err = a.client.Post(serviceGroup, commandSendGroupMsg, req, resp); err != nil { + return + } else { + ret = &SendMessageRet{ + MsgSeq: resp.MsgSeq, + MsgTime: resp.MsgTime, + } + } + + return +} + +// SendNotification 在群组中发送系统通知 +// App 管理员可以通过该接口在群组中发送系统通知。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1630 +func (a *api) SendNotification(groupId, content string, userIds ...string) (err error) { + req := &sendNotificationReq{GroupId: groupId, Content: content, UserIds: userIds} + + if err = a.client.Post(serviceGroup, commandSendGroupSystemNotification, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// ChangeGroupOwner 转让群主 +// App 管理员可以通过该接口将群主身份转移给他人。 +// 没有群主的群,App 管理员可以通过此接口指定他人作为群主。 +// 新群主必须为群内成员。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1633 +func (a *api) ChangeGroupOwner(groupId, userId string) (err error) { + req := &changeGroupOwnerReq{GroupId: groupId, OwnerUserId: userId} + + if err = a.client.Post(serviceGroup, commandChangeGroupOwner, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// RevokeMessage 撤回单条群消息 +// 本方法由“撤回多条群消息(RevokeMessages)”拓展而来 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/12341 +func (a *api) RevokeMessage(groupId string, msgSeq int) (err error) { + var results map[int]int + + if results, err = a.RevokeMessages(groupId, msgSeq); err != nil { + return + } + + if results[msgSeq] != enum.SuccessCode { + err = core.NewError(results[msgSeq], "message revoke failed") + return + } + + return +} + +// RevokeMessages 撤回多条群消息 +// App 管理员通过该接口撤回指定群组的消息,消息需要在漫游有效期以内。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/12341 +func (a *api) RevokeMessages(groupId string, msgSeq ...int) (results map[int]int, err error) { + req := revokeMessagesReq{} + req.GroupId = groupId + req.MsgSeqList = make([]msgSeqItem, 0, len(msgSeq)) + for _, seq := range msgSeq { + req.MsgSeqList = append(req.MsgSeqList, msgSeqItem{ + MsgSeq: seq, + }) + } + + resp := &revokeMessagesResp{} + + if err = a.client.Post(serviceGroup, commandRecallGroupMsg, req, resp); err != nil { + return + } + + results = make(map[int]int) + for _, item := range resp.Results { + results[item.MsgSeq] = item.RetCode + } + + return +} + +// ImportGroup 导入群基础资料 +// App 管理员可以通过该接口导入群组,不会触发回调、不会下发通知;当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群组数据。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1634 +func (a *api) ImportGroup(group *Group) (groupId string, err error) { + if err = group.checkImportError(); err != nil { + return + } + + req := &importGroupReq{} + req.GroupId = group.id + req.OwnerUserId = group.owner + req.Type = group.groupType + req.Name = group.name + req.FaceUrl = group.avatar + req.Introduction = group.introduction + req.Notification = group.notification + req.MaxMemberNum = group.maxMemberNum + req.ApplyJoinOption = group.applyJoinOption + req.CreateTime = group.createTime + + if data := group.GetAllCustomData(); data != nil { + req.AppDefinedData = make([]*customDataItem, 0, len(data)) + for key, val := range data { + req.AppDefinedData = append(req.AppDefinedData, &customDataItem{ + Key: key, + Value: val, + }) + } + } + + resp := &importGroupResp{} + + if err = a.client.Post(serviceGroup, commandImportGroup, req, resp); err != nil { + return + } else { + groupId = resp.GroupId + } + + return +} + +// ImportMessages 导入群消息 +// 该 API 接口的作用是导入群组的消息,不会触发回调、不会下发通知。 +// 当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群消息数据。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1635 +func (a *api) ImportMessages(groupId string, messages ...*Message) (results []ImportMessagesResult, err error) { + req := &importMessagesReq{GroupId: groupId, Messages: make([]messageItem, 0, len(messages))} + + for _, message := range messages { + if err = message.checkImportError(); err != nil { + return + } + + req.Messages = append(req.Messages, messageItem{ + FromUserId: message.GetSender(), + MsgBody: message.GetBody(), + SendTime: message.GetSendTime(), + Random: message.GetRandom(), + }) + } + + resp := &importMessagesResp{} + + if err = a.client.Post(serviceGroup, commandImportGroupMsg, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// ImportMembers 导入多个群成员 +// 该 API 接口的作用是导入群组成员,不会触发回调、不会下发通知。 +// 当 App 需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议导入存量群成员数据。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1636 +func (a *api) ImportMembers(groupId string, members ...*Member) (results []ImportMemberResult, err error) { + req := &importMembersReq{GroupId: groupId, Members: make([]*memberItem, 0, len(members))} + resp := &importMembersResp{} + + for _, member := range members { + req.Members = append(req.Members, &memberItem{ + UserId: member.userId, + Role: member.role, + JoinTime: member.joinTime, + UnreadMsgNum: member.unreadMsgNum, + }) + } + + if err = a.client.Post(serviceGroup, commandImportGroupMember, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// SetMemberUnreadMsgNum 设置成员未读消息计数 +// App管理员使用该接口设置群组成员未读消息数,不会触发回调、不会下发通知。 +// 当App需要从其他即时通信系统迁移到即时通信 IM 时,使用该协议设置群成员的未读消息计数。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1637 +func (a *api) SetMemberUnreadMsgNum(groupId, userId string, unreadMsgNum int) (err error) { + req := &setMemberUnreadMsgNumReq{GroupId: groupId, UserId: userId, UnreadMsgNum: unreadMsgNum} + + if err = a.client.Post(serviceGroup, commandSetUnreadMsgNum, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// RevokeMemberMessages 撤回指定用户发送的消息 +// 该API接口的作用是撤回最近1000条消息中指定用户发送的消息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2359 +func (a *api) RevokeMemberMessages(groupId, userId string) (err error) { + req := &revokeMemberMessagesReq{GroupId: groupId, UserId: userId} + + if err = a.client.Post(serviceGroup, commandDeleteGroupMsgBySender, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// FetchMessages 拉取群历史消息 +// 即时通信 IM 的群消息是按 Seq 排序的,按照 server 收到群消息的顺序分配 Seq,先发的群消息 Seq 小,后发的 Seq 大。 +// 如果用户想拉取一个群的全量消息,首次拉取时不用填拉取 Seq,Server 会自动返回最新的消息,以后拉取时拉取 Seq 填上次返回的最小 Seq 减1。 +// 如果返回消息的 IsPlaceMsg 为1,表示这个 Seq 的消息或者过期、或者存储失败、或者被删除了。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2738 +func (a *api) FetchMessages(groupId string, limit int, msgSeq ...int) (ret *FetchMessagesRet, err error) { + req := &fetchMessagesReq{GroupId: groupId, ReqMsgNumber: limit} + + if len(msgSeq) > 0 { + req.ReqMsgSeq = msgSeq[0] + } + + resp := &fetchMessagesResp{} + + if err = a.client.Post(serviceGroup, commandGetGroupSimpleMsg, req, resp); err != nil { + return + } + + ret = &FetchMessagesRet{} + ret.IsFinished = resp.IsFinished + + if ret.IsFinished == 0 { + ret.HasMore = true + } + + if count := len(resp.RspMsgList); count > 0 { + ret.NextSeq = resp.RspMsgList[count-1].MsgSeq - 1 + + if ret.IsFinished == 1 && count == limit { + ret.HasMore = true + } + } + + ret.List = make([]*Message, 0, len(resp.RspMsgList)) + for _, item := range resp.RspMsgList { + message := NewMessage() + message.SetSender(item.FromUserId) + message.SetRandom(item.MsgRandom) + message.seq = item.MsgSeq + message.timestamp = item.MsgTimeStamp + message.status = MsgStatus(item.IsPlaceMsg) + switch item.MsgPriority { + case 1: + message.priority = MsgPriorityHigh + case 2: + message.priority = MsgPriorityNormal + case 3: + message.priority = MsgPriorityLow + case 4: + message.priority = MsgPriorityLowest + } + } + + return +} + +// PullMessages 续拉取群历史消息 +// 本方法由“拉取群历史消息(FetchMessages)”拓展而来 +// 即时通信 IM 的群消息是按 Seq 排序的,按照 server 收到群消息的顺序分配 Seq,先发的群消息 Seq 小,后发的 Seq 大。 +// 如果用户想拉取一个群的全量消息,首次拉取时不用填拉取 Seq,Server 会自动返回最新的消息,以后拉取时拉取 Seq 填上次返回的最小 Seq 减1。 +// 如果返回消息的 IsPlaceMsg 为1,表示这个 Seq 的消息或者过期、或者存储失败、或者被删除了。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2738 +func (a *api) PullMessages(groupId string, limit int, fn func(ret *FetchMessagesRet)) (err error) { + var ( + ret *FetchMessagesRet + msgSeq int + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchMessages(groupId, limit, msgSeq) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + msgSeq = ret.NextSeq + } + } + + return +} + +// GetOnlineMemberNum 获取直播群在线人数 +// App 管理员可以根据群组 ID 获取直播群在线人数。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/49180 +func (a *api) GetOnlineMemberNum(groupId string) (num int, err error) { + req := &getOnlineMemberNumReq{GroupId: groupId} + resp := &getOnlineMemberNumResp{} + + if err = a.client.Post(serviceGroup, commandGetOnlineMemberNum, req, resp); err != nil { + return + } + + num = resp.OnlineMemberNum + + return +} diff --git a/group/filter.go b/group/filter.go new file mode 100644 index 0000000..c732c04 --- /dev/null +++ b/group/filter.go @@ -0,0 +1,210 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/8 10:17 + * @Desc: 群组响应过滤器 + */ + +package group + +type ( + // BaseInfoField 群基础信息字段 + BaseInfoField string + + // MemberInfoField 群成员信息字段 + MemberInfoField string +) + +const ( + BaseFieldGroupId BaseInfoField = "GroupId" // 群组的唯一标识 + BaseFieldType BaseInfoField = "Type" // 群组类型 + BaseFieldName BaseInfoField = "Name" // 群组名称 + BaseFieldIntroduction BaseInfoField = "Introduction" // 群组简介 + BaseFieldNotification BaseInfoField = "Notification" // 群组公告 + BaseFieldAvatar BaseInfoField = "FaceUrl" // 群组头像URL + BaseFieldOwner BaseInfoField = "Owner_Account" // 群主ID + BaseFieldCreateTime BaseInfoField = "CreateTime" // 群组的创建时间 + BaseFieldInfoSeq BaseInfoField = "InfoSeq" // 群资料变更次数 + BaseFieldLastInfoTime BaseInfoField = "LastInfoTime" // 群组最后一次信息变更时间 + BaseFieldLastMsgTime BaseInfoField = "LastMsgTime" // 群组内最后发消息的时间 + BaseFieldNextMsgSeq BaseInfoField = "NextMsgSeq" // 群内下一条消息的Seq + BaseFieldMemberNum BaseInfoField = "MemberNum" // 当前成员数量 + BaseFieldMaxMemberNum BaseInfoField = "MaxMemberNum" // 最大成员数量 + BaseFieldApplyJoinOption BaseInfoField = "ApplyJoinOption" // 申请加群选项 + + MemberFieldUserId MemberInfoField = "Member_Account" // 群成员ID + MemberFieldRole MemberInfoField = "Role" // 群内身份 + MemberFieldJoinTime MemberInfoField = "JoinTime" // 入群时间 + MemberFieldMsgSeq MemberInfoField = "MsgSeq" // 该成员当前已读消息Seq + MemberFieldMsgFlag MemberInfoField = "MsgFlag" // 消息接收选项 + MemberFieldLastSendMsgTime MemberInfoField = "LastSendMsgTime" // 最后发送消息的时间 + MemberFieldNameCard MemberInfoField = "NameCard" // 群名片 +) + +type Filter struct { + baseInfo map[string]bool + memberInfo map[string]bool + memberRole map[string]bool + groupCustomData map[string]bool + memberCustomData map[string]bool +} + +// AddBaseInfoFilter 添加基础信息过滤器 +func (f *Filter) AddBaseInfoFilter(field BaseInfoField) { + if f.baseInfo == nil { + f.baseInfo = make(map[string]bool) + } + + f.baseInfo[string(field)] = true +} + +// RemBaseInfoFilter 移除基础信息过滤器 +func (f *Filter) RemBaseInfoFilter(field BaseInfoField) { + if f.baseInfo == nil { + return + } + + delete(f.baseInfo, string(field)) +} + +// GetAllBaseInfoFilterFields 获取所有基础信息过滤器字段 +func (f *Filter) GetAllBaseInfoFilterFields() (filters []string) { + if f.baseInfo == nil { + return + } + + filters = make([]string, 0, len(f.baseInfo)) + for k, _ := range f.baseInfo { + filters = append(filters, k) + } + + return +} + +// AddMemberInfoFilter 添加成员信息过滤器 +func (f *Filter) AddMemberInfoFilter(field MemberInfoField) { + if f.memberInfo == nil { + f.memberInfo = make(map[string]bool) + } + + f.memberInfo[string(field)] = true +} + +// RemMemberInfoFilter 移除成员信息过滤器 +func (f *Filter) RemMemberInfoFilter(field MemberInfoField) { + if f.memberInfo == nil { + return + } + + delete(f.memberInfo, string(field)) +} + +// GetAllMemberInfoFilterFields 获取所有成员信息过滤器字段 +func (f *Filter) GetAllMemberInfoFilterFields() (filters []string) { + if f.memberInfo == nil { + return + } + + filters = make([]string, 0, len(f.memberInfo)) + for k, _ := range f.memberInfo { + filters = append(filters, k) + } + + return +} + +// AddMemberRoleFilter 添加群成员角色过滤器 +func (f *Filter) AddMemberRoleFilter(field string) { + if f.memberRole == nil { + f.memberRole = make(map[string]bool) + } + + f.memberRole[field] = true +} + +// RemMemberRoleFilter 移除群成员角色过滤器 +func (f *Filter) RemMemberRoleFilter(field string) { + if f.memberRole == nil { + return + } + + delete(f.memberRole, field) +} + +// GetAllMemberRoleFilterValues 获取所有群成员角色过滤器值 +func (f *Filter) GetAllMemberRoleFilterValues() (filters []string) { + if f.memberRole == nil { + return + } + + filters = make([]string, 0, len(f.memberRole)) + for k, _ := range f.memberRole { + filters = append(filters, k) + } + + return +} + +// AddGroupCustomDataFilter 添加群自定义数据过滤器 +func (f *Filter) AddGroupCustomDataFilter(field string) { + if f.groupCustomData == nil { + f.groupCustomData = make(map[string]bool) + } + + f.groupCustomData[field] = true +} + +// RemGroupCustomDataFilter 移除群自定义数据过滤器 +func (f *Filter) RemGroupCustomDataFilter(field string) { + if f.groupCustomData == nil { + return + } + + delete(f.groupCustomData, field) +} + +// GetAllGroupCustomDataFilterFields 获取所有群自定义数据过滤器字段 +func (f *Filter) GetAllGroupCustomDataFilterFields() (filters []string) { + if f.groupCustomData == nil { + return + } + + filters = make([]string, 0, len(f.groupCustomData)) + for k, _ := range f.groupCustomData { + filters = append(filters, k) + } + + return +} + +// AddMemberCustomDataFilter 添加群成员自定义数据过滤器 +func (f *Filter) AddMemberCustomDataFilter(field string) { + if f.memberCustomData == nil { + f.memberCustomData = make(map[string]bool) + } + + f.memberCustomData[field] = true +} + +// RemMemberCustomDataFilter 移除群成员自定义数据过滤器 +func (f *Filter) RemMemberCustomDataFilter(field string) { + if f.memberCustomData == nil { + return + } + + delete(f.memberCustomData, field) +} + +// GetAllMemberCustomDataFilterFields 获取所有群成员自定义数据过滤器字段 +func (f *Filter) GetAllMemberCustomDataFilterFields() (filters []string) { + if f.memberCustomData == nil { + return + } + + filters = make([]string, 0, len(f.memberCustomData)) + for k, _ := range f.memberCustomData { + filters = append(filters, k) + } + + return +} diff --git a/group/group.go b/group/group.go new file mode 100644 index 0000000..82bbc28 --- /dev/null +++ b/group/group.go @@ -0,0 +1,383 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 17:11 + * @Desc: TODO + */ + +package group + +import ( + "time" + + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/enum" +) + +var ( + errNotSetGroupType = core.NewError(enum.InvalidParamsCode, "group type is not set") + errNotSetGroupName = core.NewError(enum.InvalidParamsCode, "group name is not set") + errGroupNameTooLong = core.NewError(enum.InvalidParamsCode, "group name is too long") + errInvalidGroupType = core.NewError(enum.InvalidParamsCode, "invalid group type") + errGroupIntroductionTooLong = core.NewError(enum.InvalidParamsCode, "group introduction is too long") + errGroupNotificationTooLong = core.NewError(enum.InvalidParamsCode, "group notification is too long") +) + +type ( + // Type 群类型 + Type string + + // ApplyJoinOption 申请加群处理方式 + ApplyJoinOption string + + // ShutUpStatus 全员禁言状态 + ShutUpStatus string +) + +const ( + TypePublic Type = "Public" // Public(陌生人社交群) + TypePrivate Type = "Private" // Private(即 Work,好友工作群) + TypeChatRoom Type = "ChatRoom" // ChatRoom(即 Meeting,会议群) + TypeLiveRoom Type = "AVChatRoom" // AVChatRoom(直播群) + + ApplyJoinOptionFreeAccess ApplyJoinOption = "FreeAccess" // 自由加入 + ApplyJoinOptionNeedPermission ApplyJoinOption = "NeedPermission" // 需要验证 + ApplyJoinOptionDisableApply ApplyJoinOption = "DisableApply" // 禁止加群 + + ShutUpStatusOn ShutUpStatus = "On" // 开启 + ShutUpStatusOff ShutUpStatus = "Off" // 关闭 +) + +type Group struct { + err error + id string // 群ID + name string // 群名称 + groupType Type // 群类型 + owner string // 群主ID + introduction string // 群简介 + notification string // 群公告 + avatar string // 群头像 + memberNum uint // 群成员数 + maxMemberNum uint // 最大群成员数量 + applyJoinOption string // 申请加群处理方式 + members []*Member // 群成员 + customData map[string]interface{} // 群自定义数据 + createTime int64 // 群创建时间 + lastInfoTime int64 // 最后群资料变更时间 + lastMsgTime int64 // 群内最后一条消息的时间 + nextMsgSeq int // 群内下一条消息的Seq + shutUpStatus string // 群全员禁言状态 +} + +func NewGroup(id ...string) *Group { + group := &Group{} + if len(id) > 0 { + group.SetGroupId(id[0]) + } + return group +} + +// SetGroupId 设置群ID +func (g *Group) SetGroupId(id string) { + g.id = id +} + +// GetGroupId 获取群ID +func (g *Group) GetGroupId() string { + return g.id +} + +// SetOwner 设置群主ID +func (g *Group) SetOwner(owner string) { + g.owner = owner +} + +// GetOwner 获取群主ID +func (g *Group) GetOwner() string { + return g.owner +} + +// SetName 设置群名称 +func (g *Group) SetName(name string) { + g.name = name +} + +// GetName 获取群名称 +func (g *Group) GetName() string { + return g.name +} + +// SetGroupType 设置群类型 +func (g *Group) SetGroupType(groupType Type) { + g.groupType = groupType +} + +// GetGroupType 获取群类型 +func (g *Group) GetGroupType() Type { + return g.groupType +} + +// SetIntroduction 设置群简介 +func (g *Group) SetIntroduction(introduction string) { + g.introduction = introduction +} + +// GetIntroduction 获取群简介 +func (g *Group) GetIntroduction() string { + return g.introduction +} + +// SetNotification 设置群公告 +func (g *Group) SetNotification(notification string) { + g.notification = notification +} + +// GetNotification 获取群公告 +func (g *Group) GetNotification() string { + return g.notification +} + +// SetAvatar 设置群头像 +func (g *Group) SetAvatar(avatar string) { + g.avatar = avatar +} + +// GetAvatar 获取群头像 +func (g *Group) GetAvatar() string { + return g.avatar +} + +// SetMaxMemberNum 设置最大群成员数量 +func (g *Group) SetMaxMemberNum(maxMemberNum uint) { + g.maxMemberNum = maxMemberNum +} + +// GetMaxMemberNum 获取最大群成员数量 +func (g *Group) GetMaxMemberNum() uint { + return g.maxMemberNum +} + +// GetMemberNum 获取群成员数 +func (g *Group) GetMemberNum() uint { + return g.memberNum +} + +// SetApplyJoinOption 设置申请加群处理方式 +func (g *Group) SetApplyJoinOption(applyJoinOption ApplyJoinOption) { + g.applyJoinOption = string(applyJoinOption) +} + +// GetApplyJoinOption 获取申请加群处理方式 +func (g *Group) GetApplyJoinOption() string { + return g.applyJoinOption +} + +// AddMembers 添加群成员 +func (g *Group) AddMembers(member ...*Member) { + if g.members == nil { + g.members = make([]*Member, 0) + } + + g.members = append(g.members, member...) +} + +// SetMembers 设置群成员 +func (g *Group) SetMembers(member ...*Member) { + if g.members != nil { + g.members = g.members[0:0] + } + + g.AddMembers(member...) +} + +// SetCustomData 设置自定义数据 +func (g *Group) SetCustomData(name string, value interface{}) { + if g.customData == nil { + g.customData = make(map[string]interface{}) + } + + g.customData[name] = value +} + +// GetCustomData 获取自定义数据 +func (g *Group) GetCustomData(name string) (val interface{}, exist bool) { + if g.customData == nil { + return + } + + val, exist = g.customData[name] + + return +} + +// GetAllCustomData 获取所有自定义数据 +func (g *Group) GetAllCustomData() map[string]interface{} { + return g.customData +} + +// GetMembers 获取群成员 +func (g *Group) GetMembers() []*Member { + return g.members +} + +// GetGroupCreateTime 获取群创建时间 +func (g *Group) GetGroupCreateTime() time.Time { + return time.Unix(g.createTime, 0) +} + +// GetLastInfoTime 获取最后群资料变更时间 +func (g *Group) GetLastInfoTime() time.Time { + return time.Unix(g.lastInfoTime, 0) +} + +// GetLastMsgTime 获取群内最后一条消息的时间 +func (g *Group) GetLastMsgTime() time.Time { + return time.Unix(g.lastMsgTime, 0) +} + +// GetNextMsgSeq 获取群内下一条消息的Seq +func (g *Group) GetNextMsgSeq() int { + return g.nextMsgSeq +} + +// SetShutUpStatus 设置全员禁言状态 +func (g *Group) SetShutUpStatus(shutUpStatus ShutUpStatus) { + g.shutUpStatus = string(shutUpStatus) +} + +// GetShutUpStatus 获取群全员禁言状态 +func (g *Group) GetShutUpStatus() string { + return g.shutUpStatus +} + +// SetCreateTime 设置群组创建时间 +func (g *Group) SetCreateTime(createTime int64) { + g.createTime = createTime +} + +// GetCreateTime 获取群组创建时间 +func (g *Group) GetCreateTime() int64 { + return g.createTime +} + +// IsValid 检测用户是否有效 +func (g *Group) IsValid() bool { + return g.err == nil +} + +// GetError 获取异常错误 +func (g *Group) GetError() error { + return g.err +} + +// 设置异常错误 +func (g *Group) setError(code int, message string) { + if code != enum.SuccessCode { + g.err = core.NewError(code, message) + } +} + +// 检测创建错误 +func (g *Group) checkCreateError() (err error) { + if err = g.checkTypeArgError(); err != nil { + return + } + + if err = g.checkNameArgError(); err != nil { + return + } + + if err = g.checkIntroductionArgError(); err != nil { + return + } + + if err = g.checkNotificationArgError(); err != nil { + return + } + + return +} + +// 检测导入错误 +func (g *Group) checkImportError() (err error) { + if err = g.checkTypeArgError(); err != nil { + return + } + + if err = g.checkNameArgError(); err != nil { + return + } + + if err = g.checkIntroductionArgError(); err != nil { + return + } + + if err = g.checkNotificationArgError(); err != nil { + return + } + + return +} + +// 检测更新错误 +func (g *Group) checkUpdateError() (err error) { + if err = g.checkNameArgError(); err != nil { + return + } + + if err = g.checkIntroductionArgError(); err != nil { + return + } + + if err = g.checkNotificationArgError(); err != nil { + return + } + + return +} + +// 检测群名称参数错误 +func (g *Group) checkNameArgError() error { + if g.name == "" { + return errNotSetGroupName + } + + if len(g.name) > 30 { + return errGroupNameTooLong + } + + return nil +} + +// 检测群类型参数错误 +func (g *Group) checkTypeArgError() error { + if g.groupType == "" { + return errNotSetGroupType + } + + switch Type(g.groupType) { + case TypePublic, TypePrivate, TypeChatRoom, TypeLiveRoom: + default: + return errInvalidGroupType + } + + return nil +} + +// 检测群简介参数错误 +func (g *Group) checkIntroductionArgError() error { + if len(g.introduction) > 240 { + return errGroupIntroductionTooLong + } + + return nil +} + +// 检测群公告参数错误 +func (g *Group) checkNotificationArgError() error { + if len(g.notification) > 300 { + return errGroupNotificationTooLong + } + + return nil +} diff --git a/group/member.go b/group/member.go new file mode 100644 index 0000000..195b57c --- /dev/null +++ b/group/member.go @@ -0,0 +1,160 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 18:06 + * @Desc: TODO + */ + +package group + +import ( + "errors" + "time" +) + +var errNotSetUserId = errors.New("member's userid is not set") + +type ( + // MsgFlag 消息接收选项 + MsgFlag string +) + +const ( + MsgFlagAcceptAndNotify MsgFlag = "AcceptAndNotify" // 接收并提示 + MsgFlagAcceptNotNotify MsgFlag = "AcceptNotNotify" // 接收不提示(不会触发 APNs 远程推送) + MsgFlagDiscard MsgFlag = "Discard" // 屏蔽群消息(不会向客户端推送消息) +) + +type Member struct { + userId string // 成员ID + role string // 群内身份 + joinTime int64 // 加入时间 + nameCard string // 群名片 + msgSeq int // 成员当前已读消息Seq + msgFlag MsgFlag // 消息接收选项 + lastSendMsgTime int64 // 最后发送消息的时间 + shutUpUntil *int64 // 需禁言时间,单位为秒,0表示取消禁言 + unreadMsgNum int // 未读入群成员的未读消息计数 + customData map[string]interface{} // 自定义数据 +} + +func NewMember(userId ...string) *Member { + member := &Member{} + if len(userId) > 0 { + member.SetUserId(userId[0]) + } + return member +} + +// SetUserId 设置群成员ID +func (m *Member) SetUserId(userId string) { + m.userId = userId +} + +// GetUserId 获取群成员ID +func (m *Member) GetUserId() string { + return m.userId +} + +// SetRole 设置群内身份 +func (m *Member) SetRole(role string) { + m.role = role +} + +// GetRole 获取群内身份 +func (m *Member) GetRole() string { + return m.role +} + +// SetJoinTime 设置加入时间 +func (m *Member) SetJoinTime(joinTime time.Time) { + m.joinTime = joinTime.Unix() +} + +// GetJoinTime 获取加入时间 +func (m *Member) GetJoinTime() time.Time { + return time.Unix(m.joinTime, 0) +} + +// SetNameCard 设置群名片 +func (m *Member) SetNameCard(nameCard string) { + m.nameCard = nameCard +} + +// GetNameCard 获取群名片 +func (m *Member) GetNameCard() string { + return m.nameCard +} + +// GetMsgSeq 获取成员当前已读消息Seq +func (m *Member) GetMsgSeq() int { + return m.msgSeq +} + +// SetMsgFlag 设置消息接收选项 +func (m *Member) SetMsgFlag(msgFlag MsgFlag) { + m.msgFlag = msgFlag +} + +// GetMsgFlag 获取消息接收选项 +func (m *Member) GetMsgFlag() MsgFlag { + return m.msgFlag +} + +// SetShutUpUntil 设置需禁言时间,单位为秒,0表示取消禁言 +func (m *Member) SetShutUpUntil(shutUpUntil int64) { + m.shutUpUntil = &shutUpUntil +} + +// GetShutUpUntil 获取需禁言时间,单位为秒,0表示取消禁言 +func (m *Member) GetShutUpUntil() int64 { + if m.shutUpUntil == nil { + return 0 + } else { + return *m.shutUpUntil + } +} + +// SetUnreadMsgNum 设置成员的未读消息计数 +func (m *Member) SetUnreadMsgNum(unreadMsgNum int) { + m.unreadMsgNum = unreadMsgNum +} + +// GetUnreadMsgNum 获取成员的未读消息计数 +func (m *Member) GetUnreadMsgNum() int { + return m.unreadMsgNum +} + +// SetCustomData 设置自定义数据 +func (m *Member) SetCustomData(name string, value interface{}) { + if m.customData == nil { + m.customData = make(map[string]interface{}) + } + + m.customData[name] = value +} + +// GetCustomData 获取自定义数据 +func (m *Member) GetCustomData(name string) (val interface{}, exist bool) { + if m.customData == nil { + return + } + + val, exist = m.customData[name] + + return +} + +// GetAllCustomData 获取所有自定义数据 +func (m *Member) GetAllCustomData() map[string]interface{} { + return m.customData +} + +// 检测参数错误 +func (m *Member) checkError() (err error) { + if m.userId == "" { + return errNotSetUserId + } + + return nil +} diff --git a/group/message.go b/group/message.go new file mode 100644 index 0000000..22922fc --- /dev/null +++ b/group/message.go @@ -0,0 +1,221 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/8/31 18:04 + * @Desc: 私聊消息实体 + */ + +package group + +import ( + "errors" + + "git.echol.cn/loser/tencent-im/internal/entity" +) + +var ( + errNotSetSender = errors.New("message's sender not set") + errNotSetSendTime = errors.New("message's send time not set") +) + +type ( + // MsgOnlineOnlyFlag 只发送在线成员标识 + MsgOnlineOnlyFlag int + + // MsgPriority 消息优先级 + MsgPriority string + + // MsgStatus 消息状态 + MsgStatus int +) + +const ( + MsgOnlineOnlyFlagNo MsgOnlineOnlyFlag = 0 // 发送所有成员 + MsgOnlineOnlyFlagYes MsgOnlineOnlyFlag = 1 // 仅发送在线成员 + + MsgPriorityHigh MsgPriority = "High" // 高优先级消息 + MsgPriorityNormal MsgPriority = "Normal" // 普通优先级消息 + MsgPriorityLow MsgPriority = "Low" // 低优先级消息 + MsgPriorityLowest MsgPriority = "Lowest" // 最低优先级消息 + + MsgStatusNormal MsgStatus = 0 // 正常消息 + MsgStatusInvalid MsgStatus = 1 // 被删除或者消息过期的消息 + MsgStatusRevoked MsgStatus = 2 // 被撤回的消息 + + AtAllMembersFlag = "@all" // @所有成员的标识 +) + +type Message struct { + entity.Message + priority MsgPriority // 消息的优先级 + onlineOnlyFlag MsgOnlineOnlyFlag // 仅发送在线成员标识 + sendTime int64 // 消息发送时间 + timestamp int64 // 消息时间戳,UNIX 时间戳(单位:秒) + seq int // 消息序列号 + status MsgStatus // 消息状态 + customData interface{} // 自定义数据 + sendControls map[string]bool // 发送消息控制 + callbackControls map[string]bool // 禁用回调 + atMembers map[string]bool // @用户 +} + +func NewMessage() *Message { + return &Message{} +} + +// SetPriority 设置消息优先级 +func (m *Message) SetPriority(priority MsgPriority) { + m.priority = priority +} + +// GetPriority 获取消息优先级 +func (m *Message) GetPriority() MsgPriority { + return m.priority +} + +// SetCustomData 设置自定义数据 +func (m *Message) SetCustomData(data interface{}) { + m.customData = data +} + +// GetCustomData 获取自定义数据 +func (m *Message) GetCustomData() interface{} { + return m.customData +} + +// SetOnlineOnlyFlag 设置仅发送在线成员标识 +func (m *Message) SetOnlineOnlyFlag(flag MsgOnlineOnlyFlag) { + m.onlineOnlyFlag = flag +} + +// GetOnlineOnlyFlag 获取仅发送在线成员标识 +func (m *Message) GetOnlineOnlyFlag() MsgOnlineOnlyFlag { + return m.onlineOnlyFlag +} + +// SetSendTime 设置发送时间 +func (m *Message) SetSendTime(sendTime int64) { + m.sendTime = sendTime +} + +// GetSendTime 获取发送时间 +func (m *Message) GetSendTime() int64 { + return m.sendTime +} + +// GetStatus 获取消息状态 +func (m *Message) GetStatus() MsgStatus { + return m.status +} + +// SetForbidBeforeSendMsgCallback 设置禁止发消息前回调 +func (m *Message) SetForbidBeforeSendMsgCallback() { + if m.callbackControls == nil { + m.callbackControls = make(map[string]bool, 0) + } + m.callbackControls["ForbidBeforeSendMsgCallback"] = true +} + +// SetForbidAfterSendMsgCallback 设置禁止发消息后回调 +func (m *Message) SetForbidAfterSendMsgCallback() { + if m.callbackControls == nil { + m.callbackControls = make(map[string]bool, 0) + } + m.callbackControls["ForbidAfterSendMsgCallback"] = true +} + +// GetForbidCallbackControl 获取消息回调禁止开关 +func (m *Message) GetForbidCallbackControl() (controls []string) { + if m.callbackControls != nil { + if n := len(m.callbackControls); n > 0 { + controls = make([]string, 0, n) + for k := range m.callbackControls { + controls = append(controls, k) + } + } + } + + return +} + +// SetNoUnread 设置该条消息不计入未读数 +func (m *Message) SetNoUnread() { + if m.sendControls == nil { + m.sendControls = make(map[string]bool, 0) + } + m.sendControls["NoUnread"] = true +} + +// SetNoLastMsg 设置该条消息不更新会话列表 +func (m *Message) SetNoLastMsg() { + if m.sendControls == nil { + m.sendControls = make(map[string]bool, 0) + } + m.sendControls["NoLastMsg"] = true +} + +// GetSendMsgControl 获取消息发送控制选项 +func (m *Message) GetSendMsgControl() (controls []string) { + if m.sendControls != nil { + if n := len(m.sendControls); n > 0 { + controls = make([]string, 0, n) + for k := range m.sendControls { + controls = append(controls, k) + } + } + } + + return +} + +// AtMembers @某个成员 +func (m *Message) AtMembers(userId ...string) { + if m.atMembers == nil { + m.atMembers = make(map[string]bool) + } + + for _, id := range userId { + m.atMembers[id] = true + } +} + +// AtAllMembers @所有成员 +func (m *Message) AtAllMembers() { + m.AtMembers(AtAllMembersFlag) +} + +// ClearAtMembers 清空所有的的@成员 +func (m *Message) ClearAtMembers() { + m.atMembers = nil +} + +// GetTimestamp 获取消息的时间戳 +func (m *Message) GetTimestamp() int64 { + return m.timestamp +} + +// 检测发送错误 +func (m *Message) checkSendError() (err error) { + if err = m.CheckBodyArgError(); err != nil { + return + } + + return +} + +// 检测导入错误 +func (m *Message) checkImportError() (err error) { + if m.GetSender() == "" { + return errNotSetSender + } + + if m.sendTime == 0 { + return errNotSetSendTime + } + + if err = m.CheckBodyArgError(); err != nil { + return + } + + return +} diff --git a/group/types.go b/group/types.go new file mode 100644 index 0000000..8a5e318 --- /dev/null +++ b/group/types.go @@ -0,0 +1,508 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 17:43 + * @Desc: 群组管理 + */ + +package group + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + // 拉取App中的所有群组(请求) + fetchGroupIdsReq struct { + Limit int `json:"Limit,omitempty"` // (选填)本次获取的群组 ID 数量的上限,不得超过 10000。如果不填,默认为最大值 10000 + Next int `json:"Next,omitempty"` // (选填)群太多时分页拉取标志,第一次填0,以后填上一次返回的值,返回的 Next 为0代表拉完了 + Type string `json:"Type,omitempty"` // (选填)如果仅需要返回特定群组形态的群组,可以通过 Type 进行过滤,但此时返回的 TotalCount 的含义就变成了 App 中属于该群组形态的群组总数。不填为获取所有类型的群组。 + } + + // 拉取App中的所有群组(响应) + fetchGroupIdsResp struct { + types.ActionBaseResp + Next int `json:"Next"` // 分页拉取的标志 + TotalCount int `json:"TotalCount"` // App 当前的群组总数。 + GroupIdList []groupIdItem `json:"GroupIdList"` // 获取到的群组 ID 的集合 + } + + // FetchGroupIdsRet 拉取App中的所有群组ID返回 + FetchGroupIdsRet struct { + Total int // App 当前的群组总数 + Next int // 分页拉取的标志 + HasMore bool // 是否还有更多数据 + List []string // 群组ID列表 + } + + // FetchGroupsRet 拉取APP中的所有群返回 + FetchGroupsRet struct { + Total int // App 当前的群组总数 + Next int // 分页拉取的标志 + HasMore bool // 是否还有更多数据 + List []*Group // 群组列表 + } + + // PullGroupsArg 续拉取群信息(参数) + PullGroupsArg struct { + Limit int // 分页限制 + Type Type // 群组类型 + Filter *Filter // 过滤器 + } + + // 群ID + groupIdItem struct { + GroupId string `json:"GroupId"` // 群ID + } + + // 自定义数据 + customDataItem struct { + Key string `json:"Key"` + Value interface{} `json:"Value"` + } + + // 创建群(请求) + createGroupReq struct { + OwnerUserId string `json:"Owner_Account,omitempty"` // (选填)群主 ID(需是 已导入 的账号)。填写后自动添加到群成员中;如果不填,群没有群主 + GroupId string `json:"GroupId,omitempty"` // (选填)为了使得群组 ID 更加简单,便于记忆传播,腾讯云支持 App 在通过 REST API 创建群组时 自定义群组 ID + Type Type `json:"Type"` // (必填)群组形态,包括 Public(陌生人社交群),Private(即 Work,好友工作群),ChatRoom(即 Meeting,会议群),AVChatRoom(直播群) + Name string `json:"Name"` // (必填)群名称,最长30字节,使用 UTF-8 编码,1个汉字占3个字节 + Introduction string `json:"Introduction,omitempty"` // (选填)群简介,最长240字节,使用 UTF-8 编码,1个汉字占3个字节 + Notification string `json:"Notification,omitempty"` // (选填)群公告,最长300字节,使用 UTF-8 编码,1个汉字占3个字节 + FaceUrl string `json:"FaceUrl,omitempty"` // (选填)群头像 URL,最长100字节 + MaxMemberNum uint `json:"MaxMemberCount,omitempty"` // (选填)最大群成员数量,缺省时的默认值:付费套餐包上限,例如体验版是20,如果升级套餐包,需按照修改群基础资料修改这个字段 + ApplyJoinOption string `json:"ApplyJoinOption,omitempty"` // (选填)申请加群处理方式。包含 FreeAccess(自由加入),NeedPermission(需要验证),DisableApply(禁止加群),不填默认为 NeedPermission(需要验证) 仅当创建支持申请加群的 群组 时,该字段有效 + AppDefinedData []*customDataItem `json:"AppDefinedData,omitempty"` // (选填)群组维度的自定义字段,默认情况是没有的,可以通过 即时通信 IM 控制台 进行配置,详情请参阅 自定义字段 + MemberList []*memberItem `json:"MemberList,omitempty"` // (选填)初始群成员列表,最多100个;成员信息字段详情请参阅 群成员资料 + } + + // 创建群(响应) + createGroupResp struct { + types.ActionBaseResp + GroupId string `json:"GroupId"` // 群ID + } + + // 群成员信息 + memberItem struct { + UserId string `json:"Member_Account"` // 群成员ID + Role string `json:"Role,omitempty"` // 群内身份 + JoinTime int64 `json:"JoinTime,omitempty"` // 入群时间 + MsgSeq int `json:"MsgSeq,omitempty"` // 该成员当前已读消息Seq + MsgFlag string `json:"MsgFlag,omitempty"` // 消息接收选项 + LastSendMsgTime int64 `json:"LastSendMsgTime,omitempty"` // 最后发送消息的时间 + NameCard string `json:"NameCard,omitempty"` // 群名片 + ShutUpUntil int64 `json:"ShutUpUntil"` // 禁言截至时间 + UnreadMsgNum int `json:"UnreadMsgNum,omitempty"` // 待导入群成员的未读消息计数 + AppMemberDefinedData []*customDataItem `json:"AppMemberDefinedData,omitempty"` // 群成员自定义数据 + } + + // 解散群(请求) + destroyGroupReq struct { + GroupId string `json:"GroupId"` // (必填)操作的群 ID + } + + // 响应过滤器 + responseFilter struct { + GroupBaseInfoFilter []string `json:"GroupBaseInfoFilter,omitempty"` + MemberInfoFilter []string `json:"MemberInfoFilter,omitempty"` + GroupCustomDataFilter []string `json:"AppDefinedDataFilter_Group,omitempty"` + MemberCustomDataFilter []string `json:"AppDefinedDataFilter_GroupMember,omitempty"` + SelfInfoFilter []string `json:"SelfInfoFilter,omitempty"` + } + + // 获取群详细资料(请求) + getGroupsReq struct { + GroupIds []string `json:"GroupIdList"` + ResponseFilter *responseFilter `json:"ResponseFilter,omitempty"` + } + + // 获取群详细资料(响应) + getGroupsResp struct { + types.ActionBaseResp + GroupInfos []*groupInfo `json:"GroupInfo"` + } + + groupInfo struct { + GroupId string `json:"GroupId"` + ErrorCode int `json:"ErrorCode"` + ErrorInfo string `json:"ErrorInfo"` + Type Type `json:"Type"` + Name string `json:"Name"` + AppId int `json:"Appid"` + Introduction string `json:"Introduction"` + Notification string `json:"Notification"` + FaceUrl string `json:"FaceUrl"` + OwnerUserId string `json:"Owner_Account"` + CreateTime int64 `json:"CreateTime"` + LastInfoTime int64 `json:"LastInfoTime"` + LastMsgTime int64 `json:"LastMsgTime"` + NextMsgSeq int `json:"NextMsgSeq"` + MemberNum uint `json:"MemberNum"` + MaxMemberNum uint `json:"MaxMemberNum"` + ApplyJoinOption string `json:"ApplyJoinOption"` + ShutUpAllMember string `json:"ShutUpAllMember"` + AppDefinedData []customDataItem `json:"AppDefinedData"` + MemberList []memberItem `json:"MemberList"` + MemberInfo *memberItem `json:"SelfInfo,omitempty"` // 成员在群中的信息(仅在获取用户所加入的群组接口返回) + } + + // 获取群成员详细资料(请求) + fetchMembersReq struct { + GroupId string `json:"GroupId"` // (必填)需要拉取成员信息的群组的 ID + Limit int `json:"Limit"` // (选填)一次最多获取多少个成员的资料,不得超过6000。如果不填,则获取群内全部成员的信息 + Offset int `json:"Offset"` // (选填)从第几个成员开始获取,如果不填则默认为0,表示从第一个成员开始获取 + MemberInfoFilter []string `json:"MemberInfoFilter"` // (选填)需要获取哪些信息, 如果没有该字段则为群成员全部资料,成员信息字段详情请参阅 群成员资料 + MemberRoleFilter []string `json:"MemberRoleFilter"` // (选填)拉取指定身份的群成员资料。如没有填写该字段,默认为所有身份成员资料,成员身份可以为:“Owner”,“Admin”,“Member” + MemberCustomDataFilter []string `json:"AppDefinedDataFilter_GroupMember"` // (选填)默认情况是没有的。该字段用来群成员维度的自定义字段过滤器,指定需要获取的群成员维度的自定义字段,群成员维度的自定义字段详情请参阅 自定义字段 + } + + // 获取群成员详细资料(响应) + fetchMembersResp struct { + types.ActionBaseResp + MemberNum int `json:"MemberNum"` // 本群组的群成员总数 + MemberList []memberItem `json:"MemberList"` // 获取到的群成员列表,其中包含了全部或者指定的群成员信息,成员信息字段详情请参阅 群成员资料 + } + + // FetchMembersRet 拉取群成员结果 + FetchMembersRet struct { + Total int // 成员数量 + HasMore bool // 是否还有更多数据 + List []*Member // 成员列表 + } + + // PullMembersArg 续拉取群成员(参数) + PullMembersArg struct { + GroupId string // (必填)需要拉取成员信息的群组的 ID + Limit int // (选填)一次最多获取多少个成员的资料,不得超过6000。如果不填,则获取群内全部成员的信息 + Filter *Filter // (选填)返回过滤器 + } + + // 修改群基础资料(请求) + updateGroupReq struct { + GroupId string `json:"GroupId"` + Name string `json:"Name,omitempty"` + Introduction string `json:"Introduction,omitempty"` + Notification string `json:"Notification,omitempty"` + FaceUrl string `json:"FaceUrl,omitempty"` + MaxMemberNum uint `json:"MaxMemberNum,omitempty"` + ApplyJoinOption string `json:"ApplyJoinOption,omitempty"` + ShutUpAllMember string `json:"ShutUpAllMember,omitempty"` + AppDefinedData []customDataItem `json:"AppDefinedData,omitempty"` + } + + // 添加群成员(请求) + addMembersReq struct { + GroupId string `json:"GroupId"` + Silence int `json:"Silence,omitempty"` + MemberList []addMemberItem `json:"MemberList"` + } + + // 添加群成员(响应) + addMembersResp struct { + types.ActionBaseResp + MemberList []AddMembersResult `json:"MemberList"` + } + + addMemberItem struct { + UserId string `json:"Member_Account"` + } + + // AddMembersResult 添加群成员结果 + AddMembersResult struct { + UserId string `json:"Member_Account"` + Result int `json:"Result"` + } + + // 删除群成员(请求) + deleteMembersReq struct { + GroupId string `json:"GroupId"` // (必填)操作的群ID + Silence int `json:"Silence"` // (选填)是否静默删人 + Reason string `json:"Reason"` // (选填)踢出用户原因 + UserIds []string `json:"MemberToDel_Account"` // (必填)待删除的群成员 + } + + // 修改群成员资料(请求) + updateMemberReq struct { + GroupId string `json:"GroupId"` // (必填)群ID + UserId string `json:"Member_Account"` // (必填)群成员ID + Role string `json:"Role,omitempty"` // (选填)群内身份 + NameCard string `json:"NameCard,omitempty"` // (选填)群名片 + MsgFlag string `json:"MsgFlag,omitempty"` // (选填)消息接收选项 + ShutUpUntil *int64 `json:"ShutUpUntil,omitempty"` // (选填)禁言截至时间 + AppMemberDefinedData []customDataItem `json:"AppMemberDefinedData,omitempty"` // (选填)群成员自定义数据 + } + + // FetchMemberGroupsArg 拉取用户所加入的群组(参数) + FetchMemberGroupsArg struct { + UserId string // (必填)用户ID + Limit int // (选填)单次拉取的群组数量,如果不填代表所有群组 + Offset int // (选填)从第多少个群组开始拉取 + Type Type // (选填)拉取哪种群组类型 + Filter *Filter // (选填)过滤器 + IsWithNoActiveGroups bool // (选填)是否获取用户已加入但未激活的 Private(即新版本中 Work,好友工作群) 群信息 + IsWithLiveRoomGroups bool // (选填)是否获取用户加入的 AVChatRoom(直播群) + } + + // FetchMemberGroupsRet 拉取用户所加入的群组(返回) + FetchMemberGroupsRet struct { + Total int // 群组总数 + HasMore bool // 是否还有更多数据 + List []*Group // 列表 + } + + // PullMemberGroupsArg 续拉取用户所加入的群组(参数) + PullMemberGroupsArg struct { + UserId string // (必填)用户ID + Limit int // (选填)单次拉取的群组数量,如果不填代表所有群组 + Type Type // (选填)拉取哪种群组类型 + Filter *Filter // (选填)过滤器 + IsWithNoActiveGroups bool // (选填)是否获取用户已加入但未激活的 Private(即新版本中 Work,好友工作群) 群信息 + IsWithLiveRoomGroups bool // (选填)是否获取用户加入的 AVChatRoom(直播群) + } + + // 拉取用户所加入的群组(请求) + fetchMemberGroupsReq struct { + UserId string `json:"Member_Account"` // (必填)用户ID + Limit int `json:"Limit,omitempty"` // (选填)单次拉取的群组数量,如果不填代表所有群组 + Offset int `json:"Offset,omitempty"` // (选填)从第多少个群组开始拉取 + Type Type `json:"Type,omitempty"` // (选填)拉取哪种群组类型 + WithHugeGroups int `json:"WithHugeGroups,omitempty"` + WithNoActiveGroups int `json:"WithNoActiveGroups,omitempty"` + ResponseFilter *responseFilter `json:"ResponseFilter,omitempty"` // (选填)响应过滤 + } + + // 拉取用户所加入的群组(响应) + fetchMemberGroupsResp struct { + types.ActionBaseResp + TotalCount int `json:"TotalCount"` + GroupList []groupInfo `json:"GroupIdList"` + } + + // 获取 + getRolesInGroupReq struct { + GroupId string `json:"GroupId"` + UserIds []string `json:"User_Account"` + } + + getRolesInGroupResp struct { + types.ActionBaseResp + MemberRoleList []memberRole `json:"UserIdList"` + } + + memberRole struct { + UserId string `json:"Member_Account"` + Role string `json:"Role"` + } + + // 批量禁言(请求) + forbidSendMessageReq struct { + GroupId string `json:"GroupId"` // (必填)需要查询的群组 ID + UserIds []string `json:"Members_Account"` // (必填)需要禁言的用户帐号,最多支持500个帐号 + ShutUpTime int64 `json:"ShutUpTime"` // (必填)需禁言时间,单位为秒,为0时表示取消禁言,4294967295为永久禁言。 + } + + // 获取被禁言群成员列表(请求) + getShuttedUpMembersReq struct { + GroupId string `json:"GroupId"` // (必填)需要获取被禁言成员列表的群组 ID + } + + // 获取被禁言群成员列表(响应) + getShuttedUpMembersResp struct { + types.ActionBaseResp + ShuttedUpList []shuttedUp `json:"ShuttedUinList"` + } + + // 被禁言信息 + shuttedUp struct { + UserId string `json:"Member_Account"` // 用户ID + ShuttedUntil int64 `json:"ShuttedUntil"` // 禁言到的时间(使用 UTC 时间,即世界协调时间) + } + + // 在群组中发送普通消息(请求) + sendMessageReq struct { + GroupId string `json:"GroupId"` // (必填)向哪个群组发送消息 + Random uint32 `json:"Random"` // (必填)无符号32位整数 + MsgPriority string `json:"MsgPriority,omitempty"` // (选填)消息的优先级 + FromUserId string `json:"From_Account,omitempty"` // (选填)消息来源帐号 + MsgBody []*types.MsgBody `json:"MsgBody"` // (必填)消息体 + OnlineOnlyFlag int `json:"MsgOnlineOnlyFlag,omitempty"` // (选填)1表示消息仅发送在线成员,默认0表示发送所有成员,AVChatRoom(直播群)不支持该参数 + SendMsgControl []string `json:"SendMsgControl,omitempty"` // (选填)消息发送权限,NoLastMsg 只对单条消息有效,表示不更新最近联系人会话;NoUnread 不计未读,只对单条消息有效。(如果该消息 MsgOnlineOnlyFlag 设置为1,则不允许使用该字段。) + ForbidCallbackControl []string `json:"ForbidCallbackControl,omitempty"` // (选填)消息回调禁止开关,只对单条消息有效 + OfflinePushInfo *types.OfflinePushInfo `json:"OfflinePushInfo,omitempty"` // (选填)离线推送信息配置 + CloudCustomData string `json:"CloudCustomData,omitempty"` // (选填)消息自定义数据(云端保存,会发送到对端,程序卸载重装后还能拉取到) + GroupAtInfo []atInfo `json:"GroupAtInfo,omitempty"` // (选填)@某个用户或者所有人 + } + + // 在群组中发送普通消息(响应) + sendMessageResp struct { + types.ActionBaseResp + MsgTime int `json:"MsgTime"` + MsgSeq int `json:"MsgSeq"` + } + + // SendMessageRet 发送消息结果 + SendMessageRet struct { + MsgSeq int // 消息唯一标识,用于撤回。长度不超过50个字符 + MsgTime int // 消息时间戳,UNIX 时间戳 + } + + atInfo struct { + GroupAtAllFlag int `json:"GroupAtAllFlag"` + GroupAtUserId string `json:"GroupAt_Account,omitempty"` + } + + // 在群组中发送系统通知(请求) + sendNotificationReq struct { + GroupId string `json:"GroupId"` // (必填)向哪个群组发送系统通知 + Content string `json:"Content"` // (必填)系统通知的内容 + UserIds []string `json:"ToMembers_Account,omitempty"` // (选填)接收者群成员列表,请填写接收者 UserID,不填或为空表示全员下发 + } + + // 转让群主(请求) + changeGroupOwnerReq struct { + GroupId string `json:"GroupId"` + OwnerUserId string `json:"NewOwner_Account"` + } + + msgSeqItem struct { + MsgSeq int `json:"MsgSeq"` // 请求撤回的消息seq + } + + // 撤销消息(请求) + revokeMessagesReq struct { + GroupId string `json:"GroupId"` // (必填)操作的群ID + MsgSeqList []msgSeqItem `json:"MsgSeqList"` // (必填)被撤回的消息 seq 列表 + } + + // 撤销消息(响应) + revokeMessagesResp struct { + types.ActionBaseResp + Results []revokeMessageResult `json:"Results"` // 撤销结果列表 + } + + // 撤销消息结果 + revokeMessageResult struct { + MsgSeq int `json:"MsgSeq"` // 单个被撤回消息的 seq + RetCode int `json:"RetCode"` // 单个消息的被撤回结果:0表示成功;其它表示失败 + } + + // 导入群基础资料(请求) + importGroupReq struct { + OwnerUserId string `json:"Owner_Account,omitempty"` // (选填)群主 ID(需是 已导入 的账号)。填写后自动添加到群成员中;如果不填,群没有群主 + GroupId string `json:"GroupId,omitempty"` // (选填)为了使得群组 ID 更加简单,便于记忆传播,腾讯云支持 App 在通过 REST API 创建群组时 自定义群组 ID + Type Type `json:"Type"` // (必填)群组形态,包括 Public(陌生人社交群),Private(即 Work,好友工作群),ChatRoom(即 Meeting,会议群),AVChatRoom(直播群) + Name string `json:"Name"` // (必填)群名称,最长30字节,使用 UTF-8 编码,1个汉字占3个字节 + Introduction string `json:"Introduction,omitempty"` // (选填)群简介,最长240字节,使用 UTF-8 编码,1个汉字占3个字节 + Notification string `json:"Notification,omitempty"` // (选填)群公告,最长300字节,使用 UTF-8 编码,1个汉字占3个字节 + FaceUrl string `json:"FaceUrl,omitempty"` // (选填)群头像 URL,最长100字节 + MaxMemberNum uint `json:"MaxMemberCount,omitempty"` // (选填)最大群成员数量,缺省时的默认值:付费套餐包上限,例如体验版是20,如果升级套餐包,需按照修改群基础资料修改这个字段 + ApplyJoinOption string `json:"ApplyJoinOption,omitempty"` // (选填)申请加群处理方式。包含 FreeAccess(自由加入),NeedPermission(需要验证),DisableApply(禁止加群),不填默认为 NeedPermission(需要验证) 仅当创建支持申请加群的 群组 时,该字段有效 + AppDefinedData []*customDataItem `json:"AppDefinedData,omitempty"` // (选填)群组维度的自定义字段,默认情况是没有的,可以通过 即时通信 IM 控制台 进行配置,详情请参阅 自定义字段 + CreateTime int64 `json:"CreateTime"` // (选填)群组的创建时间 + } + + // 导入群基础资料(响应) + importGroupResp struct { + types.ActionBaseResp + GroupId string `json:"GroupId"` // 群ID + } + + // 消息信息 + messageItem struct { + FromUserId string `json:"From_Account"` // (必填)消息来源帐号 + MsgBody []*types.MsgBody `json:"MsgBody"` // (必填)消息体 + SendTime int64 `json:"SendTime"` // (必填)消息发送时间 + Random uint32 `json:"Random,omitempty"` // (选填)无符号32位整数 + } + + // 导入群消息(请求) + importMessagesReq struct { + GroupId string `json:"GroupId"` // (必填)要导入消息的群ID + Messages []messageItem `json:"MsgList"` // (必填)导入的消息列表 + } + + // 导入群消息(响应) + importMessagesResp struct { + types.ActionBaseResp + Results []ImportMessagesResult `json:"ImportMsgResult"` // 导入群消息结果 + } + + // ImportMessagesResult 导入群消息结果 + ImportMessagesResult struct { + MsgSeq int `json:"MsgSeq"` // 消息序列号,唯一标示一条消息 + MsgTime int `json:"MsgTime"` // 消息的时间戳 + Result int `json:"Result"` // 单条消息导入结果 0表示单条消息成功 10004表示单条消息发送时间无效 80001表示单条消息包含脏字,拒绝存储此消息 80002表示为消息内容过长,目前支持8000字节的消息,请调整消息长度 + } + + // 导入群成员(请求) + importMembersReq struct { + GroupId string `json:"GroupId"` // (必填)操作的群ID + Members []*memberItem `json:"MemberList"` // (必填)添加的群成员数组 + } + + // 导入群成员(响应) + importMembersResp struct { + types.ActionBaseResp + Results []ImportMemberResult `json:"MemberList"` // 添加的群成员结果 + } + + // ImportMemberResult 导入成员结果 + ImportMemberResult struct { + UserId string `json:"Member_Account"` // 群成员帐号 + Result int `json:"Result"` // 导入结果:0表示失败;1表示成功;2表示已经是群成员 + } + + // 设置成员未读消息计数(请求) + setMemberUnreadMsgNumReq struct { + GroupId string `json:"GroupId"` // (必填)操作的群 ID + UserId string `json:"Member_Account"` // (必填)要操作的群成员 + UnreadMsgNum int `json:"UnreadMsgNum"` // (必填)成员未读消息数 + } + + // 撤回指定用户发送的消息(请求) + revokeMemberMessagesReq struct { + GroupId string `json:"GroupId"` // (必填)要撤回消息的群 ID + UserId string `json:"Sender_Account"` // (必填)被撤回消息的发送者 ID + } + + // 拉取群历史消息(请求) + fetchMessagesReq struct { + GroupId string `json:"GroupId"` // (必填)要拉取历史消息的群组 ID + ReqMsgSeq int `json:"ReqMsgSeq"` // (选填)拉取消息的最大seq + ReqMsgNumber int `json:"ReqMsgNumber,omitempty"` // (必填)拉取的历史消息的条数,目前一次请求最多返回20条历史消息,所以这里最好小于等于20 + } + + // 拉取群历史消息(响应) + fetchMessagesResp struct { + types.ActionBaseResp + GroupId string `json:"GroupId"` + IsFinished int `json:"IsFinished"` + RspMsgList []rspMsgItem `json:"RspMsgList"` + } + + FetchMessagesRet struct { + IsFinished int // 是否返回了请求区间的全部消息 当成功返回了请求区间的全部消息时,值为1; 当消息长度太长或者区间太大(超过20)导致无法返回全部消息时,值为0; 当消息长度太长或者区间太大(超过20)且所有消息都过期时,值为2 + HasMore bool // 是否还有更多数据 + NextSeq int // 下一个消息Seq + List []*Message // 列表 + } + + rspMsgItem struct { + FromUserId string `json:"From_Account"` + IsPlaceMsg int `json:"IsPlaceMsg"` + MsgBody []types.MsgBody `json:"MsgBody"` + MsgPriority int `json:"MsgPriority"` + MsgRandom uint32 `json:"MsgRandom"` + MsgSeq int `json:"MsgSeq"` + MsgTimeStamp int64 `json:"MsgTimeStamp"` + } + + // 获取直播群在线人数(请求) + getOnlineMemberNumReq struct { + GroupId string `json:"GroupId"` // (必填)操作的群ID + } + + // 获取直播群在线人数(响应) + getOnlineMemberNumResp struct { + types.ActionBaseResp + OnlineMemberNum int `json:"OnlineMemberNum"` // 该群组的在线人数 + } +) diff --git a/im.go b/im.go new file mode 100644 index 0000000..8ef56a8 --- /dev/null +++ b/im.go @@ -0,0 +1,212 @@ +/** + * @Author: Echo + * @Author: 1711788888@qq.com + * @Date: 2022/9/26 20:29 + * @Desc: 腾讯云IM + */ + +package im + +import ( + "sync" + "time" + + "git.echol.cn/loser/tencent-im/account" + "git.echol.cn/loser/tencent-im/callback" + "git.echol.cn/loser/tencent-im/group" + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/sign" + "git.echol.cn/loser/tencent-im/mute" + "git.echol.cn/loser/tencent-im/operation" + "git.echol.cn/loser/tencent-im/private" + "git.echol.cn/loser/tencent-im/profile" + "git.echol.cn/loser/tencent-im/push" + "git.echol.cn/loser/tencent-im/recentcontact" + "git.echol.cn/loser/tencent-im/sns" +) + +type Error = core.Error + +type ( + IM interface { + // GetUserSig 获取UserSig签名 + GetUserSig(userId string, expiration ...int) UserSig + // SNS 获取关系链管理接口 + SNS() sns.API + // Mute 获取全局禁言管理接口 + Mute() mute.API + // Push 获取全员推送接口 + Push() push.API + // Group 获取群组管理接口 + Group() group.API + // Account 获取账号管理接口 + Account() account.API + // Profile 获取资料管理接口 + Profile() profile.API + // Private 获取私聊消息接口 + Private() private.API + // Operation 获取运营管理接口 + Operation() operation.API + // RecentContact 获取最近联系人接口 + RecentContact() recentcontact.API + // Callback 获取回调接口 + Callback() callback.Callback + } + + Options struct { + AppId int // 应用SDKAppID,可在即时通信 IM 控制台 的应用卡片中获取。 + AppSecret string // 密钥信息,可在即时通信 IM 控制台 的应用详情页面中获取,具体操作请参见 获取密钥 + UserId string // 用户ID + Expiration int // UserSig过期时间 + } + + UserSig struct { + UserSig string // 用户签名 + ExpireAt int64 // 签名过期时间 + } + + im struct { + opt *Options + client core.Client + sns struct { + once sync.Once + instance sns.API + } + mute struct { + once sync.Once + instance mute.API + } + push struct { + once sync.Once + instance push.API + } + group struct { + once sync.Once + instance group.API + } + account struct { + once sync.Once + instance account.API + } + profile struct { + once sync.Once + instance profile.API + } + private struct { + once sync.Once + instance private.API + } + operation struct { + once sync.Once + instance operation.API + } + recentcontact struct { + once sync.Once + instance recentcontact.API + } + callback struct { + once sync.Once + instance callback.Callback + } + } +) + +func NewIM(opt *Options) IM { + return &im{opt: opt, client: core.NewClient(&core.Options{ + AppId: opt.AppId, + AppSecret: opt.AppSecret, + UserId: opt.UserId, + Expiration: opt.Expiration, + })} +} + +// GetUserSig 获取UserSig签名 +func (i *im) GetUserSig(userId string, expiration ...int) UserSig { + if len(expiration) == 0 { + expiration = append(expiration, i.opt.Expiration) + } + + userSig, _ := sign.GenUserSig(i.opt.AppId, i.opt.AppSecret, userId, expiration[0]) + expireAt := time.Now().Add(time.Duration(i.opt.Expiration) * time.Second).Unix() + return UserSig{UserSig: userSig, ExpireAt: expireAt} +} + +// SNS 获取关系链管理接口ok +func (i *im) SNS() sns.API { + i.sns.once.Do(func() { + i.sns.instance = sns.NewAPI(i.client) + }) + return i.sns.instance +} + +// Mute 获取全局禁言管理接口ok +func (i *im) Mute() mute.API { + i.mute.once.Do(func() { + i.mute.instance = mute.NewAPI(i.client) + }) + return i.mute.instance +} + +// Push 获取全员推送接口 +func (i *im) Push() push.API { + i.push.once.Do(func() { + i.push.instance = push.NewAPI(i.client) + }) + return i.push.instance +} + +// Group 获取群组管理接口 +func (i *im) Group() group.API { + i.group.once.Do(func() { + i.group.instance = group.NewAPI(i.client) + }) + return i.group.instance +} + +// Account 获取账号管理接口ok +func (i *im) Account() account.API { + i.account.once.Do(func() { + i.account.instance = account.NewAPI(i.client) + }) + return i.account.instance +} + +// Profile 获取资料管理接口ok +func (i *im) Profile() profile.API { + i.profile.once.Do(func() { + i.profile.instance = profile.NewAPI(i.client) + }) + return i.profile.instance +} + +// Private 获取私聊消息接口ok +func (i *im) Private() private.API { + i.private.once.Do(func() { + i.private.instance = private.NewAPI(i.client) + }) + return i.private.instance +} + +// Operation 获取运营管理接口ok +func (i *im) Operation() operation.API { + i.operation.once.Do(func() { + i.operation.instance = operation.NewAPI(i.client) + }) + return i.operation.instance +} + +// RecentContact 获取最近联系人接口ok +func (i *im) RecentContact() recentcontact.API { + i.recentcontact.once.Do(func() { + i.recentcontact.instance = recentcontact.NewAPI(i.client) + }) + return i.recentcontact.instance +} + +// Callback 获取回调接口 +func (i *im) Callback() callback.Callback { + i.callback.once.Do(func() { + i.callback.instance = callback.NewCallback(i.opt.AppId) + }) + return i.callback.instance +} diff --git a/im_test.go b/im_test.go new file mode 100644 index 0000000..01b435a --- /dev/null +++ b/im_test.go @@ -0,0 +1,1511 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/27 1:40 下午 + * @Desc: TODO + */ + +package im_test + +import ( + "fmt" + "math/rand" + "strconv" + "testing" + "time" + + "git.echol.cn/loser/tencent-im" + "git.echol.cn/loser/tencent-im/account" + "git.echol.cn/loser/tencent-im/group" + "git.echol.cn/loser/tencent-im/operation" + "git.echol.cn/loser/tencent-im/private" + "git.echol.cn/loser/tencent-im/profile" + "git.echol.cn/loser/tencent-im/push" + "git.echol.cn/loser/tencent-im/recentcontact" + "git.echol.cn/loser/tencent-im/sns" +) + +const ( + assistant = "assistant" + accountPrefix = "test" + test1 = "test1" + test2 = "test2" + test3 = "test3" + test4 = "test4" + test5 = "test5" + test6 = "test6" + test7 = "test7" + test8 = "test8" + test9 = "test9" +) + +func NewIM() im.IM { + return im.NewIM(&im.Options{ + AppId: 1400564830, + AppSecret: "0d2a321b087fdb8fd5ed5ea14fe0489139086eb1b03541774fc9feeab8f2bfd3", + UserId: "administrator", + Expiration: 3600, + }) +} + +// 处理错误 +func handleError(t *testing.T, callName string, err error) { + if e, ok := err.(im.Error); ok { + t.Fatalf("call %s failed, code:%d, message:%s.", callName, e.Code(), e.Message()) + } else { + t.Fatalf("call %s failed, err:%s.", callName, e.Error()) + } +} + +func testUserIds() []string { + return []string{ + test1, + test2, + test3, + test4, + test5, + test6, + test7, + test8, + test9, + } +} + +// GetUserSig 获取UserSig签名 +func TestIm_GetUserSig(t *testing.T) { + tim := NewIM() + + for i := 0; i < 1000; i++ { + tim.GetUserSig(assistant) + } + + t.Log("Success") +} + +// 导入单个账号 +func TestIm_Account_ImportAccount(t *testing.T) { + if err := NewIM().Account().ImportAccount(&account.Account{ + UserId: assistant, + Nickname: "小助手", + FaceUrl: "http://www.qq.com", + }); err != nil { + t.Fatal(err) + } + + t.Log("Success") +} + +// 导入多个帐号 +func TestIm_Account_ImportAccounts(t *testing.T) { + failedAccounts, err := NewIM().Account().ImportAccounts( + test1, + test2, + test3, + test4, + test5, + test6, + test7, + test8, + test9, + ) + if err != nil { + t.Fatal(err) + } + + t.Log(failedAccounts) +} + +// 删除单个账号 +func TestIm_Account_DeleteAccount(t *testing.T) { + err := NewIM().Account().DeleteAccount(test1) + if err != nil { + t.Fatal(err) + } + + t.Log("Success") +} + +// 删除多个帐号 +func TestIm_Account_DeleteAccounts(t *testing.T) { + deleteResults, err := NewIM().Account().DeleteAccounts( + test1, + test2, + test3, + test4, + test5, + test6, + test7, + test8, + test9, + ) + if err != nil { + t.Fatal(err) + } + + t.Log(deleteResults) +} + +// 查询多个帐号 +func TestIm_Account_CheckAccounts(t *testing.T) { + checkResults, err := NewIM().Account().CheckAccounts( + test1, + test2, + test3, + test4, + test5, + test6, + test7, + test8, + test9, + ) + if err != nil { + t.Fatal(err) + } + + t.Log(checkResults) +} + +// 使帐号登录状态失效 +func TestIm_Account_KickAccount(t *testing.T) { + if err := NewIM().Account().KickAccount(test1); err != nil { + t.Fatal(err) + } + + t.Log("Success") +} + +// 查询帐号在线状态 +func TestIm_Account_QueryAccountOnlineStatus(t *testing.T) { + ret, err := NewIM().Account().GetAccountOnlineState(test2, true) + if err != nil { + t.Fatal(err) + } + + t.Log(ret) +} + +// 查询多个帐号在线状态 +func TestIm_Account_QueryAccountsOnlineStatus(t *testing.T) { + resp, err := NewIM().Account().GetAccountsOnlineState([]string{ + test1, + test2, + }) + if err != nil { + t.Fatal(err) + } + + t.Log(resp.Results) + t.Log(resp.Errors) +} + +// 全员推送 +func TestIm_Push_PushMessage(t *testing.T) { + message := push.NewMessage() + message.SetSender(assistant) + message.SetLifeTime(5000) + message.SetContent(push.MsgTextContent{ + Text: "Hello Tencent IM", + }) + message.OfflinePush().SetTitle("你好腾讯IM") + message.OfflinePush().SetDesc("你好腾讯IM,我来了~~~") + message.OfflinePush().SetPushFlag(push.PushFlagYes) + message.OfflinePush().SetExt(map[string]interface{}{ + "url": "http://www.tencent.com", + }) + message.OfflinePush().SetAndroidExtAsHuaweiIntentParam(push.HuaweiIntentParamIntent) + message.OfflinePush().SetApnsBadgeMode(push.BadgeModeNormal) + + taskId, err := NewIM().Push().PushMessage(message) + if err != nil { + handleError(t, "push.PushMessage", err) + } + + t.Log(taskId) +} + +// 设置应用属性名称 +func TestIm_Push_SetAttrNames(t *testing.T) { + if err := NewIM().Push().SetAttrNames(map[int]string{ + 0: "age", + 1: "city", + }); err != nil { + handleError(t, "push.SetAttrNames", err) + } + + t.Log("Success") +} + +// 获取应用属性名称 +func TestIm_Push_GetAttrNames(t *testing.T) { + ret, err := NewIM().Push().GetAttrNames() + if err != nil { + handleError(t, "push.GetAttrNames", err) + } + + t.Log(ret) +} + +// 获取用户属性 +func TestIm_Push_GetUserAttrs(t *testing.T) { + ret, err := NewIM().Push().GetUserAttrs(test1) + if err != nil { + handleError(t, "push.GetUserAttrs", err) + } + + t.Log(ret) +} + +// 设置用户属性 +func TestIm_Push_SetUserAttrs(t *testing.T) { + err := NewIM().Push().SetUserAttrs(map[string]map[string]interface{}{ + test1: { + "age": 20, + "city": "成都", + }, + }) + if err != nil { + handleError(t, "push.SetUserAttrs", err) + } + + t.Log("Success") +} + +// 删除用户属性 +func TestIm_Push_DeleteUserAttrs(t *testing.T) { + err := NewIM().Push().DeleteUserAttrs(map[string][]string{ + test1: {"age", "city"}, + }) + if err != nil { + handleError(t, "push.DeleteUserAttrs", err) + } + + t.Log("Success") +} + +// 获取用户标签 +func TestIm_Push_GetUserTags(t *testing.T) { + ret, err := NewIM().Push().GetUserTags(test1) + if err != nil { + handleError(t, "push.GetUserTags", err) + } + + t.Log(ret) +} + +// 添加用户标签 +func TestIm_Push_AddUserTags(t *testing.T) { + err := NewIM().Push().AddUserTags(map[string][]string{ + test1: {"chengdu"}, + }) + if err != nil { + handleError(t, "push.AddUserTags", err) + } + + t.Log("Success") +} + +// 删除用户标签 +func TestIm_Push_DeleteUserTags(t *testing.T) { + err := NewIM().Push().DeleteUserTags(map[string][]string{ + test1: {"chengdu"}, + }) + if err != nil { + handleError(t, "push.DeleteUserTags", err) + } + + t.Log("Success") +} + +// 删除用户所有标签 +func TestIm_Push_DeleteUserAllTags(t *testing.T) { + err := NewIM().Push().DeleteUserAllTags(test1, test2) + if err != nil { + handleError(t, "push.DeleteUserAllTags", err) + } + + t.Log("Success") +} + +// 设置资料 +func TestIm_Profile_SetProfile(t *testing.T) { + p := profile.NewProfile() + p.SetUserId(assistant) + p.SetNickname("小助手") + p.SetAvatar("http://www.qq.com") + p.SetGender(profile.GenderTypeMale) + p.SetLocation(1, 23, 7465, 92) + p.SetLanguage(20) + + if err := NewIM().Profile().SetProfile(p); err != nil { + handleError(t, "profile.SetProfile", err) + } + + t.Log("Success") +} + +// 获取资料 +func TestIm_Profile_GetProfile(t *testing.T) { + profiles, err := NewIM().Profile().GetProfiles([]string{ + assistant, + }, []string{ + profile.StandardAttrNickname, + profile.StandardAttrGender, + profile.StandardAttrBirthday, + profile.StandardAttrLocation, + profile.StandardAttrLanguage, + }) + if err != nil { + handleError(t, "profile.GetProfiles", err) + } + + for _, p := range profiles { + t.Log(p.GetUserId()) + t.Log(p.GetNickname()) + t.Log(p.GetGender()) + t.Log(p.GetBirthday()) + t.Log(p.GetLocation()) + t.Log(p.GetLanguage()) + } +} + +// 拉取运营数据 +func TestIm_Operation_GetOperationData(t *testing.T) { + data, err := NewIM().Operation().GetOperationData() + if err != nil { + handleError(t, "operation.GetOperationData", err) + } + + t.Log(data[0].AppId) + t.Log(data[0].AppName) + t.Log(data[0].ActiveUserNum) + t.Log("Success") +} + +// 拉取运营数据 +func TestIm_Operation_GetHistoryData(t *testing.T) { + files, err := NewIM().Operation().GetHistoryData(operation.ChatTypeC2C, time.Date(2021, time.November, 4, 14, 0, 0, 0, time.Local)) + if err != nil { + handleError(t, "operation.GetHistoryData", err) + } + + t.Log(files) + t.Log("Success") +} + +// 获取服务器IP地址 +func TestIm_Operation_GetIpList(t *testing.T) { + ips, err := NewIM().Operation().GetIPList() + if err != nil { + handleError(t, "operation.GetIPList", err) + } + + t.Log(ips) + t.Log("Success") +} + +// 设置全局禁言 +func TestIm_Mute_SetNoSpeaking(t *testing.T) { + var privateMuteTime uint = 400 + var groupMuteTime uint = 200 + if err := NewIM().Mute().SetNoSpeaking(assistant, &privateMuteTime, &groupMuteTime); err != nil { + handleError(t, "mute.SetNoSpeaking", err) + } + + t.Log("Success") +} + +// 查询全局禁言 +func TestIm_Mute_GetNoSpeaking(t *testing.T) { + ret, err := NewIM().Mute().GetNoSpeaking(assistant) + if err != nil { + handleError(t, "mute.GetNoSpeaking", err) + } + + t.Log(ret.PrivateMuteTime) + t.Log(ret.GroupMuteTime) +} + +// 添加好友 +func TestIm_SNS_AddFriends(t *testing.T) { + var ( + userIds = testUserIds() + friends = make([]*sns.Friend, 0, len(userIds)) + friend *sns.Friend + ) + + for _, userId := range userIds { + friend = sns.NewFriend(userId) + friend.SetAddSource("android") + friends = append(friends, friend) + } + + failUserIds, err := NewIM().Account().ImportAccounts(userIds...) + if err != nil { + handleError(t, "account.ImportAccounts", err) + } + + t.Log(failUserIds) + + results, err := NewIM().SNS().AddFriends(assistant, true, false, friends...) + if err != nil { + handleError(t, "sns.AddFriends", err) + } + + t.Log(results) +} + +// 导入好友 +func TestIm_SNS_ImportFriends(t *testing.T) { + var ( + userIds = testUserIds() + friends = make([]*sns.Friend, 0, len(userIds)) + friend *sns.Friend + now = time.Now().Unix() + ) + + for _, userId := range userIds { + friend = sns.NewFriend(userId) + friend.SetAddSource("android") + friend.SetGroup("测试组") + friend.SetAddWording("测试一下") + friend.SetAddTime(now) + friend.SetRemark("测试好友") + friend.SetRemarkTime(now) + friends = append(friends, friend) + } + + failUserIds, err := NewIM().Account().ImportAccounts(userIds...) + if err != nil { + handleError(t, "account.ImportAccounts", err) + } + + t.Log(failUserIds) + + results, err := NewIM().SNS().ImportFriends(assistant, friends...) + if err != nil { + handleError(t, "sns.ImportFriends", err) + } + + t.Log(results) + t.Log("Success") +} + +// 更新单个好友 +func TestIm_SNS_UpdateFriend(t *testing.T) { + friend := sns.NewFriend(test1) + friend.SetAddSource("android") // 忽略更新 + friend.SetGroup("测试组") + friend.SetAddWording("更新单个好友") // 忽略更新 + friend.SetAddTime(time.Now().Unix()) // 忽略更新 + friend.SetRemark("更新单个好友") + friend.SetRemarkTime(time.Now().Unix()) // 忽略更新 + + err := NewIM().SNS().UpdateFriend(assistant, friend) + if err != nil { + handleError(t, "sns.UpdateFriend", err) + } + + t.Log("Success") +} + +// 更新好友 +func TestIm_SNS_UpdateFriends(t *testing.T) { + var ( + total = 10 + friends = make([]*sns.Friend, 0, total) + friend *sns.Friend + now = time.Now().Unix() + ) + + for i := 0; i < total; i++ { + friend = sns.NewFriend(accountPrefix + strconv.Itoa(i)) + friend.SetAddSource("android") + friend.SetGroup("测试组") + friend.SetAddWording("测试一下") + friend.SetAddTime(now) + friend.SetRemark("测试好友") + friend.SetRemarkTime(now) + friends = append(friends, friend) + } + + results, err := NewIM().SNS().UpdateFriends(assistant, friends...) + if err != nil { + handleError(t, "sns.UpdateFriends", err) + } + + t.Log(results) + t.Log("Success") +} + +// 删除好友 +func TestIm_SNS_DeleteFriends(t *testing.T) { + var userIds = testUserIds() + + results, err := NewIM().SNS().DeleteFriends(assistant, false, userIds...) + if err != nil { + handleError(t, "sns.DeleteFriends", err) + } + + t.Log(results) + t.Log("Success") +} + +// 删除所有好友 +func TestIm_SNS_DeleteAllFriends(t *testing.T) { + err := NewIM().SNS().DeleteAllFriends(assistant) + if err != nil { + handleError(t, "sns.DeleteAllFriends", err) + } + + t.Log("Success") +} + +// 校验好友 +func TestIm_SNS_CheckFriends(t *testing.T) { + var userIds = testUserIds() + + results, err := NewIM().SNS().CheckFriends(assistant, sns.CheckTypeSingle, userIds...) + if err != nil { + handleError(t, "sns.CheckFriends", err) + } + + t.Log(results) + t.Log("Success") +} + +// 拉取指定好友 +func TestIm_SNS_GetFriends(t *testing.T) { + var userIds = testUserIds() + + friends, err := NewIM().SNS().GetFriends(assistant, []string{ + sns.FriendAttrAddSource, + sns.FriendAttrRemark, + sns.FriendAttrRemarkTime, // 此Tag无效,GetFriends内部忽略了 + sns.FriendAttrAddTime, + sns.FriendAttrAddWording, + sns.FriendAttrGroup, + sns.StandardAttrNickname, + sns.StandardAttrBirthday, + }, userIds...) + if err != nil { + handleError(t, "sns.GetFriends", err) + } + + // 第一种获取方式 + for _, friend := range friends { + if friend.IsValid() { + t.Log(friend.GetUserId()) + t.Log(friend.GetAddSource()) + t.Log(friend.GetRemark()) + t.Log(friend.GetRemarkTime()) + t.Log(friend.GetGroup()) + t.Log(friend.GetNickname()) + t.Log(friend.GetBirthday()) + fmt.Println() + } + } + + // 第二种获取方式 + for _, friend := range friends { + if err = friend.GetError(); err != nil { + t.Log(fmt.Sprintf("获取账号%s失败:%s", friend.GetUserId(), err.Error())) + } else { + t.Log(friend.GetUserId()) + t.Log(friend.GetAddSource()) + t.Log(friend.GetRemark()) + t.Log(friend.GetRemarkTime()) + t.Log(friend.GetGroup()) + t.Log(friend.GetNickname()) + t.Log(friend.GetBirthday()) + fmt.Println() + } + } +} + +// 拉取好友 +func TestIm_SNS_FetchFriends(t *testing.T) { + var ( + err error + ret *sns.FetchFriendsRet + s = NewIM().SNS() + startIndex = 0 + standardSequence = 0 + customSequence = 0 + ) + + for ret == nil || ret.HasMore { + ret, err = s.FetchFriends(assistant, startIndex, standardSequence, customSequence) + if err != nil { + handleError(t, "sns.FetchFriends", err) + } + + startIndex = ret.StartIndex + standardSequence = ret.StandardSequence + customSequence = ret.CustomSequence + + t.Log("下一个开始点:", ret.StartIndex) + t.Log("标准排序:", ret.StandardSequence) + t.Log("自定义排序:", ret.CustomSequence) + t.Log("好友总数:", ret.Total) + t.Log("是否还有数据:", ret.HasMore) + t.Log("好友列表:") + fmt.Println() + for _, friend := range ret.List { + if err = friend.GetError(); err != nil { + t.Log(fmt.Sprintf("获取账号%s失败:%s", friend.GetUserId(), err.Error())) + } else { + t.Log(friend.GetUserId()) + t.Log(friend.GetAddSource()) + t.Log(friend.GetRemark()) + t.Log(friend.GetRemarkTime()) + t.Log(friend.GetGroup()) + t.Log(friend.GetNickname()) + t.Log(friend.GetBirthday()) + fmt.Println() + } + } + } +} + +// 续拉取好友 +func TestIm_SNS_PullFriends(t *testing.T) { + err := NewIM().SNS().PullFriends(assistant, func(ret *sns.FetchFriendsRet) { + t.Log("下一个开始点:", ret.StartIndex) + t.Log("标准排序:", ret.StandardSequence) + t.Log("自定义排序:", ret.CustomSequence) + t.Log("好友总数:", ret.Total) + t.Log("是否还有数据:", ret.HasMore) + t.Log("好友列表:") + fmt.Println() + for _, friend := range ret.List { + if err := friend.GetError(); err != nil { + t.Log(fmt.Sprintf("获取账号%s失败:%s", friend.GetUserId(), err.Error())) + } else { + t.Log(friend.GetUserId()) + t.Log(friend.GetAddSource()) + t.Log(friend.GetRemark()) + t.Log(friend.GetRemarkTime()) + t.Log(friend.GetGroup()) + t.Log(friend.GetNickname()) + t.Log(friend.GetBirthday()) + fmt.Println() + } + } + }) + if err != nil { + handleError(t, "sns.PullFriends", err) + } + t.Log("Success") +} + +// 添加黑名单 +func TestIm_SNS_AddBlacklist(t *testing.T) { + var userIds = testUserIds() + + results, err := NewIM().SNS().AddBlacklist(assistant, userIds...) + if err != nil { + handleError(t, "sns.AddBlacklist", err) + } + + t.Log(results) + t.Log("Success") +} + +// 删除黑名单 +func TestIm_SNS_DeleteBlacklist(t *testing.T) { + var userIds = testUserIds() + + results, err := NewIM().SNS().DeleteBlacklist(assistant, userIds...) + if err != nil { + handleError(t, "sns.DeleteBlacklist", err) + } + + t.Log(results) + t.Log("Success") +} + +// 拉取黑名单 +func TestIm_SNS_FetchBlacklist(t *testing.T) { + var ( + err error + ret *sns.FetchBlacklistRet + s = NewIM().SNS() + startIndex = 0 + maxLimited = 2 + standardSequence = 0 + ) + + for ret == nil || ret.HasMore { + ret, err = s.FetchBlacklist(assistant, startIndex, maxLimited, standardSequence) + if err != nil { + handleError(t, "sns.FetchBlacklist", err) + } + + startIndex = ret.StartIndex + standardSequence = ret.StandardSequence + + t.Log("下一个开始点:", startIndex) + t.Log("标准排序:", standardSequence) + t.Log("黑名单列表:") + fmt.Println() + for _, blacklist := range ret.List { + t.Log(blacklist.UserId) + t.Log(blacklist.Time) + fmt.Println() + } + } + + t.Log("Success") +} + +// 续拉取黑名单 +func TestIm_SNS_PullBlacklist(t *testing.T) { + err := NewIM().SNS().PullBlacklist(assistant, 2, func(ret *sns.FetchBlacklistRet) { + t.Log("下一个开始点:", ret.StartIndex) + t.Log("标准排序:", ret.StandardSequence) + t.Log("黑名单列表:") + fmt.Println() + for _, blacklist := range ret.List { + t.Log(blacklist.UserId) + t.Log(blacklist.Time) + fmt.Println() + } + }) + if err != nil { + handleError(t, "sns.PullBlacklist", err) + } + + t.Log("Success") +} + +// 校验黑名单 +func TestIm_SNS_CheckBlacklist(t *testing.T) { + var userIds = testUserIds() + + results, err := NewIM().SNS().CheckBlacklist(assistant, sns.BlacklistCheckTypeSingle, userIds...) + if err != nil { + handleError(t, "sns.CheckBlacklist", err) + } + + for _, result := range results { + if result.ResultCode == 0 { + t.Log(result.UserId) + t.Log(result.Relation) + switch result.Relation { + case sns.BlackCheckResultTypeNO: + t.Log("From_Account 的黑名单中没有 To_Account,但无法确定 To_Account 的黑名单中是否有 From_Account") + case sns.BlackCheckResultTypeAWithB: + t.Log("From_Account 的黑名单中有 To_Account,但无法确定 To_Account 的黑名单中是否有 From_Account") + } + } else { + t.Log(result.ResultCode) + t.Log(result.ResultInfo) + } + fmt.Println() + } + + t.Log("Success") +} + +// 添加分组 +func TestIm_SNS_AddGroups(t *testing.T) { + var userIds = testUserIds() + + _, results, err := NewIM().SNS().AddGroups("assistant", []string{ + "测试5", + "测试6", + }, userIds) + if err != nil { + handleError(t, "sns.AddGroups", err) + } + + t.Log(results) + t.Log("Success") +} + +// 删除分组 +func TestIm_SNS_DeleteGroups(t *testing.T) { + _, err := NewIM().SNS().DeleteGroups("assistant", "测试3", "测试4") + if err != nil { + handleError(t, "sns.DeleteGroups", err) + } + + t.Log("Success") +} + +// 拉取分组 +func TestIm_SNS_GetGroups(t *testing.T) { + var ( + err error + lastSequence int + results []*sns.GroupResult + ) + + lastSequence, results, err = NewIM().SNS().GetGroups("assistant", lastSequence, true, "测试1", "测试2") + if err != nil { + handleError(t, "sns.GetGroups", err) + } + + t.Log(results) + t.Log("Success") +} + +// 发送单聊消息 +func TestIm_Private_SendMessage(t *testing.T) { + message := private.NewMessage() + message.SetSender(assistant) + message.SetReceivers(test1) + message.SetLifeTime(30000) + message.SetTimestamp(time.Now().Unix()) + message.SetContent(private.MsgTextContent{ + Text: "Hello world", + }) + message.OfflinePush().SetTitle("你好腾讯IM") + message.OfflinePush().SetDesc("你好腾讯IM,我来了~~~") + message.OfflinePush().SetPushFlag(private.PushFlagYes) + message.OfflinePush().SetExt(map[string]interface{}{ + "url": "http://www.tencent.com", + }) + message.OfflinePush().SetAndroidExtAsHuaweiIntentParam(private.HuaweiIntentParamIntent) + message.OfflinePush().SetApnsBadgeMode(private.BadgeModeNormal) + + ret, err := NewIM().Private().SendMessage(message) + if err != nil { + handleError(t, "private.SendMessage", err) + } + + t.Log(ret.MsgKey) + t.Log(ret.MsgTime) + t.Log("Success") +} + +// 批量发单聊消息 +func TestIm_Private_SendMessages(t *testing.T) { + message := private.NewMessage() + message.SetSender("assistant") + message.AddReceivers("test1", "test2") + message.SetContent(private.MsgTextContent{ + Text: "Hello world", + }) + + ret, err := NewIM().Private().SendMessages(message) + if err != nil { + handleError(t, "private.SendMessages", err) + } + + t.Log(ret.MsgKey) + t.Log(ret.Errors) +} + +// 导入单聊消息 +func TestIm_Private_ImportMessage(t *testing.T) { + message := private.NewMessage() + message.SetSender("assistant") + message.SetReceivers("test1") + message.SetTimestamp(time.Now().Unix()) + message.SetSyncOtherMachine(private.SyncOtherMachineYes) + message.SetContent(private.MsgTextContent{ + Text: "Hello world", + }) + + err := NewIM().Private().ImportMessage(message) + if err != nil { + handleError(t, "private.ImportMessage", err) + } + + t.Log("Success") +} + +// 查询单聊消息 +func TestIm_Private_FetchMessages(t *testing.T) { + var ( + err error + p = NewIM().Private() + ret *private.FetchMessagesRet + arg = &private.FetchMessagesArg{ + FromUserId: test1, + ToUserId: assistant, + MaxLimited: 5, + MinTime: time.Now().Add(-20 * time.Hour).Unix(), + MaxTime: time.Now().Unix(), + } + ) + + for ret == nil || ret.HasMore { + ret, err = p.FetchMessages(arg) + if err != nil { + handleError(t, "private.FetchMessages", err) + } + + if ret.HasMore { + arg.LastMsgKey = ret.LastMsgKey + arg.MaxTime = ret.LastMsgTime + } + + t.Log(ret.HasMore) + t.Log(ret.LastMsgKey) + t.Log(ret.LastMsgTime) + t.Log(ret.Count) + t.Log(ret.List) + fmt.Println() + } +} + +// 分页拉取所有消息 +func TestIm_Private_PullMessages(t *testing.T) { + err := NewIM().Private().PullMessages(&private.PullMessagesArg{ + FromUserId: "test1", + ToUserId: "assistant", + MaxLimited: 5, + MinTime: time.Now().Add(-30 * time.Hour).Unix(), + MaxTime: time.Now().Unix(), + }, func(ret *private.FetchMessagesRet) { + t.Log(ret.HasMore) + t.Log(ret.LastMsgKey) + t.Log(ret.LastMsgTime) + t.Log(ret.Count) + t.Log(ret.List) + fmt.Println() + }) + if err != nil { + handleError(t, "private.PullMessages", err) + } + + t.Log("Success") +} + +// 撤销消息 +func TestIm_Private_RevokeMessage(t *testing.T) { + err := NewIM().Private().RevokeMessage(assistant, test1, "31906_833502_1572869830") + if err != nil { + handleError(t, "private.RevokeMessage", err) + } + + t.Log("Success") +} + +// 设置单聊消息已读 +func TestIm_Private_SetMessageRead(t *testing.T) { + err := NewIM().Private().SetMessageRead(assistant, test1) + if err != nil { + handleError(t, "private.SetMessageRead", err) + } + + t.Log("Success") +} + +// 获取未读消息数 +func TestIm_Private_GetUnreadMessageNum(t *testing.T) { + ret, err := NewIM().Private().GetUnreadMessageNum(assistant, test1, test2) + if err != nil { + handleError(t, "private.GetUnreadMessageNum", err) + } + + t.Log(ret.Total) + t.Log(ret.Results) + t.Log(ret.Errors) +} + +// 创建群组 +func TestIm_Group_CreateGroup(t *testing.T) { + g := group.NewGroup() + g.SetName("测试群2") + g.SetGroupType(group.TypePrivate) + g.SetMaxMemberNum(30) + g.SetAvatar("http://www.baidu.com") + g.SetGroupId("test_group2") + g.SetIntroduction("这是一个测试群") + g.SetNotification("这是一个测试群公告") + + for i := 1; i < 10; i++ { + member := group.NewMember(accountPrefix + strconv.Itoa(i)) + member.SetJoinTime(time.Now()) + g.AddMembers(member) + } + + groupId, err := NewIM().Group().CreateGroup(g) + if err != nil { + handleError(t, "group.CreateGroup", err) + } + + t.Log(groupId) +} + +// 解散单个群 +func TestIm_Group_DestroyGroup(t *testing.T) { + err := NewIM().Group().DestroyGroup("test_group2") + if err != nil { + handleError(t, "group.DestroyGroup", err) + } + + t.Log("Success") +} + +// 获取单个群详细资料 +func TestIm_Group_GetGroup(t *testing.T) { + g, err := NewIM().Group().GetGroup("test_group2") + if err != nil { + handleError(t, "group.GetGroup", err) + } + + if g != nil { + t.Log(g.GetGroupId()) + t.Log(g.GetName()) + t.Log(g.GetGroupType()) + t.Log(g.GetOwner()) + t.Log(g.GetAvatar()) + } +} + +// 获取多个群详细资料 +func TestIm_Group_GetGroups(t *testing.T) { + groups, err := NewIM().Group().GetGroups([]string{ + "test_group2", + }) + if err != nil { + handleError(t, "group.GetGroups", err) + } + + for _, g := range groups { + if err = g.GetError(); err != nil { + t.Error(err) + } else { + t.Log(g.GetGroupId()) + t.Log(g.GetName()) + t.Log(g.GetGroupType()) + t.Log(g.GetOwner()) + t.Log(g.GetAvatar()) + } + } +} + +// 添加群成员 +func TestIm_Group_AddGroupMembers(t *testing.T) { + results, err := NewIM().Group().AddMembers("test_group2", []string{ + test1, + test2, + }, true) + if err != nil { + handleError(t, "group.AddMembers", err) + } + + t.Log(results) +} + +// 删除群成员 +func TestIm_Group_DeleteGroupMembers(t *testing.T) { + err := NewIM().Group().DeleteMembers("test_group2", []string{ + test1, + test2, + test3, + }, "测试删除", true) + if err != nil { + handleError(t, "group.DeleteMembers", err) + } + + t.Log("Success") +} + +// 转让群组 +func TestIm_Group_ChangeGroupOwner(t *testing.T) { + err := NewIM().Group().ChangeGroupOwner("test_group2", test1) + if err != nil { + handleError(t, "group.ChangeGroupOwner", err) + } + + t.Log("Success") +} + +// 修改群基础资料 +func TestIm_Group_UpdateGroup(t *testing.T) { + g := group.NewGroup() + g.SetName("测试群1") + g.SetGroupType(group.TypePublic) + g.SetMaxMemberNum(30) + g.SetAvatar("http://www.baidu.com") + g.SetGroupId("test_group2") + g.SetIntroduction("这是一个测试群") + g.SetNotification("这是一个测试群公告") + + err := NewIM().Group().UpdateGroup(g) + if err != nil { + handleError(t, "group.UpdateGroup", err) + } + + t.Log("Success") +} + +// 查询用户在群组中的身份 +func TestIm_Group_GetRolesInGroup(t *testing.T) { + ret, err := NewIM().Group().GetRolesInGroup("test_group2", []string{ + test1, + test2, + test3, + }) + if err != nil { + handleError(t, "group.GetRolesInGroup", err) + } + + t.Log(ret) +} + +// 拉取群成员详细资料 +func TestIm_Group_FetchGroupMembers(t *testing.T) { + ret, err := NewIM().Group().FetchMembers("test_group1", 3, 2) + if err != nil { + handleError(t, "group.FetchMembers", err) + } + + t.Log(ret.HasMore) + t.Log(ret.Total) + + for _, member := range ret.List { + t.Log(member.GetUserId()) + } +} + +// 拉取群成员详细资料 +func TestIm_Group_PullGroupMembers(t *testing.T) { + err := NewIM().Group().PullMembers(&group.PullMembersArg{ + GroupId: "test_group2", + Limit: 3, + }, func(ret *group.FetchMembersRet) { + t.Log(ret.HasMore) + t.Log(ret.Total) + + for _, member := range ret.List { + t.Log(member.GetUserId()) + } + }) + if err != nil { + handleError(t, "group.PullMembers", err) + } +} + +// 拉取App中的所有群组 +func TestIm_Group_FetchGroupIds(t *testing.T) { + ret, err := NewIM().Group().FetchGroupIds(3, 7964653962) + if err != nil { + handleError(t, "group.FetchGroupIds", err) + } + + t.Log(ret.Total) + t.Log(ret.Next) + t.Log(ret.HasMore) + t.Log(ret.List) +} + +// 拉取App中的所有群组 +func TestIm_Group_FetchGroups(t *testing.T) { + ret, err := NewIM().Group().FetchGroups(50, 7964653962) + if err != nil { + handleError(t, "group.FetchGroups", err) + } + + t.Log(ret.Total) + t.Log(ret.Next) + t.Log(ret.HasMore) + + for _, g := range ret.List { + t.Log(g.GetGroupId()) + t.Log(g.GetOwner()) + t.Log(g.GetName()) + } +} + +// 续拉取App中的所有群组 +func TestIm_Group_PullGroups(t *testing.T) { + err := NewIM().Group().PullGroups(&group.PullGroupsArg{ + Limit: 50, + }, func(ret *group.FetchGroupsRet) { + t.Log(ret.Total) + t.Log(ret.Next) + t.Log(ret.HasMore) + + for _, g := range ret.List { + t.Log(g.GetGroupId()) + t.Log(g.GetOwner()) + t.Log(g.GetName()) + } + }) + if err != nil { + handleError(t, "group.PullGroups", err) + } +} + +// 修改群成员资料 +func TestIm_Group_UpdateGroupMember(t *testing.T) { + member := group.NewMember(test1) + member.SetRole("Admin") + member.SetNameCard("这是一个测试名片信息") + member.SetMsgFlag(group.MsgFlagAcceptAndNotify) + + err := NewIM().Group().UpdateMember("test_group2", member) + if err != nil { + handleError(t, "group.UpdateMember", err) + } + + t.Log("Success") +} + +// 拉取用户所加入的群组 +func TestIm_Group_FetchMemberGroups(t *testing.T) { + ret, err := NewIM().Group().FetchMemberGroups(&group.FetchMemberGroupsArg{ + UserId: test1, + Limit: 3, + Offset: 0, + IsWithLiveRoomGroups: true, + IsWithNoActiveGroups: true, + }) + if err != nil { + handleError(t, "group.FetchMemberGroups", err) + } + + t.Log(ret.Total) + t.Log(ret.HasMore) + t.Log(ret.List) +} + +// 续拉取用户所加入的群组 +func TestIm_Group_PullMemberGroups(t *testing.T) { + err := NewIM().Group().PullMemberGroups(&group.PullMemberGroupsArg{ + UserId: test1, + Limit: 3, + IsWithLiveRoomGroups: true, + IsWithNoActiveGroups: true, + }, func(ret *group.FetchMemberGroupsRet) { + t.Log(ret.Total) + t.Log(ret.HasMore) + t.Log(ret.List) + }) + if err != nil { + handleError(t, "group.PullMemberGroups", err) + } +} + +// 批量禁言 +func TestIm_Group_ForbidSendMessage(t *testing.T) { + err := NewIM().Group().ForbidSendMessage("test_group1", []string{ + test1, + }, 1000) + if err != nil { + handleError(t, "group.ForbidSendMessage", err) + } + + t.Log("Success") +} + +// 取消禁言 +func TestIm_Group_AllowSendMessage(t *testing.T) { + err := NewIM().Group().AllowSendMessage("test_group2", []string{ + test1, + test2, + }) + if err != nil { + t.Fatal(err) + } + + t.Log("Success") +} + +// 获取被禁言群成员列表 +func TestIm_Group_GetShuttedUpMembers(t *testing.T) { + shuttedUps, err := NewIM().Group().GetShuttedUpMembers("test_group2") + if err != nil { + handleError(t, "group.GetShuttedUpMembers", err) + } + + t.Log(shuttedUps) +} + +// 撤回单条群消息 +func TestIm_Group_RevokeMessage(t *testing.T) { + err := NewIM().Group().RevokeMessage("test_group2", 100) + if err != nil { + handleError(t, "group.RevokeMessage", err) + } + + t.Log("Success") +} + +// 撤回多条群消息 +func TestIm_Group_RevokeMessages(t *testing.T) { + results, err := NewIM().Group().RevokeMessages("test_group2", 100) + if err != nil { + handleError(t, "group.RevokeMessages", err) + } + + t.Log(results) +} + +// 设置成员未读消息计数 +func TestIm_Group_SetMemberUnreadMsgNum(t *testing.T) { + err := NewIM().Group().SetMemberUnreadMsgNum("test_group2", test1, 100) + if err != nil { + handleError(t, "group.SetMemberUnreadMsgNum", err) + } + + t.Log("Success") +} + +// 撤回指定用户发送的消息 +func TestIm_Group_RevokeMemberMessages(t *testing.T) { + err := NewIM().Group().RevokeMemberMessages("test_group2", test1) + if err != nil { + handleError(t, "group.RevokeMemberMessages", err) + } + + t.Log("Success") +} + +// 在群组中发送普通消息 +func TestIm_Group_SendMessage(t *testing.T) { + message := group.NewMessage() + message.SetSender(test1) + message.SetNoLastMsg() + message.SetPriority("first") + // message.SetOnlineOnlyFlag(group.OnlineOnlyFlagYes) + message.SetContent(private.MsgTextContent{ + Text: "Hello world", + }) + message.OfflinePush().SetTitle("你好腾讯IM") + message.OfflinePush().SetDesc("你好腾讯IM,我来了~~~") + message.OfflinePush().SetPushFlag(private.PushFlagYes) + message.OfflinePush().SetExt(map[string]interface{}{ + "url": "http://www.tencent.com", + }) + message.OfflinePush().SetAndroidExtAsHuaweiIntentParam(private.HuaweiIntentParamIntent) + message.OfflinePush().SetApnsBadgeMode(private.BadgeModeNormal) + message.AtAllMembers() + message.AtMembers(test2) + message.ClearAtMembers() + + ret, err := NewIM().Group().SendMessage("test_group2", message) + if err != nil { + handleError(t, "group.SendMessage", err) + } + + t.Log(ret.MsgSeq) + t.Log(ret.MsgTime) +} + +// 在群组中发送普通消息 +func TestIm_Group_SendNotification(t *testing.T) { + err := NewIM().Group().SendNotification("test_group2", "Hello welcome to the test group", test1) + if err != nil { + handleError(t, "group.SendNotification", err) + } + + t.Log("Success") +} + +// 导入群基础资料 +func TestIm_Group_ImportGroup(t *testing.T) { + g := group.NewGroup() + g.SetName("测试群1") + g.SetGroupType(group.TypePublic) + g.SetMaxMemberNum(30) + g.SetAvatar("http://www.baidu.com") + g.SetIntroduction("这是一个测试群") + g.SetNotification("这是一个测试群公告") + + groupId, err := NewIM().Group().ImportGroup(g) + if err != nil { + handleError(t, "group.ImportGroup", err) + } + + t.Log(groupId) +} + +// 导入群消息 +func TestIm_Group_ImportMessages(t *testing.T) { + message := group.NewMessage() + message.SetSender(test1) + message.SetSendTime(time.Now().Unix()) + message.SetRandom(rand.Uint32()) + message.SetContent(private.MsgTextContent{ + Text: "Hello world", + }) + + results, err := NewIM().Group().ImportMessages("test_group2", message) + if err != nil { + handleError(t, "group.ImportMessages", err) + } + + t.Log(results) +} + +// 导入多个群成员 +func TestIm_Group_ImportMembers(t *testing.T) { + members := make([]*group.Member, 0) + for i := 1; i < 10; i++ { + member := group.NewMember() + member.SetUserId("test" + strconv.Itoa(i)) + member.SetRole("Admin") + member.SetUnreadMsgNum(10) + member.SetJoinTime(time.Now()) + members = append(members, member) + } + + results, err := NewIM().Group().ImportMembers("test_group2", members...) + if err != nil { + handleError(t, "group.ImportMembers", err) + } + + t.Log(results) +} + +// 拉取群历史消息 +func TestIm_Group_FetchMessages(t *testing.T) { + ret, err := NewIM().Group().FetchMessages("test_group2", 5) + if err != nil { + handleError(t, "group.FetchMessages", err) + } + + t.Log(ret) +} + +// 续拉取群历史消息 +func TestIm_Group_PullMessages(t *testing.T) { + err := NewIM().Group().PullMessages("test_group2", 5, func(ret *group.FetchMessagesRet) { + t.Log(ret) + }) + if err != nil { + handleError(t, "group.PullMessages", err) + } +} + +// 拉取会话列表 +func TestIm_RecentContact_FetchSessions(t *testing.T) { + ret, err := NewIM().RecentContact().FetchSessions(&recentcontact.FetchSessionsArg{ + UserId: assistant, + }) + if err != nil { + handleError(t, "recentcontact.FetchSessions", err) + } + + t.Log(ret) + t.Log("Success") +} + +// 拉取会话列表 +func TestIm_RecentContact_PullSessions(t *testing.T) { + err := NewIM().RecentContact().PullSessions(&recentcontact.PullSessionsArg{ + UserId: assistant, + }, func(ret *recentcontact.FetchSessionsRet) { + t.Log(ret) + }) + if err != nil { + handleError(t, "recentcontact.PullSessions", err) + } + t.Log("Success") +} + +// 删除单个会话 +func TestIm_RecentContact_DeleteSession(t *testing.T) { + err := NewIM().RecentContact().DeleteSession(assistant, test1, recentcontact.SessionTypeC2C, true) + if err != nil { + handleError(t, "recentcontact.DeleteSession", err) + } + t.Log("Success") +} diff --git a/internal/conv/conv.go b/internal/conv/conv.go new file mode 100644 index 0000000..785c076 --- /dev/null +++ b/internal/conv/conv.go @@ -0,0 +1,108 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/31 11:16 下午 + * @Desc: TODO + */ + +package conv + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "time" +) + +type stringInterface interface { + String() string +} + +type errorInterface interface { + Error() string +} + +func String(any interface{}) string { + switch v := any.(type) { + case nil: + return "" + case string: + return v + case int: + return strconv.Itoa(v) + case int8: + return strconv.Itoa(int(v)) + case int16: + return strconv.Itoa(int(v)) + case int32: + return strconv.Itoa(int(v)) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint16: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case bool: + return strconv.FormatBool(v) + case []byte: + return string(v) + case time.Time: + return v.String() + case *time.Time: + if v == nil { + return "" + } + return v.String() + default: + if v == nil { + return "" + } + + if i, ok := v.(stringInterface); ok { + return i.String() + } + + if i, ok := v.(errorInterface); ok { + return i.Error() + } + + var ( + rv = reflect.ValueOf(v) + kind = rv.Kind() + ) + + switch kind { + case reflect.Chan, + reflect.Map, + reflect.Slice, + reflect.Func, + reflect.Ptr, + reflect.Interface, + reflect.UnsafePointer: + if rv.IsNil() { + return "" + } + case reflect.String: + return rv.String() + } + + if kind == reflect.Ptr { + return String(rv.Elem().Interface()) + } + + if b, e := json.Marshal(v); e != nil { + return fmt.Sprint(v) + } else { + return string(b) + } + } +} diff --git a/internal/core/client.go b/internal/core/client.go new file mode 100644 index 0000000..21c618c --- /dev/null +++ b/internal/core/client.go @@ -0,0 +1,146 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/27 11:31 上午 + * @Desc: TODO + */ + +package core + +import ( + "fmt" + "math/rand" + "time" + + "git.echol.cn/loser/http" + + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/sign" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + defaultBaseUrl = "https://console.tim.qq.com" + defaultVersion = "v4" + defaultContentType = "json" + defaultExpiration = 3600 +) + +var invalidResponse = NewError(enum.InvalidResponseCode, "invalid response") + +type Client interface { + // Get GET请求 + Get(serviceName string, command string, data interface{}, resp interface{}) error + // Post POST请求 + Post(serviceName string, command string, data interface{}, resp interface{}) error + // Put PUT请求 + Put(serviceName string, command string, data interface{}, resp interface{}) error + // Patch PATCH请求 + Patch(serviceName string, command string, data interface{}, resp interface{}) error + // Delete DELETE请求 + Delete(serviceName string, command string, data interface{}, resp interface{}) error +} + +type client struct { + client *http.Client + opt *Options + userSig string + userSigExpireAt int64 +} + +type Options struct { + AppId int // 应用SDKAppID,可在即时通信 IM 控制台 的应用卡片中获取。 + AppSecret string // 密钥信息,可在即时通信 IM 控制台 的应用详情页面中获取,具体操作请参见 获取密钥 + UserId string // 用户ID + Expiration int // UserSig过期时间 +} + +func NewClient(opt *Options) Client { + rand.Seed(time.Now().UnixNano()) + c := new(client) + c.opt = opt + c.client = http.NewClient() + c.client.SetContentType(http.ContentTypeJson) + c.client.SetBaseUrl(defaultBaseUrl) + + return c +} + +// Get GET请求 +func (c *client) Get(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodGet, serviceName, command, data, resp) +} + +// Post POST请求 +func (c *client) Post(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodPost, serviceName, command, data, resp) +} + +// Put PUT请求 +func (c *client) Put(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodPut, serviceName, command, data, resp) +} + +// Patch PATCH请求 +func (c *client) Patch(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodPatch, serviceName, command, data, resp) +} + +// Delete DELETE请求 +func (c *client) Delete(serviceName string, command string, data interface{}, resp interface{}) error { + return c.request(http.MethodDelete, serviceName, command, data, resp) +} + +// request Request请求 +func (c *client) request(method, serviceName, command string, data, resp interface{}) error { + res, err := c.client.Request(method, c.buildUrl(serviceName, command), data) + if err != nil { + return err + } + + if err = res.Scan(resp); err != nil { + return err + } + + if r, ok := resp.(types.ActionBaseRespInterface); ok { + if r.GetActionStatus() == enum.FailActionStatus { + return NewError(r.GetErrorCode(), r.GetErrorInfo()) + } + + if r.GetErrorCode() != enum.SuccessCode { + return NewError(r.GetErrorCode(), r.GetErrorInfo()) + } + } else if r, ok := resp.(types.BaseRespInterface); ok { + if r.GetErrorCode() != enum.SuccessCode { + return NewError(r.GetErrorCode(), r.GetErrorInfo()) + } + } else { + return invalidResponse + } + + return nil +} + +// buildUrl 构建一个请求URL +func (c *client) buildUrl(serviceName string, command string) string { + format := "/%s/%s/%s?sdkappid=%d&identifier=%s&usersig=%s&random=%d&contenttype=%s" + random := rand.Int31() + userSig := c.getUserSig() + return fmt.Sprintf(format, defaultVersion, serviceName, command, c.opt.AppId, c.opt.UserId, userSig, random, defaultContentType) +} + +// getUserSig 获取签名 +func (c *client) getUserSig() string { + now, expiration := time.Now(), c.opt.Expiration + + if expiration <= 0 { + expiration = defaultExpiration + } + + if c.userSig == "" || c.userSigExpireAt <= now.Unix() { + c.userSig, _ = sign.GenUserSig(c.opt.AppId, c.opt.AppSecret, c.opt.UserId, expiration) + c.userSigExpireAt = now.Add(time.Duration(expiration) * time.Second).Unix() + } + + return c.userSig +} diff --git a/internal/core/error.go b/internal/core/error.go new file mode 100644 index 0000000..eb1dc4d --- /dev/null +++ b/internal/core/error.go @@ -0,0 +1,38 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/27 1:12 下午 + * @Desc: TODO + */ + +package core + +type Error interface { + error + Code() int + Message() string +} + +type respError struct { + code int + message string +} + +func NewError(code int, message string) Error { + return &respError{ + code: code, + message: message, + } +} + +func (e *respError) Error() string { + return e.message +} + +func (e *respError) Code() int { + return e.code +} + +func (e *respError) Message() string { + return e.message +} diff --git a/internal/entity/mesage.go b/internal/entity/mesage.go new file mode 100644 index 0000000..6490e7c --- /dev/null +++ b/internal/entity/mesage.go @@ -0,0 +1,170 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/3 18:19 + * @Desc: 基础消息实体 + */ + +package entity + +import ( + "errors" + "math/rand" + + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +var ( + errInvalidMsgContent = errors.New("invalid message content") + errInvalidMsgLifeTime = errors.New("invalid message life time") + errNotSetMsgContent = errors.New("message content is not set") +) + +type Message struct { + sender string // 发送方UserId + lifeTime int // 消息离线保存时长(单位:秒),最长为7天(604800秒) + random uint32 // 消息随机数,由随机函数产生 + body []*types.MsgBody // 消息体 + offlinePush *offlinePush // 推送实体 +} + +// SetSender 设置发送方UserId +func (m *Message) SetSender(userId string) { + m.sender = userId +} + +// GetSender 获取发送者 +func (m *Message) GetSender() string { + return m.sender +} + +// SetLifeTime 设置消息离线保存时长 +func (m *Message) SetLifeTime(lifeTime int) { + m.lifeTime = lifeTime +} + +// GetLifeTime 获取消息离线保存时长 +func (m *Message) GetLifeTime() int { + return m.lifeTime +} + +// SetRandom 设置消息随机数 +func (m *Message) SetRandom(random uint32) { + m.random = random +} + +// GetRandom 获取消息随机数 +func (m *Message) GetRandom() uint32 { + if m.random == 0 { + m.random = rand.Uint32() + } + + return m.random +} + +// AddContent 添加消息内容(添加会累加之前的消息内容) +func (m *Message) AddContent(msgContent ...interface{}) { + if m.body == nil { + m.body = make([]*types.MsgBody, 0) + } + + if len(msgContent) > 0 { + var msgType string + for _, content := range msgContent { + switch content.(type) { + case types.MsgTextContent, *types.MsgTextContent: + msgType = enum.MsgText + case types.MsgLocationContent, *types.MsgLocationContent: + msgType = enum.MsgLocation + case types.MsgFaceContent, *types.MsgFaceContent: + msgType = enum.MsgFace + case types.MsgCustomContent, *types.MsgCustomContent: + msgType = enum.MsgCustom + case types.MsgSoundContent, *types.MsgSoundContent: + msgType = enum.MsgSound + case types.MsgImageContent, *types.MsgImageContent: + msgType = enum.MsgImage + case types.MsgFileContent, *types.MsgFileContent: + msgType = enum.MsgFile + case types.MsgVideoContent, *types.MsgVideoContent: + msgType = enum.MsgVideo + default: + msgType = "" + } + + m.body = append(m.body, &types.MsgBody{ + MsgType: msgType, + MsgContent: content, + }) + } + } +} + +// SetContent 设置消息内容(设置会冲掉之前的消息内容) +func (m *Message) SetContent(msgContent ...interface{}) { + if m.body != nil { + m.body = m.body[0:0] + } + m.AddContent(msgContent...) +} + +// GetBody 获取消息体 +func (m *Message) GetBody() []*types.MsgBody { + return m.body +} + +// OfflinePush 新建离线推送对象 +func (m *Message) OfflinePush() *offlinePush { + if m.offlinePush == nil { + m.offlinePush = newOfflinePush() + } + + return m.offlinePush +} + +// GetOfflinePushInfo 获取离线推送消息 +func (m *Message) GetOfflinePushInfo() *types.OfflinePushInfo { + if m.offlinePush == nil { + return nil + } + + return &types.OfflinePushInfo{ + PushFlag: m.offlinePush.pushFlag, + Title: m.offlinePush.title, + Desc: m.offlinePush.desc, + Ext: m.offlinePush.ext, + AndroidInfo: m.offlinePush.androidInfo, + ApnsInfo: m.offlinePush.apnsInfo, + } +} + +// CheckLifeTimeArgError 检测参数错误 +func (m *Message) CheckLifeTimeArgError() error { + if m.body != nil && len(m.body) > 0 { + for _, item := range m.body { + if item.MsgType == "" { + return errInvalidMsgContent + } + } + } else { + return errNotSetMsgContent + } + + return nil +} + +// CheckBodyArgError 检测参数错误 +func (m *Message) CheckBodyArgError() error { + if m.body != nil && len(m.body) > 0 { + for _, item := range m.body { + if item.MsgType == "" { + return errInvalidMsgContent + } + } + } else { + return errNotSetMsgContent + } + + return nil +} diff --git a/internal/entity/offline_push.go b/internal/entity/offline_push.go new file mode 100644 index 0000000..8658051 --- /dev/null +++ b/internal/entity/offline_push.go @@ -0,0 +1,150 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/3 18:47 + * @Desc: 离线推送 + */ + +package entity + +import ( + "git.echol.cn/loser/tencent-im/internal/conv" + "git.echol.cn/loser/tencent-im/internal/types" +) + +type offlinePush struct { + pushFlag int // 推送标识。0表示推送,1表示不离线推送。 + title string // 离线推送标题。该字段为 iOS 和 Android 共用。 + desc string // 离线推送内容。 + ext string // 离线推送透传内容。 + androidInfo *types.AndroidInfo // Android离线推送消息 + apnsInfo *types.ApnsInfo // IOS离线推送消息 +} + +func newOfflinePush() *offlinePush { + return &offlinePush{} +} + +// SetPushFlag 设置推送消息 +func (o *offlinePush) SetPushFlag(pushFlag types.PushFlag) { + o.pushFlag = int(pushFlag) +} + +// SetTitle 设置离线推送标题 +func (o *offlinePush) SetTitle(title string) { + o.title = title +} + +// SetDesc 设置离线推送内容 +func (o *offlinePush) SetDesc(desc string) { + o.desc = desc +} + +// SetExt 设置离线推送透传内容 +func (o *offlinePush) SetExt(ext interface{}) { + o.ext = conv.String(ext) +} + +// SetAndroidSound 设置Android离线推送声音文件路径 +func (o *offlinePush) SetAndroidSound(sound string) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.Sound = sound +} + +// SetAndroidHuaWeiChannelId 设置华为手机 EMUI 10.0 及以上的通知渠道字段 +func (o *offlinePush) SetAndroidHuaWeiChannelId(channelId string) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.HuaWeiChannelID = channelId +} + +// SetAndroidXiaoMiChannelId 设置小米手机 MIUI 10 及以上的通知类别(Channel)适配字段 +func (o *offlinePush) SetAndroidXiaoMiChannelId(channelId string) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.XiaoMiChannelID = channelId +} + +// SetAndroidOppoChannelId 设置OPPO手机 Android 8.0 及以上的 NotificationChannel 通知适配字段 +func (o *offlinePush) SetAndroidOppoChannelId(channelId string) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.OPPOChannelID = channelId +} + +// SetAndroidGoogleChannelId 设置Google 手机 Android 8.0 及以上的通知渠道字段 +func (o *offlinePush) SetAndroidGoogleChannelId(channelId string) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.GoogleChannelID = channelId +} + +// SetAndroidVivoClassification 设置VIVO 手机推送消息分类,“0”代表运营消息,“1”代表系统消息,不填默认为1 +func (o *offlinePush) SetAndroidVivoClassification(classification types.VivoClassification) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.VIVOClassification = int(classification) +} + +// SetAndroidHuaWeiImportance 设置华为推送通知消息分类 +func (o *offlinePush) SetAndroidHuaWeiImportance(importance types.HuaWeiImportance) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.HuaWeiImportance = string(importance) +} + +// SetAndroidExtAsHuaweiIntentParam 设置在控制台配置华为推送为“打开应用内指定页面”的前提下,传“1”表示将透传内容 Ext 作为 Intent 的参数,“0”表示将透传内容 Ext 作为 Action 参数。不填默认为0。 +func (o *offlinePush) SetAndroidExtAsHuaweiIntentParam(param types.HuaweiIntentParam) { + if o.androidInfo == nil { + o.androidInfo = &types.AndroidInfo{} + } + o.androidInfo.ExtAsHuaweiIntentParam = int(param) +} + +// SetApnsBadgeMode 设置IOS徽章计数模式 +func (o *offlinePush) SetApnsBadgeMode(badgeMode types.BadgeMode) { + if o.apnsInfo == nil { + o.apnsInfo = &types.ApnsInfo{} + } + o.apnsInfo.BadgeMode = int(badgeMode) +} + +// SetApnsTitle 设置APNs推送的标题 +func (o *offlinePush) SetApnsTitle(title string) { + if o.apnsInfo == nil { + o.apnsInfo = &types.ApnsInfo{} + } + o.apnsInfo.Title = title +} + +// SetApnsSubTitle 设置APNs推送的子标题 +func (o *offlinePush) SetApnsSubTitle(subTitle string) { + if o.apnsInfo == nil { + o.apnsInfo = &types.ApnsInfo{} + } + o.apnsInfo.SubTitle = subTitle +} + +// SetApnsImage 设置APNs携带的图片地址 +func (o *offlinePush) SetApnsImage(image string) { + if o.apnsInfo == nil { + o.apnsInfo = &types.ApnsInfo{} + } + o.apnsInfo.Image = image +} + +// SetApnsMutableContent 设置iOS10的推送扩展开关 +func (o *offlinePush) SetApnsMutableContent(mutable types.MutableContent) { + if o.apnsInfo == nil { + o.apnsInfo = &types.ApnsInfo{} + } + o.apnsInfo.MutableContent = int(mutable) +} diff --git a/internal/entity/user.go b/internal/entity/user.go new file mode 100644 index 0000000..c868a76 --- /dev/null +++ b/internal/entity/user.go @@ -0,0 +1,330 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/30 4:23 下午 + * @Desc: 用户 + */ + +package entity + +import ( + "fmt" + "strconv" + "strings" + "time" + + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +type User struct { + userId string + attrs map[string]interface{} + err error +} + +// SetUserId 设置用户账号 +func (u *User) SetUserId(userId string) { + u.userId = userId +} + +// GetUserId 获取用户账号 +func (u *User) GetUserId() string { + return u.userId +} + +// SetNickname 设置昵称 +func (u *User) SetNickname(nickname string) { + u.SetAttr(enum.StandardAttrNickname, nickname) +} + +// GetNickname 获取昵称 +func (u *User) GetNickname() (nickname string, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrNickname); exist { + nickname = v.(string) + } + + return +} + +// SetGender 设置性别 +func (u *User) SetGender(gender types.GenderType) { + u.SetAttr(enum.StandardAttrGender, gender) +} + +// GetGender 获取性别 +func (u *User) GetGender() (gender types.GenderType, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrGender); exist { + gender = types.GenderType(v.(string)) + } + + return +} + +// SetBirthday 设置生日 +func (u *User) SetBirthday(birthday time.Time) { + b, _ := strconv.Atoi(birthday.Format("20060102")) + u.SetAttr(enum.StandardAttrBirthday, b) +} + +// GetBirthday 获取昵称 +func (u *User) GetBirthday() (birthday time.Time, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrBirthday); exist { + if val := v.(string); val != "" { + birthday, _ = time.Parse("20060102", val) + } + } + + return +} + +// SetLocation 设置所在地 +func (u *User) SetLocation(country uint32, province uint32, city uint32, region uint32) { + var ( + str string + location = []uint32{country, province, city, region} + builder strings.Builder + ) + + builder.Grow(16) + + for _, v := range location { + str = strconv.Itoa(int(v)) + + if len(str) > 4 { + u.SetError(enum.InvalidParamsCode, "invalid location params") + break + } + + builder.WriteString(strings.Repeat("0", 4-len(str))) + builder.WriteString(str) + } + + u.SetAttr(enum.StandardAttrLocation, builder.String()) +} + +// GetLocation 获取所在地 +func (u *User) GetLocation() (country uint32, province uint32, city uint32, region uint32, exist bool) { + var v interface{} + + if v, exist = u.attrs[enum.StandardAttrLocation]; exist { + str := v.(string) + + if len(str) != 16 { + exist = false + return + } + + if c, err := strconv.Atoi(str[0:4]); err != nil || c < 0 { + exist = false + return + } else { + country = uint32(c) + } + + if c, err := strconv.Atoi(str[4:8]); err != nil || c < 0 { + exist = false + return + } else { + province = uint32(c) + } + + if c, err := strconv.Atoi(str[8:12]); err != nil || c < 0 { + exist = false + return + } else { + city = uint32(c) + } + + if c, err := strconv.Atoi(str[12:16]); err != nil || c < 0 { + exist = false + return + } else { + region = uint32(c) + } + } + + return +} + +// SetSignature 设置个性签名 +func (u *User) SetSignature(signature string) { + u.SetAttr(enum.StandardAttrSignature, signature) +} + +// GetSignature 获取个性签名 +func (u *User) GetSignature() (signature string, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrSignature); exist { + signature = v.(string) + } + + return +} + +// SetAllowType 设置加好友验证方式 +func (u *User) SetAllowType(allowType types.AllowType) { + u.SetAttr(enum.StandardAttrAllowType, allowType) +} + +// GetAllowType 获取加好友验证方式 +func (u *User) GetAllowType() (allowType types.AllowType, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrAllowType); exist { + allowType = types.AllowType(v.(string)) + } + + return +} + +// SetLanguage 设置语言 +func (u *User) SetLanguage(language uint) { + u.SetAttr(enum.StandardAttrLanguage, language) +} + +// GetLanguage 获取语言 +func (u *User) GetLanguage() (language uint, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrLanguage); exist { + language = uint(v.(float64)) + } + + return +} + +// SetAvatar 设置头像URL +func (u *User) SetAvatar(avatar string) { + u.SetAttr(enum.StandardAttrAvatar, avatar) +} + +// GetAvatar 获取头像URL +func (u *User) GetAvatar() (avatar string, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrAvatar); exist { + avatar = v.(string) + } + + return +} + +// SetMsgSettings 设置消息设置 +func (u *User) SetMsgSettings(settings uint) { + u.SetAttr(enum.StandardAttrMsgSettings, settings) +} + +// GetMsgSettings 获取消息设置 +func (u *User) GetMsgSettings() (settings uint, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrMsgSettings); exist { + settings = uint(v.(float64)) + } + + return +} + +// SetAdminForbidType 设置管理员禁止加好友标识 +func (u *User) SetAdminForbidType(forbidType types.AdminForbidType) { + u.SetAttr(enum.StandardAttrAdminForbidType, forbidType) +} + +// GetAdminForbidType 获取管理员禁止加好友标识 +func (u *User) GetAdminForbidType() (forbidType types.AdminForbidType, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrAdminForbidType); exist { + forbidType = types.AdminForbidType(v.(string)) + } + + return +} + +// SetLevel 设置等级 +func (u *User) SetLevel(level uint) { + u.SetAttr(enum.StandardAttrLevel, level) +} + +// GetLevel 获取等级 +func (u *User) GetLevel() (level uint, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrLevel); exist { + level = uint(v.(float64)) + } + + return +} + +// SetRole 设置角色 +func (u *User) SetRole(role uint) { + u.SetAttr(enum.StandardAttrRole, role) +} + +// GetRole 获取角色 +func (u *User) GetRole() (role uint, exist bool) { + var v interface{} + + if v, exist = u.GetAttr(enum.StandardAttrRole); exist { + role = uint(v.(float64)) + } + + return +} + +// SetCustomAttr 设置自定义属性 +func (u *User) SetCustomAttr(name string, value interface{}) { + u.SetAttr(fmt.Sprintf("%s_%s", enum.CustomAttrPrefix, name), value) +} + +// GetCustomAttr 获取自定义属性 +func (u *User) GetCustomAttr(name string) (val interface{}, exist bool) { + val, exist = u.GetAttr(fmt.Sprintf("%s_%s", enum.CustomAttrPrefix, name)) + return +} + +// IsValid 检测用户是否有效 +func (u *User) IsValid() bool { + return u.err == nil +} + +// SetError 设置异常错误 +func (u *User) SetError(code int, message string) { + if code != enum.SuccessCode { + u.err = core.NewError(code, message) + } +} + +// GetError 获取异常错误 +func (u *User) GetError() error { + return u.err +} + +// SetAttr 设置属性 +func (u *User) SetAttr(name string, value interface{}) { + if u.attrs == nil { + u.attrs = make(map[string]interface{}) + } + u.attrs[name] = value +} + +// GetAttr 获取属性 +func (u *User) GetAttr(name string) (value interface{}, exist bool) { + value, exist = u.attrs[name] + return +} + +// GetAttrs 获取所有属性 +func (u *User) GetAttrs() map[string]interface{} { + return u.attrs +} diff --git a/internal/enum/code.go b/internal/enum/code.go new file mode 100644 index 0000000..6ee9e94 --- /dev/null +++ b/internal/enum/code.go @@ -0,0 +1,16 @@ +/** + * @Author: wanglin + * @Author: wanglin@vspn.com + * @Date: 2021/11/3 10:45 + * @Desc: TODO + */ + +package enum + +const ( + SuccessActionStatus = "OK" // 成功状态 + FailActionStatus = "FAIL" // 失败状态 + SuccessCode = 0 // 成功 + InvalidParamsCode = -1 // 无效参数(自定义) + InvalidResponseCode = -2 // 无效响应(自定义) +) diff --git a/internal/enum/enum.go b/internal/enum/enum.go new file mode 100644 index 0000000..488d7e9 --- /dev/null +++ b/internal/enum/enum.go @@ -0,0 +1,95 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/28 1:14 上午 + * @Desc: TODO + */ + +package enum + +import ( + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + // 消息类型 + MsgText = "TIMTextElem" // 消息元素 + MsgLocation = "TIMLocationElem" // 地理位置消息元素 + MsgFace = "TIMFaceElem" // 表情消息元素 + MsgCustom = "TIMCustomElem" // 自定义消息元素 + MsgSound = "TIMSoundElem" // 语音消息元素 + MsgImage = "TIMImageElem" // 图像消息元素 + MsgFile = "TIMFileElem" // 文件消息元素 + MsgVideo = "TIMVideoFileElem" // 视频消息元素 + + // 图片格式 + ImageFormatJPG = 1 // JPG格式 + ImageFormatGIF = 2 // GIF格式 + ImageFormatPNG = 3 // PNG格式 + ImageFormatBMP = 4 // BMP格式 + ImageFormatOTHER = 255 // 其他格式 + + // 图片类型 + ImageTypeOriginal = 1 // 原图 + ImageTypePic = 2 // 大图 + ImageTypeThumb = 3 // 缩略图 + + // 标准资料字段 + StandardAttrNickname = "Tag_Profile_IM_Nick" // 昵称 + StandardAttrGender = "Tag_Profile_IM_Gender" // 性别 + StandardAttrBirthday = "Tag_Profile_IM_BirthDay" // 生日 + StandardAttrLocation = "Tag_Profile_IM_Location" // 所在地 + StandardAttrSignature = "Tag_Profile_IM_SelfSignature" // 个性签名 + StandardAttrAllowType = "Tag_Profile_IM_AllowType" // 加好友验证方式 + StandardAttrLanguage = "Tag_Profile_IM_Language" // 语言 + StandardAttrAvatar = "Tag_Profile_IM_Image" // 头像URL + StandardAttrMsgSettings = "Tag_Profile_IM_MsgSettings" // 消息设置 + StandardAttrAdminForbidType = "Tag_Profile_IM_AdminForbidType" // 管理员禁止加好友标识 + StandardAttrLevel = "Tag_Profile_IM_Level" // 等级 + StandardAttrRole = "Tag_Profile_IM_Role" // 角色 + + // 自定义属性前缀 + CustomAttrPrefix = "Tag_Profile_Custom" // 自定义属性前缀 + + // 性别类型 + GenderTypeUnknown types.GenderType = "Gender_Type_Unknown" // 没设置性别 + GenderTypeFemale types.GenderType = "Gender_Type_Female" // 女性 + GenderTypeMale types.GenderType = "Gender_Type_Male" // 男性 + + // 加好友验证方式 + AllowTypeNeedConfirm types.AllowType = "AllowType_Type_NeedConfirm" // 需要经过自己确认对方才能添加自己为好友 + AllowTypeAllowAny types.AllowType = "AllowType_Type_AllowAny" // 允许任何人添加自己为好友 + AllowTypeDenyAny types.AllowType = "AllowType_Type_DenyAny" // 不允许任何人添加自己为好友 + + // 管理员禁止加好友标识类型 + AdminForbidTypeNone types.AdminForbidType = "AdminForbid_Type_None" // 默认值,允许加好友 + AdminForbidTypeSendOut types.AdminForbidType = "AdminForbid_Type_SendOut" // 禁止该用户发起加好友请求 + + // 同步至其他设备 + SyncOtherMachineYes types.SyncOtherMachine = 1 // 把消息同步到From_Account在线终端和漫游上 + SyncOtherMachineNo types.SyncOtherMachine = 2 // 消息不同步至From_Account + + // 推送标识 + PushFlagYes types.PushFlag = 0 // 正常推送 + PushFlagNo types.PushFlag = 1 // 不离线推送 + + // 华为推送通知消息分类 + HuaWeiImportanceLow types.HuaWeiImportance = "LOW" // LOW类消息 + HuaWeiImportanceNormal types.HuaWeiImportance = "NORMAL" // NORMAL类消息 + + // 华为推送为“打开应用内指定页面”的前提下透传参数行为 + HuaweiIntentParamAction types.HuaweiIntentParam = 0 // 将透传内容Ext作为Action参数 + HuaweiIntentParamIntent types.HuaweiIntentParam = 1 // 将透传内容Ext作为Intent参数 + + // VIVO手机推送消息分类 + VivoClassificationOperation types.VivoClassification = 0 // 运营类消息 + VivoClassificationSystem types.VivoClassification = 1 // 系统类消息 + + // IOS徽章计数模式 + BadgeModeNormal types.BadgeMode = 0 // 本条消息需要计数 + BadgeModeIgnore types.BadgeMode = 1 // 本条消息不需要计数 + + // iOS10的推送扩展开关 + MutableContentNormal types.MutableContent = 0 // 关闭iOS10的推送扩展 + MutableContentEnable types.MutableContent = 1 // 开启iOS10的推送扩展 +) diff --git a/internal/random/random.go b/internal/random/random.go new file mode 100644 index 0000000..59acd97 --- /dev/null +++ b/internal/random/random.go @@ -0,0 +1,63 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/12 10:35 + * @Desc: 随机数类库 + */ + +package random + +import ( + "math/rand" + "time" +) + +const ( + AlphaStr = iota // 字母字 + AlphaLowerStr // 小写字母 + AlphaUpperStr // 大写字母 + NumericStr // 数字 + NoZeroNumericStr // 无0数字 +) + +// GenStr 生成指定长度的字符串 +func GenStr(mode, length int) string { + var ( + pos int + lastStr string + seedStr string + ) + + rand.Seed(time.Now().UnixNano()) + switch mode { + case AlphaStr: + seedStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + case AlphaLowerStr: + seedStr = "abcdefghijklmnopqrstuvwxyz" + case AlphaUpperStr: + seedStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + case NumericStr: + seedStr = "0123456789" + case NoZeroNumericStr: + seedStr = "123456789" + } + + seedLen := len(seedStr) + for i := 0; i < length; i++ { + pos = rand.Intn(seedLen) + lastStr += seedStr[pos : pos+1] + } + + return lastStr +} + +// GenNumeric 生成指定范围的数字 +func GenNumeric(min int, max int) int { + rand.Seed(time.Now().UnixNano()) + + if min < max { + return rand.Intn(max-min) + min + } else { + return rand.Intn(min-max) + max + } +} diff --git a/internal/sign/base64.go b/internal/sign/base64.go new file mode 100644 index 0000000..1960a3b --- /dev/null +++ b/internal/sign/base64.go @@ -0,0 +1,30 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 19:15 + * @Desc: BASE64 + */ + +package sign + +import ( + "encoding/base64" + "strings" +) + +// base64Encode base64 encode a string. +func base64Encode(data []byte) string { + str := base64.StdEncoding.EncodeToString(data) + str = strings.Replace(str, "+", "*", -1) + str = strings.Replace(str, "/", "-", -1) + str = strings.Replace(str, "=", "_", -1) + return str +} + +// base64Decode base64 decode a string. +func base64Decode(str string) ([]byte, error) { + str = strings.Replace(str, "_", "=", -1) + str = strings.Replace(str, "-", "/", -1) + str = strings.Replace(str, "*", "+", -1) + return base64.StdEncoding.DecodeString(str) +} diff --git a/internal/sign/sign.go b/internal/sign/sign.go new file mode 100644 index 0000000..3b8fd24 --- /dev/null +++ b/internal/sign/sign.go @@ -0,0 +1,178 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 19:11 + * @Desc: Transmission signature. + */ + +package sign + +import ( + "bytes" + "compress/zlib" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "strconv" + "time" +) + +// GenUserSig gen a user sign. +func GenUserSig(sdkAppId int, key string, userid string, expire int) (string, error) { + return genUserSig(sdkAppId, key, userid, expire, nil) +} + +// GenPrivateMapKey gen a private map. +func GenPrivateMapKey(sdkAppId int, key string, userid string, expire int, roomId uint32, privilegeMap uint32) (string, error) { + var userBuf []byte = genUserBuf(userid, sdkAppId, roomId, expire, privilegeMap, 0, "") + return genUserSig(sdkAppId, key, userid, expire, userBuf) +} + +// GenPrivateMapKeyWithRoomId gen a private map with room id. +func GenPrivateMapKeyWithRoomId(sdkAppId int, key string, userid string, expire int, roomId string, privilegeMap uint32) (string, error) { + var userBuf []byte = genUserBuf(userid, sdkAppId, 0, expire, privilegeMap, 0, roomId) + return genUserSig(sdkAppId, key, userid, expire, userBuf) +} + +// genUserBuf gen a user buffer. +func genUserBuf(account string, dwSdkappid int, dwAuthID uint32, + dwExpTime int, dwPrivilegeMap uint32, dwAccountType uint32, roomStr string) []byte { + + offset := 0 + length := 1 + 2 + len(account) + 20 + len(roomStr) + if len(roomStr) > 0 { + length = length + 2 + } + + userBuf := make([]byte, length) + + // ver + if len(roomStr) > 0 { + userBuf[offset] = 1 + } else { + userBuf[offset] = 0 + } + + offset++ + userBuf[offset] = (byte)((len(account) & 0xFF00) >> 8) + offset++ + userBuf[offset] = (byte)(len(account) & 0x00FF) + offset++ + + for ; offset < len(account)+3; offset++ { + userBuf[offset] = account[offset-3] + } + + // dwSdkAppid + userBuf[offset] = (byte)((int64(dwSdkappid) & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwSdkappid & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwSdkappid & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwSdkappid & 0x000000FF) + offset++ + + // dwAuthId + userBuf[offset] = (byte)((dwAuthID & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwAuthID & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwAuthID & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwAuthID & 0x000000FF) + offset++ + + // dwExpTime now+300; + currTime := time.Now().Unix() + var expire = currTime + int64(dwExpTime) + userBuf[offset] = (byte)((expire & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((expire & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((expire & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(expire & 0x000000FF) + offset++ + + // dwPrivilegeMap + userBuf[offset] = (byte)((dwPrivilegeMap & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwPrivilegeMap & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwPrivilegeMap & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwPrivilegeMap & 0x000000FF) + offset++ + + // dwAccountType + userBuf[offset] = (byte)((dwAccountType & 0xFF000000) >> 24) + offset++ + userBuf[offset] = (byte)((dwAccountType & 0x00FF0000) >> 16) + offset++ + userBuf[offset] = (byte)((dwAccountType & 0x0000FF00) >> 8) + offset++ + userBuf[offset] = (byte)(dwAccountType & 0x000000FF) + offset++ + + if len(roomStr) > 0 { + userBuf[offset] = (byte)((len(roomStr) & 0xFF00) >> 8) + offset++ + userBuf[offset] = (byte)(len(roomStr) & 0x00FF) + offset++ + + for ; offset < length; offset++ { + userBuf[offset] = roomStr[offset-(length-len(roomStr))] + } + } + + return userBuf +} + +// hmacSha256 encrypt with HMAC SHA256. +func hmacSha256(sdkAppId int, key string, identifier string, currTime int64, expire int, base64UserBuf *string) string { + var contentToBeSigned string + contentToBeSigned = "TLS.identifier:" + identifier + "\n" + contentToBeSigned += "TLS.sdkappid:" + strconv.Itoa(sdkAppId) + "\n" + contentToBeSigned += "TLS.time:" + strconv.FormatInt(currTime, 10) + "\n" + contentToBeSigned += "TLS.expire:" + strconv.Itoa(expire) + "\n" + if nil != base64UserBuf { + contentToBeSigned += "TLS.userbuf:" + *base64UserBuf + "\n" + } + + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(contentToBeSigned)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +// genUserSig gen a sign +func genUserSig(sdkAppId int, key string, identifier string, expire int, userBuf []byte) (string, error) { + currTime := time.Now().Unix() + var sigDoc map[string]interface{} + sigDoc = make(map[string]interface{}) + sigDoc["TLS.ver"] = "2.0" + sigDoc["TLS.identifier"] = identifier + sigDoc["TLS.sdkappid"] = sdkAppId + sigDoc["TLS.expire"] = expire + sigDoc["TLS.time"] = currTime + var base64UserBuf string + if nil != userBuf { + base64UserBuf = base64.StdEncoding.EncodeToString(userBuf) + sigDoc["TLS.userbuf"] = base64UserBuf + sigDoc["TLS.sig"] = hmacSha256(sdkAppId, key, identifier, currTime, expire, &base64UserBuf) + } else { + sigDoc["TLS.sig"] = hmacSha256(sdkAppId, key, identifier, currTime, expire, nil) + } + + data, err := json.Marshal(sigDoc) + if err != nil { + return "", err + } + + var b bytes.Buffer + w := zlib.NewWriter(&b) + _, _ = w.Write(data) + _ = w.Close() + return base64Encode(b.Bytes()), nil +} diff --git a/internal/types/interface.go b/internal/types/interface.go new file mode 100644 index 0000000..f6a5240 --- /dev/null +++ b/internal/types/interface.go @@ -0,0 +1,30 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/27 12:54 下午 + * @Desc: TODO + */ + +package types + +type BaseRespInterface interface { + GetErrorCode() int + GetErrorInfo() string +} + +func (r *BaseResp) GetErrorCode() int { + return r.ErrorCode +} + +func (r *BaseResp) GetErrorInfo() string { + return r.ErrorInfo +} + +type ActionBaseRespInterface interface { + BaseRespInterface + GetActionStatus() string +} + +func (r *ActionBaseResp) GetActionStatus() string { + return r.ActionStatus +} diff --git a/internal/types/types.go b/internal/types/types.go new file mode 100644 index 0000000..cfc6c64 --- /dev/null +++ b/internal/types/types.go @@ -0,0 +1,171 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/28 19:24 + * @Desc: TODO + */ + +package types + +type ( + BaseResp struct { + ErrorCode int `json:"ErrorCode"` + ErrorInfo string `json:"ErrorInfo"` + ErrorDisplay string `json:"ErrorDisplay,omitempty"` + } + + ActionBaseResp struct { + BaseResp + ActionStatus string `json:"ActionStatus"` + } + + // AndroidInfo Android离线推送消息 + AndroidInfo struct { + Sound string `json:"Sound,omitempty"` // (选填)Android 离线推送声音文件路径。 + HuaWeiChannelID string `json:"HuaWeiChannelID,omitempty"` // (选填)华为手机 EMUI 10.0 及以上的通知渠道字段。该字段不为空时,会覆盖控制台配置的 ChannelID 值;该字段为空时,不会覆盖控制台配置的 ChannelID 值。 + XiaoMiChannelID string `json:"XiaoMiChannelID,omitempty"` // (选填)小米手机 MIUI 10 及以上的通知类别(Channel)适配字段。该字段不为空时,会覆盖控制台配置的 ChannelID 值;该字段为空时,不会覆盖控制台配置的 ChannelID 值。 + OPPOChannelID string `json:"OPPOChannelID,omitempty"` // (选填)OPPO 手机 Android 8.0 及以上的 NotificationChannel 通知适配字段。该字段不为空时,会覆盖控制台配置的 ChannelID 值;该字段为空时,不会覆盖控制台配置的 ChannelID 值。 + GoogleChannelID string `json:"GoogleChannelID,omitempty"` // (选填)Google 手机 Android 8.0 及以上的通知渠道字段。Google 推送新接口(上传证书文件)支持 channel id,旧接口(填写服务器密钥)不支持。 + VIVOClassification int `json:"VIVOClassification,omitempty"` // (选填)VIVO 手机推送消息分类,“0”代表运营消息,“1”代表系统消息,不填默认为1。 + HuaWeiImportance string `json:"HuaWeiImportance,omitempty"` // (选填)华为推送通知消息分类,取值为 LOW、NORMAL,不填默认为 NORMAL。 + ExtAsHuaweiIntentParam int `json:"ExtAsHuaweiIntentParam,omitempty"` // (选填)在控制台配置华为推送为“打开应用内指定页面”的前提下,传“1”表示将透传内容 Ext 作为 Intent 的参数,“0”表示将透传内容 Ext 作为 Action 参数。不填默认为0。两种传参区别可参见 华为推送文档。 + } + + // ApnsInfo IOS离线推送消息 + ApnsInfo struct { + BadgeMode int `json:"BadgeMode,omitempty"` // (选填)这个字段缺省或者为0表示需要计数,为1表示本条消息不需要计数,即右上角图标数字不增加。 + Title string `json:"Title,omitempty"` // (选填)该字段用于标识 APNs 推送的标题,若填写则会覆盖最上层 Title。 + SubTitle string `json:"SubTitle,omitempty"` // (选填)该字段用于标识 APNs 推送的子标题。 + Image string `json:"Image,omitempty"` // (选填)该字段用于标识 APNs 携带的图片地址,当客户端拿到该字段时,可以通过下载图片资源的方式将图片展示在弹窗上。 + MutableContent int `json:"MutableContent,omitempty"` // (选填)为1表示开启 iOS 10 的推送扩展,默认为0。 + } + + // OfflinePushInfo 离线推送消息 + OfflinePushInfo struct { + PushFlag int `json:"PushFlag,omitempty"` // (选填)推送标识。0表示推送,1表示不离线推送。 + Title string `json:"Title,omitempty"` // (选填)离线推送标题。该字段为 iOS 和 Android 共用。 + Desc string `json:"Desc,omitempty"` // (选填)离线推送内容。该字段会覆盖上面各种消息元素 TIMMsgElement 的离线推送展示文本。若发送的消息只有一个 TIMCustomElem 自定义消息元素,该 Desc 字段会覆盖 TIMCustomElem 中的 Desc 字段。如果两个 Desc 字段都不填,将收不到该自定义消息的离线推送。 + Ext string `json:"Ext,omitempty"` // (选填)离线推送透传内容。由于国内各 Android 手机厂商的推送平台要求各不一样,请保证此字段为 JSON 格式,否则可能会导致收不到某些厂商的离线推送。 + AndroidInfo *AndroidInfo `json:"AndroidInfo,omitempty"` // (选填)Android 离线推送消息 + ApnsInfo *ApnsInfo `json:"ApnsInfo,omitempty"` // (选填)IOS离线推送消息 + } + + // MsgBody 消息内容 + MsgBody struct { + MsgType string `json:"MsgType"` + MsgContent interface{} `json:"MsgContent"` + } + + // TagPair 标签对 + TagPair struct { + Tag string `json:"Tag"` // 标签 + Value interface{} `json:"Value"` // 标签值 + } + + // MsgTextContent 文本消息内容 + MsgTextContent struct { + Text string `json:"Text"` // (必填)消息内容。当接收方为 iOS 或 Android 后台在线时,作为离线推送的文本展示。 + } + + // MsgLocationContent 地理位置消息元素 + MsgLocationContent struct { + Desc string `json:"Desc"` // (必填)地理位置描述信息 + Latitude float64 `json:"Latitude"` // (必填)纬度 + Longitude float64 `json:"Longitude"` // (必填)经度 + } + + // MsgFaceContent 表情消息元素 + MsgFaceContent struct { + Index int `json:"Index"` // (必填)表情索引,用户自定义 + Data string `json:"Data"` // (选填)额外数据 + } + + // MsgCustomContent 自定义消息元素 + MsgCustomContent struct { + Desc string `json:"Desc"` // (选填)自定义消息描述信息。当接收方为 iOS 或 Android 后台在线时,做离线推送文本展示。 若发送自定义消息的同时设置了 OfflinePushInfo.Desc 字段,此字段会被覆盖,请优先填 OfflinePushInfo.Desc 字段。 + Data string `json:"Data"` // (必填)自定义消息数据。 不作为 APNs 的 payload 字段下发,故从 payload 中无法获取 Data 字段 + Ext string `json:"Ext"` // (选填)扩展字段。当接收方为 iOS 系统且应用处在后台时,此字段作为 APNs 请求包 Payloads 中的 Ext 键值下发,Ext 的协议格式由业务方确定,APNs 只做透传。 + Sound string `json:"Sound"` // (选填)自定义 APNs 推送铃音。 + } + + // MsgSoundContent 语音消息元素 + MsgSoundContent struct { + UUID string `json:"UUID"` // (必填)语音的唯一标识,类型为 String。客户端用于索引语音的键值。无法通过该字段下载相应的语音。若需要获取该语音,请升级 IM SDK 版本至4.X。 + Url string `json:"Url"` // (必填)语音下载地址,可通过该 URL 地址直接下载相应语音 + Size int `json:"Size"` // (必填)语音数据大小,单位:字节。 + Second int `json:"Second"` // (必填)语音时长,单位:秒。 + DownloadFlag int `json:"Download_Flag"` // (必填)语音下载方式标记。目前 Download_Flag 取值只能为2,表示可通过Url字段值的 URL 地址直接下载语音。 + } + + // MsgImageContent 图像消息元素 + MsgImageContent struct { + UUID string `json:"UUID"` // (必填)图片序列号。后台用于索引图片的键值。 + ImageFormat int `json:"ImageFormat"` // (必填)图片格式。JPG = 1,GIF = 2,PNG = 3,BMP = 4,其他 = 255。 + ImageInfos []*ImageInfo `json:"ImageInfoArray"` // (必填)原图、缩略图或者大图下载信息。 + } + + // MsgFileContent 文件消息元素 + MsgFileContent struct { + Url string `json:"Url"` // (必填)文件下载地址,可通过该 URL 地址直接下载相应文件 + UUID string `json:"UUID"` // (必填)文件的唯一标识,客户端用于索引文件的键值。 + FileSize int `json:"FileSize"` // (必填)文件数据大小,单位:字节 + FileName string `json:"FileName"` // (必填)文件名称 + DownloadFlag int `json:"Download_Flag"` // (必填)文件下载方式标记。目前 Download_Flag 取值只能为2,表示可通过Url字段值的 URL 地址直接下载文件。 + } + + // MsgVideoContent 视频消息元素 + MsgVideoContent struct { + VideoUUID string `json:"VideoUUID"` // (必填)视频的唯一标识,客户端用于索引视频的键值。 + VideoUrl string `json:"VideoUrl"` // (必填)视频下载地址。可通过该 URL 地址直接下载相应视频 + VideoSize int `json:"VideoSize"` // (必填)视频数据大小,单位:字节 + VideoSecond int `json:"VideoSecond"` // (必填)视频时长,单位:秒 + VideoFormat string `json:"VideoFormat"` // (必填)视频格式,例如 mp4 + VideoDownloadFlag int `json:"VideoDownloadFlag"` // (必填)视频下载方式标记。目前 VideoDownloadFlag 取值只能为2,表示可通过VideoUrl字段值的 URL 地址直接下载视频。 + ThumbUrl string `json:"ThumbUrl"` // (必填)视频缩略图下载地址。可通过该 URL 地址直接下载相应视频缩略图。 + ThumbUUID string `json:"ThumbUUID"` // (必填)视频缩略图的唯一标识,客户端用于索引视频缩略图的键值。 + ThumbSize int `json:"ThumbSize"` // (必填)缩略图大小,单位:字节 + ThumbWidth int `json:"ThumbWidth"` // (必填)缩略图宽度 + ThumbHeight int `json:"ThumbHeight"` // (必填)缩略图高度 + ThumbFormat string `json:"ThumbFormat"` // (必填)缩略图格式,例如 JPG、BMP 等 + ThumbDownloadFlag int `json:"ThumbDownloadFlag"` // (必填)视频缩略图下载方式标记。目前 ThumbDownloadFlag 取值只能为2,表示可通过ThumbUrl字段值的 URL 地址直接下载视频缩略图。 + } + + // ImageInfo 图片下载信息 + ImageInfo struct { + Type int `json:"Type"` // (必填)图片类型: 1-原图,2-大图,3-缩略图。 + Size int `json:"Size"` // (必填)图片数据大小,单位:字节。 + Width int `json:"Width"` // (必填)图片宽度。 + Height int `json:"Height"` // (必填)图片高度。 + Url string `json:"URL"` // (必填)图片下载地址。 + } + + // GenderType 性别类型 + GenderType string + + // AllowType 加好友验证方式 + AllowType string + + // AdminForbidType 管理员禁止加好友标识类型 + AdminForbidType string + + // SyncOtherMachine 同步至其他设备 + SyncOtherMachine int + + // PushFlag 推送标识 + PushFlag int + + // HuaWeiImportance 华为推送通知消息分类 + HuaWeiImportance string + + // HuaweiIntentParam 华为推送为“打开应用内指定页面”的前提下透传参数行为 + HuaweiIntentParam int + + // VivoClassification VIVO手机推送消息分类 + VivoClassification int + + // BadgeMode IOS徽章计数模式 + BadgeMode int + + // MutableContent IOS10的推送扩展开关 + MutableContent int +) diff --git a/mute/api.go b/mute/api.go new file mode 100644 index 0000000..d30bff6 --- /dev/null +++ b/mute/api.go @@ -0,0 +1,83 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/30 2:41 上午 + * @Desc: 全局禁言管理 + */ + +package mute + +import ( + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + service = "openconfigsvr" + commandSetNoSpeaking = "setnospeaking" + commandGetNoSpeaking = "getnospeaking" +) + +type API interface { + // SetNoSpeaking 设置全局禁言 + // 设置帐号的单聊消息全局禁言。 + // 设置帐号的群组消息全局禁言。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/4230 + SetNoSpeaking(userId string, privateMuteTime, groupMuteTime *uint) (err error) + + // GetNoSpeaking 查询全局禁言 + // 查询帐号的单聊消息全局禁言。 + // 查询帐号的群组消息全局禁言。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/4229 + GetNoSpeaking(userId string) (ret *GetNoSpeakingRet, err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// SetNoSpeaking 设置全局禁言 +// 设置帐号的单聊消息全局禁言。 +// 设置帐号的群组消息全局禁言。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/4230 +func (a *api) SetNoSpeaking(userId string, privateMuteTime, groupMuteTime *uint) (err error) { + req := &setNoSpeakingReq{ + UserId: userId, + PrivateMuteTime: privateMuteTime, + GroupMuteTime: groupMuteTime, + } + + if err = a.client.Post(service, commandSetNoSpeaking, req, &types.BaseResp{}); err != nil { + return + } + + return +} + +// GetNoSpeaking 查询全局禁言 +// 查询帐号的单聊消息全局禁言。 +// 查询帐号的群组消息全局禁言。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/4229 +func (a *api) GetNoSpeaking(userId string) (ret *GetNoSpeakingRet, err error) { + req := &getNoSpeakingReq{UserId: userId} + resp := &getNoSpeakingResp{} + + if err = a.client.Post(service, commandGetNoSpeaking, req, resp); err != nil { + return + } + + ret = &GetNoSpeakingRet{ + PrivateMuteTime: resp.PrivateMuteTime, + GroupMuteTime: resp.GroupMuteTime, + } + + return +} diff --git a/mute/types.go b/mute/types.go new file mode 100644 index 0000000..f794cb6 --- /dev/null +++ b/mute/types.go @@ -0,0 +1,37 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/30 2:41 上午 + * @Desc: 全局禁言数据类型 + */ + +package mute + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + // 设置全局禁言(请求) + setNoSpeakingReq struct { + UserId string `json:"Set_Account"` // (必填)设置禁言配置的帐号 + PrivateMuteTime *uint `json:"C2CmsgNospeakingTime,omitempty"` // (选填)单聊消息禁言时间,单位为秒,非负整数,最大值为4294967295(十六进制 0xFFFFFFFF) 0表示取消该帐号的单聊消息禁言;4294967295表示该帐号被设置永久禁言;其它值表示该帐号具体的禁言时间 + GroupMuteTime *uint `json:"GroupmsgNospeakingTime,omitempty"` // (选填)单聊消息禁言时间,单位为秒,非负整数,最大值为4294967295(十六进制 0xFFFFFFFF) 0表示取消该帐号的单聊消息禁言;4294967295表示该帐号被设置永久禁言;其它值表示该帐号具体的禁言时间 + } + + // 设置全局禁言(请求) + getNoSpeakingReq struct { + UserId string `json:"Get_Account"` // (必填)查询禁言信息的帐号 + } + + // 设置全局禁言(响应) + getNoSpeakingResp struct { + types.BaseResp + PrivateMuteTime uint `json:"C2CmsgNospeakingTime"` // 单聊消息禁言时长,单位为秒,非负整数。等于 0 代表没有被设置禁言;等于最大值4294967295(十六进制 0xFFFFFFFF)代表被设置永久禁言;其它代表该帐号禁言时长,如果等于3600表示该帐号被禁言一小时 + GroupMuteTime uint `json:"GroupmsgNospeakingTime"` // 群组消息禁言时长,单位为秒,非负整数。等于0代表没有被设置禁言;等于最大值4294967295(十六进制 0xFFFFFFFF)代表被设置永久禁言;其它代表该帐号禁言时长,如果等于3600表示该帐号被禁言一小时 + } + + // GetNoSpeakingRet 获取全局禁言(返回) + GetNoSpeakingRet struct { + PrivateMuteTime uint // 单聊消息禁言时长,单位为秒,非负整数。等于 0 代表没有被设置禁言;等于最大值4294967295(十六进制 0xFFFFFFFF)代表被设置永久禁言;其它代表该帐号禁言时长,如果等于3600表示该帐号被禁言一小时 + GroupMuteTime uint // 群组消息禁言时长,单位为秒,非负整数。等于0代表没有被设置禁言;等于最大值4294967295(十六进制 0xFFFFFFFF)代表被设置永久禁言;其它代表该帐号禁言时长,如果等于3600表示该帐号被禁言一小时 + } +) diff --git a/operation/api.go b/operation/api.go new file mode 100644 index 0000000..c57062a --- /dev/null +++ b/operation/api.go @@ -0,0 +1,104 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 18:38 + * @Desc: 运营管理 + */ + +package operation + +import ( + "time" + + "git.echol.cn/loser/tencent-im/internal/core" +) + +const ( + serviceOperation = "openconfigsvr" + serviceOpenMessage = "open_msg_svc" + serviceConfig = "ConfigSvc" + commandGetAppInfo = "getappinfo" + commandGetHistory = "get_history" + commandGetIPList = "GetIPList" +) + +type API interface { + // GetOperationData 拉取运营数据 + // App 管理员可以通过该接口拉取最近30天的运营数据,可拉取的字段见下文可拉取的运营字段。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/4193 + GetOperationData(fields ...FieldType) (data []*OperationData, err error) + + // GetHistoryData 下载最近消息记录 + // App 管理员可以通过该接口获取 App 中最近7天中某天某小时的所有单发或群组消息记录的下载地址 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1650 + GetHistoryData(chatType ChatType, msgTime time.Time) (files []*HistoryFile, err error) + + // GetIPList 获取服务器IP地址 + // 基于安全等考虑,您可能需要获知服务器的 IP 地址列表,以便进行相关限制。 + // App 管理员可以通过该接口获得 SDK、第三方回调所使用到的服务器 IP 地址列表或 IP 网段信息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45438 + GetIPList() (ips []string, err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// GetOperationData 拉取运营数据 +// App 管理员可以通过该接口拉取最近30天的运营数据,可拉取的字段见下文可拉取的运营字段。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/4193 +func (a *api) GetOperationData(fields ...FieldType) (data []*OperationData, err error) { + req := &getOperationDataReq{Fields: fields} + resp := &getOperationDataResp{} + + if err = a.client.Post(serviceOperation, commandGetAppInfo, req, resp); err != nil { + return + } + + data = resp.Data + + return +} + +// GetHistoryData 下载最近消息记录 +// App 管理员可以通过该接口获取 App 中最近7天中某天某小时的所有单发或群组消息记录的下载地址 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1650 +func (a *api) GetHistoryData(chatType ChatType, msgTime time.Time) (files []*HistoryFile, err error) { + req := &getHistoryDataReq{ChatType: chatType, MsgTime: msgTime.Format("2006010215")} + resp := &getHistoryDataResp{} + + if err = a.client.Post(serviceOpenMessage, commandGetHistory, req, resp); err != nil { + return + } + + files = resp.Files + + return +} + +// GetIPList 获取服务器IP地址 +// 基于安全等考虑,您可能需要获知服务器的 IP 地址列表,以便进行相关限制。 +// App 管理员可以通过该接口获得 SDK、第三方回调所使用到的服务器 IP 地址列表或 IP 网段信息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45438 +func (a *api) GetIPList() (ips []string, err error) { + req := &getIPListReq{} + resp := &getIPListResp{} + + if err = a.client.Post(serviceConfig, commandGetIPList, req, resp); err != nil { + return + } + + ips = resp.IPList + + return +} diff --git a/operation/enum.go b/operation/enum.go new file mode 100644 index 0000000..93ce474 --- /dev/null +++ b/operation/enum.go @@ -0,0 +1,51 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 10:28 + * @Desc: 运营管理枚举参数 + */ + +package operation + +type ( + // ChatType 聊天类型 + ChatType string + + // FieldType 运营数据字段类型 + FieldType string +) + +const ( + ChatTypeC2C ChatType = "C2C" // 单聊消息 + ChatTypeGroup ChatType = "Group" // 群聊消息 + + FieldTypeAppName FieldType = "AppName" // 应用名称 + FieldTypeAppId FieldType = "AppId" // 应用 SDKAppID + FieldTypeCompany FieldType = "Company" // 所属客户名称 + FieldTypeActiveUserNum FieldType = "ActiveUserNum" // 活跃用户数 + FieldTypeRegisterUserNumOneDay FieldType = "RegistUserNumOneDay" // 新增注册人数 + FieldTypeRegisterUserNumTotal FieldType = "RegistUserNumTotal" // 累计注册人数 + FieldTypeLoginTimes FieldType = "LoginTimes" // 登录次数 + FieldTypeLoginUserNum FieldType = "LoginUserNum" // 登录人数 + FieldTypeUpMsgNum FieldType = "UpMsgNum" // 上行消息数 + FieldTypeSendMsgUserNum FieldType = "SendMsgUserNum" // 发消息人数 + FieldTypeAPNSMsgNum FieldType = "APNSMsgNum" // APNs 推送数 + FieldTypeC2CUpMsgNum FieldType = "C2CUpMsgNum" // 上行消息数(C2C) + FieldTypeC2CSendMsgUserNum FieldType = "C2CSendMsgUserNum" // 发消息人数(C2C) + FieldTypeC2CAPNSMsgNum FieldType = "C2CAPNSMsgNum" // APNs 推送数(C2C) + FieldTypeMaxOnlineNum FieldType = "MaxOnlineNum" // 最高在线人数 + FieldTypeChainIncrease FieldType = "ChainIncrease" // 关系链对数增加量 + FieldTypeChainDecrease FieldType = "ChainDecrease" // 关系链对数删除量 + FieldTypeGroupUpMsgNum FieldType = "GroupUpMsgNum" // 上行消息数(群) + FieldTypeGroupSendMsgUserNum FieldType = "GroupSendMsgUserNum" // 发消息人数(群) + FieldTypeGroupAPNSMsgNum FieldType = "GroupAPNSMsgNum" // APNs 推送数(群) + FieldTypeGroupSendMsgGroupNum FieldType = "GroupSendMsgGroupNum" // 发消息群组数 + FieldTypeGroupJoinGroupTimes FieldType = "GroupJoinGroupTimes" // 入群总数 + FieldTypeGroupQuitGroupTimes FieldType = "GroupQuitGroupTimes" // 退群总数 + FieldTypeGroupNewGroupNum FieldType = "GroupNewGroupNum" // 新增群组数 + FieldTypeGroupAllGroupNum FieldType = "GroupAllGroupNum" // 累计群组数 + FieldTypeGroupDestroyGroupNum FieldType = "GroupDestroyGroupNum" // 解散群个数 + FieldTypeCallBackReq FieldType = "CallBackReq" // 回调请求数 + FieldTypeCallBackRsp FieldType = "CallBackRsp" // 回调应答数 + FieldTypeDate FieldType = "Date" // 日期 +) diff --git a/operation/types.go b/operation/types.go new file mode 100644 index 0000000..20bf41f --- /dev/null +++ b/operation/types.go @@ -0,0 +1,91 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 18:39 + * @Desc: 运营管理数据类型 + */ + +package operation + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + // 拉取运营数据(请求) + getOperationDataReq struct { + Fields []FieldType `json:"RequestField,omitempty"` // 该字段用来指定需要拉取的运营数据,不填默认拉取所有字段。 + } + + // 拉取运营数据(响应) + getOperationDataResp struct { + types.BaseResp + Data []*OperationData `json:"Result"` + } + + // OperationData 运营数据 + OperationData struct { + AppId string `json:"AppId"` // 应用AppID + AppName string `json:"AppName"` // 应用名称 + Company string `json:"Company"` // 所属客户名称 + ActiveUserNum string `json:"ActiveUserNum"` // 活跃用户数 + RegistUserNumOneDay string `json:"RegistUserNumOneDay"` // 新增注册人数 + RegistUserNumTotal string `json:"RegistUserNumTotal"` // 累计注册人数 + LoginTimes string `json:"LoginTimes"` // 登录次数 + LoginUserNum string `json:"LoginUserNum"` // 登录人数 + UpMsgNum string `json:"UpMsgNum"` // 上行消息数 + DownMsgNum string `json:"DownMsgNum"` // 下行消息数 + SendMsgUserNum string `json:"SendMsgUserNum"` // 发消息人数 + APNSMsgNum string `json:"APNSMsgNum"` // APNs推送数 + C2CUpMsgNum string `json:"C2CUpMsgNum"` // 上行消息数(C2C) + C2CSendMsgUserNum string `json:"C2CSendMsgUserNum"` // 发消息人数(C2C) + C2CAPNSMsgNum string `json:"C2CAPNSMsgNum"` // APNs推送数(C2C) + C2CDownMsgNum string `json:"C2CDownMsgNum"` // 下行消息数(C2C) + MaxOnlineNum string `json:"MaxOnlineNum"` // 最高在线人数 + ChainDecrease string `json:"ChainDecrease"` // 关系链对数删除量 + ChainIncrease string `json:"ChainIncrease"` // 关系链对数增加量 + GroupUpMsgNum string `json:"GroupUpMsgNum"` // 上行消息数(群) + GroupDownMsgNum string `json:"GroupDownMsgNum"` // 下行消息数(群) + GroupSendMsgUserNum string `json:"GroupSendMsgUserNum"` // 发消息人数(群) + GroupAPNSMsgNum string `json:"GroupAPNSMsgNum"` // APNs推送数(群) + GroupSendMsgGroupNum string `json:"GroupSendMsgGroupNum"` // 发消息群组数 + GroupJoinGroupTimes string `json:"GroupJoinGroupTimes"` // 入群总数 + GroupQuitGroupTimes string `json:"GroupQuitGroupTimes"` // 退群总数 + GroupNewGroupNum string `json:"GroupNewGroupNum"` // 新增群组数 + GroupAllGroupNum string `json:"GroupAllGroupNum"` // 累计群组数 + GroupDestroyGroupNum string `json:"GroupDestroyGroupNum"` // 解散群个数 + CallBackReq string `json:"CallBackReq"` // 回调请求数 + CallBackRsp string `json:"CallBackRsp"` // 回调应答数 + Date string `json:"Date"` // 日期 + } + + // 获取历史数据(请求) + getHistoryDataReq struct { + ChatType ChatType `json:"ChatType"` // (必填)消息类型,C2C 表示单发消息 Group 表示群组消息 + MsgTime string `json:"MsgTime"` // (必填)需要下载的消息记录的时间段,2015120121表示获取2015年12月1日21:00 - 21:59的消息的下载地址。该字段需精确到小时。每次请求只能获取某天某小时的所有单发或群组消息记录 + } + + // 获取历史数据(响应) + getHistoryDataResp struct { + types.BaseResp + Files []*HistoryFile `json:"File"` // 消息记录文件下载信息 + } + + // HistoryFile 历史数据文件 + HistoryFile struct { + URL string `json:"URL"` // 消息记录文件下载地址 + ExpireTime string `json:"ExpireTime"` // 下载地址过期时间,请在过期前进行下载,若地址失效,请通过该接口重新获取 + FileSize int `json:"FileSize"` // GZip 压缩前的文件大小(单位 Byte) + FileMD5 string `json:"FileMD5"` // GZip 压缩前的文件 MD5 + GzipSize int `json:"GzipSize"` // GZip 压缩后的文件大小(单位 Byte) + GzipMD5 string `json:"GzipMD5"` // GZip 压缩后的文件 MD5 + } + + // 获取服务器IP地址(请求) + getIPListReq struct { + } + + // 获取服务器IP地址(响应) + getIPListResp struct { + types.BaseResp + IPList []string `json:"IPList"` // 服务器IP列表 + } +) diff --git a/private/api.go b/private/api.go new file mode 100644 index 0000000..069f33b --- /dev/null +++ b/private/api.go @@ -0,0 +1,354 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 19:32 + * @Desc: 单聊消息 + */ + +package private + +import ( + "git.echol.cn/loser/tencent-im/internal/conv" + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + service = "openim" + commandSendMessage = "sendmsg" + commandSendMessages = "batchsendmsg" + commandImportMessage = "importmsg" + commandFetchMessages = "admin_getroammsg" + commandRevokeMessage = "admin_msgwithdraw" + commandSetMessageRead = "admin_set_msg_read" + commandGetUnreadMessageNum = "get_c2c_unread_msg_num" +) + +type API interface { + // SendMessage 单发单聊消息 + // 管理员向帐号发消息,接收方看到消息发送者是管理员。 + // 管理员指定某一帐号向其他帐号发消息,接收方看到发送者不是管理员,而是管理员指定的帐号。 + // 该接口不会检查发送者和接收者的好友关系(包括黑名单),同时不会检查接收者是否被禁言。 + // 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2282 + SendMessage(message *Message) (ret *SendMessageRet, err error) + + // SendMessages 批量发单聊消息 + // 支持一次对最多500个用户进行单发消息。 + // 与单发消息相比,该接口更适用于营销类消息、系统通知 tips 等时效性较强的消息。 + // 管理员指定某一帐号向目标帐号批量发消息,接收方看到发送者不是管理员,而是管理员指定的帐号。 + // 该接口不触发回调请求。 + // 该接口不会检查发送者和接收者的好友关系(包括黑名单),同时不会检查接收者是否被禁言。 + // 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1612 + SendMessages(message *Message) (ret *SendMessagesRet, err error) + + // ImportMessage 导入单聊消息 + // 导入历史单聊消息到即时通信 IM。 + // 平滑过渡期间,将原有即时通信实时单聊消息导入到即时通信 IM。 + // 该接口不会触发回调。 + // 该接口会根据 From_Account , To_Account ,MsgSeq , MsgRandom , MsgTimeStamp 字段的值对导入的消息进行去重。仅当这五个字段的值都对应相同时,才判定消息是重复的,消息是否重复与消息内容本身无关。 + // 重复导入的消息不会覆盖之前已导入的消息(即消息内容以首次导入的为准)。 + // 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/2568 + ImportMessage(message *Message) (err error) + + // FetchMessages 查询单聊消息 + // 管理员按照时间范围查询某单聊会话的消息记录。 + // 查询的单聊会话由请求中的 From_Account 和 To_Account 指定。查询结果包含会话双方互相发送的消息,具体每条消息的发送方和接收方由每条消息里的 From_Account 和 To_Account 指定。 + // 一般情况下,请求中的 From_Account 和 To_Account 字段值互换,查询结果不变。但通过 单发单聊消息 或 批量发单聊消息 接口发送的消息,如果指定 SyncOtherMachine 值为2,则需要指定正确的 From_Account 和 To_Account 字段值才能查询到该消息。 + // 例如,通过 单发单聊消息 接口指定帐号 A 给帐号 B 发一条消息,同时指定 SyncOtherMachine 值为2。则调用本接口时,From_Account 必须设置为帐号 B,To_Account 必须设置为帐号 A 才能查询到该消息。 + // 查询结果包含被撤回的消息,由消息里的 MsgFlagBits 字段标识。 + // 若想通过 REST API 撤回单聊消息 接口撤回某条消息,可先用本接口查询出该消息的 MsgKey,然后再调用撤回接口进行撤回。 + // 可查询的消息记录的时间范围取决于漫游消息存储时长,默认是7天。支持在控制台修改消息漫游时长,延长消息漫游时长是增值服务。具体请参考 漫游消息存储。 + // 若请求时间段内的消息总大小超过应答包体大小限制(目前为13K)时,则需要续拉。您可以通过应答中的 Complete 字段查看是否已拉取请求的全部消息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/42794 + FetchMessages(arg *FetchMessagesArg) (ret *FetchMessagesRet, err error) + + // PullMessages 续拉取单聊消息 + // 本API是借助"查询单聊消息"API进行扩展实现 + // 管理员按照时间范围查询某单聊会话的全部消息记录 + // 查询的单聊会话由请求中的 From_Account 和 To_Account 指定。查询结果包含会话双方互相发送的消息,具体每条消息的发送方和接收方由每条消息里的 From_Account 和 To_Account 指定。 + // 一般情况下,请求中的 From_Account 和 To_Account 字段值互换,查询结果不变。但通过 单发单聊消息 或 批量发单聊消息 接口发送的消息,如果指定 SyncOtherMachine 值为2,则需要指定正确的 From_Account 和 To_Account 字段值才能查询到该消息。 + // 例如,通过 单发单聊消息 接口指定帐号 A 给帐号 B 发一条消息,同时指定 SyncOtherMachine 值为2。则调用本接口时,From_Account 必须设置为帐号 B,To_Account 必须设置为帐号 A 才能查询到该消息。 + // 查询结果包含被撤回的消息,由消息里的 MsgFlagBits 字段标识。 + // 若想通过 REST API 撤回单聊消息 接口撤回某条消息,可先用本接口查询出该消息的 MsgKey,然后再调用撤回接口进行撤回。 + // 可查询的消息记录的时间范围取决于漫游消息存储时长,默认是7天。支持在控制台修改消息漫游时长,延长消息漫游时长是增值服务。具体请参考 漫游消息存储。 + // 若请求时间段内的消息总大小超过应答包体大小限制(目前为13K)时,则需要续拉。您可以通过应答中的 Complete 字段查看是否已拉取请求的全部消息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/42794 + PullMessages(arg *PullMessagesArg, fn func(ret *FetchMessagesRet)) (err error) + + // RevokeMessage 撤回单聊消息 + // 管理员撤回单聊消息。 + // 该接口可以撤回所有单聊消息,包括客户端发出的单聊消息,由 REST API 单发 和 批量发 接口发出的单聊消息。 + // 若需要撤回由客户端发出的单聊消息,您可以开通 发单聊消息之前回调 或 发单聊消息之后回调 ,通过该回调接口记录每条单聊消息的 MsgKey ,然后填在本接口的 MsgKey 字段进行撤回。您也可以通过 查询单聊消息 查询出待撤回的单聊消息的 MsgKey 后,填在本接口的 MsgKey 字段进行撤回。 + // 若需要撤回由 REST API 单发 和 批量发 接口发出的单聊消息,需要记录这些接口回包里的 MsgKey 字段以进行撤回。 + // 调用该接口撤回消息后,该条消息的离线、漫游存储,以及消息发送方和接收方的客户端的本地缓存都会被撤回。 + // 该接口可撤回的单聊消息没有时间限制,即可以撤回任何时间的单聊消息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/38980 + RevokeMessage(fromUserId, toUserId, msgKey string) (err error) + + // SetMessageRead 设置单聊消息已读 + // 设置用户的某个单聊会话的消息全部已读。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/50349 + SetMessageRead(userId, peerUserId string) (err error) + + // GetUnreadMessageNum 查询单聊未读消息计数 + // App 后台可以通过该接口查询特定账号的单聊总未读数(包含所有的单聊会话)或者单个单聊会话的未读数。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/56043 + GetUnreadMessageNum(userId string, peerUserIds ...string) (ret *GetUnreadMessageNumRet, err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// SendMessage 单发单聊消息 +// 管理员向帐号发消息,接收方看到消息发送者是管理员。 +// 管理员指定某一帐号向其他帐号发消息,接收方看到发送者不是管理员,而是管理员指定的帐号。 +// 该接口不会检查发送者和接收者的好友关系(包括黑名单),同时不会检查接收者是否被禁言。 +// 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2282 +func (a *api) SendMessage(message *Message) (ret *SendMessageRet, err error) { + if err = message.CheckError(); err != nil { + return + } + + req := &sendMessageReq{} + req.FromUserId = message.GetSender() + req.ToUserId = message.GetLastReceiver() + req.MsgLifeTime = message.GetLifeTime() + req.MsgTimeStamp = message.GetTimestamp() + req.OfflinePushInfo = message.GetOfflinePushInfo() + req.CloudCustomData = conv.String(message.GetCustomData()) + req.MsgSeq = message.GetSerialNo() + req.MsgBody = message.GetBody() + req.MsgRandom = message.GetRandom() + req.SendMsgControl = message.GetSendMsgControl() + req.ForbidCallbackControl = message.GetForbidCallbackControl() + req.SyncOtherMachine = message.GetSyncOtherMachine() + + resp := &sendMessageResp{} + + if err = a.client.Post(service, commandSendMessage, req, resp); err != nil { + return + } + + ret = &SendMessageRet{ + MsgKey: resp.MsgKey, + MsgTime: resp.MsgTime, + } + + return +} + +// SendMessages 批量发单聊消息 +// 支持一次对最多500个用户进行单发消息。 +// 与单发消息相比,该接口更适用于营销类消息、系统通知 tips 等时效性较强的消息。 +// 管理员指定某一帐号向目标帐号批量发消息,接收方看到发送者不是管理员,而是管理员指定的帐号。 +// 该接口不触发回调请求。 +// 该接口不会检查发送者和接收者的好友关系(包括黑名单),同时不会检查接收者是否被禁言。 +// 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1612 +func (a *api) SendMessages(message *Message) (ret *SendMessagesRet, err error) { + if err = message.CheckError(); err != nil { + return + } + + req := &sendMessagesReq{} + req.FromUserId = message.GetSender() + req.ToUserIds = message.GetReceivers() + req.OfflinePushInfo = message.GetOfflinePushInfo() + req.CloudCustomData = conv.String(message.GetCustomData()) + req.MsgSeq = message.GetSerialNo() + req.MsgBody = message.GetBody() + req.MsgRandom = message.GetRandom() + req.SendMsgControl = message.GetSendMsgControl() + req.SyncOtherMachine = message.GetSyncOtherMachine() + + resp := &sendMessagesResp{} + + if err = a.client.Post(service, commandSendMessages, req, resp); err != nil { + return + } + + ret = &SendMessagesRet{ + MsgKey: resp.MsgKey, + Errors: resp.Errors, + } + + return +} + +// ImportMessage 导入单聊消息 +// 导入历史单聊消息到即时通信 IM。 +// 平滑过渡期间,将原有即时通信实时单聊消息导入到即时通信 IM。 +// 该接口不会触发回调。 +// 该接口会根据 From_Account , To_Account ,MsgSeq , MsgRandom , MsgTimeStamp 字段的值对导入的消息进行去重。仅当这五个字段的值都对应相同时,才判定消息是重复的,消息是否重复与消息内容本身无关。 +// 重复导入的消息不会覆盖之前已导入的消息(即消息内容以首次导入的为准)。 +// 单聊消息 MsgSeq 字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以 MsgSeq 排序。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/2568 +func (a *api) ImportMessage(message *Message) (err error) { + if err = message.CheckError(); err != nil { + return + } + + req := &importMessageReq{} + req.FromUserId = message.GetSender() + req.ToUserId = message.GetLastReceiver() + req.MsgTimeStamp = message.GetTimestamp() + req.CloudCustomData = conv.String(message.GetCustomData()) + req.MsgSeq = message.GetSerialNo() + req.MsgBody = message.GetBody() + req.MsgRandom = message.GetRandom() + req.SyncFromOldSystem = message.GetSyncOtherMachine() + + if err = a.client.Post(service, commandImportMessage, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// FetchMessages 查询单聊消息 +// 管理员按照时间范围查询某单聊会话的消息记录。 +// 查询的单聊会话由请求中的 From_Account 和 To_Account 指定。查询结果包含会话双方互相发送的消息,具体每条消息的发送方和接收方由每条消息里的 From_Account 和 To_Account 指定。 +// 一般情况下,请求中的 From_Account 和 To_Account 字段值互换,查询结果不变。但通过 单发单聊消息 或 批量发单聊消息 接口发送的消息,如果指定 SyncOtherMachine 值为2,则需要指定正确的 From_Account 和 To_Account 字段值才能查询到该消息。 +// 例如,通过 单发单聊消息 接口指定帐号 A 给帐号 B 发一条消息,同时指定 SyncOtherMachine 值为2。则调用本接口时,From_Account 必须设置为帐号 B,To_Account 必须设置为帐号 A 才能查询到该消息。 +// 查询结果包含被撤回的消息,由消息里的 MsgFlagBits 字段标识。 +// 若想通过 REST API 撤回单聊消息 接口撤回某条消息,可先用本接口查询出该消息的 MsgKey,然后再调用撤回接口进行撤回。 +// 可查询的消息记录的时间范围取决于漫游消息存储时长,默认是7天。支持在控制台修改消息漫游时长,延长消息漫游时长是增值服务。具体请参考 漫游消息存储。 +// 若请求时间段内的消息总大小超过应答包体大小限制(目前为13K)时,则需要续拉。您可以通过应答中的 Complete 字段查看是否已拉取请求的全部消息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/42794 +func (a *api) FetchMessages(arg *FetchMessagesArg) (ret *FetchMessagesRet, err error) { + resp := &fetchMessagesResp{} + + if err = a.client.Post(service, commandFetchMessages, arg, resp); err != nil { + return + } + + ret = &FetchMessagesRet{ + LastMsgKey: resp.LastMsgKey, + LastMsgTime: resp.LastMsgTime, + Count: resp.MsgCount, + List: resp.MsgList, + HasMore: resp.Complete != 1, + } + + return +} + +// PullMessages 续拉取单聊消息 +// 本API是借助"查询单聊消息"API进行扩展实现 +// 管理员按照时间范围查询某单聊会话的全部消息记录 +// 查询的单聊会话由请求中的 From_Account 和 To_Account 指定。查询结果包含会话双方互相发送的消息,具体每条消息的发送方和接收方由每条消息里的 From_Account 和 To_Account 指定。 +// 一般情况下,请求中的 From_Account 和 To_Account 字段值互换,查询结果不变。但通过 单发单聊消息 或 批量发单聊消息 接口发送的消息,如果指定 SyncOtherMachine 值为2,则需要指定正确的 From_Account 和 To_Account 字段值才能查询到该消息。 +// 例如,通过 单发单聊消息 接口指定帐号 A 给帐号 B 发一条消息,同时指定 SyncOtherMachine 值为2。则调用本接口时,From_Account 必须设置为帐号 B,To_Account 必须设置为帐号 A 才能查询到该消息。 +// 查询结果包含被撤回的消息,由消息里的 MsgFlagBits 字段标识。 +// 若想通过 REST API 撤回单聊消息 接口撤回某条消息,可先用本接口查询出该消息的 MsgKey,然后再调用撤回接口进行撤回。 +// 可查询的消息记录的时间范围取决于漫游消息存储时长,默认是7天。支持在控制台修改消息漫游时长,延长消息漫游时长是增值服务。具体请参考 漫游消息存储。 +// 若请求时间段内的消息总大小超过应答包体大小限制(目前为13K)时,则需要续拉。您可以通过应答中的 Complete 字段查看是否已拉取请求的全部消息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/42794 +func (a *api) PullMessages(arg *PullMessagesArg, fn func(ret *FetchMessagesRet)) (err error) { + var ( + ret *FetchMessagesRet + req = &FetchMessagesArg{ + FromUserId: arg.FromUserId, + ToUserId: arg.ToUserId, + MaxLimited: arg.MaxLimited, + MinTime: arg.MinTime, + MaxTime: arg.MaxTime, + } + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchMessages(req) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + req.LastMsgKey = ret.LastMsgKey + req.MaxTime = ret.LastMsgTime + } + } + + return +} + +// RevokeMessage 撤回单聊消息 +// 管理员撤回单聊消息。 +// 该接口可以撤回所有单聊消息,包括客户端发出的单聊消息,由 REST API 单发 和 批量发 接口发出的单聊消息。 +// 若需要撤回由客户端发出的单聊消息,您可以开通 发单聊消息之前回调 或 发单聊消息之后回调 ,通过该回调接口记录每条单聊消息的 MsgKey ,然后填在本接口的 MsgKey 字段进行撤回。您也可以通过 查询单聊消息 查询出待撤回的单聊消息的 MsgKey 后,填在本接口的 MsgKey 字段进行撤回。 +// 若需要撤回由 REST API 单发 和 批量发 接口发出的单聊消息,需要记录这些接口回包里的 MsgKey 字段以进行撤回。 +// 调用该接口撤回消息后,该条消息的离线、漫游存储,以及消息发送方和接收方的客户端的本地缓存都会被撤回。 +// 该接口可撤回的单聊消息没有时间限制,即可以撤回任何时间的单聊消息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/38980 +func (a *api) RevokeMessage(fromUserId, toUserId, msgKey string) (err error) { + req := &revokeMessageReq{FromUserId: fromUserId, ToUserId: toUserId, MsgKey: msgKey} + + if err = a.client.Post(service, commandRevokeMessage, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// SetMessageRead 设置单聊消息已读 +// 设置用户的某个单聊会话的消息全部已读。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/50349 +func (a *api) SetMessageRead(userId, peerUserId string) (err error) { + req := &setMessageReadReq{UserId: userId, PeerUserId: peerUserId} + + if err = a.client.Post(service, commandSetMessageRead, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// GetUnreadMessageNum 查询单聊未读消息计数 +// App 后台可以通过该接口查询特定账号的单聊总未读数(包含所有的单聊会话)或者单个单聊会话的未读数。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/56043 +func (a *api) GetUnreadMessageNum(userId string, peerUserIds ...string) (ret *GetUnreadMessageNumRet, err error) { + req := &getUnreadMessageNumReq{UserId: userId, PeerUserIds: peerUserIds} + resp := &getUnreadMessageNumResp{} + + if err = a.client.Post(service, commandGetUnreadMessageNum, req, resp); err != nil { + return + } + + ret = &GetUnreadMessageNumRet{ + Total: resp.AllUnreadMsgNum, + Results: make(map[string]int, len(resp.PeerUnreadMsgNums)), + Errors: resp.PeerErrors, + } + + for _, item := range resp.PeerUnreadMsgNums { + ret.Results[item.UserId] = item.UnreadMsgNum + } + + return +} diff --git a/private/enum.go b/private/enum.go new file mode 100644 index 0000000..6aff35e --- /dev/null +++ b/private/enum.go @@ -0,0 +1,42 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 10:22 + * @Desc: TODO + */ + +package private + +import ( + "git.echol.cn/loser/tencent-im/internal/enum" +) + +const ( + // 同步至其他设备 + SyncOtherMachineYes = enum.SyncOtherMachineYes // 把消息同步到From_Account在线终端和漫游上 + SyncOtherMachineNo = enum.SyncOtherMachineNo // 消息不同步至From_Account + + // 推送标识 + PushFlagYes = enum.PushFlagYes // 正常推送 + PushFlagNo = enum.PushFlagYes // 不离线推送 + + // 华为推送通知消息分类 + HuaWeiImportanceLow = enum.HuaWeiImportanceLow // LOW类消息 + HuaWeiImportanceNormal = enum.HuaWeiImportanceNormal // NORMAL类消息 + + // 华为推送为“打开应用内指定页面”的前提下透传参数行为 + HuaweiIntentParamAction = enum.HuaweiIntentParamAction // 将透传内容Ext作为Action参数 + HuaweiIntentParamIntent = enum.HuaweiIntentParamIntent // 将透传内容Ext作为Intent参数 + + // VIVO手机推送消息分类 + VivoClassificationOperation = enum.VivoClassificationOperation // 运营类消息 + VivoClassificationSystem = enum.VivoClassificationSystem // 系统类消息 + + // IOS徽章计数模式 + BadgeModeNormal = enum.BadgeModeNormal // 本条消息需要计数 + BadgeModeIgnore = enum.BadgeModeIgnore // 本条消息不需要计数 + + // IOS10的推送扩展开关 + MutableContentNormal = enum.MutableContentNormal // 关闭iOS10的推送扩展 + MutableContentEnable = enum.MutableContentEnable // 开启iOS10的推送扩展 +) diff --git a/private/message.go b/private/message.go new file mode 100644 index 0000000..d4cbf0a --- /dev/null +++ b/private/message.go @@ -0,0 +1,183 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/8/31 18:04 + * @Desc: 私聊消息实体 + */ + +package private + +import ( + "errors" + + "git.echol.cn/loser/tencent-im/internal/entity" + "git.echol.cn/loser/tencent-im/internal/types" +) + +var errNotSetMsgReceiver = errors.New("message receiver is not set") + +type Message struct { + entity.Message + receivers []string // 接收方UserId(可以为多个) + syncOtherMachine int // 同步到其他器 + timestamp int64 // 消息时间戳,UNIX 时间戳(单位:秒) + seq int // 消息序列号 + customData interface{} // 自定义数据 + sendControls map[string]bool // 发送消息控制 + callbackControls map[string]bool // 禁用回调 +} + +func NewMessage() *Message { + return &Message{} +} + +// AddReceivers 添加接收方 +func (m *Message) AddReceivers(userId ...string) { + if m.receivers == nil { + m.receivers = make([]string, 0) + } + m.receivers = append(m.receivers, userId...) +} + +// SetReceivers 设置接收方 +func (m *Message) SetReceivers(userId ...string) { + if m.receivers != nil { + m.receivers = m.receivers[0:0] + } + m.AddReceivers(userId...) +} + +// GetReceivers 获取接收方 +func (m *Message) GetReceivers() []string { + return m.receivers +} + +func (m *Message) GetLastReceiver() string { + return m.receivers[0] +} + +// SetSyncOtherMachine 设置同步到其他机器 +func (m *Message) SetSyncOtherMachine(syncOtherMachine types.SyncOtherMachine) { + m.syncOtherMachine = int(syncOtherMachine) +} + +// GetSyncOtherMachine 获取同步至其他设备 +func (m *Message) GetSyncOtherMachine() int { + return m.syncOtherMachine +} + +// SetSerialNo 设置消息序列号 +func (m *Message) SetSerialNo(seq int) { + m.seq = seq +} + +// GetSerialNo 获取消息序列号 +func (m *Message) GetSerialNo() int { + return m.seq +} + +// SetTimestamp 设置消息的时间戳 +func (m *Message) SetTimestamp(timestamp int64) { + m.timestamp = timestamp +} + +// GetTimestamp 获取消息的时间戳 +func (m *Message) GetTimestamp() int64 { + return m.timestamp +} + +// SetCustomData 设置自定义数据 +func (m *Message) SetCustomData(data interface{}) { + m.customData = data +} + +// GetCustomData 获取自定义数据 +func (m *Message) GetCustomData() interface{} { + return m.customData +} + +// SetForbidBeforeSendMsgCallback 设置禁止发消息前回调 +func (m *Message) SetForbidBeforeSendMsgCallback() { + if m.callbackControls == nil { + m.callbackControls = make(map[string]bool, 0) + } + m.callbackControls["ForbidBeforeSendMsgCallback"] = true +} + +// SetForbidAfterSendMsgCallback 设置禁止发消息后回调 +func (m *Message) SetForbidAfterSendMsgCallback() { + if m.callbackControls == nil { + m.callbackControls = make(map[string]bool, 0) + } + m.callbackControls["ForbidAfterSendMsgCallback"] = true +} + +// GetForbidCallbackControl 获取消息回调禁止开关 +func (m *Message) GetForbidCallbackControl() (controls []string) { + if m.callbackControls != nil { + if n := len(m.callbackControls); n > 0 { + controls = make([]string, 0, n) + for k := range m.callbackControls { + controls = append(controls, k) + } + } + } + + return +} + +// SetNoUnread 设置该条消息不计入未读数 +func (m *Message) SetNoUnread() { + if m.sendControls == nil { + m.sendControls = make(map[string]bool, 0) + } + m.sendControls["NoUnread"] = true +} + +// SetNoLastMsg 设置该条消息不更新会话列表 +func (m *Message) SetNoLastMsg() { + if m.sendControls == nil { + m.sendControls = make(map[string]bool, 0) + } + m.sendControls["NoLastMsg"] = true +} + +// GetSendMsgControl 获取消息发送控制选项 +func (m *Message) GetSendMsgControl() (controls []string) { + if m.sendControls != nil { + if n := len(m.sendControls); n > 0 { + controls = make([]string, 0, n) + for k := range m.sendControls { + controls = append(controls, k) + } + } + } + + return +} + +// CheckError 检测错误 +func (m *Message) CheckError() (err error) { + if err = m.CheckLifeTimeArgError(); err != nil { + return + } + + if err = m.CheckBodyArgError(); err != nil { + return + } + + if err = m.checkReceiverArgError(); err != nil { + return + } + + return +} + +// checkReceiverArgError 检测接收方参数 +func (m *Message) checkReceiverArgError() error { + if m.receivers == nil || len(m.receivers) == 0 { + return errNotSetMsgReceiver + } + + return nil +} diff --git a/private/types.go b/private/types.go new file mode 100644 index 0000000..e7a6207 --- /dev/null +++ b/private/types.go @@ -0,0 +1,192 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 17:42 + * @Desc: 私聊消息数据结构 + */ + +package private + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + // 发送消息(请求) + sendMessageReq struct { + FromUserId string `json:"From_Account,omitempty"` // (选填)消息发送方UserID(用于指定发送消息方帐号) + ToUserId string `json:"To_Account"` // (必填)消息接收方UserID + MsgLifeTime int `json:"MsgLifeTime,omitempty"` // (选填)消息离线保存时长(单位:秒),最长为7天(604800秒) + MsgSeq int `json:"MsgSeq,omitempty"` // (选填)消息序列号,后台会根据该字段去重及进行同秒内消息的排序,详细规则请看本接口的功能说明。若不填该字段,则由后台填入随机数。 + MsgRandom uint32 `json:"MsgRandom"` // (必填)消息随机数,后台用于同一秒内的消息去重。请确保该字段填的是随机数 + MsgTimeStamp int64 `json:"MsgTimeStamp,omitempty"` // (选填)消息时间戳,UNIX 时间戳(单位:秒) + MsgBody []*types.MsgBody `json:"MsgBody"` // (必填)消息内容,具体格式请参考 消息格式描述(注意,一条消息可包括多种消息元素,MsgBody 为 Array 类型) + SyncOtherMachine int `json:"SyncOtherMachine,omitempty"` // (选填)消息是否同步到在线终端和漫游上 1:把消息同步到 From_Account 在线终端和漫游上;2:消息不同步至 From_Account; 若不填写默认情况下会将消息存 From_Account 漫游 + CloudCustomData string `json:"CloudCustomData,omitempty"` // (选填)消息回调禁止开关,只对本条消息有效, + SendMsgControl []string `json:"SendMsgControl,omitempty"` // (选填)消息发送控制选项,是一个 String 数组,只对本条消息有效。 + ForbidCallbackControl []string `json:"ForbidCallbackControl,omitempty"` // (选填)消息回调禁止开关,只对本条消息有效 + OfflinePushInfo *types.OfflinePushInfo `json:"OfflinePushInfo,omitempty"` // (选填)离线推送信息配置 + } + + // 发送消息(响应) + sendMessageResp struct { + types.ActionBaseResp + MsgTime int `json:"MsgTime"` // 消息时间戳,UNIX 时间戳 + MsgKey string `json:"MsgKey"` // 消息唯一标识,用于撤回。长度不超过50个字符 + } + + // SendMessageRet 发送消息结果 + SendMessageRet struct { + MsgKey string // 消息唯一标识,用于撤回。长度不超过50个字符 + MsgTime int // 消息时间戳,UNIX 时间戳 + } + + // 批量发单聊消息(请求) + sendMessagesReq struct { + FromUserId string `json:"From_Account,omitempty"` // (选填)消息发送方UserID(用于指定发送消息方帐号) + ToUserIds []string `json:"To_Account"` // (必填)消息接收方UserID + MsgSeq int `json:"MsgSeq,omitempty"` // (选填)消息序列号,后台会根据该字段去重及进行同秒内消息的排序,详细规则请看本接口的功能说明。若不填该字段,则由后台填入随机数。 + MsgRandom uint32 `json:"MsgRandom"` // (必填)消息随机数,后台用于同一秒内的消息去重。请确保该字段填的是随机数 + MsgBody []*types.MsgBody `json:"MsgBody"` // (必填)消息内容,具体格式请参考 消息格式描述(注意,一条消息可包括多种消息元素,MsgBody 为 Array 类型) + SyncOtherMachine int `json:"SyncOtherMachine,omitempty"` // (选填)消息是否同步到在线终端和漫游上 1:把消息同步到 From_Account 在线终端和漫游上;2:消息不同步至 From_Account; 若不填写默认情况下会将消息存 From_Account 漫游 + CloudCustomData string `json:"CloudCustomData,omitempty"` // (选填)消息回调禁止开关,只对本条消息有效, + SendMsgControl []string `json:"SendMsgControl,omitempty"` // (选填)消息发送控制选项,是一个 String 数组,只对本条消息有效。 + OfflinePushInfo *types.OfflinePushInfo `json:"OfflinePushInfo,omitempty"` // (选填)离线推送信息配置 + } + + // 批量发单聊消息(响应) + sendMessagesResp struct { + types.ActionBaseResp + MsgKey string `json:"MsgKey"` + Errors []SendMessageError `json:"ErrorList"` + } + + // SendMessageError 发送消息错误项 + SendMessageError struct { + UserId string `json:"To_Account"` + ErrorCode int `json:"ErrorCode"` + } + + // SendMessagesRet 发送消息结果 + SendMessagesRet struct { + MsgKey string + Errors []SendMessageError + } + + // 导入消息(请求) + importMessageReq struct { + FromUserId string `json:"From_Account,omitempty"` // (选填)消息发送方UserID(用于指定发送消息方帐号) + ToUserId string `json:"To_Account"` // (必填)消息接收方UserID + MsgSeq int `json:"MsgSeq,omitempty"` // (选填)消息序列号,后台会根据该字段去重及进行同秒内消息的排序,详细规则请看本接口的功能说明。若不填该字段,则由后台填入随机数。 + MsgRandom uint32 `json:"MsgRandom"` // (必填)消息随机数,后台用于同一秒内的消息去重。请确保该字段填的是随机数 + MsgTimeStamp int64 `json:"MsgTimeStamp,omitempty"` // (选填)消息时间戳,UNIX 时间戳(单位:秒) + MsgBody []*types.MsgBody `json:"MsgBody"` // (必填)消息内容,具体格式请参考 消息格式描述(注意,一条消息可包括多种消息元素,MsgBody 为 Array 类型) + SyncFromOldSystem int `json:"SyncFromOldSystem,omitempty"` // (选填)消息是否同步到在线终端和漫游上 1:把消息同步到 From_Account 在线终端和漫游上;2:消息不同步至 From_Account; 若不填写默认情况下会将消息存 From_Account 漫游 + CloudCustomData string `json:"CloudCustomData,omitempty"` // (选填)消息回调禁止开关,只对本条消息有效, + } + + // FetchMessagesArg 拉取消息参数 + FetchMessagesArg struct { + FromUserId string `json:"From_Account"` // (必填)会话其中一方的 UserID,若已指定发送消息方帐号,则为消息发送方 + ToUserId string `json:"To_Account"` // (必填)会话其中一方的 UserID + MaxLimited int `json:"MaxCnt"` // (必填)请求的消息条数 + MinTime int64 `json:"MinTime"` // (必填)请求的消息时间范围的最小值 + MaxTime int64 `json:"MaxTime"` // (必填)请求的消息时间范围的最大值 + LastMsgKey string `json:"LastMsgKey,omitempty"` // (选填)上一次拉取到的最后一条消息的 MsgKey,续拉时需要填该字段,填写方法见上方 + } + + // 拉取消息参数(响应) + fetchMessagesResp struct { + types.ActionBaseResp + Complete int `json:"Complete"` // 是否全部拉取,0表示未全部拉取,需要续拉,1表示已全部拉取 + LastMsgTime int64 `json:"LastMsgTime"` // 本次拉取到的消息里的最后一条消息的时间 + LastMsgKey string `json:"LastMsgKey"` // 本次拉取到的消息里的最后一条消息的标识 + MsgCount int `json:"MsgCnt"` // 本次拉取到的消息条数 + MsgList []*MessageItem `json:"MsgList"` // 消息列表 + } + + // FetchMessagesRet 消息结果 + FetchMessagesRet struct { + LastMsgTime int64 // 本次拉取到的消息里的最后一条消息的时间 + LastMsgKey string // 本次拉取到的消息里的最后一条消息的标识 + Count int // 本次拉取到的消息条数 + HasMore bool // 是否还有更多数据 + List []*MessageItem // 消息列表 + } + + // MessageItem 消息项 + MessageItem struct { + FromUserId string `json:"From_Account"` + ToUserId string `json:"To_Account"` + MsgSeq int `json:"MsgSeq"` + MsgRandom int `json:"MsgRandom"` + MsgTimeStamp int64 `json:"MsgTimeStamp"` + MsgFlagBits int `json:"MsgFlagBits"` + MsgKey string `json:"MsgKey"` + MsgBody []*types.MsgBody `json:"MsgBody"` + CloudCustomData string `json:"CloudCustomData"` + } + + // PullMessagesArg 持续拉取单聊消息参数 + PullMessagesArg struct { + FromUserId string `json:"From_Account"` // (必填)会话其中一方的 UserID,若已指定发送消息方帐号,则为消息发送方 + ToUserId string `json:"To_Account"` // (必填)会话其中一方的 UserID + MaxLimited int `json:"MaxCnt"` // (必填)请求的消息条数 + MinTime int64 `json:"MinTime"` // (必填)请求的消息时间范围的最小值 + MaxTime int64 `json:"MaxTime"` // (必填)请求的消息时间范围的最大值 + } + + // 撤销消息(请求) + revokeMessageReq struct { + FromUserId string `json:"From_Account"` // (必填)消息发送方UserID + ToUserId string `json:"To_Account"` // (必填)消息接收方UserID + MsgKey string `json:"MsgKey"` // (必填)待撤回消息的唯一标识。该字段由 REST API 接口 单发单聊消息 和 批量发单聊消息 返回 + } + + // 设置单聊消息已读(请求) + setMessageReadReq struct { + UserId string `json:"Report_Account"` // (必填)进行消息已读的用户UserId + PeerUserId string `json:"Peer_Account"` // (必填)进行消息已读的单聊会话的另一方用户UserId + } + + // 查询单聊未读消息计数(请求) + getUnreadMessageNumReq struct { + UserId string `json:"To_Account"` // (必填)待查询的用户UserId + PeerUserIds []string `json:"Peer_Account,omitempty"` // (选填)待查询的单聊会话对端的用户UserId + } + + // 查询单聊未读消息计数(响应) + getUnreadMessageNumResp struct { + types.ActionBaseResp + AllUnreadMsgNum int `json:"AllC2CUnreadMsgNum"` // 单聊消息总未读数 + PeerUnreadMsgNums []unreadMessageNum `json:"C2CUnreadMsgNumList"` // 单聊消息未读对端列表 + PeerErrors []*UnreadMessageError `json:"ErrorList"` // 查询错误列表 + } + + // 未读消息数 + unreadMessageNum struct { + UserId string `json:"Peer_Account"` // 单聊会话对端UserId + UnreadMsgNum int `json:"C2CUnreadMsgNum"` // 该单聊会话的未读数 + } + + // 查询错误项 + UnreadMessageError struct { + UserId string `json:"Peer_Account"` // 查询错误的目标UserId + ErrorCode int `json:"ErrorCode"` // 查询错误的错误码。若目标帐号的错误码为70107表示该帐号不存在 + } + + // GetUnreadMessageNumRet 未读消息结果 + GetUnreadMessageNumRet struct { + Total int // 单聊消息总未读数 + Results map[string]int // 未读消息数列表 + Errors []*UnreadMessageError // 错误消息列表 + } + + ImageInfo = types.ImageInfo + MsgTextContent = types.MsgTextContent + MsgFaceContent = types.MsgFaceContent + MsgFileContent = types.MsgFileContent + MsgImageContent = types.MsgImageContent + MsgSoundContent = types.MsgSoundContent + MsgVideoContent = types.MsgVideoContent + MsgCustomContent = types.MsgCustomContent + MsgLocationContent = types.MsgLocationContent +) diff --git a/profile/api.go b/profile/api.go new file mode 100644 index 0000000..c612515 --- /dev/null +++ b/profile/api.go @@ -0,0 +1,106 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 20:44 + * @Desc: 资料管理 + */ + +package profile + +import ( + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + service = "profile" + commandSetProfile = "portrait_set" + commandGetProfiles = "portrait_get" +) + +type API interface { + // SetProfile 设置资料 + // 支持 标配资料字段 和 自定义资料字段 的设置 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1640 + SetProfile(profile *Profile) (err error) + + // GetProfiles 拉取资料 + // 支持拉取好友和非好友的资料字段。 + // 支持拉取 标配资料字段 和 自定义资料字段。 + // 建议每次拉取的用户数不超过100,避免因回包数据量太大导致回包失败。 + // 请确保请求中的所有帐号都已导入即时通信 IM,如果请求中含有未导入即时通信 IM 的帐号,即时通信 IM 后台将会提示错误。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1639 + GetProfiles(userIds []string, attrs []string) (profiles []*Profile, err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// SetProfile 设置资料 +// 支持 标配资料字段 和 自定义资料字段 的设置 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1640 +func (a *api) SetProfile(profile *Profile) (err error) { + if err = profile.CheckError(); err != nil { + return + } + + userId := profile.GetUserId() + + attrs := profile.GetAttrs() + + if len(attrs) == 0 { + err = core.NewError(enum.InvalidParamsCode, "the attributes is not set") + return + } + + req := &setProfileReq{UserId: userId, Attrs: make([]*types.TagPair, 0, len(attrs))} + + for tag, value := range attrs { + req.Attrs = append(req.Attrs, &types.TagPair{ + Tag: tag, + Value: value, + }) + } + + if err = a.client.Post(service, commandSetProfile, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// GetProfiles 拉取资料 +// 支持拉取好友和非好友的资料字段。 +// 支持拉取 标配资料字段 和 自定义资料字段。 +// 建议每次拉取的用户数不超过100,避免因回包数据量太大导致回包失败。 +// 请确保请求中的所有帐号都已导入即时通信 IM,如果请求中含有未导入即时通信 IM 的帐号,即时通信 IM 后台将会提示错误。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1639 +func (a *api) GetProfiles(userIds []string, attrs []string) (profiles []*Profile, err error) { + req := &getProfileReq{UserIds: userIds, TagList: attrs} + resp := &getProfileResp{} + + if err = a.client.Post(service, commandGetProfiles, req, resp); err != nil { + return + } + + for _, account := range resp.UserProfiles { + p := NewProfile(account.UserId) + p.SetError(account.ResultCode, account.ResultInfo) + for _, item := range account.Profile { + p.SetAttr(item.Tag, item.Value) + } + profiles = append(profiles, p) + } + + return +} diff --git a/profile/enum.go b/profile/enum.go new file mode 100644 index 0000000..f7489fb --- /dev/null +++ b/profile/enum.go @@ -0,0 +1,54 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 14:03 + * @Desc: TODO + */ + +package profile + +import ( + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +type ( + // GenderType 性别类型 + GenderType = types.GenderType + + // AllowType 加好友验证方式 + AllowType = types.AllowType + + // AdminForbidType 管理员禁止加好友标识类型 + AdminForbidType = types.AdminForbidType +) + +const ( + // 性别类型 + GenderTypeUnknown = enum.GenderTypeUnknown // 没设置性别 + GenderTypeFemale = enum.GenderTypeFemale // 女性 + GenderTypeMale = enum.GenderTypeMale // 男性 + + // 加好友验证方式 + AllowTypeNeedConfirm = enum.AllowTypeNeedConfirm // 需要经过自己确认对方才能添加自己为好友 + AllowTypeAllowAny = enum.AllowTypeAllowAny // 允许任何人添加自己为好友 + AllowTypeDenyAny = enum.AllowTypeDenyAny // 不允许任何人添加自己为好友 + + // 管理员禁止加好友标识类型 + AdminForbidTypeNone = enum.AdminForbidTypeNone // 默认值,允许加好友 + AdminForbidTypeSendOut = enum.AdminForbidTypeSendOut // 禁止该用户发起加好友请求 + + // 标准资料字段 + StandardAttrNickname = enum.StandardAttrNickname // 昵称 + StandardAttrGender = enum.StandardAttrGender // 性别 + StandardAttrBirthday = enum.StandardAttrBirthday // 生日 + StandardAttrLocation = enum.StandardAttrLocation // 所在地 + StandardAttrSignature = enum.StandardAttrSignature // 个性签名 + StandardAttrAllowType = enum.StandardAttrAllowType // 加好友验证方式 + StandardAttrLanguage = enum.StandardAttrLanguage // 语言 + StandardAttrAvatar = enum.StandardAttrAvatar // 头像URL + StandardAttrMsgSettings = enum.StandardAttrMsgSettings // 消息设置 + StandardAttrAdminForbidType = enum.StandardAttrAdminForbidType // 管理员禁止加好友标识 + StandardAttrLevel = enum.StandardAttrLevel // 等级 + StandardAttrRole = enum.StandardAttrRole // 角色 +) diff --git a/profile/profile.go b/profile/profile.go new file mode 100644 index 0000000..7830556 --- /dev/null +++ b/profile/profile.go @@ -0,0 +1,39 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/28 11:23 上午 + * @Desc: TODO + */ + +package profile + +import ( + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/entity" + "git.echol.cn/loser/tencent-im/internal/enum" +) + +type Profile struct { + entity.User +} + +func NewProfile(userId ...string) *Profile { + p := &Profile{} + if len(userId) > 0 { + p.SetUserId(userId[0]) + } + return p +} + +// CheckError 检测错误 +func (p *Profile) CheckError() (err error) { + if userId := p.GetUserId(); userId == "" { + return core.NewError(enum.InvalidParamsCode, "the userid is not set") + } + + if err = p.GetError(); err != nil { + return + } + + return +} diff --git a/profile/types.go b/profile/types.go new file mode 100644 index 0000000..41f99cd --- /dev/null +++ b/profile/types.go @@ -0,0 +1,39 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 17:38 + * @Desc: 资料管理结构体定义 + */ + +package profile + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + // 设置资料(请求) + setProfileReq struct { + UserId string `json:"From_Account"` // (必填)需要设置该 UserID 的资料 + Attrs []*types.TagPair `json:"ProfileItem"` // (必填)待设置的用户的资料对象数组 + } + + // 获取资料(请求) + getProfileReq struct { + UserIds []string `json:"To_Account"` // (必填)需要拉取这些UserID的资料 + TagList []string `json:"TagList"` // (必填)指定要拉取的资料字段的 Tag,支持的字段有 + } + + // 获取资料(响应) + getProfileResp struct { + types.ActionBaseResp + ErrorDisplay string `json:"ErrorDisplay"` // 详细的客户端展示信息 + UserProfiles []UserProfile `json:"UserProfileItem"` // 用户资料结构化信息 + } + + // UserProfile 用户资料 + UserProfile struct { + UserId string `json:"To_Account"` // 用户的UserID + Profile []types.TagPair `json:"ProfileItem"` // 用户的资料对象数组 + ResultCode int `json:"ResultCode"` // 处理结果,0表示成功,非0表示失败 + ResultInfo string `json:"ResultInfo"` // 错误描述信息,成功时该字段为空 + } +) diff --git a/push/api.go b/push/api.go new file mode 100644 index 0000000..6e3ac98 --- /dev/null +++ b/push/api.go @@ -0,0 +1,398 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 20:42 + * @Desc: 全员推送 + */ + +package push + +import ( + "fmt" + "strconv" + + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + service = "all_member_push" + commandPushMessage = "im_push" + commandSetAttrNames = "im_set_attr_name" + commandGetAttrNames = "im_get_attr_name" + commandGetUserAttrs = "im_get_attr" + commandSetUserAttrs = "im_set_attr" + commandDeleteUserAttrs = "im_remove_attr" + commandGetUserTags = "im_get_tag" + commandAddUserTags = "im_add_tag" + commandDeleteUserTags = "im_remove_tag" + commandDeleteUserAllTags = "im_remove_all_tags" + + batchSetAttrNamesLimit = 10 // 批量设置应用属性名限制 + batchGetUserAttrsLimit = 100 // 批量获取用户属性限制 + batchSetUserAttrsLimit = 100 // 批量设置用户属性限制 + batchDeleteUserAttrsLimit = 100 // 批量删除用户属性限制 + batchAddUserTagsLimit = 100 // 批量添加用户标签限制 + batchGetUserTagsLimit = 100 // 批量获取用户标签限制 + batchDeleteUserTagsLimit = 100 // 批量删除用户标签限制 + batchDeleteUserAllTagsUserLimit = 100 // 批量删除用户所有标签的用户限制 +) + +type API interface { + // PushMessage 全员推送 + // 支持全员推送。 + // 支持按用户属性推送。 + // 支持按用户标签推送。 + // 管理员推送消息,接收方看到消息发送者是管理员。 + // 管理员指定某一帐号向其他帐号推送消息,接收方看到发送者不是管理员,而是管理员指定的帐号。 + // 支持消息离线存储,不支持漫游。 + // 由于全员推送需要下发的帐号数量巨大,下发完全部帐号需要一定时间(根据帐号总数而定,一般在一分钟内)。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45934 + PushMessage(message *Message) (taskId string, err error) + + // SetAttrNames 设置应用属性名称 + // 每个应用可以设置自定义的用户属性,最多可以有10个。通过本接口可以设置每个属性的名称,设置完成后,即可用于按用户属性推送等。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45935 + SetAttrNames(attrNames map[int]string) (err error) + + // GetAttrNames 获取应用属性名称 + // 管理员获取应用属性名称。使用前请先 设置应用属性名称 。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45936 + GetAttrNames() (attrNames map[int]string, err error) + + // GetUserAttrs 获取用户属性 + // 获取用户属性(必须以管理员帐号调用);每次最多只能获取100个用户的属性。使用前请先 设置应用属性名称 。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45937 + GetUserAttrs(userIds ...string) (attrs map[string]map[string]interface{}, err error) + + // SetUserAttrs 设置用户属性 + // 管理员给用户设置属性。每次最多只能给100个用户设置属性。使用前请先 设置应用属性名称 。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45938 + SetUserAttrs(userAttrs map[string]map[string]interface{}) (err error) + + // DeleteUserAttrs 删除用户属性 + // 管理员给用户删除属性。注意每次最多只能给100个用户删除属性。使用前请先 设置应用属性名称。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45939 + DeleteUserAttrs(userAttrs map[string][]string) (err error) + + // GetUserTags 获取用户标签 + // 获取用户标签(必须以管理员帐号调用)。每次最多只能获取100个用户的标签。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45940 + GetUserTags(userIds ...string) (tags map[string][]string, err error) + + // AddUserTags 添加用户标签 + // 管理员给用户添加标签。 + // 每次请求最多只能给100个用户添加标签,请求体中单个用户添加标签数最多为10个。 + // 单个用户可设置最大标签数为100个,若用户当前标签超过100,则添加新标签之前请先删除旧标签。 + // 单个标签最大长度为50字节。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45941 + AddUserTags(userTags map[string][]string) (err error) + + // DeleteUserTags 删除用户标签 + // 管理员给用户删除标签。注意每次最多只能给100个用户删除标签。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45942 + DeleteUserTags(userTags map[string][]string) (err error) + + // DeleteUserAllTags 删除用户所有标签 + // 管理员给用户删除所有标签。注意每次最多只能给100个用户删除所有标签。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/45943 + DeleteUserAllTags(userIds ...string) (err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// PushMessage 全员推送 +// 支持全员推送。 +// 支持按用户属性推送。 +// 支持按用户标签推送。 +// 管理员推送消息,接收方看到消息发送者是管理员。 +// 管理员指定某一帐号向其他帐号推送消息,接收方看到发送者不是管理员,而是管理员指定的帐号。 +// 支持消息离线存储,不支持漫游。 +// 由于全员推送需要下发的帐号数量巨大,下发完全部帐号需要一定时间(根据帐号总数而定,一般在一分钟内)。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45934 +func (a *api) PushMessage(message *Message) (taskId string, err error) { + if err = message.checkError(); err != nil { + return + } + + req := &pushMessageReq{} + req.FromUserId = message.GetSender() + req.MsgLifeTime = message.GetLifeTime() + req.OfflinePushInfo = message.GetOfflinePushInfo() + req.MsgBody = message.GetBody() + req.MsgRandom = message.GetRandom() + req.Condition = message.GetCondition() + + resp := &pushMessageResp{} + + if err = a.client.Post(service, commandPushMessage, req, resp); err != nil { + return + } + + taskId = resp.TaskId + + return +} + +// SetAttrNames 设置应用属性名称 +// 每个应用可以设置自定义的用户属性,最多可以有10个。通过本接口可以设置每个属性的名称,设置完成后,即可用于按用户属性推送等。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45935 +func (a *api) SetAttrNames(attrNames map[int]string) (err error) { + if c := len(attrNames); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the attribute names is not set") + return + } else if c > batchSetAttrNamesLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of attribute names to be set cannot exceed %d", batchSetAttrNamesLimit)) + return + } + + req := &setAttrNamesReq{AttrNames: make(map[string]string, len(attrNames))} + + for i, attrName := range attrNames { + req.AttrNames[strconv.Itoa(i)] = attrName + } + + if err = a.client.Post(service, commandSetAttrNames, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// GetAttrNames 获取应用属性名称 +// 管理员获取应用属性名称。使用前请先 设置应用属性名称 。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45936 +func (a *api) GetAttrNames() (attrNames map[int]string, err error) { + req := &getAttrNamesReq{} + resp := &getAttrNamesResp{} + + if err = a.client.Post(service, commandGetAttrNames, req, resp); err != nil { + return + } + + if len(resp.AttrNames) > 0 { + var i int + attrNames = make(map[int]string, len(resp.AttrNames)) + for key, attrName := range resp.AttrNames { + i, _ = strconv.Atoi(key) + attrNames[i] = attrName + } + } + + return +} + +// GetUserAttrs 获取用户属性 +// 获取用户属性(必须以管理员帐号调用);每次最多只能获取100个用户的属性。使用前请先 设置应用属性名称 。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45937 +func (a *api) GetUserAttrs(userIds ...string) (attrs map[string]map[string]interface{}, err error) { + if c := len(userIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the accounts is not set") + return + } else if c > batchGetUserAttrsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of accounts being queried cannot exceed %d", batchGetUserAttrsLimit)) + return + } + + req := &getUserAttrsReq{UserIds: userIds} + resp := &getUserAttrsResp{} + + if err = a.client.Post(service, commandGetUserAttrs, req, resp); err != nil { + return + } + + attrs = make(map[string]map[string]interface{}, len(resp.Attrs)) + for _, attr := range resp.Attrs { + attrs[attr.UserId] = attr.Attrs + } + + return +} + +// SetUserAttrs 设置用户属性 +// 管理员给用户设置属性。每次最多只能给100个用户设置属性。使用前请先 设置应用属性名称 。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45938 +func (a *api) SetUserAttrs(userAttrs map[string]map[string]interface{}) (err error) { + if c := len(userAttrs); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the attributes is not set") + return + } else if c > batchSetUserAttrsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of attributes to be set cannot exceed %d", batchSetUserAttrsLimit)) + return + } + + req := &setUserAttrsReq{Attrs: make([]*userAttrItem, 0, len(userAttrs))} + for userId, attrs := range userAttrs { + req.Attrs = append(req.Attrs, &userAttrItem{ + UserId: userId, + Attrs: attrs, + }) + } + + if err = a.client.Post(service, commandSetUserAttrs, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// DeleteUserAttrs 删除用户属性 +// 管理员给用户删除属性。注意每次最多只能给100个用户删除属性。使用前请先 设置应用属性名称。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45939 +func (a *api) DeleteUserAttrs(userAttrs map[string][]string) (err error) { + if c := len(userAttrs); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the attributes is not set") + return + } else if c > batchDeleteUserAttrsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of attributes to be delete cannot exceed %d", batchDeleteUserAttrsLimit)) + return + } + + req := &deleteUserAttrsReq{Attrs: make([]deleteUserAttr, 0, len(userAttrs))} + for userId, attrs := range userAttrs { + req.Attrs = append(req.Attrs, deleteUserAttr{ + UserId: userId, + Attrs: attrs, + }) + } + + if err = a.client.Post(service, commandDeleteUserAttrs, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// GetUserTags 获取用户标签 +// 获取用户标签(必须以管理员帐号调用)。每次最多只能获取100个用户的标签。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45940 +func (a *api) GetUserTags(userIds ...string) (tags map[string][]string, err error) { + if c := len(userIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the accounts is not set") + return + } else if c > batchGetUserTagsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of tags being queried cannot exceed %d", batchGetUserTagsLimit)) + return + } + + req := &getUserTagsReq{UserIds: userIds} + resp := &getUserTagsResp{} + + if err = a.client.Post(service, commandGetUserTags, req, resp); err != nil { + return + } + + if len(resp.Tags) > 0 { + tags = make(map[string][]string, len(resp.Tags)) + for _, item := range resp.Tags { + tags[item.UserId] = item.Tags + } + } + + return +} + +// AddUserTags 添加用户标签 +// 管理员给用户添加标签。 +// 每次请求最多只能给100个用户添加标签,请求体中单个用户添加标签数最多为10个。 +// 单个用户可设置最大标签数为100个,若用户当前标签超过100,则添加新标签之前请先删除旧标签。 +// 单个标签最大长度为50字节。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45941 +func (a *api) AddUserTags(userTags map[string][]string) (err error) { + if c := len(userTags); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the tags of user is not set") + return + } else if c > batchAddUserTagsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of tags to be add cannot exceed %d", batchAddUserTagsLimit)) + return + } + + req := &addUserTagsReq{Tags: make([]*userTag, 0, len(userTags))} + for userId, tags := range userTags { + req.Tags = append(req.Tags, &userTag{ + UserId: userId, + Tags: tags, + }) + } + + if err = a.client.Post(service, commandAddUserTags, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// DeleteUserTags 删除用户标签 +// 管理员给用户删除标签。注意每次最多只能给100个用户删除标签。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45942 +func (a *api) DeleteUserTags(userTags map[string][]string) (err error) { + if c := len(userTags); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the tags of user is not set") + return + } else if c > batchDeleteUserTagsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of tags to be delete cannot exceed %d", batchDeleteUserTagsLimit)) + return + } + + req := &deleteUserTagsReq{Tags: make([]*userTag, 0, len(userTags))} + for userId, tags := range userTags { + req.Tags = append(req.Tags, &userTag{ + UserId: userId, + Tags: tags, + }) + } + + if err = a.client.Post(service, commandDeleteUserTags, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// DeleteUserAllTags 删除用户所有标签 +// 管理员给用户删除所有标签。注意每次最多只能给100个用户删除所有标签。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/45943 +func (a *api) DeleteUserAllTags(userIds ...string) (err error) { + if c := len(userIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the accounts is not set") + return + } else if c > batchDeleteUserAllTagsUserLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of accounts to be delete cannot exceed %d", batchDeleteUserAllTagsUserLimit)) + return + } + + req := &deleteUserAllTagsReq{UserIds: userIds} + + if err = a.client.Post(service, commandDeleteUserAllTags, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} diff --git a/push/enum.go b/push/enum.go new file mode 100644 index 0000000..ae47902 --- /dev/null +++ b/push/enum.go @@ -0,0 +1,38 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 10:16 + * @Desc: TODO + */ + +package push + +import ( + "git.echol.cn/loser/tencent-im/internal/enum" +) + +const ( + // 推送标识 + PushFlagYes = enum.PushFlagYes // 正常推送 + PushFlagNo = enum.PushFlagYes // 不离线推送 + + // 华为推送通知消息分类 + HuaWeiImportanceLow = enum.HuaWeiImportanceLow // LOW类消息 + HuaWeiImportanceNormal = enum.HuaWeiImportanceNormal // NORMAL类消息 + + // 华为推送为“打开应用内指定页面”的前提下透传参数行为 + HuaweiIntentParamAction = enum.HuaweiIntentParamAction // 将透传内容Ext作为Action参数 + HuaweiIntentParamIntent = enum.HuaweiIntentParamIntent // 将透传内容Ext作为Intent参数 + + // VIVO手机推送消息分类 + VivoClassificationOperation = enum.VivoClassificationOperation // 运营类消息 + VivoClassificationSystem = enum.VivoClassificationSystem // 系统类消息 + + // IOS徽章计数模式 + BadgeModeNormal = enum.BadgeModeNormal // 本条消息需要计数 + BadgeModeIgnore = enum.BadgeModeIgnore // 本条消息不需要计数 + + // IOS10的推送扩展开关 + MutableContentNormal = enum.MutableContentNormal // 关闭iOS10的推送扩展 + MutableContentEnable = enum.MutableContentEnable // 开启iOS10的推送扩展 +) diff --git a/push/message.go b/push/message.go new file mode 100644 index 0000000..5760c3c --- /dev/null +++ b/push/message.go @@ -0,0 +1,152 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/3 18:15 + * @Desc: 推送消息实体 + */ + +package push + +import ( + "errors" + + "git.echol.cn/loser/tencent-im/internal/entity" +) + +var errInvalidPushCondition = errors.New("attrs and tags condition cannot be set at the same time") + +type Message struct { + entity.Message + condition *condition +} + +func NewMessage() *Message { + return &Message{} +} + +// SetConditionTagsOr 设置标签的或条件(设置会冲掉之前的标签或条件) +func (m *Message) SetConditionTagsOr(tags ...string) { + if m.condition != nil && m.condition.TagsOr != nil { + m.condition.TagsOr = m.condition.TagsOr[0:0] + } + + m.AddConditionTagsOr(tags...) +} + +// AddConditionTagsOr 添加标签的或条件(添加会累加之前的条件或条件) +func (m *Message) AddConditionTagsOr(tags ...string) { + if m.condition == nil { + m.condition = &condition{} + } + if m.condition.TagsOr == nil { + m.condition.TagsOr = make([]string, 0) + } + m.condition.TagsOr = append(m.condition.TagsOr, tags...) +} + +// SetConditionTagsAnd 设置标签的与条件(设置会冲掉之前的标签与条件) +func (m *Message) SetConditionTagsAnd(tags ...string) { + if m.condition != nil && m.condition.TagsAnd != nil { + m.condition.TagsAnd = m.condition.TagsAnd[0:0] + } + + m.AddConditionTagsAnd(tags...) +} + +// AddConditionTagsAnd 添加标签的与条件(添加会累加之前的标签与条件) +func (m *Message) AddConditionTagsAnd(tags ...string) { + if m.condition == nil { + m.condition = &condition{} + } + if m.condition.TagsAnd == nil { + m.condition.TagsAnd = make([]string, 0) + } + m.condition.TagsAnd = append(m.condition.TagsAnd, tags...) +} + +// SetConditionAttrsOr 设置属性的或条件(设置会冲掉之前的属性或条件) +func (m *Message) SetConditionAttrsOr(attrs map[string]interface{}) { + if m.condition != nil && m.condition.AttrsOr != nil { + m.condition.AttrsOr = make(map[string]interface{}) + } + + m.AddConditionAttrsOr(attrs) +} + +// AddConditionAttrsOr 添加属性的或条件(添加会累加之前的属性或条件) +func (m *Message) AddConditionAttrsOr(attrs map[string]interface{}) { + if m.condition == nil { + m.condition = &condition{} + } + if m.condition.AttrsOr == nil { + m.condition.AttrsOr = make(map[string]interface{}) + } + for k, v := range attrs { + m.condition.AttrsOr[k] = v + } +} + +// SetConditionAttrsAnd 设置属性的与条件(设置会冲掉之前的属性与条件) +func (m *Message) SetConditionAttrsAnd(attrs map[string]interface{}) { + if m.condition != nil && m.condition.AttrsAnd != nil { + m.condition.AttrsAnd = make(map[string]interface{}) + } + + m.AddConditionAttrsAnd(attrs) +} + +// AddConditionAttrsAnd 添加属性的与条件(添加会累加之前的属性与条件) +func (m *Message) AddConditionAttrsAnd(attrs map[string]interface{}) { + if m.condition == nil { + m.condition = &condition{} + } + if m.condition.AttrsAnd == nil { + m.condition.AttrsAnd = make(map[string]interface{}) + } + for k, v := range attrs { + m.condition.AttrsAnd[k] = v + } +} + +// GetCondition 获取推送条件 +func (m *Message) GetCondition() *condition { + return m.condition +} + +// checkError 检测错误 +func (m *Message) checkError() (err error) { + if err = m.CheckLifeTimeArgError(); err != nil { + return + } + + if err = m.CheckBodyArgError(); err != nil { + return + } + + if err = m.checkConditionArgError(); err != nil { + return + } + + return +} + +// checkConditionArgError 检测条件参数错误 +func (m *Message) checkConditionArgError() error { + hasAttrs, hasTags := false, false + + if m.condition != nil { + if m.condition.AttrsAnd != nil || m.condition.AttrsOr != nil { + hasAttrs = true + } + + if m.condition.TagsAnd != nil || m.condition.TagsOr != nil { + hasTags = true + } + } + + if hasAttrs && hasTags { + return errInvalidPushCondition + } + + return nil +} diff --git a/push/types.go b/push/types.go new file mode 100644 index 0000000..16dab12 --- /dev/null +++ b/push/types.go @@ -0,0 +1,126 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 17:39 + * @Desc: Push Api Request And Response Type Definition. + */ + +package push + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + // 推送条件 + condition struct { + TagsAnd []string `json:"TagsAnd"` // (选填)标签条件的交集。标签是一个不超过50字节的字符串。注意属性推送和标签推送不可同时作为推送条件。TagsAnd 条件中的标签个数不能超过10个 + TagsOr []string `json:"TagsOr"` // (选填)标签条件的并集。标签是一个不超过50字节的字符串。注意属性推送和标签推送不可同时作为推送条件。TagsOr 条件中的标签个数不能超过10个 + AttrsAnd map[string]interface{} `json:"AttrsAnd"` // (选填)属性条件的交集。注意属性推送和标签推送不可同时作为推送条件 + AttrsOr map[string]interface{} `json:"AttrsOr"` // (选填)属性条件的并集。注意属性推送和标签推送不可同时作为推送条件 + } + + // 推送(请求) + pushMessageReq struct { + FromUserId string `json:"From_Account,omitempty"` // (选填)消息推送方帐号 + Condition *condition `json:"Condition,omitempty"` // (选填)推送条件 + MsgRandom uint32 `json:"MsgRandom"` // (必填)消息随机数,由随机函数产生 + MsgBody []*types.MsgBody `json:"MsgBody"` // (必填)消息内容 + MsgLifeTime int `json:"MsgLifeTime,omitempty"` // (选填)消息离线存储时间,单位秒,最多保存7天(604800秒)。默认为0,表示不离线存储 + OfflinePushInfo *types.OfflinePushInfo `json:"OfflinePushInfo,omitempty"` // (选填)离线推送信息配置 + } + + // 推送(响应) + pushMessageResp struct { + types.ActionBaseResp + TaskId string `json:"TaskId"` // 推送任务ID + } + + // 设置应用属性名称(请求) + setAttrNamesReq struct { + AttrNames map[string]string `json:"AttrNames"` // (必填)属性名 + } + + // 获取应用属性名称(请求) + getAttrNamesReq struct { + } + + // 获取应用属性名称(响应) + getAttrNamesResp struct { + types.ActionBaseResp + AttrNames map[string]string `json:"AttrNames"` // 属性名 + } + + // 获取用户属性(请求) + getUserAttrsReq struct { + UserIds []string `json:"To_Account"` // (必填)目标用户帐号列表 + } + + // 获取用户属性(响应) + getUserAttrsResp struct { + types.ActionBaseResp + Attrs []*userAttrItem `json:"Attrs"` + } + + // 用户属性 + userAttrItem struct { + UserId string `json:"To_Account"` // 用户UserId + Attrs map[string]interface{} `json:"Attrs"` // 用户属性 + } + + // 设置用户属性(请求) + setUserAttrsReq struct { + Attrs []*userAttrItem `json:"Attrs"` // (必填)用户属性 + } + + // 删除用户属性(请求) + deleteUserAttrsReq struct { + Attrs []deleteUserAttr `json:"Attrs"` // (必填)用户属性 + } + + // 删除的用户属性 + deleteUserAttr struct { + UserId string `json:"To_Account"` // 用户UserId + Attrs []string `json:"Attrs"` // 用户属性 + } + + // 获取用户标签(请求) + getUserTagsReq struct { + UserIds []string `json:"To_Account"` // (必填)目标用户帐号列表 + } + + // 获取用户标签(响应) + getUserTagsResp struct { + types.ActionBaseResp + Tags []userTag `json:"Tags"` // 用户标签内容列表 + } + + // 用户标签 + userTag struct { + UserId string `json:"To_Account"` + Tags []string `json:"Tags"` + } + + // 添加用户标签(请求) + addUserTagsReq struct { + Tags []*userTag `json:"Tags"` // (必填)用户标签内容列表 + } + + // 删除用户标签(请求) + deleteUserTagsReq struct { + Tags []*userTag `json:"Tags"` // (必填)用户标签内容列表 + } + + // 删除用户所有标签(请求) + deleteUserAllTagsReq struct { + UserIds []string `json:"To_Account"` + } + + ImageInfo = types.ImageInfo + MsgTextContent = types.MsgTextContent + MsgFaceContent = types.MsgFaceContent + MsgFileContent = types.MsgFileContent + MsgImageContent = types.MsgImageContent + MsgSoundContent = types.MsgSoundContent + MsgVideoContent = types.MsgVideoContent + MsgCustomContent = types.MsgCustomContent + MsgLocationContent = types.MsgLocationContent +) diff --git a/recentcontact/api.go b/recentcontact/api.go new file mode 100644 index 0000000..146fb35 --- /dev/null +++ b/recentcontact/api.go @@ -0,0 +1,148 @@ +/** + * @Author: wanglin + * @Author: wanglin@vspn.com + * @Date: 2021/10/28 16:05 + * @Desc: TODO + */ + +package recentcontact + +import ( + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + service = "recentcontact" + commandFetchSessions = "get_list" + commandDeleteSession = "delete" +) + +type API interface { + // FetchSessions 拉取会话列表 + // 支持分页拉取会话列表 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/62118 + FetchSessions(arg *FetchSessionsArg) (ret *FetchSessionsRet, err error) + + // PullSessions 续拉取会话列表 + // 本API是借助"拉取会话列表"API进行扩展实现 + // 支持分页拉取会话列表 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/62118 + PullSessions(arg *PullSessionsArg, fn func(ret *FetchSessionsRet)) (err error) + + // DeleteSession 删除单个会话 + // 删除指定会话,支持同步清理漫游消息。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/62119 + DeleteSession(fromUserId, toUserId string, SessionType SessionType, isClearRamble ...bool) (err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// FetchSessions 拉取会话列表 +// 支持分页拉取会话列表 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/62118 +func (a *api) FetchSessions(arg *FetchSessionsArg) (ret *FetchSessionsRet, err error) { + req := &fetchSessionsReq{ + UserId: arg.UserId, + TimeStamp: arg.TimeStamp, + StartIndex: arg.StartIndex, + TopTimeStamp: arg.TopTimeStamp, + TopStartIndex: arg.TopStartIndex, + } + + if arg.IsAllowTopSession { + req.AssistFlags += 1 << 0 + } + + if arg.IsReturnEmptySession { + req.AssistFlags += 1 << 1 + } + + if arg.IsAllowTopSessionPaging { + req.AssistFlags += 1 << 2 + } + + resp := &fetchSessionsResp{} + + if err = a.client.Post(service, commandFetchSessions, req, resp); err != nil { + return + } + + ret = &FetchSessionsRet{ + TimeStamp: resp.TimeStamp, + StartIndex: resp.StartIndex, + TopTimeStamp: resp.TopTimeStamp, + TopStartIndex: resp.TopStartIndex, + List: resp.Sessions, + HasMore: resp.CompleteFlag == 0, + } + + return +} + +// PullSessions 续拉取会话列表 +// 本API是借助"拉取会话列表"API进行扩展实现 +// 支持分页拉取会话列表 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/62118 +func (a *api) PullSessions(arg *PullSessionsArg, fn func(ret *FetchSessionsRet)) (err error) { + var ( + ret *FetchSessionsRet + req = &FetchSessionsArg{ + UserId: arg.UserId, + IsAllowTopSession: arg.IsAllowTopSession, + IsReturnEmptySession: arg.IsReturnEmptySession, + IsAllowTopSessionPaging: arg.IsAllowTopSessionPaging, + } + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchSessions(req) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + req.TimeStamp = ret.TimeStamp + req.StartIndex = ret.StartIndex + req.TopTimeStamp = ret.TopTimeStamp + req.TopStartIndex = ret.TopStartIndex + } + } + + return +} + +// DeleteSession 删除单个会话 +// 删除指定会话,支持同步清理漫游消息。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/62119 +func (a *api) DeleteSession(fromUserId, toUserId string, SessionType SessionType, isClearRamble ...bool) (err error) { + req := &deleteSessionReq{ + FromUserId: fromUserId, + ToUserId: toUserId, + Type: SessionType, + } + + if len(isClearRamble) > 0 && isClearRamble[0] { + req.ClearRamble = 1 + } + + if err = a.client.Post(service, commandDeleteSession, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} diff --git a/recentcontact/enum.go b/recentcontact/enum.go new file mode 100644 index 0000000..97ea4fe --- /dev/null +++ b/recentcontact/enum.go @@ -0,0 +1,16 @@ +/** + * @Author: wanglin + * @Author: wanglin@vspn.com + * @Date: 2021/10/28 17:22 + * @Desc: TODO + */ + +package recentcontact + +// SessionType 会话类型 +type SessionType int + +const ( + SessionTypeC2C SessionType = 1 // C2C 会话 + SessionTypeG2C SessionType = 2 // G2C 会话 +) diff --git a/recentcontact/types.go b/recentcontact/types.go new file mode 100644 index 0000000..84d52aa --- /dev/null +++ b/recentcontact/types.go @@ -0,0 +1,78 @@ +/** + * @Author: wanglin + * @Author: wanglin@vspn.com + * @Date: 2021/10/28 16:11 + * @Desc: TODO + */ + +package recentcontact + +import "git.echol.cn/loser/tencent-im/internal/types" + +// FetchSessionsArg 拉取会话列表(参数) +type FetchSessionsArg struct { + UserId string // (必填)请求拉取该用户的会话列表 + TimeStamp int // (必填)普通会话的起始时间,第一页填 0 + StartIndex int // (必填)普通会话的起始位置,第一页填 0 + TopTimeStamp int // (必填)置顶会话的起始时间,第一页填 0 + TopStartIndex int // (必填)置顶会话的起始位置,第一页填 0 + IsAllowTopSession bool // (选填)是否支持置顶会话 + IsReturnEmptySession bool // (选填)是否返回空会话 + IsAllowTopSessionPaging bool // (选填)是否支持置顶会话分页 +} + +// PullSessionsArg 续拉取会话列表(参数) +type PullSessionsArg struct { + UserId string // (必填)请求拉取该用户的会话列表 + IsAllowTopSession bool // (选填)是否支持置顶会话 + IsReturnEmptySession bool // (选填)是否返回空会话 + IsAllowTopSessionPaging bool // (选填)是否支持置顶会话分页 +} + +// FetchSessionsRet 拉取会话列表(返回) +type FetchSessionsRet struct { + TimeStamp int // 普通会话下一页拉取的起始时间,分页拉取时通过请求包的 TimeStamp 字段带给移动通信后台 + StartIndex int // 普通会话下一页拉取的起始位置,分页拉取时通过请求包的 StartIndex 字段带给移动通信后台 + TopTimeStamp int // 置顶会话下一页拉取的起始时间,分页拉取时通过请求包的 TopTimeStamp 字段带给移动通信后台 + TopStartIndex int // 置顶会话下一页拉取的起始位置,分页拉取时通过请求包的 TopStartIndex 字段带给移动通信后台 + HasMore bool // 是否拉完了数据 + List []*SessionItem // 会话对象数组 +} + +// fetchSessionsReq 拉取会话列表(请求) +type fetchSessionsReq struct { + UserId string `json:"From_Account"` // (必填)请求拉取该用户的会话列表 + TimeStamp int `json:"TimeStamp"` // (必填)普通会话的起始时间,第一页填 0 + StartIndex int `json:"StartIndex"` // (必填)普通会话的起始位置,第一页填 0 + TopTimeStamp int `json:"TopTimeStamp"` // (必填)置顶会话的起始时间,第一页填 0 + TopStartIndex int `json:"TopStartIndex"` // (必填)置顶会话的起始位置,第一页填 0 + AssistFlags int `json:"AssistFlags"` // (必填)会话辅助标志位(bit 0 - 是否支持置顶会话;bit 1 - 是否返回空会话;bit 2 - 是否支持置顶会话分页) +} + +// fetchSessionsResp 拉取会话列表(响应) +type fetchSessionsResp struct { + types.ActionBaseResp + CompleteFlag int `json:"CompleteFlag"` // 结束标识:1 表示已返回全量会话,0 表示还有会话没拉完 + TimeStamp int `json:"TimeStamp"` // 普通会话下一页拉取的起始时间,分页拉取时通过请求包的 TimeStamp 字段带给移动通信后台 + StartIndex int `json:"StartIndex"` // 普通会话下一页拉取的起始位置,分页拉取时通过请求包的 StartIndex 字段带给移动通信后台 + TopTimeStamp int `json:"TopTimeStamp"` // 置顶会话下一页拉取的起始时间,分页拉取时通过请求包的 TopTimeStamp 字段带给移动通信后台 + TopStartIndex int `json:"TopStartIndex"` // 置顶会话下一页拉取的起始位置,分页拉取时通过请求包的 TopStartIndex 字段带给移动通信后台 + Sessions []*SessionItem `json:"SessionItem"` // 会话对象数组 +} + +// SessionItem 会话对象 +type SessionItem struct { + Type SessionType `json:"Type"` // 会话类型:1 表示 C2C 会话;2 表示 G2C 会话 + UserId string `json:"To_Account,omitempty"` // C2C 会话才会返回,返回会话方的 UserID + GroupId string `json:"GroupId,omitempty"` // G2C 会话才会返回,返回群 ID + MsgTime int `json:"MsgTime"` // 会话时间 + TopFlag int `json:"TopFlag"` // 置顶标记:0 标识普通会话;1 标识置顶会话 +} + +// deleteSessionReq 删除单个会话(请求) +type deleteSessionReq struct { + FromUserId string `json:"From_Account"` // (必填)请求删除该 UserID 的会话 + Type SessionType `json:"type"` // (必填)会话类型:1 表示 C2C 会话;2 表示 G2C 会话 + ToUserId string `json:"To_Account"` // (必填)待删除的会话的 UserID + ClearRamble int `json:"ClearRamble,omitempty"` // (选填)是否清理漫游消息:1 表示清理漫游消息;0 表示不清理漫游消息 +} diff --git a/sns/api.go b/sns/api.go new file mode 100644 index 0000000..2f31339 --- /dev/null +++ b/sns/api.go @@ -0,0 +1,901 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/27 20:45 + * @Desc: 关系链管理 + */ + +package sns + +import ( + "fmt" + + "git.echol.cn/loser/tencent-im/internal/core" + "git.echol.cn/loser/tencent-im/internal/enum" + "git.echol.cn/loser/tencent-im/internal/types" +) + +const ( + service = "sns" + commandAddFriend = "friend_add" + commandImportFriend = "friend_import" + commandUpdateFriend = "friend_update" + commandDeleteFriend = "friend_delete" + commandDeleteAllFriend = "friend_delete_all" + commandCheckFriend = "friend_check" + commandGetFriend = "friend_get_list" + commandFetchFriend = "friend_get" + commandAddBlackList = "black_list_add" + commandDeleteBlackList = "black_list_delete" + commandGetBlackList = "black_list_get" + commandCheckBlackList = "black_list_check" + commandAddGroup = "group_add" + commandDeleteGroup = "group_delete" + commandGetGroup = "group_get" + + batchCheckFriendsLimit = 100 // 批量校验好友限制 + batchGetFriendsLimit = 100 // 批量获取好友限制 + batchAddBlacklistLimit = 1000 // 批量添加黑名单限制 + batchDeleteBlacklistLimit = 1000 // 批量删除黑名单限制 + batchCheckBlacklistLimit = 1000 // 批量校验黑名单限制 + batchAddGroupsLimit = 100 // 批量添加分组限制 + batchJoinGroupsLimit = 1000 // 批量加入群组账号限制 + batchDeleteGroupsLimit = 100 // 批量删除分组限制 + batchGetGroupsLimit = 100 // 批量获取分组限制 +) + +type API interface { + // AddFriend 添加单个好友 + // 本方法拓展于“添加多个好友(AddFriends)”方法。 + // 添加好友,仅支持添加单个好友 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1643 + AddFriend(userId string, isBothAdd, isForceAdd bool, friend *Friend) (err error) + + // AddFriends 添加多个好友 + // 添加好友,支持批量添加好友 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1643 + AddFriends(userId string, isBothAdd, isForceAdd bool, friends ...*Friend) (results []*Result, err error) + + // ImportFriend 导入单个好友 + // 本方法拓展于“添加多个好友(ImportFriends)”方法。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/8301 + ImportFriend(userId string, friend *Friend) (err error) + + // ImportFriends 导入多个好友 + // 支持批量导入单向好友。 + // 往同一个用户导入好友时建议采用批量导入的方式,避免并发写导致的写冲突。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/8301 + ImportFriends(userId string, friends ...*Friend) (results []*Result, err error) + + // UpdateFriend 更新单个好友 + // 本方法拓展于“更新多个好友(UpdateFriends)”方法。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/12525 + UpdateFriend(userId string, friend *Friend) (err error) + + // UpdateFriends 更新多个好友 + // 支持批量更新同一用户的多个好友的关系链数据。 + // 更新一个用户多个好友时,建议采用批量方式,避免并发写导致的写冲突。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/12525 + UpdateFriends(userId string, friends ...*Friend) (results []*Result, err error) + + // DeleteFriend 删除单个好友 + // 本方法拓展于“删除多个好友(DeleteFriends)”方法。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1644 + DeleteFriend(userId string, isBothDelete bool, deletedUserId string) (err error) + + // DeleteFriends 删除多个好友 + // 删除好友,支持单向删除好友和双向删除好友。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1644 + DeleteFriends(userId string, isBothDelete bool, deletedUserIds ...string) (results []*Result, err error) + + // DeleteAllFriends 删除所有好友 + // 清除指定用户的标配好友数据和自定义好友数据。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1645 + DeleteAllFriends(userId string, deleteType ...DeleteType) (err error) + + // CheckFriend 校验单个好友 + // 本方法拓展于“校验多个好友(CheckFriends)”方法。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1646 + CheckFriend(userId string, checkType CheckType, checkedUserId string) (relation string, err error) + + // CheckFriends 校验多个好友 + // 支持批量校验好友关系。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1646 + CheckFriends(userId string, checkType CheckType, checkedUserIds ...string) (results []*CheckResult, err error) + + // GetFriend 拉取单个指定好友 + // 本方法拓展于“拉取多个指定好友(GetFriends)”方法。 + // 支持拉取指定好友的好友数据和资料数据。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/8609 + GetFriend(userId string, tagList []string, friendUserId string) (friend *Friend, err error) + + // GetFriends 拉取多个指定好友 + // 支持拉取指定好友的好友数据和资料数据。 + // 建议每次拉取的好友数不超过100,避免因数据量太大导致回包失败。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/8609 + GetFriends(userId string, tagList []string, friendUserIds ...string) (friends []*Friend, err error) + + // FetchFriends 拉取好友 + // 分页拉取全量好友数据。 + // 不支持资料数据的拉取。 + // 不需要指定请求拉取的字段,默认返回全量的标配好友数据和自定义好友数据。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1647 + FetchFriends(userId string, startIndex int, sequence ...int) (ret *FetchFriendsRet, err error) + + // PullFriends 续拉取好友 + // 本API是借助"拉取好友"API进行扩展实现 + // 分页拉取全量好友数据。 + // 不支持资料数据的拉取。 + // 不需要指定请求拉取的字段,默认返回全量的标配好友数据和自定义好友数据。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/1647 + PullFriends(userId string, fn func(ret *FetchFriendsRet)) (err error) + + // AddBlacklist 添加黑名单 + // 添加黑名单,支持批量添加黑名单。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/3718 + AddBlacklist(userId string, blackedUserIds ...string) (results []*Result, err error) + + // DeleteBlacklist 删除黑名单 + // 删除指定黑名单。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/3719 + DeleteBlacklist(userId string, deletedUserIds ...string) (results []*Result, err error) + + // FetchBlacklist 拉取黑名单 + // 支持分页拉取所有黑名单。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/3722 + FetchBlacklist(userId string, maxLimited int, startIndexAndSequence ...int) (ret *FetchBlacklistRet, err error) + + // PullBlacklist 拉取黑名单 + // 本API是借助"拉取黑名单"API进行扩展实现 + // 支持分页拉取所有黑名单。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/3722 + PullBlacklist(userId string, maxLimited int, fn func(ret *FetchBlacklistRet)) (err error) + + // CheckBlacklist 校验黑名单 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/3725 + CheckBlacklist(userId string, checkType BlacklistCheckType, checkedUserIds ...string) (results []*CheckResult, err error) + + // AddGroups 添加分组 + // 添加分组,支持批量添加分组,并将指定好友加入到新增分组中。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/10107 + AddGroups(userId string, groupNames []string, joinedUserIds ...[]string) (currentSequence int, results []*Result, err error) + + // DeleteGroups 删除分组 + // 删除指定分组。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/10108 + DeleteGroups(userId string, groupNames ...string) (currentSequence int, err error) + + // GetGroups 拉取分组 + // 拉取分组,支持指定分组以及拉取分组下的好友列表。 + // 点击查看详细文档: + // https://cloud.tencent.com/document/product/269/54763 + GetGroups(userId string, lastSequence int, isGetFriends bool, groupNames ...string) (currentSequence int, results []*GroupResult, err error) +} + +type api struct { + client core.Client +} + +func NewAPI(client core.Client) API { + return &api{client: client} +} + +// AddFriend 添加单个好友 +// 本方法拓展于“添加多个好友(AddFriends)”方法。 +// 添加好友,仅支持添加单个好友 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1643 +func (a *api) AddFriend(userId string, isBothAdd, isForceAdd bool, friend *Friend) (err error) { + var results []*Result + + if results, err = a.AddFriends(userId, isBothAdd, isForceAdd, friend); err != nil { + return + } + + for _, result := range results { + if result.UserId == friend.GetUserId() && result.ResultCode != enum.SuccessCode { + return core.NewError(result.ResultCode, result.ResultInfo) + } + } + + return +} + +// AddFriends 添加多个好友 +// 添加好友,支持批量添加好友 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1643 +func (a *api) AddFriends(userId string, isBothAdd, isForceAdd bool, friends ...*Friend) (results []*Result, err error) { + if len(friends) == 0 { + err = core.NewError(enum.InvalidParamsCode, "the friends is not set") + return + } + + req := &addFriendsReq{UserId: userId, Friends: make([]*addFriendItem, 0, len(friends))} + + for _, friend := range friends { + if err = friend.checkError(); err != nil { + return + } + + item := new(addFriendItem) + item.UserId = friend.GetUserId() + item.Remark, _ = friend.GetRemark() + item.AddWording, _ = friend.GetAddWording() + item.AddSource, _ = friend.GetSrcAddSource() + if groups, exist := friend.GetGroup(); exist { + item.GroupName = groups[0] + } + + req.Friends = append(req.Friends, item) + } + + if isBothAdd { + req.AddType = AddTypeBoth + } else { + req.AddType = AddTypeSingle + } + + if isForceAdd { + req.ForceAddFlags = ForceAddYes + } else { + req.ForceAddFlags = ForceAddNo + } + + resp := &addFriendsResp{} + + if err = a.client.Post(service, commandAddFriend, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// ImportFriend 导入单个好友 +// 本方法拓展于“添加多个好友(ImportFriends)”方法。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/8301 +func (a *api) ImportFriend(userId string, friend *Friend) (err error) { + var results []*Result + + if results, err = a.ImportFriends(userId, friend); err != nil { + return + } + + for _, result := range results { + if result.UserId == friend.GetUserId() && result.ResultCode != enum.SuccessCode { + return core.NewError(result.ResultCode, result.ResultInfo) + } + } + + return +} + +// ImportFriends 导入多个好友 +// 支持批量导入单向好友。 +// 往同一个用户导入好友时建议采用批量导入的方式,避免并发写导致的写冲突。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/8301 +func (a *api) ImportFriends(userId string, friends ...*Friend) (results []*Result, err error) { + if len(friends) == 0 { + err = core.NewError(enum.InvalidParamsCode, "the friends is not set") + return + } + + req := &importFriendsReq{UserId: userId, Friends: make([]*importFriendItem, 0, len(friends))} + + for _, friend := range friends { + if err = friend.checkError(); err != nil { + return + } + + item := new(importFriendItem) + item.UserId = friend.GetUserId() + item.Remark, _ = friend.GetRemark() + item.AddWording, _ = friend.GetAddWording() + item.AddTime, _ = friend.GetAddTime() + item.RemarkTime, _ = friend.GetRemarkTime() + item.AddSource, _ = friend.GetSrcAddSource() + item.GroupName, _ = friend.GetGroup() + + if customAttrs := friend.GetSNSCustomAttrs(); len(customAttrs) > 0 { + item.CustomData = make([]*types.TagPair, 0, len(customAttrs)) + for k, v := range customAttrs { + item.CustomData = append(item.CustomData, &types.TagPair{ + Tag: k, + Value: v, + }) + } + } + + req.Friends = append(req.Friends, item) + } + + resp := &importFriendsResp{} + + if err = a.client.Post(service, commandImportFriend, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// UpdateFriend 更新单个好友 +// 本方法拓展于“更新多个好友(UpdateFriends)”方法。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/12525 +func (a *api) UpdateFriend(userId string, friend *Friend) (err error) { + var results []*Result + + if results, err = a.UpdateFriends(userId, friend); err != nil { + return + } + + for _, result := range results { + if result.UserId == friend.GetUserId() && result.ResultCode != enum.SuccessCode { + return core.NewError(result.ResultCode, result.ResultInfo) + } + } + + return +} + +// UpdateFriends 更新多个好友 +// 支持批量更新同一用户的多个好友的关系链数据。 +// 更新一个用户多个好友时,建议采用批量方式,避免并发写导致的写冲突。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/12525 +func (a *api) UpdateFriends(userId string, friends ...*Friend) (results []*Result, err error) { + if len(friends) == 0 { + err = core.NewError(enum.InvalidParamsCode, "the friends is not set") + return + } + + req := &updateFriendsReq{UserId: userId, Friends: make([]*updateFriendItem, 0, len(friends))} + + for _, friend := range friends { + item := new(updateFriendItem) + item.UserId = friend.GetUserId() + + for k, v := range friend.GetSNSAttrs() { + switch k { + case FriendAttrAddSource, FriendAttrAddTime, FriendAttrRemarkTime, FriendAttrAddWording: + default: + item.Attrs = append(item.Attrs, &types.TagPair{ + Tag: k, + Value: v, + }) + } + } + + for k, v := range friend.GetSNSCustomAttrs() { + item.Attrs = append(item.Attrs, &types.TagPair{ + Tag: k, + Value: v, + }) + } + + req.Friends = append(req.Friends, item) + } + + resp := &updateFriendsResp{} + + if err = a.client.Post(service, commandUpdateFriend, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// DeleteFriend 删除单个好友 +// 本方法拓展于“删除多个好友(DeleteFriends)”方法。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1644 +func (a *api) DeleteFriend(userId string, isBothDelete bool, deletedUserId string) (err error) { + var results []*Result + + if results, err = a.DeleteFriends(userId, isBothDelete, deletedUserId); err != nil { + return + } + + if results != nil && len(results) > 0 { + for _, result := range results { + if result.UserId == deletedUserId && result.ResultCode != enum.SuccessCode { + return core.NewError(result.ResultCode, result.ResultInfo) + } + } + } + + return +} + +// DeleteFriends 删除多个好友 +// 删除好友,支持单向删除好友和双向删除好友。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1644 +func (a *api) DeleteFriends(userId string, isBothDelete bool, deletedUserIds ...string) (results []*Result, err error) { + req := &deleteFriendsReq{UserId: userId, DeletedUserIds: deletedUserIds} + resp := &deleteFriendsResp{} + + if isBothDelete { + req.DeleteType = DeleteTypeBoth + } else { + req.DeleteType = DeleteTypeSingle + } + + if err = a.client.Post(service, commandDeleteFriend, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// DeleteAllFriends 删除所有好友 +// 清除指定用户的标配好友数据和自定义好友数据。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1645 +func (a *api) DeleteAllFriends(userId string, deleteType ...DeleteType) (err error) { + req := &deleteAllFriendsReq{UserId: userId} + + if len(deleteType) > 0 { + req.DeleteType = deleteType[0] + } else { + req.DeleteType = DeleteTypeSingle + } + + if err = a.client.Post(service, commandDeleteAllFriend, req, &types.ActionBaseResp{}); err != nil { + return + } + + return +} + +// CheckFriend 校验单个好友 +// 本方法拓展于“校验多个好友(CheckFriends)”方法。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1646 +func (a *api) CheckFriend(userId string, checkType CheckType, checkedUserId string) (relation string, err error) { + var results []*CheckResult + + if results, err = a.CheckFriends(userId, checkType, checkedUserId); err != nil { + return + } + + if results != nil && len(results) > 0 { + for _, result := range results { + if result.UserId == checkedUserId { + if result.ResultCode != enum.SuccessCode { + err = core.NewError(result.ResultCode, result.ResultInfo) + return + } + + relation = result.Relation + return + } + } + } + + return +} + +// CheckFriends 校验多个好友 +// 支持批量校验好友关系。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1646 +func (a *api) CheckFriends(userId string, checkType CheckType, checkedUserIds ...string) (results []*CheckResult, err error) { + if c := len(checkedUserIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the accounts is not set") + return + } else if c > batchCheckFriendsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of checked accounts cannot exceed %d", batchCheckFriendsLimit)) + return + } + + req := &checkFriendsReq{UserId: userId, CheckedUserIds: checkedUserIds, CheckType: checkType} + resp := &checkFriendsResp{} + + if err = a.client.Post(service, commandCheckFriend, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// GetFriend 拉取单个指定好友 +// 本方法拓展于“拉取多个指定好友(GetFriends)”方法。 +// 支持拉取指定好友的好友数据和资料数据。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/8609 +func (a *api) GetFriend(userId string, tagList []string, friendUserId string) (friend *Friend, err error) { + var friends []*Friend + + if friends, err = a.GetFriends(userId, tagList, friendUserId); err != nil { + return + } + + if len(friends) > 0 { + friend = friends[0] + } + + return +} + +// GetFriends 拉取多个指定好友 +// 支持拉取指定好友的好友数据和资料数据。 +// 建议每次拉取的好友数不超过100,避免因数据量太大导致回包失败。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/8609 +func (a *api) GetFriends(userId string, tagList []string, friendUserIds ...string) (friends []*Friend, err error) { + if c := len(friendUserIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the account of friends is not set") + return + } else if c > batchGetFriendsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of friend's account cannot exceed %d", batchGetFriendsLimit)) + return + } + + req := &getFriendsReq{UserId: userId, FriendUserIds: friendUserIds} + resp := &getFriendsResp{} + + for _, tag := range tagList { + switch tag { + case FriendAttrRemarkTime: + default: + req.TagList = append(req.TagList, tag) + } + } + + if err = a.client.Post(service, commandGetFriend, req, resp); err != nil { + return + } + + friends = make([]*Friend, 0, len(resp.Friends)) + + for _, item := range resp.Friends { + friend := NewFriend(item.UserId) + friend.SetError(item.ResultCode, item.ResultInfo) + for _, v := range item.Profiles { + friend.SetAttr(v.Tag, v.Value) + } + friends = append(friends, friend) + } + + return +} + +// FetchFriends 拉取好友 +// 分页拉取全量好友数据。 +// 不支持资料数据的拉取。 +// 不需要指定请求拉取的字段,默认返回全量的标配好友数据和自定义好友数据。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1647 +func (a *api) FetchFriends(userId string, startIndex int, sequence ...int) (ret *FetchFriendsRet, err error) { + req := &fetchFriendsReq{UserId: userId, StartIndex: startIndex} + resp := &fetchFriendsResp{} + + if len(sequence) > 0 { + req.StandardSequence = sequence[0] + } + + if len(sequence) > 1 { + req.CustomSequence = sequence[1] + } + + if err = a.client.Post(service, commandFetchFriend, req, resp); err != nil { + return + } + + ret = &FetchFriendsRet{ + StandardSequence: resp.StandardSequence, + CustomSequence: resp.CustomSequence, + StartIndex: resp.NextStartIndex, + Total: resp.FriendNum, + HasMore: resp.CompleteFlag == 0, + List: make([]*Friend, 0, len(resp.Friends)), + } + + for _, item := range resp.Friends { + friend := NewFriend(item.UserId) + for _, v := range item.Values { + friend.SetAttr(v.Tag, v.Value) + } + ret.List = append(ret.List, friend) + } + + return +} + +// PullFriends 续拉取好友 +// 本API是借助"拉取好友"API进行扩展实现 +// 分页拉取全量好友数据。 +// 不支持资料数据的拉取。 +// 不需要指定请求拉取的字段,默认返回全量的标配好友数据和自定义好友数据。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/1647 +func (a *api) PullFriends(userId string, fn func(ret *FetchFriendsRet)) (err error) { + var ( + ret *FetchFriendsRet + startIndex int + standardSequence int + customSequence int + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchFriends(userId, startIndex, standardSequence, customSequence) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + startIndex = ret.StartIndex + standardSequence = ret.StandardSequence + customSequence = ret.CustomSequence + } + } + + return +} + +// AddBlacklist 添加黑名单 +// 添加黑名单,支持批量添加黑名单。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/3718 +func (a *api) AddBlacklist(userId string, blackedUserIds ...string) (results []*Result, err error) { + if c := len(blackedUserIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the blacked accounts is not set") + return + } else if c > batchAddBlacklistLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of blacked accounts cannot exceed %d", batchAddBlacklistLimit)) + return + } + + req := &addBlacklistReq{UserId: userId, BlackedUserIds: blackedUserIds} + resp := &addBlacklistResp{} + + if err = a.client.Post(service, commandAddBlackList, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// DeleteBlacklist 删除黑名单 +// 删除指定黑名单。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/3719 +func (a *api) DeleteBlacklist(userId string, deletedUserIds ...string) (results []*Result, err error) { + if c := len(deletedUserIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the deleted accounts is not set") + return + } else if c > batchDeleteBlacklistLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of deleted accounts cannot exceed %d", batchDeleteBlacklistLimit)) + return + } + + req := &deleteBlacklistReq{UserId: userId, DeletedUserIds: deletedUserIds} + resp := &deleteBlacklistResp{} + + if err = a.client.Post(service, commandDeleteBlackList, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// FetchBlacklist 拉取黑名单 +// 支持分页拉取所有黑名单。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/3722 +func (a *api) FetchBlacklist(userId string, maxLimited int, startIndexAndSequence ...int) (ret *FetchBlacklistRet, err error) { + req := &fetchBlacklistReq{UserId: userId, MaxLimited: maxLimited} + + if len(startIndexAndSequence) > 0 { + req.StartIndex = startIndexAndSequence[0] + } + + if len(startIndexAndSequence) > 1 { + req.LastSequence = startIndexAndSequence[1] + } + + resp := &fetchBlacklistResp{} + + if err = a.client.Post(service, commandGetBlackList, req, resp); err != nil { + return + } + + ret = &FetchBlacklistRet{ + StartIndex: resp.StartIndex, + StandardSequence: resp.CurrentSequence, + List: resp.Blacklists, + HasMore: resp.StartIndex != 0, + } + + return +} + +// PullBlacklist 拉取黑名单 +// 本API是借助"拉取黑名单"API进行扩展实现 +// 支持分页拉取所有黑名单。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/3722 +func (a *api) PullBlacklist(userId string, maxLimited int, fn func(ret *FetchBlacklistRet)) (err error) { + var ( + ret *FetchBlacklistRet + startIndex = 0 + standardSequence = 0 + ) + + for ret == nil || ret.HasMore { + ret, err = a.FetchBlacklist(userId, maxLimited, startIndex, standardSequence) + if err != nil { + return + } + + fn(ret) + + if ret.HasMore { + startIndex = ret.StartIndex + standardSequence = ret.StandardSequence + } + } + + return +} + +// CheckBlacklist 校验黑名单 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/3725 +func (a *api) CheckBlacklist(userId string, checkType BlacklistCheckType, checkedUserIds ...string) (results []*CheckResult, err error) { + if c := len(checkedUserIds); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the checked accounts is not set") + return + } else if c > batchCheckBlacklistLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of checked accounts cannot exceed %d", batchCheckBlacklistLimit)) + return + } + + req := &checkBlacklistReq{UserId: userId, CheckedUserIds: checkedUserIds, CheckType: checkType} + resp := &checkBlacklistResp{} + + if err = a.client.Post(service, commandCheckBlackList, req, resp); err != nil { + return + } + + results = resp.Results + + return +} + +// AddGroups 添加分组 +// 添加分组,支持批量添加分组,并将指定好友加入到新增分组中。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/10107 +func (a *api) AddGroups(userId string, groupNames []string, joinedUserIds ...[]string) (currentSequence int, results []*Result, err error) { + if c := len(groupNames); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the added groups is not set") + return + } else if c > batchAddGroupsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of added groups cannot exceed %d", batchAddGroupsLimit)) + return + } + + req := &addGroupsReq{UserId: userId, GroupNames: groupNames} + + if len(joinedUserIds) > 0 { + if c := len(joinedUserIds[0]); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the added groups is not set") + return + } else if c > batchJoinGroupsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of accounts joining the group cannot exceed %d", batchJoinGroupsLimit)) + return + } + + req.JoinedUserIds = joinedUserIds[0] + } + + resp := &addGroupsResp{} + + if err = a.client.Post(service, commandAddGroup, req, resp); err != nil { + return + } + + currentSequence = resp.CurrentSequence + results = resp.Results + + return +} + +// DeleteGroups 删除分组 +// 删除指定分组。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/10108 +func (a *api) DeleteGroups(userId string, groupNames ...string) (currentSequence int, err error) { + if c := len(groupNames); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the deleted groups is not set") + return + } else if c > batchDeleteGroupsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of deleted groups cannot exceed %d", batchDeleteGroupsLimit)) + return + } + + req := &deleteGroupsReq{UserId: userId, GroupNames: groupNames} + resp := &deleteGroupsResp{} + + if err = a.client.Post(service, commandDeleteGroup, req, resp); err != nil { + return + } + + currentSequence = resp.CurrentSequence + + return +} + +// GetGroups 拉取分组 +// 拉取分组,支持指定分组以及拉取分组下的好友列表。 +// 点击查看详细文档: +// https://cloud.tencent.com/document/product/269/54763 +func (a *api) GetGroups(userId string, lastSequence int, isGetFriends bool, groupNames ...string) (currentSequence int, results []*GroupResult, err error) { + if c := len(groupNames); c == 0 { + err = core.NewError(enum.InvalidParamsCode, "the gotten groups is not set") + return + } else if c > batchGetGroupsLimit { + err = core.NewError(enum.InvalidParamsCode, fmt.Sprintf("the number of gotten groups cannot exceed %d", batchGetGroupsLimit)) + return + } + + req := &getGroupsReq{UserId: userId, LastSequence: lastSequence, GroupNames: groupNames} + resp := &getGroupsResp{} + + if isGetFriends { + req.NeedFriend = NeedFriendYes + } else { + req.NeedFriend = NeedFriendNo + } + + if err = a.client.Post(service, commandGetGroup, req, resp); err != nil { + return + } + + currentSequence = resp.CurrentSequence + results = resp.Results + + return +} diff --git a/sns/enum.go b/sns/enum.go new file mode 100644 index 0000000..a3b729d --- /dev/null +++ b/sns/enum.go @@ -0,0 +1,90 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/9/7 14:40 + * @Desc: TODO + */ + +package sns + +import "git.echol.cn/loser/tencent-im/internal/enum" + +type ( + // AddType 添加类型 + AddType string + + // DeleteType 删除类型 + DeleteType string + + // CheckType 校验模式 + CheckType string + + // ForceAddType 强制添加类型 + ForceAddType int + + // BlacklistCheckType 黑名单校验模式 + BlacklistCheckType string + + // NeedFriendType 是否需要拉取分组下的User列表 + NeedFriendType string +) + +const ( + // 标准资料字段 + StandardAttrNickname = enum.StandardAttrNickname // 昵称 + StandardAttrGender = enum.StandardAttrGender // 性别 + StandardAttrBirthday = enum.StandardAttrBirthday // 生日 + StandardAttrLocation = enum.StandardAttrLocation // 所在地 + StandardAttrSignature = enum.StandardAttrSignature // 个性签名 + StandardAttrAllowType = enum.StandardAttrAllowType // 加好友验证方式 + StandardAttrLanguage = enum.StandardAttrLanguage // 语言 + StandardAttrAvatar = enum.StandardAttrAvatar // 头像URL + StandardAttrMsgSettings = enum.StandardAttrMsgSettings // 消息设置 + StandardAttrAdminForbidType = enum.StandardAttrAdminForbidType // 管理员禁止加好友标识 + StandardAttrLevel = enum.StandardAttrLevel // 等级 + StandardAttrRole = enum.StandardAttrRole // 角色 + + // 好友属性 + FriendAttrAddSource = "Tag_SNS_IM_AddSource" // 添加源 + FriendAttrRemark = "Tag_SNS_IM_Remark" // 备注 + FriendAttrGroup = "Tag_SNS_IM_Group" // 分组 + FriendAttrAddWording = "Tag_SNS_IM_AddWording" // 附言信息 + FriendAttrAddTime = "Tag_SNS_IM_AddTime" // 添加时间 + FriendAttrRemarkTime = "Tag_SNS_IM_RemarkTime" // 备注时间 + + // 添加类型 + AddTypeSingle AddType = "Add_Type_Single" // 单向添加 + AddTypeBoth AddType = "Add_Type_Both" // 双向添加 + + // 删除类型 + DeleteTypeSingle DeleteType = "Delete_Type_Single" // 单向删除 + DeleteTypeBoth DeleteType = "Delete_Type_Both" // 双向删除 + + // 校验模式 + CheckTypeSingle CheckType = "CheckResult_Type_Single" // 单向校验好友关系 + CheckTypeBoth CheckType = "CheckResult_Type_Both" // 双向校验好友关系 + + // 黑名单校验模式 + BlacklistCheckTypeSingle BlacklistCheckType = "BlackCheckResult_Type_Single" // 单向校验黑名单关系 + BlacklistCheckTypeBoth BlacklistCheckType = "BlackCheckResult_Type_Both" // 双向校验黑名单关系 + + // 强制加好友类型 + ForceAddYes ForceAddType = 1 // 强制加好友 + ForceAddNo ForceAddType = 0 // 常规加好友 + + // 是否需要拉取分组下的 User 列表 + NeedFriendYes NeedFriendType = "Need_Friend_Type_Yes" // 需要拉取 + NeedFriendNo NeedFriendType = "Need_Friend_Type_No" // 不需要拉取 + + // 好友关系结果 + CheckResultTypeNoRelation = "CheckResult_Type_NoRelation" // From_Account 的好友表中没有 To_Account,但无法确定 To_Account 的好友表中是否有 From_Account + CheckResultTypeAWithB = "CheckResult_Type_AWithB" // From_Account 的好友表中有 To_Account,但无法确定 To_Account 的好友表中是否有 From_Account + CheckResultTypeBWithA = "CheckResult_Type_BWithA" // From_Account 的好友表中没有 To_Account,但 To_Account 的好友表中有 From_Account + CheckResultTypeBothWay = "CheckResult_Type_BothWay" // From_Account 的好友表中有 To_Account,To_Account 的好友表中也有 From_Account + + // 黑名单关系结果 + BlackCheckResultTypeNO = "BlackCheckResult_Type_NO" // From_Account 的黑名单中没有 To_Account,但无法确定 To_Account 的黑名单中是否有 From_Account + BlackCheckResultTypeAWithB = "BlackCheckResult_Type_AWithB" // From_Account 的黑名单中有 To_Account,但无法确定 To_Account 的黑名单中是否有 From_Account + BlackCheckResultTypeBWithA = "BlackCheckResult_Type_BWithA" // From_Account 的黑名单中没有 To_Account,但 To_Account 的黑名单中有 From_Account + BlackCheckResultTypeBothWay = "BlackCheckResult_Type_BothWay" // From_Account 的黑名单中有 To_Account,To_Account 的黑名单中也有 From_Account +) diff --git a/sns/friend.go b/sns/friend.go new file mode 100644 index 0000000..6a2fc4d --- /dev/null +++ b/sns/friend.go @@ -0,0 +1,206 @@ +/** + * @Author: Echo + * @Email:1711788888@qq.com + * @Date: 2021/8/30 2:55 下午 + * @Desc: 好友关系 + */ + +package sns + +import ( + "errors" + "fmt" + "strings" + + "git.echol.cn/loser/tencent-im/internal/entity" +) + +var ( + errNotSetAccount = errors.New("the friend's account is not set") + errNotSetAddSource = errors.New("the friend's add source is not set") +) + +type Friend struct { + entity.User + customAttrs map[string]interface{} +} + +func NewFriend(userId ...string) *Friend { + f := new(Friend) + f.customAttrs = make(map[string]interface{}) + + if len(userId) > 0 { + f.SetUserId(userId[0]) + } + + return f +} + +// SetAddSource 设置添加来源 +func (f *Friend) SetAddSource(addSource string) { + f.SetAttr(FriendAttrAddSource, "AddSource_Type_"+addSource) +} + +// GetAddSource 获取添加来源 +func (f *Friend) GetAddSource() (addSource string, exist bool) { + var v interface{} + if v, exist = f.GetAttr(FriendAttrAddSource); exist { + addSource = strings.TrimLeft(v.(string), "AddSource_Type_") + } + + return +} + +// GetSrcAddSource 获取添加来源(原始的) +func (f *Friend) GetSrcAddSource() (addSource string, exist bool) { + var v interface{} + if v, exist = f.GetAttr(FriendAttrAddSource); exist { + addSource = v.(string) + } + + return +} + +// SetRemark 设置备注 +func (f *Friend) SetRemark(remark string) { + f.SetAttr(FriendAttrRemark, remark) +} + +// GetRemark 获取备注 +func (f *Friend) GetRemark() (remark string, exist bool) { + var v interface{} + if v, exist = f.GetAttr(FriendAttrRemark); exist { + remark = v.(string) + } + + return +} + +// SetGroup 设置分组 +func (f *Friend) SetGroup(groupName ...string) { + f.SetAttr(FriendAttrGroup, groupName) +} + +// GetGroup 获取分组 +func (f *Friend) GetGroup() (groups []string, exist bool) { + var v interface{} + if v, exist = f.GetAttr(FriendAttrGroup); exist && v != nil { + if vv, ok := v.([]interface{}); ok { + for _, group := range vv { + groups = append(groups, group.(string)) + } + } + } + + return +} + +// SetAddWording 设置形成好友关系时的附言信息 +func (f *Friend) SetAddWording(addWording string) { + f.SetAttr(FriendAttrAddWording, addWording) +} + +// GetAddWording 获取形成好友关系时的附言信息 +func (f *Friend) GetAddWording() (addWording string, exist bool) { + var v interface{} + if v, exist = f.GetAttr(FriendAttrAddWording); exist { + addWording = v.(string) + } + + return +} + +// SetAddTime 设置添加时间(忽略) +func (f *Friend) SetAddTime(addTime int64) { + f.SetAttr(FriendAttrAddTime, addTime) +} + +// GetAddTime 获取添加时间 +func (f *Friend) GetAddTime() (addTime int64, exist bool) { + var v interface{} + if v, exist = f.GetAttr(FriendAttrAddTime); exist { + addTime = v.(int64) + } + + return +} + +// SetRemarkTime 设置备注时间 +func (f *Friend) SetRemarkTime(remarkTime int64) { + f.SetAttr(FriendAttrRemarkTime, remarkTime) +} + +// GetRemarkTime 获取备注时间 +func (f *Friend) GetRemarkTime() (remarkTime int64, exist bool) { + var v interface{} + if v, exist = f.GetAttr(FriendAttrRemarkTime); exist { + remarkTime = v.(int64) + } + + return +} + +// SetSNSCustomAttr 设置SNS自定义关系数据(自定义字段需要单独申请,请在 IM 控制台 >应用配置>功能配置申请自定义好友字段,申请提交后,自定义好友字段将在5分钟内生效) +func (f *Friend) SetSNSCustomAttr(name string, value interface{}) { + if f.customAttrs == nil { + f.customAttrs = make(map[string]interface{}) + } + + f.customAttrs[fmt.Sprintf("%s_%s", "Tag_SNS_Custom", name)] = value +} + +// GetSNSCustomAttr 设置SNS自定义关系数据 (自定义字段需要单独申请,请在 IM 控制台 >应用配置>功能配置申请自定义好友字段,申请提交后,自定义好友字段将在5分钟内生效) +func (f *Friend) GetSNSCustomAttr(name string) (value interface{}, exist bool) { + if f.customAttrs == nil { + return + } + + value, exist = f.customAttrs[fmt.Sprintf("%s_%s", "Tag_SNS_Custom", name)] + return +} + +// GetSNSAttrs 获取SNS标准关系数据 +func (f *Friend) GetSNSAttrs() (attrs map[string]interface{}) { + attrs = make(map[string]interface{}) + + for k, v := range f.GetAttrs() { + switch k { + case FriendAttrAddSource, FriendAttrRemark, FriendAttrGroup, FriendAttrAddWording, FriendAttrAddTime, FriendAttrRemarkTime: + attrs[k] = v + } + } + + return +} + +// GetSNSCustomAttrs 获取SNS自定义关系数据(自定义字段需要单独申请,请在 IM 控制台 >应用配置>功能配置申请自定义好友字段,申请提交后,自定义好友字段将在5分钟内生效) +func (f *Friend) GetSNSCustomAttrs() (attrs map[string]interface{}) { + attrs = make(map[string]interface{}) + + if f.customAttrs == nil { + return + } + + for k, v := range f.customAttrs { + switch k { + case FriendAttrAddSource, FriendAttrRemark, FriendAttrGroup, FriendAttrAddWording, FriendAttrAddTime, FriendAttrRemarkTime: + default: + attrs[k] = v + } + } + + return +} + +// checkError 检测参数错误 +func (f *Friend) checkError() error { + if f.GetUserId() == "" { + return errNotSetAccount + } + + if _, exist := f.GetSrcAddSource(); !exist { + return errNotSetAddSource + } + + return nil +} diff --git a/sns/types.go b/sns/types.go new file mode 100644 index 0000000..23d549f --- /dev/null +++ b/sns/types.go @@ -0,0 +1,301 @@ +/** + * @Author: Echo + * @Author:1711788888@qq.com + * @Date: 2021/5/29 17:34 + * @Desc: 关系链管理数据类型 + */ + +package sns + +import "git.echol.cn/loser/tencent-im/internal/types" + +type ( + // 添加的好友信息 + addFriendItem struct { + UserId string `json:"To_Account"` // (必填)好友的UserID + AddSource string `json:"AddSource"` // (必填)加好友来源 + Remark string `json:"Remark,omitempty"` // (选填)加好友备注 + GroupName string `json:"GroupName,omitempty"` // (选填)分组信息,添加好友时只允许设置一个分组,因此使用 String 类型即可 + AddWording string `json:"AddWording,omitempty"` // (选填)好友关系时的附言信息 + } + + // 添加好友(请求) + addFriendsReq struct { + UserId string `json:"From_Account"` // (必填)需要为该UserID添加好友 + Friends []*addFriendItem `json:"AddFriendItem"` // (必填)好友结构体对象 + AddType AddType `json:"AddType"` // (选填)加好友方式(默认双向加好友方式)Add_Type_Single:表示单向加好友;Add_Type_Both:表示双向加好友 + ForceAddFlags ForceAddType `json:"ForceAddFlags"` // (选填)管理员强制加好友标记:1表示强制加好友,0表示常规加好友方式 + } + + // 添加好友(响应) + addFriendsResp struct { + types.ActionBaseResp + Results []*Result `json:"ResultItem"` // 批量加好友的结果对象数组 + } + + // Result 添加结果 + Result struct { + UserId string `json:"To_Account"` // 请求添加的好友的UserID + ResultCode int `json:"ResultCode"` // 处理结果,0表示成功,非0表示失败 + ResultInfo string `json:"ResultInfo"` // 错误描述信息 + } + + // 导入好友(请求) + importFriendsReq struct { + UserId string `json:"From_Account"` // 需要为该UserID添加好友 + Friends []*importFriendItem `json:"AddFriendItem"` // 好友结构体对象 + } + + // 导入好友(响应) + importFriendsResp struct { + types.ActionBaseResp + Results []*Result `json:"ResultItem"` // 结果对象数组 + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + } + + // 导入好友 + importFriendItem struct { + UserId string `json:"To_Account"` // (必填)好友的UserID + AddSource string `json:"AddSource"` // (必填)加好友来源 + Remark string `json:"Remark,omitempty"` // (选填)加好友备注 + GroupName []string `json:"GroupName,omitempty"` // (选填)分组信息 + AddWording string `json:"AddWording,omitempty"` // (选填)好友关系时的附言信息 + AddTime int64 `json:"AddTime,omitempty"` // (选填)形成好友关系的时间 + RemarkTime int64 `json:"RemarkTime,omitempty"` // (选填)好友备注时间 + CustomData []*types.TagPair `json:"CustomItem,omitempty"` // (选填)自定义好友数据 + } + + // 更新好友(请求) + updateFriendsReq struct { + UserId string `json:"From_Account"` // (必填)需要更新该UserID的关系链数据 + Friends []*updateFriendItem `json:"UpdateItem"` // (必填)需要更新的好友对象数组 + } + + // 更新好友(响应) + updateFriendsResp struct { + types.ActionBaseResp + Results []*Result `json:"ResultItem"` // 结果对象数组 + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + } + + // 更新好友 + updateFriendItem struct { + UserId string `json:"To_Account"` // (必填)好友的UserID + Attrs []*types.TagPair `json:"SnsItem"` // (必填)需要更新的关系链数据对象数组 + } + + // 删除好友(请求) + deleteFriendsReq struct { + UserId string `json:"From_Account"` // (必填)需要删除该UserID的好友 + DeletedUserIds []string `json:"To_Account"` // (必填)待删除的好友的 UserID 列表,单次请求的 To_Account 数不得超过1000 + DeleteType DeleteType `json:"DeleteType"` // (选填)删除模式 + } + + // 删除好友(响应) + deleteFriendsResp struct { + types.ActionBaseResp + ErrorDisplay string `json:"ErrorDisplay"` // 详细的客户端展示信息 + Results []*Result `json:"ResultItem"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + } + + // 删除所有好友(请求) + deleteAllFriendsReq struct { + UserId string `json:"From_Account"` // (必填)需要删除该UserID的好友 + DeleteType DeleteType `json:"DeleteType"` // (选填)删除类型 + } + + // 校验好友(请求) + checkFriendsReq struct { + UserId string `json:"From_Account"` // (必填)需要校验该 UserID 的好友 + CheckedUserIds []string `json:"To_Account"` // (必填)请求校验的好友的 UserID 列表,单次请求的 To_Account 数不得超过1000 + CheckType CheckType `json:"CheckType"` // (必填)校验模式 + } + + // 校验好友(响应) + checkFriendsResp struct { + types.ActionBaseResp + Results []*CheckResult `json:"InfoItem"` // 结果对象数组 + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + } + + // CheckResult 校验结果 + CheckResult struct { + UserId string `json:"To_Account"` // 请求校验的用户的 UserID + Relation string `json:"Relation"` // 校验成功时 To_Account 与 From_Account 之间的好友关系 + ResultCode int `json:"ResultCode"` // 处理结果,0表示成功,非0表示失败 + ResultInfo string `json:"ResultInfo"` // 描述信息,成功时该字段为空 + } + + // 拉取指定好友(请求) + getFriendsReq struct { + UserId string `json:"From_Account"` // (必填)指定要拉取好友数据的用户的 UserID + FriendUserIds []string `json:"To_Account"` // (必填)好友的 UserID 列表,建议每次请求的好友数不超过100,避免因数据量太大导致回包失败 + TagList []string `json:"TagList"` // (必填)指定要拉取的资料字段及好友字段 + } + + // 拉取指定好友(响应) + getFriendsResp struct { + types.ActionBaseResp + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + Friends []friendData `json:"InfoItem"` // 好友对象数组 + } + + // 拉取好友(请求) + fetchFriendsReq struct { + UserId string `json:"From_Account"` // (必填)需要拉取该 UserID 的黑名单 + StartIndex int `json:"StartIndex"` // (必填)拉取的起始位置 + StandardSequence int `json:"StandardSequence"` // (选填)上次拉好友数据时返回的 StandardSequence,如果 StandardSequence 字段的值与后台一致,后台不会返回标配好友数据 + CustomSequence int `json:"CustomSequence"` // (选填)上次拉好友数据时返回的 CustomSequence,如果 CustomSequence 字段的值与后台一致,后台不会返回自定义好友数据 + } + + // 拉取好友(响应) + fetchFriendsResp struct { + types.ActionBaseResp + Friends []friendData `json:"UserDataItem"` // 好友对象数组 + StandardSequence int `json:"StandardSequence"` // 标配好友数据的 Sequence,客户端可以保存该 Sequence,下次请求时通过请求的 StandardSequence 字段返回给后台 + CustomSequence int `json:"CustomSequence"` // 自定义好友数据的 Sequence,客户端可以保存该 Sequence,下次请求时通过请求的 CustomSequence 字段返回给后台 + FriendNum int `json:"FriendNum"` // 好友总数 + CompleteFlag int `json:"CompleteFlag"` // 分页的结束标识,非0值表示已完成全量拉取 + NextStartIndex int `json:"NextStartIndex"` // 分页接口下一页的起始位置 + } + + // 好友信息 + friendData struct { + UserId string `json:"To_Account"` // 好友的 UserID + Values []types.TagPair `json:"ValueItem"` // 好友数据的数组 + Profiles []types.TagPair `json:"SnsProfileItem"` // 好友数据的数组 + ResultCode int `json:"ResultCode"` // 处理结果,0表示成功,非0表示失败 + ResultInfo string `json:"ResultInfo"` // 错误描述信息 + } + + // FetchFriendsRet 好友列表 + FetchFriendsRet struct { + StandardSequence int // 标准排序 + CustomSequence int // 自定义排序 + StartIndex int // 分页接口下一页的起始位置 + Total int // 好友总数 + HasMore bool // 是否还有更多数据 + List []*Friend // 好友列表 + } + + // 添加黑名单(请求) + addBlacklistReq struct { + UserId string `json:"From_Account"` // (必填)请求为该 UserID 添加黑名单 + BlackedUserIds []string `json:"To_Account"` // (必填)待添加为黑名单的用户 UserID 列表,单次请求的 To_Account 数不得超过1000 + } + + // 添加黑名单(响应) + addBlacklistResp struct { + types.ActionBaseResp + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + Results []*Result `json:"ResultItem"` // 批量添加黑名单的结果对象数组 + } + + // 删除黑名单(请求) + deleteBlacklistReq struct { + UserId string `json:"From_Account"` // (必填)需要删除该 UserID 的黑名单 + DeletedUserIds []string `json:"To_Account"` // (必填)待删除的黑名单的 UserID 列表,单次请求的 To_Account 数不得超过1000 + } + + // 删除黑名单(响应) + deleteBlacklistResp struct { + types.ActionBaseResp + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + Results []*Result `json:"ResultItem"` // 批量添加黑名单的结果对象数组 + } + + // 拉取黑名单(请求) + fetchBlacklistReq struct { + UserId string `json:"From_Account"` // (必填)需要拉取该 UserID 的黑名单 + StartIndex int `json:"StartIndex"` // (必填)拉取的起始位置 + MaxLimited int `json:"MaxLimited"` // (必填)每页最多拉取的黑名单数 + LastSequence int `json:"LastSequence"` // (必填)上一次拉黑名单时后台返回给客户端的 Seq,初次拉取时为0 + } + + // 拉取黑名单(响应) + fetchBlacklistResp struct { + types.ActionBaseResp + ErrorDisplay string `json:"ErrorDisplay"` // 详细的客户端展示信息 + StartIndex int `json:"StartIndex"` // 下页拉取的起始位置,0表示已拉完 + CurrentSequence int `json:"CurrentSequence"` // 黑名单最新的 Seq + Blacklists []*Blacklist `json:"BlackListItem"` // 黑名单对象数组 + } + + // FetchBlacklistRet 拉取黑名单结果 + FetchBlacklistRet struct { + StandardSequence int // 标准排序 + StartIndex int // 分页接口下一页的起始位置 + HasMore bool // 是否还有数据 + List []*Blacklist // 黑名单列表 + } + + // Blacklist 黑名单 + Blacklist struct { + UserId string `json:"To_Account"` // 黑名单的 UserID + Time int `json:"AddBlackTimeStamp"` // 添加黑名单的时间 + } + + // 校验黑名单(请求) + checkBlacklistReq struct { + UserId string `json:"From_Account"` // (必填)需要校验该 UserID 的黑名单 + CheckedUserIds []string `json:"To_Account"` // (必填)待校验的黑名单的 UserID 列表,单次请求的 To_Account 数不得超过1000 + CheckType BlacklistCheckType `json:"CheckType"` // (必填)校验模式 + } + + // 校验黑名单(响应) + checkBlacklistResp struct { + types.ActionBaseResp + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + Results []*CheckResult `json:"BlackListCheckItem"` // 校验结果对象数组 + } + + // 添加分组(请求) + addGroupsReq struct { + UserId string `json:"From_Account"` // (必填)需要为该 UserID 添加新分组 + GroupNames []string `json:"GroupName"` // (必填)新增分组列表 + JoinedUserIds []string `json:"To_Account"` // (必填)需要加入新增分组的好友的 UserID 列表 + } + + // 添加分组(响应) + addGroupsResp struct { + types.ActionBaseResp + ErrorDisplay string `json:"ErrorDisplay"` // 详细的客户端展示信息 + FailUserIds []string `json:"Fail_Account"` // 返回处理失败的用户列表,仅当存在失败用户时才返回该字段 + CurrentSequence int `json:"CurrentSequence"` // 返回最新的分组 Sequence + Results []*Result `json:"ResultItem"` // 好友加入新增分组的结果对象数组 + } + + // 删除分组(请求) + deleteGroupsReq struct { + UserId string `json:"From_Account"` // (必填)需要删除该 UserID 的分组 + GroupNames []string `json:"GroupName"` // (必填)要删除的分组列表 + } + + // 删除分组(响应) + deleteGroupsResp struct { + types.ActionBaseResp + CurrentSequence int `json:"CurrentSequence"` // 返回最新的分组 Sequence + } + + // 拉取分组(请求) + getGroupsReq struct { + UserId string `json:"From_Account"` // (必填)指定要拉取分组的用户的 UserID + LastSequence int `json:"LastSequence"` // (必填)上一次拉取分组时后台返回给客户端的 Seq,初次拉取时为0,只有 GroupName 为空时有效 + NeedFriend NeedFriendType `json:"NeedFriend"` // (选填)是否需要拉取分组下的 User 列表 + GroupNames []string `json:"GroupName"` // (选填)要拉取的分组名称 + } + + // 拉取分组(响应) + getGroupsResp struct { + types.ActionBaseResp + CurrentSequence int `json:"CurrentSequence"` // 返回最新的分组 Sequence + Results []*GroupResult `json:"ResultItem"` // 拉取分组的结果对象数组 + } + + // GroupResult 分组结果 + GroupResult struct { + GroupName string `json:"GroupName"` // 分组名 + FriendNumber int `json:"FriendNumber"` // 该分组下的好友数量 + UserIds []string `json:"To_Account"` // 该分组下的好友的 UserID + } +)