mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-07-01 07:18:15 +08:00
好友和非好友静音设置
This commit is contained in:
parent
7defda3c5c
commit
542d479829
518
docs/redpacket-api.md
Normal file
518
docs/redpacket-api.md
Normal file
@ -0,0 +1,518 @@
|
||||
# 红包 API 接口文档
|
||||
|
||||
**Base URL:** `/redpacket`
|
||||
**协议:** HTTP POST,`Content-Type: application/json`
|
||||
**认证:** 请求头携带 `token: <JWT令牌>`(标注需要登录的接口)
|
||||
|
||||
> **统一响应结构**
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "errCode": 0,
|
||||
> "errMsg": "ok",
|
||||
> "errDlt": "",
|
||||
> "data": { }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> `errCode` 为 `0` 表示成功,非 0 表示错误。
|
||||
|
||||
---
|
||||
|
||||
## 1. 创建红包订单
|
||||
|
||||
**POST** `/redpacket/create_order`
|
||||
需要登录。创建一条待上链的红包订单,返回业务 ID(bizID)供后续链上交易关联。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"chainType": "EVM",
|
||||
"chainID": 1,
|
||||
"contractAddress": "0xAbCd...",
|
||||
"creatorWallet": "0x1234...",
|
||||
"groupID": "group_001",
|
||||
"scopeType": "GROUP",
|
||||
"receiverUserID": "",
|
||||
"receiverUserIDs": [],
|
||||
"packetType": 0,
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"totalAmount": "1000000000000000000",
|
||||
"totalShares": 10,
|
||||
"expiryAt": 1800000000,
|
||||
"remark": "新年快乐"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `chainType` | string | **必填** | 链类型,仅支持 `"EVM"` 或 `"TRON"` |
|
||||
| `chainID` | int64 | 可选 | 链 ID;为 0 时从链客户端自动获取 |
|
||||
| `contractAddress` | string | 可选 | 合约地址;为空时从链客户端自动获取 |
|
||||
| `creatorWallet` | string | **必填** | 创建者钱包地址 |
|
||||
| `scopeType` | string | 可选 | 范围类型:`GROUP`(群组)/`DIRECT`(定向)/`PUBLIC`(公开),默认 `PUBLIC` |
|
||||
| `groupID` | string | **GROUP 时必填** | 群组 ID(`scopeType=GROUP` 时必须提供) |
|
||||
| `receiverUserID` | string | **transfer 时必填** | 接收者用户 ID(`packetType=2` 且 `scopeType=DIRECT` 时必须提供) |
|
||||
| `receiverUserIDs` | []string | **DIRECT + fixed/random 时必填** | 多接收者用户 ID 列表(`scopeType=DIRECT` 且 `packetType=0/1` 时使用) |
|
||||
| `packetType` | int32 | **必填** | 红包类型:`0`=均分红包,`1`=随机红包,`2`=转账红包 |
|
||||
| `token` | string | 可选 | ERC20 代币合约地址;为空表示原生代币 |
|
||||
| `totalAmount` | string | **必填** | 总金额(最小单位整数字符串,如 wei),必须为正整数 |
|
||||
| `totalShares` | int32 | **必填(packetType 0/1)** | 红包份数;均分/随机红包 >0 且 ≤10000;转账红包固定为 1 |
|
||||
| `expiryAt` | int64 | 可选 | 过期时间(Unix 时间戳秒);0 表示不过期;必须为将来时间 |
|
||||
| `remark` | string | 可选 | 备注 |
|
||||
|
||||
**packetType 规则说明:**
|
||||
|
||||
- `0`(均分):`scopeType` 必须为 `GROUP`,`totalAmount` 必须被 `totalShares` 整除
|
||||
- `1`(随机):`scopeType` 必须为 `GROUP`,`totalAmount` ≥ `totalShares`
|
||||
- `2`(转账):`scopeType` 必须为 `DIRECT`,`totalShares` 必须为 1,`receiverUserID` 必须提供,不能转给自己
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"bizID": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `bizID` | string | 业务订单 ID,用于链上交易完成后提交回调 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 红包创建回调(链上确认)
|
||||
|
||||
**POST** `/redpacket/created_callback`
|
||||
需要登录。链上交易广播后,由创建者提交交易哈希以激活红包。仅创建者可调用。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"bizID": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"txHash": "0xabc123...",
|
||||
"packetID": "",
|
||||
"groupID": "",
|
||||
"scopeType": "",
|
||||
"receiverUserID": "",
|
||||
"receiverUserIDs": []
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `bizID` | string | **必填** | 创建订单时返回的业务 ID |
|
||||
| `txHash` | string | **必填** | 链上交易哈希 |
|
||||
| `packetID` | string | 条件必填 | 链上红包 ID;链客户端离线时必须手动提供 |
|
||||
| `groupID` | string | 可选 | 覆盖订单的群组 ID(不填则继承订单值) |
|
||||
| `scopeType` | string | 可选 | 覆盖订单的范围类型 |
|
||||
| `receiverUserID` | string | 可选 | 覆盖单一接收者 |
|
||||
| `receiverUserIDs` | []string | 可选 | 覆盖多接收者列表 |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 查询红包详情
|
||||
|
||||
**POST** `/redpacket/detail`
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packetID": "12345"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `packetID` | string | **必填** | 链上红包 ID |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"record": {
|
||||
"bizID": "550e8400-...",
|
||||
"chainType": "EVM",
|
||||
"packetID": "12345",
|
||||
"chainID": 1,
|
||||
"contractAddress": "0xAbCd...",
|
||||
"creatorUserID": "user_001",
|
||||
"creatorWallet": "0x1234...",
|
||||
"groupID": "group_001",
|
||||
"scopeType": "GROUP",
|
||||
"receiverUserID": "",
|
||||
"receiverUserIDs": [],
|
||||
"packetType": 0,
|
||||
"token": "0x0000000000000000000000000000000000000000",
|
||||
"totalAmount": "1000000000000000000",
|
||||
"totalShares": 10,
|
||||
"claimedAmount": "300000000000000000",
|
||||
"claimedShares": 3,
|
||||
"expiryAt": 1800000000,
|
||||
"txHash": "0xabc123...",
|
||||
"status": "ACTIVE",
|
||||
"createdAt": 1715500000,
|
||||
"updatedAt": 1715500100
|
||||
},
|
||||
"claims": [
|
||||
{
|
||||
"packetID": "12345",
|
||||
"userID": "user_002",
|
||||
"claimerWallet": "0x5678...",
|
||||
"authNonce": "1715500050000000000",
|
||||
"claimTxHash": "0xdef456...",
|
||||
"claimedAmount": "100000000000000000",
|
||||
"blockNumber": 19000000,
|
||||
"status": "CONFIRMED",
|
||||
"createdAt": 1715500050,
|
||||
"updatedAt": 1715500060
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**record 字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `status` | string | 红包状态:`PENDING` / `ACTIVE` / `COMPLETED` / `REFUNDED` / `EXPIRED` |
|
||||
| `totalAmount` | string | 总金额(最小单位) |
|
||||
| `claimedAmount` | string | 已领金额 |
|
||||
| `totalShares` / `claimedShares` | int32 | 总份数 / 已领份数 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 申请领取签名
|
||||
|
||||
**POST** `/redpacket/issue_claim_sign`
|
||||
需要登录。领取红包前,先获取服务端签名用于链上验证。会校验领取资格(是否群成员/好友、是否已领取、红包状态等)。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packetID": "12345",
|
||||
"claimer": "0x5678...",
|
||||
"randomSeed": ""
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `packetID` | string | **必填** | 链上红包 ID |
|
||||
| `claimer` | string | **必填** | 领取者钱包地址 |
|
||||
| `randomSeed` | string | 可选 | 随机种子(十进制整数字符串);为空或 `"0"` 时服务端自动生成 |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"authNonce": "1715500050000000000",
|
||||
"deadline": 1715500350,
|
||||
"signature": "0xaabbcc...",
|
||||
"randomSeed": "8765309000000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `authNonce` | string | 认证随机数(传给合约) |
|
||||
| `deadline` | int64 | 签名过期时间(Unix 时间戳,约 5 分钟后) |
|
||||
| `signature` | string | 服务端签名(0x 前缀十六进制,65 字节) |
|
||||
| `randomSeed` | string | 使用的随机种子 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 提交领取结果
|
||||
|
||||
**POST** `/redpacket/claim_result`
|
||||
需要登录。链上领取交易广播后提交,服务端解析链上事件并更新领取记录。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packetID": "12345",
|
||||
"claimer": "0x5678...",
|
||||
"txHash": "0xdef456..."
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `packetID` | string | **必填** | 链上红包 ID |
|
||||
| `claimer` | string | **必填** | 领取者钱包地址 |
|
||||
| `txHash` | string | **必填** | 领取交易哈希 |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 申请退款
|
||||
|
||||
**POST** `/redpacket/request_refund`
|
||||
需要登录。仅创建者可调用,红包到期后提交链上退款交易。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packetID": "12345"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `packetID` | string | **必填** | 链上红包 ID(红包必须已到期) |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"txHash": "0xghi789...",
|
||||
"status": "PENDING"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `txHash` | string | 退款交易哈希 |
|
||||
| `status` | string | `PENDING`(退款交易已提交)或 `REFUNDED`(已退款) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 查询退款记录
|
||||
|
||||
**POST** `/redpacket/get_refund`
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"packetID": "12345"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `packetID` | string | **必填** | 链上红包 ID |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"packetID": "12345",
|
||||
"refundTo": "0x1234...",
|
||||
"txHash": "0xghi789...",
|
||||
"amount": "700000000000000000",
|
||||
"createdAt": 1715600000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `refundTo` | string | 退款目标钱包地址 |
|
||||
| `amount` | string | 退款金额(最小单位) |
|
||||
| `createdAt` | int64 | 退款记录创建时间(Unix 时间戳) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 发起钱包绑定挑战
|
||||
|
||||
**POST** `/redpacket/wallet_bind/challenge`
|
||||
需要登录。生成一条待用户签名的消息,用于将钱包地址绑定到当前用户账户(EVM 使用 EIP-4361 SIWE,TRON 使用 signMessageV2)。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"chainType": "EVM",
|
||||
"chainID": 1,
|
||||
"walletAddress": "0x5678...",
|
||||
"domain": "myapp.example.com",
|
||||
"uri": "https://myapp.example.com/wallet-bind"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `chainType` | string | **必填** | 链类型,`"EVM"` 或 `"TRON"` |
|
||||
| `walletAddress` | string | **必填** | 待绑定的钱包地址 |
|
||||
| `chainID` | int64 | 可选 | 链 ID(EVM 时建议提供) |
|
||||
| `domain` | string | 可选 | 应用域名,默认 `"redpacket"` |
|
||||
| `uri` | string | 可选 | 应用 URI,默认 `"https://redpacket.local/wallet-bind"` |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"challengeID": "aaaa-bbbb-cccc-dddd",
|
||||
"userID": "user_001",
|
||||
"chainType": "EVM",
|
||||
"chainID": 1,
|
||||
"wallet": "0x5678...",
|
||||
"protocol": "siwe-eip4361",
|
||||
"signMethod": "personal_sign",
|
||||
"nonce": "xxxx-yyyy-zzzz",
|
||||
"message": "myapp.example.com wants you to sign in with your Ethereum account:\n0x5678...\n\nBind wallet ...",
|
||||
"issuedAt": "2026-05-12T08:39:00Z",
|
||||
"expiresAt": "2026-05-12T08:49:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `challengeID` | string | 挑战 ID,确认绑定时使用 |
|
||||
| `message` | string | 待签名的完整消息文本 |
|
||||
| `protocol` | string | EVM: `siwe-eip4361`;TRON: `tron-signmessagev2` |
|
||||
| `signMethod` | string | EVM: `personal_sign`;TRON: `signMessageV2` |
|
||||
| `expiresAt` | string | 挑战过期时间(RFC3339,10 分钟有效期) |
|
||||
|
||||
---
|
||||
|
||||
## 9. 确认钱包绑定
|
||||
|
||||
**POST** `/redpacket/wallet_bind/confirm`
|
||||
提交用户对挑战消息的签名,服务端验证后完成钱包绑定。挑战有效期 10 分钟。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"challengeID": "aaaa-bbbb-cccc-dddd",
|
||||
"signature": "0xaabbccdd..."
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `challengeID` | string | **必填** | 挑战 ID(来自 `/wallet_bind/challenge`) |
|
||||
| `signature` | string | **必填** | 钱包签名(0x 前缀十六进制,65 字节) |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"userID": "user_001",
|
||||
"chainType": "EVM",
|
||||
"chainID": 1,
|
||||
"walletAddress": "0x5678...",
|
||||
"status": "ACTIVE",
|
||||
"verifiedAt": "2026-05-12T08:42:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `status` | string | 绑定状态,成功时为 `ACTIVE` |
|
||||
| `verifiedAt` | string | 绑定验证时间(RFC3339) |
|
||||
|
||||
---
|
||||
|
||||
## 10. 查询钱包绑定信息
|
||||
|
||||
**POST** `/redpacket/wallet_bind/detail`
|
||||
需要登录。查询当前登录用户在指定链上的活跃钱包绑定。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"chainType": "EVM",
|
||||
"walletAddress": "0x5678..."
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `chainType` | string | **必填** | 链类型,`"EVM"` 或 `"TRON"` |
|
||||
| `walletAddress` | string | 可选 | 钱包地址过滤条件 |
|
||||
|
||||
### 响应体
|
||||
|
||||
```json
|
||||
{
|
||||
"errCode": 0,
|
||||
"errMsg": "ok",
|
||||
"data": {
|
||||
"userID": "user_001",
|
||||
"chainType": "EVM",
|
||||
"chainID": 1,
|
||||
"walletAddress": "0x5678...",
|
||||
"status": "ACTIVE",
|
||||
"challengeID": "aaaa-bbbb-cccc-dddd",
|
||||
"verifiedAt": "2026-05-12T08:42:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:公共枚举值
|
||||
|
||||
| 枚举 | 可选值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `chainType` | `EVM` / `TRON` | 区块链类型 |
|
||||
| `scopeType` | `GROUP` / `DIRECT` / `PUBLIC` | 红包范围 |
|
||||
| `packetType` | `0` / `1` / `2` | 均分 / 随机 / 转账 |
|
||||
| 红包 `status` | `PENDING` / `ACTIVE` / `COMPLETED` / `REFUNDED` / `EXPIRED` | 红包状态 |
|
||||
| 领取 `status` | `PENDING` / `CONFIRMED` / `FAILED` | 领取记录状态 |
|
||||
| 钱包绑定 `status` | `ACTIVE` | 绑定状态(查询时只返回活跃绑定) |
|
||||
|
||||
---
|
||||
|
||||
## 实现参考
|
||||
|
||||
- 路由:`internal/api/router.go`(`/redpacket` 分组)
|
||||
- HTTP 处理:`internal/api/redpacket.go`
|
||||
- Proto 定义:`protocol/redpacket/redpacket.proto`
|
||||
- RPC 实现:`internal/rpc/redpacket/service.go`、`internal/rpc/redpacket/wallet.go`
|
||||
@ -126,3 +126,11 @@ func (o *FriendApi) GetPinnedFriendIDs(c *gin.Context) {
|
||||
func (o *FriendApi) AddOnewayFriend(c *gin.Context) {
|
||||
a2r.Call(c, relation.FriendClient.AddOnewayFriend, o.Client)
|
||||
}
|
||||
|
||||
func (o *FriendApi) SetMute(c *gin.Context) {
|
||||
a2r.Call(c, relation.FriendClient.SetMute, o.Client)
|
||||
}
|
||||
|
||||
func (o *FriendApi) GetMute(c *gin.Context) {
|
||||
a2r.Call(c, relation.FriendClient.GetMute, o.Client)
|
||||
}
|
||||
|
||||
@ -221,6 +221,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
||||
friendRouterGroup.POST("/get_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount)
|
||||
friendRouterGroup.POST("/get_pinned_friend_ids", f.GetPinnedFriendIDs)
|
||||
friendRouterGroup.POST("/add_oneway_friend", f.AddOnewayFriend)
|
||||
friendRouterGroup.POST("/set_mute", f.SetMute)
|
||||
friendRouterGroup.POST("/get_mute", f.GetMute)
|
||||
}
|
||||
|
||||
g := NewGroupApi(group.NewGroupClient(groupConn))
|
||||
|
||||
@ -72,6 +72,7 @@ type msgServer struct {
|
||||
conversationClient *rpcli.ConversationClient
|
||||
spamReportDB database.SpamReport
|
||||
globalBlackDB controller.UserGlobalBlackDatabase
|
||||
userMuteDB controller.UserMuteDatabase
|
||||
msgBurnDeadlineDB database.MsgBurnDeadline
|
||||
}
|
||||
|
||||
@ -137,6 +138,10 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userMuteMgo, err := mgo.NewUserMuteMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s := &msgServer{
|
||||
MsgDatabase: msgDatabase,
|
||||
RegisterCenter: client,
|
||||
@ -149,6 +154,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
||||
conversationClient: conversationClient,
|
||||
spamReportDB: spamReportDB,
|
||||
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo),
|
||||
userMuteDB: controller.NewUserMuteDatabase(userMuteMgo),
|
||||
msgBurnDeadlineDB: msgBurnDeadlineDB,
|
||||
}
|
||||
|
||||
|
||||
@ -304,25 +304,38 @@ func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, us
|
||||
|
||||
// 第三优先级:会话级接收偏好
|
||||
singleOpt, err := m.ConversationLocalCache.GetSingleConversationRecvMsgOpt(ctx, userID, conversationID)
|
||||
if errs.ErrRecordNotFound.Is(err) {
|
||||
return true, nil
|
||||
} else if err != nil {
|
||||
if err != nil && !errs.ErrRecordNotFound.Is(err) {
|
||||
return false, err
|
||||
}
|
||||
switch singleOpt {
|
||||
case constant.ReceiveMessage:
|
||||
return true, nil
|
||||
case constant.NotReceiveMessage:
|
||||
if datautil.Contain(int(pb.MsgData.ContentType), ExcludeContentType...) {
|
||||
if err == nil {
|
||||
switch singleOpt {
|
||||
case constant.NotReceiveMessage:
|
||||
if datautil.Contain(int(pb.MsgData.ContentType), ExcludeContentType...) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
case constant.ReceiveNotNotifyMessage:
|
||||
if pb.MsgData.Options == nil {
|
||||
pb.MsgData.Options = make(map[string]bool, 10)
|
||||
}
|
||||
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
case constant.ReceiveNotNotifyMessage:
|
||||
if pb.MsgData.Options == nil {
|
||||
pb.MsgData.Options = make(map[string]bool, 10)
|
||||
}
|
||||
|
||||
// 第四优先级:用户静音设置(user_mute 集合,支持好友与非好友)
|
||||
// 无论会话记录是否存在均检查,以支持对非好友的静音
|
||||
if m.userMuteDB != nil {
|
||||
muted, err := m.userMuteDB.IsMuted(ctx, userID, pb.MsgData.SendID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if muted {
|
||||
if pb.MsgData.Options == nil {
|
||||
pb.MsgData.Options = make(map[string]bool, 10)
|
||||
}
|
||||
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
|
||||
}
|
||||
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
|
||||
return true, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ package relation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/notification/common_user"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/rpcli"
|
||||
@ -56,6 +57,7 @@ type friendServer struct {
|
||||
db controller.FriendDatabase
|
||||
blackDatabase controller.BlackDatabase
|
||||
globalBlackDB controller.UserGlobalBlackDatabase
|
||||
userMuteDB controller.UserMuteDatabase
|
||||
notificationSender *FriendNotificationSender
|
||||
RegisterCenter discovery.SvcDiscoveryRegistry
|
||||
config *Config
|
||||
@ -107,6 +109,11 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
||||
return err
|
||||
}
|
||||
|
||||
userMuteMongoDB, err := mgo.NewUserMuteMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -144,6 +151,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
||||
redis.NewBlackCacheRedis(rdb, &config.LocalCacheConfig, blackMongoDB, redis.GetRocksCacheOptions()),
|
||||
),
|
||||
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMongoDB),
|
||||
userMuteDB: controller.NewUserMuteDatabase(userMuteMongoDB),
|
||||
notificationSender: notificationSender,
|
||||
RegisterCenter: client,
|
||||
config: config,
|
||||
@ -718,6 +726,43 @@ func (s *friendServer) AddOnewayFriend(ctx context.Context, req *relation.ApplyT
|
||||
return &relation.ApplyToAddFriendResp{}, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) SetMute(ctx context.Context, req *relation.SetMuteReq) (*relation.SetMuteResp, error) {
|
||||
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Duration == 0 {
|
||||
return &relation.SetMuteResp{}, s.userMuteDB.Delete(ctx, req.OwnerUserID, req.TargetUserID)
|
||||
}
|
||||
var muteEndTime int64
|
||||
if req.Duration != -1 {
|
||||
muteEndTime = time.Now().Unix() + req.Duration
|
||||
}
|
||||
return &relation.SetMuteResp{}, s.userMuteDB.Upsert(ctx, &model.UserMute{
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
MutedUserID: req.TargetUserID,
|
||||
MuteEndTime: muteEndTime,
|
||||
CreateTime: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *friendServer) GetMute(ctx context.Context, req *relation.GetMuteReq) (*relation.GetMuteResp, error) {
|
||||
if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rec, err := s.userMuteDB.Get(ctx, req.OwnerUserID, req.TargetUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rec == nil {
|
||||
return &relation.GetMuteResp{Muted: false, MuteEndTime: 0}, nil
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
if rec.MuteEndTime != 0 && rec.MuteEndTime <= now {
|
||||
return &relation.GetMuteResp{Muted: false, MuteEndTime: 0}, nil
|
||||
}
|
||||
return &relation.GetMuteResp{Muted: true, MuteEndTime: rec.MuteEndTime}, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) getCommonUserMap(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error) {
|
||||
users, err := s.userClient.GetUsersInfo(ctx, userIDs)
|
||||
if err != nil {
|
||||
|
||||
44
pkg/common/storage/controller/user_mute.go
Normal file
44
pkg/common/storage/controller/user_mute.go
Normal file
@ -0,0 +1,44 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
)
|
||||
|
||||
// UserMuteDatabase 用户静音业务接口
|
||||
type UserMuteDatabase interface {
|
||||
// Upsert 新增或更新静音记录
|
||||
Upsert(ctx context.Context, mute *model.UserMute) error
|
||||
// Delete 取消静音
|
||||
Delete(ctx context.Context, ownerUserID, mutedUserID string) error
|
||||
// IsMuted 检查 ownerUserID 是否对 mutedUserID 设置了有效静音
|
||||
IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error)
|
||||
// Get 查询静音记录;不存在则 (nil, nil)
|
||||
Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error)
|
||||
}
|
||||
|
||||
type userMuteDatabase struct {
|
||||
db database.UserMute
|
||||
}
|
||||
|
||||
func NewUserMuteDatabase(db database.UserMute) UserMuteDatabase {
|
||||
return &userMuteDatabase{db: db}
|
||||
}
|
||||
|
||||
func (u *userMuteDatabase) Upsert(ctx context.Context, mute *model.UserMute) error {
|
||||
return u.db.Upsert(ctx, mute)
|
||||
}
|
||||
|
||||
func (u *userMuteDatabase) Delete(ctx context.Context, ownerUserID, mutedUserID string) error {
|
||||
return u.db.Delete(ctx, ownerUserID, mutedUserID)
|
||||
}
|
||||
|
||||
func (u *userMuteDatabase) IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error) {
|
||||
return u.db.IsMuted(ctx, ownerUserID, mutedUserID)
|
||||
}
|
||||
|
||||
func (u *userMuteDatabase) Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error) {
|
||||
return u.db.Get(ctx, ownerUserID, mutedUserID)
|
||||
}
|
||||
95
pkg/common/storage/database/mgo/user_mute.go
Normal file
95
pkg/common/storage/database/mgo/user_mute.go
Normal file
@ -0,0 +1,95 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func NewUserMuteMongo(db *mongo.Database) (database.UserMute, error) {
|
||||
coll := db.Collection(database.UserMuteName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "owner_user_id", Value: 1},
|
||||
{Key: "muted_user_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &UserMuteMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type UserMuteMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (u *UserMuteMgo) Upsert(ctx context.Context, mute *model.UserMute) error {
|
||||
if mute.CreateTime.IsZero() {
|
||||
mute.CreateTime = time.Now()
|
||||
}
|
||||
filter := bson.M{
|
||||
"owner_user_id": mute.OwnerUserID,
|
||||
"muted_user_id": mute.MutedUserID,
|
||||
}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"mute_end_time": mute.MuteEndTime,
|
||||
},
|
||||
"$setOnInsert": bson.M{
|
||||
"owner_user_id": mute.OwnerUserID,
|
||||
"muted_user_id": mute.MutedUserID,
|
||||
"create_time": mute.CreateTime,
|
||||
},
|
||||
}
|
||||
_, err := u.coll.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
func (u *UserMuteMgo) Delete(ctx context.Context, ownerUserID, mutedUserID string) error {
|
||||
_, err := u.coll.DeleteOne(ctx, bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"muted_user_id": mutedUserID,
|
||||
})
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
func (u *UserMuteMgo) IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error) {
|
||||
now := time.Now().Unix()
|
||||
// mute_end_time == 0 means permanent; mute_end_time > now means still active
|
||||
filter := bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"muted_user_id": mutedUserID,
|
||||
"$or": bson.A{
|
||||
bson.M{"mute_end_time": 0},
|
||||
bson.M{"mute_end_time": bson.M{"$gt": now}},
|
||||
},
|
||||
}
|
||||
count, err := u.coll.CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
return false, errs.Wrap(err)
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (u *UserMuteMgo) Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error) {
|
||||
var out model.UserMute
|
||||
err := u.coll.FindOne(ctx, bson.M{
|
||||
"owner_user_id": ownerUserID,
|
||||
"muted_user_id": mutedUserID,
|
||||
}).Decode(&out)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
@ -25,4 +25,5 @@ const (
|
||||
SpamReportName = "spam_report"
|
||||
MsgBurnDeadlineName = "msg_burn_deadline"
|
||||
UserOfflineRecordName = "user_offline_record"
|
||||
UserMuteName = "user_mute"
|
||||
)
|
||||
|
||||
19
pkg/common/storage/database/user_mute.go
Normal file
19
pkg/common/storage/database/user_mute.go
Normal file
@ -0,0 +1,19 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
)
|
||||
|
||||
// UserMute 用户静音持久化接口(支持好友与非好友)
|
||||
type UserMute interface {
|
||||
// Upsert 新增或更新静音记录
|
||||
Upsert(ctx context.Context, mute *model.UserMute) error
|
||||
// Delete 取消静音(删除记录)
|
||||
Delete(ctx context.Context, ownerUserID, mutedUserID string) error
|
||||
// IsMuted 检查 ownerUserID 是否对 mutedUserID 设置了有效的静音
|
||||
IsMuted(ctx context.Context, ownerUserID, mutedUserID string) (bool, error)
|
||||
// Get 按 owner + muted 查询一条记录;不存在则 (nil, nil)
|
||||
Get(ctx context.Context, ownerUserID, mutedUserID string) (*model.UserMute, error)
|
||||
}
|
||||
12
pkg/common/storage/model/user_mute.go
Normal file
12
pkg/common/storage/model/user_mute.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// UserMute records a mute relationship: OwnerUserID has muted MutedUserID.
|
||||
// Works for both friends and strangers. MuteEndTime == 0 means permanent mute.
|
||||
type UserMute struct {
|
||||
OwnerUserID string `bson:"owner_user_id"` // who set the mute
|
||||
MutedUserID string `bson:"muted_user_id"` // who is muted
|
||||
MuteEndTime int64 `bson:"mute_end_time"` // Unix seconds; 0 = permanent
|
||||
CreateTime time.Time `bson:"create_time"`
|
||||
}
|
||||
2
protocol
2
protocol
@ -1 +1 @@
|
||||
Subproject commit 24c72259373e9080ab005bb10385524753d4dfc3
|
||||
Subproject commit ab69a8df51841e7e70e637033c3f05b7e40bb747
|
||||
Loading…
x
Reference in New Issue
Block a user