Merge pull request #21 from sok-im/feature/conversation_mute

会话静音
pull/3727/head
haoyunlt 2 weeks ago committed by GitHub
commit 62ea94f2d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,14 +1,14 @@
# Cursor 代码索引忽略(语法与 .gitignore 相同)
# 与根目录 .gitignore 对齐;未列出的规则仍以 .gitignore 为准Git 不索引的路径 Cursor 通常也不关心)
# 与根目录 .gitignore 对齐;以下为补充规则,减少生成物/文档噪音,保留业务源码与 .proto
### OpenIM与 .gitignore 一致)###
logs
.devcontainer
components
out-test
logs/
.devcontainer/
components/
out-test/
Dockerfile.cross
### macOS / 本地工具(不入索引)###
### macOS / 本地工具 ###
.DS_Store
.playwright-mcp/
@ -17,26 +17,51 @@ tmp/
bin/
output/
_output/
build/
dist/
deployments/charts/generated-configs/
### 配置与密钥(勿入索引)###
.env
config/config.yaml
config/notification.yaml
start-config.yml
### 部署生成物 ###
deployments/openim-server/charts
deployments/openim-server/charts/
### 本地笔记 ###
.idea.md
.todo.md
.note.md
### 生成代码(以 .proto 为准,勿重复索引)###
protocol/**/*.pb.go
protocol/**/*_grpc.pb.go
### 文档与资源(保留 docs/contrib、根 README忽略多语言 readme 与静态资源)###
docs/readme/
docs/.generated_docs
docs/contributing/
assets/
virgil_chat_server_design.md
docs/virgil-e2ee-*.md
### 测试与脚本输出 ###
test/e2e/output/
scripts/**/*.log
### 通用备份与临时文件 ###
*.bak
*.gho
*.ori
*.orig
*.tmp
*~
dist/
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
### VS Code除团队共享配置外###
.vscode/*
@ -44,6 +69,7 @@ dist/
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
### Go ###
*.exe
@ -54,13 +80,19 @@ dist/
*.test
*.out
vendor/
go.work
go.work.sum
go.sum
### JetBrains / IDE ###
.idea/
out/
### Tags ###
### Git / CI低价值索引###
.git/
.github/
### Tags / 索引工具 ###
TAGS
tags
gtags.files
@ -70,3 +102,5 @@ GPATH
GSYMS
cscope.files
cscope.out
cscope.in.out
cscope.po.out

@ -71,3 +71,7 @@ func (o *ConversationApi) GetNotNotifyConversationIDs(c *gin.Context) {
func (o *ConversationApi) GetPinnedConversationIDs(c *gin.Context) {
a2r.Call(c, conversation.ConversationClient.GetPinnedConversationIDs, o.Client)
}
func (o *ConversationApi) SetMute(c *gin.Context) {
a2r.Call(c, conversation.ConversationClient.SetConversationMute, o.Client)
}

@ -131,13 +131,6 @@ 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)
}
func (o *FriendApi) PinFriend(c *gin.Context) {
a2r.Call(c, relation.FriendClient.PinFriend, o.Client)

@ -222,8 +222,6 @@ 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)
friendRouterGroup.POST("/pin", f.PinFriend)
friendRouterGroup.POST("/unpin", f.UnpinFriend)
}
@ -358,6 +356,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
conversationGroup.POST("/get_owner_conversation", c.GetOwnerConversation)
conversationGroup.POST("/get_not_notify_conversation_ids", c.GetNotNotifyConversationIDs)
conversationGroup.POST("/get_pinned_conversation_ids", c.GetPinnedConversationIDs)
conversationGroup.POST("/set_mute", c.SetMute)
}
{

@ -49,7 +49,6 @@ type conversationServer struct {
pbconversation.UnimplementedConversationServer
conversationDatabase controller.ConversationDatabase
msgBurnDeadlineDB database.MsgBurnDeadline
userMuteDB controller.UserMuteDatabase
conversationNotificationSender *ConversationNotificationSender
config *Config
@ -86,10 +85,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
if err != nil {
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
@ -109,7 +104,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
conversationDatabase: controller.NewConversationDatabase(conversationDB,
redis.NewConversationRedis(rdb, &config.LocalCacheConfig, redis.GetRocksCacheOptions(), conversationDB), mgocli.GetTx()),
msgBurnDeadlineDB: msgBurnDeadlineDB,
userMuteDB: controller.NewUserMuteDatabase(userMuteMongoDB),
userClient: rpcli.NewUserClient(userConn),
groupClient: rpcli.NewGroupClient(groupConn),
msgClient: msgClient,
@ -202,14 +196,14 @@ func (c *conversationServer) GetSortedConversationList(ctx context.Context, req
}
conversation_notPinTime[time] = conversationID
}
if c.userMuteDB != nil {
for _, v := range conversations {
elem, ok := conversationMsg[v.ConversationID]
if !ok {
continue
}
c.fillConversationElemUserMute(ctx, c.userMuteDB, req.UserID, elem, v.ConversationType, v.UserID)
for _, v := range conversations {
elem, ok := conversationMsg[v.ConversationID]
if !ok {
continue
}
elem.MuteDuration = v.MuteDuration
elem.MuteEndTime = v.MuteEndTime
elem.IsMuted = computeIsMuted(v.MuteDuration, v.MuteEndTime)
}
resp = &pbconversation.GetSortedConversationListResp{
ConversationTotal: int64(len(chatLogs)),
@ -916,3 +910,34 @@ func (c *conversationServer) ClearBurnExpiredMsgs(ctx context.Context, req *pbco
}
return &pbconversation.ClearBurnExpiredMsgsResp{Count: processed}, nil
}
func (c *conversationServer) SetConversationMute(ctx context.Context, req *pbconversation.SetConversationMuteReq) (*pbconversation.SetConversationMuteResp, error) {
var (
muteDuration int32
muteEndTime int64
)
switch {
case req.Duration == 0:
// 取消静音:清零所有静音字段
case req.Duration == -1:
// 永久静音
muteDuration = -1
default:
// 定时静音
muteDuration = req.Duration
muteEndTime = time.Now().Unix() + int64(req.Duration)
}
if err := c.conversationDatabase.UpdateUsersConversationField(
ctx,
[]string{req.OwnerUserID},
req.ConversationID,
map[string]any{
"mute_duration": muteDuration,
"mute_end_time": muteEndTime,
},
); err != nil {
return nil, err
}
c.conversationNotificationSender.ConversationChangeNotification(ctx, req.OwnerUserID, []string{req.ConversationID})
return &pbconversation.SetConversationMuteResp{}, nil
}

@ -4,95 +4,37 @@ package conversation
import (
"context"
"math"
"time"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
"github.com/openimsdk/protocol/constant"
pbconversation "github.com/openimsdk/protocol/conversation"
"github.com/openimsdk/tools/log"
)
// int64MuteDurationToProto 将 user_mute 的秒数配置写入 Conversation.muteDurationint32正数过大时截断。
func int64MuteDurationToProto(d int64) int32 {
if d > int64(math.MaxInt32) {
return math.MaxInt32
// computeIsMuted 根据会话模型中存储的 mute_duration 和 mute_end_time 计算当前是否处于静音状态:
// - duration == 0 且 end == 0未静音
// - duration == -1 且 end == 0永久静音
// - end > 0 且 end > now定时静音仍有效
// - end > 0 且 end <= now定时静音已过期视为未静音
func computeIsMuted(muteDuration int32, muteEndTime int64) bool {
if muteDuration == 0 && muteEndTime == 0 {
return false
}
if d < int64(math.MinInt32) {
return math.MinInt32
if muteDuration == -1 && muteEndTime == 0 {
return true
}
return int32(d)
return muteEndTime > time.Now().Unix()
}
// conversationMuteFromRecord 与 relation.GetMute 判定一致:未记录/已过期则未静音;永久为 duration=-1 且 end=0。
func conversationMuteFromRecord(rec *model.UserMute, nowUnix int64) (isMuted bool, muteDuration int32, muteEndTime int64) {
if rec == nil {
return false, 0, 0
}
if rec.MuteEndTime != 0 && rec.MuteEndTime <= nowUnix {
return false, 0, 0
}
d := rec.MuteDuration
if d == 0 && rec.MuteEndTime == 0 {
d = -1
}
md := int64MuteDurationToProto(d)
me := rec.MuteEndTime
isMuted = (md == -1) || (me > nowUnix)
return isMuted, md, me
}
func (c *conversationServer) fillConversationUserMute(ctx context.Context, conv *pbconversation.Conversation) {
if c == nil || c.userMuteDB == nil || conv == nil {
// fillConversationUserMute 根据会话模型字段(已由 ConversationDB2Pb 通过 CopyStructFields 填入
// conv.MuteDuration / conv.MuteEndTime计算并设置 conv.IsMuted无需额外数据库查询。
func (c *conversationServer) fillConversationUserMute(_ context.Context, conv *pbconversation.Conversation) {
if conv == nil {
return
}
if conv.ConversationType != constant.SingleChatType || conv.UserID == "" {
return
}
rec, err := c.userMuteDB.Get(ctx, conv.OwnerUserID, conv.UserID)
if err != nil {
log.ZWarn(ctx, "fillConversationUserMute Get", err, "owner", conv.OwnerUserID, "peer", conv.UserID)
return
}
now := time.Now().Unix()
isMuted, dur, end := conversationMuteFromRecord(rec, now)
conv.IsMuted = isMuted
conv.MuteDuration = dur
conv.MuteEndTime = end
conv.IsMuted = computeIsMuted(conv.MuteDuration, conv.MuteEndTime)
}
func (c *conversationServer) fillConversationsUserMute(ctx context.Context, list []*pbconversation.Conversation) {
if len(list) == 0 {
return
}
for _, conv := range list {
c.fillConversationUserMute(ctx, conv)
}
}
func (c *conversationServer) fillConversationElemUserMute(
ctx context.Context,
db controller.UserMuteDatabase,
ownerUserID string,
elem *pbconversation.ConversationElem,
conversationType int32,
peerUserID string,
) {
if db == nil || elem == nil || ownerUserID == "" {
return
}
if conversationType != constant.SingleChatType || peerUserID == "" {
return
}
rec, err := db.Get(ctx, ownerUserID, peerUserID)
if err != nil {
log.ZWarn(ctx, "fillConversationElemUserMute Get", err, "owner", ownerUserID, "peer", peerUserID)
return
}
now := time.Now().Unix()
isMuted, dur, end := conversationMuteFromRecord(rec, now)
elem.IsMuted = isMuted
elem.MuteDuration = dur
elem.MuteEndTime = end
}

@ -72,7 +72,6 @@ type msgServer struct {
conversationClient *rpcli.ConversationClient
spamReportDB database.SpamReport
globalBlackDB controller.UserGlobalBlackDatabase
userMuteDB controller.UserMuteDatabase
msgBurnDeadlineDB database.MsgBurnDeadline
}
@ -138,10 +137,6 @@ 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,
@ -154,7 +149,6 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
conversationClient: conversationClient,
spamReportDB: spamReportDB,
globalBlackDB: controller.NewUserGlobalBlackDatabase(globalBlackMgo),
userMuteDB: controller.NewUserMuteDatabase(userMuteMgo),
msgBurnDeadlineDB: msgBurnDeadlineDB,
}

@ -325,14 +325,20 @@ func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, us
}
}
// 第四优先级用户静音设置user_mute 集合,支持好友与非好友)
// 无论会话记录是否存在均检查,以支持对非好友的静音
if m.userMuteDB != nil {
muted, err := m.userMuteDB.IsMuted(ctx, userID, pb.MsgData.SendID)
if err != nil {
return false, err
// 第四优先级:会话静音设置(存储于 conversations 集合的 mute_duration/mute_end_time
conv, convErr := m.ConversationLocalCache.GetConversation(ctx, userID, conversationID)
if convErr != nil && !errs.ErrRecordNotFound.Is(convErr) {
return false, convErr
}
if convErr == nil && conv != nil {
var isMuted bool
switch {
case conv.MuteDuration == -1 && conv.MuteEndTime == 0:
isMuted = true
case conv.MuteEndTime > 0:
isMuted = conv.MuteEndTime > time.Now().Unix()
}
if muted {
if isMuted {
if pb.MsgData.Options == nil {
pb.MsgData.Options = make(map[string]bool, 10)
}

@ -37,4 +37,6 @@ type Conversation struct {
IsMsgDestruct bool `bson:"is_msg_destruct"`
MsgDestructTime int64 `bson:"msg_destruct_time"`
LatestMsgDestructTime time.Time `bson:"latest_msg_destruct_time"`
MuteDuration int32 `bson:"mute_duration"`
MuteEndTime int64 `bson:"mute_end_time"`
}

Loading…
Cancel
Save