From 887e0b7314d10fdecc6b2ff443fa57e32f9e2b99 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Mon, 3 Mar 2025 10:39:53 +0800 Subject: [PATCH 01/59] fix: solve uncorrect notification when set group info (#3172) * fix: setGroupInfoEx uncorrect call. * update notification logic. * update group notification logic. * update update group announcement notication. * fix errror. * refresh * solve conflict. * update args. --- internal/rpc/group/db_map.go | 24 +++++++++---- internal/rpc/group/group.go | 55 +++++++++++++++--------------- internal/rpc/group/notification.go | 4 +-- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/internal/rpc/group/db_map.go b/internal/rpc/group/db_map.go index e99f9e772..7504bc851 100644 --- a/internal/rpc/group/db_map.go +++ b/internal/rpc/group/db_map.go @@ -16,6 +16,7 @@ package group import ( "context" + "strings" "time" pbgroup "github.com/openimsdk/protocol/group" @@ -55,41 +56,52 @@ func UpdateGroupInfoMap(ctx context.Context, group *sdkws.GroupInfoForSet) map[s return m } -func UpdateGroupInfoExMap(ctx context.Context, group *pbgroup.SetGroupInfoExReq) (map[string]any, error) { - m := make(map[string]any) +func UpdateGroupInfoExMap(ctx context.Context, group *pbgroup.SetGroupInfoExReq) (m map[string]any, normalFlag, groupNameFlag, notificationFlag bool, err error) { + m = make(map[string]any) if group.GroupName != nil { - if group.GroupName.Value != "" { + if strings.TrimSpace(group.GroupName.Value) != "" { m["group_name"] = group.GroupName.Value + groupNameFlag = true } else { - return nil, errs.ErrArgs.WrapMsg("group name is empty") + return nil, normalFlag, notificationFlag, groupNameFlag, errs.ErrArgs.WrapMsg("group name is empty") } } + if group.Notification != nil { + notificationFlag = true + group.Notification.Value = strings.TrimSpace(group.Notification.Value) // if Notification only contains spaces, set it to empty string + m["notification"] = group.Notification.Value - m["notification_update_time"] = time.Now() m["notification_user_id"] = mcontext.GetOpUserID(ctx) + m["notification_update_time"] = time.Now() } if group.Introduction != nil { m["introduction"] = group.Introduction.Value + normalFlag = true } if group.FaceURL != nil { m["face_url"] = group.FaceURL.Value + normalFlag = true } if group.NeedVerification != nil { m["need_verification"] = group.NeedVerification.Value + normalFlag = true } if group.LookMemberInfo != nil { m["look_member_info"] = group.LookMemberInfo.Value + normalFlag = true } if group.ApplyMemberFriend != nil { m["apply_member_friend"] = group.ApplyMemberFriend.Value + normalFlag = true } if group.Ex != nil { m["ex"] = group.Ex.Value + normalFlag = true } - return m, nil + return m, normalFlag, groupNameFlag, notificationFlag, nil } func UpdateGroupStatusMap(status int) map[string]any { diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 89ab35d2f..db852c944 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -288,10 +288,11 @@ func (g *groupServer) CreateGroup(ctx context.Context, req *pbgroup.CreateGroupR g.notification.GroupCreatedNotification(ctx, tips, req.SendMessage) if req.GroupInfo.Notification != "" { + notificationFlag := true g.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{ Group: tips.Group, OpUser: tips.OpUser, - }) + }, ¬ificationFlag) } reqCallBackAfter := &pbgroup.CreateGroupReq{ @@ -1026,7 +1027,8 @@ func (g *groupServer) SetGroupInfo(ctx context.Context, req *pbgroup.SetGroupInf log.ZWarn(ctx, "SetConversations", err, "UserIDs", resp.UserIDs, "conversation", conversation) } }() - g.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}) + notficationFlag := true + g.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}, ¬ficationFlag) } if req.GroupInfoForSet.GroupName != "" { num-- @@ -1087,7 +1089,7 @@ func (g *groupServer) SetGroupInfoEx(ctx context.Context, req *pbgroup.SetGroupI return nil, err } - updatedData, err := UpdateGroupInfoExMap(ctx, req) + updatedData, normalFlag, groupNameFlag, notificationFlag, err := UpdateGroupInfoExMap(ctx, req) if len(updatedData) == 0 { return &pbgroup.SetGroupInfoExResp{}, nil } @@ -1115,41 +1117,38 @@ func (g *groupServer) SetGroupInfoEx(ctx context.Context, req *pbgroup.SetGroupI tips.OpUser = g.groupMemberDB2PB(opMember, 0) } - num := len(updatedData) - - if req.Notification != nil { - num -= 3 - + if notificationFlag { if req.Notification.Value != "" { - func() { - conversation := &pbconv.ConversationReq{ - ConversationID: msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupID), - ConversationType: constant.ReadGroupChatType, - GroupID: req.GroupID, - } + conversation := &pbconv.ConversationReq{ + ConversationID: msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupID), + ConversationType: constant.ReadGroupChatType, + GroupID: req.GroupID, + } - resp, err := g.GetGroupMemberUserIDs(ctx, &pbgroup.GetGroupMemberUserIDsReq{GroupID: req.GroupID}) - if err != nil { - log.ZWarn(ctx, "GetGroupMemberIDs is failed.", err) - return - } + resp, err := g.GetGroupMemberUserIDs(ctx, &pbgroup.GetGroupMemberUserIDsReq{GroupID: req.GroupID}) + if err != nil { + log.ZWarn(ctx, "GetGroupMemberIDs is failed.", err) + return nil, err + } - conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.GroupNotification} - if err := g.conversationClient.SetConversations(ctx, resp.UserIDs, conversation); err != nil { - log.ZWarn(ctx, "SetConversations", err, "UserIDs", resp.UserIDs, "conversation", conversation) - } - }() + conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.GroupNotification} + if err := g.conversationClient.SetConversations(ctx, resp.UserIDs, conversation); err != nil { + log.ZWarn(ctx, "SetConversations", err, "UserIDs", resp.UserIDs, "conversation", conversation) + } - g.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}) + g.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}, ¬ificationFlag) + } else { + notificationFlag = false + g.notification.GroupInfoSetAnnouncementNotification(ctx, &sdkws.GroupInfoSetAnnouncementTips{Group: tips.Group, OpUser: tips.OpUser}, ¬ificationFlag) } } - if req.GroupName != nil { - num-- + if groupNameFlag { g.notification.GroupInfoSetNameNotification(ctx, &sdkws.GroupInfoSetNameTips{Group: tips.Group, OpUser: tips.OpUser}) } - if num > 0 { + // if updatedData > 0, send the normal notification + if normalFlag { g.notification.GroupInfoSetNotification(ctx, tips) } diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index 8bd8a9503..5fbb78d64 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -350,7 +350,7 @@ func (g *NotificationSender) GroupInfoSetNameNotification(ctx context.Context, t g.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetNameNotification, tips) } -func (g *NotificationSender) GroupInfoSetAnnouncementNotification(ctx context.Context, tips *sdkws.GroupInfoSetAnnouncementTips) { +func (g *NotificationSender) GroupInfoSetAnnouncementNotification(ctx context.Context, tips *sdkws.GroupInfoSetAnnouncementTips, sendMessage *bool) { var err error defer func() { if err != nil { @@ -361,7 +361,7 @@ func (g *NotificationSender) GroupInfoSetAnnouncementNotification(ctx context.Co return } g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID) - g.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetAnnouncementNotification, tips, notification.WithRpcGetUserName()) + g.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetAnnouncementNotification, tips, notification.WithRpcGetUserName(), notification.WithSendMessage(sendMessage)) } func (g *NotificationSender) JoinGroupApplicationNotification(ctx context.Context, req *pbgroup.JoinGroupReq) { From ab6c9dca713877264f5c44652b661710e8dc9ad3 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:59:35 +0800 Subject: [PATCH 02/59] feat: optimizing BatchGetIncrementalGroupMember (#3180) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: optimizing BatchGetIncrementalGroupMember --- internal/rpc/group/sync.go | 168 +++++-------------------------------- 1 file changed, 22 insertions(+), 146 deletions(-) diff --git a/internal/rpc/group/sync.go b/internal/rpc/group/sync.go index 50ac8252f..d311ae076 100644 --- a/internal/rpc/group/sync.go +++ b/internal/rpc/group/sync.go @@ -11,10 +11,10 @@ import ( "github.com/openimsdk/protocol/constant" pbgroup "github.com/openimsdk/protocol/group" "github.com/openimsdk/protocol/sdkws" - "github.com/openimsdk/tools/errs" - "github.com/openimsdk/tools/log" ) +const versionSyncLimit = 500 + func (g *groupServer) GetFullGroupMemberUserIDs(ctx context.Context, req *pbgroup.GetFullGroupMemberUserIDsReq) (*pbgroup.GetFullGroupMemberUserIDsResp, error) { vl, err := g.db.FindMaxGroupMemberVersionCache(ctx, req.GroupID) if err != nil { @@ -132,150 +132,6 @@ func (g *groupServer) GetIncrementalGroupMember(ctx context.Context, req *pbgrou return resp, nil } -func (g *groupServer) BatchGetIncrementalGroupMember(ctx context.Context, req *pbgroup.BatchGetIncrementalGroupMemberReq) (resp *pbgroup.BatchGetIncrementalGroupMemberResp, err error) { - type VersionInfo struct { - GroupID string - VersionID string - VersionNumber uint64 - } - - var groupIDs []string - - groupsVersionMap := make(map[string]*VersionInfo) - groupsMap := make(map[string]*model.Group) - hasGroupUpdateMap := make(map[string]bool) - sortVersionMap := make(map[string]uint64) - - var targetKeys, versionIDs []string - var versionNumbers []uint64 - - var requestBodyLen int - - for _, group := range req.ReqList { - groupsVersionMap[group.GroupID] = &VersionInfo{ - GroupID: group.GroupID, - VersionID: group.VersionID, - VersionNumber: group.Version, - } - - groupIDs = append(groupIDs, group.GroupID) - } - - groups, err := g.db.FindGroup(ctx, groupIDs) - if err != nil { - return nil, errs.Wrap(err) - } - - for _, group := range groups { - if group.Status == constant.GroupStatusDismissed { - err = servererrs.ErrDismissedAlready.Wrap() - log.ZError(ctx, "This group is Dismissed Already", err, "group is", group.GroupID) - - delete(groupsVersionMap, group.GroupID) - } else { - groupsMap[group.GroupID] = group - } - } - - for groupID, vInfo := range groupsVersionMap { - targetKeys = append(targetKeys, groupID) - versionIDs = append(versionIDs, vInfo.VersionID) - versionNumbers = append(versionNumbers, vInfo.VersionNumber) - } - - opt := incrversion.BatchOption[[]*sdkws.GroupMemberFullInfo, pbgroup.BatchGetIncrementalGroupMemberResp]{ - Ctx: ctx, - TargetKeys: targetKeys, - VersionIDs: versionIDs, - VersionNumbers: versionNumbers, - Versions: func(ctx context.Context, groupIDs []string, versions []uint64, limits []int) (map[string]*model.VersionLog, error) { - vLogs, err := g.db.BatchFindMemberIncrVersion(ctx, groupIDs, versions, limits) - if err != nil { - return nil, errs.Wrap(err) - } - - for groupID, vlog := range vLogs { - vlogElems := make([]model.VersionLogElem, 0, len(vlog.Logs)) - for i, log := range vlog.Logs { - switch log.EID { - case model.VersionGroupChangeID: - vlog.LogLen-- - hasGroupUpdateMap[groupID] = true - case model.VersionSortChangeID: - vlog.LogLen-- - sortVersionMap[groupID] = uint64(log.Version) - default: - vlogElems = append(vlogElems, vlog.Logs[i]) - } - } - vlog.Logs = vlogElems - if vlog.LogLen > 0 { - hasGroupUpdateMap[groupID] = true - } - } - - return vLogs, nil - }, - CacheMaxVersions: g.db.BatchFindMaxGroupMemberVersionCache, - Find: func(ctx context.Context, groupID string, ids []string) ([]*sdkws.GroupMemberFullInfo, error) { - memberInfo, err := g.getGroupMembersInfo(ctx, groupID, ids) - if err != nil { - return nil, err - } - - return memberInfo, err - }, - Resp: func(versions map[string]*model.VersionLog, deleteIdsMap map[string][]string, insertListMap, updateListMap map[string][]*sdkws.GroupMemberFullInfo, fullMap map[string]bool) *pbgroup.BatchGetIncrementalGroupMemberResp { - resList := make(map[string]*pbgroup.GetIncrementalGroupMemberResp) - - for groupID, versionLog := range versions { - resList[groupID] = &pbgroup.GetIncrementalGroupMemberResp{ - VersionID: versionLog.ID.Hex(), - Version: uint64(versionLog.Version), - Full: fullMap[groupID], - Delete: deleteIdsMap[groupID], - Insert: insertListMap[groupID], - Update: updateListMap[groupID], - SortVersion: sortVersionMap[groupID], - } - - requestBodyLen += len(insertListMap[groupID]) + len(updateListMap[groupID]) + len(deleteIdsMap[groupID]) - if requestBodyLen > 200 { - break - } - } - - return &pbgroup.BatchGetIncrementalGroupMemberResp{ - RespList: resList, - } - }, - } - - resp, err = opt.Build() - if err != nil { - return nil, errs.Wrap(err) - } - - for groupID, val := range resp.RespList { - if val.Full || hasGroupUpdateMap[groupID] { - count, err := g.db.FindGroupMemberNum(ctx, groupID) - if err != nil { - return nil, err - } - - owner, err := g.db.TakeGroupOwner(ctx, groupID) - if err != nil { - return nil, err - } - - resp.RespList[groupID].Group = g.groupDB2PB(groupsMap[groupID], owner.UserID, count) - } - } - - return resp, nil - -} - func (g *groupServer) GetIncrementalJoinGroup(ctx context.Context, req *pbgroup.GetIncrementalJoinGroupReq) (*pbgroup.GetIncrementalJoinGroupResp, error) { if err := authverify.CheckAccessV3(ctx, req.UserID, g.config.Share.IMAdminUserID); err != nil { return nil, err @@ -301,3 +157,23 @@ func (g *groupServer) GetIncrementalJoinGroup(ctx context.Context, req *pbgroup. } return opt.Build() } + +func (g *groupServer) BatchGetIncrementalGroupMember(ctx context.Context, req *pbgroup.BatchGetIncrementalGroupMemberReq) (*pbgroup.BatchGetIncrementalGroupMemberResp, error) { + var num int + resp := make(map[string]*pbgroup.GetIncrementalGroupMemberResp) + for _, memberReq := range req.ReqList { + if _, ok := resp[memberReq.GroupID]; ok { + continue + } + memberResp, err := g.GetIncrementalGroupMember(ctx, memberReq) + if err != nil { + return nil, err + } + resp[memberReq.GroupID] = memberResp + num += len(memberResp.Insert) + len(memberResp.Update) + len(memberResp.Delete) + if num >= versionSyncLimit { + break + } + } + return &pbgroup.BatchGetIncrementalGroupMemberResp{RespList: resp}, nil +} From 68ee86c1d9e28cdc746bed691d8985a6cfc999c9 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Tue, 4 Mar 2025 16:09:29 +0800 Subject: [PATCH 03/59] fix: the sorting is wrong after canceling the administrator in group settings (#3185) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings --- internal/rpc/group/notification.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index 5fbb78d64..cc70a86f6 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -782,7 +782,7 @@ func (g *NotificationSender) GroupMemberSetToAdminNotification(ctx context.Conte if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil { return } - g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID) + g.setSortVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID, &tips.GroupSortVersion) g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberSetToAdminNotification, tips) } @@ -807,6 +807,6 @@ func (g *NotificationSender) GroupMemberSetToOrdinaryUserNotification(ctx contex if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil { return } - g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID) + g.setSortVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID, &tips.GroupSortVersion) g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberSetToOrdinaryUserNotification, tips) } From 737169a466d4381bc4c20ce402fefb4ae808e224 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:04:21 +0800 Subject: [PATCH 04/59] feat: system account send msg doesn't need friend verify (#3187) --- internal/rpc/msg/verify.go | 8 ++++++++ pkg/authverify/token.go | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/internal/rpc/msg/verify.go b/internal/rpc/msg/verify.go index a492232a4..213545d83 100644 --- a/internal/rpc/msg/verify.go +++ b/internal/rpc/msg/verify.go @@ -20,6 +20,7 @@ import ( "strconv" "time" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/common/servererrs" "github.com/openimsdk/tools/utils/datautil" "github.com/openimsdk/tools/utils/encrypt" @@ -63,6 +64,13 @@ func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgRe if err := m.webhookBeforeSendSingleMsg(ctx, &m.config.WebhooksConfig.BeforeSendSingleMsg, data); err != nil { return err } + u, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID) + if err != nil { + return err + } + if authverify.CheckSystemAccount(ctx, u.AppMangerLevel) { + return nil + } black, err := m.FriendLocalCache.IsBlack(ctx, data.MsgData.SendID, data.MsgData.RecvID) if err != nil { return err diff --git a/pkg/authverify/token.go b/pkg/authverify/token.go index f1b377bad..872feb1cf 100644 --- a/pkg/authverify/token.go +++ b/pkg/authverify/token.go @@ -20,6 +20,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/openimsdk/open-im-server/v3/pkg/common/servererrs" + "github.com/openimsdk/protocol/constant" "github.com/openimsdk/tools/mcontext" "github.com/openimsdk/tools/utils/datautil" ) @@ -55,3 +56,7 @@ func CheckAdmin(ctx context.Context, imAdminUserID []string) error { func IsManagerUserID(opUserID string, imAdminUserID []string) bool { return datautil.Contain(opUserID, imAdminUserID...) } + +func CheckSystemAccount(ctx context.Context, level int32) bool { + return level >= constant.AppAdmin +} From 0541d0bf06165cb5c066605e105be9620efcae17 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Tue, 4 Mar 2025 18:12:35 +0800 Subject: [PATCH 05/59] fix: solve uncorrect GroupMember enter group notification type. (#3188) --- internal/rpc/group/group.go | 10 +++++++-- internal/rpc/group/notification.go | 34 ++++++++++++++---------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index db852c944..0a877a03e 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -824,8 +824,14 @@ func (g *groupServer) GroupApplicationResponse(ctx context.Context, req *pbgroup if member == nil { log.ZDebug(ctx, "GroupApplicationResponse", "member is nil") } else { - if err = g.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, nil, groupRequest.InviterUserID, req.FromUserID); err != nil { - return nil, err + if groupRequest.InviterUserID == "" { + if err = g.notification.MemberEnterNotification(ctx, req.GroupID, req.FromUserID); err != nil { + return nil, err + } + } else { + if err = g.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, nil, groupRequest.InviterUserID, req.FromUserID); err != nil { + return nil, err + } } } case constant.GroupResponseRefuse: diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index cc70a86f6..0a135af23 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -234,17 +234,17 @@ func (g *NotificationSender) groupMemberDB2PB(member *model.GroupMember, appMang return result, nil } */ -func (g *NotificationSender) fillOpUser(ctx context.Context, opUser **sdkws.GroupMemberFullInfo, groupID string) (err error) { - return g.fillOpUserByUserID(ctx, mcontext.GetOpUserID(ctx), opUser, groupID) +func (g *NotificationSender) fillOpUser(ctx context.Context, targetUser **sdkws.GroupMemberFullInfo, groupID string) (err error) { + return g.fillUserByUserID(ctx, mcontext.GetOpUserID(ctx), targetUser, groupID) } -func (g *NotificationSender) fillOpUserByUserID(ctx context.Context, userID string, opUser **sdkws.GroupMemberFullInfo, groupID string) error { - if opUser == nil { +func (g *NotificationSender) fillUserByUserID(ctx context.Context, userID string, targetUser **sdkws.GroupMemberFullInfo, groupID string) error { + if targetUser == nil { return errs.ErrInternalServer.WrapMsg("**sdkws.GroupMemberFullInfo is nil") } if groupID != "" { if authverify.IsManagerUserID(userID, g.config.Share.IMAdminUserID) { - *opUser = &sdkws.GroupMemberFullInfo{ + *targetUser = &sdkws.GroupMemberFullInfo{ GroupID: groupID, UserID: userID, RoleLevel: constant.GroupAdmin, @@ -253,7 +253,7 @@ func (g *NotificationSender) fillOpUserByUserID(ctx context.Context, userID stri } else { member, err := g.db.TakeGroupMember(ctx, groupID, userID) if err == nil { - *opUser = g.groupMemberDB2PB(member, 0) + *targetUser = g.groupMemberDB2PB(member, 0) } else if !(errors.Is(err, mongo.ErrNoDocuments) || errs.ErrRecordNotFound.Is(err)) { return err } @@ -263,8 +263,8 @@ func (g *NotificationSender) fillOpUserByUserID(ctx context.Context, userID stri if err != nil { return err } - if *opUser == nil { - *opUser = &sdkws.GroupMemberFullInfo{ + if *targetUser == nil { + *targetUser = &sdkws.GroupMemberFullInfo{ GroupID: groupID, UserID: userID, Nickname: user.Nickname, @@ -272,11 +272,11 @@ func (g *NotificationSender) fillOpUserByUserID(ctx context.Context, userID stri OperatorUserID: userID, } } else { - if (*opUser).Nickname == "" { - (*opUser).Nickname = user.Nickname + if (*targetUser).Nickname == "" { + (*targetUser).Nickname = user.Nickname } - if (*opUser).FaceURL == "" { - (*opUser).FaceURL = user.FaceURL + if (*targetUser).FaceURL == "" { + (*targetUser).FaceURL = user.FaceURL } } return nil @@ -557,15 +557,13 @@ func (g *NotificationSender) GroupApplicationAgreeMemberEnterNotification(ctx co InvitedUserList: users, } opUserID := mcontext.GetOpUserID(ctx) - if err = g.fillOpUserByUserID(ctx, opUserID, &tips.OpUser, tips.Group.GroupID); err != nil { + if err = g.fillUserByUserID(ctx, opUserID, &tips.OpUser, tips.Group.GroupID); err != nil { return nil } - switch { - case invitedOpUserID == "": - case invitedOpUserID == opUserID: + if invitedOpUserID == opUserID { tips.InviterUser = tips.OpUser - default: - if err = g.fillOpUserByUserID(ctx, invitedOpUserID, &tips.InviterUser, tips.Group.GroupID); err != nil { + } else { + if err = g.fillUserByUserID(ctx, invitedOpUserID, &tips.InviterUser, tips.Group.GroupID); err != nil { return err } } From 964ee7a8dd6b673f6800f7f243462570d595130c Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:04:57 +0800 Subject: [PATCH 06/59] feat: sending messages supports returning fields modified by webhook (#3192) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook --- go.mod | 2 +- go.sum | 4 +- internal/api/msg.go | 84 ++++++++++++++++- internal/rpc/msg/callback.go | 17 +++- internal/rpc/msg/send.go | 60 +++++++----- pkg/apistruct/manage.go | 12 +++ pkg/apistruct/msg_test.go | 1 - test/webhook/msgmodify/main.go | 65 +++++++++++++ tools/streammsg/main.go | 161 --------------------------------- 9 files changed, 208 insertions(+), 198 deletions(-) delete mode 100644 pkg/apistruct/msg_test.go create mode 100644 test/webhook/msgmodify/main.go delete mode 100644 tools/streammsg/main.go diff --git a/go.mod b/go.mod index 57b2b6f7e..7bf9e6ef6 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/openimsdk/protocol v0.0.72-alpha.78 + github.com/openimsdk/protocol v0.0.72-alpha.79 github.com/openimsdk/tools v0.0.50-alpha.74 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 0f870fa45..2a86d97ea 100644 --- a/go.sum +++ b/go.sum @@ -347,8 +347,8 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/openimsdk/gomake v0.0.15-alpha.2 h1:5Q8yl8ezy2yx+q8/ucU/t4kJnDfCzNOrkXcDACCqtyM= github.com/openimsdk/gomake v0.0.15-alpha.2/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= -github.com/openimsdk/protocol v0.0.72-alpha.78 h1:n9HVj5olMPiGLF3Z4apPvvYzn2yOpyrsn2/YiAaIsxw= -github.com/openimsdk/protocol v0.0.72-alpha.78/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= +github.com/openimsdk/protocol v0.0.72-alpha.79 h1:e46no8WVAsmTzyy405klrdoUiG7u+1ohDsXvQuFng4s= +github.com/openimsdk/protocol v0.0.72-alpha.79/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= github.com/openimsdk/tools v0.0.50-alpha.74 h1:yh10SiMiivMEjicEQg+QAsH4pvaO+4noMPdlw+ew0Kc= github.com/openimsdk/tools v0.0.50-alpha.74/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/internal/api/msg.go b/internal/api/msg.go index a7c2c888f..1ec1f44a7 100644 --- a/internal/api/msg.go +++ b/internal/api/msg.go @@ -17,10 +17,12 @@ package api import ( "encoding/base64" "encoding/json" + "sync" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/mitchellh/mapstructure" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/openimsdk/open-im-server/v3/pkg/apistruct" "github.com/openimsdk/open-im-server/v3/pkg/authverify" @@ -41,6 +43,39 @@ import ( "github.com/openimsdk/tools/utils/timeutil" ) +var ( + msgDataDescriptor []protoreflect.FieldDescriptor + msgDataDescriptorOnce sync.Once +) + +func getMsgDataDescriptor() []protoreflect.FieldDescriptor { + msgDataDescriptorOnce.Do(func() { + skip := make(map[string]struct{}) + respFields := new(msg.SendMsgResp).ProtoReflect().Descriptor().Fields() + for i := 0; i < respFields.Len(); i++ { + field := respFields.Get(i) + if !field.HasJSONName() { + continue + } + skip[field.JSONName()] = struct{}{} + } + fields := new(sdkws.MsgData).ProtoReflect().Descriptor().Fields() + num := fields.Len() + msgDataDescriptor = make([]protoreflect.FieldDescriptor, 0, num) + for i := 0; i < num; i++ { + field := fields.Get(i) + if !field.HasJSONName() { + continue + } + if _, ok := skip[field.JSONName()]; ok { + continue + } + msgDataDescriptor = append(msgDataDescriptor, fields.Get(i)) + } + }) + return msgDataDescriptor +} + type MessageApi struct { Client msg.MsgClient userClient *rpcli.UserClient @@ -197,6 +232,42 @@ func (m *MessageApi) getSendMsgReq(c *gin.Context, req apistruct.SendMsg) (sendM return m.newUserSendMsgReq(c, &req), nil } +func (m *MessageApi) getModifyFields(req, respModify *sdkws.MsgData) map[string]any { + if req == nil || respModify == nil { + return nil + } + fields := make(map[string]any) + reqProtoReflect := req.ProtoReflect() + respProtoReflect := respModify.ProtoReflect() + for _, descriptor := range getMsgDataDescriptor() { + reqValue := reqProtoReflect.Get(descriptor) + respValue := respProtoReflect.Get(descriptor) + if !reqValue.Equal(respValue) { + val := respValue.Interface() + name := descriptor.JSONName() + if name == "content" { + if bs, ok := val.([]byte); ok { + val = string(bs) + } + } + fields[name] = val + } + } + if len(fields) == 0 { + fields = nil + } + return fields +} + +func (m *MessageApi) ginRespSendMsg(c *gin.Context, req *msg.SendMsgReq, resp *msg.SendMsgResp) { + res := m.getModifyFields(req.MsgData, resp.Modify) + resp.Modify = nil + apiresp.GinSuccess(c, &apistruct.SendMsgResp{ + SendMsgResp: resp, + Modify: res, + }) +} + // SendMessage handles the sending of a message. It's an HTTP handler function to be used with Gin framework. func (m *MessageApi) SendMessage(c *gin.Context) { // Initialize a request struct for sending a message. @@ -250,7 +321,7 @@ func (m *MessageApi) SendMessage(c *gin.Context) { } // Respond with a success message and the response payload. - apiresp.GinSuccess(c, respPb) + m.ginRespSendMsg(c, sendMsgReq, respPb) } func (m *MessageApi) SendBusinessNotification(c *gin.Context) { @@ -316,7 +387,7 @@ func (m *MessageApi) SendBusinessNotification(c *gin.Context) { apiresp.GinError(c, err) return } - apiresp.GinSuccess(c, respPb) + m.ginRespSendMsg(c, &sendMsgReq, respPb) } func (m *MessageApi) BatchSendMsg(c *gin.Context) { @@ -370,6 +441,7 @@ func (m *MessageApi) BatchSendMsg(c *gin.Context) { ClientMsgID: rpcResp.ClientMsgID, SendTime: rpcResp.SendTime, RecvID: recvID, + Modify: m.getModifyFields(sendMsgReq.MsgData, rpcResp.Modify), }) } apiresp.GinSuccess(c, resp) @@ -432,7 +504,11 @@ func (m *MessageApi) SendSimpleMessage(c *gin.Context) { Ex: req.Ex, } - respPb, err := m.Client.SendMsg(c, &msg.SendMsgReq{MsgData: msgData}) + sendReq := &msg.SendMsgReq{ + MsgData: msgData, + } + + respPb, err := m.Client.SendMsg(c, sendReq) if err != nil { apiresp.GinError(c, err) return @@ -449,7 +525,7 @@ func (m *MessageApi) SendSimpleMessage(c *gin.Context) { return } - apiresp.GinSuccess(c, respPb) + m.ginRespSendMsg(c, sendReq, respPb) } func (m *MessageApi) CheckMsgIsSendSuccess(c *gin.Context) { diff --git a/internal/rpc/msg/callback.go b/internal/rpc/msg/callback.go index adf1ff735..5bc98de0c 100644 --- a/internal/rpc/msg/callback.go +++ b/internal/rpc/msg/callback.go @@ -17,9 +17,11 @@ package msg import ( "context" "encoding/base64" + "encoding/json" "github.com/openimsdk/open-im-server/v3/pkg/apistruct" "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" + "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/utils/stringutil" cbapi "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" @@ -136,11 +138,11 @@ func (m *msgServer) webhookAfterSendGroupMsg(ctx context.Context, after *config. m.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendGroupMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData)) } -func (m *msgServer) webhookBeforeMsgModify(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq) error { +func (m *msgServer) webhookBeforeMsgModify(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq, beforeMsgData **sdkws.MsgData) error { return webhook.WithCondition(ctx, before, func(ctx context.Context) error { - if msg.MsgData.ContentType != constant.Text { - return nil - } + //if msg.MsgData.ContentType != constant.Text { + // return nil + //} if !filterBeforeMsg(msg, before) { return nil } @@ -151,9 +153,14 @@ func (m *msgServer) webhookBeforeMsgModify(ctx context.Context, before *config.B if err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil { return err } - + if beforeMsgData != nil { + *beforeMsgData = proto.Clone(msg.MsgData).(*sdkws.MsgData) + } if resp.Content != nil { msg.MsgData.Content = []byte(*resp.Content) + if err := json.Unmarshal(msg.MsgData.Content, &struct{}{}); err != nil { + return errs.ErrArgs.WrapMsg("webhook msg modify content is not json", "content", string(msg.MsgData.Content)) + } } datautil.NotNilReplace(msg.MsgData.OfflinePushInfo, resp.OfflinePushInfo) datautil.NotNilReplace(&msg.MsgData.RecvID, resp.RecvID) diff --git a/internal/rpc/msg/send.go b/internal/rpc/msg/send.go index 19f4e9ffd..f226c4921 100644 --- a/internal/rpc/msg/send.go +++ b/internal/rpc/msg/send.go @@ -29,31 +29,44 @@ import ( "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" "github.com/openimsdk/tools/utils/datautil" + "google.golang.org/protobuf/proto" ) func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg.SendMsgResp, error) { - if req.MsgData != nil { - m.encapsulateMsgData(req.MsgData) - if req.MsgData.ContentType == constant.Stream { - if err := m.handlerStreamMsg(ctx, req.MsgData); err != nil { - return nil, err - } - } - switch req.MsgData.SessionType { - case constant.SingleChatType: - return m.sendMsgSingleChat(ctx, req) - case constant.NotificationChatType: - return m.sendMsgNotification(ctx, req) - case constant.ReadGroupChatType: - return m.sendMsgGroupChat(ctx, req) - default: - return nil, errs.ErrArgs.WrapMsg("unknown sessionType") + if req.MsgData == nil { + return nil, errs.ErrArgs.WrapMsg("msgData is nil") + } + before := new(*sdkws.MsgData) + resp, err := m.sendMsg(ctx, req, before) + if err != nil { + return nil, err + } + if *before != nil && proto.Equal(*before, req.MsgData) == false { + resp.Modify = req.MsgData + } + return resp, nil +} + +func (m *msgServer) sendMsg(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (*pbmsg.SendMsgResp, error) { + m.encapsulateMsgData(req.MsgData) + if req.MsgData.ContentType == constant.Stream { + if err := m.handlerStreamMsg(ctx, req.MsgData); err != nil { + return nil, err } } - return nil, errs.ErrArgs.WrapMsg("msgData is nil") + switch req.MsgData.SessionType { + case constant.SingleChatType: + return m.sendMsgSingleChat(ctx, req, before) + case constant.NotificationChatType: + return m.sendMsgNotification(ctx, req, before) + case constant.ReadGroupChatType: + return m.sendMsgGroupChat(ctx, req, before) + default: + return nil, errs.ErrArgs.WrapMsg("unknown sessionType") + } } -func (m *msgServer) sendMsgGroupChat(ctx context.Context, req *pbmsg.SendMsgReq) (resp *pbmsg.SendMsgResp, err error) { +func (m *msgServer) sendMsgGroupChat(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) { if err = m.messageVerification(ctx, req); err != nil { prommetrics.GroupChatMsgProcessFailedCounter.Inc() return nil, err @@ -62,7 +75,7 @@ func (m *msgServer) sendMsgGroupChat(ctx context.Context, req *pbmsg.SendMsgReq) if err = m.webhookBeforeSendGroupMsg(ctx, &m.config.WebhooksConfig.BeforeSendGroupMsg, req); err != nil { return nil, err } - if err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req); err != nil { + if err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req, before); err != nil { return nil, err } err = m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForGroup(req.MsgData.GroupID), req.MsgData) @@ -144,7 +157,7 @@ func (m *msgServer) setConversationAtInfo(nctx context.Context, msg *sdkws.MsgDa } } -func (m *msgServer) sendMsgNotification(ctx context.Context, req *pbmsg.SendMsgReq) (resp *pbmsg.SendMsgResp, err error) { +func (m *msgServer) sendMsgNotification(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) { if err := m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil { return nil, err } @@ -156,7 +169,7 @@ func (m *msgServer) sendMsgNotification(ctx context.Context, req *pbmsg.SendMsgR return resp, nil } -func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq) (resp *pbmsg.SendMsgResp, err error) { +func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) { if err := m.messageVerification(ctx, req); err != nil { return nil, err } @@ -176,12 +189,11 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq } if !isSend { prommetrics.SingleChatMsgProcessFailedCounter.Inc() - return nil, nil + return nil, errs.ErrArgs.WrapMsg("message is not sent") } else { - if err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req); err != nil { + if err := m.webhookBeforeMsgModify(ctx, &m.config.WebhooksConfig.BeforeMsgModify, req, before); err != nil { return nil, err } - if err := m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForSingle(req.MsgData.SendID, req.MsgData.RecvID), req.MsgData); err != nil { prommetrics.SingleChatMsgProcessFailedCounter.Inc() return nil, err diff --git a/pkg/apistruct/manage.go b/pkg/apistruct/manage.go index 4f1d5863f..cb2b1756f 100644 --- a/pkg/apistruct/manage.go +++ b/pkg/apistruct/manage.go @@ -15,6 +15,7 @@ package apistruct import ( + pbmsg "github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/sdkws" ) @@ -139,4 +140,15 @@ type SingleReturnResult struct { // RecvID uniquely identifies the receiver of the message. RecvID string `json:"recvID"` + + // Modify fields modified via webhook. + Modify map[string]any `json:"modify,omitempty"` +} + +type SendMsgResp struct { + // SendMsgResp original response. + *pbmsg.SendMsgResp + + // Modify fields modified via webhook. + Modify map[string]any `json:"modify,omitempty"` } diff --git a/pkg/apistruct/msg_test.go b/pkg/apistruct/msg_test.go deleted file mode 100644 index 28f878a9f..000000000 --- a/pkg/apistruct/msg_test.go +++ /dev/null @@ -1 +0,0 @@ -package apistruct diff --git a/test/webhook/msgmodify/main.go b/test/webhook/msgmodify/main.go new file mode 100644 index 000000000..3bdfa0ec5 --- /dev/null +++ b/test/webhook/msgmodify/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + cbapi "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" + "github.com/openimsdk/protocol/constant" +) + +func main() { + g := gin.Default() + g.POST("/callbackExample/callbackBeforeMsgModifyCommand", toGin(handlerMsg)) + if err := g.Run(":10006"); err != nil { + panic(err) + } +} + +func toGin[R any](fn func(c *gin.Context, req *R)) gin.HandlerFunc { + return func(c *gin.Context) { + body, err := io.ReadAll(c.Request.Body) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + fmt.Printf("HTTP %s %s %s\n", c.Request.Method, c.Request.URL, body) + var req R + if err := json.Unmarshal(body, &req); err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + fn(c, &req) + } +} + +func handlerMsg(c *gin.Context, req *cbapi.CallbackMsgModifyCommandReq) { + var resp cbapi.CallbackMsgModifyCommandResp + if req.ContentType != constant.Text { + c.JSON(http.StatusOK, &resp) + return + } + var textElem struct { + Content string `json:"content"` + } + if err := json.Unmarshal([]byte(req.Content), &textElem); err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + const word = "xxx" + if strings.Contains(textElem.Content, word) { + textElem.Content = strings.ReplaceAll(textElem.Content, word, strings.Repeat("*", len(word))) + content, err := json.Marshal(&textElem) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + tmp := string(content) + resp.Content = &tmp + } + c.JSON(http.StatusOK, &resp) +} diff --git a/tools/streammsg/main.go b/tools/streammsg/main.go deleted file mode 100644 index bb567e233..000000000 --- a/tools/streammsg/main.go +++ /dev/null @@ -1,161 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/openimsdk/open-im-server/v3/pkg/apistruct" - cbapi "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" - "github.com/openimsdk/protocol/auth" - "github.com/openimsdk/protocol/constant" - "github.com/openimsdk/protocol/msg" - "github.com/openimsdk/tools/apiresp" - "github.com/openimsdk/tools/errs" - "io" - "net/http" - "strings" - "time" -) - -const ( - getAdminToken = "/auth/get_admin_token" - sendMsgApi = "/msg/send_msg" - appendStreamMsg = "/msg/append_stream_msg" -) - -var ( - ApiAddr = "http://127.0.0.1:10002" - Token string -) - -func ApiCall[R any](api string, req any) (*R, error) { - data, err := json.Marshal(req) - if err != nil { - return nil, err - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) - defer cancel() - request, err := http.NewRequestWithContext(ctx, http.MethodPost, ApiAddr+api, bytes.NewBuffer(data)) - if err != nil { - return nil, err - } - if Token != "" { - request.Header.Set("token", Token) - } - request.Header.Set(constant.OperationID, uuid.New().String()) - response, err := http.DefaultClient.Do(request) - if err != nil { - return nil, err - } - defer response.Body.Close() - var resp R - apiResponse := apiresp.ApiResponse{ - Data: &resp, - } - if err := json.NewDecoder(response.Body).Decode(&apiResponse); err != nil { - return nil, err - } - if apiResponse.ErrCode != 0 { - return nil, errs.NewCodeError(apiResponse.ErrCode, apiResponse.ErrMsg) - } - return &resp, nil -} - -func main() { - resp, err := ApiCall[auth.GetAdminTokenResp](getAdminToken, &auth.GetAdminTokenReq{ - Secret: "openIM123", - UserID: "imAdmin", - }) - if err != nil { - fmt.Println("get admin token failed", err) - return - } - Token = resp.Token - g := gin.Default() - g.POST("/callbackExample/callbackAfterSendSingleMsgCommand", toGin(handlerUserMsg)) - if err := g.Run(":10006"); err != nil { - panic(err) - } -} - -func toGin[R any](fn func(c *gin.Context, req *R) error) gin.HandlerFunc { - return func(c *gin.Context) { - body, err := io.ReadAll(c.Request.Body) - if err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - fmt.Printf("HTTP %s %s %s\n", c.Request.Method, c.Request.URL, body) - var req R - if err := json.Unmarshal(body, &req); err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - if err := fn(c, &req); err != nil { - c.String(http.StatusInternalServerError, err.Error()) - return - } - c.String(http.StatusOK, "{}") - } -} - -func handlerUserMsg(c *gin.Context, req *cbapi.CallbackAfterSendSingleMsgReq) error { - if req.ContentType != constant.Text { - return nil - } - if !strings.Contains(req.Content, "stream") { - return nil - } - apiReq := apistruct.SendMsgReq{ - RecvID: req.SendID, - SendMsg: apistruct.SendMsg{ - SendID: req.RecvID, - SenderNickname: "xxx", - SenderFaceURL: "", - SenderPlatformID: constant.AdminPlatformID, - ContentType: constant.Stream, - SessionType: req.SessionType, - SendTime: time.Now().UnixMilli(), - Content: map[string]any{ - "type": "xxx", - "content": "server test stream msg", - }, - }, - } - go func() { - if err := doPushStreamMsg(&apiReq); err != nil { - fmt.Println("doPushStreamMsg failed", err) - return - } - fmt.Println("doPushStreamMsg success") - }() - return nil -} - -func doPushStreamMsg(sendReq *apistruct.SendMsgReq) error { - resp, err := ApiCall[msg.SendMsgResp](sendMsgApi, sendReq) - if err != nil { - return err - } - const num = 5 - for i := 1; i <= num; i++ { - _, err := ApiCall[msg.AppendStreamMsgResp](appendStreamMsg, &msg.AppendStreamMsgReq{ - ClientMsgID: resp.ClientMsgID, - StartIndex: int64(i - 1), - Packets: []string{ - fmt.Sprintf("stream_msg_packet_%03d", i), - }, - End: i == num, - }) - if err != nil { - fmt.Println("append stream msg failed", "clientMsgID", resp.ClientMsgID, "index", fmt.Sprintf("%d/%d", i, num), "error", err) - return err - } - fmt.Println("append stream msg success", "clientMsgID", resp.ClientMsgID, "index", fmt.Sprintf("%d/%d", i, num)) - time.Sleep(time.Second * 10) - } - return nil -} From 7859f2ccb37d1b6eebf8e4139c6e4baf3e58bfd1 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Fri, 7 Mar 2025 17:03:40 +0800 Subject: [PATCH 07/59] refactor: improve setConversations method. (#3194) --- internal/rpc/conversation/conversation.go | 99 +++-------------------- internal/rpc/conversation/db_map.go | 85 +++++++++++++++++++ 2 files changed, 95 insertions(+), 89 deletions(-) create mode 100644 internal/rpc/conversation/db_map.go diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index cd7839234..165d1bdc2 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -237,6 +237,7 @@ func (c *conversationServer) SetConversations(ctx context.Context, req *pbconver if req.Conversation == nil { return nil, errs.ErrArgs.WrapMsg("conversation must not be nil") } + if req.Conversation.ConversationType == constant.WriteGroupChatType { groupInfo, err := c.groupClient.GetGroupInfo(ctx, req.Conversation.GroupID) if err != nil { @@ -271,109 +272,29 @@ func (c *conversationServer) SetConversations(ctx context.Context, req *pbconver conversation.UserID = req.Conversation.UserID conversation.GroupID = req.Conversation.GroupID - m := make(map[string]any) - - setConversationFieldsFunc := func() { - if req.Conversation.RecvMsgOpt != nil { - conversation.RecvMsgOpt = req.Conversation.RecvMsgOpt.Value - m["recv_msg_opt"] = req.Conversation.RecvMsgOpt.Value - } - if req.Conversation.AttachedInfo != nil { - conversation.AttachedInfo = req.Conversation.AttachedInfo.Value - m["attached_info"] = req.Conversation.AttachedInfo.Value - } - if req.Conversation.Ex != nil { - conversation.Ex = req.Conversation.Ex.Value - m["ex"] = req.Conversation.Ex.Value - } - if req.Conversation.IsPinned != nil { - conversation.IsPinned = req.Conversation.IsPinned.Value - m["is_pinned"] = req.Conversation.IsPinned.Value - } - if req.Conversation.GroupAtType != nil { - conversation.GroupAtType = req.Conversation.GroupAtType.Value - m["group_at_type"] = req.Conversation.GroupAtType.Value - } - if req.Conversation.MsgDestructTime != nil { - conversation.MsgDestructTime = req.Conversation.MsgDestructTime.Value - m["msg_destruct_time"] = req.Conversation.MsgDestructTime.Value - } - if req.Conversation.IsMsgDestruct != nil { - conversation.IsMsgDestruct = req.Conversation.IsMsgDestruct.Value - m["is_msg_destruct"] = req.Conversation.IsMsgDestruct.Value - } - if req.Conversation.BurnDuration != nil { - conversation.BurnDuration = req.Conversation.BurnDuration.Value - m["burn_duration"] = req.Conversation.BurnDuration.Value - } + m, conversation, err := UpdateConversationsMap(ctx, req) + if err != nil { + return nil, err } - // set need set field in conversation - setConversationFieldsFunc() - for userID := range conversationMap { - unequal := len(m) - - if req.Conversation.RecvMsgOpt != nil { - if req.Conversation.RecvMsgOpt.Value == conversationMap[userID].RecvMsgOpt { - unequal-- - } - } - - if req.Conversation.AttachedInfo != nil { - if req.Conversation.AttachedInfo.Value == conversationMap[userID].AttachedInfo { - unequal-- - } - } + unequal := UserUpdateCheckMap(ctx, userID, req.Conversation, conversationMap[userID]) - if req.Conversation.Ex != nil { - if req.Conversation.Ex.Value == conversationMap[userID].Ex { - unequal-- - } - } - if req.Conversation.IsPinned != nil { - if req.Conversation.IsPinned.Value == conversationMap[userID].IsPinned { - unequal-- - } - } - - if req.Conversation.GroupAtType != nil { - if req.Conversation.GroupAtType.Value == conversationMap[userID].GroupAtType { - unequal-- - } - } - - if req.Conversation.MsgDestructTime != nil { - if req.Conversation.MsgDestructTime.Value == conversationMap[userID].MsgDestructTime { - unequal-- - } - } - - if req.Conversation.IsMsgDestruct != nil { - if req.Conversation.IsMsgDestruct.Value == conversationMap[userID].IsMsgDestruct { - unequal-- - } - } - - if req.Conversation.BurnDuration != nil { - if req.Conversation.BurnDuration.Value == conversationMap[userID].BurnDuration { - unequal-- - } - } - - if unequal > 0 { + if unequal { needUpdateUsersList = append(needUpdateUsersList, userID) } } + if len(m) != 0 && len(needUpdateUsersList) != 0 { if err := c.conversationDatabase.SetUsersConversationFieldTx(ctx, needUpdateUsersList, &conversation, m); err != nil { return nil, err } - for _, v := range needUpdateUsersList { - c.conversationNotificationSender.ConversationChangeNotification(ctx, v, []string{req.Conversation.ConversationID}) + for _, userID := range needUpdateUsersList { + c.conversationNotificationSender.ConversationChangeNotification(ctx, userID, []string{req.Conversation.ConversationID}) } } + if req.Conversation.IsPrivateChat != nil && req.Conversation.ConversationType != constant.ReadGroupChatType { var conversations []*dbModel.Conversation for _, ownerUserID := range req.UserIDs { diff --git a/internal/rpc/conversation/db_map.go b/internal/rpc/conversation/db_map.go new file mode 100644 index 000000000..4acb16e4b --- /dev/null +++ b/internal/rpc/conversation/db_map.go @@ -0,0 +1,85 @@ +package conversation + +import ( + "context" + + dbModel "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/protocol/conversation" +) + +func UpdateConversationsMap(ctx context.Context, req *conversation.SetConversationsReq) (m map[string]any, conversation dbModel.Conversation, err error) { + m = make(map[string]any) + + conversation.ConversationID = req.Conversation.ConversationID + conversation.ConversationType = req.Conversation.ConversationType + conversation.UserID = req.Conversation.UserID + conversation.GroupID = req.Conversation.GroupID + + if req.Conversation.RecvMsgOpt != nil { + conversation.RecvMsgOpt = req.Conversation.RecvMsgOpt.Value + m["recv_msg_opt"] = req.Conversation.RecvMsgOpt.Value + } + + if req.Conversation.AttachedInfo != nil { + conversation.AttachedInfo = req.Conversation.AttachedInfo.Value + m["attached_info"] = req.Conversation.AttachedInfo.Value + } + + if req.Conversation.Ex != nil { + conversation.Ex = req.Conversation.Ex.Value + m["ex"] = req.Conversation.Ex.Value + } + if req.Conversation.IsPinned != nil { + conversation.IsPinned = req.Conversation.IsPinned.Value + m["is_pinned"] = req.Conversation.IsPinned.Value + } + if req.Conversation.GroupAtType != nil { + conversation.GroupAtType = req.Conversation.GroupAtType.Value + m["group_at_type"] = req.Conversation.GroupAtType.Value + } + if req.Conversation.MsgDestructTime != nil { + conversation.MsgDestructTime = req.Conversation.MsgDestructTime.Value + m["msg_destruct_time"] = req.Conversation.MsgDestructTime.Value + } + if req.Conversation.IsMsgDestruct != nil { + conversation.IsMsgDestruct = req.Conversation.IsMsgDestruct.Value + m["is_msg_destruct"] = req.Conversation.IsMsgDestruct.Value + } + if req.Conversation.BurnDuration != nil { + conversation.BurnDuration = req.Conversation.BurnDuration.Value + m["burn_duration"] = req.Conversation.BurnDuration.Value + } + + return m, conversation, nil +} + +func UserUpdateCheckMap(ctx context.Context, userID string, req *conversation.ConversationReq, conversation *dbModel.Conversation) (unequal bool) { + unequal = false + + if req.RecvMsgOpt != nil && conversation.RecvMsgOpt != req.RecvMsgOpt.Value { + unequal = true + } + if req.AttachedInfo != nil && conversation.AttachedInfo != req.AttachedInfo.Value { + unequal = true + } + if req.Ex != nil && conversation.Ex != req.Ex.Value { + unequal = true + } + if req.IsPinned != nil && conversation.IsPinned != req.IsPinned.Value { + unequal = true + } + if req.GroupAtType != nil && conversation.GroupAtType != req.GroupAtType.Value { + unequal = true + } + if req.MsgDestructTime != nil && conversation.MsgDestructTime != req.MsgDestructTime.Value { + unequal = true + } + if req.IsMsgDestruct != nil && conversation.IsMsgDestruct != req.IsMsgDestruct.Value { + unequal = true + } + if req.BurnDuration != nil && conversation.BurnDuration != req.BurnDuration.Value { + unequal = true + } + + return unequal +} From c0c4ba21a8e53547385b403906d48e4378a809e5 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Wed, 12 Mar 2025 11:38:18 +0800 Subject: [PATCH 08/59] fix: solve unocrrect invite notification (#3213) --- internal/rpc/group/group.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 0a877a03e..e1604e7e5 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -390,6 +390,8 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite if err := g.PopulateGroupMember(ctx, groupMember); err != nil { return nil, err } + } else { + opUserID = mcontext.GetOpUserID(ctx) } if err := g.webhookBeforeInviteUserToGroup(ctx, &g.config.WebhooksConfig.BeforeInviteUserToGroup, req); err != nil && err != servererrs.ErrCallbackContinue { @@ -425,6 +427,7 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite } } } + var groupMembers []*model.GroupMember for _, userID := range req.InvitedUserIDs { member := &model.GroupMember{ From e76e02fdba3f03a87b91bbbc1acb3c659ee11d7b Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:54:37 +0800 Subject: [PATCH 09/59] feat: set configs (#3183) --- internal/api/config_manager.go | 105 ++++++++++++++++++++++++++++++++ internal/api/router.go | 1 + pkg/apistruct/config_manager.go | 4 ++ 3 files changed, 110 insertions(+) diff --git a/internal/api/config_manager.go b/internal/api/config_manager.go index 15f5e7004..aca02c52c 100644 --- a/internal/api/config_manager.go +++ b/internal/api/config_manager.go @@ -15,6 +15,7 @@ import ( "github.com/openimsdk/tools/apiresp" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" + "github.com/openimsdk/tools/utils/datautil" "github.com/openimsdk/tools/utils/runtimeenv" clientv3 "go.etcd.io/etcd/client/v3" ) @@ -144,6 +145,110 @@ func (cm *ConfigManager) SetConfig(c *gin.Context) { apiresp.GinSuccess(c, nil) } +func (cm *ConfigManager) SetConfigs(c *gin.Context) { + if cm.config.Discovery.Enable != config.ETCD { + apiresp.GinError(c, errs.New("only etcd support set config").Wrap()) + return + } + var req apistruct.SetConfigsReq + if err := c.BindJSON(&req); err != nil { + apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) + return + } + var ( + err error + ops []*clientv3.Op + ) + + for _, cf := range req.Configs { + var op *clientv3.Op + switch cf.ConfigName { + case cm.config.Discovery.GetConfigFileName(): + op, err = compareAndOp[config.Discovery](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Kafka.GetConfigFileName(): + op, err = compareAndOp[config.Kafka](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.LocalCache.GetConfigFileName(): + op, err = compareAndOp[config.LocalCache](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Log.GetConfigFileName(): + op, err = compareAndOp[config.Log](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Minio.GetConfigFileName(): + op, err = compareAndOp[config.Minio](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Mongo.GetConfigFileName(): + op, err = compareAndOp[config.Mongo](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Notification.GetConfigFileName(): + op, err = compareAndOp[config.Notification](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.API.GetConfigFileName(): + op, err = compareAndOp[config.API](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.CronTask.GetConfigFileName(): + op, err = compareAndOp[config.CronTask](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.MsgGateway.GetConfigFileName(): + op, err = compareAndOp[config.MsgGateway](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.MsgTransfer.GetConfigFileName(): + op, err = compareAndOp[config.MsgTransfer](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Push.GetConfigFileName(): + op, err = compareAndOp[config.Push](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Auth.GetConfigFileName(): + op, err = compareAndOp[config.Auth](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Conversation.GetConfigFileName(): + op, err = compareAndOp[config.Conversation](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Friend.GetConfigFileName(): + op, err = compareAndOp[config.Friend](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Group.GetConfigFileName(): + op, err = compareAndOp[config.Group](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Msg.GetConfigFileName(): + op, err = compareAndOp[config.Msg](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Third.GetConfigFileName(): + op, err = compareAndOp[config.Third](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.User.GetConfigFileName(): + op, err = compareAndOp[config.User](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Redis.GetConfigFileName(): + op, err = compareAndOp[config.Redis](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Share.GetConfigFileName(): + op, err = compareAndOp[config.Share](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + case cm.config.Webhooks.GetConfigFileName(): + op, err = compareAndOp[config.Webhooks](c, cm.config.Name2Config(cf.ConfigName), &cf, cm) + default: + apiresp.GinError(c, errs.ErrArgs.Wrap()) + return + } + if err != nil { + apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) + return + } + if op != nil { + ops = append(ops, op) + } + } + if len(ops) > 0 { + tx := cm.client.Txn(c) + if _, err = tx.Then(datautil.Batch(func(op *clientv3.Op) clientv3.Op { return *op }, ops)...).Commit(); err != nil { + apiresp.GinError(c, errs.WrapMsg(err, "save to etcd failed")) + return + } + + } + + apiresp.GinSuccess(c, nil) +} + +func compareAndOp[T any](c *gin.Context, old any, req *apistruct.SetConfigReq, cm *ConfigManager) (*clientv3.Op, error) { + conf := new(T) + err := json.Unmarshal([]byte(req.Data), &conf) + if err != nil { + return nil, errs.ErrArgs.WithDetail(err.Error()).Wrap() + } + eq := reflect.DeepEqual(old, conf) + if eq { + return nil, nil + } + data, err := json.Marshal(conf) + if err != nil { + return nil, errs.ErrArgs.WithDetail(err.Error()).Wrap() + } + op := clientv3.OpPut(etcd.BuildKey(req.ConfigName), string(data)) + return &op, nil +} + func compareAndSave[T any](c *gin.Context, old any, req *apistruct.SetConfigReq, cm *ConfigManager) error { conf := new(T) err := json.Unmarshal([]byte(req.Data), &conf) diff --git a/internal/api/router.go b/internal/api/router.go index ebaff019c..850c23972 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -307,6 +307,7 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin configGroup.POST("/get_config_list", cm.GetConfigList) configGroup.POST("/get_config", cm.GetConfig) configGroup.POST("/set_config", cm.SetConfig) + configGroup.POST("/set_configs", cm.SetConfigs) configGroup.POST("/reset_config", cm.ResetConfig) configGroup.POST("/set_enable_config_manager", cm.SetEnableConfigManager) configGroup.POST("/get_enable_config_manager", cm.GetEnableConfigManager) diff --git a/pkg/apistruct/config_manager.go b/pkg/apistruct/config_manager.go index 9b8641c9d..ca5683a3e 100644 --- a/pkg/apistruct/config_manager.go +++ b/pkg/apistruct/config_manager.go @@ -15,6 +15,10 @@ type SetConfigReq struct { Data string `json:"data"` } +type SetConfigsReq struct { + Configs []SetConfigReq `json:"configs"` +} + type SetEnableConfigManagerReq struct { Enable bool `json:"enable"` } From 3516f843dbd0d9d421436fc10b6af8f8a0bf2427 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:17:50 +0800 Subject: [PATCH 10/59] fix: AdminToken save to redis && limit 1 for each userID (#3224) --- internal/rpc/user/user.go | 4 +-- pkg/common/storage/controller/auth.go | 45 ++++++++++++--------------- pkg/common/storage/controller/user.go | 5 ++- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index 07e3c6201..cd239aae3 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -88,7 +88,7 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr users := make([]*tablerelation.User, 0) for _, v := range config.Share.IMAdminUserID { - users = append(users, &tablerelation.User{UserID: v, Nickname: v, AppMangerLevel: constant.AppNotificationAdmin}) + users = append(users, &tablerelation.User{UserID: v, Nickname: v, AppMangerLevel: constant.AppAdmin}) } userDB, err := mgo.NewUserMongo(mgocli.GetDB()) if err != nil { @@ -605,7 +605,7 @@ func (s *userServer) GetNotificationAccount(ctx context.Context, req *pbuser.Get if err != nil { return nil, servererrs.ErrUserIDNotFound.Wrap() } - if user.AppMangerLevel == constant.AppAdmin || user.AppMangerLevel >= constant.AppNotificationAdmin { + if user.AppMangerLevel >= constant.AppAdmin { return &pbuser.GetNotificationAccountResp{Account: &pbuser.NotificationAccountInfo{ UserID: user.UserID, FaceURL: user.FaceURL, diff --git a/pkg/common/storage/controller/auth.go b/pkg/common/storage/controller/auth.go index ee2a06f53..013d8b155 100644 --- a/pkg/common/storage/controller/auth.go +++ b/pkg/common/storage/controller/auth.go @@ -80,31 +80,28 @@ func (a *authDatabase) BatchSetTokenMapByUidPid(ctx context.Context, tokens []st // Create Token. func (a *authDatabase) CreateToken(ctx context.Context, userID string, platformID int) (string, error) { - isAdmin := authverify.IsManagerUserID(userID, a.adminUserIDs) - if !isAdmin { - tokens, err := a.cache.GetAllTokensWithoutError(ctx, userID) - if err != nil { - return "", err - } + tokens, err := a.cache.GetAllTokensWithoutError(ctx, userID) + if err != nil { + return "", err + } - deleteTokenKey, kickedTokenKey, err := a.checkToken(ctx, tokens, platformID) + deleteTokenKey, kickedTokenKey, err := a.checkToken(ctx, tokens, platformID) + if err != nil { + return "", err + } + if len(deleteTokenKey) != 0 { + err = a.cache.DeleteTokenByUidPid(ctx, userID, platformID, deleteTokenKey) if err != nil { return "", err } - if len(deleteTokenKey) != 0 { - err = a.cache.DeleteTokenByUidPid(ctx, userID, platformID, deleteTokenKey) + } + if len(kickedTokenKey) != 0 { + for _, k := range kickedTokenKey { + err := a.cache.SetTokenFlagEx(ctx, userID, platformID, k, constant.KickedToken) if err != nil { return "", err } - } - if len(kickedTokenKey) != 0 { - for _, k := range kickedTokenKey { - err := a.cache.SetTokenFlagEx(ctx, userID, platformID, k, constant.KickedToken) - if err != nil { - return "", err - } - log.ZDebug(ctx, "kicked token in create token", "token", k) - } + log.ZDebug(ctx, "kicked token in create token", "token", k) } } @@ -115,10 +112,8 @@ func (a *authDatabase) CreateToken(ctx context.Context, userID string, platformI return "", errs.WrapMsg(err, "token.SignedString") } - if !isAdmin { - if err = a.cache.SetTokenFlagEx(ctx, userID, platformID, tokenString, constant.NormalToken); err != nil { - return "", err - } + if err = a.cache.SetTokenFlagEx(ctx, userID, platformID, tokenString, constant.NormalToken); err != nil { + return "", err } return tokenString, nil @@ -224,9 +219,6 @@ func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string } //var adminTokenMaxNum = a.multiLogin.MaxNumOneEnd - //if a.multiLogin.Policy == constant.Customize { - // adminTokenMaxNum = a.multiLogin.CustomizeLoginNum[constant.AdminPlatformID] - //} //l := len(adminToken) //if platformID == constant.AdminPlatformID { // l++ @@ -234,5 +226,8 @@ func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string //if l > adminTokenMaxNum { // kickToken = append(kickToken, adminToken[:l-adminTokenMaxNum]...) //} + if platformID == constant.AdminPlatformID { + kickToken = append(kickToken, adminToken...) + } return deleteToken, kickToken, nil } diff --git a/pkg/common/storage/controller/user.go b/pkg/common/storage/controller/user.go index a8ef1033e..f97c5330b 100644 --- a/pkg/common/storage/controller/user.go +++ b/pkg/common/storage/controller/user.go @@ -21,12 +21,11 @@ import ( "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/protocol/constant" + "github.com/openimsdk/protocol/user" "github.com/openimsdk/tools/db/pagination" "github.com/openimsdk/tools/db/tx" - "github.com/openimsdk/tools/utils/datautil" - - "github.com/openimsdk/protocol/user" "github.com/openimsdk/tools/errs" + "github.com/openimsdk/tools/utils/datautil" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" ) From 0b9dbd301c211c6ec27bcda9f7aaa311fe5064c5 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Thu, 13 Mar 2025 18:21:48 +0800 Subject: [PATCH 11/59] feat: check if the secret in config/share.yml has been changed during registration (#3223) * feat: check if the secret in config/share.yml has been changed during registration. * fix: cicd * fix: code * fix: cicd * fix: cicd * fix: cicd * fix: cicd * fix: cicd --- .github/workflows/go-build-test.yml | 25 ++++++++++++++++++++----- internal/rpc/user/user.go | 8 ++++++++ pkg/common/servererrs/code.go | 14 ++++++++------ pkg/common/servererrs/predefine.go | 2 ++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.github/workflows/go-build-test.yml b/.github/workflows/go-build-test.yml index 4033603e6..9e2aa3f1c 100644 --- a/.github/workflows/go-build-test.yml +++ b/.github/workflows/go-build-test.yml @@ -12,6 +12,10 @@ jobs: go-build: name: Test with go ${{ matrix.go_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} + + env: + SHARE_CONFIG_PATH: config/share.yml + permissions: contents: write pull-requests: write @@ -40,6 +44,10 @@ jobs: with: compose-file: "./docker-compose.yml" + - name: Modify Server Configuration + run: | + yq e '.secret = 123456' -i ${{ env.SHARE_CONFIG_PATH }} + # - name: Get Internal IP Address # id: get-ip # run: | @@ -71,6 +79,11 @@ jobs: go mod download go install github.com/magefile/mage@latest + - name: Modify Chat Configuration + run: | + cd ${{ github.workspace }}/chat-repo + yq e '.openIM.secret = 123456' -i ${{ env.SHARE_CONFIG_PATH }} + - name: Build and test Chat Services run: | cd ${{ github.workspace }}/chat-repo @@ -132,7 +145,7 @@ jobs: # Test get admin token get_admin_token_response=$(curl -X POST -H "Content-Type: application/json" -H "operationID: imAdmin" -d '{ - "secret": "openIM123", + "secret": "123456", "platformID": 2, "userID": "imAdmin" }' http://127.0.0.1:10002/auth/get_admin_token) @@ -169,7 +182,8 @@ jobs: contents: write env: SDK_DIR: openim-sdk-core - CONFIG_PATH: config/notification.yml + NOTIFICATION_CONFIG_PATH: config/notification.yml + SHARE_CONFIG_PATH: config/share.yml strategy: matrix: @@ -184,7 +198,7 @@ jobs: uses: actions/checkout@v4 with: repository: "openimsdk/openim-sdk-core" - ref: "release-v3.8" + ref: "main" path: ${{ env.SDK_DIR }} - name: Set up Go ${{ matrix.go_version }} @@ -199,8 +213,9 @@ jobs: - name: Modify Server Configuration run: | - yq e '.groupCreated.isSendMsg = true' -i ${{ env.CONFIG_PATH }} - yq e '.friendApplicationApproved.isSendMsg = true' -i ${{ env.CONFIG_PATH }} + yq e '.groupCreated.isSendMsg = true' -i ${{ env.NOTIFICATION_CONFIG_PATH }} + yq e '.friendApplicationApproved.isSendMsg = true' -i ${{ env.NOTIFICATION_CONFIG_PATH }} + yq e '.secret = 123456' -i ${{ env.SHARE_CONFIG_PATH }} - name: Start Server Services run: | diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index cd239aae3..3e8ec3537 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -49,6 +49,10 @@ import ( "google.golang.org/grpc" ) +const ( + defaultSecret = "openIM123" +) + type userServer struct { pbuser.UnimplementedUserServer online cache.OnlineCache @@ -273,6 +277,10 @@ func (s *userServer) UserRegister(ctx context.Context, req *pbuser.UserRegisterR if len(req.Users) == 0 { return nil, errs.ErrArgs.WrapMsg("users is empty") } + // check if secret is changed + if s.config.Share.Secret == defaultSecret { + return nil, servererrs.ErrSecretNotChanged.Wrap() + } if err = authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { return nil, err diff --git a/pkg/common/servererrs/code.go b/pkg/common/servererrs/code.go index 3d0aa4a71..906f890a5 100644 --- a/pkg/common/servererrs/code.go +++ b/pkg/common/servererrs/code.go @@ -37,7 +37,8 @@ const ( // General error codes. const ( - NoError = 0 // No error + NoError = 0 // No error + DatabaseError = 90002 // Database error (redis/mysql, etc.) NetworkError = 90004 // Network error DataError = 90007 // Data error @@ -45,11 +46,12 @@ const ( CallbackError = 80000 // General error codes. - ServerInternalError = 500 // Server internal error - ArgsError = 1001 // Input parameter error - NoPermissionError = 1002 // Insufficient permission - DuplicateKeyError = 1003 - RecordNotFoundError = 1004 // Record does not exist + ServerInternalError = 500 // Server internal error + ArgsError = 1001 // Input parameter error + NoPermissionError = 1002 // Insufficient permission + DuplicateKeyError = 1003 + RecordNotFoundError = 1004 // Record does not exist + SecretNotChangedError = 1050 // secret not changed // Account error codes. UserIDNotFoundError = 1101 // UserID does not exist or is not registered diff --git a/pkg/common/servererrs/predefine.go b/pkg/common/servererrs/predefine.go index ab09aa512..b1d6b06a9 100644 --- a/pkg/common/servererrs/predefine.go +++ b/pkg/common/servererrs/predefine.go @@ -17,6 +17,8 @@ package servererrs import "github.com/openimsdk/tools/errs" var ( + ErrSecretNotChanged = errs.NewCodeError(SecretNotChangedError, "secret not changed, please change secret in config/share.yml for security reasons") + ErrDatabase = errs.NewCodeError(DatabaseError, "DatabaseError") ErrNetwork = errs.NewCodeError(NetworkError, "NetworkError") ErrCallback = errs.NewCodeError(CallbackError, "CallbackError") From b969827b9af9120b28f267b7f600e3b97c928e59 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Fri, 14 Mar 2025 16:46:29 +0800 Subject: [PATCH 12/59] feat: Implement webhook in createConversation (#3228) * update test method args. * feat: implement createConversations webhook function. * improve webhookCreateConversations Implement * implement createconversation webhook. * remove unused paramaters. --- config/webhooks.yml | 20 ++- internal/rpc/conversation/callback.go | 117 ++++++++++++++++++ internal/rpc/conversation/conversation.go | 73 ++++++++--- internal/tools/cron/cron_test.go | 2 +- pkg/callbackstruct/constant.go | 98 ++++++++------- pkg/callbackstruct/conversation.go | 91 ++++++++++++++ pkg/common/cmd/conversation.go | 2 + pkg/common/config/config.go | 102 +++++++-------- pkg/common/config/load_config_test.go | 16 +-- pkg/common/storage/controller/conversation.go | 19 ++- 10 files changed, 410 insertions(+), 130 deletions(-) create mode 100644 internal/rpc/conversation/callback.go create mode 100644 pkg/callbackstruct/conversation.go diff --git a/config/webhooks.yml b/config/webhooks.yml index 41c60e7e2..283a23ed4 100644 --- a/config/webhooks.yml +++ b/config/webhooks.yml @@ -7,11 +7,11 @@ beforeSendSingleMsg: # If not set, all contentType messages will through this filter. deniedTypes: [] beforeUpdateUserInfoEx: - enable: false + enable: false timeout: 5 failedContinue: true afterUpdateUserInfoEx: - enable: false + enable: false timeout: 5 afterSendSingleMsg: enable: false @@ -181,3 +181,19 @@ afterImportFriends: afterRemoveBlack: enable: false timeout: 5 +beforeCreateSingleChatConversations: + enable: false + timeout: 5 + failedContinue: false +afterCreateSingleChatConversations: + enable: false + timeout: 5 + failedContinue: false +beforeCreateGroupChatConversations: + enable: false + timeout: 5 + failedContinue: false +afterCreateGroupChatConversations: + enable: false + timeout: 5 + failedContinue: false diff --git a/internal/rpc/conversation/callback.go b/internal/rpc/conversation/callback.go new file mode 100644 index 000000000..93e925afd --- /dev/null +++ b/internal/rpc/conversation/callback.go @@ -0,0 +1,117 @@ +package conversation + +import ( + "context" + + "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" + "github.com/openimsdk/open-im-server/v3/pkg/common/config" + dbModel "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" + "github.com/openimsdk/tools/utils/datautil" +) + +func (c *conversationServer) webhookBeforeCreateSingleChatConversations(ctx context.Context, before *config.BeforeConfig, req *dbModel.Conversation) error { + return webhook.WithCondition(ctx, before, func(ctx context.Context) error { + cbReq := &callbackstruct.CallbackBeforeCreateSingleChatConversationsReq{ + CallbackCommand: callbackstruct.CallbackBeforeCreateSingleChatConversationsCommand, + OwnerUserID: req.OwnerUserID, + ConversationID: req.ConversationID, + ConversationType: req.ConversationType, + UserID: req.UserID, + RecvMsgOpt: req.RecvMsgOpt, + IsPinned: req.IsPinned, + IsPrivateChat: req.IsPrivateChat, + BurnDuration: req.BurnDuration, + GroupAtType: req.GroupAtType, + AttachedInfo: req.AttachedInfo, + Ex: req.Ex, + } + + resp := &callbackstruct.CallbackBeforeCreateSingleChatConversationsResp{} + + if err := c.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil { + return err + } + + datautil.NotNilReplace(&req.RecvMsgOpt, resp.RecvMsgOpt) + datautil.NotNilReplace(&req.IsPinned, resp.IsPinned) + datautil.NotNilReplace(&req.IsPrivateChat, resp.IsPrivateChat) + datautil.NotNilReplace(&req.BurnDuration, resp.BurnDuration) + datautil.NotNilReplace(&req.GroupAtType, resp.GroupAtType) + datautil.NotNilReplace(&req.AttachedInfo, resp.AttachedInfo) + datautil.NotNilReplace(&req.Ex, resp.Ex) + return nil + }) +} + +func (c *conversationServer) webhookAfterCreateSingleChatConversations(ctx context.Context, after *config.AfterConfig, req *dbModel.Conversation) error { + cbReq := &callbackstruct.CallbackAfterCreateSingleChatConversationsReq{ + CallbackCommand: callbackstruct.CallbackAfterCreateSingleChatConversationsCommand, + OwnerUserID: req.OwnerUserID, + ConversationID: req.ConversationID, + ConversationType: req.ConversationType, + UserID: req.UserID, + RecvMsgOpt: req.RecvMsgOpt, + IsPinned: req.IsPinned, + IsPrivateChat: req.IsPrivateChat, + BurnDuration: req.BurnDuration, + GroupAtType: req.GroupAtType, + AttachedInfo: req.AttachedInfo, + Ex: req.Ex, + } + + c.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterCreateSingleChatConversationsResp{}, after) + return nil +} + +func (c *conversationServer) webhookBeforeCreateGroupChatConversations(ctx context.Context, before *config.BeforeConfig, req *dbModel.Conversation) error { + return webhook.WithCondition(ctx, before, func(ctx context.Context) error { + cbReq := &callbackstruct.CallbackBeforeCreateGroupChatConversationsReq{ + CallbackCommand: callbackstruct.CallbackBeforeCreateGroupChatConversationsCommand, + ConversationID: req.ConversationID, + ConversationType: req.ConversationType, + GroupID: req.GroupID, + RecvMsgOpt: req.RecvMsgOpt, + IsPinned: req.IsPinned, + IsPrivateChat: req.IsPrivateChat, + BurnDuration: req.BurnDuration, + GroupAtType: req.GroupAtType, + AttachedInfo: req.AttachedInfo, + Ex: req.Ex, + } + + resp := &callbackstruct.CallbackBeforeCreateGroupChatConversationsResp{} + + if err := c.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil { + return err + } + + datautil.NotNilReplace(&req.RecvMsgOpt, resp.RecvMsgOpt) + datautil.NotNilReplace(&req.IsPinned, resp.IsPinned) + datautil.NotNilReplace(&req.IsPrivateChat, resp.IsPrivateChat) + datautil.NotNilReplace(&req.BurnDuration, resp.BurnDuration) + datautil.NotNilReplace(&req.GroupAtType, resp.GroupAtType) + datautil.NotNilReplace(&req.AttachedInfo, resp.AttachedInfo) + datautil.NotNilReplace(&req.Ex, resp.Ex) + return nil + }) +} + +func (c *conversationServer) webhookAfterCreateGroupChatConversations(ctx context.Context, after *config.AfterConfig, req *dbModel.Conversation) error { + cbReq := &callbackstruct.CallbackAfterCreateGroupChatConversationsReq{ + CallbackCommand: callbackstruct.CallbackAfterCreateGroupChatConversationsCommand, + ConversationID: req.ConversationID, + ConversationType: req.ConversationType, + GroupID: req.GroupID, + RecvMsgOpt: req.RecvMsgOpt, + IsPinned: req.IsPinned, + IsPrivateChat: req.IsPrivateChat, + BurnDuration: req.BurnDuration, + GroupAtType: req.GroupAtType, + AttachedInfo: req.AttachedInfo, + Ex: req.Ex, + } + + c.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterCreateGroupChatConversationsResp{}, after) + return nil +} diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index 165d1bdc2..8bc3fd2a9 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -30,6 +30,7 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" dbModel "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" "github.com/openimsdk/open-im-server/v3/pkg/localcache" "github.com/openimsdk/open-im-server/v3/pkg/msgprocessor" "github.com/openimsdk/protocol/constant" @@ -49,9 +50,10 @@ type conversationServer struct { conversationNotificationSender *ConversationNotificationSender config *Config - userClient *rpcli.UserClient - msgClient *rpcli.MsgClient - groupClient *rpcli.GroupClient + webhookClient *webhook.Client + userClient *rpcli.UserClient + msgClient *rpcli.MsgClient + groupClient *rpcli.GroupClient } type Config struct { @@ -60,6 +62,7 @@ type Config struct { MongodbConfig config.Mongo NotificationConfig config.Notification Share config.Share + WebhooksConfig config.Webhooks LocalCacheConfig config.LocalCache Discovery config.Discovery } @@ -90,16 +93,25 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr if err != nil { return err } + msgClient := rpcli.NewMsgClient(msgConn) + + cs := conversationServer{ + config: config, + webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL), + userClient: rpcli.NewUserClient(userConn), + groupClient: rpcli.NewGroupClient(groupConn), + msgClient: msgClient, + } + + cs.conversationNotificationSender = NewConversationNotificationSender(&config.NotificationConfig, msgClient) + cs.conversationDatabase = controller.NewConversationDatabase( + conversationDB, + redis.NewConversationRedis(rdb, &config.LocalCacheConfig, conversationDB), + mgocli.GetTx()) + localcache.InitLocalCache(&config.LocalCacheConfig) - pbconversation.RegisterConversationServer(server, &conversationServer{ - conversationNotificationSender: NewConversationNotificationSender(&config.NotificationConfig, msgClient), - conversationDatabase: controller.NewConversationDatabase(conversationDB, - redis.NewConversationRedis(rdb, &config.LocalCacheConfig, conversationDB), mgocli.GetTx()), - userClient: rpcli.NewUserClient(userConn), - groupClient: rpcli.NewGroupClient(groupConn), - msgClient: msgClient, - }) + pbconversation.RegisterConversationServer(server, &cs) return nil } @@ -326,49 +338,76 @@ func (c *conversationServer) GetRecvMsgNotNotifyUserIDs(ctx context.Context, req func (c *conversationServer) CreateSingleChatConversations(ctx context.Context, req *pbconversation.CreateSingleChatConversationsReq, ) (*pbconversation.CreateSingleChatConversationsResp, error) { + var conversation dbModel.Conversation switch req.ConversationType { case constant.SingleChatType: - var conversation dbModel.Conversation + // sendUser create conversation.ConversationID = req.ConversationID conversation.ConversationType = req.ConversationType conversation.OwnerUserID = req.SendID conversation.UserID = req.RecvID + if err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue { + return nil, err + } + err := c.conversationDatabase.CreateConversation(ctx, []*dbModel.Conversation{&conversation}) if err != nil { log.ZWarn(ctx, "create conversation failed", err, "conversation", conversation) } + c.webhookAfterCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateSingleChatConversations, &conversation) + + // recvUser create conversation2 := conversation conversation2.OwnerUserID = req.RecvID conversation2.UserID = req.SendID + if err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue { + return nil, err + } + err = c.conversationDatabase.CreateConversation(ctx, []*dbModel.Conversation{&conversation2}) if err != nil { log.ZWarn(ctx, "create conversation failed", err, "conversation2", conversation) } + + c.webhookAfterCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateSingleChatConversations, &conversation2) case constant.NotificationChatType: - var conversation dbModel.Conversation conversation.ConversationID = req.ConversationID conversation.ConversationType = req.ConversationType conversation.OwnerUserID = req.RecvID conversation.UserID = req.SendID + if err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue { + return nil, err + } + err := c.conversationDatabase.CreateConversation(ctx, []*dbModel.Conversation{&conversation}) if err != nil { log.ZWarn(ctx, "create conversation failed", err, "conversation2", conversation) } + + c.webhookAfterCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateSingleChatConversations, &conversation) } return &pbconversation.CreateSingleChatConversationsResp{}, nil } func (c *conversationServer) CreateGroupChatConversations(ctx context.Context, req *pbconversation.CreateGroupChatConversationsReq) (*pbconversation.CreateGroupChatConversationsResp, error) { - err := c.conversationDatabase.CreateGroupChatConversation(ctx, req.GroupID, req.UserIDs) - if err != nil { + var conversation dbModel.Conversation + + conversation.ConversationID = msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupID) + conversation.GroupID = req.GroupID + conversation.ConversationType = constant.ReadGroupChatType + + if err := c.webhookBeforeCreateGroupChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateGroupChatConversations, &conversation); err != nil { return nil, err } - conversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupID) - if err := c.msgClient.SetUserConversationMaxSeq(ctx, conversationID, req.UserIDs, 0); err != nil { + + err := c.conversationDatabase.CreateGroupChatConversation(ctx, req.GroupID, req.UserIDs, &conversation) + if err != nil { return nil, err } + + c.webhookAfterCreateGroupChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateGroupChatConversations, &conversation) return &pbconversation.CreateGroupChatConversationsResp{}, nil } diff --git a/internal/tools/cron/cron_test.go b/internal/tools/cron/cron_test.go index b98b14f14..af827fc38 100644 --- a/internal/tools/cron/cron_test.go +++ b/internal/tools/cron/cron_test.go @@ -46,7 +46,7 @@ func TestName(t *testing.T) { srv := &cronServer{ ctx: ctx, - config: &CronTaskConfig{ + config: &Config{ CronTask: config.CronTask{ RetainChatRecords: 1, FileExpireTime: 1, diff --git a/pkg/callbackstruct/constant.go b/pkg/callbackstruct/constant.go index 73f89a719..bbc34e71f 100644 --- a/pkg/callbackstruct/constant.go +++ b/pkg/callbackstruct/constant.go @@ -15,51 +15,55 @@ package callbackstruct const ( - CallbackBeforeInviteJoinGroupCommand = "callbackBeforeInviteJoinGroupCommand" - CallbackAfterJoinGroupCommand = "callbackAfterJoinGroupCommand" - CallbackAfterSetGroupInfoCommand = "callbackAfterSetGroupInfoCommand" - CallbackAfterSetGroupInfoExCommand = "callbackAfterSetGroupInfoExCommand" - CallbackBeforeSetGroupInfoCommand = "callbackBeforeSetGroupInfoCommand" - CallbackBeforeSetGroupInfoExCommand = "callbackBeforeSetGroupInfoExCommand" - CallbackAfterRevokeMsgCommand = "callbackBeforeAfterMsgCommand" - CallbackBeforeAddBlackCommand = "callbackBeforeAddBlackCommand" - CallbackAfterAddFriendCommand = "callbackAfterAddFriendCommand" - CallbackBeforeAddFriendAgreeCommand = "callbackBeforeAddFriendAgreeCommand" - CallbackAfterAddFriendAgreeCommand = "callbackAfterAddFriendAgreeCommand" - CallbackAfterDeleteFriendCommand = "callbackAfterDeleteFriendCommand" - CallbackBeforeImportFriendsCommand = "callbackBeforeImportFriendsCommand" - CallbackAfterImportFriendsCommand = "callbackAfterImportFriendsCommand" - CallbackAfterRemoveBlackCommand = "callbackAfterRemoveBlackCommand" - CallbackAfterQuitGroupCommand = "callbackAfterQuitGroupCommand" - CallbackAfterKickGroupCommand = "callbackAfterKickGroupCommand" - CallbackAfterDisMissGroupCommand = "callbackAfterDisMissGroupCommand" - CallbackBeforeJoinGroupCommand = "callbackBeforeJoinGroupCommand" - CallbackAfterGroupMsgReadCommand = "callbackAfterGroupMsgReadCommand" - CallbackBeforeMsgModifyCommand = "callbackBeforeMsgModifyCommand" - CallbackAfterUpdateUserInfoCommand = "callbackAfterUpdateUserInfoCommand" - CallbackAfterUpdateUserInfoExCommand = "callbackAfterUpdateUserInfoExCommand" - CallbackBeforeUpdateUserInfoExCommand = "callbackBeforeUpdateUserInfoExCommand" - CallbackBeforeUserRegisterCommand = "callbackBeforeUserRegisterCommand" - CallbackAfterUserRegisterCommand = "callbackAfterUserRegisterCommand" - CallbackAfterTransferGroupOwnerCommand = "callbackAfterTransferGroupOwnerCommand" - CallbackBeforeSetFriendRemarkCommand = "callbackBeforeSetFriendRemarkCommand" - CallbackAfterSetFriendRemarkCommand = "callbackAfterSetFriendRemarkCommand" - CallbackAfterSingleMsgReadCommand = "callbackAfterSingleMsgReadCommand" - CallbackBeforeSendSingleMsgCommand = "callbackBeforeSendSingleMsgCommand" - CallbackAfterSendSingleMsgCommand = "callbackAfterSendSingleMsgCommand" - CallbackBeforeSendGroupMsgCommand = "callbackBeforeSendGroupMsgCommand" - CallbackAfterSendGroupMsgCommand = "callbackAfterSendGroupMsgCommand" - CallbackAfterUserOnlineCommand = "callbackAfterUserOnlineCommand" - CallbackAfterUserOfflineCommand = "callbackAfterUserOfflineCommand" - CallbackAfterUserKickOffCommand = "callbackAfterUserKickOffCommand" - CallbackBeforeOfflinePushCommand = "callbackBeforeOfflinePushCommand" - CallbackBeforeOnlinePushCommand = "callbackBeforeOnlinePushCommand" - CallbackBeforeGroupOnlinePushCommand = "callbackBeforeGroupOnlinePushCommand" - CallbackBeforeAddFriendCommand = "callbackBeforeAddFriendCommand" - CallbackBeforeUpdateUserInfoCommand = "callbackBeforeUpdateUserInfoCommand" - CallbackBeforeCreateGroupCommand = "callbackBeforeCreateGroupCommand" - CallbackAfterCreateGroupCommand = "callbackAfterCreateGroupCommand" - CallbackBeforeMembersJoinGroupCommand = "callbackBeforeMembersJoinGroupCommand" - CallbackBeforeSetGroupMemberInfoCommand = "callbackBeforeSetGroupMemberInfoCommand" - CallbackAfterSetGroupMemberInfoCommand = "callbackAfterSetGroupMemberInfoCommand" + CallbackBeforeInviteJoinGroupCommand = "callbackBeforeInviteJoinGroupCommand" + CallbackAfterJoinGroupCommand = "callbackAfterJoinGroupCommand" + CallbackAfterSetGroupInfoCommand = "callbackAfterSetGroupInfoCommand" + CallbackAfterSetGroupInfoExCommand = "callbackAfterSetGroupInfoExCommand" + CallbackBeforeSetGroupInfoCommand = "callbackBeforeSetGroupInfoCommand" + CallbackBeforeSetGroupInfoExCommand = "callbackBeforeSetGroupInfoExCommand" + CallbackAfterRevokeMsgCommand = "callbackBeforeAfterMsgCommand" + CallbackBeforeAddBlackCommand = "callbackBeforeAddBlackCommand" + CallbackAfterAddFriendCommand = "callbackAfterAddFriendCommand" + CallbackBeforeAddFriendAgreeCommand = "callbackBeforeAddFriendAgreeCommand" + CallbackAfterAddFriendAgreeCommand = "callbackAfterAddFriendAgreeCommand" + CallbackAfterDeleteFriendCommand = "callbackAfterDeleteFriendCommand" + CallbackBeforeImportFriendsCommand = "callbackBeforeImportFriendsCommand" + CallbackAfterImportFriendsCommand = "callbackAfterImportFriendsCommand" + CallbackAfterRemoveBlackCommand = "callbackAfterRemoveBlackCommand" + CallbackAfterQuitGroupCommand = "callbackAfterQuitGroupCommand" + CallbackAfterKickGroupCommand = "callbackAfterKickGroupCommand" + CallbackAfterDisMissGroupCommand = "callbackAfterDisMissGroupCommand" + CallbackBeforeJoinGroupCommand = "callbackBeforeJoinGroupCommand" + CallbackAfterGroupMsgReadCommand = "callbackAfterGroupMsgReadCommand" + CallbackBeforeMsgModifyCommand = "callbackBeforeMsgModifyCommand" + CallbackAfterUpdateUserInfoCommand = "callbackAfterUpdateUserInfoCommand" + CallbackAfterUpdateUserInfoExCommand = "callbackAfterUpdateUserInfoExCommand" + CallbackBeforeUpdateUserInfoExCommand = "callbackBeforeUpdateUserInfoExCommand" + CallbackBeforeUserRegisterCommand = "callbackBeforeUserRegisterCommand" + CallbackAfterUserRegisterCommand = "callbackAfterUserRegisterCommand" + CallbackAfterTransferGroupOwnerCommand = "callbackAfterTransferGroupOwnerCommand" + CallbackBeforeSetFriendRemarkCommand = "callbackBeforeSetFriendRemarkCommand" + CallbackAfterSetFriendRemarkCommand = "callbackAfterSetFriendRemarkCommand" + CallbackAfterSingleMsgReadCommand = "callbackAfterSingleMsgReadCommand" + CallbackBeforeSendSingleMsgCommand = "callbackBeforeSendSingleMsgCommand" + CallbackAfterSendSingleMsgCommand = "callbackAfterSendSingleMsgCommand" + CallbackBeforeSendGroupMsgCommand = "callbackBeforeSendGroupMsgCommand" + CallbackAfterSendGroupMsgCommand = "callbackAfterSendGroupMsgCommand" + CallbackAfterUserOnlineCommand = "callbackAfterUserOnlineCommand" + CallbackAfterUserOfflineCommand = "callbackAfterUserOfflineCommand" + CallbackAfterUserKickOffCommand = "callbackAfterUserKickOffCommand" + CallbackBeforeOfflinePushCommand = "callbackBeforeOfflinePushCommand" + CallbackBeforeOnlinePushCommand = "callbackBeforeOnlinePushCommand" + CallbackBeforeGroupOnlinePushCommand = "callbackBeforeGroupOnlinePushCommand" + CallbackBeforeAddFriendCommand = "callbackBeforeAddFriendCommand" + CallbackBeforeUpdateUserInfoCommand = "callbackBeforeUpdateUserInfoCommand" + CallbackBeforeCreateGroupCommand = "callbackBeforeCreateGroupCommand" + CallbackAfterCreateGroupCommand = "callbackAfterCreateGroupCommand" + CallbackBeforeMembersJoinGroupCommand = "callbackBeforeMembersJoinGroupCommand" + CallbackBeforeSetGroupMemberInfoCommand = "callbackBeforeSetGroupMemberInfoCommand" + CallbackAfterSetGroupMemberInfoCommand = "callbackAfterSetGroupMemberInfoCommand" + CallbackBeforeCreateSingleChatConversationsCommand = "callbackBeforeCreateSingleChatConversationsCommand" + CallbackAfterCreateSingleChatConversationsCommand = "callbackAfterCreateSingleChatConversationsCommand" + CallbackBeforeCreateGroupChatConversationsCommand = "callbackBeforeCreateGroupChatConversationsCommand" + CallbackAfterCreateGroupChatConversationsCommand = "callbackAfterCreateGroupChatConversationsCommand" ) diff --git a/pkg/callbackstruct/conversation.go b/pkg/callbackstruct/conversation.go new file mode 100644 index 000000000..14e78094c --- /dev/null +++ b/pkg/callbackstruct/conversation.go @@ -0,0 +1,91 @@ +package callbackstruct + +type CallbackBeforeCreateSingleChatConversationsReq struct { + CallbackCommand `json:"callbackCommand"` + OwnerUserID string `json:"owner_user_id"` + ConversationID string `json:"conversation_id"` + ConversationType int32 `json:"conversation_type"` + UserID string `json:"user_id"` + RecvMsgOpt int32 `json:"recv_msg_opt"` + IsPinned bool `json:"is_pinned"` + IsPrivateChat bool `json:"is_private_chat"` + BurnDuration int32 `json:"burn_duration"` + GroupAtType int32 `json:"group_at_type"` + AttachedInfo string `json:"attached_info"` + Ex string `json:"ex"` +} + +type CallbackBeforeCreateSingleChatConversationsResp struct { + CommonCallbackResp + RecvMsgOpt *int32 `json:"recv_msg_opt"` + IsPinned *bool `json:"is_pinned"` + IsPrivateChat *bool `json:"is_private_chat"` + BurnDuration *int32 `json:"burn_duration"` + GroupAtType *int32 `json:"group_at_type"` + AttachedInfo *string `json:"attached_info"` + Ex *string `json:"ex"` +} + +type CallbackAfterCreateSingleChatConversationsReq struct { + CallbackCommand `json:"callbackCommand"` + OwnerUserID string `json:"owner_user_id"` + ConversationID string `json:"conversation_id"` + ConversationType int32 `json:"conversation_type"` + UserID string `json:"user_id"` + RecvMsgOpt int32 `json:"recv_msg_opt"` + IsPinned bool `json:"is_pinned"` + IsPrivateChat bool `json:"is_private_chat"` + BurnDuration int32 `json:"burn_duration"` + GroupAtType int32 `json:"group_at_type"` + AttachedInfo string `json:"attached_info"` + Ex string `json:"ex"` +} + +type CallbackAfterCreateSingleChatConversationsResp struct { + CommonCallbackResp +} + +type CallbackBeforeCreateGroupChatConversationsReq struct { + CallbackCommand `json:"callbackCommand"` + OwnerUserID string `json:"owner_user_id"` + ConversationID string `json:"conversation_id"` + ConversationType int32 `json:"conversation_type"` + GroupID string `json:"group_id"` + RecvMsgOpt int32 `json:"recv_msg_opt"` + IsPinned bool `json:"is_pinned"` + IsPrivateChat bool `json:"is_private_chat"` + BurnDuration int32 `json:"burn_duration"` + GroupAtType int32 `json:"group_at_type"` + AttachedInfo string `json:"attached_info"` + Ex string `json:"ex"` +} + +type CallbackBeforeCreateGroupChatConversationsResp struct { + CommonCallbackResp + RecvMsgOpt *int32 `json:"recv_msg_opt"` + IsPinned *bool `json:"is_pinned"` + IsPrivateChat *bool `json:"is_private_chat"` + BurnDuration *int32 `json:"burn_duration"` + GroupAtType *int32 `json:"group_at_type"` + AttachedInfo *string `json:"attached_info"` + Ex *string `json:"ex"` +} + +type CallbackAfterCreateGroupChatConversationsReq struct { + CallbackCommand `json:"callbackCommand"` + OwnerUserID string `json:"owner_user_id"` + ConversationID string `json:"conversation_id"` + ConversationType int32 `json:"conversation_type"` + GroupID string `json:"group_id"` + RecvMsgOpt int32 `json:"recv_msg_opt"` + IsPinned bool `json:"is_pinned"` + IsPrivateChat bool `json:"is_private_chat"` + BurnDuration int32 `json:"burn_duration"` + GroupAtType int32 `json:"group_at_type"` + AttachedInfo string `json:"attached_info"` + Ex string `json:"ex"` +} + +type CallbackAfterCreateGroupChatConversationsResp struct { + CommonCallbackResp +} diff --git a/pkg/common/cmd/conversation.go b/pkg/common/cmd/conversation.go index 2f8769897..428c442da 100644 --- a/pkg/common/cmd/conversation.go +++ b/pkg/common/cmd/conversation.go @@ -41,6 +41,7 @@ func NewConversationRpcCmd() *ConversationRpcCmd { config.MongodbConfigFileName: &conversationConfig.MongodbConfig, config.ShareFileName: &conversationConfig.Share, config.NotificationFileName: &conversationConfig.NotificationConfig, + config.WebhooksConfigFileName: &conversationConfig.WebhooksConfig, config.LocalCacheConfigFileName: &conversationConfig.LocalCacheConfig, config.DiscoveryConfigFilename: &conversationConfig.Discovery, } @@ -67,6 +68,7 @@ func (a *ConversationRpcCmd) runE() error { a.conversationConfig.NotificationConfig.GetConfigFileName(), a.conversationConfig.Share.GetConfigFileName(), a.conversationConfig.LocalCacheConfig.GetConfigFileName(), + a.conversationConfig.WebhooksConfig.GetConfigFileName(), a.conversationConfig.Discovery.GetConfigFileName(), }, nil, conversation.Start) diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go index b47e3db68..fa93e3406 100644 --- a/pkg/common/config/config.go +++ b/pkg/common/config/config.go @@ -386,55 +386,59 @@ func (r *RpcService) GetServiceNames() []string { // FullConfig stores all configurations for before and after events type Webhooks struct { - URL string `yaml:"url"` - BeforeSendSingleMsg BeforeConfig `yaml:"beforeSendSingleMsg"` - BeforeUpdateUserInfoEx BeforeConfig `yaml:"beforeUpdateUserInfoEx"` - AfterUpdateUserInfoEx AfterConfig `yaml:"afterUpdateUserInfoEx"` - AfterSendSingleMsg AfterConfig `yaml:"afterSendSingleMsg"` - BeforeSendGroupMsg BeforeConfig `yaml:"beforeSendGroupMsg"` - BeforeMsgModify BeforeConfig `yaml:"beforeMsgModify"` - AfterSendGroupMsg AfterConfig `yaml:"afterSendGroupMsg"` - AfterUserOnline AfterConfig `yaml:"afterUserOnline"` - AfterUserOffline AfterConfig `yaml:"afterUserOffline"` - AfterUserKickOff AfterConfig `yaml:"afterUserKickOff"` - BeforeOfflinePush BeforeConfig `yaml:"beforeOfflinePush"` - BeforeOnlinePush BeforeConfig `yaml:"beforeOnlinePush"` - BeforeGroupOnlinePush BeforeConfig `yaml:"beforeGroupOnlinePush"` - BeforeAddFriend BeforeConfig `yaml:"beforeAddFriend"` - BeforeUpdateUserInfo BeforeConfig `yaml:"beforeUpdateUserInfo"` - AfterUpdateUserInfo AfterConfig `yaml:"afterUpdateUserInfo"` - BeforeCreateGroup BeforeConfig `yaml:"beforeCreateGroup"` - AfterCreateGroup AfterConfig `yaml:"afterCreateGroup"` - BeforeMemberJoinGroup BeforeConfig `yaml:"beforeMemberJoinGroup"` - BeforeSetGroupMemberInfo BeforeConfig `yaml:"beforeSetGroupMemberInfo"` - AfterSetGroupMemberInfo AfterConfig `yaml:"afterSetGroupMemberInfo"` - AfterQuitGroup AfterConfig `yaml:"afterQuitGroup"` - AfterKickGroupMember AfterConfig `yaml:"afterKickGroupMember"` - AfterDismissGroup AfterConfig `yaml:"afterDismissGroup"` - BeforeApplyJoinGroup BeforeConfig `yaml:"beforeApplyJoinGroup"` - AfterGroupMsgRead AfterConfig `yaml:"afterGroupMsgRead"` - AfterSingleMsgRead AfterConfig `yaml:"afterSingleMsgRead"` - BeforeUserRegister BeforeConfig `yaml:"beforeUserRegister"` - AfterUserRegister AfterConfig `yaml:"afterUserRegister"` - AfterTransferGroupOwner AfterConfig `yaml:"afterTransferGroupOwner"` - BeforeSetFriendRemark BeforeConfig `yaml:"beforeSetFriendRemark"` - AfterSetFriendRemark AfterConfig `yaml:"afterSetFriendRemark"` - AfterGroupMsgRevoke AfterConfig `yaml:"afterGroupMsgRevoke"` - AfterJoinGroup AfterConfig `yaml:"afterJoinGroup"` - BeforeInviteUserToGroup BeforeConfig `yaml:"beforeInviteUserToGroup"` - AfterSetGroupInfo AfterConfig `yaml:"afterSetGroupInfo"` - BeforeSetGroupInfo BeforeConfig `yaml:"beforeSetGroupInfo"` - AfterSetGroupInfoEx AfterConfig `yaml:"afterSetGroupInfoEx"` - BeforeSetGroupInfoEx BeforeConfig `yaml:"beforeSetGroupInfoEx"` - AfterRevokeMsg AfterConfig `yaml:"afterRevokeMsg"` - BeforeAddBlack BeforeConfig `yaml:"beforeAddBlack"` - AfterAddFriend AfterConfig `yaml:"afterAddFriend"` - BeforeAddFriendAgree BeforeConfig `yaml:"beforeAddFriendAgree"` - AfterAddFriendAgree AfterConfig `yaml:"afterAddFriendAgree"` - AfterDeleteFriend AfterConfig `yaml:"afterDeleteFriend"` - BeforeImportFriends BeforeConfig `yaml:"beforeImportFriends"` - AfterImportFriends AfterConfig `yaml:"afterImportFriends"` - AfterRemoveBlack AfterConfig `yaml:"afterRemoveBlack"` + URL string `yaml:"url"` + BeforeSendSingleMsg BeforeConfig `yaml:"beforeSendSingleMsg"` + BeforeUpdateUserInfoEx BeforeConfig `yaml:"beforeUpdateUserInfoEx"` + AfterUpdateUserInfoEx AfterConfig `yaml:"afterUpdateUserInfoEx"` + AfterSendSingleMsg AfterConfig `yaml:"afterSendSingleMsg"` + BeforeSendGroupMsg BeforeConfig `yaml:"beforeSendGroupMsg"` + BeforeMsgModify BeforeConfig `yaml:"beforeMsgModify"` + AfterSendGroupMsg AfterConfig `yaml:"afterSendGroupMsg"` + AfterUserOnline AfterConfig `yaml:"afterUserOnline"` + AfterUserOffline AfterConfig `yaml:"afterUserOffline"` + AfterUserKickOff AfterConfig `yaml:"afterUserKickOff"` + BeforeOfflinePush BeforeConfig `yaml:"beforeOfflinePush"` + BeforeOnlinePush BeforeConfig `yaml:"beforeOnlinePush"` + BeforeGroupOnlinePush BeforeConfig `yaml:"beforeGroupOnlinePush"` + BeforeAddFriend BeforeConfig `yaml:"beforeAddFriend"` + BeforeUpdateUserInfo BeforeConfig `yaml:"beforeUpdateUserInfo"` + AfterUpdateUserInfo AfterConfig `yaml:"afterUpdateUserInfo"` + BeforeCreateGroup BeforeConfig `yaml:"beforeCreateGroup"` + AfterCreateGroup AfterConfig `yaml:"afterCreateGroup"` + BeforeMemberJoinGroup BeforeConfig `yaml:"beforeMemberJoinGroup"` + BeforeSetGroupMemberInfo BeforeConfig `yaml:"beforeSetGroupMemberInfo"` + AfterSetGroupMemberInfo AfterConfig `yaml:"afterSetGroupMemberInfo"` + AfterQuitGroup AfterConfig `yaml:"afterQuitGroup"` + AfterKickGroupMember AfterConfig `yaml:"afterKickGroupMember"` + AfterDismissGroup AfterConfig `yaml:"afterDismissGroup"` + BeforeApplyJoinGroup BeforeConfig `yaml:"beforeApplyJoinGroup"` + AfterGroupMsgRead AfterConfig `yaml:"afterGroupMsgRead"` + AfterSingleMsgRead AfterConfig `yaml:"afterSingleMsgRead"` + BeforeUserRegister BeforeConfig `yaml:"beforeUserRegister"` + AfterUserRegister AfterConfig `yaml:"afterUserRegister"` + AfterTransferGroupOwner AfterConfig `yaml:"afterTransferGroupOwner"` + BeforeSetFriendRemark BeforeConfig `yaml:"beforeSetFriendRemark"` + AfterSetFriendRemark AfterConfig `yaml:"afterSetFriendRemark"` + AfterGroupMsgRevoke AfterConfig `yaml:"afterGroupMsgRevoke"` + AfterJoinGroup AfterConfig `yaml:"afterJoinGroup"` + BeforeInviteUserToGroup BeforeConfig `yaml:"beforeInviteUserToGroup"` + AfterSetGroupInfo AfterConfig `yaml:"afterSetGroupInfo"` + BeforeSetGroupInfo BeforeConfig `yaml:"beforeSetGroupInfo"` + AfterSetGroupInfoEx AfterConfig `yaml:"afterSetGroupInfoEx"` + BeforeSetGroupInfoEx BeforeConfig `yaml:"beforeSetGroupInfoEx"` + AfterRevokeMsg AfterConfig `yaml:"afterRevokeMsg"` + BeforeAddBlack BeforeConfig `yaml:"beforeAddBlack"` + AfterAddFriend AfterConfig `yaml:"afterAddFriend"` + BeforeAddFriendAgree BeforeConfig `yaml:"beforeAddFriendAgree"` + AfterAddFriendAgree AfterConfig `yaml:"afterAddFriendAgree"` + AfterDeleteFriend AfterConfig `yaml:"afterDeleteFriend"` + BeforeImportFriends BeforeConfig `yaml:"beforeImportFriends"` + AfterImportFriends AfterConfig `yaml:"afterImportFriends"` + AfterRemoveBlack AfterConfig `yaml:"afterRemoveBlack"` + BeforeCreateSingleChatConversations BeforeConfig `yaml:"beforeCreateSingleChatConversations"` + AfterCreateSingleChatConversations AfterConfig `yaml:"afterCreateSingleChatConversations"` + BeforeCreateGroupChatConversations BeforeConfig `yaml:"beforeCreateGroupChatConversations"` + AfterCreateGroupChatConversations AfterConfig `yaml:"afterCreateGroupChatConversations"` } type ZooKeeper struct { diff --git a/pkg/common/config/load_config_test.go b/pkg/common/config/load_config_test.go index 612d44335..f11d91dad 100644 --- a/pkg/common/config/load_config_test.go +++ b/pkg/common/config/load_config_test.go @@ -10,7 +10,7 @@ import ( func TestLoadLogConfig(t *testing.T) { var log Log os.Setenv("IMENV_LOG_REMAINLOGLEVEL", "5") - err := Load("../../../config/", "log.yml", "IMENV_LOG", "source", &log) + err := Load("../../../config/", "log.yml", "IMENV_LOG", &log) assert.Nil(t, err) t.Log(log.RemainLogLevel) // assert.Equal(t, "../../../../logs/", log.StorageLocation) @@ -22,7 +22,7 @@ func TestLoadMongoConfig(t *testing.T) { os.Setenv("IMENV_MONGODB_PASSWORD", "openIM1231231") // os.Setenv("IMENV_MONGODB_URI", "openIM123") // os.Setenv("IMENV_MONGODB_USERNAME", "openIM123") - err := Load("../../../config/", "mongodb.yml", "IMENV_MONGODB", "source", &mongo) + err := Load("../../../config/", "mongodb.yml", "IMENV_MONGODB", &mongo) // err := LoadApiConfig("../../../config/mongodb.yml", "IMENV_MONGODB", &mongo) assert.Nil(t, err) @@ -38,14 +38,14 @@ func TestLoadMongoConfig(t *testing.T) { func TestLoadMinioConfig(t *testing.T) { var storageConfig Minio - err := Load("../../../config/minio.yml", "IMENV_MINIO", "", "source", &storageConfig) + err := Load("../../../config/minio.yml", "IMENV_MINIO", "", &storageConfig) assert.Nil(t, err) assert.Equal(t, "openim", storageConfig.Bucket) } func TestLoadWebhooksConfig(t *testing.T) { var webhooks Webhooks - err := Load("../../../config/webhooks.yml", "IMENV_WEBHOOKS", "", "source", &webhooks) + err := Load("../../../config/webhooks.yml", "IMENV_WEBHOOKS", "", &webhooks) assert.Nil(t, err) assert.Equal(t, 5, webhooks.BeforeAddBlack.Timeout) @@ -53,7 +53,7 @@ func TestLoadWebhooksConfig(t *testing.T) { func TestLoadOpenIMRpcUserConfig(t *testing.T) { var user User - err := Load("../../../config/openim-rpc-user.yml", "IMENV_OPENIM_RPC_USER", "", "source", &user) + err := Load("../../../config/openim-rpc-user.yml", "IMENV_OPENIM_RPC_USER", "", &user) assert.Nil(t, err) //export IMENV_OPENIM_RPC_USER_RPC_LISTENIP="0.0.0.0" assert.Equal(t, "0.0.0.0", user.RPC.ListenIP) @@ -63,14 +63,14 @@ func TestLoadOpenIMRpcUserConfig(t *testing.T) { func TestLoadNotificationConfig(t *testing.T) { var noti Notification - err := Load("../../../config/notification.yml", "IMENV_NOTIFICATION", "", "source", ¬i) + err := Load("../../../config/notification.yml", "IMENV_NOTIFICATION", "", ¬i) assert.Nil(t, err) assert.Equal(t, "Your friend's profile has been changed", noti.FriendRemarkSet.OfflinePush.Title) } func TestLoadOpenIMThirdConfig(t *testing.T) { var third Third - err := Load("../../../config/openim-rpc-third.yml", "IMENV_OPENIM_RPC_THIRD", "", "source", &third) + err := Load("../../../config/openim-rpc-third.yml", "IMENV_OPENIM_RPC_THIRD", "", &third) assert.Nil(t, err) assert.Equal(t, "enabled", third.Object.Enable) assert.Equal(t, "https://oss-cn-chengdu.aliyuncs.com", third.Object.Oss.Endpoint) @@ -86,7 +86,7 @@ func TestLoadOpenIMThirdConfig(t *testing.T) { func TestTransferConfig(t *testing.T) { var tran MsgTransfer - err := Load("../../../config/openim-msgtransfer.yml", "IMENV_OPENIM-MSGTRANSFER", "", "source", &tran) + err := Load("../../../config/openim-msgtransfer.yml", "IMENV_OPENIM-MSGTRANSFER", "", &tran) assert.Nil(t, err) assert.Equal(t, true, tran.Prometheus.Enable) assert.Equal(t, true, tran.Prometheus.AutoSetPorts) diff --git a/pkg/common/storage/controller/conversation.go b/pkg/common/storage/controller/conversation.go index d4088e0c0..7578394b5 100644 --- a/pkg/common/storage/controller/conversation.go +++ b/pkg/common/storage/controller/conversation.go @@ -22,7 +22,6 @@ import ( relationtb "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" - "github.com/openimsdk/open-im-server/v3/pkg/msgprocessor" "github.com/openimsdk/protocol/constant" "github.com/openimsdk/tools/db/pagination" "github.com/openimsdk/tools/db/tx" @@ -48,7 +47,7 @@ type ConversationDatabase interface { // transactional. SetUsersConversationFieldTx(ctx context.Context, userIDs []string, conversation *relationtb.Conversation, fieldMap map[string]any) error // CreateGroupChatConversation creates a group chat conversation for the specified group ID and user IDs. - CreateGroupChatConversation(ctx context.Context, groupID string, userIDs []string) error + CreateGroupChatConversation(ctx context.Context, groupID string, userIDs []string, conversations *relationtb.Conversation) error // GetConversationIDs retrieves conversation IDs for a given user. GetConversationIDs(ctx context.Context, userID string) ([]string, error) // GetUserConversationIDsHash gets the hash of conversation IDs for a given user. @@ -298,10 +297,10 @@ func (c *conversationDatabase) SetUserConversations(ctx context.Context, ownerUs // return c.cache.GetSuperGroupRecvMsgNotNotifyUserIDs(ctx, groupID) //} -func (c *conversationDatabase) CreateGroupChatConversation(ctx context.Context, groupID string, userIDs []string) error { +func (c *conversationDatabase) CreateGroupChatConversation(ctx context.Context, groupID string, userIDs []string, conversation *relationtb.Conversation) error { return c.tx.Transaction(ctx, func(ctx context.Context) error { cache := c.cache.CloneConversationCache() - conversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID) + conversationID := conversation.ConversationID existConversationUserIDs, err := c.conversationDB.FindUserID(ctx, userIDs, []string{conversationID}) if err != nil { return err @@ -309,7 +308,15 @@ func (c *conversationDatabase) CreateGroupChatConversation(ctx context.Context, notExistUserIDs := stringutil.DifferenceString(userIDs, existConversationUserIDs) var conversations []*relationtb.Conversation for _, v := range notExistUserIDs { - conversation := relationtb.Conversation{ConversationType: constant.ReadGroupChatType, GroupID: groupID, OwnerUserID: v, ConversationID: conversationID} + conversation := relationtb.Conversation{ + ConversationType: conversation.ConversationType, GroupID: groupID, OwnerUserID: v, ConversationID: conversationID, + // the parameters have default value + RecvMsgOpt: conversation.RecvMsgOpt, IsPinned: conversation.IsPinned, IsPrivateChat: conversation.IsPrivateChat, + BurnDuration: conversation.BurnDuration, GroupAtType: conversation.GroupAtType, AttachedInfo: conversation.AttachedInfo, + Ex: conversation.Ex, MaxSeq: conversation.MaxSeq, MinSeq: conversation.MinSeq, CreateTime: conversation.CreateTime, + MsgDestructTime: conversation.MsgDestructTime, IsMsgDestruct: conversation.IsMsgDestruct, LatestMsgDestructTime: conversation.LatestMsgDestructTime, + } + conversations = append(conversations, &conversation) cache = cache.DelConversations(v, conversationID).DelConversationNotReceiveMessageUserIDs(conversationID) } @@ -320,7 +327,7 @@ func (c *conversationDatabase) CreateGroupChatConversation(ctx context.Context, return err } } - _, err = c.conversationDB.UpdateByMap(ctx, existConversationUserIDs, conversationID, map[string]any{"max_seq": 0}) + _, err = c.conversationDB.UpdateByMap(ctx, existConversationUserIDs, conversationID, map[string]any{"max_seq": conversation.MaxSeq}) if err != nil { return err } From 11044eac586ee2bec8870cc638cb0688631cfdb5 Mon Sep 17 00:00:00 2001 From: OpenIM-Gordon <1432970085@qq.com> Date: Fri, 21 Mar 2025 15:10:31 +0800 Subject: [PATCH 13/59] =?UTF-8?q?feat:=20add=20a=20function=20for=20busine?= =?UTF-8?q?ss=20info=20change=20to=20update=20related=20conve=E2=80=A6=20(?= =?UTF-8?q?#3225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add a function for business info change to update related conversation's ex info. * feat: add a function for business info change to update related conversation's ex info. * feat: add a function for business info change to update related conversation's ex info. * feat: add a function for business info change to update related conversation's ex info. --- go.mod | 2 +- go.sum | 4 +- internal/api/conversation.go | 5 + internal/api/msg.go | 8 -- internal/api/router.go | 6 +- internal/rpc/conversation/conversation.go | 16 ++- internal/rpc/msg/notification.go | 4 - internal/rpc/msg/send.go | 8 +- internal/rpc/msg/server.go | 10 +- internal/rpc/msg/stream_msg.go | 115 ------------------ pkg/common/storage/controller/conversation.go | 15 +++ pkg/common/storage/controller/stream_msg.go | 34 ------ pkg/common/storage/database/conversation.go | 1 + .../storage/database/mgo/conversation.go | 57 +++++++-- pkg/common/storage/database/mgo/stream_msg.go | 60 --------- pkg/common/storage/database/stream_msg.go | 13 -- pkg/common/storage/model/stream_msg.go | 21 ---- 17 files changed, 95 insertions(+), 284 deletions(-) delete mode 100644 internal/rpc/msg/stream_msg.go delete mode 100644 pkg/common/storage/controller/stream_msg.go delete mode 100644 pkg/common/storage/database/mgo/stream_msg.go delete mode 100644 pkg/common/storage/database/stream_msg.go delete mode 100644 pkg/common/storage/model/stream_msg.go diff --git a/go.mod b/go.mod index 7bf9e6ef6..d762f9fae 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/openimsdk/protocol v0.0.72-alpha.79 + github.com/openimsdk/protocol v0.0.72-alpha.81 github.com/openimsdk/tools v0.0.50-alpha.74 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 2a86d97ea..ea5a6e5b7 100644 --- a/go.sum +++ b/go.sum @@ -347,8 +347,8 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/openimsdk/gomake v0.0.15-alpha.2 h1:5Q8yl8ezy2yx+q8/ucU/t4kJnDfCzNOrkXcDACCqtyM= github.com/openimsdk/gomake v0.0.15-alpha.2/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= -github.com/openimsdk/protocol v0.0.72-alpha.79 h1:e46no8WVAsmTzyy405klrdoUiG7u+1ohDsXvQuFng4s= -github.com/openimsdk/protocol v0.0.72-alpha.79/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= +github.com/openimsdk/protocol v0.0.72-alpha.81 h1:6tDuZ3Anfi1uhX/V5mWxITqJnGQPnvgeaxeqJlEHIVE= +github.com/openimsdk/protocol v0.0.72-alpha.81/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= github.com/openimsdk/tools v0.0.50-alpha.74 h1:yh10SiMiivMEjicEQg+QAsH4pvaO+4noMPdlw+ew0Kc= github.com/openimsdk/tools v0.0.50-alpha.74/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/internal/api/conversation.go b/internal/api/conversation.go index f7dbc133c..f6eadf15a 100644 --- a/internal/api/conversation.go +++ b/internal/api/conversation.go @@ -16,6 +16,7 @@ package api import ( "github.com/gin-gonic/gin" + "github.com/openimsdk/protocol/conversation" "github.com/openimsdk/tools/a2r" ) @@ -71,3 +72,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) UpdateConversationsByUser(c *gin.Context) { + a2r.Call(c, conversation.ConversationClient.UpdateConversationsByUser, o.Client) +} diff --git a/internal/api/msg.go b/internal/api/msg.go index 1ec1f44a7..090f3329b 100644 --- a/internal/api/msg.go +++ b/internal/api/msg.go @@ -551,11 +551,3 @@ func (m *MessageApi) SearchMsg(c *gin.Context) { func (m *MessageApi) GetServerTime(c *gin.Context) { a2r.Call(c, msg.MsgClient.GetServerTime, m.Client) } - -func (m *MessageApi) GetStreamMsg(c *gin.Context) { - a2r.Call(c, msg.MsgClient.GetServerTime, m.Client) -} - -func (m *MessageApi) AppendStreamMsg(c *gin.Context) { - a2r.Call(c, msg.MsgClient.GetServerTime, m.Client) -} diff --git a/internal/api/router.go b/internal/api/router.go index 850c23972..657493b23 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -9,6 +9,8 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" + clientv3 "go.etcd.io/etcd/client/v3" + "github.com/openimsdk/open-im-server/v3/internal/api/jssdk" "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" @@ -27,7 +29,6 @@ import ( "github.com/openimsdk/tools/discovery/etcd" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mw" - clientv3 "go.etcd.io/etcd/client/v3" ) const ( @@ -246,8 +247,6 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin msgGroup.POST("/batch_send_msg", m.BatchSendMsg) msgGroup.POST("/check_msg_is_send_success", m.CheckMsgIsSendSuccess) msgGroup.POST("/get_server_time", m.GetServerTime) - msgGroup.POST("/get_stream_msg", m.GetStreamMsg) - msgGroup.POST("/append_stream_msg", m.AppendStreamMsg) } // Conversation { @@ -264,6 +263,7 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin 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("/update_conversations_by_user", c.UpdateConversationsByUser) } { diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index 8bc3fd2a9..ca2c58878 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -22,6 +22,8 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/dbbuild" "github.com/openimsdk/open-im-server/v3/pkg/rpcli" + "google.golang.org/grpc" + "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/convert" "github.com/openimsdk/open-im-server/v3/pkg/common/servererrs" @@ -40,7 +42,6 @@ import ( "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/utils/datautil" - "google.golang.org/grpc" ) type conversationServer struct { @@ -329,6 +330,19 @@ func (c *conversationServer) SetConversations(ctx context.Context, req *pbconver return &pbconversation.SetConversationsResp{}, nil } +func (c *conversationServer) UpdateConversationsByUser(ctx context.Context, req *pbconversation.UpdateConversationsByUserReq) (*pbconversation.UpdateConversationsByUserResp, error) { + m := make(map[string]any) + if req.Ex != nil { + m["ex"] = req.Ex.Value + } + if len(m) > 0 { + if err := c.conversationDatabase.UpdateUserConversations(ctx, req.UserID, m); err != nil { + return nil, err + } + } + return &pbconversation.UpdateConversationsByUserResp{}, nil +} + // Get user IDs with "Do Not Disturb" enabled in super large groups. func (c *conversationServer) GetRecvMsgNotNotifyUserIDs(ctx context.Context, req *pbconversation.GetRecvMsgNotNotifyUserIDsReq) (*pbconversation.GetRecvMsgNotNotifyUserIDsResp, error) { return nil, errs.New("deprecated") diff --git a/internal/rpc/msg/notification.go b/internal/rpc/msg/notification.go index 0daafbe6c..0418823d6 100644 --- a/internal/rpc/msg/notification.go +++ b/internal/rpc/msg/notification.go @@ -48,7 +48,3 @@ func (m *MsgNotificationSender) MarkAsReadNotification(ctx context.Context, conv } m.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips) } - -func (m *MsgNotificationSender) StreamMsgNotification(ctx context.Context, sendID string, recvID string, sessionType int32, tips *sdkws.StreamMsgTips) { - m.NotificationWithSessionType(ctx, sendID, recvID, constant.StreamMsgNotification, sessionType, tips) -} diff --git a/internal/rpc/msg/send.go b/internal/rpc/msg/send.go index f226c4921..6b2ec30b5 100644 --- a/internal/rpc/msg/send.go +++ b/internal/rpc/msg/send.go @@ -17,6 +17,8 @@ package msg import ( "context" + "google.golang.org/protobuf/proto" + "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" "github.com/openimsdk/open-im-server/v3/pkg/msgprocessor" "github.com/openimsdk/open-im-server/v3/pkg/util/conversationutil" @@ -29,7 +31,6 @@ import ( "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" "github.com/openimsdk/tools/utils/datautil" - "google.golang.org/protobuf/proto" ) func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg.SendMsgResp, error) { @@ -49,11 +50,6 @@ func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg. func (m *msgServer) sendMsg(ctx context.Context, req *pbmsg.SendMsgReq, before **sdkws.MsgData) (*pbmsg.SendMsgResp, error) { m.encapsulateMsgData(req.MsgData) - if req.MsgData.ContentType == constant.Stream { - if err := m.handlerStreamMsg(ctx, req.MsgData); err != nil { - return nil, err - } - } switch req.MsgData.SessionType { case constant.SingleChatType: return m.sendMsgSingleChat(ctx, req, before) diff --git a/internal/rpc/msg/server.go b/internal/rpc/msg/server.go index d1002cca3..cfc750c5b 100644 --- a/internal/rpc/msg/server.go +++ b/internal/rpc/msg/server.go @@ -59,9 +59,8 @@ type Config struct { // MsgServer encapsulates dependencies required for message handling. type msgServer struct { msg.UnimplementedMsgServer - RegisterCenter discovery.Conn // Service discovery registry for service registration. - MsgDatabase controller.CommonMsgDatabase // Interface for message database operations. - StreamMsgDatabase controller.StreamMsgDatabase + RegisterCenter discovery.Conn // Service discovery registry for service registration. + MsgDatabase controller.CommonMsgDatabase // Interface for message database operations. UserLocalCache *rpccache.UserLocalCache // Local cache for user data. FriendLocalCache *rpccache.FriendLocalCache // Local cache for friend data. GroupLocalCache *rpccache.GroupLocalCache // Local cache for group data. @@ -117,10 +116,6 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr if err != nil { return err } - streamMsg, err := mgo.NewStreamMsgMongo(mgocli.GetDB()) - if err != nil { - return err - } seqUserCache := redis.NewSeqUserCacheRedis(rdb, seqUser) userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User) if err != nil { @@ -142,7 +137,6 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr msgDatabase := controller.NewCommonMsgDatabase(msgDocModel, msgModel, seqUserCache, seqConversationCache, redisProducer) s := &msgServer{ MsgDatabase: msgDatabase, - StreamMsgDatabase: controller.NewStreamMsgDatabase(streamMsg), RegisterCenter: client, UserLocalCache: rpccache.NewUserLocalCache(rpcli.NewUserClient(userConn), &config.LocalCacheConfig, rdb), GroupLocalCache: rpccache.NewGroupLocalCache(rpcli.NewGroupClient(groupConn), &config.LocalCacheConfig, rdb), diff --git a/internal/rpc/msg/stream_msg.go b/internal/rpc/msg/stream_msg.go deleted file mode 100644 index 688d766c8..000000000 --- a/internal/rpc/msg/stream_msg.go +++ /dev/null @@ -1,115 +0,0 @@ -package msg - -import ( - "context" - "fmt" - "time" - - "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" - "github.com/openimsdk/open-im-server/v3/pkg/msgprocessor" - "github.com/openimsdk/protocol/constant" - "github.com/openimsdk/protocol/msg" - "github.com/openimsdk/protocol/sdkws" - "github.com/openimsdk/tools/errs" -) - -const StreamDeadlineTime = time.Second * 60 * 10 - -func (m *msgServer) handlerStreamMsg(ctx context.Context, msgData *sdkws.MsgData) error { - now := time.Now() - val := &model.StreamMsg{ - ClientMsgID: msgData.ClientMsgID, - ConversationID: msgprocessor.GetConversationIDByMsg(msgData), - UserID: msgData.SendID, - CreateTime: now, - DeadlineTime: now.Add(StreamDeadlineTime), - } - return m.StreamMsgDatabase.CreateStreamMsg(ctx, val) -} - -func (m *msgServer) getStreamMsg(ctx context.Context, clientMsgID string) (*model.StreamMsg, error) { - res, err := m.StreamMsgDatabase.GetStreamMsg(ctx, clientMsgID) - if err != nil { - return nil, err - } - now := time.Now() - if !res.End && res.DeadlineTime.Before(now) { - res.End = true - res.DeadlineTime = now - _ = m.StreamMsgDatabase.AppendStreamMsg(ctx, res.ClientMsgID, 0, nil, true, now) - } - return res, nil -} - -func (m *msgServer) AppendStreamMsg(ctx context.Context, req *msg.AppendStreamMsgReq) (*msg.AppendStreamMsgResp, error) { - res, err := m.getStreamMsg(ctx, req.ClientMsgID) - if err != nil { - return nil, err - } - if res.End { - return nil, errs.ErrNoPermission.WrapMsg("stream msg is end") - } - if len(res.Packets) < int(req.StartIndex) { - return nil, errs.ErrNoPermission.WrapMsg("start index is invalid") - } - if val := len(res.Packets) - int(req.StartIndex); val > 0 { - exist := res.Packets[int(req.StartIndex):] - for i, s := range exist { - if len(req.Packets) == 0 { - break - } - if s != req.Packets[i] { - return nil, errs.ErrNoPermission.WrapMsg(fmt.Sprintf("packet %d has been written and is inconsistent", i)) - } - req.StartIndex++ - req.Packets = req.Packets[1:] - } - } - if len(req.Packets) == 0 && res.End == req.End { - return &msg.AppendStreamMsgResp{}, nil - } - deadlineTime := time.Now().Add(StreamDeadlineTime) - if err := m.StreamMsgDatabase.AppendStreamMsg(ctx, req.ClientMsgID, int(req.StartIndex), req.Packets, req.End, deadlineTime); err != nil { - return nil, err - } - conversation, err := m.conversationClient.GetConversation(ctx, res.ConversationID, res.UserID) - if err != nil { - return nil, err - } - tips := &sdkws.StreamMsgTips{ - ConversationID: res.ConversationID, - ClientMsgID: res.ClientMsgID, - StartIndex: req.StartIndex, - Packets: req.Packets, - End: req.End, - } - var ( - recvID string - sessionType int32 - ) - if conversation.GroupID == "" { - sessionType = constant.SingleChatType - recvID = conversation.UserID - } else { - sessionType = constant.ReadGroupChatType - recvID = conversation.GroupID - } - m.msgNotificationSender.StreamMsgNotification(ctx, res.UserID, recvID, sessionType, tips) - return &msg.AppendStreamMsgResp{}, nil -} - -func (m *msgServer) GetStreamMsg(ctx context.Context, req *msg.GetStreamMsgReq) (*msg.GetStreamMsgResp, error) { - res, err := m.getStreamMsg(ctx, req.ClientMsgID) - if err != nil { - return nil, err - } - return &msg.GetStreamMsgResp{ - ClientMsgID: res.ClientMsgID, - ConversationID: res.ConversationID, - UserID: res.UserID, - Packets: res.Packets, - End: res.End, - CreateTime: res.CreateTime.UnixMilli(), - DeadlineTime: res.DeadlineTime.UnixMilli(), - }, nil -} diff --git a/pkg/common/storage/controller/conversation.go b/pkg/common/storage/controller/conversation.go index 7578394b5..27442ca66 100644 --- a/pkg/common/storage/controller/conversation.go +++ b/pkg/common/storage/controller/conversation.go @@ -46,6 +46,9 @@ type ConversationDatabase interface { // SetUsersConversationFieldTx updates a specific field for multiple users' conversations, creating new conversations if they do not exist, or updates them otherwise. This operation is // transactional. SetUsersConversationFieldTx(ctx context.Context, userIDs []string, conversation *relationtb.Conversation, fieldMap map[string]any) error + // UpdateUserConversations updates all conversations related to a specified user. + // This function does NOT update the user's own conversations but rather the conversations where this user is involved (e.g., other users' conversations referencing this user). + UpdateUserConversations(ctx context.Context, userID string, args map[string]any) error // CreateGroupChatConversation creates a group chat conversation for the specified group ID and user IDs. CreateGroupChatConversation(ctx context.Context, groupID string, userIDs []string, conversations *relationtb.Conversation) error // GetConversationIDs retrieves conversation IDs for a given user. @@ -145,6 +148,18 @@ func (c *conversationDatabase) SetUsersConversationFieldTx(ctx context.Context, }) } +func (c *conversationDatabase) UpdateUserConversations(ctx context.Context, userID string, args map[string]any) error { + conversations, err := c.conversationDB.UpdateUserConversations(ctx, userID, args) + if err != nil { + return err + } + cache := c.cache.CloneConversationCache() + for _, conversation := range conversations { + cache = cache.DelUsersConversation(conversation.ConversationID, conversation.OwnerUserID).DelConversationVersionUserIDs(conversation.OwnerUserID) + } + return cache.ChainExecDel(ctx) +} + func (c *conversationDatabase) UpdateUsersConversationField(ctx context.Context, userIDs []string, conversationID string, args map[string]any) error { _, err := c.conversationDB.UpdateByMap(ctx, userIDs, conversationID, args) if err != nil { diff --git a/pkg/common/storage/controller/stream_msg.go b/pkg/common/storage/controller/stream_msg.go deleted file mode 100644 index 3409ccd93..000000000 --- a/pkg/common/storage/controller/stream_msg.go +++ /dev/null @@ -1,34 +0,0 @@ -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" - "time" -) - -type StreamMsgDatabase interface { - CreateStreamMsg(ctx context.Context, model *model.StreamMsg) error - AppendStreamMsg(ctx context.Context, clientMsgID string, startIndex int, packets []string, end bool, deadlineTime time.Time) error - GetStreamMsg(ctx context.Context, clientMsgID string) (*model.StreamMsg, error) -} - -func NewStreamMsgDatabase(db database.StreamMsg) StreamMsgDatabase { - return &streamMsgDatabase{db: db} -} - -type streamMsgDatabase struct { - db database.StreamMsg -} - -func (m *streamMsgDatabase) CreateStreamMsg(ctx context.Context, model *model.StreamMsg) error { - return m.db.CreateStreamMsg(ctx, model) -} - -func (m *streamMsgDatabase) AppendStreamMsg(ctx context.Context, clientMsgID string, startIndex int, packets []string, end bool, deadlineTime time.Time) error { - return m.db.AppendStreamMsg(ctx, clientMsgID, startIndex, packets, end, deadlineTime) -} - -func (m *streamMsgDatabase) GetStreamMsg(ctx context.Context, clientMsgID string) (*model.StreamMsg, error) { - return m.db.GetStreamMsg(ctx, clientMsgID) -} diff --git a/pkg/common/storage/database/conversation.go b/pkg/common/storage/database/conversation.go index 1fb53cfed..d612dfc2d 100644 --- a/pkg/common/storage/database/conversation.go +++ b/pkg/common/storage/database/conversation.go @@ -24,6 +24,7 @@ import ( type Conversation interface { Create(ctx context.Context, conversations []*model.Conversation) (err error) UpdateByMap(ctx context.Context, userIDs []string, conversationID string, args map[string]any) (rows int64, err error) + UpdateUserConversations(ctx context.Context, userID string, args map[string]any) ([]*model.Conversation, error) Update(ctx context.Context, conversation *model.Conversation) (err error) Find(ctx context.Context, ownerUserID string, conversationIDs []string) (conversations []*model.Conversation, err error) FindUserID(ctx context.Context, userIDs []string, conversationIDs []string) ([]string, error) diff --git a/pkg/common/storage/database/mgo/conversation.go b/pkg/common/storage/database/mgo/conversation.go index 536827450..89f13ea3d 100644 --- a/pkg/common/storage/database/mgo/conversation.go +++ b/pkg/common/storage/database/mgo/conversation.go @@ -21,23 +21,32 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "github.com/openimsdk/protocol/constant" "github.com/openimsdk/tools/db/mongoutil" "github.com/openimsdk/tools/db/pagination" "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 NewConversationMongo(db *mongo.Database) (*ConversationMgo, error) { coll := db.Collection(database.ConversationName) - _, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{ - Keys: bson.D{ - {Key: "owner_user_id", Value: 1}, - {Key: "conversation_id", Value: 1}, + _, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "owner_user_id", Value: 1}, + {Key: "conversation_id", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "user_id", Value: 1}, + }, + Options: options.Index(), }, - Options: options.Index().SetUnique(true), }) if err != nil { return nil, errs.Wrap(err) @@ -101,6 +110,38 @@ func (c *ConversationMgo) UpdateByMap(ctx context.Context, userIDs []string, con return rows, nil } +func (c *ConversationMgo) UpdateUserConversations(ctx context.Context, userID string, args map[string]any) ([]*model.Conversation, error) { + if len(args) == 0 { + return nil, nil + } + filter := bson.M{ + "user_id": userID, + } + + conversations, err := mongoutil.Find[*model.Conversation](ctx, c.coll, filter, options.Find().SetProjection(bson.M{"_id": 0, "owner_user_id": 1, "conversation_id": 1})) + if err != nil { + return nil, err + } + err = mongoutil.IncrVersion(func() error { + _, err := mongoutil.UpdateMany(ctx, c.coll, filter, bson.M{"$set": args}) + if err != nil { + return err + } + return nil + }, func() error { + for _, conversation := range conversations { + if err := c.version.IncrVersion(ctx, conversation.OwnerUserID, []string{conversation.ConversationID}, model.VersionStateUpdate); err != nil { + return err + } + } + return nil + }) + if err != nil { + return nil, err + } + return conversations, nil +} + func (c *ConversationMgo) Update(ctx context.Context, conversation *model.Conversation) (err error) { return mongoutil.IncrVersion(func() error { return mongoutil.UpdateOne(ctx, c.coll, bson.M{"owner_user_id": conversation.OwnerUserID, "conversation_id": conversation.ConversationID}, bson.M{"$set": conversation}, true) diff --git a/pkg/common/storage/database/mgo/stream_msg.go b/pkg/common/storage/database/mgo/stream_msg.go deleted file mode 100644 index c57798daa..000000000 --- a/pkg/common/storage/database/mgo/stream_msg.go +++ /dev/null @@ -1,60 +0,0 @@ -package mgo - -import ( - "context" - "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/db/mongoutil" - "github.com/openimsdk/tools/errs" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "time" -) - -func NewStreamMsgMongo(db *mongo.Database) (*StreamMsgMongo, error) { - coll := db.Collection(database.StreamMsgName) - _, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{ - Keys: bson.D{ - {Key: "client_msg_id", Value: 1}, - }, - Options: options.Index().SetUnique(true), - }) - if err != nil { - return nil, errs.Wrap(err) - } - return &StreamMsgMongo{coll: coll}, nil -} - -type StreamMsgMongo struct { - coll *mongo.Collection -} - -func (m *StreamMsgMongo) CreateStreamMsg(ctx context.Context, val *model.StreamMsg) error { - if val.Packets == nil { - val.Packets = []string{} - } - return mongoutil.InsertMany(ctx, m.coll, []*model.StreamMsg{val}) -} - -func (m *StreamMsgMongo) AppendStreamMsg(ctx context.Context, clientMsgID string, startIndex int, packets []string, end bool, deadlineTime time.Time) error { - update := bson.M{ - "$set": bson.M{ - "end": end, - "deadline_time": deadlineTime, - }, - } - if len(packets) > 0 { - update["$push"] = bson.M{ - "packets": bson.M{ - "$each": packets, - "$position": startIndex, - }, - } - } - return mongoutil.UpdateOne(ctx, m.coll, bson.M{"client_msg_id": clientMsgID, "end": false}, update, true) -} - -func (m *StreamMsgMongo) GetStreamMsg(ctx context.Context, clientMsgID string) (*model.StreamMsg, error) { - return mongoutil.FindOne[*model.StreamMsg](ctx, m.coll, bson.M{"client_msg_id": clientMsgID}) -} diff --git a/pkg/common/storage/database/stream_msg.go b/pkg/common/storage/database/stream_msg.go deleted file mode 100644 index e83fffbaa..000000000 --- a/pkg/common/storage/database/stream_msg.go +++ /dev/null @@ -1,13 +0,0 @@ -package database - -import ( - "context" - "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" - "time" -) - -type StreamMsg interface { - CreateStreamMsg(ctx context.Context, model *model.StreamMsg) error - AppendStreamMsg(ctx context.Context, clientMsgID string, startIndex int, packets []string, end bool, deadlineTime time.Time) error - GetStreamMsg(ctx context.Context, clientMsgID string) (*model.StreamMsg, error) -} diff --git a/pkg/common/storage/model/stream_msg.go b/pkg/common/storage/model/stream_msg.go deleted file mode 100644 index c040426a4..000000000 --- a/pkg/common/storage/model/stream_msg.go +++ /dev/null @@ -1,21 +0,0 @@ -package model - -import ( - "time" -) - -const ( - StreamMsgStatusWait = 0 - StreamMsgStatusDone = 1 - StreamMsgStatusFail = 2 -) - -type StreamMsg struct { - ClientMsgID string `bson:"client_msg_id"` - ConversationID string `bson:"conversation_id"` - UserID string `bson:"user_id"` - Packets []string `bson:"packets"` - End bool `bson:"end"` - CreateTime time.Time `bson:"create_time"` - DeadlineTime time.Time `bson:"deadline_time"` -} From 73934fd9553fd5848a0409766c3306b3e729a93b Mon Sep 17 00:00:00 2001 From: OpenIM-Gordon <1432970085@qq.com> Date: Fri, 21 Mar 2025 15:19:21 +0800 Subject: [PATCH 14/59] feat: add filtering for invalid messages and invalid conversations to prevent data-fetching exceptions after conversations are deleted. (#3239) --- internal/api/jssdk/jssdk.go | 80 +++++++++++++++++++++++++++---------- pkg/rpcli/msg.go | 4 +- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/internal/api/jssdk/jssdk.go b/internal/api/jssdk/jssdk.go index 3c0911207..0d30b1ea0 100644 --- a/internal/api/jssdk/jssdk.go +++ b/internal/api/jssdk/jssdk.go @@ -2,10 +2,14 @@ package jssdk import ( "context" - "github.com/openimsdk/open-im-server/v3/pkg/rpcli" "sort" + "github.com/openimsdk/open-im-server/v3/pkg/rpcli" + "github.com/openimsdk/protocol/constant" + "github.com/openimsdk/tools/log" + "github.com/gin-gonic/gin" + "github.com/openimsdk/protocol/conversation" "github.com/openimsdk/protocol/jssdk" "github.com/openimsdk/protocol/msg" @@ -109,10 +113,7 @@ func (x *JSSdk) getActiveConversations(ctx context.Context, req *jssdk.GetActive if len(conversationIDs) == 0 { return &jssdk.GetActiveConversationsResp{}, nil } - readSeq, err := x.msgClient.GetHasReadSeqs(ctx, conversationIDs, req.OwnerUserID) - if err != nil { - return nil, err - } + activeConversation, err := x.msgClient.GetActiveConversation(ctx, conversationIDs) if err != nil { return nil, err @@ -120,6 +121,10 @@ func (x *JSSdk) getActiveConversations(ctx context.Context, req *jssdk.GetActive if len(activeConversation) == 0 { return &jssdk.GetActiveConversationsResp{}, nil } + readSeq, err := x.msgClient.GetHasReadSeqs(ctx, conversationIDs, req.OwnerUserID) + if err != nil { + return nil, err + } sortConversations := sortActiveConversations{ Conversation: activeConversation, } @@ -147,6 +152,7 @@ func (x *JSSdk) getActiveConversations(ctx context.Context, req *jssdk.GetActive if err != nil { return nil, err } + x.checkMessagesAndGetLastMessage(ctx, req.OwnerUserID, msgs) conversationMap := datautil.SliceToMap(conversations, func(c *conversation.Conversation) string { return c.ConversationID }) @@ -156,16 +162,15 @@ func (x *JSSdk) getActiveConversations(ctx context.Context, req *jssdk.GetActive if !ok { continue } - var lastMsg *sdkws.MsgData if msgList, ok := msgs[c.ConversationID]; ok && len(msgList.Msgs) > 0 { - lastMsg = msgList.Msgs[0] + resp = append(resp, &jssdk.ConversationMsg{ + Conversation: conv, + LastMsg: msgList.Msgs[0], + MaxSeq: c.MaxSeq, + ReadSeq: readSeq[c.ConversationID], + }) } - resp = append(resp, &jssdk.ConversationMsg{ - Conversation: conv, - LastMsg: lastMsg, - MaxSeq: c.MaxSeq, - ReadSeq: readSeq[c.ConversationID], - }) + } if err := x.fillConversations(ctx, resp); err != nil { return nil, err @@ -219,18 +224,18 @@ func (x *JSSdk) getConversations(ctx context.Context, req *jssdk.GetConversation return nil, err } } + x.checkMessagesAndGetLastMessage(ctx, req.OwnerUserID, msgs) resp := make([]*jssdk.ConversationMsg, 0, len(conversations)) for _, c := range conversations { - var lastMsg *sdkws.MsgData if msgList, ok := msgs[c.ConversationID]; ok && len(msgList.Msgs) > 0 { - lastMsg = msgList.Msgs[0] + resp = append(resp, &jssdk.ConversationMsg{ + Conversation: c, + LastMsg: msgList.Msgs[0], + MaxSeq: maxSeqs[c.ConversationID], + ReadSeq: readSeqs[c.ConversationID], + }) } - resp = append(resp, &jssdk.ConversationMsg{ - Conversation: c, - LastMsg: lastMsg, - MaxSeq: maxSeqs[c.ConversationID], - ReadSeq: readSeqs[c.ConversationID], - }) + } if err := x.fillConversations(ctx, resp); err != nil { return nil, err @@ -247,3 +252,36 @@ func (x *JSSdk) getConversations(ctx context.Context, req *jssdk.GetConversation UnreadCount: unreadCount, }, nil } + +// This function checks whether the latest MaxSeq message is valid. +// If not, it needs to fetch a valid message again. +func (x *JSSdk) checkMessagesAndGetLastMessage(ctx context.Context, userID string, messages map[string]*sdkws.PullMsgs) { + var conversationIDs []string + + for conversationID, message := range messages { + allInValid := true + for _, data := range message.Msgs { + if data.Status < constant.MsgStatusHasDeleted { + allInValid = false + break + } + } + if allInValid { + conversationIDs = append(conversationIDs, conversationID) + } + } + if len(conversationIDs) > 0 { + resp, err := x.msgClient.GetLastMessage(ctx, &msg.GetLastMessageReq{ + UserID: userID, + ConversationIDs: conversationIDs, + }) + if err != nil { + log.ZError(ctx, "fetchLatestValidMessages", err, "conversationIDs", conversationIDs) + return + } + for conversationID, message := range resp.Msgs { + messages[conversationID] = &sdkws.PullMsgs{Msgs: []*sdkws.MsgData{message}} + } + } + +} diff --git a/pkg/rpcli/msg.go b/pkg/rpcli/msg.go index 0c44b7c8b..e4d1ece6e 100644 --- a/pkg/rpcli/msg.go +++ b/pkg/rpcli/msg.go @@ -2,9 +2,11 @@ package rpcli import ( "context" + + "google.golang.org/grpc" + "github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/sdkws" - "google.golang.org/grpc" ) func NewMsgClient(cc grpc.ClientConnInterface) *MsgClient { From 4dc9b4586188302d0a93edf3d1f9f8f4f8380db4 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Fri, 28 Mar 2025 15:46:42 +0800 Subject: [PATCH 15/59] feat: implement stress-test tools. (#3261) * feat: implement stress-test tools. * revert config file. --- config/share.yml | 4 +- tools/stress-test/README.md | 25 ++ tools/stress-test/main.go | 452 ++++++++++++++++++++++++++++++++++++ 3 files changed, 479 insertions(+), 2 deletions(-) create mode 100644 tools/stress-test/README.md create mode 100755 tools/stress-test/main.go diff --git a/config/share.yml b/config/share.yml index a5fbeac75..0913c1e88 100644 --- a/config/share.yml +++ b/config/share.yml @@ -1,9 +1,9 @@ secret: openIM123 -imAdminUserID: [ imAdmin ] +imAdminUserID: [imAdmin] # 1: For Android, iOS, Windows, Mac, and web platforms, only one instance can be online at a time multiLogin: policy: 1 # max num of tokens in one end - maxNumOneEnd: 30 \ No newline at end of file + maxNumOneEnd: 30 diff --git a/tools/stress-test/README.md b/tools/stress-test/README.md new file mode 100644 index 000000000..531233a20 --- /dev/null +++ b/tools/stress-test/README.md @@ -0,0 +1,25 @@ +# Stress Test + +## Usage + +You need set `TestTargetUserList` and `DefaultGroupID` variables. + +### Build + +```bash +go build -o _output/bin/tools/linux/amd64/stress-test tools/stress-test/main.go + +# or + +go build -o tools/stress-test/stress-test tools/stress-test/main.go +``` + +### Excute + +```bash +_output/bin/tools/linux/amd64/stress-test -c config/ + +#or + +tools/stress-test/stress-test -c config/ +``` diff --git a/tools/stress-test/main.go b/tools/stress-test/main.go new file mode 100755 index 000000000..ee58c9749 --- /dev/null +++ b/tools/stress-test/main.go @@ -0,0 +1,452 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/openimsdk/open-im-server/v3/pkg/common/config" + "github.com/openimsdk/protocol/auth" + "github.com/openimsdk/protocol/constant" + "github.com/openimsdk/protocol/group" + "github.com/openimsdk/protocol/relation" + "github.com/openimsdk/protocol/sdkws" + pbuser "github.com/openimsdk/protocol/user" + "github.com/openimsdk/tools/log" + "github.com/openimsdk/tools/system/program" +) + +/* + 1. Create one user every minute + 2. Import target users as friends + 3. Add users to the default group + 4. Send a message to the default group every second, containing index and current timestamp + 5. Create a new group every minute and invite target users to join +*/ + +// !!! ATTENTION: This variable is must be added! +var ( + // Use default userIDs List for testing, need to be created. + TestTargetUserList = []string{ + "", + } + DefaultGroupID = "" // Use default group ID for testing, need to be created. +) + +var ( + ApiAddress string + + // API method + GetAdminToken = "/auth/get_admin_token" + CreateUser = "/user/user_register" + ImportFriend = "/friend/import_friend" + InviteToGroup = "/group/invite_user_to_group" + SendMsg = "/msg/send_msg" + CreateGroup = "/group/create_group" + GetUserToken = "/auth/user_token" +) + +const ( + MaxUser = 10000 + MaxGroup = 1000 + + CreateUserTicker = 1 * time.Minute // Ticker is 1min in create user + SendMessageTicker = 1 * time.Second // Ticker is 1s in send message + CreateGroupTicker = 1 * time.Minute +) + +type BaseResp struct { + ErrCode int `json:"errCode"` + ErrMsg string `json:"errMsg"` + Data json.RawMessage `json:"data"` +} + +type StressTest struct { + Conf *conf + AdminUserID string + AdminToken string + DefaultGroupID string + DefaultSendUserID string + UserCounter int + GroupCounter int + MsgCounter int + CreatedUsers []string + CreatedGroups []string + Mutex sync.Mutex + Ctx context.Context + Cancel context.CancelFunc + HttpClient *http.Client + Wg sync.WaitGroup + Once sync.Once +} + +type conf struct { + Share config.Share + Api config.API +} + +func initConfig(configDir string) (*config.Share, *config.API, error) { + var ( + share = &config.Share{} + apiConfig = &config.API{} + ) + + err := config.Load(configDir, config.ShareFileName, config.EnvPrefixMap[config.ShareFileName], share) + if err != nil { + return nil, nil, err + } + + err = config.Load(configDir, config.OpenIMAPICfgFileName, config.EnvPrefixMap[config.OpenIMAPICfgFileName], apiConfig) + if err != nil { + return nil, nil, err + } + + return share, apiConfig, nil +} + +// Post Request +func (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) ([]byte, error) { + // Marshal body + jsonBody, err := json.Marshal(reqbody) + if err != nil { + log.ZError(ctx, "Failed to marshal request body", err, "url", url, "reqbody", reqbody) + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("operationID", st.AdminUserID) + if st.AdminToken != "" { + req.Header.Set("token", st.AdminToken) + } + + // log.ZInfo(ctx, "Header info is ", "Content-Type", "application/json", "operationID", st.AdminUserID, "token", st.AdminToken) + + resp, err := st.HttpClient.Do(req) + if err != nil { + log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody) + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.ZError(ctx, "Failed to read response body", err, "url", url) + return nil, err + } + + var baseResp BaseResp + if err := json.Unmarshal(respBody, &baseResp); err != nil { + log.ZError(ctx, "Failed to unmarshal response body", err, "url", url, "respBody", string(respBody)) + return nil, err + } + + if baseResp.ErrCode != 0 { + err = fmt.Errorf(baseResp.ErrMsg) + log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody, "resp", baseResp) + return nil, err + } + + return baseResp.Data, nil +} + +func (st *StressTest) GetAdminToken(ctx context.Context) (string, error) { + req := auth.GetAdminTokenReq{ + Secret: st.Conf.Share.Secret, + UserID: st.AdminUserID, + } + + resp, err := st.PostRequest(ctx, ApiAddress+GetAdminToken, &req) + if err != nil { + return "", err + } + + data := &auth.GetAdminTokenResp{} + if err := json.Unmarshal(resp, &data); err != nil { + return "", err + } + + return data.Token, nil +} + +func (st *StressTest) CreateUser(ctx context.Context, userID string) (string, error) { + user := &sdkws.UserInfo{ + UserID: userID, + Nickname: userID, + } + + req := pbuser.UserRegisterReq{ + Users: []*sdkws.UserInfo{user}, + } + + _, err := st.PostRequest(ctx, ApiAddress+CreateUser, &req) + if err != nil { + return "", err + } + + st.UserCounter++ + return userID, nil +} + +func (st *StressTest) ImportFriend(ctx context.Context, userID string) error { + req := relation.ImportFriendReq{ + OwnerUserID: userID, + FriendUserIDs: TestTargetUserList, + } + + _, err := st.PostRequest(ctx, ApiAddress+ImportFriend, &req) + if err != nil { + return err + } + + return nil +} + +func (st *StressTest) InviteToGroup(ctx context.Context, userID string) error { + req := group.InviteUserToGroupReq{ + GroupID: st.DefaultGroupID, + InvitedUserIDs: []string{userID}, + } + _, err := st.PostRequest(ctx, ApiAddress+InviteToGroup, &req) + if err != nil { + return err + } + + return nil +} + +func (st *StressTest) SendMsg(ctx context.Context, userID string) error { + contentObj := map[string]any{ + "content": fmt.Sprintf("index %d. The current time is %s", st.MsgCounter, time.Now().Format("2006-01-02 15:04:05.000")), + } + + req := map[string]any{ + "sendID": userID, + "groupID": st.DefaultGroupID, + "contentType": constant.Text, + "sessionType": constant.ReadGroupChatType, + "content": contentObj, + } + + _, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req) + if err != nil { + log.ZError(ctx, "Failed to send message", err, "userID", userID, "req", &req) + return err + } + + st.MsgCounter++ + + return nil +} + +func (st *StressTest) CreateGroup(ctx context.Context, userID string) (string, error) { + groupID := fmt.Sprintf("StressTestGroup_%d_%s", st.GroupCounter, time.Now().Format("20060102150405")) + + req := map[string]any{ + "memberUserIDs": TestTargetUserList, + "ownerUserID": userID, + "groupInfo": map[string]any{ + "groupID": groupID, + "groupName": groupID, + "groupType": constant.WorkingGroup, + }, + } + resp := group.CreateGroupResp{} + + response, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req) + if err != nil { + return "", err + } + + if err := json.Unmarshal(response, &resp); err != nil { + return "", err + } + + st.GroupCounter++ + + return resp.GroupInfo.GroupID, nil +} + +func main() { + var configPath string + // defaultConfigDir := filepath.Join("..", "..", "..", "..", "..", "config") + // flag.StringVar(&configPath, "c", defaultConfigDir, "config path") + flag.StringVar(&configPath, "c", "", "config path") + flag.Parse() + + if configPath == "" { + _, _ = fmt.Fprintln(os.Stderr, "config path is empty") + os.Exit(1) + return + } + + fmt.Printf(" Config Path: %s\n", configPath) + + share, apiConfig, err := initConfig(configPath) + if err != nil { + program.ExitWithError(err) + return + } + + ApiAddress = fmt.Sprintf("http://%s:%s", "127.0.0.1", fmt.Sprint(apiConfig.Api.Ports[0])) + + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan struct{}) + + defer cancel() + + st := &StressTest{ + Conf: &conf{ + Share: *share, + Api: *apiConfig, + }, + AdminUserID: share.IMAdminUserID[0], + Ctx: ctx, + Cancel: cancel, + HttpClient: &http.Client{ + Timeout: 50 * time.Second, + }, + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("\nReceived stop signal, stopping...") + + select { + case <-ch: + default: + close(ch) + } + + st.Cancel() + }() + + token, err := st.GetAdminToken(st.Ctx) + if err != nil { + log.ZError(ctx, "Get Admin Token failed.", err, "AdminUserID", st.AdminUserID) + } + + st.AdminToken = token + fmt.Println("Admin Token:", st.AdminToken) + fmt.Println("ApiAddress:", ApiAddress) + + st.DefaultGroupID = DefaultGroupID + + st.Wg.Add(1) + go func() { + defer st.Wg.Done() + + ticker := time.NewTicker(CreateUserTicker) + defer ticker.Stop() + + for st.UserCounter < MaxUser { + select { + case <-st.Ctx.Done(): + log.ZInfo(st.Ctx, "Stop Create user", "reason", "context done") + return + + case <-ticker.C: + // Create User + userID := fmt.Sprintf("%d_Stresstest_%s", st.UserCounter, time.Now().Format("0102150405")) + + userCreatedID, err := st.CreateUser(st.Ctx, userID) + if err != nil { + log.ZError(st.Ctx, "Create User failed.", err, "UserID", userID) + os.Exit(1) + return + } + // fmt.Println("User Created ID:", userCreatedID) + + // Import Friend + if err = st.ImportFriend(st.Ctx, userCreatedID); err != nil { + log.ZError(st.Ctx, "Import Friend failed.", err, "UserID", userCreatedID) + os.Exit(1) + return + } + + // Invite To Group + if err = st.InviteToGroup(st.Ctx, userCreatedID); err != nil { + log.ZError(st.Ctx, "Invite To Group failed.", err, "UserID", userCreatedID) + os.Exit(1) + return + } + + st.Once.Do(func() { + st.DefaultSendUserID = userCreatedID + fmt.Println("Default Send User Created ID:", userCreatedID) + close(ch) + }) + } + } + }() + + st.Wg.Add(1) + go func() { + defer st.Wg.Done() + + ticker := time.NewTicker(SendMessageTicker) + defer ticker.Stop() + <-ch + + for { + select { + case <-st.Ctx.Done(): + log.ZInfo(st.Ctx, "Stop Send message", "reason", "context done") + return + + case <-ticker.C: + // Send Message + if err = st.SendMsg(st.Ctx, st.DefaultSendUserID); err != nil { + log.ZError(st.Ctx, "Send Message failed.", err, "UserID", st.DefaultSendUserID) + continue + } + } + } + }() + + st.Wg.Add(1) + go func() { + defer st.Wg.Done() + + ticker := time.NewTicker(CreateGroupTicker) + defer ticker.Stop() + <-ch + + for st.GroupCounter < MaxGroup { + + select { + case <-st.Ctx.Done(): + log.ZInfo(st.Ctx, "Stop Create Group", "reason", "context done") + return + + case <-ticker.C: + + // Create Group + _, err := st.CreateGroup(st.Ctx, st.DefaultSendUserID) + if err != nil { + log.ZError(st.Ctx, "Create Group failed.", err, "UserID", st.DefaultSendUserID) + os.Exit(1) + return + } + + // fmt.Println("Group Created ID:", groupID) + } + } + }() + + st.Wg.Wait() +} From 3b710fdfdbcc825dabd29afea637df9f484299ae Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Mon, 31 Mar 2025 16:45:52 +0800 Subject: [PATCH 16/59] fix: improve stress test tools parms. (#3265) * feat: implement stress-test tools. * revert config file. * fix: improve tools parms. * fix modify args. --- tools/stress-test/main.go | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/tools/stress-test/main.go b/tools/stress-test/main.go index ee58c9749..aa52b69ed 100755 --- a/tools/stress-test/main.go +++ b/tools/stress-test/main.go @@ -14,6 +14,7 @@ import ( "syscall" "time" + "github.com/openimsdk/open-im-server/v3/pkg/apistruct" "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/protocol/auth" "github.com/openimsdk/protocol/constant" @@ -232,12 +233,15 @@ func (st *StressTest) SendMsg(ctx context.Context, userID string) error { "content": fmt.Sprintf("index %d. The current time is %s", st.MsgCounter, time.Now().Format("2006-01-02 15:04:05.000")), } - req := map[string]any{ - "sendID": userID, - "groupID": st.DefaultGroupID, - "contentType": constant.Text, - "sessionType": constant.ReadGroupChatType, - "content": contentObj, + req := &apistruct.SendMsgReq{ + SendMsg: apistruct.SendMsg{ + SendID: userID, + SenderNickname: userID, + GroupID: st.DefaultGroupID, + ContentType: constant.Text, + SessionType: constant.ReadGroupChatType, + Content: contentObj, + }, } _, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req) @@ -254,15 +258,18 @@ func (st *StressTest) SendMsg(ctx context.Context, userID string) error { func (st *StressTest) CreateGroup(ctx context.Context, userID string) (string, error) { groupID := fmt.Sprintf("StressTestGroup_%d_%s", st.GroupCounter, time.Now().Format("20060102150405")) - req := map[string]any{ - "memberUserIDs": TestTargetUserList, - "ownerUserID": userID, - "groupInfo": map[string]any{ - "groupID": groupID, - "groupName": groupID, - "groupType": constant.WorkingGroup, - }, + groupInfo := &sdkws.GroupInfo{ + GroupID: groupID, + GroupName: groupID, + GroupType: constant.WorkingGroup, + } + + req := group.CreateGroupReq{ + OwnerUserID: userID, + MemberUserIDs: TestTargetUserList, + GroupInfo: groupInfo, } + resp := group.CreateGroupResp{} response, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req) From aca0eac955555cfd9103ee9d7612ec0a621ea225 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Tue, 1 Apr 2025 17:13:32 +0800 Subject: [PATCH 17/59] fix: oss specifies content-type when uploading (#3267) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break --- go.mod | 2 +- go.sum | 4 ++-- internal/api/init.go | 2 +- internal/rpc/third/s3.go | 2 +- pkg/common/storage/controller/s3.go | 6 +++--- tools/s3/internal/conversion.go | 13 +++++++------ version/version.go | 10 +++++++++- 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index d762f9fae..782a306f4 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/openimsdk/protocol v0.0.72-alpha.81 - github.com/openimsdk/tools v0.0.50-alpha.74 + github.com/openimsdk/tools v0.0.50-alpha.79 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index ea5a6e5b7..aa0dfa6ac 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ github.com/openimsdk/gomake v0.0.15-alpha.2 h1:5Q8yl8ezy2yx+q8/ucU/t4kJnDfCzNOrk github.com/openimsdk/gomake v0.0.15-alpha.2/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.72-alpha.81 h1:6tDuZ3Anfi1uhX/V5mWxITqJnGQPnvgeaxeqJlEHIVE= github.com/openimsdk/protocol v0.0.72-alpha.81/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= -github.com/openimsdk/tools v0.0.50-alpha.74 h1:yh10SiMiivMEjicEQg+QAsH4pvaO+4noMPdlw+ew0Kc= -github.com/openimsdk/tools v0.0.50-alpha.74/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= +github.com/openimsdk/tools v0.0.50-alpha.79 h1:jxYEbrzaze4Z2r4NrKad816buZ690ix0L9MTOOOH3ik= +github.com/openimsdk/tools v0.0.50-alpha.79/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/internal/api/init.go b/internal/api/init.go index 4a1404ffc..378f03eda 100644 --- a/internal/api/init.go +++ b/internal/api/init.go @@ -90,7 +90,7 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, service g //case <-ctx.Done(): //} <-apiCtx.Done() - exitCause := context.Cause(ctx) + exitCause := context.Cause(apiCtx) log.ZWarn(ctx, "api server exit", exitCause) timer := time.NewTimer(time.Second * 15) defer timer.Stop() diff --git a/internal/rpc/third/s3.go b/internal/rpc/third/s3.go index 97206dd6d..757320dac 100644 --- a/internal/rpc/third/s3.go +++ b/internal/rpc/third/s3.go @@ -62,7 +62,7 @@ func (t *thirdServer) InitiateMultipartUpload(ctx context.Context, req *third.In return nil, err } expireTime := time.Now().Add(t.defaultExpire) - result, err := t.s3dataBase.InitiateMultipartUpload(ctx, req.Hash, req.Size, t.defaultExpire, int(req.MaxParts)) + result, err := t.s3dataBase.InitiateMultipartUpload(ctx, req.Hash, req.Size, t.defaultExpire, int(req.MaxParts), req.ContentType) if err != nil { if haErr, ok := errs.Unwrap(err).(*cont.HashAlreadyExistsError); ok { obj := &model.Object{ diff --git a/pkg/common/storage/controller/s3.go b/pkg/common/storage/controller/s3.go index 30d8d20ec..9ab31c5a6 100644 --- a/pkg/common/storage/controller/s3.go +++ b/pkg/common/storage/controller/s3.go @@ -33,7 +33,7 @@ type S3Database interface { PartLimit() (*s3.PartLimit, error) PartSize(ctx context.Context, size int64) (int64, error) AuthSign(ctx context.Context, uploadID string, partNumbers []int) (*s3.AuthSignResult, error) - InitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int) (*cont.InitiateUploadResult, error) + InitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int, contentType string) (*cont.InitiateUploadResult, error) CompleteMultipartUpload(ctx context.Context, uploadID string, parts []string) (*cont.UploadResult, error) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (time.Time, string, error) SetObject(ctx context.Context, info *model.Object) error @@ -73,8 +73,8 @@ func (s *s3Database) AuthSign(ctx context.Context, uploadID string, partNumbers return s.s3.AuthSign(ctx, uploadID, partNumbers) } -func (s *s3Database) InitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int) (*cont.InitiateUploadResult, error) { - return s.s3.InitiateUpload(ctx, hash, size, expire, maxParts) +func (s *s3Database) InitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int, contentType string) (*cont.InitiateUploadResult, error) { + return s.s3.InitiateUploadContentType(ctx, hash, size, expire, maxParts, contentType) } func (s *s3Database) CompleteMultipartUpload(ctx context.Context, uploadID string, parts []string) (*cont.UploadResult, error) { diff --git a/tools/s3/internal/conversion.go b/tools/s3/internal/conversion.go index ba2174535..af391ec42 100644 --- a/tools/s3/internal/conversion.go +++ b/tools/s3/internal/conversion.go @@ -4,6 +4,11 @@ import ( "context" "errors" "fmt" + "log" + "net/http" + "path/filepath" + "time" + "github.com/mitchellh/mapstructure" "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis" @@ -19,10 +24,6 @@ import ( "github.com/openimsdk/tools/s3/oss" "github.com/spf13/viper" "go.mongodb.org/mongo-driver/mongo" - "log" - "net/http" - "path/filepath" - "time" ) const defaultTimeout = time.Second * 10 @@ -159,7 +160,7 @@ func doObject(db database.ObjectInfo, newS3, oldS3 s3.Interface, skip int) (*Res if err != nil { return nil, err } - putURL, err := newS3.PresignedPutObject(ctx, obj.Key, time.Hour) + putURL, err := newS3.PresignedPutObject(ctx, obj.Key, time.Hour, &s3.PutOption{ContentType: obj.ContentType}) if err != nil { return nil, err } @@ -176,7 +177,7 @@ func doObject(db database.ObjectInfo, newS3, oldS3 s3.Interface, skip int) (*Res return nil, fmt.Errorf("download object failed %s", downloadResp.Status) } log.Printf("file size %d", obj.Size) - request, err := http.NewRequest(http.MethodPut, putURL, downloadResp.Body) + request, err := http.NewRequest(http.MethodPut, putURL.URL, downloadResp.Body) if err != nil { return nil, err } diff --git a/version/version.go b/version/version.go index 23b3a82f5..32ad27808 100644 --- a/version/version.go +++ b/version/version.go @@ -1,6 +1,14 @@ package version -import _ "embed" +import ( + _ "embed" + "strings" +) //go:embed version var Version string + +func init() { + Version = strings.Trim(Version, "\n") + Version = strings.TrimSpace(Version) +} From d385fdd0aa51a689a3f735991159557ea7549829 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:18:06 +0800 Subject: [PATCH 18/59] feat: support server-issued configuration, which can be set for individual users (#3271) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: support client config * feat: support client config --- go.mod | 4 +- go.sum | 4 +- internal/api/router.go | 5 + internal/api/user.go | 16 +++ internal/rpc/user/config.go | 71 +++++++++++++ internal/rpc/user/user.go | 11 ++- .../storage/cache/cachekey/client_config.go | 10 ++ pkg/common/storage/cache/client_config.go | 8 ++ .../storage/cache/redis/client_config.go | 69 +++++++++++++ .../storage/controller/client_config.go | 58 +++++++++++ pkg/common/storage/database/client_config.go | 15 +++ .../storage/database/mgo/client_config.go | 99 +++++++++++++++++++ pkg/common/storage/model/client_config.go | 7 ++ 13 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 internal/rpc/user/config.go create mode 100644 pkg/common/storage/cache/cachekey/client_config.go create mode 100644 pkg/common/storage/cache/client_config.go create mode 100644 pkg/common/storage/cache/redis/client_config.go create mode 100644 pkg/common/storage/controller/client_config.go create mode 100644 pkg/common/storage/database/client_config.go create mode 100644 pkg/common/storage/database/mgo/client_config.go create mode 100644 pkg/common/storage/model/client_config.go diff --git a/go.mod b/go.mod index 782a306f4..b002ac377 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/openimsdk/protocol v0.0.72-alpha.81 + github.com/openimsdk/protocol v0.0.73-alpha.3 github.com/openimsdk/tools v0.0.50-alpha.79 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 @@ -219,3 +219,5 @@ require ( golang.org/x/crypto v0.27.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) + +//replace github.com/openimsdk/protocol => /Users/chao/Desktop/code/protocol diff --git a/go.sum b/go.sum index aa0dfa6ac..e6408cfcd 100644 --- a/go.sum +++ b/go.sum @@ -347,8 +347,8 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/openimsdk/gomake v0.0.15-alpha.2 h1:5Q8yl8ezy2yx+q8/ucU/t4kJnDfCzNOrkXcDACCqtyM= github.com/openimsdk/gomake v0.0.15-alpha.2/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= -github.com/openimsdk/protocol v0.0.72-alpha.81 h1:6tDuZ3Anfi1uhX/V5mWxITqJnGQPnvgeaxeqJlEHIVE= -github.com/openimsdk/protocol v0.0.72-alpha.81/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= +github.com/openimsdk/protocol v0.0.73-alpha.3 h1:mf/REUZA5in2gk8ggwqJD8444xLvB7WlF7M97oXN78g= +github.com/openimsdk/protocol v0.0.73-alpha.3/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= github.com/openimsdk/tools v0.0.50-alpha.79 h1:jxYEbrzaze4Z2r4NrKad816buZ690ix0L9MTOOOH3ik= github.com/openimsdk/tools v0.0.50-alpha.79/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/internal/api/router.go b/internal/api/router.go index 657493b23..920bd5366 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -125,6 +125,11 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin userRouterGroup.POST("/add_notification_account", u.AddNotificationAccount) userRouterGroup.POST("/update_notification_account", u.UpdateNotificationAccountInfo) userRouterGroup.POST("/search_notification_account", u.SearchNotificationAccount) + + userRouterGroup.POST("/get_user_client_config", u.GetUserClientConfig) + userRouterGroup.POST("/set_user_client_config", u.SetUserClientConfig) + userRouterGroup.POST("/del_user_client_config", u.DelUserClientConfig) + userRouterGroup.POST("/page_user_client_config", u.PageUserClientConfig) } // friend routing group { diff --git a/internal/api/user.go b/internal/api/user.go index 6427e222e..93a311a9b 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -242,3 +242,19 @@ func (u *UserApi) UpdateNotificationAccountInfo(c *gin.Context) { func (u *UserApi) SearchNotificationAccount(c *gin.Context) { a2r.Call(c, user.UserClient.SearchNotificationAccount, u.Client) } + +func (u *UserApi) GetUserClientConfig(c *gin.Context) { + a2r.Call(c, user.UserClient.GetUserClientConfig, u.Client) +} + +func (u *UserApi) SetUserClientConfig(c *gin.Context) { + a2r.Call(c, user.UserClient.SetUserClientConfig, u.Client) +} + +func (u *UserApi) DelUserClientConfig(c *gin.Context) { + a2r.Call(c, user.UserClient.DelUserClientConfig, u.Client) +} + +func (u *UserApi) PageUserClientConfig(c *gin.Context) { + a2r.Call(c, user.UserClient.PageUserClientConfig, u.Client) +} diff --git a/internal/rpc/user/config.go b/internal/rpc/user/config.go new file mode 100644 index 000000000..5a9a46359 --- /dev/null +++ b/internal/rpc/user/config.go @@ -0,0 +1,71 @@ +package user + +import ( + "context" + + "github.com/openimsdk/open-im-server/v3/pkg/authverify" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + pbuser "github.com/openimsdk/protocol/user" + "github.com/openimsdk/tools/utils/datautil" +) + +func (s *userServer) GetUserClientConfig(ctx context.Context, req *pbuser.GetUserClientConfigReq) (*pbuser.GetUserClientConfigResp, error) { + if req.UserID != "" { + if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + return nil, err + } + if _, err := s.db.GetUserByID(ctx, req.UserID); err != nil { + return nil, err + } + } + res, err := s.clientConfig.GetUserConfig(ctx, req.UserID) + if err != nil { + return nil, err + } + return &pbuser.GetUserClientConfigResp{Configs: res}, nil +} + +func (s *userServer) SetUserClientConfig(ctx context.Context, req *pbuser.SetUserClientConfigReq) (*pbuser.SetUserClientConfigResp, error) { + if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + return nil, err + } + if req.UserID != "" { + if _, err := s.db.GetUserByID(ctx, req.UserID); err != nil { + return nil, err + } + } + if err := s.clientConfig.SetUserConfig(ctx, req.UserID, req.Configs); err != nil { + return nil, err + } + return &pbuser.SetUserClientConfigResp{}, nil +} + +func (s *userServer) DelUserClientConfig(ctx context.Context, req *pbuser.DelUserClientConfigReq) (*pbuser.DelUserClientConfigResp, error) { + if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + return nil, err + } + if err := s.clientConfig.DelUserConfig(ctx, req.UserID, req.Keys); err != nil { + return nil, err + } + return &pbuser.DelUserClientConfigResp{}, nil +} + +func (s *userServer) PageUserClientConfig(ctx context.Context, req *pbuser.PageUserClientConfigReq) (*pbuser.PageUserClientConfigResp, error) { + if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + return nil, err + } + total, res, err := s.clientConfig.GetUserConfigPage(ctx, req.UserID, req.Key, req.Pagination) + if err != nil { + return nil, err + } + return &pbuser.PageUserClientConfigResp{ + Total: total, + Configs: datautil.Slice(res, func(e *model.ClientConfig) *pbuser.ClientConfig { + return &pbuser.ClientConfig{ + UserID: e.UserID, + Key: e.Key, + Value: e.Value, + } + }), + }, nil +} diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index 3e8ec3537..6ef61f773 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -64,6 +64,7 @@ type userServer struct { webhookClient *webhook.Client groupClient *rpcli.GroupClient relationClient *rpcli.RelationClient + clientConfig controller.ClientConfigDatabase } type Config struct { @@ -98,6 +99,10 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr if err != nil { return err } + clientConfigDB, err := mgo.NewClientConfig(mgocli.GetDB()) + if err != nil { + return err + } msgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg) if err != nil { return err @@ -122,9 +127,9 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr userNotificationSender: NewUserNotificationSender(config, msgClient, WithUserFunc(database.FindWithError)), config: config, webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL), - - groupClient: rpcli.NewGroupClient(groupConn), - relationClient: rpcli.NewRelationClient(friendConn), + clientConfig: controller.NewClientConfigDatabase(clientConfigDB, redis.NewClientConfigCache(rdb, clientConfigDB), mgocli.GetTx()), + groupClient: rpcli.NewGroupClient(groupConn), + relationClient: rpcli.NewRelationClient(friendConn), } pbuser.RegisterUserServer(server, u) return u.db.InitOnce(context.Background(), users) diff --git a/pkg/common/storage/cache/cachekey/client_config.go b/pkg/common/storage/cache/cachekey/client_config.go new file mode 100644 index 000000000..16770adef --- /dev/null +++ b/pkg/common/storage/cache/cachekey/client_config.go @@ -0,0 +1,10 @@ +package cachekey + +const ClientConfig = "CLIENT_CONFIG" + +func GetClientConfigKey(userID string) string { + if userID == "" { + return ClientConfig + } + return ClientConfig + ":" + userID +} diff --git a/pkg/common/storage/cache/client_config.go b/pkg/common/storage/cache/client_config.go new file mode 100644 index 000000000..329f25c59 --- /dev/null +++ b/pkg/common/storage/cache/client_config.go @@ -0,0 +1,8 @@ +package cache + +import "context" + +type ClientConfigCache interface { + DeleteUserCache(ctx context.Context, userIDs []string) error + GetUserConfig(ctx context.Context, userID string) (map[string]string, error) +} diff --git a/pkg/common/storage/cache/redis/client_config.go b/pkg/common/storage/cache/redis/client_config.go new file mode 100644 index 000000000..c5a455146 --- /dev/null +++ b/pkg/common/storage/cache/redis/client_config.go @@ -0,0 +1,69 @@ +package redis + +import ( + "context" + "time" + + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" + "github.com/redis/go-redis/v9" +) + +func NewClientConfigCache(rdb redis.UniversalClient, mgo database.ClientConfig) cache.ClientConfigCache { + rc := newRocksCacheClient(rdb) + return &ClientConfigCache{ + mgo: mgo, + rcClient: rc, + delete: rc.GetBatchDeleter(), + } +} + +type ClientConfigCache struct { + mgo database.ClientConfig + rcClient *rocksCacheClient + delete cache.BatchDeleter +} + +func (x *ClientConfigCache) getExpireTime(userID string) time.Duration { + if userID == "" { + return time.Hour * 24 + } else { + return time.Hour + } +} + +func (x *ClientConfigCache) getClientConfigKey(userID string) string { + return cachekey.GetClientConfigKey(userID) +} + +func (x *ClientConfigCache) GetConfig(ctx context.Context, userID string) (map[string]string, error) { + return getCache(ctx, x.rcClient, x.getClientConfigKey(userID), x.getExpireTime(userID), func(ctx context.Context) (map[string]string, error) { + return x.mgo.Get(ctx, userID) + }) +} + +func (x *ClientConfigCache) DeleteUserCache(ctx context.Context, userIDs []string) error { + keys := make([]string, 0, len(userIDs)) + for _, userID := range userIDs { + keys = append(keys, x.getClientConfigKey(userID)) + } + return x.delete.ExecDelWithKeys(ctx, keys) +} + +func (x *ClientConfigCache) GetUserConfig(ctx context.Context, userID string) (map[string]string, error) { + config, err := x.GetConfig(ctx, "") + if err != nil { + return nil, err + } + if userID != "" { + userConfig, err := x.GetConfig(ctx, userID) + if err != nil { + return nil, err + } + for k, v := range userConfig { + config[k] = v + } + } + return config, nil +} diff --git a/pkg/common/storage/controller/client_config.go b/pkg/common/storage/controller/client_config.go new file mode 100644 index 000000000..1c3787634 --- /dev/null +++ b/pkg/common/storage/controller/client_config.go @@ -0,0 +1,58 @@ +package controller + +import ( + "context" + + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" + "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/db/pagination" + "github.com/openimsdk/tools/db/tx" +) + +type ClientConfigDatabase interface { + SetUserConfig(ctx context.Context, userID string, config map[string]string) error + GetUserConfig(ctx context.Context, userID string) (map[string]string, error) + DelUserConfig(ctx context.Context, userID string, keys []string) error + GetUserConfigPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) +} + +func NewClientConfigDatabase(db database.ClientConfig, cache cache.ClientConfigCache, tx tx.Tx) ClientConfigDatabase { + return &clientConfigDatabase{ + tx: tx, + db: db, + cache: cache, + } +} + +type clientConfigDatabase struct { + tx tx.Tx + db database.ClientConfig + cache cache.ClientConfigCache +} + +func (x *clientConfigDatabase) SetUserConfig(ctx context.Context, userID string, config map[string]string) error { + return x.tx.Transaction(ctx, func(ctx context.Context) error { + if err := x.db.Set(ctx, userID, config); err != nil { + return err + } + return x.cache.DeleteUserCache(ctx, []string{userID}) + }) +} + +func (x *clientConfigDatabase) GetUserConfig(ctx context.Context, userID string) (map[string]string, error) { + return x.cache.GetUserConfig(ctx, userID) +} + +func (x *clientConfigDatabase) DelUserConfig(ctx context.Context, userID string, keys []string) error { + return x.tx.Transaction(ctx, func(ctx context.Context) error { + if err := x.db.Del(ctx, userID, keys); err != nil { + return err + } + return x.cache.DeleteUserCache(ctx, []string{userID}) + }) +} + +func (x *clientConfigDatabase) GetUserConfigPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) { + return x.db.GetPage(ctx, userID, key, pagination) +} diff --git a/pkg/common/storage/database/client_config.go b/pkg/common/storage/database/client_config.go new file mode 100644 index 000000000..7fa888d24 --- /dev/null +++ b/pkg/common/storage/database/client_config.go @@ -0,0 +1,15 @@ +package database + +import ( + "context" + + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/tools/db/pagination" +) + +type ClientConfig interface { + Set(ctx context.Context, userID string, config map[string]string) error + Get(ctx context.Context, userID string) (map[string]string, error) + Del(ctx context.Context, userID string, keys []string) error + GetPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) +} diff --git a/pkg/common/storage/database/mgo/client_config.go b/pkg/common/storage/database/mgo/client_config.go new file mode 100644 index 000000000..0aa462899 --- /dev/null +++ b/pkg/common/storage/database/mgo/client_config.go @@ -0,0 +1,99 @@ +// Copyright © 2023 OpenIM open source community. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mgo + +import ( + "context" + + "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/db/mongoutil" + "github.com/openimsdk/tools/db/pagination" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/openimsdk/tools/errs" +) + +func NewClientConfig(db *mongo.Database) (database.ClientConfig, error) { + coll := db.Collection("config") + _, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "key", Value: 1}, + {Key: "user_id", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + }) + if err != nil { + return nil, errs.Wrap(err) + } + return &ClientConfig{ + coll: coll, + }, nil +} + +type ClientConfig struct { + coll *mongo.Collection +} + +func (x *ClientConfig) Set(ctx context.Context, userID string, config map[string]string) error { + if len(config) == 0 { + return nil + } + for key, value := range config { + filter := bson.M{"key": key, "user_id": userID} + update := bson.M{ + "value": value, + } + err := mongoutil.UpdateOne(ctx, x.coll, filter, bson.M{"$set": update}, false, options.Update().SetUpsert(true)) + if err != nil { + return err + } + } + return nil +} + +func (x *ClientConfig) Get(ctx context.Context, userID string) (map[string]string, error) { + cs, err := mongoutil.Find[*model.ClientConfig](ctx, x.coll, bson.M{"user_id": userID}) + if err != nil { + return nil, err + } + cm := make(map[string]string) + for _, config := range cs { + cm[config.Key] = config.Value + } + return cm, nil +} + +func (x *ClientConfig) Del(ctx context.Context, userID string, keys []string) error { + if len(keys) == 0 { + return nil + } + return mongoutil.DeleteMany(ctx, x.coll, bson.M{"key": bson.M{"$in": keys}, "user_id": userID}) +} + +func (x *ClientConfig) GetPage(ctx context.Context, userID string, key string, pagination pagination.Pagination) (int64, []*model.ClientConfig, error) { + filter := bson.M{} + if userID != "" { + filter["user_id"] = userID + } + if key != "" { + filter["key"] = key + } + return mongoutil.FindPage[*model.ClientConfig](ctx, x.coll, filter, pagination) +} diff --git a/pkg/common/storage/model/client_config.go b/pkg/common/storage/model/client_config.go new file mode 100644 index 000000000..f06e29102 --- /dev/null +++ b/pkg/common/storage/model/client_config.go @@ -0,0 +1,7 @@ +package model + +type ClientConfig struct { + Key string `bson:"key"` + UserID string `bson:"user_id"` + Value string `bson:"value"` +} From 16976511002c75c8a4c5fd879741d7535401ad66 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:18:07 +0800 Subject: [PATCH 19/59] feat: GetConversationsHasReadAndMaxSeq support pinned (#3281) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned --- go.mod | 4 +--- go.sum | 4 ++-- internal/rpc/msg/as_read.go | 7 +++++++ pkg/rpccache/conversation.go | 25 +++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index b002ac377..fbaa19434 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/openimsdk/protocol v0.0.73-alpha.3 + github.com/openimsdk/protocol v0.0.73-alpha.6 github.com/openimsdk/tools v0.0.50-alpha.79 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 @@ -219,5 +219,3 @@ require ( golang.org/x/crypto v0.27.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) - -//replace github.com/openimsdk/protocol => /Users/chao/Desktop/code/protocol diff --git a/go.sum b/go.sum index e6408cfcd..390a51c4a 100644 --- a/go.sum +++ b/go.sum @@ -347,8 +347,8 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/openimsdk/gomake v0.0.15-alpha.2 h1:5Q8yl8ezy2yx+q8/ucU/t4kJnDfCzNOrkXcDACCqtyM= github.com/openimsdk/gomake v0.0.15-alpha.2/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= -github.com/openimsdk/protocol v0.0.73-alpha.3 h1:mf/REUZA5in2gk8ggwqJD8444xLvB7WlF7M97oXN78g= -github.com/openimsdk/protocol v0.0.73-alpha.3/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= +github.com/openimsdk/protocol v0.0.73-alpha.6 h1:sna9coWG7HN1zObBPtvG0Ki/vzqHXiB4qKbA5P3w7kc= +github.com/openimsdk/protocol v0.0.73-alpha.6/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= github.com/openimsdk/tools v0.0.50-alpha.79 h1:jxYEbrzaze4Z2r4NrKad816buZ690ix0L9MTOOOH3ik= github.com/openimsdk/tools v0.0.50-alpha.79/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/internal/rpc/msg/as_read.go b/internal/rpc/msg/as_read.go index de1879438..b25eae6b1 100644 --- a/internal/rpc/msg/as_read.go +++ b/internal/rpc/msg/as_read.go @@ -61,6 +61,13 @@ func (m *msgServer) GetConversationsHasReadAndMaxSeq(ctx context.Context, req *m return nil, err } resp := &msg.GetConversationsHasReadAndMaxSeqResp{Seqs: make(map[string]*msg.Seqs)} + if req.ReturnPinned { + pinnedConversationIDs, err := m.ConversationLocalCache.GetPinnedConversationIDs(ctx, req.UserID) + if err != nil { + return nil, err + } + resp.PinnedConversationIDs = pinnedConversationIDs + } for conversationID, maxSeq := range maxSeqs { resp.Seqs[conversationID] = &msg.Seqs{ HasReadSeq: hasReadSeqs[conversationID], diff --git a/pkg/rpccache/conversation.go b/pkg/rpccache/conversation.go index 70f5acfd1..162fda596 100644 --- a/pkg/rpccache/conversation.go +++ b/pkg/rpccache/conversation.go @@ -16,6 +16,7 @@ package rpccache import ( "context" + "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" "github.com/openimsdk/open-im-server/v3/pkg/localcache" @@ -153,6 +154,26 @@ func (c *ConversationLocalCache) getConversationNotReceiveMessageUserIDs(ctx con })) } +func (c *ConversationLocalCache) getPinnedConversationIDs(ctx context.Context, userID string) (val []string, err error) { + log.ZDebug(ctx, "ConversationLocalCache getPinnedConversations req", "userID", userID) + defer func() { + if err == nil { + log.ZDebug(ctx, "ConversationLocalCache getPinnedConversations return", "userID", userID, "value", val) + } else { + log.ZError(ctx, "ConversationLocalCache getPinnedConversations return", err, "userID", userID) + } + }() + var cache cacheProto[pbconversation.GetPinnedConversationIDsResp] + resp, err := cache.Unmarshal(c.local.Get(ctx, cachekey.GetPinnedConversationIDs(userID), func(ctx context.Context) ([]byte, error) { + log.ZDebug(ctx, "ConversationLocalCache getConversationNotReceiveMessageUserIDs rpc", "userID", userID) + return cache.Marshal(c.client.ConversationClient.GetPinnedConversationIDs(ctx, &pbconversation.GetPinnedConversationIDsReq{UserID: userID})) + })) + if err != nil { + return nil, err + } + return resp.ConversationIDs, nil +} + func (c *ConversationLocalCache) GetConversationNotReceiveMessageUserIDs(ctx context.Context, conversationID string) ([]string, error) { res, err := c.getConversationNotReceiveMessageUserIDs(ctx, conversationID) if err != nil { @@ -168,3 +189,7 @@ func (c *ConversationLocalCache) GetConversationNotReceiveMessageUserIDMap(ctx c } return datautil.SliceSet(res.UserIDs), nil } + +func (c *ConversationLocalCache) GetPinnedConversationIDs(ctx context.Context, userID string) ([]string, error) { + return c.getPinnedConversationIDs(ctx, userID) +} From 52bd5e8be9027de8ca9aca2407469b915eb8a97d Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:27:39 +0800 Subject: [PATCH 20/59] fix: transferring the group owner to a muted member, incremental version error (#3284) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error --- internal/rpc/group/notification.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index 0a135af23..f9da88863 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -284,7 +284,8 @@ func (g *NotificationSender) fillUserByUserID(ctx context.Context, userID string func (g *NotificationSender) setVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string) { versions := versionctx.GetVersionLog(ctx).Get() - for _, coll := range versions { + for i := len(versions) - 1; i >= 0; i-- { + coll := versions[i] if coll.Name == collName && coll.Doc.DID == id { *version = uint64(coll.Doc.Version) *versionID = coll.Doc.ID.Hex() From 8e824c7e8eb58265a6e4c32edb5ad2bed2f1be67 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:28:23 +0800 Subject: [PATCH 21/59] fix: group status in GroupDismissedNotification (#3286) --- internal/rpc/group/group.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index e1604e7e5..63d9336d5 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -1370,6 +1370,7 @@ func (g *groupServer) DismissGroup(ctx context.Context, req *pbgroup.DismissGrou if err != nil { return nil, err } + group.Status = constant.GroupStatusDismissed tips := &sdkws.GroupDismissedTips{ Group: g.groupDB2PB(group, owner.UserID, num), OpUser: &sdkws.GroupMemberFullInfo{}, From 045afd74b9049bba137ff8df6ef48ebc6d4f33b2 Mon Sep 17 00:00:00 2001 From: skiffer-git <72860476+skiffer-git@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:35:50 +0800 Subject: [PATCH 22/59] Update LICENSE --- LICENSE | 670 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 648 insertions(+), 22 deletions(-) diff --git a/LICENSE b/LICENSE index 4591ca426..0ad25db4b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,35 +1,661 @@ -# Open Source License + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -OpenIM is licensed under the Apache License 2.0, with the following additional conditions: + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -1. OpenIM may be utilized commercially, including as a backend service for other applications or as an application development platform for enterprises. -A commercial license must be obtained from the producer if: - - Under no circumstances may you operate a multi-tenant or multi-business environment using the OpenIM source code, whether or not you have modified the repository code. In other words, a single instance of OpenIM may not simultaneously serve multiple enterprises, nor may it serve multiple lines of business within the same enterprise. - - If you intend to operate in such a multi-tenant or multi-business manner, you must obtain a commercial license from the producer in advance. + Preamble -2. As a contributor, you should agree that: + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. - a. The producer can adjust the open-source agreement to be more strict or more relaxed as deemed necessary. - b. Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. -Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. -For any licensing-related questions or to obtain a commercial license, please contact contact@openim.io. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. -© 2024 OpenIMSDK + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. ----------- + The precise terms and conditions for copying, distribution and +modification follow. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + TERMS AND CONDITIONS - http://www.apache.org/licenses/LICENSE-2.0 + 0. Definitions. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From 97b8c07d57337baf2d4b6b7d89c7ac4236135a84 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Tue, 22 Apr 2025 15:52:55 +0800 Subject: [PATCH 23/59] feat: Implement stress test v2. (#3292) * feat: improve stress test code. * feat: Implement stress test v2. --- tools/stress-test-v2/main.go | 736 +++++++++++++++++++++++++++++++++++ tools/stress-test/main.go | 43 +- 2 files changed, 757 insertions(+), 22 deletions(-) create mode 100644 tools/stress-test-v2/main.go diff --git a/tools/stress-test-v2/main.go b/tools/stress-test-v2/main.go new file mode 100644 index 000000000..0c309b9c9 --- /dev/null +++ b/tools/stress-test-v2/main.go @@ -0,0 +1,736 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/openimsdk/open-im-server/v3/pkg/apistruct" + "github.com/openimsdk/open-im-server/v3/pkg/common/config" + "github.com/openimsdk/protocol/auth" + "github.com/openimsdk/protocol/constant" + "github.com/openimsdk/protocol/group" + "github.com/openimsdk/protocol/sdkws" + pbuser "github.com/openimsdk/protocol/user" + "github.com/openimsdk/tools/log" + "github.com/openimsdk/tools/system/program" +) + +// 1. Create 100K New Users +// 2. Create 100 100K Groups +// 3. Create 1000 999 Groups +// 4. Send message to 100K Groups every second +// 5. Send message to 999 Groups every minute + +var ( + // Use default userIDs List for testing, need to be created. + TestTargetUserList = []string{ + // "", + } + // DefaultGroupID = "" // Use default group ID for testing, need to be created. +) + +var ( + ApiAddress string + + // API method + GetAdminToken = "/auth/get_admin_token" + UserCheck = "/user/account_check" + CreateUser = "/user/user_register" + ImportFriend = "/friend/import_friend" + InviteToGroup = "/group/invite_user_to_group" + GetGroupMemberInfo = "/group/get_group_members_info" + SendMsg = "/msg/send_msg" + CreateGroup = "/group/create_group" + GetUserToken = "/auth/user_token" +) + +const ( + MaxUser = 100000 + Max100KGroup = 100 + Max999Group = 1000 + MaxInviteUserLimit = 999 + + CreateUserTicker = 1 * time.Second + CreateGroupTicker = 1 * time.Second + Create100KGroupTicker = 1 * time.Second + Create999GroupTicker = 1 * time.Second + SendMsgTo100KGroupTicker = 1 * time.Second + SendMsgTo999GroupTicker = 1 * time.Minute +) + +type BaseResp struct { + ErrCode int `json:"errCode"` + ErrMsg string `json:"errMsg"` + Data json.RawMessage `json:"data"` +} + +type StressTest struct { + Conf *conf + AdminUserID string + AdminToken string + DefaultGroupID string + DefaultUserID string + UserCounter int + CreateUserCounter int + Create100kGroupCounter int + Create999GroupCounter int + MsgCounter int + CreatedUsers []string + CreatedGroups []string + Mutex sync.Mutex + Ctx context.Context + Cancel context.CancelFunc + HttpClient *http.Client + Wg sync.WaitGroup + Once sync.Once +} + +type conf struct { + Share config.Share + Api config.API +} + +func initConfig(configDir string) (*config.Share, *config.API, error) { + var ( + share = &config.Share{} + apiConfig = &config.API{} + ) + + err := config.Load(configDir, config.ShareFileName, config.EnvPrefixMap[config.ShareFileName], share) + if err != nil { + return nil, nil, err + } + + err = config.Load(configDir, config.OpenIMAPICfgFileName, config.EnvPrefixMap[config.OpenIMAPICfgFileName], apiConfig) + if err != nil { + return nil, nil, err + } + + return share, apiConfig, nil +} + +// Post Request +func (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) ([]byte, error) { + // Marshal body + jsonBody, err := json.Marshal(reqbody) + if err != nil { + log.ZError(ctx, "Failed to marshal request body", err, "url", url, "reqbody", reqbody) + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("operationID", st.AdminUserID) + if st.AdminToken != "" { + req.Header.Set("token", st.AdminToken) + } + + // log.ZInfo(ctx, "Header info is ", "Content-Type", "application/json", "operationID", st.AdminUserID, "token", st.AdminToken) + + resp, err := st.HttpClient.Do(req) + if err != nil { + log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody) + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + log.ZError(ctx, "Failed to read response body", err, "url", url) + return nil, err + } + + var baseResp BaseResp + if err := json.Unmarshal(respBody, &baseResp); err != nil { + log.ZError(ctx, "Failed to unmarshal response body", err, "url", url, "respBody", string(respBody)) + return nil, err + } + + if baseResp.ErrCode != 0 { + err = fmt.Errorf(baseResp.ErrMsg) + log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody, "resp", baseResp) + return nil, err + } + + return baseResp.Data, nil +} + +func (st *StressTest) GetAdminToken(ctx context.Context) (string, error) { + req := auth.GetAdminTokenReq{ + Secret: st.Conf.Share.Secret, + UserID: st.AdminUserID, + } + + resp, err := st.PostRequest(ctx, ApiAddress+GetAdminToken, &req) + if err != nil { + return "", err + } + + data := &auth.GetAdminTokenResp{} + if err := json.Unmarshal(resp, &data); err != nil { + return "", err + } + + return data.Token, nil +} + +func (st *StressTest) CheckUser(ctx context.Context, userIDs []string) ([]string, error) { + req := pbuser.AccountCheckReq{ + CheckUserIDs: userIDs, + } + + resp, err := st.PostRequest(ctx, ApiAddress+UserCheck, &req) + if err != nil { + return nil, err + } + + data := &pbuser.AccountCheckResp{} + if err := json.Unmarshal(resp, &data); err != nil { + return nil, err + } + + unRegisteredUserIDs := make([]string, 0) + + for _, res := range data.Results { + if res.AccountStatus == constant.UnRegistered { + unRegisteredUserIDs = append(unRegisteredUserIDs, res.UserID) + } + } + + return unRegisteredUserIDs, nil +} + +func (st *StressTest) CreateUser(ctx context.Context, userID string) (string, error) { + user := &sdkws.UserInfo{ + UserID: userID, + Nickname: userID, + } + + req := pbuser.UserRegisterReq{ + Users: []*sdkws.UserInfo{user}, + } + + _, err := st.PostRequest(ctx, ApiAddress+CreateUser, &req) + if err != nil { + return "", err + } + + st.UserCounter++ + return userID, nil +} + +func (st *StressTest) CreateUserBatch(ctx context.Context, userIDs []string) error { + // The method can import a large number of users at once. + var userList []*sdkws.UserInfo + + defer st.Once.Do( + func() { + st.DefaultUserID = userIDs[0] + fmt.Println("Default Send User Created ID:", st.DefaultUserID) + }) + + needUserIDs, err := st.CheckUser(ctx, userIDs) + if err != nil { + return err + } + + for _, userID := range needUserIDs { + user := &sdkws.UserInfo{ + UserID: userID, + Nickname: userID, + } + userList = append(userList, user) + } + + req := pbuser.UserRegisterReq{ + Users: userList, + } + + _, err = st.PostRequest(ctx, ApiAddress+CreateUser, &req) + if err != nil { + return err + } + + st.UserCounter += len(userList) + return nil +} + +func (st *StressTest) GetGroupMembersInfo(ctx context.Context, groupID string, userIDs []string) ([]string, error) { + needInviteUserIDs := make([]string, 0) + + const maxBatchSize = 500 + if len(userIDs) > maxBatchSize { + for i := 0; i < len(userIDs); i += maxBatchSize { + end := min(i+maxBatchSize, len(userIDs)) + batchUserIDs := userIDs[i:end] + + // log.ZInfo(ctx, "Processing group members batch", "groupID", groupID, "batch", i/maxBatchSize+1, + // "batchUserCount", len(batchUserIDs)) + + // Process a single batch + batchReq := group.GetGroupMembersInfoReq{ + GroupID: groupID, + UserIDs: batchUserIDs, + } + + resp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &batchReq) + if err != nil { + log.ZError(ctx, "Batch query failed", err, "batch", i/maxBatchSize+1) + continue + } + + data := &group.GetGroupMembersInfoResp{} + if err := json.Unmarshal(resp, &data); err != nil { + log.ZError(ctx, "Failed to parse batch response", err, "batch", i/maxBatchSize+1) + continue + } + + // Process the batch results + existingMembers := make(map[string]bool) + for _, member := range data.Members { + existingMembers[member.UserID] = true + } + + for _, userID := range batchUserIDs { + if !existingMembers[userID] { + needInviteUserIDs = append(needInviteUserIDs, userID) + } + } + } + + return needInviteUserIDs, nil + } + + req := group.GetGroupMembersInfoReq{ + GroupID: groupID, + UserIDs: userIDs, + } + + resp, err := st.PostRequest(ctx, ApiAddress+GetGroupMemberInfo, &req) + if err != nil { + return nil, err + } + + data := &group.GetGroupMembersInfoResp{} + if err := json.Unmarshal(resp, &data); err != nil { + return nil, err + } + + existingMembers := make(map[string]bool) + for _, member := range data.Members { + existingMembers[member.UserID] = true + } + + for _, userID := range userIDs { + if !existingMembers[userID] { + needInviteUserIDs = append(needInviteUserIDs, userID) + } + } + + return needInviteUserIDs, nil +} + +func (st *StressTest) InviteToGroup(ctx context.Context, groupID string, userIDs []string) error { + req := group.InviteUserToGroupReq{ + GroupID: groupID, + InvitedUserIDs: userIDs, + } + _, err := st.PostRequest(ctx, ApiAddress+InviteToGroup, &req) + if err != nil { + return err + } + + return nil +} + +func (st *StressTest) SendMsg(ctx context.Context, userID string, groupID string) error { + contentObj := map[string]any{ + // "content": fmt.Sprintf("index %d. The current time is %s", st.MsgCounter, time.Now().Format("2006-01-02 15:04:05.000")), + "content": fmt.Sprintf("The current time is %s", time.Now().Format("2006-01-02 15:04:05.000")), + } + + req := &apistruct.SendMsgReq{ + SendMsg: apistruct.SendMsg{ + SendID: userID, + SenderNickname: userID, + GroupID: groupID, + ContentType: constant.Text, + SessionType: constant.ReadGroupChatType, + Content: contentObj, + }, + } + + _, err := st.PostRequest(ctx, ApiAddress+SendMsg, &req) + if err != nil { + log.ZError(ctx, "Failed to send message", err, "userID", userID, "req", &req) + return err + } + + st.MsgCounter++ + + return nil +} + +// Max userIDs number is 1000 +func (st *StressTest) CreateGroup(ctx context.Context, groupID string, userID string, userIDsList []string) (string, error) { + groupInfo := &sdkws.GroupInfo{ + GroupID: groupID, + GroupName: groupID, + GroupType: constant.WorkingGroup, + } + + req := group.CreateGroupReq{ + OwnerUserID: userID, + MemberUserIDs: userIDsList, + GroupInfo: groupInfo, + } + + resp := group.CreateGroupResp{} + + response, err := st.PostRequest(ctx, ApiAddress+CreateGroup, &req) + if err != nil { + return "", err + } + + if err := json.Unmarshal(response, &resp); err != nil { + return "", err + } + + // st.GroupCounter++ + + return resp.GroupInfo.GroupID, nil +} + +func main() { + var configPath string + // defaultConfigDir := filepath.Join("..", "..", "..", "..", "..", "config") + // flag.StringVar(&configPath, "c", defaultConfigDir, "config path") + flag.StringVar(&configPath, "c", "", "config path") + flag.Parse() + + if configPath == "" { + _, _ = fmt.Fprintln(os.Stderr, "config path is empty") + os.Exit(1) + return + } + + fmt.Printf(" Config Path: %s\n", configPath) + + share, apiConfig, err := initConfig(configPath) + if err != nil { + program.ExitWithError(err) + return + } + + ApiAddress = fmt.Sprintf("http://%s:%s", "127.0.0.1", fmt.Sprint(apiConfig.Api.Ports[0])) + + ctx, cancel := context.WithCancel(context.Background()) + // ch := make(chan struct{}) + + st := &StressTest{ + Conf: &conf{ + Share: *share, + Api: *apiConfig, + }, + AdminUserID: share.IMAdminUserID[0], + Ctx: ctx, + Cancel: cancel, + HttpClient: &http.Client{ + Timeout: 50 * time.Second, + }, + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("\nReceived stop signal, stopping...") + + go func() { + // time.Sleep(5 * time.Second) + fmt.Println("Force exit") + os.Exit(0) + }() + + st.Cancel() + }() + + token, err := st.GetAdminToken(st.Ctx) + if err != nil { + log.ZError(ctx, "Get Admin Token failed.", err, "AdminUserID", st.AdminUserID) + } + + st.AdminToken = token + fmt.Println("Admin Token:", st.AdminToken) + fmt.Println("ApiAddress:", ApiAddress) + + for i := range MaxUser { + userID := fmt.Sprintf("v2_StressTest_User_%d", i) + st.CreatedUsers = append(st.CreatedUsers, userID) + st.CreateUserCounter++ + } + + // err = st.CreateUserBatch(st.Ctx, st.CreatedUsers) + // if err != nil { + // log.ZError(ctx, "Create user failed.", err) + // } + + const batchSize = 1000 + totalUsers := len(st.CreatedUsers) + successCount := 0 + + if st.DefaultUserID == "" && len(st.CreatedUsers) > 0 { + st.DefaultUserID = st.CreatedUsers[0] + } + + for i := 0; i < totalUsers; i += batchSize { + end := min(i+batchSize, totalUsers) + + userBatch := st.CreatedUsers[i:end] + log.ZInfo(st.Ctx, "Creating user batch", "batch", i/batchSize+1, "count", len(userBatch)) + + err = st.CreateUserBatch(st.Ctx, userBatch) + if err != nil { + log.ZError(st.Ctx, "Batch user creation failed", err, "batch", i/batchSize+1) + } else { + successCount += len(userBatch) + log.ZInfo(st.Ctx, "Batch user creation succeeded", "batch", i/batchSize+1, + "progress", fmt.Sprintf("%d/%d", successCount, totalUsers)) + } + } + + // Execute create 100k group + st.Wg.Add(1) + go func() { + defer st.Wg.Done() + + create100kGroupTicker := time.NewTicker(Create100KGroupTicker) + defer create100kGroupTicker.Stop() + + for i := range Max100KGroup { + select { + case <-st.Ctx.Done(): + log.ZInfo(st.Ctx, "Stop Create 100K Group") + return + + case <-create100kGroupTicker.C: + // Create 100K groups + st.Wg.Add(1) + go func(idx int) { + defer st.Wg.Done() + defer func() { + st.Create100kGroupCounter++ + }() + + groupID := fmt.Sprintf("v2_StressTest_Group_100K_%d", idx) + + if _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil { + log.ZError(st.Ctx, "Create group failed.", err) + // continue + } + + for i := 0; i < MaxUser/MaxInviteUserLimit; i++ { + InviteUserIDs := make([]string, 0) + // ensure TargetUserList is in group + InviteUserIDs = append(InviteUserIDs, TestTargetUserList...) + + startIdx := max(i*MaxInviteUserLimit, 1) + endIdx := min((i+1)*MaxInviteUserLimit, MaxUser) + + for j := startIdx; j < endIdx; j++ { + userCreatedID := fmt.Sprintf("v2_StressTest_User_%d", j) + InviteUserIDs = append(InviteUserIDs, userCreatedID) + } + + if len(InviteUserIDs) == 0 { + log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + continue + } + + InviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs) + if err != nil { + log.ZError(st.Ctx, "GetGroupMembersInfo failed.", err, "groupID", groupID) + continue + } + + if len(InviteUserIDs) == 0 { + log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + continue + } + + // Invite To Group + if err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil { + log.ZError(st.Ctx, "Invite To Group failed.", err, "UserID", InviteUserIDs) + continue + // os.Exit(1) + // return + } + } + }(i) + } + } + }() + + // create 999 groups + st.Wg.Add(1) + go func() { + defer st.Wg.Done() + + create999GroupTicker := time.NewTicker(Create999GroupTicker) + defer create999GroupTicker.Stop() + + for i := range Max999Group { + select { + case <-st.Ctx.Done(): + log.ZInfo(st.Ctx, "Stop Create 999 Group") + return + + case <-create999GroupTicker.C: + // Create 999 groups + st.Wg.Add(1) + go func(idx int) { + defer st.Wg.Done() + defer func() { + st.Create999GroupCounter++ + }() + + groupID := fmt.Sprintf("v2_StressTest_Group_1K_%d", idx) + + if _, err = st.CreateGroup(st.Ctx, groupID, st.DefaultUserID, TestTargetUserList); err != nil { + log.ZError(st.Ctx, "Create group failed.", err) + // continue + } + for i := 0; i < MaxUser/MaxInviteUserLimit; i++ { + InviteUserIDs := make([]string, 0) + // ensure TargetUserList is in group + InviteUserIDs = append(InviteUserIDs, TestTargetUserList...) + + startIdx := max(i*MaxInviteUserLimit, 1) + endIdx := min((i+1)*MaxInviteUserLimit, MaxUser) + + for j := startIdx; j < endIdx; j++ { + userCreatedID := fmt.Sprintf("v2_StressTest_User_%d", j) + InviteUserIDs = append(InviteUserIDs, userCreatedID) + } + + if len(InviteUserIDs) == 0 { + log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + continue + } + + InviteUserIDs, err := st.GetGroupMembersInfo(ctx, groupID, InviteUserIDs) + if err != nil { + log.ZError(st.Ctx, "GetGroupMembersInfo failed.", err, "groupID", groupID) + continue + } + + if len(InviteUserIDs) == 0 { + log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + continue + } + + // Invite To Group + if err = st.InviteToGroup(st.Ctx, groupID, InviteUserIDs); err != nil { + log.ZError(st.Ctx, "Invite To Group failed.", err, "UserID", InviteUserIDs) + continue + // os.Exit(1) + // return + } + } + }(i) + } + } + }() + + // Send message to 100K groups + st.Wg.Wait() + fmt.Println("All groups created successfully, starting to send messages...") + log.ZInfo(ctx, "All groups created successfully, starting to send messages...") + + var groups100K []string + var groups999 []string + + for i := range Max100KGroup { + groupID := fmt.Sprintf("v2_StressTest_Group_100K_%d", i) + groups100K = append(groups100K, groupID) + } + + for i := range Max999Group { + groupID := fmt.Sprintf("v2_StressTest_Group_1K_%d", i) + groups999 = append(groups999, groupID) + } + + send100kGroupLimiter := make(chan struct{}, 20) + send999GroupLimiter := make(chan struct{}, 100) + + // execute Send message to 100K groups + go func() { + ticker := time.NewTicker(SendMsgTo100KGroupTicker) + defer ticker.Stop() + + for { + select { + case <-st.Ctx.Done(): + log.ZInfo(st.Ctx, "Stop Send Message to 100K Group") + return + + case <-ticker.C: + // Send message to 100K groups + for _, groupID := range groups100K { + send100kGroupLimiter <- struct{}{} + go func(groupID string) { + defer func() { <-send100kGroupLimiter }() + if err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil { + log.ZError(st.Ctx, "Send message to 100K group failed.", err) + } + }(groupID) + } + // log.ZInfo(st.Ctx, "Send message to 100K groups successfully.") + } + } + }() + + // execute Send message to 999 groups + go func() { + ticker := time.NewTicker(SendMsgTo999GroupTicker) + defer ticker.Stop() + + for { + select { + case <-st.Ctx.Done(): + log.ZInfo(st.Ctx, "Stop Send Message to 999 Group") + return + + case <-ticker.C: + // Send message to 999 groups + for _, groupID := range groups999 { + send999GroupLimiter <- struct{}{} + go func(groupID string) { + defer func() { <-send999GroupLimiter }() + + if err := st.SendMsg(st.Ctx, st.DefaultUserID, groupID); err != nil { + log.ZError(st.Ctx, "Send message to 999 group failed.", err) + } + }(groupID) + } + // log.ZInfo(st.Ctx, "Send message to 999 groups successfully.") + } + } + }() + + <-st.Ctx.Done() + fmt.Println("Received signal to exit, shutting down...") +} diff --git a/tools/stress-test/main.go b/tools/stress-test/main.go index aa52b69ed..6adbd12ee 100755 --- a/tools/stress-test/main.go +++ b/tools/stress-test/main.go @@ -72,22 +72,22 @@ type BaseResp struct { } type StressTest struct { - Conf *conf - AdminUserID string - AdminToken string - DefaultGroupID string - DefaultSendUserID string - UserCounter int - GroupCounter int - MsgCounter int - CreatedUsers []string - CreatedGroups []string - Mutex sync.Mutex - Ctx context.Context - Cancel context.CancelFunc - HttpClient *http.Client - Wg sync.WaitGroup - Once sync.Once + Conf *conf + AdminUserID string + AdminToken string + DefaultGroupID string + DefaultUserID string + UserCounter int + GroupCounter int + MsgCounter int + CreatedUsers []string + CreatedGroups []string + Mutex sync.Mutex + Ctx context.Context + Cancel context.CancelFunc + HttpClient *http.Client + Wg sync.WaitGroup + Once sync.Once } type conf struct { @@ -384,7 +384,6 @@ func main() { os.Exit(1) return } - // Invite To Group if err = st.InviteToGroup(st.Ctx, userCreatedID); err != nil { log.ZError(st.Ctx, "Invite To Group failed.", err, "UserID", userCreatedID) @@ -393,7 +392,7 @@ func main() { } st.Once.Do(func() { - st.DefaultSendUserID = userCreatedID + st.DefaultUserID = userCreatedID fmt.Println("Default Send User Created ID:", userCreatedID) close(ch) }) @@ -417,8 +416,8 @@ func main() { case <-ticker.C: // Send Message - if err = st.SendMsg(st.Ctx, st.DefaultSendUserID); err != nil { - log.ZError(st.Ctx, "Send Message failed.", err, "UserID", st.DefaultSendUserID) + if err = st.SendMsg(st.Ctx, st.DefaultUserID); err != nil { + log.ZError(st.Ctx, "Send Message failed.", err, "UserID", st.DefaultUserID) continue } } @@ -443,9 +442,9 @@ func main() { case <-ticker.C: // Create Group - _, err := st.CreateGroup(st.Ctx, st.DefaultSendUserID) + _, err := st.CreateGroup(st.Ctx, st.DefaultUserID) if err != nil { - log.ZError(st.Ctx, "Create Group failed.", err, "UserID", st.DefaultSendUserID) + log.ZError(st.Ctx, "Create Group failed.", err, "UserID", st.DefaultUserID) os.Exit(1) return } From 58994095a5e9ba78067dca16bf86e0d587dc6346 Mon Sep 17 00:00:00 2001 From: skiffer-git <72860476+skiffer-git@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:48:12 +0800 Subject: [PATCH 24/59] License (#3293) * 3.6.1 code conventions (#2203) * Adjust configuration settings * Adjust configuration settings * Adjust configuration settings * refactor: webhooks update. * refactor: kafka update. * Simplify the Docker Compose configuration, remove unnecessary environment variables, and eliminate the gateway service. * refactor: kafka update. * refactor: kafka update. * Simplify the Docker Compose configuration, remove unnecessary environment variables, and eliminate the gateway service. * Simplify the Docker Compose configuration, remove unnecessary environment variables, and eliminate the gateway service. * Windows can compile and run. * Windows can compile and run. * refactor: kafka update. * feat: msg cache split * refactor: webhooks update * refactor: webhooks update * refactor: friends update * refactor: group update * refactor: third update * refactor: api update * refactor: crontab update * refactor: msggateway update * mage * mage * refactor: all module update. * check * refactor: all module update. * load config * load config * load config * load config * refactor: all module update. * refactor: all module update. * refactor: all module update. * refactor: all module update. * refactor: all module update. * Optimize Docker configuration and script. * refactor: all module update. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * refactor: all module update. * Optimize Docker configuration and script. * refactor: all module update. * refactor: all module update. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * update tools * update tools * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * update protocol * Optimize Docker configuration and script. * Optimize Docker configuration and script. * refactor: all module update. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * refactor: api remove token auth by redis directly. * Code Refactoring * refactor: websocket auth change to call rpc of auth. * refactor: kick online user and remove token change to call auth rpc. * refactor: kick online user and remove token change to call auth rpc. * refactor: remove msggateway redis. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor webhook * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor webhook * refactor: cmd update. * refactor: cmd update. * fix: runtime: goroutine stack exceeds * refactor: cmd update. * refactor notification * refactor notification * refactor * refactor: cmd update. * refactor: cmd update. * refactor * refactor * refactor * protojson * protojson * protojson * go mod * wrapperspb * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: context update. * refactor: websocket update info. * refactor: websocket update info. * refactor: websocket update info. * refactor: websocket update info. * refactor: api name change. * refactor: debug info. * refactor: debug info. * refactor: debug info. * fix: update file * refactor * refactor * refactor: debug info. * refactor: debug info. * refactor: debug info. * refactor: debug info. * refactor: debug info. * refactor: debug info. * fix: callback update. * fix: callback update. * refactor * fix: update message. * fix: msg cache timeout. * refactor * refactor * fix: push update. * fix: push update. * fix: push update. * fix: push update. * fix: push update. * fix: push update. * refactor * refactor * fix: push update. * fix: websocket handle error remove when upgrade error. * fix: priority url * fix: minio config * refactor: add zk logger. * refactor * fix: minio config * refactor * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * fix bug: get localIP * refactor * refactor * refactor * refactor: remove zk logger. * refactor: update tools version. * refactor * refactor: update server version to 3.7.0. * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor: zk log debug. * refactor: zk log debug. * refactor: zk log debug. * refactor: zk log debug. * refactor: zk log debug. * refactor * refactor * refactor * refactor: log level change. * refactor: 3.7.0 code conventions. --------- Co-authored-by: skiffer-git <44203734@qq.com> Co-authored-by: withchao <993506633@qq.com> Co-authored-by: root * update go.mod go.sum (#2209) * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * remove \r * fix bug: get localIP * update some ci file (#2200) * Update openimci.yml * Update golangci-lint.yml * Update e2e-test.yml * 3.6.1 code conventions (#2202) * refactor: webhooks update. * Adjust configuration settings * Adjust configuration settings * Adjust configuration settings * feat: s3 api addr * refactor: webhooks update. * Adjust configuration settings * Adjust configuration settings * Adjust configuration settings * Adjust configuration settings * Adjust configuration settings * Adjust configuration settings * Adjust configuration settings * refactor: webhooks update. * refactor: kafka update. * Simplify the Docker Compose configuration, remove unnecessary environment variables, and eliminate the gateway service. * refactor: kafka update. * refactor: kafka update. * Simplify the Docker Compose configuration, remove unnecessary environment variables, and eliminate the gateway service. * Simplify the Docker Compose configuration, remove unnecessary environment variables, and eliminate the gateway service. * Windows can compile and run. * Windows can compile and run. * refactor: kafka update. * feat: msg cache split * refactor: webhooks update * refactor: webhooks update * refactor: friends update * refactor: group update * refactor: third update * refactor: api update * refactor: crontab update * refactor: msggateway update * mage * mage * refactor: all module update. * check * refactor: all module update. * load config * load config * load config * load config * refactor: all module update. * refactor: all module update. * refactor: all module update. * refactor: all module update. * refactor: all module update. * Optimize Docker configuration and script. * refactor: all module update. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * refactor: all module update. * Optimize Docker configuration and script. * refactor: all module update. * refactor: all module update. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * update tools * update tools * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * update protocol * Optimize Docker configuration and script. * Optimize Docker configuration and script. * refactor: all module update. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * Optimize Docker configuration and script. * refactor: api remove token auth by redis directly. * Code Refactoring * refactor: websocket auth change to call rpc of auth. * refactor: kick online user and remove token change to call auth rpc. * refactor: kick online user and remove token change to call auth rpc. * refactor: remove msggateway redis. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor webhook * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor webhook * refactor: cmd update. * refactor: cmd update. * fix: runtime: goroutine stack exceeds * refactor: cmd update. * refactor notification * refactor notification * refactor * refactor: cmd update. * refactor: cmd update. * refactor * refactor * refactor * protojson * protojson * protojson * go mod * wrapperspb * refactor: cmd update. * refactor: cmd update. * refactor: cmd update. * refactor: context update. * refactor: websocket update info. * refactor: websocket update info. * refactor: websocket update info. * refactor: websocket update info. * refactor: api name change. * refactor: debug info. * refactor: debug info. * refactor: debug info. * fix: update file * refactor * refactor * refactor: debug info. * refactor: debug info. * refactor: debug info. * refactor: debug info. * refactor: debug info. * refactor: debug info. * fix: callback update. * fix: callback update. * refactor * fix: update message. * fix: msg cache timeout. * refactor * refactor * fix: push update. * fix: push update. * fix: push update. * fix: push update. * fix: push update. * fix: push update. * refactor * refactor * fix: push update. * fix: websocket handle error remove when upgrade error. * fix: priority url * fix: minio config * refactor: add zk logger. * refactor * fix: minio config * refactor * refactor * refactor * refactor * refactor: remove zk logger. * refactor: update tools version. * refactor * refactor: update server version to 3.7.0. * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor: zk log debug. * refactor: zk log debug. * refactor: zk log debug. * refactor: zk log debug. * refactor: zk log debug. * refactor * refactor * refactor * refactor: log level change. * refactor: 3.7.0 code conventions. --------- Co-authored-by: skiffer-git <44203734@qq.com> Co-authored-by: withchao <993506633@qq.com> * update go.mod go.sum * Remove Chinese comments * user localhost for minio * user localhost for minio * Remove Chinese comments * Remove Chinese comments * Remove Chinese comments * Set up 4 instances of transfer * Set up 4 instances of transfer * Add comments to the configuration file * Add comments to the configuration file --------- Co-authored-by: root Co-authored-by: xuan <146319162+wxuanF@users.noreply.github.com> Co-authored-by: OpenIM-Gordon <46924906+FGadvancer@users.noreply.github.com> Co-authored-by: withchao <993506633@qq.com> * Update the document (#2221) * Update the document * Update the document * use openim/openim-admin openim/openim-web image * Update .golangci.yml * Add etcd as a service discovery mechanism * Add etcd as a service discovery mechanism * update * update license * update license * update license * update license * update license --------- Co-authored-by: OpenIM-Gordon <46924906+FGadvancer@users.noreply.github.com> Co-authored-by: withchao <993506633@qq.com> Co-authored-by: root Co-authored-by: xuan <146319162+wxuanF@users.noreply.github.com> --- .golangci.yml | 2 ++ README.md | 9 ++++++++- README_zh_CN.md | 12 ++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index a95e980f8..7d6c6b596 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,6 +28,8 @@ run: # - util # - .*~ # - api/swagger/docs + + # - server/docs # - components/mnt/config/certs # - logs diff --git a/README.md b/README.md index 3b88935eb..a23c72c61 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,14 @@ Thank you for contributing to building a powerful instant messaging solution! ## :closed_book: License -For more details, please refer to [here](./LICENSE). +This software is licensed under a dual-license model: + +- The GNU Affero General Public License (AGPL), Version 3 or later; **OR** +- Commercial license terms from OpenIMSDK. + +If you wish to use this software under commercial terms, please contact us at: contact@openim.io + +For more information, see: https://www.openim.io/en/licensing diff --git a/README_zh_CN.md b/README_zh_CN.md index 59198eafb..2340ad09a 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -131,9 +131,17 @@ 感谢您的贡献,一起来打造强大的即时通讯解决方案! -## :closed_book: 许可证 +## :closed_book: 开源许可证 License + +本软件采用双重授权模型: + +GNU Affero 通用公共许可证(AGPL)第 3 版或更高版本;或 + +来自 OpenIMSDK 的商业授权条款。 + +如需商用,请联系:contact@openim.io +详见:https://www.openim.io/en/licensing - OpenIMSDK 在 Apache License 2.0 许可下可用。查看[LICENSE 文件](https://github.com/openimsdk/open-im-server/blob/main/LICENSE)了解更多信息。 ## 🔮 Thanks to our contributors! From fa3d251dcb3bd063289b37e3b8530ff53c6a6af8 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:02:45 +0800 Subject: [PATCH 25/59] feat: GroupApplicationAgreeMemberEnterNotification splitting (#3297) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: GroupApplicationAgreeMemberEnterNotification splitting, rpc body size limit * feat: GroupApplicationAgreeMemberEnterNotification splitting, rpc body size limit --- config/share.yml | 4 +++ internal/rpc/group/group.go | 23 ++++++++++--- internal/rpc/group/notification.go | 4 +++ pkg/common/config/config.go | 12 +++++-- pkg/common/startrpc/start.go | 53 +++++++++++++++++++++++++++++- 5 files changed, 87 insertions(+), 9 deletions(-) diff --git a/config/share.yml b/config/share.yml index 0913c1e88..2e9821436 100644 --- a/config/share.yml +++ b/config/share.yml @@ -7,3 +7,7 @@ multiLogin: policy: 1 # max num of tokens in one end maxNumOneEnd: 30 + +rpcMaxBodySize: + requestMaxBodySize: 8388608 + responseMaxBodySize: 8388608 diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 63d9336d5..455b77635 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -448,12 +448,25 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite return nil, err } - if err := g.db.CreateGroup(ctx, nil, groupMembers); err != nil { - return nil, err - } + const singleQuantity = 50 + for start := 0; start < len(groupMembers); start += singleQuantity { + end := start + singleQuantity + if end > len(groupMembers) { + end = len(groupMembers) + } + currentMembers := groupMembers[start:end] - if err = g.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, req.SendMessage, opUserID, req.InvitedUserIDs...); err != nil { - return nil, err + if err := g.db.CreateGroup(ctx, nil, currentMembers); err != nil { + return nil, err + } + + userIDs := datautil.Slice(currentMembers, func(e *model.GroupMember) string { + return e.UserID + }) + + if err = g.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, req.SendMessage, opUserID, userIDs...); err != nil { + return nil, err + } } return &pbgroup.InviteUserToGroupResp{}, nil } diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index f9da88863..43becf4f2 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -522,6 +522,10 @@ func (g *NotificationSender) MemberKickedNotification(ctx context.Context, tips } func (g *NotificationSender) GroupApplicationAgreeMemberEnterNotification(ctx context.Context, groupID string, SendMessage *bool, invitedOpUserID string, entrantUserID ...string) error { + return g.groupApplicationAgreeMemberEnterNotification(ctx, groupID, SendMessage, invitedOpUserID, entrantUserID...) +} + +func (g *NotificationSender) groupApplicationAgreeMemberEnterNotification(ctx context.Context, groupID string, SendMessage *bool, invitedOpUserID string, entrantUserID ...string) error { var err error defer func() { if err != nil { diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go index fa93e3406..d5ae68ec0 100644 --- a/pkg/common/config/config.go +++ b/pkg/common/config/config.go @@ -348,9 +348,15 @@ type AfterConfig struct { } type Share struct { - Secret string `yaml:"secret"` - IMAdminUserID []string `yaml:"imAdminUserID"` - MultiLogin MultiLogin `yaml:"multiLogin"` + Secret string `yaml:"secret"` + IMAdminUserID []string `yaml:"imAdminUserID"` + MultiLogin MultiLogin `yaml:"multiLogin"` + RPCMaxBodySize MaxRequestBody `yaml:"rpcMaxBodySize"` +} + +type MaxRequestBody struct { + RequestMaxBodySize int `yaml:"requestMaxBodySize"` + ResponseMaxBodySize int `yaml:"responseMaxBodySize"` } type MultiLogin struct { diff --git a/pkg/common/startrpc/start.go b/pkg/common/startrpc/start.go index a69edae20..af50c408d 100644 --- a/pkg/common/startrpc/start.go +++ b/pkg/common/startrpc/start.go @@ -21,6 +21,7 @@ import ( "net" "os" "os/signal" + "reflect" "strconv" "syscall" "time" @@ -45,6 +46,36 @@ func init() { prommetrics.RegistryAll() } +func getConfigRpcMaxRequestBody(value reflect.Value) *conf.MaxRequestBody { + for value.Kind() == reflect.Pointer { + value = value.Elem() + } + if value.Kind() == reflect.Struct { + num := value.NumField() + for i := 0; i < num; i++ { + field := value.Field(i) + if !field.CanInterface() { + continue + } + for field.Kind() == reflect.Pointer { + field = field.Elem() + } + switch elem := field.Interface().(type) { + case conf.Share: + return &elem.RPCMaxBodySize + case conf.MaxRequestBody: + return &elem + } + if field.Kind() == reflect.Struct { + if elem := getConfigRpcMaxRequestBody(field); elem != nil { + return elem + } + } + } + } + return nil +} + func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *conf.Prometheus, listenIP, registerIP string, autoSetPorts bool, rpcPorts []int, index int, rpcRegisterName string, notification *conf.Notification, config T, watchConfigNames []string, watchServiceNames []string, @@ -55,7 +86,24 @@ func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *c conf.InitNotification(notification) } - options = append(options, mw.GrpcServer()) + maxRequestBody := getConfigRpcMaxRequestBody(reflect.ValueOf(config)) + + log.ZDebug(ctx, "rpc start", "rpcMaxRequestBody", maxRequestBody, "rpcRegisterName", rpcRegisterName, "registerIP", registerIP, "listenIP", listenIP) + + options = append(options, + mw.GrpcServer(), + ) + var clientOptions []grpc.DialOption + if maxRequestBody != nil { + if maxRequestBody.RequestMaxBodySize > 0 { + options = append(options, grpc.MaxRecvMsgSize(maxRequestBody.RequestMaxBodySize)) + clientOptions = append(clientOptions, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(maxRequestBody.RequestMaxBodySize))) + } + if maxRequestBody.ResponseMaxBodySize > 0 { + options = append(options, grpc.MaxSendMsgSize(maxRequestBody.ResponseMaxBodySize)) + clientOptions = append(clientOptions, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxRequestBody.ResponseMaxBodySize))) + } + } registerIP, err := network.GetRpcRegisterIP(registerIP) if err != nil { @@ -84,6 +132,9 @@ func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *c mw.GrpcClient(), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"LoadBalancingPolicy": "%s"}`, "round_robin")), ) + if len(clientOptions) > 0 { + client.AddOption(clientOptions...) + } ctx, cancel := context.WithCancelCause(ctx) From c54b948f1a6b35a4aa1f2f924443ba44524b2c84 Mon Sep 17 00:00:00 2001 From: skiffer-git <72860476+skiffer-git@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:47:13 +0800 Subject: [PATCH 26/59] Update LICENSE --- LICENSE | 862 +++++++++++++------------------------------------------- 1 file changed, 201 insertions(+), 661 deletions(-) diff --git a/LICENSE b/LICENSE index 0ad25db4b..261eeb9e9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,201 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 986237af2e767590e1481991dbbd698e3a8ee29a Mon Sep 17 00:00:00 2001 From: skiffer-git <72860476+skiffer-git@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:52:02 +0800 Subject: [PATCH 27/59] Update README.md --- README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index a23c72c61..e59bf20c4 100644 --- a/README.md +++ b/README.md @@ -132,14 +132,7 @@ Thank you for contributing to building a powerful instant messaging solution! ## :closed_book: License -This software is licensed under a dual-license model: - -- The GNU Affero General Public License (AGPL), Version 3 or later; **OR** -- Commercial license terms from OpenIMSDK. - -If you wish to use this software under commercial terms, please contact us at: contact@openim.io - -For more information, see: https://www.openim.io/en/licensing +This software is licensed under the Apache License 2.0 From bed81af75c46078303102c57dd805a13569777d0 Mon Sep 17 00:00:00 2001 From: skiffer-git <72860476+skiffer-git@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:52:32 +0800 Subject: [PATCH 28/59] Update README_zh_CN.md --- README_zh_CN.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README_zh_CN.md b/README_zh_CN.md index 2340ad09a..2147c0243 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -133,14 +133,7 @@ ## :closed_book: 开源许可证 License -本软件采用双重授权模型: - -GNU Affero 通用公共许可证(AGPL)第 3 版或更高版本;或 - -来自 OpenIMSDK 的商业授权条款。 - -如需商用,请联系:contact@openim.io -详见:https://www.openim.io/en/licensing +This software is licensed under the Apache License 2.0 ## 🔮 Thanks to our contributors! From 91db18168d74874f5637609af6df1758270e8a16 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:26:52 +0800 Subject: [PATCH 29/59] fix: data version SetVersion will add record (#3304) --- tools/seq/internal/seq.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/seq/internal/seq.go b/tools/seq/internal/seq.go index 7e5d5598c..9fd352a96 100644 --- a/tools/seq/internal/seq.go +++ b/tools/seq/internal/seq.go @@ -337,7 +337,7 @@ func SetVersion(coll *mongo.Collection, key string, version int) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() option := options.Update().SetUpsert(true) - filter := bson.M{"key": key, "value": strconv.Itoa(version)} + filter := bson.M{"key": key} update := bson.M{"$set": bson.M{"key": key, "value": strconv.Itoa(version)}} return mongoutil.UpdateOne(ctx, coll, filter, update, false, option) } From dd981b976dbf2f84f09b1e34c91a532320d4dacc Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Fri, 25 Apr 2025 15:57:23 +0800 Subject: [PATCH 30/59] refactor: move stress-test tools location. (#3295) * refactor: move stress-test tools location. * improve stress tools. * improve stress_test-v2 --- test/stress-test-v2/README.md | 19 +++++++++++++ {tools => test}/stress-test-v2/main.go | 39 ++++++++++++++++++++------ test/stress-test/README.md | 19 +++++++++++++ {tools => test}/stress-test/main.go | 0 tools/stress-test/README.md | 25 ----------------- 5 files changed, 69 insertions(+), 33 deletions(-) create mode 100644 test/stress-test-v2/README.md rename {tools => test}/stress-test-v2/main.go (93%) create mode 100644 test/stress-test/README.md rename {tools => test}/stress-test/main.go (100%) delete mode 100644 tools/stress-test/README.md diff --git a/test/stress-test-v2/README.md b/test/stress-test-v2/README.md new file mode 100644 index 000000000..cbd4bdbde --- /dev/null +++ b/test/stress-test-v2/README.md @@ -0,0 +1,19 @@ +# Stress Test V2 + +## Usage + +You need set `TestTargetUserList` variables. + +### Build + +```bash + +go build -o test/stress-test-v2/stress-test-v2 test/stress-test-v2/main.go +``` + +### Excute + +```bash + +tools/stress-test-v2/stress-test-v2 -c config/ +``` diff --git a/tools/stress-test-v2/main.go b/test/stress-test-v2/main.go similarity index 93% rename from tools/stress-test-v2/main.go rename to test/stress-test-v2/main.go index 0c309b9c9..0e4609964 100644 --- a/tools/stress-test-v2/main.go +++ b/test/stress-test-v2/main.go @@ -56,6 +56,7 @@ var ( const ( MaxUser = 100000 + Max1kUser = 1000 Max100KGroup = 100 Max999Group = 1000 MaxInviteUserLimit = 999 @@ -161,7 +162,7 @@ func (st *StressTest) PostRequest(ctx context.Context, url string, reqbody any) if baseResp.ErrCode != 0 { err = fmt.Errorf(baseResp.ErrMsg) - log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody, "resp", baseResp) + // log.ZError(ctx, "Failed to send request", err, "url", url, "reqbody", reqbody, "resp", baseResp) return nil, err } @@ -530,9 +531,20 @@ func main() { // Create 100K groups st.Wg.Add(1) go func(idx int) { + startTime := time.Now() + defer func() { + elapsedTime := time.Since(startTime) + log.ZInfo(st.Ctx, "100K group creation completed", + "groupID", fmt.Sprintf("v2_StressTest_Group_100K_%d", idx), + "index", idx, + "duration", elapsedTime.String()) + }() + defer st.Wg.Done() defer func() { + st.Mutex.Lock() st.Create100kGroupCounter++ + st.Mutex.Unlock() }() groupID := fmt.Sprintf("v2_StressTest_Group_100K_%d", idx) @@ -542,7 +554,7 @@ func main() { // continue } - for i := 0; i < MaxUser/MaxInviteUserLimit; i++ { + for i := 0; i <= MaxUser/MaxInviteUserLimit; i++ { InviteUserIDs := make([]string, 0) // ensure TargetUserList is in group InviteUserIDs = append(InviteUserIDs, TestTargetUserList...) @@ -556,7 +568,7 @@ func main() { } if len(InviteUserIDs) == 0 { - log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + // log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) continue } @@ -567,7 +579,7 @@ func main() { } if len(InviteUserIDs) == 0 { - log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + // log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) continue } @@ -602,9 +614,20 @@ func main() { // Create 999 groups st.Wg.Add(1) go func(idx int) { + startTime := time.Now() + defer func() { + elapsedTime := time.Since(startTime) + log.ZInfo(st.Ctx, "999 group creation completed", + "groupID", fmt.Sprintf("v2_StressTest_Group_1K_%d", idx), + "index", idx, + "duration", elapsedTime.String()) + }() + defer st.Wg.Done() defer func() { + st.Mutex.Lock() st.Create999GroupCounter++ + st.Mutex.Unlock() }() groupID := fmt.Sprintf("v2_StressTest_Group_1K_%d", idx) @@ -613,13 +636,13 @@ func main() { log.ZError(st.Ctx, "Create group failed.", err) // continue } - for i := 0; i < MaxUser/MaxInviteUserLimit; i++ { + for i := 0; i <= Max1kUser/MaxInviteUserLimit; i++ { InviteUserIDs := make([]string, 0) // ensure TargetUserList is in group InviteUserIDs = append(InviteUserIDs, TestTargetUserList...) startIdx := max(i*MaxInviteUserLimit, 1) - endIdx := min((i+1)*MaxInviteUserLimit, MaxUser) + endIdx := min((i+1)*MaxInviteUserLimit, Max1kUser) for j := startIdx; j < endIdx; j++ { userCreatedID := fmt.Sprintf("v2_StressTest_User_%d", j) @@ -627,7 +650,7 @@ func main() { } if len(InviteUserIDs) == 0 { - log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + // log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) continue } @@ -638,7 +661,7 @@ func main() { } if len(InviteUserIDs) == 0 { - log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) + // log.ZWarn(st.Ctx, "InviteUserIDs is empty", nil, "groupID", groupID) continue } diff --git a/test/stress-test/README.md b/test/stress-test/README.md new file mode 100644 index 000000000..cba93e279 --- /dev/null +++ b/test/stress-test/README.md @@ -0,0 +1,19 @@ +# Stress Test + +## Usage + +You need set `TestTargetUserList` and `DefaultGroupID` variables. + +### Build + +```bash + +go build -o test/stress-test/stress-test test/stress-test/main.go +``` + +### Excute + +```bash + +tools/stress-test/stress-test -c config/ +``` diff --git a/tools/stress-test/main.go b/test/stress-test/main.go similarity index 100% rename from tools/stress-test/main.go rename to test/stress-test/main.go diff --git a/tools/stress-test/README.md b/tools/stress-test/README.md deleted file mode 100644 index 531233a20..000000000 --- a/tools/stress-test/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Stress Test - -## Usage - -You need set `TestTargetUserList` and `DefaultGroupID` variables. - -### Build - -```bash -go build -o _output/bin/tools/linux/amd64/stress-test tools/stress-test/main.go - -# or - -go build -o tools/stress-test/stress-test tools/stress-test/main.go -``` - -### Excute - -```bash -_output/bin/tools/linux/amd64/stress-test -c config/ - -#or - -tools/stress-test/stress-test -c config/ -``` From 56c5c1f01511064cb0ef787cf4fa15dbdb1834f9 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Tue, 6 May 2025 15:10:10 +0800 Subject: [PATCH 31/59] fix: delete token by correct platformID && feat: adminToken can be retained for five minutes after deleting (#3313) --- internal/rpc/auth/auth.go | 17 +++-- pkg/common/storage/cache/cachekey/token.go | 7 +- pkg/common/storage/cache/mcache/token.go | 38 +++++++++- pkg/common/storage/cache/redis/token.go | 73 +++++++++++++++++-- pkg/common/storage/cache/token.go | 3 + pkg/common/storage/controller/auth.go | 82 ++++++++++++++-------- pkg/common/storage/database/mgo/cache.go | 2 +- 7 files changed, 177 insertions(+), 45 deletions(-) diff --git a/internal/rpc/auth/auth.go b/internal/rpc/auth/auth.go index 399d0bd4b..6697e27c0 100644 --- a/internal/rpc/auth/auth.go +++ b/internal/rpc/auth/auth.go @@ -159,15 +159,17 @@ func (s *authServer) parseToken(ctx context.Context, tokensString string) (claim if err != nil { return nil, err } - isAdmin := authverify.IsManagerUserID(claims.UserID, s.config.Share.IMAdminUserID) - if isAdmin { - return claims, nil - } m, err := s.authDatabase.GetTokensWithoutError(ctx, claims.UserID, claims.PlatformID) if err != nil { return nil, err } if len(m) == 0 { + isAdmin := authverify.IsManagerUserID(claims.UserID, s.config.Share.IMAdminUserID) + if isAdmin { + if err = s.authDatabase.GetTemporaryTokensWithoutError(ctx, claims.UserID, claims.PlatformID, tokensString); err == nil { + return claims, nil + } + } return nil, servererrs.ErrTokenNotExist.Wrap() } if v, ok := m[tokensString]; ok { @@ -179,6 +181,13 @@ func (s *authServer) parseToken(ctx context.Context, tokensString string) (claim default: return nil, errs.Wrap(errs.ErrTokenUnknown) } + } else { + isAdmin := authverify.IsManagerUserID(claims.UserID, s.config.Share.IMAdminUserID) + if isAdmin { + if err = s.authDatabase.GetTemporaryTokensWithoutError(ctx, claims.UserID, claims.PlatformID, tokensString); err == nil { + return claims, nil + } + } } return nil, servererrs.ErrTokenNotExist.Wrap() } diff --git a/pkg/common/storage/cache/cachekey/token.go b/pkg/common/storage/cache/cachekey/token.go index 83ba2f211..6fe1bdfef 100644 --- a/pkg/common/storage/cache/cachekey/token.go +++ b/pkg/common/storage/cache/cachekey/token.go @@ -1,8 +1,9 @@ package cachekey import ( - "github.com/openimsdk/protocol/constant" "strings" + + "github.com/openimsdk/protocol/constant" ) const ( @@ -13,6 +14,10 @@ func GetTokenKey(userID string, platformID int) string { return UidPidToken + userID + ":" + constant.PlatformIDToName(platformID) } +func GetTemporaryTokenKey(userID string, platformID int, token string) string { + return UidPidToken + ":TEMPORARY:" + userID + ":" + constant.PlatformIDToName(platformID) + ":" + token +} + func GetAllPlatformTokenKey(userID string) []string { res := make([]string, len(constant.PlatformID2Name)) for k := range constant.PlatformID2Name { diff --git a/pkg/common/storage/cache/mcache/token.go b/pkg/common/storage/cache/mcache/token.go index d7ae29cfc..98b9cc066 100644 --- a/pkg/common/storage/cache/mcache/token.go +++ b/pkg/common/storage/cache/mcache/token.go @@ -27,7 +27,6 @@ type tokenCache struct { func (x *tokenCache) getTokenKey(userID string, platformID int, token string) string { return cachekey.GetTokenKey(userID, platformID) + ":" + token - } func (x *tokenCache) SetTokenFlag(ctx context.Context, userID string, platformID int, token string, flag int) error { @@ -57,6 +56,14 @@ func (x *tokenCache) GetTokensWithoutError(ctx context.Context, userID string, p return mm, nil } +func (x *tokenCache) HasTemporaryToken(ctx context.Context, userID string, platformID int, token string) error { + key := cachekey.GetTemporaryTokenKey(userID, platformID, token) + if _, err := x.cache.Get(ctx, []string{key}); err != nil { + return err + } + return nil +} + func (x *tokenCache) GetAllTokensWithoutError(ctx context.Context, userID string) (map[int]map[string]int, error) { prefix := cachekey.UidPidToken + userID + ":" tokens, err := x.cache.Prefix(ctx, prefix) @@ -128,3 +135,32 @@ func (x *tokenCache) DeleteTokenByUidPid(ctx context.Context, userID string, pla func (x *tokenCache) getExpireTime(t int64) time.Duration { return time.Hour * 24 * time.Duration(t) } + +func (x *tokenCache) DeleteTokenByTokenMap(ctx context.Context, userID string, tokens map[int][]string) error { + keys := make([]string, 0, len(tokens)) + for platformID, ts := range tokens { + for _, t := range ts { + keys = append(keys, x.getTokenKey(userID, platformID, t)) + } + } + return x.cache.Del(ctx, keys) +} + +func (x *tokenCache) DeleteAndSetTemporary(ctx context.Context, userID string, platformID int, fields []string) error { + keys := make([]string, 0, len(fields)) + for _, f := range fields { + keys = append(keys, x.getTokenKey(userID, platformID, f)) + } + if err := x.cache.Del(ctx, keys); err != nil { + return err + } + + for _, f := range fields { + k := cachekey.GetTemporaryTokenKey(userID, platformID, f) + if err := x.cache.Set(ctx, k, "", time.Minute*5); err != nil { + return errs.Wrap(err) + } + } + + return nil +} diff --git a/pkg/common/storage/cache/redis/token.go b/pkg/common/storage/cache/redis/token.go index 510da43e3..b3870daee 100644 --- a/pkg/common/storage/cache/redis/token.go +++ b/pkg/common/storage/cache/redis/token.go @@ -9,6 +9,7 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" "github.com/openimsdk/tools/errs" + "github.com/openimsdk/tools/utils/datautil" "github.com/redis/go-redis/v9" ) @@ -55,6 +56,14 @@ func (c *tokenCache) GetTokensWithoutError(ctx context.Context, userID string, p return mm, nil } +func (c *tokenCache) HasTemporaryToken(ctx context.Context, userID string, platformID int, token string) error { + err := c.rdb.Get(ctx, cachekey.GetTemporaryTokenKey(userID, platformID, token)).Err() + if err != nil { + return errs.Wrap(err) + } + return nil +} + func (c *tokenCache) GetAllTokensWithoutError(ctx context.Context, userID string) (map[int]map[string]int, error) { var ( res = make(map[int]map[string]int) @@ -101,13 +110,19 @@ func (c *tokenCache) SetTokenMapByUidPid(ctx context.Context, userID string, pla } func (c *tokenCache) BatchSetTokenMapByUidPid(ctx context.Context, tokens map[string]map[string]any) error { - pipe := c.rdb.Pipeline() - for k, v := range tokens { - pipe.HSet(ctx, k, v) - } - _, err := pipe.Exec(ctx) - if err != nil { - return errs.Wrap(err) + keys := datautil.Keys(tokens) + if err := ProcessKeysBySlot(ctx, c.rdb, keys, func(ctx context.Context, slot int64, keys []string) error { + pipe := c.rdb.Pipeline() + for k, v := range tokens { + pipe.HSet(ctx, k, v) + } + _, err := pipe.Exec(ctx) + if err != nil { + return errs.Wrap(err) + } + return nil + }); err != nil { + return err } return nil } @@ -119,3 +134,47 @@ func (c *tokenCache) DeleteTokenByUidPid(ctx context.Context, userID string, pla func (c *tokenCache) getExpireTime(t int64) time.Duration { return time.Hour * 24 * time.Duration(t) } + +// DeleteTokenByTokenMap tokens key is platformID, value is token slice +func (c *tokenCache) DeleteTokenByTokenMap(ctx context.Context, userID string, tokens map[int][]string) error { + var ( + keys = make([]string, 0, len(tokens)) + keyMap = make(map[string][]string) + ) + for k, v := range tokens { + k1 := cachekey.GetTokenKey(userID, k) + keys = append(keys, k1) + keyMap[k1] = v + } + + if err := ProcessKeysBySlot(ctx, c.rdb, keys, func(ctx context.Context, slot int64, keys []string) error { + pipe := c.rdb.Pipeline() + for k, v := range tokens { + pipe.HDel(ctx, cachekey.GetTokenKey(userID, k), v...) + } + _, err := pipe.Exec(ctx) + if err != nil { + return errs.Wrap(err) + } + return nil + }); err != nil { + return err + } + + return nil +} + +func (c *tokenCache) DeleteAndSetTemporary(ctx context.Context, userID string, platformID int, fields []string) error { + key := cachekey.GetTokenKey(userID, platformID) + if err := c.rdb.HDel(ctx, key, fields...).Err(); err != nil { + return errs.Wrap(err) + } + for _, f := range fields { + k := cachekey.GetTemporaryTokenKey(userID, platformID, f) + if err := c.rdb.Set(ctx, k, "", time.Minute*5).Err(); err != nil { + return errs.Wrap(err) + } + } + + return nil +} diff --git a/pkg/common/storage/cache/token.go b/pkg/common/storage/cache/token.go index e5e0a9383..441c08939 100644 --- a/pkg/common/storage/cache/token.go +++ b/pkg/common/storage/cache/token.go @@ -9,8 +9,11 @@ type TokenModel interface { // SetTokenFlagEx set token and flag with expire time SetTokenFlagEx(ctx context.Context, userID string, platformID int, token string, flag int) error GetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error) + HasTemporaryToken(ctx context.Context, userID string, platformID int, token string) error GetAllTokensWithoutError(ctx context.Context, userID string) (map[int]map[string]int, error) SetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error BatchSetTokenMapByUidPid(ctx context.Context, tokens map[string]map[string]any) error DeleteTokenByUidPid(ctx context.Context, userID string, platformID int, fields []string) error + DeleteTokenByTokenMap(ctx context.Context, userID string, tokens map[int][]string) error + DeleteAndSetTemporary(ctx context.Context, userID string, platformID int, fields []string) error } diff --git a/pkg/common/storage/controller/auth.go b/pkg/common/storage/controller/auth.go index 013d8b155..488a116c3 100644 --- a/pkg/common/storage/controller/auth.go +++ b/pkg/common/storage/controller/auth.go @@ -17,6 +17,8 @@ import ( type AuthDatabase interface { // If the result is empty, no error is returned. GetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error) + + GetTemporaryTokensWithoutError(ctx context.Context, userID string, platformID int, token string) error // Create token CreateToken(ctx context.Context, userID string, platformID int) (string, error) @@ -52,6 +54,10 @@ func (a *authDatabase) GetTokensWithoutError(ctx context.Context, userID string, return a.cache.GetTokensWithoutError(ctx, userID, platformID) } +func (a *authDatabase) GetTemporaryTokensWithoutError(ctx context.Context, userID string, platformID int, token string) error { + return a.cache.HasTemporaryToken(ctx, userID, platformID, token) +} + func (a *authDatabase) SetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error { return a.cache.SetTokenMapByUidPid(ctx, userID, platformID, m) } @@ -85,23 +91,30 @@ func (a *authDatabase) CreateToken(ctx context.Context, userID string, platformI return "", err } - deleteTokenKey, kickedTokenKey, err := a.checkToken(ctx, tokens, platformID) + deleteTokenKey, kickedTokenKey, adminTokens, err := a.checkToken(ctx, tokens, platformID) if err != nil { return "", err } if len(deleteTokenKey) != 0 { - err = a.cache.DeleteTokenByUidPid(ctx, userID, platformID, deleteTokenKey) + err = a.cache.DeleteTokenByTokenMap(ctx, userID, deleteTokenKey) if err != nil { return "", err } } if len(kickedTokenKey) != 0 { - for _, k := range kickedTokenKey { - err := a.cache.SetTokenFlagEx(ctx, userID, platformID, k, constant.KickedToken) - if err != nil { - return "", err + for plt, ks := range kickedTokenKey { + for _, k := range ks { + err := a.cache.SetTokenFlagEx(ctx, userID, plt, k, constant.KickedToken) + if err != nil { + return "", err + } + log.ZDebug(ctx, "kicked token in create token", "token", k) } - log.ZDebug(ctx, "kicked token in create token", "token", k) + } + } + if len(adminTokens) != 0 { + if err = a.cache.DeleteAndSetTemporary(ctx, userID, constant.AdminPlatformID, adminTokens); err != nil { + return "", err } } @@ -119,12 +132,13 @@ func (a *authDatabase) CreateToken(ctx context.Context, userID string, platformI return tokenString, nil } -func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string]int, platformID int) ([]string, []string, error) { - // todo: Move the logic for handling old data to another location. +// checkToken will check token by tokenPolicy and return deleteToken,kickToken,deleteAdminToken +func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string]int, platformID int) (map[int][]string, map[int][]string, []string, error) { + // todo: Asynchronous deletion of old data. var ( loginTokenMap = make(map[int][]string) // The length of the value of the map must be greater than 0 - deleteToken = make([]string, 0) - kickToken = make([]string, 0) + deleteToken = make(map[int][]string) + kickToken = make(map[int][]string) adminToken = make([]string, 0) unkickTerminal = "" ) @@ -133,7 +147,7 @@ func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string for k, v := range tks { _, err := tokenverify.GetClaimFromToken(k, authverify.Secret(a.accessSecret)) if err != nil || v != constant.NormalToken { - deleteToken = append(deleteToken, k) + deleteToken[plfID] = append(deleteToken[plfID], k) } else { if plfID != constant.AdminPlatformID { loginTokenMap[plfID] = append(loginTokenMap[plfID], k) @@ -153,14 +167,15 @@ func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string } limit := a.multiLogin.MaxNumOneEnd if l > limit { - kickToken = append(kickToken, ts[:l-limit]...) + kickToken[plt] = ts[:l-limit] } } case constant.AllLoginButSameTermKick: for plt, ts := range loginTokenMap { - kickToken = append(kickToken, ts[:len(ts)-1]...) + kickToken[plt] = ts[:len(ts)-1] + if plt == platformID { - kickToken = append(kickToken, ts[len(ts)-1]) + kickToken[plt] = append(kickToken[plt], ts[len(ts)-1]) } } case constant.PCAndOther: @@ -168,29 +183,33 @@ func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string if constant.PlatformIDToClass(platformID) != unkickTerminal { for plt, ts := range loginTokenMap { if constant.PlatformIDToClass(plt) != unkickTerminal { - kickToken = append(kickToken, ts...) + kickToken[plt] = ts } } } else { var ( - preKick []string - isReserve = true + preKickToken string + preKickPlt int + reserveToken = false ) for plt, ts := range loginTokenMap { if constant.PlatformIDToClass(plt) != unkickTerminal { // Keep a token from another end - if isReserve { - isReserve = false - kickToken = append(kickToken, ts[:len(ts)-1]...) - preKick = append(preKick, ts[len(ts)-1]) + if !reserveToken { + reserveToken = true + kickToken[plt] = ts[:len(ts)-1] + preKickToken = ts[len(ts)-1] + preKickPlt = plt continue } else { // Prioritize keeping Android if plt == constant.AndroidPlatformID { - kickToken = append(kickToken, preKick...) - kickToken = append(kickToken, ts[:len(ts)-1]...) + if preKickToken != "" { + kickToken[preKickPlt] = append(kickToken[preKickPlt], preKickToken) + } + kickToken[plt] = ts[:len(ts)-1] } else { - kickToken = append(kickToken, ts...) + kickToken[plt] = ts } } } @@ -203,19 +222,19 @@ func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string for plt, ts := range loginTokenMap { if constant.PlatformIDToClass(plt) == constant.PlatformIDToClass(platformID) { - kickToken = append(kickToken, ts...) + kickToken[plt] = ts } else { if _, ok := reserved[constant.PlatformIDToClass(plt)]; !ok { reserved[constant.PlatformIDToClass(plt)] = struct{}{} - kickToken = append(kickToken, ts[:len(ts)-1]...) + kickToken[plt] = ts[:len(ts)-1] continue } else { - kickToken = append(kickToken, ts...) + kickToken[plt] = ts } } } default: - return nil, nil, errs.New("unknown multiLogin policy").Wrap() + return nil, nil, nil, errs.New("unknown multiLogin policy").Wrap() } //var adminTokenMaxNum = a.multiLogin.MaxNumOneEnd @@ -226,8 +245,9 @@ func (a *authDatabase) checkToken(ctx context.Context, tokens map[int]map[string //if l > adminTokenMaxNum { // kickToken = append(kickToken, adminToken[:l-adminTokenMaxNum]...) //} + var deleteAdminToken []string if platformID == constant.AdminPlatformID { - kickToken = append(kickToken, adminToken...) + deleteAdminToken = adminToken } - return deleteToken, kickToken, nil + return deleteToken, kickToken, deleteAdminToken, nil } diff --git a/pkg/common/storage/database/mgo/cache.go b/pkg/common/storage/database/mgo/cache.go index bcf86cd56..991dfa874 100644 --- a/pkg/common/storage/database/mgo/cache.go +++ b/pkg/common/storage/database/mgo/cache.go @@ -127,7 +127,7 @@ func (x *CacheMgo) Del(ctx context.Context, key []string) error { return nil } _, err := x.coll.DeleteMany(ctx, bson.M{"key": bson.M{"$in": key}}) - return err + return errs.Wrap(err) } func (x *CacheMgo) lockKey(key string) string { From 1178808ba7d7798cfc13aada0b47c4a1e130cdce Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Fri, 9 May 2025 09:31:30 +0800 Subject: [PATCH 32/59] feat: optimize server code (#3319) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake --- go.mod | 2 +- go.sum | 4 +-- pkg/common/storage/controller/msg_transfer.go | 34 +------------------ 3 files changed, 4 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index fbaa19434..3863fde1f 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/kelindar/bitmap v1.5.2 github.com/likexian/gokit v0.25.13 - github.com/openimsdk/gomake v0.0.15-alpha.2 + github.com/openimsdk/gomake v0.0.15-alpha.5 github.com/redis/go-redis/v9 v9.4.0 github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil v3.21.11+incompatible diff --git a/go.sum b/go.sum index 390a51c4a..c80690f80 100644 --- a/go.sum +++ b/go.sum @@ -345,8 +345,8 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/openimsdk/gomake v0.0.15-alpha.2 h1:5Q8yl8ezy2yx+q8/ucU/t4kJnDfCzNOrkXcDACCqtyM= -github.com/openimsdk/gomake v0.0.15-alpha.2/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= +github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5beV3ZyOsGhY= +github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.73-alpha.6 h1:sna9coWG7HN1zObBPtvG0Ki/vzqHXiB4qKbA5P3w7kc= github.com/openimsdk/protocol v0.0.73-alpha.6/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= github.com/openimsdk/tools v0.0.50-alpha.79 h1:jxYEbrzaze4Z2r4NrKad816buZ690ix0L9MTOOOH3ik= diff --git a/pkg/common/storage/controller/msg_transfer.go b/pkg/common/storage/controller/msg_transfer.go index 260daf5c4..28392d66e 100644 --- a/pkg/common/storage/controller/msg_transfer.go +++ b/pkg/common/storage/controller/msg_transfer.go @@ -81,42 +81,10 @@ func (db *msgTransferDatabase) BatchInsertChat2DB(ctx context.Context, conversat continue } seqs[i] = msg.Seq - var offlinePushModel *model.OfflinePushModel - if msg.OfflinePushInfo != nil { - offlinePushModel = &model.OfflinePushModel{ - Title: msg.OfflinePushInfo.Title, - Desc: msg.OfflinePushInfo.Desc, - Ex: msg.OfflinePushInfo.Ex, - IOSPushSound: msg.OfflinePushInfo.IOSPushSound, - IOSBadgeCount: msg.OfflinePushInfo.IOSBadgeCount, - } - } if msg.Status == constant.MsgStatusSending { msg.Status = constant.MsgStatusSendSuccess } - msgs[i] = &model.MsgDataModel{ - SendID: msg.SendID, - RecvID: msg.RecvID, - GroupID: msg.GroupID, - ClientMsgID: msg.ClientMsgID, - ServerMsgID: msg.ServerMsgID, - SenderPlatformID: msg.SenderPlatformID, - SenderNickname: msg.SenderNickname, - SenderFaceURL: msg.SenderFaceURL, - SessionType: msg.SessionType, - MsgFrom: msg.MsgFrom, - ContentType: msg.ContentType, - Content: string(msg.Content), - Seq: msg.Seq, - SendTime: msg.SendTime, - CreateTime: msg.CreateTime, - Status: msg.Status, - Options: msg.Options, - OfflinePush: offlinePushModel, - AtUserIDList: msg.AtUserIDList, - AttachedInfo: msg.AttachedInfo, - Ex: msg.Ex, - } + msgs[i] = convert.MsgPb2DB(msg) } if err := db.BatchInsertBlock(ctx, conversationID, msgs, updateKeyMsg, msgList[0].Seq); err != nil { return err From 1d7660bedb98c692cdc1f09d84428acaf9cb7ea2 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Wed, 14 May 2025 16:07:05 +0800 Subject: [PATCH 33/59] fix: optimize grpc option and fix some interface permission checks (#3327) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake * feat: grpc mw * fix: permission verification * fix: optimizing the code * fix: optimize grpc option and fix some interface permission checks --- go.mod | 2 +- go.sum | 4 +-- internal/api/config_manager.go | 2 +- internal/api/msg.go | 6 ++-- internal/api/router.go | 13 ++++++- internal/msggateway/hub_server.go | 2 +- internal/rpc/auth/auth.go | 10 +++--- internal/rpc/conversation/sync.go | 4 +++ internal/rpc/group/group.go | 58 +++++++++++------------------- internal/rpc/group/notification.go | 2 +- internal/rpc/group/sync.go | 20 +++++++++-- internal/rpc/msg/clear.go | 5 +-- internal/rpc/msg/delete.go | 8 ++--- internal/rpc/msg/revoke.go | 6 ++-- internal/rpc/msg/sync_msg.go | 2 +- internal/rpc/relation/black.go | 9 +++-- internal/rpc/relation/friend.go | 18 +++++----- internal/rpc/relation/sync.go | 8 +++-- internal/rpc/third/log.go | 4 +-- internal/rpc/third/s3.go | 4 +-- internal/rpc/third/tool.go | 6 +--- internal/rpc/user/config.go | 8 ++--- internal/rpc/user/user.go | 33 ++++++++--------- pkg/authverify/token.go | 53 +++++++++++++++++---------- pkg/common/startrpc/mw.go | 15 ++++++++ pkg/common/startrpc/start.go | 47 ++++++++++++++++++++++-- 26 files changed, 216 insertions(+), 133 deletions(-) create mode 100644 pkg/common/startrpc/mw.go diff --git a/go.mod b/go.mod index 3863fde1f..4af0695db 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/openimsdk/protocol v0.0.73-alpha.6 - github.com/openimsdk/tools v0.0.50-alpha.79 + github.com/openimsdk/tools v0.0.50-alpha.81 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index c80690f80..6bc410a2d 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5b github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.73-alpha.6 h1:sna9coWG7HN1zObBPtvG0Ki/vzqHXiB4qKbA5P3w7kc= github.com/openimsdk/protocol v0.0.73-alpha.6/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= -github.com/openimsdk/tools v0.0.50-alpha.79 h1:jxYEbrzaze4Z2r4NrKad816buZ690ix0L9MTOOOH3ik= -github.com/openimsdk/tools v0.0.50-alpha.79/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= +github.com/openimsdk/tools v0.0.50-alpha.81 h1:VbuJKtigNXLkCKB/Q6f2UHsqoSaTOAwS8F51c1nhOCA= +github.com/openimsdk/tools v0.0.50-alpha.81/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/internal/api/config_manager.go b/internal/api/config_manager.go index aca02c52c..8786257b3 100644 --- a/internal/api/config_manager.go +++ b/internal/api/config_manager.go @@ -44,7 +44,7 @@ func NewConfigManager(IMAdminUserID []string, cfg *config.AllConfig, client *cli } func (cm *ConfigManager) CheckAdmin(c *gin.Context) { - if err := authverify.CheckAdmin(c, cm.imAdminUserID); err != nil { + if err := authverify.CheckAdmin(c); err != nil { apiresp.GinError(c, err) c.Abort() } diff --git a/internal/api/msg.go b/internal/api/msg.go index 090f3329b..5349faf87 100644 --- a/internal/api/msg.go +++ b/internal/api/msg.go @@ -281,7 +281,7 @@ func (m *MessageApi) SendMessage(c *gin.Context) { } // Check if the user has the app manager role. - if !authverify.IsAppManagerUid(c, m.imAdminUserID) { + if !authverify.IsAdmin(c) { // Respond with a permission error if the user is not an app manager. apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only app manager can send message")) return @@ -355,7 +355,7 @@ func (m *MessageApi) SendBusinessNotification(c *gin.Context) { if req.ReliabilityLevel == nil { req.ReliabilityLevel = datautil.ToPtr(1) } - if !authverify.IsAppManagerUid(c, m.imAdminUserID) { + if !authverify.IsAdmin(c) { apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only app manager can send message")) return } @@ -399,7 +399,7 @@ func (m *MessageApi) BatchSendMsg(c *gin.Context) { apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) return } - if err := authverify.CheckAdmin(c, m.imAdminUserID); err != nil { + if err := authverify.CheckAdmin(c); err != nil { apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only app manager can send message")) return } diff --git a/internal/api/router.go b/internal/api/router.go index 920bd5366..7c2c78c7d 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -9,6 +9,9 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" + "github.com/openimsdk/tools/mcontext" + "github.com/openimsdk/tools/utils/datautil" clientv3 "go.etcd.io/etcd/client/v3" "github.com/openimsdk/open-im-server/v3/internal/api/jssdk" @@ -97,7 +100,7 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin r.Use(gzip.Gzip(gzip.BestSpeed)) } r.Use(prommetricsGin(), gin.RecoveryWithWriter(gin.DefaultErrorWriter, mw.GinPanicErr), mw.CorsHandler(), - mw.GinParseOperationID(), GinParseToken(rpcli.NewAuthClient(authConn))) + mw.GinParseOperationID(), GinParseToken(rpcli.NewAuthClient(authConn)), setGinIsAdmin(cfg.Share.IMAdminUserID)) u := NewUserApi(user.NewUserClient(userConn), client, cfg.Discovery.RpcService) { @@ -354,6 +357,14 @@ func GinParseToken(authClient *rpcli.AuthClient) gin.HandlerFunc { } } +func setGinIsAdmin(imAdminUserID []string) gin.HandlerFunc { + return func(c *gin.Context) { + opUserID := mcontext.GetOpUserID(c) + admin := datautil.Contain(opUserID, imAdminUserID...) + c.Set(authverify.CtxIsAdminKey, admin) + } +} + // Whitelist api not parse token var Whitelist = []string{ "/auth/get_admin_token", diff --git a/internal/msggateway/hub_server.go b/internal/msggateway/hub_server.go index 376a697fd..3d0d1e3f9 100644 --- a/internal/msggateway/hub_server.go +++ b/internal/msggateway/hub_server.go @@ -100,7 +100,7 @@ func NewServer(longConnServer LongConnServer, conf *Config, ready func(srv *Serv } func (s *Server) GetUsersOnlineStatus(ctx context.Context, req *msggateway.GetUsersOnlineStatusReq) (*msggateway.GetUsersOnlineStatusResp, error) { - if !authverify.IsAppManagerUid(ctx, s.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { return nil, errs.ErrNoPermission.WrapMsg("only app manager") } var resp msggateway.GetUsersOnlineStatusResp diff --git a/internal/rpc/auth/auth.go b/internal/rpc/auth/auth.go index 6697e27c0..2c2691d1d 100644 --- a/internal/rpc/auth/auth.go +++ b/internal/rpc/auth/auth.go @@ -125,7 +125,7 @@ func (s *authServer) GetAdminToken(ctx context.Context, req *pbauth.GetAdminToke } func (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenReq) (*pbauth.GetUserTokenResp, error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } @@ -135,7 +135,7 @@ func (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenR resp := pbauth.GetUserTokenResp{} - if authverify.IsManagerUserID(req.UserID, s.config.Share.IMAdminUserID) { + if authverify.CheckUserIsAdmin(ctx, req.UserID) { return nil, errs.ErrNoPermission.WrapMsg("don't get Admin token") } user, err := s.userClient.GetUserInfo(ctx, req.UserID) @@ -164,7 +164,7 @@ func (s *authServer) parseToken(ctx context.Context, tokensString string) (claim return nil, err } if len(m) == 0 { - isAdmin := authverify.IsManagerUserID(claims.UserID, s.config.Share.IMAdminUserID) + isAdmin := authverify.CheckUserIsAdmin(ctx, claims.UserID) if isAdmin { if err = s.authDatabase.GetTemporaryTokensWithoutError(ctx, claims.UserID, claims.PlatformID, tokensString); err == nil { return claims, nil @@ -182,7 +182,7 @@ func (s *authServer) parseToken(ctx context.Context, tokensString string) (claim return nil, errs.Wrap(errs.ErrTokenUnknown) } } else { - isAdmin := authverify.IsManagerUserID(claims.UserID, s.config.Share.IMAdminUserID) + isAdmin := authverify.CheckUserIsAdmin(ctx, claims.UserID) if isAdmin { if err = s.authDatabase.GetTemporaryTokensWithoutError(ctx, claims.UserID, claims.PlatformID, tokensString); err == nil { return claims, nil @@ -205,7 +205,7 @@ func (s *authServer) ParseToken(ctx context.Context, req *pbauth.ParseTokenReq) } func (s *authServer) ForceLogout(ctx context.Context, req *pbauth.ForceLogoutReq) (*pbauth.ForceLogoutResp, error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } if err := s.forceKickOff(ctx, req.UserID, req.PlatformID); err != nil { diff --git a/internal/rpc/conversation/sync.go b/internal/rpc/conversation/sync.go index ad88b2bbd..cee74b319 100644 --- a/internal/rpc/conversation/sync.go +++ b/internal/rpc/conversation/sync.go @@ -4,12 +4,16 @@ import ( "context" "github.com/openimsdk/open-im-server/v3/internal/rpc/incrversion" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" "github.com/openimsdk/open-im-server/v3/pkg/util/hashutil" "github.com/openimsdk/protocol/conversation" ) func (c *conversationServer) GetFullOwnerConversationIDs(ctx context.Context, req *conversation.GetFullOwnerConversationIDsReq) (*conversation.GetFullOwnerConversationIDsResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } vl, err := c.conversationDatabase.FindMaxConversationUserVersionCache(ctx, req.UserID) if err != nil { return nil, err diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 455b77635..10cdc2546 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -152,7 +152,7 @@ func (g *groupServer) NotificationUserInfoUpdate(ctx context.Context, req *pbgro } func (g *groupServer) CheckGroupAdmin(ctx context.Context, groupID string) error { - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { groupMember, err := g.db.TakeGroupMember(ctx, groupID, mcontext.GetOpUserID(ctx)) if err != nil { return err @@ -204,7 +204,7 @@ func (g *groupServer) CreateGroup(ctx context.Context, req *pbgroup.CreateGroupR if req.OwnerUserID == "" { return nil, errs.ErrArgs.WrapMsg("no group owner") } - if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, g.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { return nil, err } userIDs := append(append(req.MemberUserIDs, req.AdminUserIDs...), req.OwnerUserID) @@ -308,7 +308,7 @@ func (g *groupServer) CreateGroup(ctx context.Context, req *pbgroup.CreateGroupR } func (g *groupServer) GetJoinedGroupList(ctx context.Context, req *pbgroup.GetJoinedGroupListReq) (*pbgroup.GetJoinedGroupListResp, error) { - if err := authverify.CheckAccessV3(ctx, req.FromUserID, g.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.FromUserID); err != nil { return nil, err } total, members, err := g.db.PageGetJoinGroup(ctx, req.FromUserID, req.Pagination) @@ -380,7 +380,7 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite var groupMember *model.GroupMember var opUserID string - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { opUserID = mcontext.GetOpUserID(ctx) var err error groupMember, err = g.db.TakeGroupMember(ctx, req.GroupID, opUserID) @@ -399,7 +399,7 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite } if group.NeedVerification == constant.AllNeedVerification { - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { if !(groupMember.RoleLevel == constant.GroupOwner || groupMember.RoleLevel == constant.GroupAdmin) { var requests []*model.GroupRequest for _, userID := range req.InvitedUserIDs { @@ -487,6 +487,11 @@ func (g *groupServer) GetGroupAllMember(ctx context.Context, req *pbgroup.GetGro } func (g *groupServer) GetGroupMemberList(ctx context.Context, req *pbgroup.GetGroupMemberListReq) (*pbgroup.GetGroupMemberListResp, error) { + if opUserID := mcontext.GetOpUserID(ctx); !datautil.Contain(opUserID, g.config.Share.IMAdminUserID...) { + if _, err := g.db.TakeGroupMember(ctx, req.GroupID, opUserID); err != nil { + return nil, err + } + } var ( total int64 members []*model.GroupMember @@ -495,7 +500,7 @@ func (g *groupServer) GetGroupMemberList(ctx context.Context, req *pbgroup.GetGr if req.Keyword == "" { total, members, err = g.db.PageGetGroupMember(ctx, req.GroupID, req.Pagination) } else { - members, err = g.db.FindGroupMemberAll(ctx, req.GroupID) + total, members, err = g.db.SearchGroupMember(ctx, req.GroupID, req.Keyword, req.Pagination) } if err != nil { return nil, err @@ -503,27 +508,6 @@ func (g *groupServer) GetGroupMemberList(ctx context.Context, req *pbgroup.GetGr if err := g.PopulateGroupMember(ctx, members...); err != nil { return nil, err } - if req.Keyword != "" { - groupMembers := make([]*model.GroupMember, 0) - for _, member := range members { - if member.UserID == req.Keyword { - groupMembers = append(groupMembers, member) - total++ - continue - } - if member.Nickname == req.Keyword { - groupMembers = append(groupMembers, member) - total++ - continue - } - } - - members := datautil.Paginate(groupMembers, int(req.Pagination.GetPageNumber()), int(req.Pagination.GetShowNumber())) - return &pbgroup.GetGroupMemberListResp{ - Total: uint32(total), - Members: datautil.Batch(convert.Db2PbGroupMember, members), - }, nil - } return &pbgroup.GetGroupMemberListResp{ Total: uint32(total), Members: datautil.Batch(convert.Db2PbGroupMember, members), @@ -564,7 +548,7 @@ func (g *groupServer) KickGroupMember(ctx context.Context, req *pbgroup.KickGrou for i, member := range members { memberMap[member.UserID] = members[i] } - isAppManagerUid := authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) + isAppManagerUid := authverify.IsAdmin(ctx) opMember := memberMap[opUserID] for _, userID := range req.KickedUserIDs { member, ok := memberMap[userID] @@ -782,7 +766,7 @@ func (g *groupServer) GroupApplicationResponse(ctx context.Context, req *pbgroup if !datautil.Contain(req.HandleResult, constant.GroupResponseAgree, constant.GroupResponseRefuse) { return nil, errs.ErrArgs.WrapMsg("HandleResult unknown") } - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { groupMember, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx)) if err != nil { return nil, err @@ -936,7 +920,7 @@ func (g *groupServer) QuitGroup(ctx context.Context, req *pbgroup.QuitGroupReq) if req.UserID == "" { req.UserID = mcontext.GetOpUserID(ctx) } else { - if err := authverify.CheckAccessV3(ctx, req.UserID, g.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } } @@ -974,7 +958,7 @@ func (g *groupServer) deleteMemberAndSetConversationSeq(ctx context.Context, gro func (g *groupServer) SetGroupInfo(ctx context.Context, req *pbgroup.SetGroupInfoReq) (*pbgroup.SetGroupInfoResp, error) { var opMember *model.GroupMember - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { var err error opMember, err = g.db.TakeGroupMember(ctx, req.GroupInfoForSet.GroupID, mcontext.GetOpUserID(ctx)) if err != nil { @@ -1068,7 +1052,7 @@ func (g *groupServer) SetGroupInfo(ctx context.Context, req *pbgroup.SetGroupInf func (g *groupServer) SetGroupInfoEx(ctx context.Context, req *pbgroup.SetGroupInfoExReq) (*pbgroup.SetGroupInfoExResp, error) { var opMember *model.GroupMember - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { var err error opMember, err = g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx)) @@ -1217,7 +1201,7 @@ func (g *groupServer) TransferGroupOwner(ctx context.Context, req *pbgroup.Trans return nil, errs.ErrArgs.WrapMsg("NewOwnerUser not in group " + req.NewOwnerUserID) } - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { if !(mcontext.GetOpUserID(ctx) == oldOwner.UserID && oldOwner.RoleLevel == constant.GroupOwner) { return nil, errs.ErrNoPermission.WrapMsg("no permission transfer group owner") } @@ -1360,7 +1344,7 @@ func (g *groupServer) DismissGroup(ctx context.Context, req *pbgroup.DismissGrou if err != nil { return nil, err } - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { if owner.UserID != mcontext.GetOpUserID(ctx) { return nil, errs.ErrNoPermission.WrapMsg("not group owner") } @@ -1417,7 +1401,7 @@ func (g *groupServer) MuteGroupMember(ctx context.Context, req *pbgroup.MuteGrou if err := g.PopulateGroupMember(ctx, member); err != nil { return nil, err } - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { opMember, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx)) if err != nil { return nil, err @@ -1453,7 +1437,7 @@ func (g *groupServer) CancelMuteGroupMember(ctx context.Context, req *pbgroup.Ca return nil, err } - if !authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { opMember, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx)) if err != nil { return nil, err @@ -1513,7 +1497,7 @@ func (g *groupServer) SetGroupMemberInfo(ctx context.Context, req *pbgroup.SetGr if opUserID == "" { return nil, errs.ErrNoPermission.WrapMsg("no op user id") } - isAppManagerUid := authverify.IsAppManagerUid(ctx, g.config.Share.IMAdminUserID) + isAppManagerUid := authverify.IsAdmin(ctx) groupMembers := make(map[string][]*pbgroup.SetGroupMemberInfo) for i, member := range req.Members { if member.RoleLevel != nil { diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index 43becf4f2..0a18371f9 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -243,7 +243,7 @@ func (g *NotificationSender) fillUserByUserID(ctx context.Context, userID string return errs.ErrInternalServer.WrapMsg("**sdkws.GroupMemberFullInfo is nil") } if groupID != "" { - if authverify.IsManagerUserID(userID, g.config.Share.IMAdminUserID) { + if authverify.CheckUserIsAdmin(ctx, userID) { *targetUser = &sdkws.GroupMemberFullInfo{ GroupID: groupID, UserID: userID, diff --git a/internal/rpc/group/sync.go b/internal/rpc/group/sync.go index d311ae076..822b15307 100644 --- a/internal/rpc/group/sync.go +++ b/internal/rpc/group/sync.go @@ -11,16 +11,24 @@ import ( "github.com/openimsdk/protocol/constant" pbgroup "github.com/openimsdk/protocol/group" "github.com/openimsdk/protocol/sdkws" + "github.com/openimsdk/tools/errs" + "github.com/openimsdk/tools/mcontext" + "github.com/openimsdk/tools/utils/datautil" ) const versionSyncLimit = 500 func (g *groupServer) GetFullGroupMemberUserIDs(ctx context.Context, req *pbgroup.GetFullGroupMemberUserIDsReq) (*pbgroup.GetFullGroupMemberUserIDsResp, error) { - vl, err := g.db.FindMaxGroupMemberVersionCache(ctx, req.GroupID) + userIDs, err := g.db.FindGroupMemberUserID(ctx, req.GroupID) if err != nil { return nil, err } - userIDs, err := g.db.FindGroupMemberUserID(ctx, req.GroupID) + if opUserID := mcontext.GetOpUserID(ctx); !datautil.Contain(opUserID, g.config.Share.IMAdminUserID...) { + if !datautil.Contain(opUserID, userIDs...) { + return nil, errs.ErrNoPermission.WrapMsg("user not in group") + } + } + vl, err := g.db.FindMaxGroupMemberVersionCache(ctx, req.GroupID) if err != nil { return nil, err } @@ -37,6 +45,9 @@ func (g *groupServer) GetFullGroupMemberUserIDs(ctx context.Context, req *pbgrou } func (g *groupServer) GetFullJoinGroupIDs(ctx context.Context, req *pbgroup.GetFullJoinGroupIDsReq) (*pbgroup.GetFullJoinGroupIDsResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } vl, err := g.db.FindMaxJoinGroupVersionCache(ctx, req.UserID) if err != nil { return nil, err @@ -65,6 +76,9 @@ func (g *groupServer) GetIncrementalGroupMember(ctx context.Context, req *pbgrou if group.Status == constant.GroupStatusDismissed { return nil, servererrs.ErrDismissedAlready.Wrap() } + if _, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx)); err != nil { + return nil, err + } var ( hasGroupUpdate bool sortVersion uint64 @@ -133,7 +147,7 @@ func (g *groupServer) GetIncrementalGroupMember(ctx context.Context, req *pbgrou } func (g *groupServer) GetIncrementalJoinGroup(ctx context.Context, req *pbgroup.GetIncrementalJoinGroupReq) (*pbgroup.GetIncrementalJoinGroupResp, error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, g.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } opt := incrversion.Option[*sdkws.GroupInfo, pbgroup.GetIncrementalJoinGroupResp]{ diff --git a/internal/rpc/msg/clear.go b/internal/rpc/msg/clear.go index 8e14b281e..96eb99aed 100644 --- a/internal/rpc/msg/clear.go +++ b/internal/rpc/msg/clear.go @@ -2,15 +2,16 @@ package msg import ( "context" + "strings" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/protocol/msg" "github.com/openimsdk/tools/log" - "strings" ) // DestructMsgs hard delete in Database. func (m *msgServer) DestructMsgs(ctx context.Context, req *msg.DestructMsgsReq) (*msg.DestructMsgsResp, error) { - if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } docs, err := m.MsgDatabase.GetRandBeforeMsg(ctx, req.Timestamp, int(req.Limit)) diff --git a/internal/rpc/msg/delete.go b/internal/rpc/msg/delete.go index d3485faaa..4590523d5 100644 --- a/internal/rpc/msg/delete.go +++ b/internal/rpc/msg/delete.go @@ -42,7 +42,7 @@ func (m *msgServer) validateDeleteSyncOpt(opt *msg.DeleteSyncOpt) (isSyncSelf, i } func (m *msgServer) ClearConversationsMsg(ctx context.Context, req *msg.ClearConversationsMsgReq) (*msg.ClearConversationsMsgResp, error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } if err := m.clearConversation(ctx, req.ConversationIDs, req.UserID, req.DeleteSyncOpt); err != nil { @@ -52,7 +52,7 @@ func (m *msgServer) ClearConversationsMsg(ctx context.Context, req *msg.ClearCon } func (m *msgServer) UserClearAllMsg(ctx context.Context, req *msg.UserClearAllMsgReq) (*msg.UserClearAllMsgResp, error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } conversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID) @@ -66,7 +66,7 @@ func (m *msgServer) UserClearAllMsg(ctx context.Context, req *msg.UserClearAllMs } func (m *msgServer) DeleteMsgs(ctx context.Context, req *msg.DeleteMsgsReq) (*msg.DeleteMsgsResp, error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } isSyncSelf, isSyncOther := m.validateDeleteSyncOpt(req.DeleteSyncOpt) @@ -102,7 +102,7 @@ func (m *msgServer) DeleteMsgPhysicalBySeq(ctx context.Context, req *msg.DeleteM } func (m *msgServer) DeleteMsgPhysical(ctx context.Context, req *msg.DeleteMsgPhysicalReq) (*msg.DeleteMsgPhysicalResp, error) { - if err := authverify.CheckAdmin(ctx, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } remainTime := timeutil.GetCurrentTimestampBySecond() - req.Timestamp diff --git a/internal/rpc/msg/revoke.go b/internal/rpc/msg/revoke.go index c2fb5833f..bd1d66ba1 100644 --- a/internal/rpc/msg/revoke.go +++ b/internal/rpc/msg/revoke.go @@ -42,7 +42,7 @@ func (m *msgServer) RevokeMsg(ctx context.Context, req *msg.RevokeMsgReq) (*msg. if req.Seq < 0 { return nil, errs.ErrArgs.WrapMsg("seq is invalid") } - if err := authverify.CheckAccessV3(ctx, req.UserID, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } user, err := m.UserLocalCache.GetUserInfo(ctx, req.UserID) @@ -63,11 +63,11 @@ func (m *msgServer) RevokeMsg(ctx context.Context, req *msg.RevokeMsgReq) (*msg. data, _ := json.Marshal(msgs[0]) log.ZDebug(ctx, "GetMsgBySeqs", "conversationID", req.ConversationID, "seq", req.Seq, "msg", string(data)) var role int32 - if !authverify.IsAppManagerUid(ctx, m.config.Share.IMAdminUserID) { + if !authverify.IsAdmin(ctx) { sessionType := msgs[0].SessionType switch sessionType { case constant.SingleChatType: - if err := authverify.CheckAccessV3(ctx, msgs[0].SendID, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, msgs[0].SendID); err != nil { return nil, err } role = user.AppMangerLevel diff --git a/internal/rpc/msg/sync_msg.go b/internal/rpc/msg/sync_msg.go index 6cf1c21d3..38eed93bc 100644 --- a/internal/rpc/msg/sync_msg.go +++ b/internal/rpc/msg/sync_msg.go @@ -118,7 +118,7 @@ func (m *msgServer) GetSeqMessage(ctx context.Context, req *msg.GetSeqMessageReq } func (m *msgServer) GetMaxSeq(ctx context.Context, req *sdkws.GetMaxSeqReq) (*sdkws.GetMaxSeqResp, error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, m.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } conversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID) diff --git a/internal/rpc/relation/black.go b/internal/rpc/relation/black.go index 2108d7dc5..4a0e60ecd 100644 --- a/internal/rpc/relation/black.go +++ b/internal/rpc/relation/black.go @@ -29,10 +29,9 @@ import ( ) func (s *friendServer) GetPaginationBlacks(ctx context.Context, req *relation.GetPaginationBlacksReq) (resp *relation.GetPaginationBlacksResp, err error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } - total, blacks, err := s.blackDatabase.FindOwnerBlacks(ctx, req.UserID, req.Pagination) if err != nil { return nil, err @@ -58,7 +57,7 @@ func (s *friendServer) IsBlack(ctx context.Context, req *relation.IsBlackReq) (* } func (s *friendServer) RemoveBlack(ctx context.Context, req *relation.RemoveBlackReq) (*relation.RemoveBlackResp, error) { - if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { return nil, err } @@ -73,7 +72,7 @@ func (s *friendServer) RemoveBlack(ctx context.Context, req *relation.RemoveBlac } func (s *friendServer) AddBlack(ctx context.Context, req *relation.AddBlackReq) (*relation.AddBlackResp, error) { - if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { return nil, err } @@ -99,7 +98,7 @@ func (s *friendServer) AddBlack(ctx context.Context, req *relation.AddBlackReq) } func (s *friendServer) GetSpecifiedBlacks(ctx context.Context, req *relation.GetSpecifiedBlacksReq) (*relation.GetSpecifiedBlacksResp, error) { - if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { return nil, err } diff --git a/internal/rpc/relation/friend.go b/internal/rpc/relation/friend.go index a4908d924..050cd5ffe 100644 --- a/internal/rpc/relation/friend.go +++ b/internal/rpc/relation/friend.go @@ -134,7 +134,7 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr // ok. func (s *friendServer) ApplyToAddFriend(ctx context.Context, req *relation.ApplyToAddFriendReq) (resp *relation.ApplyToAddFriendResp, err error) { resp = &relation.ApplyToAddFriendResp{} - if err := authverify.CheckAccessV3(ctx, req.FromUserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.FromUserID); err != nil { return nil, err } if req.ToUserID == req.FromUserID { @@ -164,7 +164,7 @@ func (s *friendServer) ApplyToAddFriend(ctx context.Context, req *relation.Apply // ok. func (s *friendServer) ImportFriends(ctx context.Context, req *relation.ImportFriendReq) (resp *relation.ImportFriendResp, err error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } @@ -200,7 +200,7 @@ func (s *friendServer) ImportFriends(ctx context.Context, req *relation.ImportFr // ok. func (s *friendServer) RespondFriendApply(ctx context.Context, req *relation.RespondFriendApplyReq) (resp *relation.RespondFriendApplyResp, err error) { resp = &relation.RespondFriendApplyResp{} - if err := authverify.CheckAccessV3(ctx, req.ToUserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.ToUserID); err != nil { return nil, err } @@ -235,7 +235,7 @@ func (s *friendServer) RespondFriendApply(ctx context.Context, req *relation.Res // ok. func (s *friendServer) DeleteFriend(ctx context.Context, req *relation.DeleteFriendReq) (resp *relation.DeleteFriendResp, err error) { - if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { return nil, err } @@ -260,7 +260,7 @@ func (s *friendServer) SetFriendRemark(ctx context.Context, req *relation.SetFri return nil, err } - if err := authverify.CheckAccessV3(ctx, req.OwnerUserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { return nil, err } @@ -330,7 +330,7 @@ func (s *friendServer) GetDesignatedFriendsApply(ctx context.Context, // Get received friend requests (i.e., those initiated by others). func (s *friendServer) GetPaginationFriendsApplyTo(ctx context.Context, req *relation.GetPaginationFriendsApplyToReq) (resp *relation.GetPaginationFriendsApplyToResp, err error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } @@ -353,7 +353,7 @@ func (s *friendServer) GetPaginationFriendsApplyTo(ctx context.Context, req *rel func (s *friendServer) GetPaginationFriendsApplyFrom(ctx context.Context, req *relation.GetPaginationFriendsApplyFromReq) (resp *relation.GetPaginationFriendsApplyFromResp, err error) { resp = &relation.GetPaginationFriendsApplyFromResp{} - if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } @@ -383,7 +383,7 @@ func (s *friendServer) IsFriend(ctx context.Context, req *relation.IsFriendReq) } func (s *friendServer) GetPaginationFriends(ctx context.Context, req *relation.GetPaginationFriendsReq) (resp *relation.GetPaginationFriendsResp, err error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } @@ -404,7 +404,7 @@ func (s *friendServer) GetPaginationFriends(ctx context.Context, req *relation.G } func (s *friendServer) GetFriendIDs(ctx context.Context, req *relation.GetFriendIDsReq) (resp *relation.GetFriendIDsResp, err error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } diff --git a/internal/rpc/relation/sync.go b/internal/rpc/relation/sync.go index 0ad94fe82..79fa0858c 100644 --- a/internal/rpc/relation/sync.go +++ b/internal/rpc/relation/sync.go @@ -2,10 +2,11 @@ package relation import ( "context" + "slices" + "github.com/openimsdk/open-im-server/v3/pkg/util/hashutil" "github.com/openimsdk/protocol/sdkws" "github.com/openimsdk/tools/log" - "slices" "github.com/openimsdk/open-im-server/v3/internal/rpc/incrversion" "github.com/openimsdk/open-im-server/v3/pkg/authverify" @@ -39,6 +40,9 @@ func (s *friendServer) NotificationUserInfoUpdate(ctx context.Context, req *rela } func (s *friendServer) GetFullFriendUserIDs(ctx context.Context, req *relation.GetFullFriendUserIDsReq) (*relation.GetFullFriendUserIDsResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } vl, err := s.db.FindMaxFriendVersionCache(ctx, req.UserID) if err != nil { return nil, err @@ -60,7 +64,7 @@ func (s *friendServer) GetFullFriendUserIDs(ctx context.Context, req *relation.G } func (s *friendServer) GetIncrementalFriends(ctx context.Context, req *relation.GetIncrementalFriendsReq) (*relation.GetIncrementalFriendsResp, error) { - if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } var sortVersion uint64 diff --git a/internal/rpc/third/log.go b/internal/rpc/third/log.go index 4d8cbc0bb..fba3ecb88 100644 --- a/internal/rpc/third/log.go +++ b/internal/rpc/third/log.go @@ -82,7 +82,7 @@ func (t *thirdServer) UploadLogs(ctx context.Context, req *third.UploadLogsReq) } func (t *thirdServer) DeleteLogs(ctx context.Context, req *third.DeleteLogsReq) (*third.DeleteLogsResp, error) { - if err := authverify.CheckAdmin(ctx, t.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } userID := "" @@ -123,7 +123,7 @@ func dbToPbLogInfos(logs []*relationtb.Log) []*third.LogInfo { } func (t *thirdServer) SearchLogs(ctx context.Context, req *third.SearchLogsReq) (*third.SearchLogsResp, error) { - if err := authverify.CheckAdmin(ctx, t.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } var ( diff --git a/internal/rpc/third/s3.go b/internal/rpc/third/s3.go index 757320dac..bfdd9f1b8 100644 --- a/internal/rpc/third/s3.go +++ b/internal/rpc/third/s3.go @@ -198,7 +198,7 @@ func (t *thirdServer) InitiateFormData(ctx context.Context, req *third.InitiateF var duration time.Duration opUserID := mcontext.GetOpUserID(ctx) var key string - if t.IsManagerUserID(opUserID) { + if authverify.CheckUserIsAdmin(ctx, opUserID) { if req.Millisecond <= 0 { duration = time.Minute * 10 } else { @@ -289,7 +289,7 @@ func (t *thirdServer) apiAddress(prefix, name string) string { } func (t *thirdServer) DeleteOutdatedData(ctx context.Context, req *third.DeleteOutdatedDataReq) (*third.DeleteOutdatedDataResp, error) { - if err := authverify.CheckAdmin(ctx, t.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } engine := t.config.RpcConfig.Object.Enable diff --git a/internal/rpc/third/tool.go b/internal/rpc/third/tool.go index 4e22ffbf9..a063fa654 100644 --- a/internal/rpc/third/tool.go +++ b/internal/rpc/third/tool.go @@ -54,7 +54,7 @@ func (t *thirdServer) checkUploadName(ctx context.Context, name string) error { if opUserID == "" { return errs.ErrNoPermission.WrapMsg("opUserID is empty") } - if !authverify.IsManagerUserID(opUserID, t.config.Share.IMAdminUserID) { + if !authverify.CheckUserIsAdmin(ctx, opUserID) { if !strings.HasPrefix(name, opUserID+"/") { return errs.ErrNoPermission.WrapMsg(fmt.Sprintf("name must start with `%s/`", opUserID)) } @@ -79,10 +79,6 @@ func checkValidObjectName(objectName string) error { return checkValidObjectNamePrefix(objectName) } -func (t *thirdServer) IsManagerUserID(opUserID string) bool { - return authverify.IsManagerUserID(opUserID, t.config.Share.IMAdminUserID) -} - func putUpdate[T any](update map[string]any, name string, val interface{ GetValuePtr() *T }) { ptrVal := val.GetValuePtr() if ptrVal == nil { diff --git a/internal/rpc/user/config.go b/internal/rpc/user/config.go index 5a9a46359..f3f5a7a96 100644 --- a/internal/rpc/user/config.go +++ b/internal/rpc/user/config.go @@ -11,7 +11,7 @@ import ( func (s *userServer) GetUserClientConfig(ctx context.Context, req *pbuser.GetUserClientConfigReq) (*pbuser.GetUserClientConfigResp, error) { if req.UserID != "" { - if err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } if _, err := s.db.GetUserByID(ctx, req.UserID); err != nil { @@ -26,7 +26,7 @@ func (s *userServer) GetUserClientConfig(ctx context.Context, req *pbuser.GetUse } func (s *userServer) SetUserClientConfig(ctx context.Context, req *pbuser.SetUserClientConfigReq) (*pbuser.SetUserClientConfigResp, error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } if req.UserID != "" { @@ -41,7 +41,7 @@ func (s *userServer) SetUserClientConfig(ctx context.Context, req *pbuser.SetUse } func (s *userServer) DelUserClientConfig(ctx context.Context, req *pbuser.DelUserClientConfigReq) (*pbuser.DelUserClientConfigResp, error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } if err := s.clientConfig.DelUserConfig(ctx, req.UserID, req.Keys); err != nil { @@ -51,7 +51,7 @@ func (s *userServer) DelUserClientConfig(ctx context.Context, req *pbuser.DelUse } func (s *userServer) PageUserClientConfig(ctx context.Context, req *pbuser.PageUserClientConfigReq) (*pbuser.PageUserClientConfigResp, error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } total, res, err := s.clientConfig.GetUserConfigPage(ctx, req.UserID, req.Key, req.Pagination) diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index 6ef61f773..7f082f784 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -150,7 +150,7 @@ func (s *userServer) GetDesignateUsers(ctx context.Context, req *pbuser.GetDesig // UpdateUserInfo func (s *userServer) UpdateUserInfo(ctx context.Context, req *pbuser.UpdateUserInfoReq) (resp *pbuser.UpdateUserInfoResp, err error) { resp = &pbuser.UpdateUserInfoResp{} - err = authverify.CheckAccessV3(ctx, req.UserInfo.UserID, s.config.Share.IMAdminUserID) + err = authverify.CheckAccess(ctx, req.UserInfo.UserID) if err != nil { return nil, err } @@ -177,7 +177,7 @@ func (s *userServer) UpdateUserInfo(ctx context.Context, req *pbuser.UpdateUserI func (s *userServer) UpdateUserInfoEx(ctx context.Context, req *pbuser.UpdateUserInfoExReq) (resp *pbuser.UpdateUserInfoExResp, err error) { resp = &pbuser.UpdateUserInfoExResp{} - err = authverify.CheckAccessV3(ctx, req.UserInfo.UserID, s.config.Share.IMAdminUserID) + err = authverify.CheckAccess(ctx, req.UserInfo.UserID) if err != nil { return nil, err } @@ -235,8 +235,7 @@ func (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckR if datautil.Duplicate(req.CheckUserIDs) { return nil, errs.ErrArgs.WrapMsg("userID repeated") } - err = authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID) - if err != nil { + if err = authverify.CheckAdmin(ctx); err != nil { return nil, err } users, err := s.db.Find(ctx, req.CheckUserIDs) @@ -283,14 +282,12 @@ func (s *userServer) UserRegister(ctx context.Context, req *pbuser.UserRegisterR return nil, errs.ErrArgs.WrapMsg("users is empty") } // check if secret is changed - if s.config.Share.Secret == defaultSecret { - return nil, servererrs.ErrSecretNotChanged.Wrap() - } - - if err = authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + //if s.config.Share.Secret == defaultSecret { + // return nil, servererrs.ErrSecretNotChanged.Wrap() + //} + if err = authverify.CheckAdmin(ctx); err != nil { return nil, err } - if datautil.DuplicateAny(req.Users, func(e *sdkws.UserInfo) string { return e.UserID }) { return nil, errs.ErrArgs.WrapMsg("userID repeated") } @@ -356,7 +353,7 @@ func (s *userServer) GetAllUserID(ctx context.Context, req *pbuser.GetAllUserIDR // ProcessUserCommandAdd user general function add. func (s *userServer) ProcessUserCommandAdd(ctx context.Context, req *pbuser.ProcessUserCommandAddReq) (*pbuser.ProcessUserCommandAddResp, error) { - err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID) + err := authverify.CheckAccess(ctx, req.UserID) if err != nil { return nil, err } @@ -384,7 +381,7 @@ func (s *userServer) ProcessUserCommandAdd(ctx context.Context, req *pbuser.Proc // ProcessUserCommandDelete user general function delete. func (s *userServer) ProcessUserCommandDelete(ctx context.Context, req *pbuser.ProcessUserCommandDeleteReq) (*pbuser.ProcessUserCommandDeleteResp, error) { - err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID) + err := authverify.CheckAccess(ctx, req.UserID) if err != nil { return nil, err } @@ -403,7 +400,7 @@ func (s *userServer) ProcessUserCommandDelete(ctx context.Context, req *pbuser.P // ProcessUserCommandUpdate user general function update. func (s *userServer) ProcessUserCommandUpdate(ctx context.Context, req *pbuser.ProcessUserCommandUpdateReq) (*pbuser.ProcessUserCommandUpdateResp, error) { - err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID) + err := authverify.CheckAccess(ctx, req.UserID) if err != nil { return nil, err } @@ -432,7 +429,7 @@ func (s *userServer) ProcessUserCommandUpdate(ctx context.Context, req *pbuser.P func (s *userServer) ProcessUserCommandGet(ctx context.Context, req *pbuser.ProcessUserCommandGetReq) (*pbuser.ProcessUserCommandGetResp, error) { - err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID) + err := authverify.CheckAccess(ctx, req.UserID) if err != nil { return nil, err } @@ -461,7 +458,7 @@ func (s *userServer) ProcessUserCommandGet(ctx context.Context, req *pbuser.Proc } func (s *userServer) ProcessUserCommandGetAll(ctx context.Context, req *pbuser.ProcessUserCommandGetAllReq) (*pbuser.ProcessUserCommandGetAllResp, error) { - err := authverify.CheckAccessV3(ctx, req.UserID, s.config.Share.IMAdminUserID) + err := authverify.CheckAccess(ctx, req.UserID) if err != nil { return nil, err } @@ -490,7 +487,7 @@ func (s *userServer) ProcessUserCommandGetAll(ctx context.Context, req *pbuser.P } func (s *userServer) AddNotificationAccount(ctx context.Context, req *pbuser.AddNotificationAccountReq) (*pbuser.AddNotificationAccountResp, error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } if req.AppMangerLevel < constant.AppNotificationAdmin { @@ -536,7 +533,7 @@ func (s *userServer) AddNotificationAccount(ctx context.Context, req *pbuser.Add } func (s *userServer) UpdateNotificationAccountInfo(ctx context.Context, req *pbuser.UpdateNotificationAccountInfoReq) (*pbuser.UpdateNotificationAccountInfoResp, error) { - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } @@ -563,7 +560,7 @@ func (s *userServer) UpdateNotificationAccountInfo(ctx context.Context, req *pbu func (s *userServer) SearchNotificationAccount(ctx context.Context, req *pbuser.SearchNotificationAccountReq) (*pbuser.SearchNotificationAccountResp, error) { // Check if user is an admin - if err := authverify.CheckAdmin(ctx, s.config.Share.IMAdminUserID); err != nil { + if err := authverify.CheckAdmin(ctx); err != nil { return nil, err } diff --git a/pkg/authverify/token.go b/pkg/authverify/token.go index 872feb1cf..75fb1448b 100644 --- a/pkg/authverify/token.go +++ b/pkg/authverify/token.go @@ -31,32 +31,49 @@ func Secret(secret string) jwt.Keyfunc { } } -func CheckAccessV3(ctx context.Context, ownerUserID string, imAdminUserID []string) (err error) { - opUserID := mcontext.GetOpUserID(ctx) - if datautil.Contain(opUserID, imAdminUserID...) { - return nil - } - if opUserID == ownerUserID { +func CheckAdmin(ctx context.Context) error { + if IsAdmin(ctx) { return nil } - return servererrs.ErrNoPermission.WrapMsg("ownerUserID", ownerUserID) + return servererrs.ErrNoPermission.WrapMsg(fmt.Sprintf("user %s is not admin userID", mcontext.GetOpUserID(ctx))) } -func IsAppManagerUid(ctx context.Context, imAdminUserID []string) bool { - return datautil.Contain(mcontext.GetOpUserID(ctx), imAdminUserID...) +//func IsManagerUserID(opUserID string, imAdminUserID []string) bool { +// return datautil.Contain(opUserID, imAdminUserID...) +//} + +func CheckUserIsAdmin(ctx context.Context, userID string) bool { + return datautil.Contain(userID, GetIMAdminUserIDs(ctx)...) } -func CheckAdmin(ctx context.Context, imAdminUserID []string) error { - if datautil.Contain(mcontext.GetOpUserID(ctx), imAdminUserID...) { - return nil - } - return servererrs.ErrNoPermission.WrapMsg(fmt.Sprintf("user %s is not admin userID", mcontext.GetOpUserID(ctx))) +func CheckSystemAccount(ctx context.Context, level int32) bool { + return level >= constant.AppAdmin } -func IsManagerUserID(opUserID string, imAdminUserID []string) bool { - return datautil.Contain(opUserID, imAdminUserID...) +const ( + CtxIsAdminKey = "CtxIsAdminKey" +) + +func WithIMAdminUserIDs(ctx context.Context, imAdminUserID []string) context.Context { + return context.WithValue(ctx, CtxIsAdminKey, imAdminUserID) } -func CheckSystemAccount(ctx context.Context, level int32) bool { - return level >= constant.AppAdmin +func GetIMAdminUserIDs(ctx context.Context) []string { + imAdminUserID, _ := ctx.Value(CtxIsAdminKey).([]string) + return imAdminUserID +} + +func IsAdmin(ctx context.Context) bool { + return datautil.Contain(mcontext.GetOpUserID(ctx), GetIMAdminUserIDs(ctx)...) +} + +func CheckAccess(ctx context.Context, ownerUserID string) error { + opUserID := mcontext.GetOpUserID(ctx) + if opUserID == ownerUserID { + return nil + } + if datautil.Contain(mcontext.GetOpUserID(ctx), GetIMAdminUserIDs(ctx)...) { + return nil + } + return servererrs.ErrNoPermission.WrapMsg("ownerUserID", ownerUserID) } diff --git a/pkg/common/startrpc/mw.go b/pkg/common/startrpc/mw.go new file mode 100644 index 000000000..c6cd55380 --- /dev/null +++ b/pkg/common/startrpc/mw.go @@ -0,0 +1,15 @@ +package startrpc + +import ( + "context" + + "github.com/openimsdk/open-im-server/v3/pkg/authverify" + "google.golang.org/grpc" +) + +func grpcServerIMAdminUserID(imAdminUserID []string) grpc.ServerOption { + return grpc.ChainUnaryInterceptor(func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + ctx = authverify.WithIMAdminUserIDs(ctx, imAdminUserID) + return handler(ctx, req) + }) +} diff --git a/pkg/common/startrpc/start.go b/pkg/common/startrpc/start.go index af50c408d..03621343b 100644 --- a/pkg/common/startrpc/start.go +++ b/pkg/common/startrpc/start.go @@ -37,7 +37,8 @@ import ( "github.com/openimsdk/tools/discovery" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" - "github.com/openimsdk/tools/mw" + grpccli "github.com/openimsdk/tools/mw/grpc/client" + grpcsrv "github.com/openimsdk/tools/mw/grpc/server" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -76,6 +77,34 @@ func getConfigRpcMaxRequestBody(value reflect.Value) *conf.MaxRequestBody { return nil } +func getConfigShare(value reflect.Value) *conf.Share { + for value.Kind() == reflect.Pointer { + value = value.Elem() + } + if value.Kind() == reflect.Struct { + num := value.NumField() + for i := 0; i < num; i++ { + field := value.Field(i) + if !field.CanInterface() { + continue + } + for field.Kind() == reflect.Pointer { + field = field.Elem() + } + switch elem := field.Interface().(type) { + case conf.Share: + return &elem + } + if field.Kind() == reflect.Struct { + if elem := getConfigShare(field); elem != nil { + return elem + } + } + } + } + return nil +} + func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *conf.Prometheus, listenIP, registerIP string, autoSetPorts bool, rpcPorts []int, index int, rpcRegisterName string, notification *conf.Notification, config T, watchConfigNames []string, watchServiceNames []string, @@ -87,12 +116,20 @@ func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *c } maxRequestBody := getConfigRpcMaxRequestBody(reflect.ValueOf(config)) + shareConfig := getConfigShare(reflect.ValueOf(config)) log.ZDebug(ctx, "rpc start", "rpcMaxRequestBody", maxRequestBody, "rpcRegisterName", rpcRegisterName, "registerIP", registerIP, "listenIP", listenIP) options = append(options, - mw.GrpcServer(), + grpcsrv.GrpcServerMetadataContext(), + grpcsrv.GrpcServerLogger(), + grpcsrv.GrpcServerErrorConvert(), + grpcsrv.GrpcServerRequestValidate(), + grpcsrv.GrpcServerPanicCapture(), ) + if shareConfig != nil && len(shareConfig.IMAdminUserID) > 0 { + options = append(options, grpcServerIMAdminUserID(shareConfig.IMAdminUserID)) + } var clientOptions []grpc.DialOption if maxRequestBody != nil { if maxRequestBody.RequestMaxBodySize > 0 { @@ -129,8 +166,12 @@ func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *c defer client.Close() client.AddOption( - mw.GrpcClient(), grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"LoadBalancingPolicy": "%s"}`, "round_robin")), + + grpccli.GrpcClientLogger(), + grpccli.GrpcClientContext(), + grpccli.GrpcClientErrorConvert(), ) if len(clientOptions) > 0 { client.AddOption(clientOptions...) From 632a65303c88b5465e4c3f8c4e9fb37d632b1911 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Fri, 16 May 2025 17:30:38 +0800 Subject: [PATCH 34/59] fix: standalone mode cannot be used (#3360) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake * fix: in standalone mode, the user online status is wrong --- go.mod | 2 +- go.sum | 4 +- internal/api/router.go | 13 +-- internal/msggateway/ws_server.go | 2 +- .../online_msg_to_mongo_handler.go | 10 +- internal/push/push_handler.go | 8 +- pkg/authverify/token.go | 6 +- pkg/common/startrpc/start.go | 60 +----------- pkg/common/startrpc/tools.go | 39 ++++++++ pkg/rpccache/online.go | 92 +++++++++++++++---- 10 files changed, 127 insertions(+), 109 deletions(-) create mode 100644 pkg/common/startrpc/tools.go diff --git a/go.mod b/go.mod index 4af0695db..d35252536 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/openimsdk/protocol v0.0.73-alpha.6 - github.com/openimsdk/tools v0.0.50-alpha.81 + github.com/openimsdk/tools v0.0.50-alpha.83 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 6bc410a2d..35fc3d4c6 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5b github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.73-alpha.6 h1:sna9coWG7HN1zObBPtvG0Ki/vzqHXiB4qKbA5P3w7kc= github.com/openimsdk/protocol v0.0.73-alpha.6/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= -github.com/openimsdk/tools v0.0.50-alpha.81 h1:VbuJKtigNXLkCKB/Q6f2UHsqoSaTOAwS8F51c1nhOCA= -github.com/openimsdk/tools v0.0.50-alpha.81/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= +github.com/openimsdk/tools v0.0.50-alpha.83 h1:7c1D40YGqIWUmGfCII5pduETGC/8c2DyS9SQ4LvoplU= +github.com/openimsdk/tools v0.0.50-alpha.83/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/internal/api/router.go b/internal/api/router.go index 7c2c78c7d..add8ef36b 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -9,12 +9,8 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" - "github.com/openimsdk/open-im-server/v3/pkg/authverify" - "github.com/openimsdk/tools/mcontext" - "github.com/openimsdk/tools/utils/datautil" - clientv3 "go.etcd.io/etcd/client/v3" - "github.com/openimsdk/open-im-server/v3/internal/api/jssdk" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" "github.com/openimsdk/open-im-server/v3/pkg/common/servererrs" @@ -32,6 +28,7 @@ import ( "github.com/openimsdk/tools/discovery/etcd" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mw" + clientv3 "go.etcd.io/etcd/client/v3" ) const ( @@ -271,7 +268,6 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin 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("/update_conversations_by_user", c.UpdateConversationsByUser) } { @@ -315,7 +311,6 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin configGroup.POST("/get_config_list", cm.GetConfigList) configGroup.POST("/get_config", cm.GetConfig) configGroup.POST("/set_config", cm.SetConfig) - configGroup.POST("/set_configs", cm.SetConfigs) configGroup.POST("/reset_config", cm.ResetConfig) configGroup.POST("/set_enable_config_manager", cm.SetEnableConfigManager) configGroup.POST("/get_enable_config_manager", cm.GetEnableConfigManager) @@ -359,9 +354,7 @@ func GinParseToken(authClient *rpcli.AuthClient) gin.HandlerFunc { func setGinIsAdmin(imAdminUserID []string) gin.HandlerFunc { return func(c *gin.Context) { - opUserID := mcontext.GetOpUserID(c) - admin := datautil.Contain(opUserID, imAdminUserID...) - c.Set(authverify.CtxIsAdminKey, admin) + c.Set(authverify.CtxAdminUserIDsKey, imAdminUserID) } } diff --git a/internal/msggateway/ws_server.go b/internal/msggateway/ws_server.go index d69062bda..211482cb1 100644 --- a/internal/msggateway/ws_server.go +++ b/internal/msggateway/ws_server.go @@ -49,7 +49,7 @@ type WsServer struct { unregisterChan chan *Client kickHandlerChan chan *kickHandler clients UserMap - online *rpccache.OnlineCache + online rpccache.OnlineCache subscription *Subscription clientPool sync.Pool onlineUserNum atomic.Int64 diff --git a/internal/msgtransfer/online_msg_to_mongo_handler.go b/internal/msgtransfer/online_msg_to_mongo_handler.go index ae14d02a1..a895bb9c4 100644 --- a/internal/msgtransfer/online_msg_to_mongo_handler.go +++ b/internal/msgtransfer/online_msg_to_mongo_handler.go @@ -48,15 +48,7 @@ func (mc *OnlineHistoryMongoConsumerHandler) HandleChatWs2Mongo(ctx context.Cont log.ZDebug(ctx, "mongo consumer recv msg", "msgs", msgFromMQ.String()) err = mc.msgTransferDatabase.BatchInsertChat2DB(ctx, msgFromMQ.ConversationID, msgFromMQ.MsgData, msgFromMQ.LastSeq) if err != nil { - log.ZError( - ctx, - "single data insert to mongo err", - err, - "msg", - msgFromMQ.MsgData, - "conversationID", - msgFromMQ.ConversationID, - ) + log.ZError(ctx, "single data insert to mongo err", err, "msg", msgFromMQ.MsgData, "conversationID", msgFromMQ.ConversationID) prommetrics.MsgInsertMongoFailedCounter.Inc() } else { prommetrics.MsgInsertMongoSuccessCounter.Inc() diff --git a/internal/push/push_handler.go b/internal/push/push_handler.go index 418c4c7f2..7b1efe3bf 100644 --- a/internal/push/push_handler.go +++ b/internal/push/push_handler.go @@ -33,7 +33,7 @@ type ConsumerHandler struct { offlinePusher offlinepush.OfflinePusher onlinePusher OnlinePusher pushDatabase controller.PushDatabase - onlineCache *rpccache.OnlineCache + onlineCache rpccache.OnlineCache groupLocalCache *rpccache.GroupLocalCache conversationLocalCache *rpccache.ConversationLocalCache webhookClient *webhook.Client @@ -120,11 +120,7 @@ func (c *ConsumerHandler) HandleMs2PsChat(ctx context.Context, msg []byte) { } func (c *ConsumerHandler) WaitCache() { - c.onlineCache.Lock.Lock() - for c.onlineCache.CurrentPhase.Load() < rpccache.DoSubscribeOver { - c.onlineCache.Cond.Wait() - } - c.onlineCache.Lock.Unlock() + c.onlineCache.WaitCache() } // Push2User Suitable for two types of conversations, one is SingleChatType and the other is NotificationChatType. diff --git a/pkg/authverify/token.go b/pkg/authverify/token.go index 75fb1448b..2e3639776 100644 --- a/pkg/authverify/token.go +++ b/pkg/authverify/token.go @@ -51,15 +51,15 @@ func CheckSystemAccount(ctx context.Context, level int32) bool { } const ( - CtxIsAdminKey = "CtxIsAdminKey" + CtxAdminUserIDsKey = "CtxAdminUserIDsKey" ) func WithIMAdminUserIDs(ctx context.Context, imAdminUserID []string) context.Context { - return context.WithValue(ctx, CtxIsAdminKey, imAdminUserID) + return context.WithValue(ctx, CtxAdminUserIDsKey, imAdminUserID) } func GetIMAdminUserIDs(ctx context.Context) []string { - imAdminUserID, _ := ctx.Value(CtxIsAdminKey).([]string) + imAdminUserID, _ := ctx.Value(CtxAdminUserIDsKey).([]string) return imAdminUserID } diff --git a/pkg/common/startrpc/start.go b/pkg/common/startrpc/start.go index 03621343b..b99d32db1 100644 --- a/pkg/common/startrpc/start.go +++ b/pkg/common/startrpc/start.go @@ -47,64 +47,6 @@ func init() { prommetrics.RegistryAll() } -func getConfigRpcMaxRequestBody(value reflect.Value) *conf.MaxRequestBody { - for value.Kind() == reflect.Pointer { - value = value.Elem() - } - if value.Kind() == reflect.Struct { - num := value.NumField() - for i := 0; i < num; i++ { - field := value.Field(i) - if !field.CanInterface() { - continue - } - for field.Kind() == reflect.Pointer { - field = field.Elem() - } - switch elem := field.Interface().(type) { - case conf.Share: - return &elem.RPCMaxBodySize - case conf.MaxRequestBody: - return &elem - } - if field.Kind() == reflect.Struct { - if elem := getConfigRpcMaxRequestBody(field); elem != nil { - return elem - } - } - } - } - return nil -} - -func getConfigShare(value reflect.Value) *conf.Share { - for value.Kind() == reflect.Pointer { - value = value.Elem() - } - if value.Kind() == reflect.Struct { - num := value.NumField() - for i := 0; i < num; i++ { - field := value.Field(i) - if !field.CanInterface() { - continue - } - for field.Kind() == reflect.Pointer { - field = field.Elem() - } - switch elem := field.Interface().(type) { - case conf.Share: - return &elem - } - if field.Kind() == reflect.Struct { - if elem := getConfigShare(field); elem != nil { - return elem - } - } - } - } - return nil -} - func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *conf.Prometheus, listenIP, registerIP string, autoSetPorts bool, rpcPorts []int, index int, rpcRegisterName string, notification *conf.Notification, config T, watchConfigNames []string, watchServiceNames []string, @@ -122,8 +64,8 @@ func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *c options = append(options, grpcsrv.GrpcServerMetadataContext(), - grpcsrv.GrpcServerLogger(), grpcsrv.GrpcServerErrorConvert(), + grpcsrv.GrpcServerLogger(), grpcsrv.GrpcServerRequestValidate(), grpcsrv.GrpcServerPanicCapture(), ) diff --git a/pkg/common/startrpc/tools.go b/pkg/common/startrpc/tools.go new file mode 100644 index 000000000..54b518181 --- /dev/null +++ b/pkg/common/startrpc/tools.go @@ -0,0 +1,39 @@ +package startrpc + +import ( + "reflect" + + conf "github.com/openimsdk/open-im-server/v3/pkg/common/config" +) + +func getConfig[T any](value reflect.Value) *T { + for value.Kind() == reflect.Pointer { + value = value.Elem() + } + if value.Kind() == reflect.Struct { + num := value.NumField() + for i := 0; i < num; i++ { + field := value.Field(i) + for field.Kind() == reflect.Pointer { + field = field.Elem() + } + if field.Kind() == reflect.Struct { + if elem, ok := field.Interface().(T); ok { + return &elem + } + if elem := getConfig[T](field); elem != nil { + return elem + } + } + } + } + return nil +} + +func getConfigRpcMaxRequestBody(value reflect.Value) *conf.MaxRequestBody { + return getConfig[conf.MaxRequestBody](value) +} + +func getConfigShare(value reflect.Value) *conf.Share { + return getConfig[conf.Share](value) +} diff --git a/pkg/rpccache/online.go b/pkg/rpccache/online.go index 959b054a2..9b8c03101 100644 --- a/pkg/rpccache/online.go +++ b/pkg/rpccache/online.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "time" + "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/rpcli" "github.com/openimsdk/protocol/constant" "github.com/openimsdk/protocol/user" @@ -23,9 +24,25 @@ import ( "github.com/redis/go-redis/v9" ) -func NewOnlineCache(client *rpcli.UserClient, group *GroupLocalCache, rdb redis.UniversalClient, fullUserCache bool, fn func(ctx context.Context, userID string, platformIDs []int32)) (*OnlineCache, error) { +const ( + Begin uint32 = iota + DoOnlineStatusOver + DoSubscribeOver +) + +type OnlineCache interface { + GetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) + GetUserOnline(ctx context.Context, userID string) (bool, error) + GetUsersOnline(ctx context.Context, userIDs []string) ([]string, []string, error) + WaitCache() +} + +func NewOnlineCache(client *rpcli.UserClient, group *GroupLocalCache, rdb redis.UniversalClient, fullUserCache bool, fn func(ctx context.Context, userID string, platformIDs []int32)) (OnlineCache, error) { + if config.Standalone() { + return disableOnlineCache{client: client}, nil + } l := &sync.Mutex{} - x := &OnlineCache{ + x := &defaultOnlineCache{ client: client, group: group, fullUserCache: fullUserCache, @@ -60,13 +77,7 @@ func NewOnlineCache(client *rpcli.UserClient, group *GroupLocalCache, rdb redis. return x, nil } -const ( - Begin uint32 = iota - DoOnlineStatusOver - DoSubscribeOver -) - -type OnlineCache struct { +type defaultOnlineCache struct { client *rpcli.UserClient group *GroupLocalCache @@ -82,7 +93,7 @@ type OnlineCache struct { CurrentPhase atomic.Uint32 } -func (o *OnlineCache) initUsersOnlineStatus(ctx context.Context) (err error) { +func (o *defaultOnlineCache) initUsersOnlineStatus(ctx context.Context) (err error) { log.ZDebug(ctx, "init users online status begin") var ( @@ -135,7 +146,7 @@ func (o *OnlineCache) initUsersOnlineStatus(ctx context.Context) (err error) { return nil } -func (o *OnlineCache) doSubscribe(ctx context.Context, rdb redis.UniversalClient, fn func(ctx context.Context, userID string, platformIDs []int32)) { +func (o *defaultOnlineCache) doSubscribe(ctx context.Context, rdb redis.UniversalClient, fn func(ctx context.Context, userID string, platformIDs []int32)) { o.Lock.Lock() ch := rdb.Subscribe(ctx, cachekey.OnlineChannel).Channel() for o.CurrentPhase.Load() < DoOnlineStatusOver { @@ -186,7 +197,7 @@ func (o *OnlineCache) doSubscribe(ctx context.Context, rdb redis.UniversalClient } } -func (o *OnlineCache) getUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) { +func (o *defaultOnlineCache) getUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) { platformIDs, err := o.lruCache.Get(userID, func() ([]int32, error) { return o.client.GetUserOnlinePlatform(ctx, userID) }) @@ -198,7 +209,7 @@ func (o *OnlineCache) getUserOnlinePlatform(ctx context.Context, userID string) return platformIDs, nil } -func (o *OnlineCache) GetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) { +func (o *defaultOnlineCache) GetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) { platformIDs, err := o.getUserOnlinePlatform(ctx, userID) if err != nil { return nil, err @@ -208,7 +219,7 @@ func (o *OnlineCache) GetUserOnlinePlatform(ctx context.Context, userID string) return platformIDs, nil } -func (o *OnlineCache) GetUserOnline(ctx context.Context, userID string) (bool, error) { +func (o *defaultOnlineCache) GetUserOnline(ctx context.Context, userID string) (bool, error) { platformIDs, err := o.getUserOnlinePlatform(ctx, userID) if err != nil { return false, err @@ -216,7 +227,7 @@ func (o *OnlineCache) GetUserOnline(ctx context.Context, userID string) (bool, e return len(platformIDs) > 0, nil } -func (o *OnlineCache) getUserOnlinePlatformBatch(ctx context.Context, userIDs []string) (map[string][]int32, error) { +func (o *defaultOnlineCache) getUserOnlinePlatformBatch(ctx context.Context, userIDs []string) (map[string][]int32, error) { if len(userIDs) == 0 { return nil, nil } @@ -240,7 +251,7 @@ func (o *OnlineCache) getUserOnlinePlatformBatch(ctx context.Context, userIDs [] return platformIDsMap, nil } -func (o *OnlineCache) GetUsersOnline(ctx context.Context, userIDs []string) ([]string, []string, error) { +func (o *defaultOnlineCache) GetUsersOnline(ctx context.Context, userIDs []string) ([]string, []string, error) { t := time.Now() var ( @@ -276,7 +287,7 @@ func (o *OnlineCache) GetUsersOnline(ctx context.Context, userIDs []string) ([]s return onlineUserIDs, offlineUserIDs, nil } -func (o *OnlineCache) setUserOnline(userID string, platformIDs []int32) { +func (o *defaultOnlineCache) setUserOnline(userID string, platformIDs []int32) { switch o.fullUserCache { case true: o.mapCache.Store(userID, platformIDs) @@ -285,6 +296,51 @@ func (o *OnlineCache) setUserOnline(userID string, platformIDs []int32) { } } -func (o *OnlineCache) setHasUserOnline(userID string, platformIDs []int32) bool { +func (o *defaultOnlineCache) setHasUserOnline(userID string, platformIDs []int32) bool { return o.lruCache.SetHas(userID, platformIDs) } + +func (o *defaultOnlineCache) WaitCache() { + o.Lock.Lock() + for o.CurrentPhase.Load() < DoSubscribeOver { + o.Cond.Wait() + } + o.Lock.Unlock() +} + +type disableOnlineCache struct { + client *rpcli.UserClient +} + +func (o disableOnlineCache) GetUserOnlinePlatform(ctx context.Context, userID string) ([]int32, error) { + return o.client.GetUserOnlinePlatform(ctx, userID) +} + +func (o disableOnlineCache) GetUserOnline(ctx context.Context, userID string) (bool, error) { + onlinePlatform, err := o.client.GetUserOnlinePlatform(ctx, userID) + if err != nil { + return false, err + } + return len(onlinePlatform) > 0, err +} + +func (o disableOnlineCache) GetUsersOnline(ctx context.Context, userIDs []string) ([]string, []string, error) { + var ( + onlineUserIDs = make([]string, 0, len(userIDs)) + offlineUserIDs = make([]string, 0, len(userIDs)) + ) + for _, userID := range userIDs { + online, err := o.GetUserOnline(ctx, userID) + if err != nil { + return nil, nil, err + } + if online { + onlineUserIDs = append(onlineUserIDs, userID) + } else { + offlineUserIDs = append(offlineUserIDs, userID) + } + } + return onlineUserIDs, offlineUserIDs, nil +} + +func (o disableOnlineCache) WaitCache() {} From 748d783d36fc3f1e4eacaee64203b10e8e2b8744 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Tue, 20 May 2025 11:30:00 +0800 Subject: [PATCH 35/59] feat: add rpc interface permission check (#3366) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake * fix: in standalone mode, the user online status is wrong * fix: add permission check * fix: add permission check --- internal/api/conversation.go | 6 +-- internal/api/router.go | 2 +- internal/rpc/conversation/conversation.go | 66 ++++++++++++++++------- internal/rpc/conversation/sync.go | 3 ++ internal/rpc/group/cache.go | 4 ++ internal/rpc/group/group.go | 48 +++++++++++++++-- internal/rpc/group/statistics.go | 4 ++ internal/rpc/group/sync.go | 15 ++---- internal/rpc/msg/as_read.go | 15 ++++-- internal/rpc/msg/delete.go | 3 ++ internal/rpc/msg/send.go | 12 ++--- internal/rpc/msg/seq.go | 3 +- internal/rpc/msg/statistics.go | 10 +++- internal/rpc/msg/sync_msg.go | 3 ++ internal/rpc/relation/black.go | 3 ++ internal/rpc/relation/friend.go | 28 +++++++--- internal/rpc/third/log.go | 3 +- internal/rpc/third/third.go | 4 ++ pkg/authverify/token.go | 49 +++++++++++++++-- pkg/tools/batcher/batcher.go | 4 +- 20 files changed, 222 insertions(+), 63 deletions(-) diff --git a/internal/api/conversation.go b/internal/api/conversation.go index f6eadf15a..5a191c0ec 100644 --- a/internal/api/conversation.go +++ b/internal/api/conversation.go @@ -49,9 +49,9 @@ func (o *ConversationApi) SetConversations(c *gin.Context) { a2r.Call(c, conversation.ConversationClient.SetConversations, o.Client) } -func (o *ConversationApi) GetConversationOfflinePushUserIDs(c *gin.Context) { - a2r.Call(c, conversation.ConversationClient.GetConversationOfflinePushUserIDs, o.Client) -} +//func (o *ConversationApi) GetConversationOfflinePushUserIDs(c *gin.Context) { +// a2r.Call(c, conversation.ConversationClient.GetConversationOfflinePushUserIDs, o.Client) +//} func (o *ConversationApi) GetFullOwnerConversationIDs(c *gin.Context) { a2r.Call(c, conversation.ConversationClient.GetFullOwnerConversationIDs, o.Client) diff --git a/internal/api/router.go b/internal/api/router.go index add8ef36b..700d8392e 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -262,7 +262,7 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin conversationGroup.POST("/get_conversation", c.GetConversation) conversationGroup.POST("/get_conversations", c.GetConversations) conversationGroup.POST("/set_conversations", c.SetConversations) - conversationGroup.POST("/get_conversation_offline_push_user_ids", c.GetConversationOfflinePushUserIDs) + //conversationGroup.POST("/get_conversation_offline_push_user_ids", c.GetConversationOfflinePushUserIDs) conversationGroup.POST("/get_full_conversation_ids", c.GetFullOwnerConversationIDs) conversationGroup.POST("/get_incremental_conversations", c.GetIncrementalConversation) conversationGroup.POST("/get_owner_conversation", c.GetOwnerConversation) diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index ca2c58878..b0b1053ed 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -19,6 +19,7 @@ import ( "sort" "time" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/dbbuild" "github.com/openimsdk/open-im-server/v3/pkg/rpcli" @@ -117,6 +118,9 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr } func (c *conversationServer) GetConversation(ctx context.Context, req *pbconversation.GetConversationReq) (*pbconversation.GetConversationResp, error) { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } conversations, err := c.conversationDatabase.FindConversations(ctx, req.OwnerUserID, []string{req.ConversationID}) if err != nil { return nil, err @@ -130,7 +134,9 @@ func (c *conversationServer) GetConversation(ctx context.Context, req *pbconvers } func (c *conversationServer) GetSortedConversationList(ctx context.Context, req *pbconversation.GetSortedConversationListReq) (resp *pbconversation.GetSortedConversationListResp, err error) { - log.ZDebug(ctx, "GetSortedConversationList", "seqs", req, "userID", req.UserID) + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } var conversationIDs []string if len(req.ConversationIDs) == 0 { conversationIDs, err = c.conversationDatabase.GetConversationIDs(ctx, req.UserID) @@ -203,6 +209,9 @@ func (c *conversationServer) GetSortedConversationList(ctx context.Context, req } func (c *conversationServer) GetAllConversations(ctx context.Context, req *pbconversation.GetAllConversationsReq) (*pbconversation.GetAllConversationsResp, error) { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } conversations, err := c.conversationDatabase.GetUserAllConversation(ctx, req.OwnerUserID) if err != nil { return nil, err @@ -213,6 +222,9 @@ func (c *conversationServer) GetAllConversations(ctx context.Context, req *pbcon } func (c *conversationServer) GetConversations(ctx context.Context, req *pbconversation.GetConversationsReq) (*pbconversation.GetConversationsResp, error) { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } conversations, err := c.getConversations(ctx, req.OwnerUserID, req.ConversationIDs) if err != nil { return nil, err @@ -233,6 +245,9 @@ func (c *conversationServer) getConversations(ctx context.Context, ownerUserID s } func (c *conversationServer) SetConversation(ctx context.Context, req *pbconversation.SetConversationReq) (*pbconversation.SetConversationResp, error) { + if err := authverify.CheckAccess(ctx, req.GetConversation().GetUserID()); err != nil { + return nil, err + } var conversation dbModel.Conversation if err := datautil.CopyStructFields(&conversation, req.Conversation); err != nil { return nil, err @@ -247,10 +262,11 @@ func (c *conversationServer) SetConversation(ctx context.Context, req *pbconvers } func (c *conversationServer) SetConversations(ctx context.Context, req *pbconversation.SetConversationsReq) (*pbconversation.SetConversationsResp, error) { - if req.Conversation == nil { - return nil, errs.ErrArgs.WrapMsg("conversation must not be nil") + for _, userID := range req.UserIDs { + if err := authverify.CheckAccess(ctx, userID); err != nil { + return nil, err + } } - if req.Conversation.ConversationType == constant.WriteGroupChatType { groupInfo, err := c.groupClient.GetGroupInfo(ctx, req.Conversation.GroupID) if err != nil { @@ -331,6 +347,9 @@ func (c *conversationServer) SetConversations(ctx context.Context, req *pbconver } func (c *conversationServer) UpdateConversationsByUser(ctx context.Context, req *pbconversation.UpdateConversationsByUserReq) (*pbconversation.UpdateConversationsByUserResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } m := make(map[string]any) if req.Ex != nil { m["ex"] = req.Ex.Value @@ -343,15 +362,8 @@ func (c *conversationServer) UpdateConversationsByUser(ctx context.Context, req return &pbconversation.UpdateConversationsByUserResp{}, nil } -// Get user IDs with "Do Not Disturb" enabled in super large groups. -func (c *conversationServer) GetRecvMsgNotNotifyUserIDs(ctx context.Context, req *pbconversation.GetRecvMsgNotNotifyUserIDsReq) (*pbconversation.GetRecvMsgNotNotifyUserIDsResp, error) { - return nil, errs.New("deprecated") -} - // create conversation without notification for msg redis transfer. -func (c *conversationServer) CreateSingleChatConversations(ctx context.Context, - req *pbconversation.CreateSingleChatConversationsReq, -) (*pbconversation.CreateSingleChatConversationsResp, error) { +func (c *conversationServer) CreateSingleChatConversations(ctx context.Context, req *pbconversation.CreateSingleChatConversationsReq) (*pbconversation.CreateSingleChatConversationsResp, error) { var conversation dbModel.Conversation switch req.ConversationType { case constant.SingleChatType: @@ -454,6 +466,9 @@ func (c *conversationServer) SetConversationMinSeq(ctx context.Context, req *pbc } func (c *conversationServer) GetConversationIDs(ctx context.Context, req *pbconversation.GetConversationIDsReq) (*pbconversation.GetConversationIDsResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } conversationIDs, err := c.conversationDatabase.GetConversationIDs(ctx, req.UserID) if err != nil { return nil, err @@ -462,6 +477,9 @@ func (c *conversationServer) GetConversationIDs(ctx context.Context, req *pbconv } func (c *conversationServer) GetUserConversationIDsHash(ctx context.Context, req *pbconversation.GetUserConversationIDsHashReq) (*pbconversation.GetUserConversationIDsHashResp, error) { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } hash, err := c.conversationDatabase.GetUserConversationIDsHash(ctx, req.OwnerUserID) if err != nil { return nil, err @@ -469,10 +487,7 @@ func (c *conversationServer) GetUserConversationIDsHash(ctx context.Context, req return &pbconversation.GetUserConversationIDsHashResp{Hash: hash}, nil } -func (c *conversationServer) GetConversationsByConversationID( - ctx context.Context, - req *pbconversation.GetConversationsByConversationIDReq, -) (*pbconversation.GetConversationsByConversationIDResp, error) { +func (c *conversationServer) GetConversationsByConversationID(ctx context.Context, req *pbconversation.GetConversationsByConversationIDReq) (*pbconversation.GetConversationsByConversationIDResp, error) { conversations, err := c.conversationDatabase.GetConversationsByConversationID(ctx, req.ConversationIDs) if err != nil { return nil, err @@ -526,10 +541,7 @@ func (c *conversationServer) conversationSort(conversations map[int64]string, re resp.ConversationElems = append(resp.ConversationElems, cons...) } -func (c *conversationServer) getConversationInfo( - ctx context.Context, - chatLogs map[string]*sdkws.MsgData, - userID string) (map[string]*pbconversation.ConversationElem, error) { +func (c *conversationServer) getConversationInfo(ctx context.Context, chatLogs map[string]*sdkws.MsgData, userID string) (map[string]*pbconversation.ConversationElem, error) { var ( sendIDs []string groupIDs []string @@ -615,6 +627,11 @@ func (c *conversationServer) GetConversationNotReceiveMessageUserIDs(ctx context } func (c *conversationServer) UpdateConversation(ctx context.Context, req *pbconversation.UpdateConversationReq) (*pbconversation.UpdateConversationResp, error) { + for _, userID := range req.UserIDs { + if err := authverify.CheckAccess(ctx, userID); err != nil { + return nil, err + } + } m := make(map[string]any) if req.RecvMsgOpt != nil { m["recv_msg_opt"] = req.RecvMsgOpt.Value @@ -661,6 +678,9 @@ func (c *conversationServer) UpdateConversation(ctx context.Context, req *pbconv } func (c *conversationServer) GetOwnerConversation(ctx context.Context, req *pbconversation.GetOwnerConversationReq) (*pbconversation.GetOwnerConversationResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } total, conversations, err := c.conversationDatabase.GetOwnerConversation(ctx, req.UserID, req.Pagination) if err != nil { return nil, err @@ -722,6 +742,9 @@ func (c *conversationServer) GetConversationsNeedClearMsg(ctx context.Context, _ } func (c *conversationServer) GetNotNotifyConversationIDs(ctx context.Context, req *pbconversation.GetNotNotifyConversationIDsReq) (*pbconversation.GetNotNotifyConversationIDsResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } conversationIDs, err := c.conversationDatabase.GetNotNotifyConversationIDs(ctx, req.UserID) if err != nil { return nil, err @@ -730,6 +753,9 @@ func (c *conversationServer) GetNotNotifyConversationIDs(ctx context.Context, re } func (c *conversationServer) GetPinnedConversationIDs(ctx context.Context, req *pbconversation.GetPinnedConversationIDsReq) (*pbconversation.GetPinnedConversationIDsResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } conversationIDs, err := c.conversationDatabase.GetPinnedConversationIDs(ctx, req.UserID) if err != nil { return nil, err diff --git a/internal/rpc/conversation/sync.go b/internal/rpc/conversation/sync.go index cee74b319..a24dd85c6 100644 --- a/internal/rpc/conversation/sync.go +++ b/internal/rpc/conversation/sync.go @@ -35,6 +35,9 @@ func (c *conversationServer) GetFullOwnerConversationIDs(ctx context.Context, re } func (c *conversationServer) GetIncrementalConversation(ctx context.Context, req *conversation.GetIncrementalConversationReq) (*conversation.GetIncrementalConversationResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } opt := incrversion.Option[*conversation.Conversation, conversation.GetIncrementalConversationResp]{ Ctx: ctx, VersionKey: req.UserID, diff --git a/internal/rpc/group/cache.go b/internal/rpc/group/cache.go index bcd246267..ec0e5b566 100644 --- a/internal/rpc/group/cache.go +++ b/internal/rpc/group/cache.go @@ -17,6 +17,7 @@ package group import ( "context" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/common/convert" pbgroup "github.com/openimsdk/protocol/group" ) @@ -33,6 +34,9 @@ func (g *groupServer) GetGroupInfoCache(ctx context.Context, req *pbgroup.GetGro } func (g *groupServer) GetGroupMemberCache(ctx context.Context, req *pbgroup.GetGroupMemberCacheReq) (*pbgroup.GetGroupMemberCacheResp, error) { + if err := authverify.CheckAccess(ctx, req.GroupMemberID); err != nil { + return nil, err + } members, err := g.db.TakeGroupMember(ctx, req.GroupID, req.GroupMemberID) if err != nil { return nil, err diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 10cdc2546..2026ba71b 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -476,6 +476,19 @@ func (g *groupServer) GetGroupAllMember(ctx context.Context, req *pbgroup.GetGro if err != nil { return nil, err } + if !authverify.IsAdmin(ctx) { + var inGroup bool + opUserID := mcontext.GetOpUserID(ctx) + for _, member := range members { + if member.UserID == opUserID { + inGroup = true + break + } + } + if !inGroup { + return nil, errs.ErrNoPermission.WrapMsg("opuser not in group") + } + } if err := g.PopulateGroupMember(ctx, members...); err != nil { return nil, err } @@ -486,11 +499,24 @@ func (g *groupServer) GetGroupAllMember(ctx context.Context, req *pbgroup.GetGro return &resp, nil } +func (g *groupServer) checkAdminOrInGroup(ctx context.Context, groupID string) error { + if authverify.IsAdmin(ctx) { + return nil + } + opUserID := mcontext.GetOpUserID(ctx) + members, err := g.db.FindGroupMembers(ctx, groupID, []string{opUserID}) + if err != nil { + return err + } + if len(members) == 0 { + return errs.ErrNoPermission.WrapMsg("op user not in group") + } + return nil +} + func (g *groupServer) GetGroupMemberList(ctx context.Context, req *pbgroup.GetGroupMemberListReq) (*pbgroup.GetGroupMemberListResp, error) { - if opUserID := mcontext.GetOpUserID(ctx); !datautil.Contain(opUserID, g.config.Share.IMAdminUserID...) { - if _, err := g.db.TakeGroupMember(ctx, req.GroupID, opUserID); err != nil { - return nil, err - } + if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil { + return nil, err } var ( total int64 @@ -631,6 +657,9 @@ func (g *groupServer) GetGroupMembersInfo(ctx context.Context, req *pbgroup.GetG if req.GroupID == "" { return nil, errs.ErrArgs.WrapMsg("groupID empty") } + if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil { + return nil, err + } members, err := g.getGroupMembersInfo(ctx, req.GroupID, req.UserIDs) if err != nil { return nil, err @@ -658,6 +687,9 @@ func (g *groupServer) getGroupMembersInfo(ctx context.Context, groupID string, u // GetGroupApplicationList handles functions that get a list of group requests. func (g *groupServer) GetGroupApplicationList(ctx context.Context, req *pbgroup.GetGroupApplicationListReq) (*pbgroup.GetGroupApplicationListResp, error) { + if err := authverify.CheckAccess(ctx, req.FromUserID); err != nil { + return nil, err + } groupIDs, err := g.db.FindUserManagedGroupID(ctx, req.FromUserID) if err != nil { return nil, err @@ -1652,6 +1684,11 @@ func (g *groupServer) GetGroupAbstractInfo(ctx context.Context, req *pbgroup.Get if datautil.Duplicate(req.GroupIDs) { return nil, errs.ErrArgs.WrapMsg("groupIDs duplicate") } + for _, groupID := range req.GroupIDs { + if err := g.checkAdminOrInGroup(ctx, groupID); err != nil { + return nil, err + } + } groups, err := g.db.FindGroup(ctx, req.GroupIDs) if err != nil { return nil, err @@ -1699,6 +1736,9 @@ func (g *groupServer) GetGroupMemberUserIDs(ctx context.Context, req *pbgroup.Ge if err != nil { return nil, err } + if err := authverify.CheckAccessIn(ctx, userIDs...); err != nil { + return nil, err + } return &pbgroup.GetGroupMemberUserIDsResp{ UserIDs: userIDs, }, nil diff --git a/internal/rpc/group/statistics.go b/internal/rpc/group/statistics.go index 1c582fda1..4ee3396da 100644 --- a/internal/rpc/group/statistics.go +++ b/internal/rpc/group/statistics.go @@ -18,6 +18,7 @@ import ( "context" "time" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/protocol/group" "github.com/openimsdk/tools/errs" ) @@ -26,6 +27,9 @@ func (g *groupServer) GroupCreateCount(ctx context.Context, req *group.GroupCrea if req.Start > req.End { return nil, errs.ErrArgs.WrapMsg("start > end: %d > %d", req.Start, req.End) } + if err := authverify.CheckAdmin(ctx); err != nil { + return nil, err + } total, err := g.db.CountTotal(ctx, nil) if err != nil { return nil, err diff --git a/internal/rpc/group/sync.go b/internal/rpc/group/sync.go index 822b15307..baee2f2d4 100644 --- a/internal/rpc/group/sync.go +++ b/internal/rpc/group/sync.go @@ -11,9 +11,6 @@ import ( "github.com/openimsdk/protocol/constant" pbgroup "github.com/openimsdk/protocol/group" "github.com/openimsdk/protocol/sdkws" - "github.com/openimsdk/tools/errs" - "github.com/openimsdk/tools/mcontext" - "github.com/openimsdk/tools/utils/datautil" ) const versionSyncLimit = 500 @@ -23,10 +20,8 @@ func (g *groupServer) GetFullGroupMemberUserIDs(ctx context.Context, req *pbgrou if err != nil { return nil, err } - if opUserID := mcontext.GetOpUserID(ctx); !datautil.Contain(opUserID, g.config.Share.IMAdminUserID...) { - if !datautil.Contain(opUserID, userIDs...) { - return nil, errs.ErrNoPermission.WrapMsg("user not in group") - } + if err := authverify.CheckAccessIn(ctx, userIDs...); err != nil { + return nil, err } vl, err := g.db.FindMaxGroupMemberVersionCache(ctx, req.GroupID) if err != nil { @@ -69,6 +64,9 @@ func (g *groupServer) GetFullJoinGroupIDs(ctx context.Context, req *pbgroup.GetF } func (g *groupServer) GetIncrementalGroupMember(ctx context.Context, req *pbgroup.GetIncrementalGroupMemberReq) (*pbgroup.GetIncrementalGroupMemberResp, error) { + if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil { + return nil, err + } group, err := g.db.TakeGroup(ctx, req.GroupID) if err != nil { return nil, err @@ -76,9 +74,6 @@ func (g *groupServer) GetIncrementalGroupMember(ctx context.Context, req *pbgrou if group.Status == constant.GroupStatusDismissed { return nil, servererrs.ErrDismissedAlready.Wrap() } - if _, err := g.db.TakeGroupMember(ctx, req.GroupID, mcontext.GetOpUserID(ctx)); err != nil { - return nil, err - } var ( hasGroupUpdate bool sortVersion uint64 diff --git a/internal/rpc/msg/as_read.go b/internal/rpc/msg/as_read.go index b25eae6b1..c52ce9c07 100644 --- a/internal/rpc/msg/as_read.go +++ b/internal/rpc/msg/as_read.go @@ -18,6 +18,7 @@ import ( "context" "errors" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" cbapi "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" "github.com/openimsdk/protocol/constant" "github.com/openimsdk/protocol/msg" @@ -29,6 +30,9 @@ import ( ) func (m *msgServer) GetConversationsHasReadAndMaxSeq(ctx context.Context, req *msg.GetConversationsHasReadAndMaxSeqReq) (*msg.GetConversationsHasReadAndMaxSeqResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } var conversationIDs []string if len(req.ConversationIDs) == 0 { var err error @@ -82,6 +86,9 @@ func (m *msgServer) GetConversationsHasReadAndMaxSeq(ctx context.Context, req *m } func (m *msgServer) SetConversationHasReadSeq(ctx context.Context, req *msg.SetConversationHasReadSeqReq) (*msg.SetConversationHasReadSeqResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } maxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID) if err != nil { return nil, err @@ -97,8 +104,8 @@ func (m *msgServer) SetConversationHasReadSeq(ctx context.Context, req *msg.SetC } func (m *msgServer) MarkMsgsAsRead(ctx context.Context, req *msg.MarkMsgsAsReadReq) (*msg.MarkMsgsAsReadResp, error) { - if len(req.Seqs) < 1 { - return nil, errs.ErrArgs.WrapMsg("seqs must not be empty") + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err } maxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID) if err != nil { @@ -139,6 +146,9 @@ func (m *msgServer) MarkMsgsAsRead(ctx context.Context, req *msg.MarkMsgsAsReadR } func (m *msgServer) MarkConversationAsRead(ctx context.Context, req *msg.MarkConversationAsReadReq) (*msg.MarkConversationAsReadResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } conversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, req.ConversationID) if err != nil { return nil, err @@ -216,5 +226,4 @@ func (m *msgServer) sendMarkAsReadNotification(ctx context.Context, conversation HasReadSeq: hasReadSeq, } m.notificationSender.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips) - } diff --git a/internal/rpc/msg/delete.go b/internal/rpc/msg/delete.go index 4590523d5..d24420ebb 100644 --- a/internal/rpc/msg/delete.go +++ b/internal/rpc/msg/delete.go @@ -94,6 +94,9 @@ func (m *msgServer) DeleteMsgs(ctx context.Context, req *msg.DeleteMsgsReq) (*ms } func (m *msgServer) DeleteMsgPhysicalBySeq(ctx context.Context, req *msg.DeleteMsgPhysicalBySeqReq) (*msg.DeleteMsgPhysicalBySeqResp, error) { + if err := authverify.CheckAdmin(ctx); err != nil { + return nil, err + } err := m.MsgDatabase.DeleteMsgsPhysicalBySeqs(ctx, req.ConversationID, req.Seqs) if err != nil { return nil, err diff --git a/internal/rpc/msg/send.go b/internal/rpc/msg/send.go index 6b2ec30b5..0e3a9950b 100644 --- a/internal/rpc/msg/send.go +++ b/internal/rpc/msg/send.go @@ -17,6 +17,7 @@ package msg import ( "context" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "google.golang.org/protobuf/proto" "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" @@ -37,6 +38,9 @@ func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg. if req.MsgData == nil { return nil, errs.ErrArgs.WrapMsg("msgData is nil") } + if err := authverify.CheckAccess(ctx, req.MsgData.SendID); err != nil { + return nil, err + } before := new(*sdkws.MsgData) resp, err := m.sendMsg(ctx, req, before) if err != nil { @@ -172,13 +176,7 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq isSend := true isNotification := msgprocessor.IsNotificationByMsg(req.MsgData) if !isNotification { - isSend, err = m.modifyMessageByUserMessageReceiveOpt( - ctx, - req.MsgData.RecvID, - conversationutil.GenConversationIDForSingle(req.MsgData.SendID, req.MsgData.RecvID), - constant.SingleChatType, - req, - ) + isSend, err = m.modifyMessageByUserMessageReceiveOpt(authverify.WithTempAdmin(ctx), req.MsgData.RecvID, conversationutil.GenConversationIDForSingle(req.MsgData.SendID, req.MsgData.RecvID), constant.SingleChatType, req) if err != nil { return nil, err } diff --git a/internal/rpc/msg/seq.go b/internal/rpc/msg/seq.go index bd68138fb..6a0461dc8 100644 --- a/internal/rpc/msg/seq.go +++ b/internal/rpc/msg/seq.go @@ -17,9 +17,10 @@ package msg import ( "context" "errors" + "sort" + pbmsg "github.com/openimsdk/protocol/msg" "github.com/redis/go-redis/v9" - "sort" ) func (m *msgServer) GetConversationMaxSeq(ctx context.Context, req *pbmsg.GetConversationMaxSeqReq) (*pbmsg.GetConversationMaxSeqResp, error) { diff --git a/internal/rpc/msg/statistics.go b/internal/rpc/msg/statistics.go index 01c0f1c46..b1f90cae4 100644 --- a/internal/rpc/msg/statistics.go +++ b/internal/rpc/msg/statistics.go @@ -16,15 +16,20 @@ package msg import ( "context" - "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" "time" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/sdkws" "github.com/openimsdk/tools/utils/datautil" ) func (m *msgServer) GetActiveUser(ctx context.Context, req *msg.GetActiveUserReq) (*msg.GetActiveUserResp, error) { + if err := authverify.CheckAdmin(ctx); err != nil { + return nil, err + } msgCount, userCount, users, dateCount, err := m.MsgDatabase.RangeUserSendCount(ctx, time.UnixMilli(req.Start), time.UnixMilli(req.End), req.Group, req.Ase, req.Pagination.PageNumber, req.Pagination.ShowNumber) if err != nil { return nil, err @@ -60,6 +65,9 @@ func (m *msgServer) GetActiveUser(ctx context.Context, req *msg.GetActiveUserReq } func (m *msgServer) GetActiveGroup(ctx context.Context, req *msg.GetActiveGroupReq) (*msg.GetActiveGroupResp, error) { + if err := authverify.CheckAdmin(ctx); err != nil { + return nil, err + } msgCount, groupCount, groups, dateCount, err := m.MsgDatabase.RangeGroupSendCount(ctx, time.UnixMilli(req.Start), time.UnixMilli(req.End), req.Ase, req.Pagination.PageNumber, req.Pagination.ShowNumber) if err != nil { return nil, err diff --git a/internal/rpc/msg/sync_msg.go b/internal/rpc/msg/sync_msg.go index 38eed93bc..259c7f85d 100644 --- a/internal/rpc/msg/sync_msg.go +++ b/internal/rpc/msg/sync_msg.go @@ -29,6 +29,9 @@ import ( ) func (m *msgServer) PullMessageBySeqs(ctx context.Context, req *sdkws.PullMessageBySeqsReq) (*sdkws.PullMessageBySeqsResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } resp := &sdkws.PullMessageBySeqsResp{} resp.Msgs = make(map[string]*sdkws.PullMsgs) resp.NotificationMsgs = make(map[string]*sdkws.PullMsgs) diff --git a/internal/rpc/relation/black.go b/internal/rpc/relation/black.go index 4a0e60ecd..07a1537b1 100644 --- a/internal/rpc/relation/black.go +++ b/internal/rpc/relation/black.go @@ -46,6 +46,9 @@ func (s *friendServer) GetPaginationBlacks(ctx context.Context, req *relation.Ge } func (s *friendServer) IsBlack(ctx context.Context, req *relation.IsBlackReq) (*relation.IsBlackResp, error) { + if err := authverify.CheckAccessIn(ctx, req.UserID1, req.UserID2); err != nil { + return nil, err + } in1, in2, err := s.blackDatabase.CheckIn(ctx, req.UserID1, req.UserID2) if err != nil { return nil, err diff --git a/internal/rpc/relation/friend.go b/internal/rpc/relation/friend.go index 050cd5ffe..06661c79d 100644 --- a/internal/rpc/relation/friend.go +++ b/internal/rpc/relation/friend.go @@ -280,6 +280,9 @@ func (s *friendServer) SetFriendRemark(ctx context.Context, req *relation.SetFri } func (s *friendServer) GetFriendInfo(ctx context.Context, req *relation.GetFriendInfoReq) (*relation.GetFriendInfoResp, error) { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } friends, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.FriendUserIDs) if err != nil { return nil, err @@ -288,6 +291,9 @@ func (s *friendServer) GetFriendInfo(ctx context.Context, req *relation.GetFrien } func (s *friendServer) GetDesignatedFriends(ctx context.Context, req *relation.GetDesignatedFriendsReq) (resp *relation.GetDesignatedFriendsResp, err error) { + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } resp = &relation.GetDesignatedFriendsResp{} if datautil.Duplicate(req.FriendUserIDs) { return nil, errs.ErrArgs.WrapMsg("friend userID repeated") @@ -313,9 +319,10 @@ func (s *friendServer) getFriend(ctx context.Context, ownerUserID string, friend } // Get the list of friend requests sent out proactively. -func (s *friendServer) GetDesignatedFriendsApply(ctx context.Context, - req *relation.GetDesignatedFriendsApplyReq, -) (resp *relation.GetDesignatedFriendsApplyResp, err error) { +func (s *friendServer) GetDesignatedFriendsApply(ctx context.Context, req *relation.GetDesignatedFriendsApplyReq) (resp *relation.GetDesignatedFriendsApplyResp, err error) { + if err := authverify.CheckAccessIn(ctx, req.FromUserID, req.ToUserID); err != nil { + return nil, err + } friendRequests, err := s.db.FindBothFriendRequests(ctx, req.FromUserID, req.ToUserID) if err != nil { return nil, err @@ -374,6 +381,9 @@ func (s *friendServer) GetPaginationFriendsApplyFrom(ctx context.Context, req *r // ok. func (s *friendServer) IsFriend(ctx context.Context, req *relation.IsFriendReq) (resp *relation.IsFriendResp, err error) { + if err := authverify.CheckAccessIn(ctx, req.UserID1, req.UserID2); err != nil { + return nil, err + } resp = &relation.IsFriendResp{} resp.InUser1Friends, resp.InUser2Friends, err = s.db.CheckIn(ctx, req.UserID1, req.UserID2) if err != nil { @@ -426,6 +436,9 @@ func (s *friendServer) GetSpecifiedFriendsInfo(ctx context.Context, req *relatio return nil, errs.ErrArgs.WrapMsg("userIDList repeated") } + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } userMap, err := s.userClient.GetUsersInfoMap(ctx, req.UserIDList) if err != nil { return nil, err @@ -494,10 +507,7 @@ func (s *friendServer) GetSpecifiedFriendsInfo(ctx context.Context, req *relatio return resp, nil } -func (s *friendServer) UpdateFriends( - ctx context.Context, - req *relation.UpdateFriendsReq, -) (*relation.UpdateFriendsResp, error) { +func (s *friendServer) UpdateFriends(ctx context.Context, req *relation.UpdateFriendsReq) (*relation.UpdateFriendsResp, error) { if len(req.FriendUserIDs) == 0 { return nil, errs.ErrArgs.WrapMsg("friendIDList is empty") } @@ -505,6 +515,10 @@ func (s *friendServer) UpdateFriends( return nil, errs.ErrArgs.WrapMsg("friendIDList repeated") } + if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil { + return nil, err + } + _, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.FriendUserIDs) if err != nil { return nil, err diff --git a/internal/rpc/third/log.go b/internal/rpc/third/log.go index fba3ecb88..9a4995ace 100644 --- a/internal/rpc/third/log.go +++ b/internal/rpc/third/log.go @@ -25,6 +25,7 @@ import ( "github.com/openimsdk/protocol/constant" "github.com/openimsdk/protocol/third" "github.com/openimsdk/tools/errs" + "github.com/openimsdk/tools/mcontext" "github.com/openimsdk/tools/utils/datautil" ) @@ -45,7 +46,7 @@ func genLogID() string { func (t *thirdServer) UploadLogs(ctx context.Context, req *third.UploadLogsReq) (*third.UploadLogsResp, error) { var dbLogs []*relationtb.Log - userID := ctx.Value(constant.OpUserID).(string) + userID := mcontext.GetOpUserID(ctx) platform := constant.PlatformID2Name[int(req.Platform)] for _, fileURL := range req.FileURLs { log := relationtb.Log{ diff --git a/internal/rpc/third/third.go b/internal/rpc/third/third.go index 0afd54014..c6dcb2ea4 100644 --- a/internal/rpc/third/third.go +++ b/internal/rpc/third/third.go @@ -19,6 +19,7 @@ import ( "fmt" "time" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache" "github.com/openimsdk/open-im-server/v3/pkg/dbbuild" @@ -148,6 +149,9 @@ func (t *thirdServer) FcmUpdateToken(ctx context.Context, req *third.FcmUpdateTo } func (t *thirdServer) SetAppBadge(ctx context.Context, req *third.SetAppBadgeReq) (resp *third.SetAppBadgeResp, err error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } err = t.thirdDatabase.SetAppBadge(ctx, req.UserID, int(req.AppUnreadCount)) if err != nil { return nil, err diff --git a/pkg/authverify/token.go b/pkg/authverify/token.go index 2e3639776..a82eba30a 100644 --- a/pkg/authverify/token.go +++ b/pkg/authverify/token.go @@ -64,16 +64,57 @@ func GetIMAdminUserIDs(ctx context.Context) []string { } func IsAdmin(ctx context.Context) bool { - return datautil.Contain(mcontext.GetOpUserID(ctx), GetIMAdminUserIDs(ctx)...) + return IsTempAdmin(ctx) || IsSystemAdmin(ctx) } func CheckAccess(ctx context.Context, ownerUserID string) error { - opUserID := mcontext.GetOpUserID(ctx) - if opUserID == ownerUserID { + if mcontext.GetOpUserID(ctx) == ownerUserID { return nil } - if datautil.Contain(mcontext.GetOpUserID(ctx), GetIMAdminUserIDs(ctx)...) { + if IsAdmin(ctx) { return nil } return servererrs.ErrNoPermission.WrapMsg("ownerUserID", ownerUserID) } + +func CheckAccessIn(ctx context.Context, ownerUserIDs ...string) error { + opUserID := mcontext.GetOpUserID(ctx) + for _, userID := range ownerUserIDs { + if opUserID == userID { + return nil + } + } + if IsAdmin(ctx) { + return nil + } + return servererrs.ErrNoPermission.WrapMsg("opUser in ownerUserIDs") +} + +var tempAdminValue = []string{"1"} + +const ctxTempAdminKey = "ctxImTempAdminKey" + +func WithTempAdmin(ctx context.Context) context.Context { + keys, _ := ctx.Value(constant.RpcCustomHeader).([]string) + if datautil.Contain(ctxTempAdminKey, keys...) { + return ctx + } + if len(keys) > 0 { + temp := make([]string, 0, len(keys)+1) + temp = append(temp, keys...) + keys = append(temp, ctxTempAdminKey) + } else { + keys = []string{ctxTempAdminKey} + } + ctx = context.WithValue(ctx, constant.RpcCustomHeader, keys) + return context.WithValue(ctx, ctxTempAdminKey, tempAdminValue) +} + +func IsTempAdmin(ctx context.Context) bool { + values, _ := ctx.Value(ctxTempAdminKey).([]string) + return datautil.Equal(tempAdminValue, values) +} + +func IsSystemAdmin(ctx context.Context) bool { + return datautil.Contain(mcontext.GetOpUserID(ctx), GetIMAdminUserIDs(ctx)...) +} diff --git a/pkg/tools/batcher/batcher.go b/pkg/tools/batcher/batcher.go index dcf5d07ad..93a31ed8f 100644 --- a/pkg/tools/batcher/batcher.go +++ b/pkg/tools/batcher/batcher.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/utils/idutil" ) @@ -253,13 +254,14 @@ func (b *Batcher[T]) distributeMessage(messages map[string][]*T, totalCount int, func (b *Batcher[T]) run(channelID int, ch <-chan *Msg[T]) { defer b.wait.Done() + ctx := authverify.WithTempAdmin(context.Background()) for { select { case messages, ok := <-ch: if !ok { return } - b.Do(context.Background(), channelID, messages) + b.Do(ctx, channelID, messages) if b.config.syncWait { b.counter.Done() } From 8b23d4f5bb3a80e1e636495b80ee0db856f1d31a Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Thu, 22 May 2025 14:21:44 +0800 Subject: [PATCH 36/59] fix: solve user not found when notification invitedUserID is zero in InviteUserToGroup. (#3375) --- internal/rpc/group/group.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 2026ba71b..5dbba1146 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -379,9 +379,9 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite } var groupMember *model.GroupMember - var opUserID string + opUserID := mcontext.GetOpUserID(ctx) + if !authverify.IsAdmin(ctx) { - opUserID = mcontext.GetOpUserID(ctx) var err error groupMember, err = g.db.TakeGroupMember(ctx, req.GroupID, opUserID) if err != nil { @@ -390,8 +390,6 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite if err := g.PopulateGroupMember(ctx, groupMember); err != nil { return nil, err } - } else { - opUserID = mcontext.GetOpUserID(ctx) } if err := g.webhookBeforeInviteUserToGroup(ctx, &g.config.WebhooksConfig.BeforeInviteUserToGroup, req); err != nil && err != servererrs.ErrCallbackContinue { @@ -450,10 +448,7 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite const singleQuantity = 50 for start := 0; start < len(groupMembers); start += singleQuantity { - end := start + singleQuantity - if end > len(groupMembers) { - end = len(groupMembers) - } + end := min(start+singleQuantity, len(groupMembers)) currentMembers := groupMembers[start:end] if err := g.db.CreateGroup(ctx, nil, currentMembers); err != nil { @@ -464,8 +459,8 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite return e.UserID }) - if err = g.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, req.SendMessage, opUserID, userIDs...); err != nil { - return nil, err + if len(userIDs) != 0 { + g.notification.GroupApplicationAgreeMemberEnterNotification(ctx, req.GroupID, req.SendMessage, opUserID, userIDs...) } } return &pbgroup.InviteUserToGroupResp{}, nil From 4d69194f62f86bb73d7ff5e563c4891f340d3d1f Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Thu, 22 May 2025 14:23:43 +0800 Subject: [PATCH 37/59] fix: send simple msg (#3362) * fix: content in sendSimpleMessage * fix: send simple msg * fix: send simple msg --- internal/api/msg.go | 24 +++++++++++++++++++----- internal/api/router.go | 1 + internal/rpc/msg/send.go | 22 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/internal/api/msg.go b/internal/api/msg.go index 5349faf87..d832bfdcd 100644 --- a/internal/api/msg.go +++ b/internal/api/msg.go @@ -467,6 +467,10 @@ func (m *MessageApi) SendSimpleMessage(c *gin.Context) { sessionType int32 recvID string ) + if err = c.BindJSON(&req); err != nil { + apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) + return + } err = json.Unmarshal(decodedData, &keyMsgData) if err != nil { apiresp.GinError(c, errs.ErrArgs.WithDetail(err.Error()).Wrap()) @@ -490,6 +494,11 @@ func (m *MessageApi) SendSimpleMessage(c *gin.Context) { return } + content, err := jsonutil.JsonMarshal(apistruct.MarkdownTextElem{Content: req.Content}) + if err != nil { + apiresp.GinError(c, errs.Wrap(err)) + return + } msgData := &sdkws.MsgData{ SendID: sendID, RecvID: recvID, @@ -498,17 +507,17 @@ func (m *MessageApi) SendSimpleMessage(c *gin.Context) { SenderPlatformID: constant.AdminPlatformID, SessionType: sessionType, MsgFrom: constant.UserMsgType, - ContentType: constant.Text, - Content: []byte(req.Content), + ContentType: constant.MarkdownText, + Content: content, OfflinePushInfo: req.OfflinePushInfo, Ex: req.Ex, } - sendReq := &msg.SendMsgReq{ + sendReq := &msg.SendSimpleMsgReq{ MsgData: msgData, } - respPb, err := m.Client.SendMsg(c, sendReq) + respPb, err := m.Client.SendSimpleMsg(c, sendReq) if err != nil { apiresp.GinError(c, err) return @@ -525,7 +534,12 @@ func (m *MessageApi) SendSimpleMessage(c *gin.Context) { return } - m.ginRespSendMsg(c, sendReq, respPb) + m.ginRespSendMsg(c, &msg.SendMsgReq{MsgData: sendReq.MsgData}, &msg.SendMsgResp{ + ServerMsgID: respPb.ServerMsgID, + ClientMsgID: respPb.ClientMsgID, + SendTime: respPb.SendTime, + Modify: respPb.Modify, + }) } func (m *MessageApi) CheckMsgIsSendSuccess(c *gin.Context) { diff --git a/internal/api/router.go b/internal/api/router.go index 700d8392e..476aafa48 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -250,6 +250,7 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin msgGroup.POST("/delete_msg_physical", m.DeleteMsgPhysical) msgGroup.POST("/batch_send_msg", m.BatchSendMsg) + msgGroup.POST("/send_simple_msg", m.SendSimpleMessage) msgGroup.POST("/check_msg_is_send_success", m.CheckMsgIsSendSuccess) msgGroup.POST("/get_server_time", m.GetServerTime) } diff --git a/internal/rpc/msg/send.go b/internal/rpc/msg/send.go index 0e3a9950b..f13b80708 100644 --- a/internal/rpc/msg/send.go +++ b/internal/rpc/msg/send.go @@ -201,3 +201,25 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq }, nil } } + +func (m *msgServer) SendSimpleMsg(ctx context.Context, req *pbmsg.SendSimpleMsgReq) (*pbmsg.SendSimpleMsgResp, error) { + if req.MsgData == nil { + return nil, errs.ErrArgs.WrapMsg("msg data is nil") + } + sender, err := m.UserLocalCache.GetUserInfo(ctx, req.MsgData.SendID) + if err != nil { + return nil, err + } + req.MsgData.SenderFaceURL = sender.FaceURL + req.MsgData.SenderNickname = sender.Nickname + resp, err := m.SendMsg(ctx, &pbmsg.SendMsgReq{MsgData: req.MsgData}) + if err != nil { + return nil, err + } + return &pbmsg.SendSimpleMsgResp{ + ServerMsgID: resp.ServerMsgID, + ClientMsgID: resp.ClientMsgID, + SendTime: resp.SendTime, + Modify: resp.Modify, + }, nil +} From 6ae00dfab98da26b1f59ae3acfef23ab03845009 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Thu, 22 May 2025 14:27:30 +0800 Subject: [PATCH 38/59] fix: solve updateUserInfoEx null pointer. (#3326) --- go.mod | 2 +- go.sum | 4 ++-- internal/rpc/user/user.go | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d35252536..dd4034ca5 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/openimsdk/protocol v0.0.73-alpha.6 + github.com/openimsdk/protocol v0.0.73-alpha.8 github.com/openimsdk/tools v0.0.50-alpha.83 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 diff --git a/go.sum b/go.sum index 35fc3d4c6..8278480c8 100644 --- a/go.sum +++ b/go.sum @@ -347,8 +347,8 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5beV3ZyOsGhY= github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= -github.com/openimsdk/protocol v0.0.73-alpha.6 h1:sna9coWG7HN1zObBPtvG0Ki/vzqHXiB4qKbA5P3w7kc= -github.com/openimsdk/protocol v0.0.73-alpha.6/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= +github.com/openimsdk/protocol v0.0.73-alpha.8 h1:GqksOHXWZSqRQaGYvuVQ4IzA7kFhIXSk7NZk0LGk35A= +github.com/openimsdk/protocol v0.0.73-alpha.8/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= github.com/openimsdk/tools v0.0.50-alpha.83 h1:7c1D40YGqIWUmGfCII5pduETGC/8c2DyS9SQ4LvoplU= github.com/openimsdk/tools v0.0.50-alpha.83/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index 7f082f784..5639baab9 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -197,6 +197,7 @@ func (s *userServer) UpdateUserInfoEx(ctx context.Context, req *pbuser.UpdateUse } s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserInfo.UserID) + //friends, err := s.friendRpcClient.GetFriendIDs(ctx, req.UserInfo.UserID) //if err != nil { // return nil, err @@ -209,6 +210,7 @@ func (s *userServer) UpdateUserInfoEx(ctx context.Context, req *pbuser.UpdateUse //for _, friendID := range friends { // s.friendNotificationSender.FriendInfoUpdatedNotification(ctx, req.UserInfo.UserID, friendID) //} + s.webhookAfterUpdateUserInfoEx(ctx, &s.config.WebhooksConfig.AfterUpdateUserInfoEx, req) if err := s.NotificationUserInfoUpdate(ctx, req.UserInfo.UserID, oldUser); err != nil { return nil, err From 4d3ec5367f6304ae415f80e181464af30a5c8111 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Thu, 22 May 2025 17:14:30 +0800 Subject: [PATCH 39/59] fix: add rpc interface permission check (#3377) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake * fix: in standalone mode, the user online status is wrong * fix: add permission check * fix: add permission check * fix: add rpc interface permission check * fix: CreateGroupChatConversations --- internal/push/push.go | 3 ++- internal/rpc/conversation/conversation.go | 3 +++ internal/rpc/group/cache.go | 3 +-- internal/rpc/group/group.go | 9 +++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/push/push.go b/internal/push/push.go index 13818e93d..f720a52ac 100644 --- a/internal/push/push.go +++ b/internal/push/push.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/openimsdk/open-im-server/v3/internal/push/offlinepush" + "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache" @@ -106,7 +107,7 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr go func() { pushHandler.WaitCache() fn := func(ctx context.Context, key string, value []byte) error { - pushHandler.HandleMs2PsChat(ctx, value) + pushHandler.HandleMs2PsChat(authverify.WithTempAdmin(ctx), value) return nil } consumerCtx := mcontext.SetOperationID(context.Background(), "push_"+strconv.Itoa(int(rand.Uint32()))) diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index b0b1053ed..ba9e7746b 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -432,6 +432,9 @@ func (c *conversationServer) CreateGroupChatConversations(ctx context.Context, r if err != nil { return nil, err } + if err := c.msgClient.SetUserConversationMaxSeq(ctx, conversation.ConversationID, req.UserIDs, 0); err != nil { + return nil, err + } c.webhookAfterCreateGroupChatConversations(ctx, &c.config.WebhooksConfig.AfterCreateGroupChatConversations, &conversation) return &pbconversation.CreateGroupChatConversationsResp{}, nil diff --git a/internal/rpc/group/cache.go b/internal/rpc/group/cache.go index ec0e5b566..27b9eb126 100644 --- a/internal/rpc/group/cache.go +++ b/internal/rpc/group/cache.go @@ -17,7 +17,6 @@ package group import ( "context" - "github.com/openimsdk/open-im-server/v3/pkg/authverify" "github.com/openimsdk/open-im-server/v3/pkg/common/convert" pbgroup "github.com/openimsdk/protocol/group" ) @@ -34,7 +33,7 @@ func (g *groupServer) GetGroupInfoCache(ctx context.Context, req *pbgroup.GetGro } func (g *groupServer) GetGroupMemberCache(ctx context.Context, req *pbgroup.GetGroupMemberCacheReq) (*pbgroup.GetGroupMemberCacheResp, error) { - if err := authverify.CheckAccess(ctx, req.GroupMemberID); err != nil { + if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil { return nil, err } members, err := g.db.TakeGroupMember(ctx, req.GroupID, req.GroupMemberID) diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 5dbba1146..f4a186594 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -1303,6 +1303,9 @@ func (g *groupServer) GetGroups(ctx context.Context, req *pbgroup.GetGroupsReq) } func (g *groupServer) GetGroupMembersCMS(ctx context.Context, req *pbgroup.GetGroupMembersCMSReq) (*pbgroup.GetGroupMembersCMSResp, error) { + if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil { + return nil, err + } total, members, err := g.db.SearchGroupMember(ctx, req.UserName, req.GroupID, req.Pagination) if err != nil { return nil, err @@ -1712,6 +1715,9 @@ func (g *groupServer) GetUserInGroupMembers(ctx context.Context, req *pbgroup.Ge if len(req.GroupIDs) == 0 { return nil, errs.ErrArgs.WrapMsg("groupIDs empty") } + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } members, err := g.db.FindGroupMemberUser(ctx, req.GroupIDs, req.UserID) if err != nil { return nil, err @@ -1743,6 +1749,9 @@ func (g *groupServer) GetGroupMemberRoleLevel(ctx context.Context, req *pbgroup. if len(req.RoleLevels) == 0 { return nil, errs.ErrArgs.WrapMsg("RoleLevels empty") } + if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil { + return nil, err + } members, err := g.db.FindGroupMemberRoleLevels(ctx, req.GroupID, req.RoleLevels) if err != nil { return nil, err From 8e61f30e9c59916671e4a75844dcc0a4dfcd5d8a Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Mon, 26 May 2025 10:06:35 +0800 Subject: [PATCH 40/59] feat: optimize friend and group applications (#3384) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake * fix: in standalone mode, the user online status is wrong * fix: add permission check * fix: add permission check * fix: add rpc interface permission check * fix: CreateGroupChatConversations * feat: optimize friend and group applications * feat: optimize friend and group applications * feat: optimize friend and group applications * feat: optimize friend and group applications --- go.mod | 4 +- go.sum | 8 +- internal/api/friend.go | 4 + internal/api/group.go | 4 + internal/api/router.go | 2 + internal/rpc/group/group.go | 70 ++++++-- internal/rpc/group/notification.go | 70 +++++++- internal/rpc/relation/friend.go | 60 +++++-- internal/rpc/relation/notification.go | 152 +++++++++++------- pkg/common/convert/friend.go | 12 +- pkg/common/storage/controller/friend.go | 21 ++- pkg/common/storage/controller/group.go | 24 +-- pkg/common/storage/database/friend_request.go | 6 +- pkg/common/storage/database/group_request.go | 6 +- .../storage/database/mgo/friend_request.go | 48 ++++-- .../storage/database/mgo/group_request.go | 56 +++++-- 16 files changed, 412 insertions(+), 135 deletions(-) diff --git a/go.mod b/go.mod index dd4034ca5..221e28b72 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/openimsdk/protocol v0.0.73-alpha.8 - github.com/openimsdk/tools v0.0.50-alpha.83 + github.com/openimsdk/protocol v0.0.73-alpha.12 + github.com/openimsdk/tools v0.0.50-alpha.84 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 8278480c8..6298f98c9 100644 --- a/go.sum +++ b/go.sum @@ -347,10 +347,10 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5beV3ZyOsGhY= github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= -github.com/openimsdk/protocol v0.0.73-alpha.8 h1:GqksOHXWZSqRQaGYvuVQ4IzA7kFhIXSk7NZk0LGk35A= -github.com/openimsdk/protocol v0.0.73-alpha.8/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= -github.com/openimsdk/tools v0.0.50-alpha.83 h1:7c1D40YGqIWUmGfCII5pduETGC/8c2DyS9SQ4LvoplU= -github.com/openimsdk/tools v0.0.50-alpha.83/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= +github.com/openimsdk/protocol v0.0.73-alpha.12 h1:2NYawXeHChYUeSme6QJ9pOLh+Empce2WmwEtbP4JvKk= +github.com/openimsdk/protocol v0.0.73-alpha.12/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= +github.com/openimsdk/tools v0.0.50-alpha.84 h1:jN60Ys/0edZjL/TDmm/5VSJFP4pGYRipkWqhILJbq/8= +github.com/openimsdk/tools v0.0.50-alpha.84/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/internal/api/friend.go b/internal/api/friend.go index 7d84ff0dc..0943e8a5d 100644 --- a/internal/api/friend.go +++ b/internal/api/friend.go @@ -114,3 +114,7 @@ func (o *FriendApi) GetIncrementalBlacks(c *gin.Context) { func (o *FriendApi) GetFullFriendUserIDs(c *gin.Context) { a2r.Call(c, relation.FriendClient.GetFullFriendUserIDs, o.Client) } + +func (o *FriendApi) GetSelfUnhandledApplyCount(c *gin.Context) { + a2r.Call(c, relation.FriendClient.GetSelfUnhandledApplyCount, o.Client) +} diff --git a/internal/api/group.go b/internal/api/group.go index 97b6b73f0..926d19a8a 100644 --- a/internal/api/group.go +++ b/internal/api/group.go @@ -165,3 +165,7 @@ func (o *GroupApi) GetFullGroupMemberUserIDs(c *gin.Context) { func (o *GroupApi) GetFullJoinGroupIDs(c *gin.Context) { a2r.Call(c, group.GroupClient.GetFullJoinGroupIDs, o.Client) } + +func (o *GroupApi) GetGroupApplicationUnhandledCount(c *gin.Context) { + a2r.Call(c, group.GroupClient.GetGroupApplicationUnhandledCount, o.Client) +} diff --git a/internal/api/router.go b/internal/api/router.go index 476aafa48..5e5f35a60 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -156,6 +156,7 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin friendRouterGroup.POST("/update_friends", f.UpdateFriends) friendRouterGroup.POST("/get_incremental_friends", f.GetIncrementalFriends) friendRouterGroup.POST("/get_full_friend_user_ids", f.GetFullFriendUserIDs) + friendRouterGroup.POST("/get_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount) } g := NewGroupApi(group.NewGroupClient(groupConn)) @@ -192,6 +193,7 @@ func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin groupRouterGroup.POST("/get_incremental_group_members_batch", g.GetIncrementalGroupMemberBatch) groupRouterGroup.POST("/get_full_group_member_user_ids", g.GetFullGroupMemberUserIDs) groupRouterGroup.POST("/get_full_join_group_ids", g.GetFullJoinGroupIDs) + groupRouterGroup.POST("/get_group_application_unhandled_count", g.GetGroupApplicationUnhandledCount) } // certificate { diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index f4a186594..5219546b7 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -25,7 +25,6 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/dbbuild" "github.com/openimsdk/open-im-server/v3/pkg/rpcli" - "google.golang.org/grpc" "github.com/openimsdk/open-im-server/v3/pkg/authverify" @@ -153,10 +152,14 @@ func (g *groupServer) NotificationUserInfoUpdate(ctx context.Context, req *pbgro func (g *groupServer) CheckGroupAdmin(ctx context.Context, groupID string) error { if !authverify.IsAdmin(ctx) { - groupMember, err := g.db.TakeGroupMember(ctx, groupID, mcontext.GetOpUserID(ctx)) + members, err := g.db.FindGroupMembers(ctx, groupID, []string{mcontext.GetOpUserID(ctx)}) if err != nil { return err } + if len(members) == 0 { + return errs.ErrNoPermission.WrapMsg("op user not in group") + } + groupMember := members[0] if !(groupMember.RoleLevel == constant.GroupOwner || groupMember.RoleLevel == constant.GroupAdmin) { return errs.ErrNoPermission.WrapMsg("no group owner or admin") } @@ -369,6 +372,10 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite return nil, servererrs.ErrDismissedAlready.WrapMsg("group dismissed checking group status found it dismissed") } + if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil { + return nil, err + } + userMap, err := g.userClient.GetUsersInfoMap(ctx, req.InvitedUserIDs) if err != nil { return nil, err @@ -419,7 +426,7 @@ func (g *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.Invite ReqMessage: request.ReqMsg, JoinSource: request.JoinSource, InviterUserID: request.InviterUserID, - }) + }, request) } return &pbgroup.InviteUserToGroupResp{}, nil } @@ -685,15 +692,34 @@ func (g *groupServer) GetGroupApplicationList(ctx context.Context, req *pbgroup. if err := authverify.CheckAccess(ctx, req.FromUserID); err != nil { return nil, err } - groupIDs, err := g.db.FindUserManagedGroupID(ctx, req.FromUserID) - if err != nil { - return nil, err + var ( + groupIDs []string + err error + ) + if len(req.GroupIDs) == 0 { + groupIDs, err = g.db.FindUserManagedGroupID(ctx, req.FromUserID) + if err != nil { + return nil, err + } + } else { + req.GroupIDs = datautil.Distinct(req.GroupIDs) + if !authverify.IsAdmin(ctx) { + for _, groupID := range req.GroupIDs { + if err := g.CheckGroupAdmin(ctx, groupID); err != nil { + return nil, err + } + } + } + groupIDs = req.GroupIDs } resp := &pbgroup.GetGroupApplicationListResp{} if len(groupIDs) == 0 { return resp, nil } - total, groupRequests, err := g.db.PageGroupRequest(ctx, groupIDs, req.Pagination) + handleResults := datautil.Slice(req.HandleResults, func(e int32) int { + return int(e) + }) + total, groupRequests, err := g.db.PageGroupRequest(ctx, groupIDs, handleResults, req.Pagination) if err != nil { return nil, err } @@ -758,6 +784,23 @@ func (g *groupServer) GetGroupsInfo(ctx context.Context, req *pbgroup.GetGroupsI }, nil } +func (g *groupServer) GetGroupApplicationUnhandledCount(ctx context.Context, req *pbgroup.GetGroupApplicationUnhandledCountReq) (*pbgroup.GetGroupApplicationUnhandledCountResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } + groupIDs, err := g.db.FindUserManagedGroupID(ctx, req.UserID) + if err != nil { + return nil, err + } + count, err := g.db.GetGroupApplicationUnhandledCount(ctx, groupIDs, req.Time) + if err != nil { + return nil, err + } + return &pbgroup.GetGroupApplicationUnhandledCountResp{ + Count: count, + }, nil +} + func (g *groupServer) getGroupsInfo(ctx context.Context, groupIDs []string) ([]*sdkws.GroupInfo, error) { if len(groupIDs) == 0 { return nil, nil @@ -939,7 +982,7 @@ func (g *groupServer) JoinGroup(ctx context.Context, req *pbgroup.JoinGroupReq) if err = g.db.CreateGroupRequest(ctx, []*model.GroupRequest{&groupRequest}); err != nil { return nil, err } - g.notification.JoinGroupApplicationNotification(ctx, req) + g.notification.JoinGroupApplicationNotification(ctx, req, &groupRequest) return &pbgroup.JoinGroupResp{}, nil } @@ -1322,11 +1365,17 @@ func (g *groupServer) GetGroupMembersCMS(ctx context.Context, req *pbgroup.GetGr } func (g *groupServer) GetUserReqApplicationList(ctx context.Context, req *pbgroup.GetUserReqApplicationListReq) (*pbgroup.GetUserReqApplicationListResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } user, err := g.userClient.GetUserInfo(ctx, req.UserID) if err != nil { return nil, err } - total, requests, err := g.db.PageGroupRequestUser(ctx, req.UserID, req.Pagination) + handleResults := datautil.Slice(req.HandleResults, func(e int32) int { + return int(e) + }) + total, requests, err := g.db.PageGroupRequestUser(ctx, req.UserID, req.GroupIDs, handleResults, req.Pagination) if err != nil { return nil, err } @@ -1767,6 +1816,9 @@ func (g *groupServer) GetGroupMemberRoleLevel(ctx context.Context, req *pbgroup. } func (g *groupServer) GetGroupUsersReqApplicationList(ctx context.Context, req *pbgroup.GetGroupUsersReqApplicationListReq) (*pbgroup.GetGroupUsersReqApplicationListResp, error) { + if err := g.CheckGroupAdmin(ctx, req.GroupID); err != nil { + return nil, err + } requests, err := g.db.FindGroupRequests(ctx, req.GroupID, req.UserIDs) if err != nil { return nil, err diff --git a/internal/rpc/group/notification.go b/internal/rpc/group/notification.go index 0a18371f9..5a3cbbe26 100644 --- a/internal/rpc/group/notification.go +++ b/internal/rpc/group/notification.go @@ -20,6 +20,7 @@ import ( "fmt" "time" + "github.com/google/uuid" "github.com/openimsdk/open-im-server/v3/pkg/rpcli" "go.mongodb.org/mongo-driver/mongo" @@ -365,13 +366,46 @@ func (g *NotificationSender) GroupInfoSetAnnouncementNotification(ctx context.Co g.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetAnnouncementNotification, tips, notification.WithRpcGetUserName(), notification.WithSendMessage(sendMessage)) } -func (g *NotificationSender) JoinGroupApplicationNotification(ctx context.Context, req *pbgroup.JoinGroupReq) { +func (g *NotificationSender) uuid() string { + return uuid.New().String() +} + +func (g *NotificationSender) getGroupRequest(ctx context.Context, groupID string, userID string) (*sdkws.GroupRequest, error) { + request, err := g.db.TakeGroupRequest(ctx, groupID, userID) + if err != nil { + return nil, err + } + users, err := g.getUsersInfo(ctx, []string{userID}) + if err != nil { + return nil, err + } + if len(users) == 0 { + return nil, servererrs.ErrUserIDNotFound.WrapMsg(fmt.Sprintf("user %s not found", userID)) + } + info, ok := users[0].(*sdkws.UserInfo) + if !ok { + info = &sdkws.UserInfo{ + UserID: users[0].GetUserID(), + Nickname: users[0].GetNickname(), + FaceURL: users[0].GetFaceURL(), + Ex: users[0].GetEx(), + } + } + return convert.Db2PbGroupRequest(request, info, nil), nil +} + +func (g *NotificationSender) JoinGroupApplicationNotification(ctx context.Context, req *pbgroup.JoinGroupReq, dbReq *model.GroupRequest) { var err error defer func() { if err != nil { log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err) } }() + request, err := g.getGroupRequest(ctx, dbReq.GroupID, dbReq.UserID) + if err != nil { + log.ZError(ctx, "JoinGroupApplicationNotification getGroupRequest", err, "dbReq", dbReq) + return + } var group *sdkws.GroupInfo group, err = g.getGroupInfo(ctx, req.GroupID) if err != nil { @@ -387,7 +421,13 @@ func (g *NotificationSender) JoinGroupApplicationNotification(ctx context.Contex return } userIDs = append(userIDs, req.InviterUserID, mcontext.GetOpUserID(ctx)) - tips := &sdkws.JoinGroupApplicationTips{Group: group, Applicant: user, ReqMsg: req.ReqMessage} + tips := &sdkws.JoinGroupApplicationTips{ + Group: group, + Applicant: user, + ReqMsg: req.ReqMessage, + Uuid: g.uuid(), + Request: request, + } for _, userID := range datautil.Distinct(userIDs) { g.Notification(ctx, mcontext.GetOpUserID(ctx), userID, constant.JoinGroupApplicationNotification, tips) } @@ -417,6 +457,11 @@ func (g *NotificationSender) GroupApplicationAcceptedNotification(ctx context.Co log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err) } }() + request, err := g.getGroupRequest(ctx, req.GroupID, req.FromUserID) + if err != nil { + log.ZError(ctx, "GroupApplicationAcceptedNotification getGroupRequest", err, "req", req) + return + } var group *sdkws.GroupInfo group, err = g.getGroupInfo(ctx, req.GroupID) if err != nil { @@ -432,8 +477,14 @@ func (g *NotificationSender) GroupApplicationAcceptedNotification(ctx context.Co if err = g.fillOpUser(ctx, &opUser, group.GroupID); err != nil { return } + tips := &sdkws.GroupApplicationAcceptedTips{ + Group: group, + OpUser: opUser, + HandleMsg: req.HandledMsg, + Uuid: g.uuid(), + Request: request, + } for _, userID := range append(userIDs, req.FromUserID) { - tips := &sdkws.GroupApplicationAcceptedTips{Group: group, OpUser: opUser, HandleMsg: req.HandledMsg} if userID == req.FromUserID { tips.ReceiverAs = applicantReceiver } else { @@ -450,6 +501,11 @@ func (g *NotificationSender) GroupApplicationRejectedNotification(ctx context.Co log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err) } }() + request, err := g.getGroupRequest(ctx, req.GroupID, req.FromUserID) + if err != nil { + log.ZError(ctx, "GroupApplicationAcceptedNotification getGroupRequest", err, "req", req) + return + } var group *sdkws.GroupInfo group, err = g.getGroupInfo(ctx, req.GroupID) if err != nil { @@ -465,8 +521,14 @@ func (g *NotificationSender) GroupApplicationRejectedNotification(ctx context.Co if err = g.fillOpUser(ctx, &opUser, group.GroupID); err != nil { return } + tips := &sdkws.GroupApplicationRejectedTips{ + Group: group, + OpUser: opUser, + HandleMsg: req.HandledMsg, + Uuid: g.uuid(), + Request: request, + } for _, userID := range append(userIDs, req.FromUserID) { - tips := &sdkws.GroupApplicationAcceptedTips{Group: group, OpUser: opUser, HandleMsg: req.HandledMsg} if userID == req.FromUserID { tips.ReceiverAs = applicantReceiver } else { diff --git a/internal/rpc/relation/friend.go b/internal/rpc/relation/friend.go index 06661c79d..8c7c40536 100644 --- a/internal/rpc/relation/friend.go +++ b/internal/rpc/relation/friend.go @@ -18,6 +18,7 @@ import ( "context" "github.com/openimsdk/open-im-server/v3/pkg/dbbuild" + "github.com/openimsdk/open-im-server/v3/pkg/notification/common_user" "github.com/openimsdk/open-im-server/v3/pkg/rpcli" "github.com/openimsdk/tools/mq/memamq" @@ -100,23 +101,24 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr return err } userClient := rpcli.NewUserClient(userConn) - + database := controller.NewFriendDatabase( + friendMongoDB, + friendRequestMongoDB, + redis.NewFriendCacheRedis(rdb, &config.LocalCacheConfig, friendMongoDB), + mgocli.GetTx(), + ) // Initialize notification sender notificationSender := NewFriendNotificationSender( &config.NotificationConfig, rpcli.NewMsgClient(msgConn), WithRpcFunc(userClient.GetUsersInfo), + WithFriendDB(database), ) localcache.InitLocalCache(&config.LocalCacheConfig) // Register Friend server with refactored MongoDB and Redis integrations relation.RegisterFriendServer(server, &friendServer{ - db: controller.NewFriendDatabase( - friendMongoDB, - friendRequestMongoDB, - redis.NewFriendCacheRedis(rdb, &config.LocalCacheConfig, friendMongoDB), - mgocli.GetTx(), - ), + db: database, blackDatabase: controller.NewBlackDatabase( blackMongoDB, redis.NewBlackCacheRedis(rdb, &config.LocalCacheConfig, blackMongoDB), @@ -328,7 +330,7 @@ func (s *friendServer) GetDesignatedFriendsApply(ctx context.Context, req *relat return nil, err } resp = &relation.GetDesignatedFriendsApplyResp{} - resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.userClient.GetUsersInfoMap) + resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap) if err != nil { return nil, err } @@ -341,13 +343,16 @@ func (s *friendServer) GetPaginationFriendsApplyTo(ctx context.Context, req *rel return nil, err } - total, friendRequests, err := s.db.PageFriendRequestToMe(ctx, req.UserID, req.Pagination) + handleResults := datautil.Slice(req.HandleResults, func(e int32) int { + return int(e) + }) + total, friendRequests, err := s.db.PageFriendRequestToMe(ctx, req.UserID, handleResults, req.Pagination) if err != nil { return nil, err } resp = &relation.GetPaginationFriendsApplyToResp{} - resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.userClient.GetUsersInfoMap) + resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap) if err != nil { return nil, err } @@ -358,18 +363,20 @@ func (s *friendServer) GetPaginationFriendsApplyTo(ctx context.Context, req *rel } func (s *friendServer) GetPaginationFriendsApplyFrom(ctx context.Context, req *relation.GetPaginationFriendsApplyFromReq) (resp *relation.GetPaginationFriendsApplyFromResp, err error) { - resp = &relation.GetPaginationFriendsApplyFromResp{} - if err := authverify.CheckAccess(ctx, req.UserID); err != nil { return nil, err } - total, friendRequests, err := s.db.PageFriendRequestFromMe(ctx, req.UserID, req.Pagination) + handleResults := datautil.Slice(req.HandleResults, func(e int32) int { + return int(e) + }) + total, friendRequests, err := s.db.PageFriendRequestFromMe(ctx, req.UserID, handleResults, req.Pagination) if err != nil { return nil, err } - resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.userClient.GetUsersInfoMap) + resp = &relation.GetPaginationFriendsApplyFromResp{} + resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap) if err != nil { return nil, err } @@ -544,3 +551,28 @@ func (s *friendServer) UpdateFriends(ctx context.Context, req *relation.UpdateFr s.notificationSender.FriendsInfoUpdateNotification(ctx, req.OwnerUserID, req.FriendUserIDs) return resp, nil } + +func (s *friendServer) GetSelfUnhandledApplyCount(ctx context.Context, req *relation.GetSelfUnhandledApplyCountReq) (*relation.GetSelfUnhandledApplyCountResp, error) { + if err := authverify.CheckAccess(ctx, req.UserID); err != nil { + return nil, err + } + + count, err := s.db.GetUnhandledCount(ctx, req.UserID, req.Time) + if err != nil { + return nil, err + } + + return &relation.GetSelfUnhandledApplyCountResp{ + Count: count, + }, 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 { + return nil, err + } + return datautil.SliceToMapAny(users, func(e *sdkws.UserInfo) (string, common_user.CommonUser) { + return e.UserID, e + }), nil +} diff --git a/internal/rpc/relation/notification.go b/internal/rpc/relation/notification.go index caf2dafe1..d6a03003e 100644 --- a/internal/rpc/relation/notification.go +++ b/internal/rpc/relation/notification.go @@ -19,6 +19,9 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/rpcli" "github.com/openimsdk/protocol/msg" + "github.com/openimsdk/tools/errs" + "github.com/openimsdk/tools/log" + "github.com/openimsdk/tools/utils/datautil" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/versionctx" @@ -52,9 +55,7 @@ func WithFriendDB(db controller.FriendDatabase) friendNotificationSenderOptions } } -func WithDBFunc( - fn func(ctx context.Context, userIDs []string) (users []*relationtb.User, err error), -) friendNotificationSenderOptions { +func WithDBFunc(fn func(ctx context.Context, userIDs []string) (users []*relationtb.User, err error)) friendNotificationSenderOptions { return func(s *FriendNotificationSender) { f := func(ctx context.Context, userIDs []string) (result []common_user.CommonUser, err error) { users, err := fn(ctx, userIDs) @@ -70,9 +71,7 @@ func WithDBFunc( } } -func WithRpcFunc( - fn func(ctx context.Context, userIDs []string) ([]*sdkws.UserInfo, error), -) friendNotificationSenderOptions { +func WithRpcFunc(fn func(ctx context.Context, userIDs []string) ([]*sdkws.UserInfo, error)) friendNotificationSenderOptions { return func(s *FriendNotificationSender) { f := func(ctx context.Context, userIDs []string) (result []common_user.CommonUser, err error) { users, err := fn(ctx, userIDs) @@ -100,10 +99,7 @@ func NewFriendNotificationSender(conf *config.Notification, msgClient *rpcli.Msg return f } -func (f *FriendNotificationSender) getUsersInfoMap( - ctx context.Context, - userIDs []string, -) (map[string]*sdkws.UserInfo, error) { +func (f *FriendNotificationSender) getUsersInfoMap(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error) { users, err := f.getUsersInfo(ctx, userIDs) if err != nil { return nil, err @@ -116,10 +112,7 @@ func (f *FriendNotificationSender) getUsersInfoMap( } //nolint:unused -func (f *FriendNotificationSender) getFromToUserNickname( - ctx context.Context, - fromUserID, toUserID string, -) (string, string, error) { +func (f *FriendNotificationSender) getFromToUserNickname(ctx context.Context, fromUserID, toUserID string) (string, string, error) { users, err := f.getUsersInfoMap(ctx, []string{fromUserID, toUserID}) if err != nil { return "", "", nil @@ -132,61 +125,108 @@ func (f *FriendNotificationSender) UserInfoUpdatedNotification(ctx context.Conte f.Notification(ctx, mcontext.GetOpUserID(ctx), changedUserID, constant.UserInfoUpdatedNotification, &tips) } -func (f *FriendNotificationSender) FriendApplicationAddNotification(ctx context.Context, req *relation.ApplyToAddFriendReq) { - tips := sdkws.FriendApplicationTips{FromToUserID: &sdkws.FromToUserID{ - FromUserID: req.FromUserID, - ToUserID: req.ToUserID, - }} - f.Notification(ctx, req.FromUserID, req.ToUserID, constant.FriendApplicationNotification, &tips) +func (f *FriendNotificationSender) getCommonUserMap(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error) { + users, err := f.getUsersInfo(ctx, userIDs) + if err != nil { + return nil, err + } + return datautil.SliceToMap(users, func(e common_user.CommonUser) string { + return e.GetUserID() + }), nil } -func (f *FriendNotificationSender) FriendApplicationAgreedNotification( - ctx context.Context, - req *relation.RespondFriendApplyReq, -) { - tips := sdkws.FriendApplicationApprovedTips{FromToUserID: &sdkws.FromToUserID{ - FromUserID: req.FromUserID, - ToUserID: req.ToUserID, - }, HandleMsg: req.HandleMsg} - f.Notification(ctx, req.ToUserID, req.FromUserID, constant.FriendApplicationApprovedNotification, &tips) +func (f *FriendNotificationSender) getFriendRequests(ctx context.Context, fromUserID, toUserID string) (*sdkws.FriendRequest, error) { + if f.db == nil { + return nil, errs.ErrInternalServer.WithDetail("db is nil") + } + friendRequests, err := f.db.FindBothFriendRequests(ctx, fromUserID, toUserID) + if err != nil { + return nil, err + } + requests, err := convert.FriendRequestDB2Pb(ctx, friendRequests, f.getCommonUserMap) + if err != nil { + return nil, err + } + for _, request := range requests { + if request.FromUserID == fromUserID && request.ToUserID == toUserID { + return request, nil + } + } + return nil, errs.ErrRecordNotFound.WrapMsg("friend request not found", "fromUserID", fromUserID, "toUserID", toUserID) } -func (f *FriendNotificationSender) FriendApplicationRefusedNotification( - ctx context.Context, - req *relation.RespondFriendApplyReq, -) { - tips := sdkws.FriendApplicationApprovedTips{FromToUserID: &sdkws.FromToUserID{ - FromUserID: req.FromUserID, - ToUserID: req.ToUserID, - }, HandleMsg: req.HandleMsg} - f.Notification(ctx, req.ToUserID, req.FromUserID, constant.FriendApplicationRejectedNotification, &tips) +func (f *FriendNotificationSender) FriendApplicationAddNotification(ctx context.Context, req *relation.ApplyToAddFriendReq) { + request, err := f.getFriendRequests(ctx, req.FromUserID, req.ToUserID) + if err != nil { + log.ZError(ctx, "FriendApplicationAddNotification get friend request", err, "fromUserID", req.FromUserID, "toUserID", req.ToUserID) + return + } + tips := sdkws.FriendApplicationTips{ + FromToUserID: &sdkws.FromToUserID{ + FromUserID: req.FromUserID, + ToUserID: req.ToUserID, + }, + Request: request, + } + f.Notification(ctx, req.FromUserID, req.ToUserID, constant.FriendApplicationNotification, &tips) } -func (f *FriendNotificationSender) FriendAddedNotification( - ctx context.Context, - operationID, opUserID, fromUserID, toUserID string, -) error { - tips := sdkws.FriendAddedTips{Friend: &sdkws.FriendInfo{}, OpUser: &sdkws.PublicUserInfo{}} - user, err := f.getUsersInfo(ctx, []string{opUserID}) +func (f *FriendNotificationSender) FriendApplicationAgreedNotification(ctx context.Context, req *relation.RespondFriendApplyReq) { + request, err := f.getFriendRequests(ctx, req.FromUserID, req.ToUserID) if err != nil { - return err + log.ZError(ctx, "FriendApplicationAgreedNotification get friend request", err, "fromUserID", req.FromUserID, "toUserID", req.ToUserID) + return } - tips.OpUser.UserID = user[0].GetUserID() - tips.OpUser.Ex = user[0].GetEx() - tips.OpUser.Nickname = user[0].GetNickname() - tips.OpUser.FaceURL = user[0].GetFaceURL() - friends, err := f.db.FindFriendsWithError(ctx, fromUserID, []string{toUserID}) - if err != nil { - return err + tips := sdkws.FriendApplicationApprovedTips{ + FromToUserID: &sdkws.FromToUserID{ + FromUserID: req.FromUserID, + ToUserID: req.ToUserID, + }, + HandleMsg: req.HandleMsg, + Request: request, } - tips.Friend, err = convert.FriendDB2Pb(ctx, friends[0], f.getUsersInfoMap) + f.Notification(ctx, req.ToUserID, req.FromUserID, constant.FriendApplicationApprovedNotification, &tips) +} + +func (f *FriendNotificationSender) FriendApplicationRefusedNotification(ctx context.Context, req *relation.RespondFriendApplyReq) { + request, err := f.getFriendRequests(ctx, req.FromUserID, req.ToUserID) if err != nil { - return err + log.ZError(ctx, "FriendApplicationRefusedNotification get friend request", err, "fromUserID", req.FromUserID, "toUserID", req.ToUserID) + return } - f.Notification(ctx, fromUserID, toUserID, constant.FriendAddedNotification, &tips) - return nil + tips := sdkws.FriendApplicationRejectedTips{ + FromToUserID: &sdkws.FromToUserID{ + FromUserID: req.FromUserID, + ToUserID: req.ToUserID, + }, + HandleMsg: req.HandleMsg, + Request: request, + } + f.Notification(ctx, req.ToUserID, req.FromUserID, constant.FriendApplicationRejectedNotification, &tips) } +//func (f *FriendNotificationSender) FriendAddedNotification(ctx context.Context, operationID, opUserID, fromUserID, toUserID string) error { +// tips := sdkws.FriendAddedTips{Friend: &sdkws.FriendInfo{}, OpUser: &sdkws.PublicUserInfo{}} +// user, err := f.getUsersInfo(ctx, []string{opUserID}) +// if err != nil { +// return err +// } +// tips.OpUser.UserID = user[0].GetUserID() +// tips.OpUser.Ex = user[0].GetEx() +// tips.OpUser.Nickname = user[0].GetNickname() +// tips.OpUser.FaceURL = user[0].GetFaceURL() +// friends, err := f.db.FindFriendsWithError(ctx, fromUserID, []string{toUserID}) +// if err != nil { +// return err +// } +// tips.Friend, err = convert.FriendDB2Pb(ctx, friends[0], f.getUsersInfoMap) +// if err != nil { +// return err +// } +// f.Notification(ctx, fromUserID, toUserID, constant.FriendAddedNotification, &tips) +// return nil +//} + func (f *FriendNotificationSender) FriendDeletedNotification(ctx context.Context, req *relation.DeleteFriendReq) { tips := sdkws.FriendDeletedTips{FromToUserID: &sdkws.FromToUserID{ FromUserID: req.OwnerUserID, diff --git a/pkg/common/convert/friend.go b/pkg/common/convert/friend.go index 6d346b0f4..e783ecb24 100644 --- a/pkg/common/convert/friend.go +++ b/pkg/common/convert/friend.go @@ -17,7 +17,9 @@ package convert import ( "context" "fmt" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "github.com/openimsdk/open-im-server/v3/pkg/notification/common_user" "github.com/openimsdk/protocol/relation" "github.com/openimsdk/protocol/sdkws" @@ -98,7 +100,7 @@ func FriendOnlyDB2PbOnly(friendsDB []*model.Friend) []*relation.FriendInfoOnly { }) } -func FriendRequestDB2Pb(ctx context.Context, friendRequests []*model.FriendRequest, getUsers func(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error)) ([]*sdkws.FriendRequest, error) { +func FriendRequestDB2Pb(ctx context.Context, friendRequests []*model.FriendRequest, getUsers func(ctx context.Context, userIDs []string) (map[string]common_user.CommonUser, error)) ([]*sdkws.FriendRequest, error) { if len(friendRequests) == 0 { return nil, nil } @@ -117,11 +119,11 @@ func FriendRequestDB2Pb(ctx context.Context, friendRequests []*model.FriendReque fromUser := users[friendRequest.FromUserID] res = append(res, &sdkws.FriendRequest{ FromUserID: friendRequest.FromUserID, - FromNickname: fromUser.Nickname, - FromFaceURL: fromUser.FaceURL, + FromNickname: fromUser.GetNickname(), + FromFaceURL: fromUser.GetFaceURL(), ToUserID: friendRequest.ToUserID, - ToNickname: toUser.Nickname, - ToFaceURL: toUser.FaceURL, + ToNickname: toUser.GetNickname(), + ToFaceURL: toUser.GetFaceURL(), HandleResult: friendRequest.HandleResult, ReqMsg: friendRequest.ReqMsg, CreateTime: friendRequest.CreateTime.UnixMilli(), diff --git a/pkg/common/storage/controller/friend.go b/pkg/common/storage/controller/friend.go index 88a5fc863..806468ea1 100644 --- a/pkg/common/storage/controller/friend.go +++ b/pkg/common/storage/controller/friend.go @@ -17,10 +17,11 @@ package controller import ( "context" "fmt" + "time" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" - "time" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" "github.com/openimsdk/protocol/constant" @@ -61,10 +62,10 @@ type FriendDatabase interface { PageInWhoseFriends(ctx context.Context, friendUserID string, pagination pagination.Pagination) (total int64, friends []*model.Friend, err error) // PageFriendRequestFromMe retrieves the friend requests sent by the user with pagination - PageFriendRequestFromMe(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) + PageFriendRequestFromMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) // PageFriendRequestToMe retrieves the friend requests received by the user with pagination - PageFriendRequestToMe(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) + PageFriendRequestToMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) // FindFriendsWithError fetches specified friends of a user and returns an error if any do not exist FindFriendsWithError(ctx context.Context, ownerUserID string, friendUserIDs []string) (friends []*model.Friend, err error) @@ -87,6 +88,8 @@ type FriendDatabase interface { FindFriendUserID(ctx context.Context, friendUserID string) ([]string, error) OwnerIncrVersion(ctx context.Context, ownerUserID string, friendUserIDs []string, state int32) error + + GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) } type friendDatabase struct { @@ -334,13 +337,13 @@ func (f *friendDatabase) PageInWhoseFriends(ctx context.Context, friendUserID st } // PageFriendRequestFromMe retrieves friend requests sent by me. It does not return an error if the result is empty. -func (f *friendDatabase) PageFriendRequestFromMe(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) { - return f.friendRequest.FindFromUserID(ctx, userID, pagination) +func (f *friendDatabase) PageFriendRequestFromMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) { + return f.friendRequest.FindFromUserID(ctx, userID, handleResults, pagination) } // PageFriendRequestToMe retrieves friend requests received by me. It does not return an error if the result is empty. -func (f *friendDatabase) PageFriendRequestToMe(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) { - return f.friendRequest.FindToUserID(ctx, userID, pagination) +func (f *friendDatabase) PageFriendRequestToMe(ctx context.Context, userID string, handleResults []int, pagination pagination.Pagination) (total int64, friends []*model.FriendRequest, err error) { + return f.friendRequest.FindToUserID(ctx, userID, handleResults, pagination) } // FindFriendsWithError retrieves specified friends' information for ownerUserID. Returns an error if any friend does not exist. @@ -397,3 +400,7 @@ func (f *friendDatabase) OwnerIncrVersion(ctx context.Context, ownerUserID strin } return f.cache.DelMaxFriendVersion(ownerUserID).ChainExecDel(ctx) } + +func (f *friendDatabase) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) { + return f.friendRequest.GetUnhandledCount(ctx, userID, ts) +} diff --git a/pkg/common/storage/controller/group.go b/pkg/common/storage/controller/group.go index 6de0432a3..037ab1c39 100644 --- a/pkg/common/storage/controller/group.go +++ b/pkg/common/storage/controller/group.go @@ -68,7 +68,7 @@ type GroupDatabase interface { // FindUserManagedGroupID retrieves group IDs managed by a user. FindUserManagedGroupID(ctx context.Context, userID string) (groupIDs []string, err error) // PageGroupRequest paginates through group requests for specified groups. - PageGroupRequest(ctx context.Context, groupIDs []string, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) + PageGroupRequest(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) // GetGroupRoleLevelMemberIDs retrieves user IDs of group members with a specific role level. GetGroupRoleLevelMemberIDs(ctx context.Context, groupID string, roleLevel int32) ([]string, error) @@ -100,7 +100,7 @@ type GroupDatabase interface { // FindGroupRequests retrieves multiple group join requests. FindGroupRequests(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupRequest, error) // PageGroupRequestUser paginates through group join requests made by a user. - PageGroupRequestUser(ctx context.Context, userID string, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) + PageGroupRequestUser(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) // CountTotal counts the total number of groups as of a certain date. CountTotal(ctx context.Context, before *time.Time) (count int64, err error) @@ -124,6 +124,8 @@ type GroupDatabase interface { SearchJoinGroup(ctx context.Context, userID string, keyword string, pagination pagination.Pagination) (int64, []*model.Group, error) FindJoinGroupID(ctx context.Context, userID string) ([]string, error) + + GetGroupApplicationUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error) } func NewGroupDatabase( @@ -304,8 +306,8 @@ func (g *groupDatabase) FindUserManagedGroupID(ctx context.Context, userID strin return g.groupMemberDB.FindUserManagedGroupID(ctx, userID) } -func (g *groupDatabase) PageGroupRequest(ctx context.Context, groupIDs []string, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) { - return g.groupRequestDB.PageGroup(ctx, groupIDs, pagination) +func (g *groupDatabase) PageGroupRequest(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) { + return g.groupRequestDB.PageGroup(ctx, groupIDs, handleResults, pagination) } func (g *groupDatabase) PageGetJoinGroup(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, totalGroupMembers []*model.GroupMember, err error) { @@ -463,16 +465,12 @@ func (g *groupDatabase) CreateGroupRequest(ctx context.Context, requests []*mode }) } -func (g *groupDatabase) TakeGroupRequest( - ctx context.Context, - groupID string, - userID string, -) (*model.GroupRequest, error) { +func (g *groupDatabase) TakeGroupRequest(ctx context.Context, groupID string, userID string) (*model.GroupRequest, error) { return g.groupRequestDB.Take(ctx, groupID, userID) } -func (g *groupDatabase) PageGroupRequestUser(ctx context.Context, userID string, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) { - return g.groupRequestDB.Page(ctx, userID, pagination) +func (g *groupDatabase) PageGroupRequestUser(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (int64, []*model.GroupRequest, error) { + return g.groupRequestDB.Page(ctx, userID, groupIDs, handleResults, pagination) } func (g *groupDatabase) CountTotal(ctx context.Context, before *time.Time) (count int64, err error) { @@ -565,3 +563,7 @@ func (g *groupDatabase) MemberGroupIncrVersion(ctx context.Context, groupID stri } return g.cache.DelMaxGroupMemberVersion(groupID).ChainExecDel(ctx) } + +func (g *groupDatabase) GetGroupApplicationUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error) { + return g.groupRequestDB.GetUnhandledCount(ctx, groupIDs, ts) +} diff --git a/pkg/common/storage/database/friend_request.go b/pkg/common/storage/database/friend_request.go index f163b4831..c0df77823 100644 --- a/pkg/common/storage/database/friend_request.go +++ b/pkg/common/storage/database/friend_request.go @@ -16,6 +16,7 @@ package database import ( "context" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" "github.com/openimsdk/tools/db/pagination" ) @@ -33,8 +34,9 @@ type FriendRequest interface { Find(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error) Take(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error) // Get list of friend requests received by toUserID - FindToUserID(ctx context.Context, toUserID string, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) + FindToUserID(ctx context.Context, toUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) // Get list of friend requests sent by fromUserID - FindFromUserID(ctx context.Context, fromUserID string, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) + FindFromUserID(ctx context.Context, fromUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) FindBothFriendRequests(ctx context.Context, fromUserID, toUserID string) (friends []*model.FriendRequest, err error) + GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) } diff --git a/pkg/common/storage/database/group_request.go b/pkg/common/storage/database/group_request.go index 7309584f0..766ea2cd5 100644 --- a/pkg/common/storage/database/group_request.go +++ b/pkg/common/storage/database/group_request.go @@ -16,6 +16,7 @@ package database import ( "context" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" "github.com/openimsdk/tools/db/pagination" ) @@ -26,6 +27,7 @@ type GroupRequest interface { UpdateHandler(ctx context.Context, groupID string, userID string, handledMsg string, handleResult int32) (err error) Take(ctx context.Context, groupID string, userID string) (groupRequest *model.GroupRequest, err error) FindGroupRequests(ctx context.Context, groupID string, userIDs []string) ([]*model.GroupRequest, error) - Page(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) - PageGroup(ctx context.Context, groupIDs []string, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) + Page(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) + PageGroup(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) + GetUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error) } diff --git a/pkg/common/storage/database/mgo/friend_request.go b/pkg/common/storage/database/mgo/friend_request.go index 4eed2f4a2..95edb77df 100644 --- a/pkg/common/storage/database/mgo/friend_request.go +++ b/pkg/common/storage/database/mgo/friend_request.go @@ -16,24 +16,32 @@ package mgo import ( "context" + "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" + "go.mongodb.org/mongo-driver/mongo/options" "github.com/openimsdk/tools/db/mongoutil" "github.com/openimsdk/tools/db/pagination" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" ) func NewFriendRequestMongo(db *mongo.Database) (database.FriendRequest, error) { coll := db.Collection(database.FriendRequestName) - _, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{ - Keys: bson.D{ - {Key: "from_user_id", Value: 1}, - {Key: "to_user_id", Value: 1}, + _, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "from_user_id", Value: 1}, + {Key: "to_user_id", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "create_time", Value: -1}, + }, }, - Options: options.Index().SetUnique(true), }) if err != nil { return nil, err @@ -45,12 +53,24 @@ type FriendRequestMgo struct { coll *mongo.Collection } -func (f *FriendRequestMgo) FindToUserID(ctx context.Context, toUserID string, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) { - return mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, bson.M{"to_user_id": toUserID}, pagination) +func (f *FriendRequestMgo) sort() any { + return bson.D{{Key: "create_time", Value: -1}} } -func (f *FriendRequestMgo) FindFromUserID(ctx context.Context, fromUserID string, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) { - return mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, bson.M{"from_user_id": fromUserID}, pagination) +func (f *FriendRequestMgo) FindToUserID(ctx context.Context, toUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) { + filter := bson.M{"to_user_id": toUserID} + if len(handleResults) > 0 { + filter["handle_result"] = bson.M{"$in": handleResults} + } + return mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, filter, pagination, options.Find().SetSort(f.sort())) +} + +func (f *FriendRequestMgo) FindFromUserID(ctx context.Context, fromUserID string, handleResults []int, pagination pagination.Pagination) (total int64, friendRequests []*model.FriendRequest, err error) { + filter := bson.M{"from_user_id": fromUserID} + if len(handleResults) > 0 { + filter["handle_result"] = bson.M{"$in": handleResults} + } + return mongoutil.FindPage[*model.FriendRequest](ctx, f.coll, filter, pagination, options.Find().SetSort(f.sort())) } func (f *FriendRequestMgo) FindBothFriendRequests(ctx context.Context, fromUserID, toUserID string) (friends []*model.FriendRequest, err error) { @@ -110,3 +130,11 @@ func (f *FriendRequestMgo) Find(ctx context.Context, fromUserID, toUserID string func (f *FriendRequestMgo) Take(ctx context.Context, fromUserID, toUserID string) (friendRequest *model.FriendRequest, err error) { return f.Find(ctx, fromUserID, toUserID) } + +func (f *FriendRequestMgo) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) { + filter := bson.M{"to_user_id": userID, "handle_result": 0} + if ts != 0 { + filter["req_time"] = bson.M{"$gt": ts} + } + return mongoutil.Count(ctx, f.coll, filter) +} diff --git a/pkg/common/storage/database/mgo/group_request.go b/pkg/common/storage/database/mgo/group_request.go index b1942b708..c67bbb3ea 100644 --- a/pkg/common/storage/database/mgo/group_request.go +++ b/pkg/common/storage/database/mgo/group_request.go @@ -16,8 +16,10 @@ package mgo import ( "context" + "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/utils/datautil" "github.com/openimsdk/tools/db/mongoutil" "github.com/openimsdk/tools/db/pagination" @@ -29,12 +31,19 @@ import ( func NewGroupRequestMgo(db *mongo.Database) (database.GroupRequest, error) { coll := db.Collection(database.GroupRequestName) - _, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{ - Keys: bson.D{ - {Key: "group_id", Value: 1}, - {Key: "user_id", Value: 1}, + _, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "group_id", Value: 1}, + {Key: "user_id", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "req_time", Value: -1}, + }, }, - Options: options.Index().SetUnique(true), }) if err != nil { return nil, errs.Wrap(err) @@ -66,10 +75,39 @@ func (g *GroupRequestMgo) FindGroupRequests(ctx context.Context, groupID string, return mongoutil.Find[*model.GroupRequest](ctx, g.coll, bson.M{"group_id": groupID, "user_id": bson.M{"$in": userIDs}}) } -func (g *GroupRequestMgo) Page(ctx context.Context, userID string, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) { - return mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, bson.M{"user_id": userID}, pagination) +func (g *GroupRequestMgo) sort() any { + return bson.D{{Key: "req_time", Value: -1}} +} + +func (g *GroupRequestMgo) Page(ctx context.Context, userID string, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) { + filter := bson.M{"user_id": userID} + if len(groupIDs) > 0 { + filter["group_id"] = bson.M{"$in": datautil.Distinct(groupIDs)} + } + if len(handleResults) > 0 { + filter["handle_result"] = bson.M{"$in": handleResults} + } + return mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, filter, pagination, options.Find().SetSort(g.sort())) +} + +func (g *GroupRequestMgo) PageGroup(ctx context.Context, groupIDs []string, handleResults []int, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) { + if len(groupIDs) == 0 { + return 0, nil, nil + } + filter := bson.M{"group_id": bson.M{"$in": groupIDs}} + if len(handleResults) > 0 { + filter["handle_result"] = bson.M{"$in": handleResults} + } + return mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, filter, pagination, options.Find().SetSort(g.sort())) } -func (g *GroupRequestMgo) PageGroup(ctx context.Context, groupIDs []string, pagination pagination.Pagination) (total int64, groups []*model.GroupRequest, err error) { - return mongoutil.FindPage[*model.GroupRequest](ctx, g.coll, bson.M{"group_id": bson.M{"$in": groupIDs}}, pagination) +func (g *GroupRequestMgo) GetUnhandledCount(ctx context.Context, groupIDs []string, ts int64) (int64, error) { + if len(groupIDs) == 0 { + return 0, nil + } + filter := bson.M{"group_id": bson.M{"$in": groupIDs}, "handle_result": 0} + if ts != 0 { + filter["req_time"] = bson.M{"$gt": ts} + } + return mongoutil.Count(ctx, g.coll, filter) } From 812c1e41271750306c3b4705c555a0bc7913aaa4 Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Tue, 27 May 2025 15:01:09 +0800 Subject: [PATCH 41/59] fix: optimize friend and group applications (#3389) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake * fix: in standalone mode, the user online status is wrong * fix: add permission check * fix: add permission check * fix: add rpc interface permission check * fix: CreateGroupChatConversations * feat: optimize friend and group applications * feat: optimize friend and group applications * feat: optimize friend and group applications * feat: optimize friend and group applications * fix: optimize friend and group applications --- pkg/common/storage/database/mgo/friend_request.go | 11 +++++++---- pkg/common/storage/database/mgo/group_request.go | 10 ++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pkg/common/storage/database/mgo/friend_request.go b/pkg/common/storage/database/mgo/friend_request.go index 95edb77df..12e63155b 100644 --- a/pkg/common/storage/database/mgo/friend_request.go +++ b/pkg/common/storage/database/mgo/friend_request.go @@ -16,15 +16,18 @@ package mgo import ( "context" + "time" + + "go.mongodb.org/mongo-driver/mongo/options" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" - "go.mongodb.org/mongo-driver/mongo/options" - "github.com/openimsdk/tools/db/mongoutil" - "github.com/openimsdk/tools/db/pagination" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + + "github.com/openimsdk/tools/db/mongoutil" + "github.com/openimsdk/tools/db/pagination" ) func NewFriendRequestMongo(db *mongo.Database) (database.FriendRequest, error) { @@ -134,7 +137,7 @@ func (f *FriendRequestMgo) Take(ctx context.Context, fromUserID, toUserID string func (f *FriendRequestMgo) GetUnhandledCount(ctx context.Context, userID string, ts int64) (int64, error) { filter := bson.M{"to_user_id": userID, "handle_result": 0} if ts != 0 { - filter["req_time"] = bson.M{"$gt": ts} + filter["create_time"] = bson.M{"$gt": time.Unix(ts, 0)} } return mongoutil.Count(ctx, f.coll, filter) } diff --git a/pkg/common/storage/database/mgo/group_request.go b/pkg/common/storage/database/mgo/group_request.go index c67bbb3ea..fb57f890f 100644 --- a/pkg/common/storage/database/mgo/group_request.go +++ b/pkg/common/storage/database/mgo/group_request.go @@ -16,17 +16,19 @@ 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/utils/datautil" - "github.com/openimsdk/tools/db/mongoutil" - "github.com/openimsdk/tools/db/pagination" - "github.com/openimsdk/tools/errs" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/openimsdk/tools/db/mongoutil" + "github.com/openimsdk/tools/db/pagination" + "github.com/openimsdk/tools/errs" ) func NewGroupRequestMgo(db *mongo.Database) (database.GroupRequest, error) { @@ -107,7 +109,7 @@ func (g *GroupRequestMgo) GetUnhandledCount(ctx context.Context, groupIDs []stri } filter := bson.M{"group_id": bson.M{"$in": groupIDs}, "handle_result": 0} if ts != 0 { - filter["req_time"] = bson.M{"$gt": ts} + filter["req_time"] = bson.M{"$gt": time.Unix(ts, 0)} } return mongoutil.Count(ctx, g.coll, filter) } From b7ca3bd95f580fc16d6de1caf8d9708e97c42dc3 Mon Sep 17 00:00:00 2001 From: HonQi <14954524+HonQii@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:46:20 +0800 Subject: [PATCH 42/59] fix redis config db field (#3395) --- pkg/common/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go index d5ae68ec0..cd57a11cf 100644 --- a/pkg/common/config/config.go +++ b/pkg/common/config/config.go @@ -328,7 +328,7 @@ type Redis struct { Username string `yaml:"username"` Password string `yaml:"password"` ClusterMode bool `yaml:"clusterMode"` - DB int `yaml:"storage"` + DB int `yaml:"db"` MaxRetry int `yaml:"maxRetry"` PoolSize int `yaml:"poolSize"` } From 04ee509b68b4ad1579d948cbaf82b37d2a2e518c Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Fri, 6 Jun 2025 11:29:45 +0800 Subject: [PATCH 43/59] fix: solve incorrect when sendMsg webhook callback after. (#3409) * fix: solve incorrect when sendMsg webhook callback after. * remove print. --- internal/msgtransfer/callback.go | 124 ++++++++++++++++++ internal/msgtransfer/init.go | 2 +- .../online_msg_to_mongo_handler.go | 18 ++- internal/rpc/msg/send.go | 6 +- 4 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 internal/msgtransfer/callback.go diff --git a/internal/msgtransfer/callback.go b/internal/msgtransfer/callback.go new file mode 100644 index 000000000..ea51c2839 --- /dev/null +++ b/internal/msgtransfer/callback.go @@ -0,0 +1,124 @@ +package msgtransfer + +import ( + "context" + "encoding/base64" + + "github.com/openimsdk/open-im-server/v3/pkg/apistruct" + "github.com/openimsdk/open-im-server/v3/pkg/common/config" + "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" + "github.com/openimsdk/protocol/constant" + "github.com/openimsdk/protocol/sdkws" + "github.com/openimsdk/tools/mcontext" + "github.com/openimsdk/tools/utils/datautil" + "github.com/openimsdk/tools/utils/stringutil" + "google.golang.org/protobuf/proto" + + cbapi "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" +) + +func toCommonCallback(ctx context.Context, msg *sdkws.MsgData, command string) cbapi.CommonCallbackReq { + return cbapi.CommonCallbackReq{ + SendID: msg.SendID, + ServerMsgID: msg.ServerMsgID, + CallbackCommand: command, + ClientMsgID: msg.ClientMsgID, + OperationID: mcontext.GetOperationID(ctx), + SenderPlatformID: msg.SenderPlatformID, + SenderNickname: msg.SenderNickname, + SessionType: msg.SessionType, + MsgFrom: msg.MsgFrom, + ContentType: msg.ContentType, + Status: msg.Status, + SendTime: msg.SendTime, + CreateTime: msg.CreateTime, + AtUserIDList: msg.AtUserIDList, + SenderFaceURL: msg.SenderFaceURL, + Content: GetContent(msg), + Seq: uint32(msg.Seq), + Ex: msg.Ex, + } +} + +func GetContent(msg *sdkws.MsgData) string { + if msg.ContentType >= constant.NotificationBegin && msg.ContentType <= constant.NotificationEnd { + var tips sdkws.TipsComm + _ = proto.Unmarshal(msg.Content, &tips) + content := tips.JsonDetail + return content + } else { + return string(msg.Content) + } +} + +func (mc *OnlineHistoryMongoConsumerHandler) webhookAfterSendSingleMsg(ctx context.Context, after *config.AfterConfig, msg *sdkws.MsgData) { + if msg.ContentType == constant.Typing { + return + } + if !filterAfterMsg(msg, after) { + return + } + cbReq := &cbapi.CallbackAfterSendSingleMsgReq{ + CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendSingleMsgCommand), + RecvID: msg.RecvID, + } + mc.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendSingleMsgResp{}, after, buildKeyMsgDataQuery(msg)) +} + +func (mc *OnlineHistoryMongoConsumerHandler) webhookAfterSendGroupMsg(ctx context.Context, after *config.AfterConfig, msg *sdkws.MsgData) { + if msg.ContentType == constant.Typing { + return + } + if !filterAfterMsg(msg, after) { + return + } + cbReq := &cbapi.CallbackAfterSendGroupMsgReq{ + CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendGroupMsgCommand), + GroupID: msg.GroupID, + } + + mc.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendGroupMsgResp{}, after, buildKeyMsgDataQuery(msg)) +} + +func buildKeyMsgDataQuery(msg *sdkws.MsgData) map[string]string { + keyMsgData := apistruct.KeyMsgData{ + SendID: msg.SendID, + RecvID: msg.RecvID, + GroupID: msg.GroupID, + } + + return map[string]string{ + webhook.Key: base64.StdEncoding.EncodeToString(stringutil.StructToJsonBytes(keyMsgData)), + } +} + +func filterAfterMsg(msg *sdkws.MsgData, after *config.AfterConfig) bool { + return filterMsg(msg, after.AttentionIds, after.DeniedTypes) +} + +func filterMsg(msg *sdkws.MsgData, attentionIds []string, deniedTypes []int32) bool { + // According to the attentionIds configuration, only some users are sent + if len(attentionIds) != 0 && !datautil.Contain(msg.RecvID, attentionIds...) { + return false + } + + if defaultDeniedTypes(msg.ContentType) { + return false + } + + if len(deniedTypes) != 0 && datautil.Contain(msg.ContentType, deniedTypes...) { + return false + } + + return true +} + +func defaultDeniedTypes(contentType int32) bool { + if contentType >= constant.NotificationBegin && contentType <= constant.NotificationEnd { + return true + } + if contentType == constant.Typing { + return true + } + return false +} diff --git a/internal/msgtransfer/init.go b/internal/msgtransfer/init.go index 175813552..b07ec6f1d 100644 --- a/internal/msgtransfer/init.go +++ b/internal/msgtransfer/init.go @@ -134,7 +134,7 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr if err != nil { return err } - historyMongoHandler := NewOnlineHistoryMongoConsumerHandler(msgTransferDatabase) + historyMongoHandler := NewOnlineHistoryMongoConsumerHandler(msgTransferDatabase,config) msgTransfer := &MsgTransfer{ historyConsumer: historyConsumer, diff --git a/internal/msgtransfer/online_msg_to_mongo_handler.go b/internal/msgtransfer/online_msg_to_mongo_handler.go index a895bb9c4..6c1498f82 100644 --- a/internal/msgtransfer/online_msg_to_mongo_handler.go +++ b/internal/msgtransfer/online_msg_to_mongo_handler.go @@ -19,6 +19,8 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller" + "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" + "github.com/openimsdk/protocol/constant" pbmsg "github.com/openimsdk/protocol/msg" "github.com/openimsdk/tools/log" "google.golang.org/protobuf/proto" @@ -26,11 +28,15 @@ import ( type OnlineHistoryMongoConsumerHandler struct { msgTransferDatabase controller.MsgTransferDatabase + config *Config + webhookClient *webhook.Client } -func NewOnlineHistoryMongoConsumerHandler(database controller.MsgTransferDatabase) *OnlineHistoryMongoConsumerHandler { +func NewOnlineHistoryMongoConsumerHandler(database controller.MsgTransferDatabase, config *Config) *OnlineHistoryMongoConsumerHandler { return &OnlineHistoryMongoConsumerHandler{ msgTransferDatabase: database, + config: config, + webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL), } } @@ -53,6 +59,16 @@ func (mc *OnlineHistoryMongoConsumerHandler) HandleChatWs2Mongo(ctx context.Cont } else { prommetrics.MsgInsertMongoSuccessCounter.Inc() } + + for _, msgData := range msgFromMQ.MsgData { + switch msgData.SessionType { + case constant.SingleChatType: + mc.webhookAfterSendSingleMsg(ctx, &mc.config.WebhooksConfig.AfterSendSingleMsg, msgData) + case constant.ReadGroupChatType: + mc.webhookAfterSendGroupMsg(ctx, &mc.config.WebhooksConfig.AfterSendGroupMsg, msgData) + } + } + //var seqs []int64 //for _, msg := range msgFromMQ.MsgData { // seqs = append(seqs, msg.Seq) diff --git a/internal/rpc/msg/send.go b/internal/rpc/msg/send.go index f13b80708..d97905bff 100644 --- a/internal/rpc/msg/send.go +++ b/internal/rpc/msg/send.go @@ -86,7 +86,8 @@ func (m *msgServer) sendMsgGroupChat(ctx context.Context, req *pbmsg.SendMsgReq, go m.setConversationAtInfo(ctx, req.MsgData) } - m.webhookAfterSendGroupMsg(ctx, &m.config.WebhooksConfig.AfterSendGroupMsg, req) + // m.webhookAfterSendGroupMsg(ctx, &m.config.WebhooksConfig.AfterSendGroupMsg, req) + prommetrics.GroupChatMsgProcessSuccessCounter.Inc() resp = &pbmsg.SendMsgResp{} resp.SendTime = req.MsgData.SendTime @@ -192,7 +193,8 @@ func (m *msgServer) sendMsgSingleChat(ctx context.Context, req *pbmsg.SendMsgReq prommetrics.SingleChatMsgProcessFailedCounter.Inc() return nil, err } - m.webhookAfterSendSingleMsg(ctx, &m.config.WebhooksConfig.AfterSendSingleMsg, req) + + // m.webhookAfterSendSingleMsg(ctx, &m.config.WebhooksConfig.AfterSendSingleMsg, req) prommetrics.SingleChatMsgProcessSuccessCounter.Inc() return &pbmsg.SendMsgResp{ ServerMsgID: req.MsgData.ServerMsgID, From 75367545ea330bdb67a6e4242cb31e5652ce44dc Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Fri, 6 Jun 2025 14:25:58 +0800 Subject: [PATCH 44/59] feat: support distributed lock in crontask. (#3401) * feat: support distributed lock in crontask. * remove space. * remove comment. * remove log. * Update logic. * Update contents. --- internal/tools/cron/cron_task.go | 21 ++++++-- internal/tools/cron/dist_look.go | 89 ++++++++++++++++++++++++++++++++ start-config.yml | 2 +- 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 internal/tools/cron/dist_look.go diff --git a/internal/tools/cron/cron_task.go b/internal/tools/cron/cron_task.go index 7ae314193..a4de309d4 100644 --- a/internal/tools/cron/cron_task.go +++ b/internal/tools/cron/cron_task.go @@ -58,6 +58,11 @@ func Start(ctx context.Context, conf *Config, client discovery.Conn, service grp cm.Watch(ctx) } + locker, err := NewEtcdLocker(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient()) + if err != nil { + return err + } + srv := &cronServer{ ctx: ctx, config: conf, @@ -65,6 +70,7 @@ func Start(ctx context.Context, conf *Config, client discovery.Conn, service grp msgClient: msg.NewMsgClient(msgConn), conversationClient: pbconversation.NewConversationClient(conversationConn), thirdClient: third.NewThirdClient(thirdConn), + locker: locker, } if err := srv.registerClearS3(); err != nil { @@ -81,6 +87,8 @@ func Start(ctx context.Context, conf *Config, client discovery.Conn, service grp log.ZDebug(ctx, "cron task server is running") <-ctx.Done() log.ZDebug(ctx, "cron task server is shutting down") + srv.cron.Stop() + return nil } @@ -91,6 +99,7 @@ type cronServer struct { msgClient msg.MsgClient conversationClient pbconversation.ConversationClient thirdClient third.ThirdClient + locker *EtcdLocker } func (c *cronServer) registerClearS3() error { @@ -98,7 +107,9 @@ func (c *cronServer) registerClearS3() error { log.ZInfo(c.ctx, "disable scheduled cleanup of s3", "fileExpireTime", c.config.CronTask.FileExpireTime, "deleteObjectType", c.config.CronTask.DeleteObjectType) return nil } - _, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, c.clearS3) + _, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() { + c.locker.ExecuteWithLock(c.ctx, "clearS3", c.clearS3) + }) return errs.WrapMsg(err, "failed to register clear s3 cron task") } @@ -107,11 +118,15 @@ func (c *cronServer) registerDeleteMsg() error { log.ZInfo(c.ctx, "disable scheduled cleanup of chat records", "retainChatRecords", c.config.CronTask.RetainChatRecords) return nil } - _, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, c.deleteMsg) + _, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() { + c.locker.ExecuteWithLock(c.ctx, "deleteMsg", c.deleteMsg) + }) return errs.WrapMsg(err, "failed to register delete msg cron task") } func (c *cronServer) registerClearUserMsg() error { - _, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, c.clearUserMsg) + _, err := c.cron.AddFunc(c.config.CronTask.CronExecuteTime, func() { + c.locker.ExecuteWithLock(c.ctx, "clearUserMsg", c.clearUserMsg) + }) return errs.WrapMsg(err, "failed to register clear user msg cron task") } diff --git a/internal/tools/cron/dist_look.go b/internal/tools/cron/dist_look.go new file mode 100644 index 000000000..d1d2d1cb0 --- /dev/null +++ b/internal/tools/cron/dist_look.go @@ -0,0 +1,89 @@ +package cron + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/openimsdk/tools/log" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/concurrency" +) + +const ( + lockLeaseTTL = 300 +) + +type EtcdLocker struct { + client *clientv3.Client + instanceID string +} + +// NewEtcdLocker creates a new etcd distributed lock +func NewEtcdLocker(client *clientv3.Client) (*EtcdLocker, error) { + hostname, _ := os.Hostname() + pid := os.Getpid() + instanceID := fmt.Sprintf("%s-pid-%d-%d", hostname, pid, time.Now().UnixNano()) + + locker := &EtcdLocker{ + client: client, + instanceID: instanceID, + } + + return locker, nil +} + +func (e *EtcdLocker) ExecuteWithLock(ctx context.Context, taskName string, task func()) { + session, err := concurrency.NewSession(e.client, concurrency.WithTTL(lockLeaseTTL)) + if err != nil { + log.ZWarn(ctx, "Failed to create etcd session", err, + "taskName", taskName, + "instanceID", e.instanceID) + return + } + defer session.Close() + + lockKey := fmt.Sprintf("openim/crontask/%s", taskName) + mutex := concurrency.NewMutex(session, lockKey) + + ctxWithTimeout, cancel := context.WithTimeout(ctx, 100*time.Millisecond) + defer cancel() + + err = mutex.TryLock(ctxWithTimeout) + if err != nil { + if err == context.DeadlineExceeded { + log.ZDebug(ctx, "Task is being executed by another instance, skipping", + "taskName", taskName, + "instanceID", e.instanceID) + } else { + log.ZWarn(ctx, "Failed to acquire task lock", err, + "taskName", taskName, + "instanceID", e.instanceID) + } + return + } + + defer func() { + if err := mutex.Unlock(ctx); err != nil { + log.ZWarn(ctx, "Failed to release task lock", err, + "taskName", taskName, + "instanceID", e.instanceID) + } else { + log.ZInfo(ctx, "Successfully released task lock", + "taskName", taskName, + "instanceID", e.instanceID) + } + }() + + log.ZInfo(ctx, "Successfully acquired task lock, starting execution", + "taskName", taskName, + "instanceID", e.instanceID, + "sessionID", session.Lease()) + + task() + + log.ZInfo(ctx, "Task execution completed", + "taskName", taskName, + "instanceID", e.instanceID) +} diff --git a/start-config.yml b/start-config.yml index 1231b5d0d..da959044d 100644 --- a/start-config.yml +++ b/start-config.yml @@ -1,6 +1,6 @@ serviceBinaries: openim-api: 1 - openim-crontask: 1 + openim-crontask: 4 openim-rpc-user: 1 openim-msggateway: 1 openim-push: 8 From d156e1e5199cbc923a919ef422a589c9ca73b385 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:29:44 +0800 Subject: [PATCH 45/59] fix: prometheus discovery (#3408) --- cmd/main.go | 17 ++------------ go.mod | 2 +- go.sum | 4 ++-- internal/api/init.go | 2 +- internal/api/prometheus_discovery.go | 27 ++++++++++++----------- internal/api/router.go | 2 +- internal/msggateway/init.go | 2 +- internal/msgtransfer/init.go | 2 +- internal/push/push.go | 2 +- internal/rpc/auth/auth.go | 2 +- internal/rpc/conversation/conversation.go | 2 +- internal/rpc/group/group.go | 2 +- internal/rpc/msg/server.go | 2 +- internal/rpc/relation/friend.go | 2 +- internal/rpc/third/third.go | 2 +- internal/rpc/user/user.go | 2 +- internal/tools/cron/cron_task.go | 2 +- pkg/common/cmd/api.go | 3 ++- pkg/common/cmd/msg_transfer.go | 3 ++- pkg/common/prommetrics/prommetrics.go | 8 ++++++- pkg/common/startrpc/start.go | 10 +++++---- 21 files changed, 49 insertions(+), 51 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 1d0b82be8..7e19f1c98 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,7 +3,6 @@ package main import ( "bytes" "context" - "encoding/json" "flag" "fmt" "net" @@ -39,7 +38,6 @@ import ( "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/system/program" "github.com/openimsdk/tools/utils/datautil" - "github.com/openimsdk/tools/utils/network" "github.com/spf13/viper" "google.golang.org/grpc" ) @@ -250,23 +248,12 @@ func (x *cmds) run(ctx context.Context) error { return err } } - ip, err := network.GetLocalIP() - if err != nil { - return err - } listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { return fmt.Errorf("prometheus listen %d error %w", port, err) } defer listener.Close() log.ZDebug(ctx, "prometheus start", "addr", listener.Addr()) - target, err := json.Marshal(prommetrics.BuildDefaultTarget(ip, listener.Addr().(*net.TCPAddr).Port)) - if err != nil { - return err - } - if err := standalone.GetKeyValue().SetKey(ctx, prommetrics.BuildDiscoveryKey(prommetrics.APIKeyName), target); err != nil { - return err - } go func() { err := prommetrics.Start(listener) if err == nil { @@ -342,7 +329,7 @@ func (x *cmds) run(ctx context.Context) error { } } -func putCmd[C any](cmd *cmds, block bool, fn func(ctx context.Context, config *C, client discovery.Conn, server grpc.ServiceRegistrar) error) { +func putCmd[C any](cmd *cmds, block bool, fn func(ctx context.Context, config *C, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error) { name := path.Base(runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()) if index := strings.Index(name, "."); index >= 0 { name = name[:index] @@ -352,7 +339,7 @@ func putCmd[C any](cmd *cmds, block bool, fn func(ctx context.Context, config *C if err := cmd.parseConf(&conf); err != nil { return err } - return fn(ctx, &conf, standalone.GetDiscoveryConn(), standalone.GetServiceRegistrar()) + return fn(ctx, &conf, standalone.GetSvcDiscoveryRegistry(), standalone.GetServiceRegistrar()) }) } diff --git a/go.mod b/go.mod index 221e28b72..845f75bb2 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/openimsdk/protocol v0.0.73-alpha.12 - github.com/openimsdk/tools v0.0.50-alpha.84 + github.com/openimsdk/tools v0.0.50-alpha.85 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 6298f98c9..b775a1056 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5b github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.73-alpha.12 h1:2NYawXeHChYUeSme6QJ9pOLh+Empce2WmwEtbP4JvKk= github.com/openimsdk/protocol v0.0.73-alpha.12/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= -github.com/openimsdk/tools v0.0.50-alpha.84 h1:jN60Ys/0edZjL/TDmm/5VSJFP4pGYRipkWqhILJbq/8= -github.com/openimsdk/tools v0.0.50-alpha.84/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= +github.com/openimsdk/tools v0.0.50-alpha.85 h1:OqTUYx6r7Zp/eH8FKB08XeNjPV405TUIG9QT6QQ+F+s= +github.com/openimsdk/tools v0.0.50-alpha.85/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/internal/api/init.go b/internal/api/init.go index 378f03eda..f3548e29a 100644 --- a/internal/api/init.go +++ b/internal/api/init.go @@ -39,7 +39,7 @@ type Config struct { Index conf.Index } -func Start(ctx context.Context, config *Config, client discovery.Conn, service grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, service grpc.ServiceRegistrar) error { apiPort, err := datautil.GetElemByIndex(config.API.Api.Ports, int(config.Index)) if err != nil { return err diff --git a/internal/api/prometheus_discovery.go b/internal/api/prometheus_discovery.go index bdcca4e26..c861003a0 100644 --- a/internal/api/prometheus_discovery.go +++ b/internal/api/prometheus_discovery.go @@ -6,35 +6,29 @@ import ( "net/http" "github.com/gin-gonic/gin" - conf "github.com/openimsdk/open-im-server/v3/pkg/common/config" "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" "github.com/openimsdk/tools/apiresp" "github.com/openimsdk/tools/discovery" - "github.com/openimsdk/tools/discovery/etcd" "github.com/openimsdk/tools/errs" - clientv3 "go.etcd.io/etcd/client/v3" ) type PrometheusDiscoveryApi struct { config *Config - client *clientv3.Client kv discovery.KeyValue } -func NewPrometheusDiscoveryApi(config *Config, client discovery.Conn) *PrometheusDiscoveryApi { +func NewPrometheusDiscoveryApi(config *Config, client discovery.SvcDiscoveryRegistry) *PrometheusDiscoveryApi { api := &PrometheusDiscoveryApi{ config: config, - } - if config.Discovery.Enable == conf.ETCD { - api.client = client.(*etcd.SvcDiscoveryRegistryImpl).GetClient() + kv: client, } return api } func (p *PrometheusDiscoveryApi) discovery(c *gin.Context, key string) { - value, err := p.kv.GetKey(c, prommetrics.BuildDiscoveryKey(key)) + value, err := p.kv.GetKeyWithPrefix(c, prommetrics.BuildDiscoveryKeyPrefix(key)) if err != nil { - if errors.Is(err, discovery.ErrNotSupportedKeyValue) { + if errors.Is(err, discovery.ErrNotSupported) { c.JSON(http.StatusOK, []struct{}{}) return } @@ -46,10 +40,17 @@ func (p *PrometheusDiscoveryApi) discovery(c *gin.Context, key string) { return } var resp prommetrics.RespTarget - if err := json.Unmarshal(value, &resp); err != nil { - apiresp.GinError(c, errs.WrapMsg(err, "json unmarshal err")) - return + for i := range value { + var tmp prommetrics.Target + if err = json.Unmarshal(value[i], &tmp); err != nil { + apiresp.GinError(c, errs.WrapMsg(err, "json unmarshal err")) + return + } + + resp.Targets = append(resp.Targets, tmp.Target) + resp.Labels = tmp.Labels // default label is fixed. See prommetrics.BuildDefaultTarget } + c.JSON(http.StatusOK, []*prommetrics.RespTarget{&resp}) } diff --git a/internal/api/router.go b/internal/api/router.go index 5e5f35a60..fcad104b8 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -53,7 +53,7 @@ func prommetricsGin() gin.HandlerFunc { } } -func newGinRouter(ctx context.Context, client discovery.Conn, cfg *Config) (*gin.Engine, error) { +func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, cfg *Config) (*gin.Engine, error) { authConn, err := client.GetConn(ctx, cfg.Discovery.RpcService.Auth) if err != nil { return nil, err diff --git a/internal/msggateway/init.go b/internal/msggateway/init.go index 8772693cc..40a57b1da 100644 --- a/internal/msggateway/init.go +++ b/internal/msggateway/init.go @@ -39,7 +39,7 @@ type Config struct { } // Start run ws server. -func Start(ctx context.Context, conf *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { log.CInfo(ctx, "MSG-GATEWAY server is initializing", "runtimeEnv", runtimeenv.RuntimeEnvironment(), "rpcPorts", conf.MsgGateway.RPC.Ports, "wsPort", conf.MsgGateway.LongConnSvr.Ports, "prometheusPorts", conf.MsgGateway.Prometheus.Ports) diff --git a/internal/msgtransfer/init.go b/internal/msgtransfer/init.go index b07ec6f1d..bbec3f9a2 100644 --- a/internal/msgtransfer/init.go +++ b/internal/msgtransfer/init.go @@ -58,7 +58,7 @@ type Config struct { Index conf.Index } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { builder := mqbuild.NewBuilder(&config.KafkaConfig) log.CInfo(ctx, "MSG-TRANSFER server is initializing", "runTimeEnv", runtimeenv.RuntimeEnvironment(), "prometheusPorts", diff --git a/internal/push/push.go b/internal/push/push.go index f720a52ac..1d6f8cb30 100644 --- a/internal/push/push.go +++ b/internal/push/push.go @@ -50,7 +50,7 @@ func (p pushServer) DelUserPushToken(ctx context.Context, return &pbpush.DelUserPushTokenResp{}, nil } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { dbb := dbbuild.NewBuilder(&config.MongoConfig, &config.RedisConfig) rdb, err := dbb.Redis(ctx) if err != nil { diff --git a/internal/rpc/auth/auth.go b/internal/rpc/auth/auth.go index 2c2691d1d..5ed9cdf12 100644 --- a/internal/rpc/auth/auth.go +++ b/internal/rpc/auth/auth.go @@ -59,7 +59,7 @@ type Config struct { Discovery config.Discovery } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { dbb := dbbuild.NewBuilder(&config.MongoConfig, &config.RedisConfig) rdb, err := dbb.Redis(ctx) if err != nil { diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index ba9e7746b..cf5a2b9c6 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -69,7 +69,7 @@ type Config struct { Discovery config.Discovery } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { dbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig) mgocli, err := dbb.Mongo(ctx) if err != nil { diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index 5219546b7..ce8a2c7aa 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -76,7 +76,7 @@ type Config struct { Discovery config.Discovery } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { dbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig) mgocli, err := dbb.Mongo(ctx) if err != nil { diff --git a/internal/rpc/msg/server.go b/internal/rpc/msg/server.go index cfc750c5b..9d5391cc9 100644 --- a/internal/rpc/msg/server.go +++ b/internal/rpc/msg/server.go @@ -78,7 +78,7 @@ func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorF } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { builder := mqbuild.NewBuilder(&config.KafkaConfig) redisProducer, err := builder.GetTopicProducer(ctx, config.KafkaConfig.ToRedisTopic) if err != nil { diff --git a/internal/rpc/relation/friend.go b/internal/rpc/relation/friend.go index 8c7c40536..d05cf7d77 100644 --- a/internal/rpc/relation/friend.go +++ b/internal/rpc/relation/friend.go @@ -66,7 +66,7 @@ type Config struct { Discovery config.Discovery } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { dbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig) mgocli, err := dbb.Mongo(ctx) if err != nil { diff --git a/internal/rpc/third/third.go b/internal/rpc/third/third.go index c6dcb2ea4..cea6a8522 100644 --- a/internal/rpc/third/third.go +++ b/internal/rpc/third/third.go @@ -64,7 +64,7 @@ type Config struct { Discovery config.Discovery } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { dbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig) mgocli, err := dbb.Mongo(ctx) if err != nil { diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index 5639baab9..28461ae0a 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -79,7 +79,7 @@ type Config struct { Discovery config.Discovery } -func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { +func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error { dbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig) mgocli, err := dbb.Mongo(ctx) if err != nil { diff --git a/internal/tools/cron/cron_task.go b/internal/tools/cron/cron_task.go index a4de309d4..abe596192 100644 --- a/internal/tools/cron/cron_task.go +++ b/internal/tools/cron/cron_task.go @@ -25,7 +25,7 @@ type Config struct { Discovery config.Discovery } -func Start(ctx context.Context, conf *Config, client discovery.Conn, service grpc.ServiceRegistrar) error { +func Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegistry, service grpc.ServiceRegistrar) error { log.CInfo(ctx, "CRON-TASK server is initializing", "runTimeEnv", runtimeenv.RuntimeEnvironment(), "chatRecordsClearTime", conf.CronTask.CronExecuteTime, "msgDestructTime", conf.CronTask.RetainChatRecords) if conf.CronTask.RetainChatRecords < 1 { log.ZInfo(ctx, "disable cron") diff --git a/pkg/common/cmd/api.go b/pkg/common/cmd/api.go index 484467798..7b7dbc89b 100644 --- a/pkg/common/cmd/api.go +++ b/pkg/common/cmd/api.go @@ -19,6 +19,7 @@ import ( "github.com/openimsdk/open-im-server/v3/internal/api" "github.com/openimsdk/open-im-server/v3/pkg/common/config" + "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" "github.com/openimsdk/open-im-server/v3/pkg/common/startrpc" "github.com/openimsdk/open-im-server/v3/version" "github.com/openimsdk/tools/system/program" @@ -84,7 +85,7 @@ func (a *ApiCmd) runE() error { a.apiConfig.API.Api.ListenIP, "", a.apiConfig.API.Prometheus.AutoSetPorts, nil, int(a.apiConfig.Index), - a.apiConfig.Discovery.RpcService.MessageGateway, + prommetrics.APIKeyName, &a.apiConfig.Notification, a.apiConfig, []string{}, diff --git a/pkg/common/cmd/msg_transfer.go b/pkg/common/cmd/msg_transfer.go index 9411a2cd0..fe6c27e54 100644 --- a/pkg/common/cmd/msg_transfer.go +++ b/pkg/common/cmd/msg_transfer.go @@ -19,6 +19,7 @@ import ( "github.com/openimsdk/open-im-server/v3/internal/msgtransfer" "github.com/openimsdk/open-im-server/v3/pkg/common/config" + "github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics" "github.com/openimsdk/open-im-server/v3/pkg/common/startrpc" "github.com/openimsdk/open-im-server/v3/version" "github.com/openimsdk/tools/system/program" @@ -65,7 +66,7 @@ func (m *MsgTransferCmd) runE() error { "", "", true, nil, int(m.msgTransferConfig.Index), - "", + prommetrics.MessageTransferKeyName, nil, m.msgTransferConfig, []string{}, diff --git a/pkg/common/prommetrics/prommetrics.go b/pkg/common/prommetrics/prommetrics.go index 153314bbb..3f683a50e 100644 --- a/pkg/common/prommetrics/prommetrics.go +++ b/pkg/common/prommetrics/prommetrics.go @@ -85,6 +85,8 @@ func Start(listener net.Listener) error { const ( APIKeyName = "api" MessageTransferKeyName = "message-transfer" + + TTL = 300 ) type Target struct { @@ -97,10 +99,14 @@ type RespTarget struct { Labels map[string]string `json:"labels"` } -func BuildDiscoveryKey(name string) string { +func BuildDiscoveryKeyPrefix(name string) string { return fmt.Sprintf("%s/%s/%s", "openim", "prometheus_discovery", name) } +func BuildDiscoveryKey(name string, index int) string { + return fmt.Sprintf("%s/%s/%s/%d", "openim", "prometheus_discovery", name, index) +} + func BuildDefaultTarget(host string, ip int) Target { return Target{ Target: fmt.Sprintf("%s:%d", host, ip), diff --git a/pkg/common/startrpc/start.go b/pkg/common/startrpc/start.go index b99d32db1..06e19d8d2 100644 --- a/pkg/common/startrpc/start.go +++ b/pkg/common/startrpc/start.go @@ -50,7 +50,7 @@ func init() { func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *conf.Prometheus, listenIP, registerIP string, autoSetPorts bool, rpcPorts []int, index int, rpcRegisterName string, notification *conf.Notification, config T, watchConfigNames []string, watchServiceNames []string, - rpcFn func(ctx context.Context, config T, client discovery.Conn, server grpc.ServiceRegistrar) error, + rpcFn func(ctx context.Context, config T, client discovery.SvcDiscoveryRegistry, server grpc.ServiceRegistrar) error, options ...grpc.ServerOption) error { if notification != nil { @@ -148,9 +148,11 @@ func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *c if err != nil { return err } - if err := client.SetKey(ctx, prommetrics.BuildDiscoveryKey(prommetrics.APIKeyName), target); err != nil { - if !errors.Is(err, discovery.ErrNotSupportedKeyValue) { - return err + if autoSetPorts { + if err = client.SetWithLease(ctx, prommetrics.BuildDiscoveryKey(rpcRegisterName, index), target, prommetrics.TTL); err != nil { + if !errors.Is(err, discovery.ErrNotSupported) { + return err + } } } go func() { From 545125884e8b1dbfe4de71a34ad1a5a5663bef7c Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Mon, 16 Jun 2025 10:46:07 +0800 Subject: [PATCH 46/59] fix: import friends send notification (#3420) --- internal/rpc/relation/friend.go | 4 ++-- internal/rpc/relation/notification.go | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/internal/rpc/relation/friend.go b/internal/rpc/relation/friend.go index d05cf7d77..43909c8e3 100644 --- a/internal/rpc/relation/friend.go +++ b/internal/rpc/relation/friend.go @@ -192,7 +192,7 @@ func (s *friendServer) ImportFriends(ctx context.Context, req *relation.ImportFr FromUserID: req.OwnerUserID, ToUserID: userID, HandleResult: constant.FriendResponseAgree, - }) + }, false) } s.webhookAfterImportFriends(ctx, &s.config.WebhooksConfig.AfterImportFriends, req) @@ -221,7 +221,7 @@ func (s *friendServer) RespondFriendApply(ctx context.Context, req *relation.Res return nil, err } s.webhookAfterAddFriendAgree(ctx, &s.config.WebhooksConfig.AfterAddFriendAgree, req) - s.notificationSender.FriendApplicationAgreedNotification(ctx, req) + s.notificationSender.FriendApplicationAgreedNotification(ctx, req, true) return resp, nil } if req.HandleResult == constant.FriendResponseRefuse { diff --git a/internal/rpc/relation/notification.go b/internal/rpc/relation/notification.go index d6a03003e..4ee45e197 100644 --- a/internal/rpc/relation/notification.go +++ b/internal/rpc/relation/notification.go @@ -171,11 +171,17 @@ func (f *FriendNotificationSender) FriendApplicationAddNotification(ctx context. f.Notification(ctx, req.FromUserID, req.ToUserID, constant.FriendApplicationNotification, &tips) } -func (f *FriendNotificationSender) FriendApplicationAgreedNotification(ctx context.Context, req *relation.RespondFriendApplyReq) { - request, err := f.getFriendRequests(ctx, req.FromUserID, req.ToUserID) - if err != nil { - log.ZError(ctx, "FriendApplicationAgreedNotification get friend request", err, "fromUserID", req.FromUserID, "toUserID", req.ToUserID) - return +func (f *FriendNotificationSender) FriendApplicationAgreedNotification(ctx context.Context, req *relation.RespondFriendApplyReq, checkReq bool) { + var ( + request *sdkws.FriendRequest + err error + ) + if checkReq { + request, err = f.getFriendRequests(ctx, req.FromUserID, req.ToUserID) + if err != nil { + log.ZError(ctx, "FriendApplicationAgreedNotification get friend request", err, "fromUserID", req.FromUserID, "toUserID", req.ToUserID) + return + } } tips := sdkws.FriendApplicationApprovedTips{ FromToUserID: &sdkws.FromToUserID{ From 1e2375faca1cb2ee3b08723f9627856db950cad4 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Tue, 17 Jun 2025 15:12:25 +0800 Subject: [PATCH 47/59] fix: improve mileston PR workflows contents. (#3382) --- .github/workflows/merge-from-milestone.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-from-milestone.yml b/.github/workflows/merge-from-milestone.yml index 1f5762ccb..a0ec2ac16 100644 --- a/.github/workflows/merge-from-milestone.yml +++ b/.github/workflows/merge-from-milestone.yml @@ -155,11 +155,27 @@ jobs: '{title: $title, head: $head, base: $base, body: $body}')") new_pr_number=$(echo "$response" | jq -r '.number') - echo "Created PR #$new_pr_number" - curl -s -X POST -H "Authorization: token $GITHUB_TOKEN" \ + if [[ "$new_pr_number" == "null" || -z "$new_pr_number" ]]; then + echo "Failed to create PR. Response: $response" + + git checkout $TARGET_BRANCH + + git branch -D $cherry_pick_branch + + echo "Deleted branch: $cherry_pick_branch" + git push origin --delete $cherry_pick_branch + else + echo "Created PR #$new_pr_number" + + curl -s -X POST -H "Authorization: token $GITHUB_TOKEN" \ -H "Accept: application/vnd.github+json" \ -d '{"labels": ["milestone-merge"]}' \ "https://api.github.com/repos/${{ github.repository }}/issues/$new_pr_number/labels" + fi + + echo "" + echo "----------------------------------------" + echo "" fi done From 1baf9a8e0f11cc94313eec2165e273482d1bdb0d Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Tue, 17 Jun 2025 15:33:25 +0800 Subject: [PATCH 48/59] feat: Implement etcd and kafka auth. (#3394) * feat: Implement etcd and kafka auth. * Update etcd command contents. * update contents. * feat: update auth logic to compatible old version. * update comment. * update contents. --- .env | 2 +- config/discovery.yml | 10 ++-- config/kafka.yml | 20 +++---- docker-compose.yml | 128 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 131 insertions(+), 29 deletions(-) diff --git a/.env b/.env index 0ab998037..2d4dfd4c7 100644 --- a/.env +++ b/.env @@ -2,7 +2,7 @@ MONGO_IMAGE=mongo:7.0 REDIS_IMAGE=redis:7.0.0 KAFKA_IMAGE=bitnami/kafka:3.5.1 MINIO_IMAGE=minio/minio:RELEASE.2024-01-11T07-46-16Z -ETCD_IMAGE=quay.io/coreos/etcd:v3.5.13 +ETCD_IMAGE=bitnami/etcd:3.5.13 PROMETHEUS_IMAGE=prom/prometheus:v2.45.6 ALERTMANAGER_IMAGE=prom/alertmanager:v0.27.0 GRAFANA_IMAGE=grafana/grafana:11.0.1 diff --git a/config/discovery.yml b/config/discovery.yml index e8d733e9f..2251dceb7 100644 --- a/config/discovery.yml +++ b/config/discovery.yml @@ -1,9 +1,11 @@ enable: etcd etcd: rootDirectory: openim - address: [ localhost:12379 ] - username: '' - password: '' + address: [localhost:12379] + ## Attention: If you set auth in etcd + ## you must also update the username and password in Chat project. + username: + password: kubernetes: namespace: default @@ -17,4 +19,4 @@ rpcService: group: group-rpc-service auth: auth-rpc-service conversation: conversation-rpc-service - third: third-rpc-service \ No newline at end of file + third: third-rpc-service diff --git a/config/kafka.yml b/config/kafka.yml index fd06ae2bb..2e9b5296c 100644 --- a/config/kafka.yml +++ b/config/kafka.yml @@ -1,13 +1,13 @@ -# Username for authentication -username: '' -# Password for authentication -password: '' +## Kafka authentication +username: +password: + # Producer acknowledgment settings -producerAck: +producerAck: # Compression type to use (e.g., none, gzip, snappy) compressType: none # List of Kafka broker addresses -address: [ localhost:19094 ] +address: [localhost:19094] # Kafka topic for Redis integration toRedisTopic: toRedis # Kafka topic for MongoDB integration @@ -29,12 +29,12 @@ tls: # Enable or disable TLS enableTLS: false # CA certificate file path - caCrt: + caCrt: # Client certificate file path - clientCrt: + clientCrt: # Client key file path - clientKey: + clientKey: # Client key password - clientKeyPwd: + clientKeyPwd: # Whether to skip TLS verification (not recommended for production) insecureSkipVerify: false diff --git a/docker-compose.yml b/docker-compose.yml index 65b4e6625..60fc865f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,8 +83,83 @@ services: - ETCD_INITIAL_CLUSTER=s1=http://0.0.0.0:2380 - ETCD_INITIAL_CLUSTER_TOKEN=tkn - ETCD_INITIAL_CLUSTER_STATE=new + - ALLOW_NONE_AUTHENTICATION=no + + ## Optional: Enable etcd authentication by setting the following credentials + # - ETCD_ROOT_USER=root + # - ETCD_ROOT_PASSWORD=openIM123 + # - ETCD_USERNAME=openIM + # - ETCD_PASSWORD=openIM123 volumes: - "${DATA_DIR}/components/etcd:/etcd-data" + command: > + /bin/sh -c ' + etcd & + export ETCDCTL_API=3 + echo "Waiting for etcd to become healthy..." + until etcdctl --endpoints=http://127.0.0.1:2379 endpoint health &>/dev/null; do + echo "Waiting for ETCD to start..." + sleep 1 + done + + echo "etcd is healthy." + + if [ -n "$${ETCD_ROOT_USER}" ] && [ -n "$${ETCD_ROOT_PASSWORD}" ] && [ -n "$${ETCD_USERNAME}" ] && [ -n "$${ETCD_PASSWORD}" ]; then + echo "Authentication credentials provided. Setting up authentication..." + + echo "Checking authentication status..." + if ! etcdctl --endpoints=http://127.0.0.1:2379 auth status | grep -q "Authentication Status: true"; then + echo "Authentication is disabled. Creating users and enabling..." + + # Create users and setup permissions + etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_ROOT_USER} --new-user-password=$${ETCD_ROOT_PASSWORD} || true + etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} || true + + etcdctl --endpoints=http://127.0.0.1:2379 role add openim-role || true + etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite / || true + etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite "" || true + etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_USERNAME} openim-role || true + + etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true + + echo "Enabling authentication..." + etcdctl --endpoints=http://127.0.0.1:2379 auth enable + echo "Authentication enabled successfully" + else + echo "Authentication is already enabled. Checking OpenIM user..." + + # Check if openIM user exists and can perform operations + if ! etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth "auth-check" &>/dev/null; then + echo "OpenIM user test failed. Recreating user with root credentials..." + + # Try to create/update the openIM user using root credentials + etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} --no-password-file || true + etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role add openim-role || true + etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite / || true + etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite "" || true + etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user grant-role $${ETCD_USERNAME} openim-role || true + etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true + + echo "OpenIM user recreated with required permissions" + else + echo "OpenIM user exists and has correct permissions" + etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth &>/dev/null + fi + fi + echo "Testing authentication with OpenIM user..." + if etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth "auth-works"; then + echo "Authentication working properly" + etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth + else + echo "WARNING: Authentication test failed" + fi + else + echo "No authentication credentials provided. Running in no-auth mode." + echo "To enable authentication, set ETCD_ROOT_USER, ETCD_ROOT_PASSWORD, ETCD_USERNAME, and ETCD_PASSWORD environment variables." + fi + + tail -f /dev/null + ' restart: always networks: - openim @@ -104,12 +179,38 @@ services: KAFKA_CFG_NODE_ID: 0 KAFKA_CFG_PROCESS_ROLES: controller,broker KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093 - KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 - KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:19094 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_NUM_PARTITIONS: 8 KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true" + + KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094" + KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092,EXTERNAL://localhost:19094" + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" + + # Authentication configuration variables - comment out to disable auth + # KAFKA_USERNAME: "openIM" + # KAFKA_PASSWORD: "openIM123" + command: > + /bin/sh -c ' + if [ -n "$${KAFKA_USERNAME}" ] && [ -n "$${KAFKA_PASSWORD}" ]; then + echo "=== Kafka SASL Authentication ENABLED ===" + echo "Username: $${KAFKA_USERNAME}" + + # Set environment variables for SASL authentication + export KAFKA_CFG_LISTENERS="SASL_PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094" + export KAFKA_CFG_ADVERTISED_LISTENERS="SASL_PLAINTEXT://kafka:9092,EXTERNAL://localhost:19094" + export KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP="CONTROLLER:PLAINTEXT,EXTERNAL:SASL_PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT" + export KAFKA_CFG_SASL_ENABLED_MECHANISMS="PLAIN" + export KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL="PLAIN" + export KAFKA_CFG_INTER_BROKER_LISTENER_NAME="SASL_PLAINTEXT" + export KAFKA_CLIENT_USERS="$${KAFKA_USERNAME}" + export KAFKA_CLIENT_PASSWORDS="$${KAFKA_PASSWORD}" + fi + + # Start Kafka with the configured environment + exec /opt/bitnami/scripts/kafka/entrypoint.sh /opt/bitnami/scripts/kafka/run.sh + ' networks: - openim @@ -148,7 +249,7 @@ services: - "11002:80" networks: - openim - + prometheus: image: ${PROMETHEUS_IMAGE} container_name: prometheus @@ -161,9 +262,9 @@ services: - ./config/instance-down-rules.yml:/etc/prometheus/instance-down-rules.yml - ${DATA_DIR}/components/prometheus/data:/prometheus command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - '--web.listen-address=:${PROMETHEUS_PORT}' + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.listen-address=:${PROMETHEUS_PORT}" network_mode: host alertmanager: @@ -176,8 +277,8 @@ services: - ./config/alertmanager.yml:/etc/alertmanager/alertmanager.yml - ./config/email.tmpl:/etc/alertmanager/email.tmpl command: - - '--config.file=/etc/alertmanager/alertmanager.yml' - - '--web.listen-address=:${ALERTMANAGER_PORT}' + - "--config.file=/etc/alertmanager/alertmanager.yml" + - "--web.listen-address=:${ALERTMANAGER_PORT}" network_mode: host grafana: @@ -209,9 +310,8 @@ services: - /sys:/host/sys:ro - /:/rootfs:ro command: - - '--path.procfs=/host/proc' - - '--path.sysfs=/host/sys' - - '--path.rootfs=/rootfs' - - '--web.listen-address=:19100' + - "--path.procfs=/host/proc" + - "--path.sysfs=/host/sys" + - "--path.rootfs=/rootfs" + - "--web.listen-address=:19100" network_mode: host - From 8f7b02979d3ffb7530005e5352bce53f6101fe18 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Wed, 18 Jun 2025 14:31:09 +0800 Subject: [PATCH 49/59] feat: support redis sentinel. (#3423) * feat: support redis sentinel. * update docker compose contents. * update config contents. * revert content. * supoort redisMode. * update config. * remvove print. --- config/redis.yml | 13 ++++-- go.mod | 2 +- go.sum | 4 +- pkg/common/config/config.go | 44 ++++++++++++------- pkg/common/discovery/discoveryregister.go | 2 +- pkg/common/storage/cache/redis/online_test.go | 2 +- tools/seq/internal/seq.go | 12 +++-- 7 files changed, 52 insertions(+), 27 deletions(-) diff --git a/config/redis.yml b/config/redis.yml index 2448bcb5c..5e62719ae 100644 --- a/config/redis.yml +++ b/config/redis.yml @@ -1,7 +1,14 @@ -address: [ localhost:16379 ] -username: +address: [localhost:16379] +username: password: openIM123 -clusterMode: false +# redis Mode, including "standalone","cluster","sentinel" +redisMode: "standalone" db: 0 maxRetry: 10 poolSize: 100 +# Sentinel configuration (only used when redisMode is "sentinel") +sentinelMode: + masterName: "redis-master" + sentinelsAddrs: ["127.0.0.1:26379", "127.0.0.1:26380", "127.0.0.1:26381"] + routeByLatency: true + routeRandomly: true diff --git a/go.mod b/go.mod index 845f75bb2..0ecd3a113 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/openimsdk/protocol v0.0.73-alpha.12 - github.com/openimsdk/tools v0.0.50-alpha.85 + github.com/openimsdk/tools v0.0.50-alpha.91 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index b775a1056..d8eb07f80 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5b github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.73-alpha.12 h1:2NYawXeHChYUeSme6QJ9pOLh+Empce2WmwEtbP4JvKk= github.com/openimsdk/protocol v0.0.73-alpha.12/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= -github.com/openimsdk/tools v0.0.50-alpha.85 h1:OqTUYx6r7Zp/eH8FKB08XeNjPV405TUIG9QT6QQ+F+s= -github.com/openimsdk/tools v0.0.50-alpha.85/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= +github.com/openimsdk/tools v0.0.50-alpha.91 h1:4zXtTwwCIUawet1VDvnD3C/1E4N4ostDfh+RfL5nz90= +github.com/openimsdk/tools v0.0.50-alpha.91/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go index cd57a11cf..619571064 100644 --- a/pkg/common/config/config.go +++ b/pkg/common/config/config.go @@ -323,14 +323,22 @@ type RPC struct { } type Redis struct { - Disable bool `yaml:"-"` - Address []string `yaml:"address"` - Username string `yaml:"username"` - Password string `yaml:"password"` - ClusterMode bool `yaml:"clusterMode"` - DB int `yaml:"db"` - MaxRetry int `yaml:"maxRetry"` - PoolSize int `yaml:"poolSize"` + Disable bool `yaml:"-"` + Address []string `yaml:"address"` + Username string `yaml:"username"` + Password string `yaml:"password"` + RedisMode string `yaml:"redisMode"` + DB int `yaml:"db"` + MaxRetry int `yaml:"maxRetry"` + PoolSize int `yaml:"poolSize"` + SentinelMode Sentinel `yaml:"sentinelMode"` +} + +type Sentinel struct { + MasterName string `yaml:"masterName"` + SentinelAddrs []string `yaml:"sentinelsAddrs"` + RouteByLatency bool `yaml:"routeByLatency"` + RouteRandomly bool `yaml:"routeRandomly"` } type BeforeConfig struct { @@ -487,13 +495,19 @@ func (m *Mongo) Build() *mongoutil.Config { func (r *Redis) Build() *redisutil.Config { return &redisutil.Config{ - ClusterMode: r.ClusterMode, - Address: r.Address, - Username: r.Username, - Password: r.Password, - DB: r.DB, - MaxRetry: r.MaxRetry, - PoolSize: r.PoolSize, + RedisMode: r.RedisMode, + Address: r.Address, + Username: r.Username, + Password: r.Password, + DB: r.DB, + MaxRetry: r.MaxRetry, + PoolSize: r.PoolSize, + Sentinel: &redisutil.Sentinel{ + MasterName: r.SentinelMode.MasterName, + SentinelAddrs: r.SentinelMode.SentinelAddrs, + RouteByLatency: r.SentinelMode.RouteByLatency, + RouteRandomly: r.SentinelMode.RouteRandomly, + }, } } diff --git a/pkg/common/discovery/discoveryregister.go b/pkg/common/discovery/discoveryregister.go index dc100be5c..87333fcac 100644 --- a/pkg/common/discovery/discoveryregister.go +++ b/pkg/common/discovery/discoveryregister.go @@ -35,7 +35,7 @@ func NewDiscoveryRegister(discovery *config.Discovery, watchNames []string) (dis return standalone.GetSvcDiscoveryRegistry(), nil } if runtimeenv.RuntimeEnvironment() == config.KUBERNETES { - return kubernetes.NewKubernetesConnManager(discovery.Kubernetes.Namespace, + return kubernetes.NewConnManager(discovery.Kubernetes.Namespace, nil, grpc.WithDefaultCallOptions( grpc.MaxCallSendMsgSize(1024*1024*20), ), diff --git a/pkg/common/storage/cache/redis/online_test.go b/pkg/common/storage/cache/redis/online_test.go index 0306f6f5d..d2ac283e0 100644 --- a/pkg/common/storage/cache/redis/online_test.go +++ b/pkg/common/storage/cache/redis/online_test.go @@ -26,7 +26,7 @@ func TestName111111(t *testing.T) { "172.16.8.124:7005", "172.16.8.124:7006", }, - ClusterMode: true, + RedisMode: "cluster", Password: "passwd123", //Address: []string{"localhost:16379"}, //Password: "openIM123", diff --git a/tools/seq/internal/seq.go b/tools/seq/internal/seq.go index 9fd352a96..e90c4a41b 100644 --- a/tools/seq/internal/seq.go +++ b/tools/seq/internal/seq.go @@ -28,6 +28,8 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +const StructTagName = "yaml" + const ( MaxSeq = "MAX_SEQ:" MinSeq = "MIN_SEQ:" @@ -54,13 +56,14 @@ func readConfig[T any](dir string, name string) (*T, error) { if err := v.ReadInConfig(); err != nil { return nil, err } - fn := func(config *mapstructure.DecoderConfig) { - config.TagName = "mapstructure" - } + var conf T - if err := v.Unmarshal(&conf, fn); err != nil { + if err := v.Unmarshal(&conf, func(config *mapstructure.DecoderConfig) { + config.TagName = StructTagName + }); err != nil { return nil, err } + return &conf, nil } @@ -69,6 +72,7 @@ func Main(conf string, del time.Duration) error { if err != nil { return err } + mongodbConfig, err := readConfig[config.Mongo](conf, config.MongodbConfigFileName) if err != nil { return err From 53bf8acc213420eca8dcbf94f4978afefcb71153 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Wed, 18 Jun 2025 14:32:29 +0800 Subject: [PATCH 50/59] fix: solve webhook incorrect attentionID references. (#3411) --- config/webhooks.yml | 4 +- internal/msgtransfer/callback.go | 10 +++- internal/rpc/msg/callback.go | 81 ++++++++++++++++---------------- 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/config/webhooks.yml b/config/webhooks.yml index 283a23ed4..9fd3eb339 100644 --- a/config/webhooks.yml +++ b/config/webhooks.yml @@ -16,7 +16,7 @@ afterUpdateUserInfoEx: afterSendSingleMsg: enable: false timeout: 5 - # Only the recvID specified in attentionIds will send the callback + # Only the recvIDs specified in attentionIds will send the callback # if not set, all user messages will be callback attentionIds: [] # See beforeSendSingleMsg comment. @@ -36,7 +36,7 @@ beforeMsgModify: afterSendGroupMsg: enable: false timeout: 5 - # Only the recvID specified in attentionIds will send the callback + # Only the GroupIDs specified in attentionIds will send the callback # if not set, all user messages will be callback attentionIds: [] # See beforeSendSingleMsg comment. diff --git a/internal/msgtransfer/callback.go b/internal/msgtransfer/callback.go index ea51c2839..f0d439779 100644 --- a/internal/msgtransfer/callback.go +++ b/internal/msgtransfer/callback.go @@ -55,9 +55,11 @@ func (mc *OnlineHistoryMongoConsumerHandler) webhookAfterSendSingleMsg(ctx conte if msg.ContentType == constant.Typing { return } + if !filterAfterMsg(msg, after) { return } + cbReq := &cbapi.CallbackAfterSendSingleMsgReq{ CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendSingleMsgCommand), RecvID: msg.RecvID, @@ -69,9 +71,11 @@ func (mc *OnlineHistoryMongoConsumerHandler) webhookAfterSendGroupMsg(ctx contex if msg.ContentType == constant.Typing { return } + if !filterAfterMsg(msg, after) { return } + cbReq := &cbapi.CallbackAfterSendGroupMsgReq{ CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendGroupMsgCommand), GroupID: msg.GroupID, @@ -98,7 +102,11 @@ func filterAfterMsg(msg *sdkws.MsgData, after *config.AfterConfig) bool { func filterMsg(msg *sdkws.MsgData, attentionIds []string, deniedTypes []int32) bool { // According to the attentionIds configuration, only some users are sent - if len(attentionIds) != 0 && !datautil.Contain(msg.RecvID, attentionIds...) { + if len(attentionIds) != 0 && msg.ContentType == constant.SingleChatType && !datautil.Contain(msg.RecvID, attentionIds...) { + return false + } + + if len(attentionIds) != 0 && msg.ContentType == constant.ReadGroupChatType && !datautil.Contain(msg.GroupID, attentionIds...) { return false } diff --git a/internal/rpc/msg/callback.go b/internal/rpc/msg/callback.go index 5bc98de0c..2c00efd43 100644 --- a/internal/rpc/msg/callback.go +++ b/internal/rpc/msg/callback.go @@ -16,13 +16,10 @@ package msg import ( "context" - "encoding/base64" "encoding/json" - "github.com/openimsdk/open-im-server/v3/pkg/apistruct" "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" "github.com/openimsdk/tools/errs" - "github.com/openimsdk/tools/utils/stringutil" cbapi "github.com/openimsdk/open-im-server/v3/pkg/callbackstruct" "github.com/openimsdk/open-im-server/v3/pkg/common/config" @@ -89,19 +86,20 @@ func (m *msgServer) webhookBeforeSendSingleMsg(ctx context.Context, before *conf }) } -func (m *msgServer) webhookAfterSendSingleMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) { - if msg.MsgData.ContentType == constant.Typing { - return - } - if !filterAfterMsg(msg, after) { - return - } - cbReq := &cbapi.CallbackAfterSendSingleMsgReq{ - CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendSingleMsgCommand), - RecvID: msg.MsgData.RecvID, - } - m.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendSingleMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData)) -} +// Move to msgtransfer +// func (m *msgServer) webhookAfterSendSingleMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) { +// if msg.MsgData.ContentType == constant.Typing { +// return +// } +// if !filterAfterMsg(msg, after) { +// return +// } +// cbReq := &cbapi.CallbackAfterSendSingleMsgReq{ +// CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendSingleMsgCommand), +// RecvID: msg.MsgData.RecvID, +// } +// m.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendSingleMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData)) +// } func (m *msgServer) webhookBeforeSendGroupMsg(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq) error { return webhook.WithCondition(ctx, before, func(ctx context.Context) error { @@ -123,20 +121,21 @@ func (m *msgServer) webhookBeforeSendGroupMsg(ctx context.Context, before *confi }) } -func (m *msgServer) webhookAfterSendGroupMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) { - if msg.MsgData.ContentType == constant.Typing { - return - } - if !filterAfterMsg(msg, after) { - return - } - cbReq := &cbapi.CallbackAfterSendGroupMsgReq{ - CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendGroupMsgCommand), - GroupID: msg.MsgData.GroupID, - } - - m.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendGroupMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData)) -} +// Move to msgtransfer +// func (m *msgServer) webhookAfterSendGroupMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) { +// if msg.MsgData.ContentType == constant.Typing { +// return +// } +// if !filterAfterMsg(msg, after) { +// return +// } +// cbReq := &cbapi.CallbackAfterSendGroupMsgReq{ +// CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackAfterSendGroupMsgCommand), +// GroupID: msg.MsgData.GroupID, +// } + +// m.webhookClient.AsyncPostWithQuery(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterSendGroupMsgResp{}, after, buildKeyMsgDataQuery(msg.MsgData)) +// } func (m *msgServer) webhookBeforeMsgModify(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq, beforeMsgData **sdkws.MsgData) error { return webhook.WithCondition(ctx, before, func(ctx context.Context) error { @@ -205,14 +204,14 @@ func (m *msgServer) webhookAfterRevokeMsg(ctx context.Context, after *config.Aft m.webhookClient.AsyncPost(ctx, callbackReq.GetCallbackCommand(), callbackReq, &cbapi.CallbackAfterRevokeMsgResp{}, after) } -func buildKeyMsgDataQuery(msg *sdkws.MsgData) map[string]string { - keyMsgData := apistruct.KeyMsgData{ - SendID: msg.SendID, - RecvID: msg.RecvID, - GroupID: msg.GroupID, - } - - return map[string]string{ - webhook.Key: base64.StdEncoding.EncodeToString(stringutil.StructToJsonBytes(keyMsgData)), - } -} +// func buildKeyMsgDataQuery(msg *sdkws.MsgData) map[string]string { +// keyMsgData := apistruct.KeyMsgData{ +// SendID: msg.SendID, +// RecvID: msg.RecvID, +// GroupID: msg.GroupID, +// } + +// return map[string]string{ +// webhook.Key: base64.StdEncoding.EncodeToString(stringutil.StructToJsonBytes(keyMsgData)), +// } +// } From 6912ad6f332e42b4b729783692e5971fdf00e7da Mon Sep 17 00:00:00 2001 From: chao <48119764+withchao@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:51:35 +0800 Subject: [PATCH 51/59] feat: add api logger (#3427) * pb * fix: Modifying other fields while setting IsPrivateChat does not take effect * fix: quote message error revoke * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * refactoring scheduled tasks * upgrading pkg tools * fix * fix * optimize log output * feat: support GetLastMessage * feat: support GetLastMessage * feat: s3 switch * feat: s3 switch * fix: GetUsersOnline * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: SendBusinessNotification supported configuration parameters * feat: seq conversion failed without exiting * fix: DeleteDoc crash * fix: fill send time * fix: fill send time * fix: crash caused by withdrawing messages from users who have left the group * fix: user msg timestamp * seq read config * seq read config * fix: the source message of the reference is withdrawn, and the referenced message is deleted * feat: optimize the default notification.yml * fix: shouldPushOffline * fix: the sorting is wrong after canceling the administrator in group settings * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * feat: Sending messages supports returning fields modified by webhook * fix: oss specifies content-type when uploading * fix: the version number contains a line break * fix: the version number contains a line break * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * feat: GetConversationsHasReadAndMaxSeq support pinned * fix: transferring the group owner to a muted member, incremental version error * feat: unified conversion code * feat: update gomake * fix: in standalone mode, the user online status is wrong * fix: add permission check * fix: add permission check * fix: add rpc interface permission check * fix: CreateGroupChatConversations * feat: optimize friend and group applications * feat: optimize friend and group applications * feat: optimize friend and group applications * feat: optimize friend and group applications * fix: optimize friend and group applications * feat: add api logger --- go.mod | 2 +- go.sum | 4 ++-- internal/api/router.go | 3 ++- internal/tools/cron/cron_task.go | 22 ++++++++++++++++++---- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 0ecd3a113..2a05b14b6 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/mitchellh/mapstructure v1.5.0 github.com/openimsdk/protocol v0.0.73-alpha.12 - github.com/openimsdk/tools v0.0.50-alpha.91 + github.com/openimsdk/tools v0.0.50-alpha.92 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index d8eb07f80..db68ca960 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,8 @@ github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5b github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.73-alpha.12 h1:2NYawXeHChYUeSme6QJ9pOLh+Empce2WmwEtbP4JvKk= github.com/openimsdk/protocol v0.0.73-alpha.12/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= -github.com/openimsdk/tools v0.0.50-alpha.91 h1:4zXtTwwCIUawet1VDvnD3C/1E4N4ostDfh+RfL5nz90= -github.com/openimsdk/tools v0.0.50-alpha.91/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= +github.com/openimsdk/tools v0.0.50-alpha.92 h1:hWfykMhmi7EQEiwgQccJqbgggIuhun/PrVkBnjmj9Ec= +github.com/openimsdk/tools v0.0.50-alpha.92/go.mod h1:n2poR3asX1e1XZce4O+MOWAp+X02QJRFvhcLCXZdzRo= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/internal/api/router.go b/internal/api/router.go index fcad104b8..43456f6d6 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -28,6 +28,7 @@ import ( "github.com/openimsdk/tools/discovery/etcd" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mw" + "github.com/openimsdk/tools/mw/api" clientv3 "go.etcd.io/etcd/client/v3" ) @@ -96,7 +97,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, cf case BestSpeed: r.Use(gzip.Gzip(gzip.BestSpeed)) } - r.Use(prommetricsGin(), gin.RecoveryWithWriter(gin.DefaultErrorWriter, mw.GinPanicErr), mw.CorsHandler(), + r.Use(api.GinLogger(), prommetricsGin(), gin.RecoveryWithWriter(gin.DefaultErrorWriter, mw.GinPanicErr), mw.CorsHandler(), mw.GinParseOperationID(), GinParseToken(rpcli.NewAuthClient(authConn)), setGinIsAdmin(cfg.Share.IMAdminUserID)) u := NewUserApi(user.NewUserClient(userConn), client, cfg.Discovery.RpcService) diff --git a/internal/tools/cron/cron_task.go b/internal/tools/cron/cron_task.go index abe596192..a6740ab6e 100644 --- a/internal/tools/cron/cron_task.go +++ b/internal/tools/cron/cron_task.go @@ -49,6 +49,7 @@ func Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegis return err } + var locker Locker if conf.Discovery.Enable == config.ETCD { cm := disetcd.NewConfigManager(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient(), []string{ conf.CronTask.GetConfigFileName(), @@ -56,11 +57,14 @@ func Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegis conf.Discovery.GetConfigFileName(), }) cm.Watch(ctx) + locker, err = NewEtcdLocker(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient()) + if err != nil { + return err + } } - locker, err := NewEtcdLocker(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient()) - if err != nil { - return err + if locker == nil { + locker = emptyLocker{} } srv := &cronServer{ @@ -92,6 +96,16 @@ func Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegis return nil } +type Locker interface { + ExecuteWithLock(ctx context.Context, taskName string, task func()) +} + +type emptyLocker struct{} + +func (emptyLocker) ExecuteWithLock(ctx context.Context, taskName string, task func()) { + task() +} + type cronServer struct { ctx context.Context config *Config @@ -99,7 +113,7 @@ type cronServer struct { msgClient msg.MsgClient conversationClient pbconversation.ConversationClient thirdClient third.ThirdClient - locker *EtcdLocker + locker Locker } func (c *cronServer) registerClearS3() error { From a5ac7f2a8117ec6b283c74a9d6a4db578a2bdd37 Mon Sep 17 00:00:00 2001 From: icey-yu <119291641+icey-yu@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:33:19 +0800 Subject: [PATCH 52/59] feat: add nickname for adminUser (#3435) * feat: add nickname for adminUser * feat: add nickname for adminUser * feat: add nickname for adminUser --- config/share.yml | 9 ++++++++- internal/api/router.go | 6 +++--- internal/msggateway/ws_server.go | 2 +- internal/push/push_handler.go | 4 ++-- internal/rpc/auth/auth.go | 12 +++++++----- internal/rpc/group/group.go | 4 +++- internal/rpc/msg/revoke.go | 4 ++-- internal/rpc/msg/server.go | 4 +++- internal/rpc/msg/verify.go | 4 ++-- internal/rpc/user/user.go | 13 ++++++++++--- internal/tools/cron/cron_task.go | 3 +-- pkg/common/config/config.go | 7 +++++-- pkg/common/startrpc/start.go | 4 ++-- pkg/common/storage/controller/user.go | 24 +++++++++++++++++++++--- test/stress-test-v2/main.go | 2 +- test/stress-test/main.go | 2 +- 16 files changed, 72 insertions(+), 32 deletions(-) diff --git a/config/share.yml b/config/share.yml index 2e9821436..a42bdcdd7 100644 --- a/config/share.yml +++ b/config/share.yml @@ -1,6 +1,13 @@ secret: openIM123 -imAdminUserID: [imAdmin] +# imAdminUser: Configuration for instant messaging system administrators +imAdminUser: + # userIDs: List of administrator user IDs. + # Each entry here corresponds by index to the matching entry in the nicknames list below. + userIDs: [imAdmin] + # nicknames: List of administrator display names. + # Each entry here corresponds by index to the matching entry in the userIDs list above. + nicknames: [superAdmin] # 1: For Android, iOS, Windows, Mac, and web platforms, only one instance can be online at a time multiLogin: diff --git a/internal/api/router.go b/internal/api/router.go index 43456f6d6..8a4199581 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -98,7 +98,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, cf r.Use(gzip.Gzip(gzip.BestSpeed)) } r.Use(api.GinLogger(), prommetricsGin(), gin.RecoveryWithWriter(gin.DefaultErrorWriter, mw.GinPanicErr), mw.CorsHandler(), - mw.GinParseOperationID(), GinParseToken(rpcli.NewAuthClient(authConn)), setGinIsAdmin(cfg.Share.IMAdminUserID)) + mw.GinParseOperationID(), GinParseToken(rpcli.NewAuthClient(authConn)), setGinIsAdmin(cfg.Share.IMAdminUser.UserIDs)) u := NewUserApi(user.NewUserClient(userConn), client, cfg.Discovery.RpcService) { @@ -232,7 +232,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, cf objectGroup.GET("/*name", t.ObjectRedirect) } // Message - m := NewMessageApi(msg.NewMsgClient(msgConn), rpcli.NewUserClient(userConn), cfg.Share.IMAdminUserID) + m := NewMessageApi(msg.NewMsgClient(msgConn), rpcli.NewUserClient(userConn), cfg.Share.IMAdminUser.UserIDs) { msgGroup := r.Group("/msg") msgGroup.POST("/newest_seq", m.GetSeq) @@ -309,7 +309,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, cf if cfg.Discovery.Enable == config.ETCD { etcdClient = client.(*etcd.SvcDiscoveryRegistryImpl).GetClient() } - cm := NewConfigManager(cfg.Share.IMAdminUserID, &cfg.AllConfig, etcdClient, string(cfg.ConfigPath)) + cm := NewConfigManager(cfg.Share.IMAdminUser.UserIDs, &cfg.AllConfig, etcdClient, string(cfg.ConfigPath)) { configGroup := r.Group("/config", cm.CheckAdmin) configGroup.POST("/get_config_list", cm.GetConfigList) diff --git a/internal/msggateway/ws_server.go b/internal/msggateway/ws_server.go index 211482cb1..9b7343938 100644 --- a/internal/msggateway/ws_server.go +++ b/internal/msggateway/ws_server.go @@ -130,7 +130,7 @@ func NewWsServer(msgGatewayConfig *Config, opts ...Option) *WsServer { for _, o := range opts { o(&config) } - //userRpcClient := rpcclient.NewUserRpcClient(client, config.Discovery.RpcService.User, config.Share.IMAdminUserID) + //userRpcClient := rpcclient.NewUserRpcClient(client, config.Discovery.RpcService.User, config.Share.IMAdminUser) v := validator.New() return &WsServer{ diff --git a/internal/push/push_handler.go b/internal/push/push_handler.go index 7b1efe3bf..4f1964084 100644 --- a/internal/push/push_handler.go +++ b/internal/push/push_handler.go @@ -317,8 +317,8 @@ func (c *ConsumerHandler) groupMessagesHandler(ctx context.Context, groupID stri return err } log.ZDebug(ctx, "GroupDismissedNotificationInfo****", "groupID", groupID, "num", len(*pushToUserIDs), "list", pushToUserIDs) - if len(c.config.Share.IMAdminUserID) > 0 { - ctx = mcontext.WithOpUserIDContext(ctx, c.config.Share.IMAdminUserID[0]) + if len(c.config.Share.IMAdminUser.UserIDs) > 0 { + ctx = mcontext.WithOpUserIDContext(ctx, c.config.Share.IMAdminUser.UserIDs[0]) } defer func(groupID string) { if err := c.groupClient.DismissGroup(ctx, groupID, true); err != nil { diff --git a/internal/rpc/auth/auth.go b/internal/rpc/auth/auth.go index 5ed9cdf12..7a8607164 100644 --- a/internal/rpc/auth/auth.go +++ b/internal/rpc/auth/auth.go @@ -49,6 +49,7 @@ type authServer struct { RegisterCenter discovery.Conn config *Config userClient *rpcli.UserClient + adminUserIDs []string } type Config struct { @@ -90,10 +91,11 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg config.Share.Secret, config.RpcConfig.TokenPolicy.Expire, config.Share.MultiLogin, - config.Share.IMAdminUserID, + config.Share.IMAdminUser.UserIDs, ), - config: config, - userClient: rpcli.NewUserClient(userConn), + config: config, + userClient: rpcli.NewUserClient(userConn), + adminUserIDs: config.Share.IMAdminUser.UserIDs, }) return nil } @@ -104,8 +106,8 @@ func (s *authServer) GetAdminToken(ctx context.Context, req *pbauth.GetAdminToke return nil, errs.ErrNoPermission.WrapMsg("secret invalid") } - if !datautil.Contain(req.UserID, s.config.Share.IMAdminUserID...) { - return nil, errs.ErrArgs.WrapMsg("userID is error.", "userID", req.UserID, "adminUserID", s.config.Share.IMAdminUserID) + if !datautil.Contain(req.UserID, s.adminUserIDs...) { + return nil, errs.ErrArgs.WrapMsg("userID is error.", "userID", req.UserID, "adminUserID", s.adminUserIDs) } diff --git a/internal/rpc/group/group.go b/internal/rpc/group/group.go index ce8a2c7aa..3700c054a 100644 --- a/internal/rpc/group/group.go +++ b/internal/rpc/group/group.go @@ -63,6 +63,7 @@ type groupServer struct { userClient *rpcli.UserClient msgClient *rpcli.MsgClient conversationClient *rpcli.ConversationClient + adminUserIDs []string } type Config struct { @@ -116,6 +117,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg userClient: rpcli.NewUserClient(userConn), msgClient: rpcli.NewMsgClient(msgConn), conversationClient: rpcli.NewConversationClient(conversationConn), + adminUserIDs: config.Share.IMAdminUser.UserIDs, } gs.db = controller.NewGroupDatabase(rdb, &config.LocalCacheConfig, groupDB, groupMemberDB, groupRequestDB, mgocli.GetTx(), grouphash.NewGroupHashFromGroupServer(&gs)) gs.notification = NewNotificationSender(gs.db, config, gs.userClient, gs.msgClient, gs.conversationClient) @@ -1901,7 +1903,7 @@ func (g *groupServer) GetSpecifiedUserGroupRequestInfo(ctx context.Context, req } adminIDs = append(adminIDs, owners[0].UserID) - adminIDs = append(adminIDs, g.config.Share.IMAdminUserID...) + adminIDs = append(adminIDs, g.adminUserIDs...) if !datautil.Contain(opUserID, adminIDs...) { return nil, errs.ErrNoPermission.WrapMsg("opUser no permission") diff --git a/internal/rpc/msg/revoke.go b/internal/rpc/msg/revoke.go index bd1d66ba1..1f4d6be68 100644 --- a/internal/rpc/msg/revoke.go +++ b/internal/rpc/msg/revoke.go @@ -109,8 +109,8 @@ func (m *msgServer) RevokeMsg(ctx context.Context, req *msg.RevokeMsgReq) (*msg. revokerUserID := mcontext.GetOpUserID(ctx) var flag bool - if len(m.config.Share.IMAdminUserID) > 0 { - flag = datautil.Contain(revokerUserID, m.config.Share.IMAdminUserID...) + if len(m.config.Share.IMAdminUser.UserIDs) > 0 { + flag = datautil.Contain(revokerUserID, m.adminUserIDs...) } tips := sdkws.RevokeMsgTips{ RevokerUserID: revokerUserID, diff --git a/internal/rpc/msg/server.go b/internal/rpc/msg/server.go index 9d5391cc9..48101cdd7 100644 --- a/internal/rpc/msg/server.go +++ b/internal/rpc/msg/server.go @@ -22,7 +22,6 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/dbbuild" "github.com/openimsdk/open-im-server/v3/pkg/mqbuild" "github.com/openimsdk/open-im-server/v3/pkg/rpcli" - "google.golang.org/grpc" "github.com/openimsdk/open-im-server/v3/pkg/common/config" @@ -71,6 +70,8 @@ type msgServer struct { config *Config // Global configuration settings. webhookClient *webhook.Client conversationClient *rpcli.ConversationClient + + adminUserIDs []string } func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) { @@ -145,6 +146,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg config: config, webhookClient: webhook.NewWebhookClient(config.WebhooksConfig.URL), conversationClient: conversationClient, + adminUserIDs: config.Share.IMAdminUser.UserIDs, } s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg)) diff --git a/internal/rpc/msg/verify.go b/internal/rpc/msg/verify.go index 213545d83..d0f36a388 100644 --- a/internal/rpc/msg/verify.go +++ b/internal/rpc/msg/verify.go @@ -54,7 +54,7 @@ type MessageRevoked struct { func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgReq) error { switch data.MsgData.SessionType { case constant.SingleChatType: - if datautil.Contain(data.MsgData.SendID, m.config.Share.IMAdminUserID...) { + if datautil.Contain(data.MsgData.SendID, m.adminUserIDs...) { return nil } if data.MsgData.ContentType <= constant.NotificationEnd && @@ -102,7 +102,7 @@ func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgRe return nil } - if datautil.Contain(data.MsgData.SendID, m.config.Share.IMAdminUserID...) { + if datautil.Contain(data.MsgData.SendID, m.adminUserIDs...) { return nil } if data.MsgData.ContentType <= constant.NotificationEnd && diff --git a/internal/rpc/user/user.go b/internal/rpc/user/user.go index 28461ae0a..91dfe736e 100644 --- a/internal/rpc/user/user.go +++ b/internal/rpc/user/user.go @@ -65,6 +65,8 @@ type userServer struct { groupClient *rpcli.GroupClient relationClient *rpcli.RelationClient clientConfig controller.ClientConfigDatabase + + adminUserIDs []string } type Config struct { @@ -92,8 +94,12 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg users := make([]*tablerelation.User, 0) - for _, v := range config.Share.IMAdminUserID { - users = append(users, &tablerelation.User{UserID: v, Nickname: v, AppMangerLevel: constant.AppAdmin}) + for i := range config.Share.IMAdminUser.UserIDs { + users = append(users, &tablerelation.User{ + UserID: config.Share.IMAdminUser.UserIDs[i], + Nickname: config.Share.IMAdminUser.Nicknames[i], + AppMangerLevel: constant.AppAdmin, + }) } userDB, err := mgo.NewUserMongo(mgocli.GetDB()) if err != nil { @@ -130,6 +136,7 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg clientConfig: controller.NewClientConfigDatabase(clientConfigDB, redis.NewClientConfigCache(rdb, clientConfigDB), mgocli.GetTx()), groupClient: rpcli.NewGroupClient(groupConn), relationClient: rpcli.NewRelationClient(friendConn), + adminUserIDs: config.Share.IMAdminUser.UserIDs, } pbuser.RegisterUserServer(server, u) return u.db.InitOnce(context.Background(), users) @@ -648,7 +655,7 @@ func (s *userServer) userModelToResp(users []*tablerelation.User, pagination pag accounts := make([]*pbuser.NotificationAccountInfo, 0) var total int64 for _, v := range users { - if v.AppMangerLevel >= constant.AppNotificationAdmin && !datautil.Contain(v.UserID, s.config.Share.IMAdminUserID...) { + if v.AppMangerLevel >= constant.AppNotificationAdmin && !datautil.Contain(v.UserID, s.adminUserIDs...) { if appManagerLevel != nil { if v.AppMangerLevel != *appManagerLevel { continue diff --git a/internal/tools/cron/cron_task.go b/internal/tools/cron/cron_task.go index a6740ab6e..2c8655d4a 100644 --- a/internal/tools/cron/cron_task.go +++ b/internal/tools/cron/cron_task.go @@ -10,7 +10,6 @@ import ( "github.com/openimsdk/protocol/third" "github.com/openimsdk/tools/discovery" "github.com/openimsdk/tools/discovery/etcd" - "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" @@ -32,7 +31,7 @@ func Start(ctx context.Context, conf *Config, client discovery.SvcDiscoveryRegis <-ctx.Done() return nil } - ctx = mcontext.SetOpUserID(ctx, conf.Share.IMAdminUserID[0]) + ctx = mcontext.SetOpUserID(ctx, conf.Share.IMAdminUser.UserIDs[0]) msgConn, err := client.GetConn(ctx, conf.Discovery.RpcService.Msg) if err != nil { diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go index 619571064..43914310e 100644 --- a/pkg/common/config/config.go +++ b/pkg/common/config/config.go @@ -356,8 +356,11 @@ type AfterConfig struct { } type Share struct { - Secret string `yaml:"secret"` - IMAdminUserID []string `yaml:"imAdminUserID"` + Secret string `yaml:"secret"` + IMAdminUser struct { + UserIDs []string `yaml:"userIDs"` + Nicknames []string `yaml:"nicknames"` + } `yaml:"imAdminUser"` MultiLogin MultiLogin `yaml:"multiLogin"` RPCMaxBodySize MaxRequestBody `yaml:"rpcMaxBodySize"` } diff --git a/pkg/common/startrpc/start.go b/pkg/common/startrpc/start.go index 06e19d8d2..9715f2aac 100644 --- a/pkg/common/startrpc/start.go +++ b/pkg/common/startrpc/start.go @@ -69,8 +69,8 @@ func Start[T any](ctx context.Context, disc *conf.Discovery, prometheusConfig *c grpcsrv.GrpcServerRequestValidate(), grpcsrv.GrpcServerPanicCapture(), ) - if shareConfig != nil && len(shareConfig.IMAdminUserID) > 0 { - options = append(options, grpcServerIMAdminUserID(shareConfig.IMAdminUserID)) + if shareConfig != nil && len(shareConfig.IMAdminUser.UserIDs) > 0 { + options = append(options, grpcServerIMAdminUserID(shareConfig.IMAdminUser.UserIDs)) } var clientOptions []grpc.DialOption if maxRequestBody != nil { diff --git a/pkg/common/storage/controller/user.go b/pkg/common/storage/controller/user.go index f97c5330b..d747ce2fa 100644 --- a/pkg/common/storage/controller/user.go +++ b/pkg/common/storage/controller/user.go @@ -97,16 +97,34 @@ func (u *userDatabase) InitOnce(ctx context.Context, users []*model.User) error } // Determine which users are missing from the database. - missingUsers := datautil.SliceAnySub(users, existingUsers, func(e *model.User) string { + var ( + missing, update []*model.User + ) + existMap := datautil.SliceToMap(existingUsers, func(e *model.User) string { return e.UserID }) + orgMap := datautil.SliceToMap(users, func(e *model.User) string { return e.UserID }) + for k, u1 := range orgMap { + if u2, ok := existMap[k]; !ok { + missing = append(missing, u1) + } else if u1.Nickname != u2.Nickname { + update = append(update, u1) + } + } // Create records for missing users. - if len(missingUsers) > 0 { - if err := u.userDB.Create(ctx, missingUsers); err != nil { + if len(missing) > 0 { + if err := u.userDB.Create(ctx, missing); err != nil { return err } } + if len(update) > 0 { + for i := range update { + if err := u.userDB.UpdateByMap(ctx, update[i].UserID, map[string]any{"nickname": update[i].Nickname}); err != nil { + return err + } + } + } return nil } diff --git a/test/stress-test-v2/main.go b/test/stress-test-v2/main.go index 0e4609964..63ce77065 100644 --- a/test/stress-test-v2/main.go +++ b/test/stress-test-v2/main.go @@ -446,7 +446,7 @@ func main() { Share: *share, Api: *apiConfig, }, - AdminUserID: share.IMAdminUserID[0], + AdminUserID: share.IMAdminUser.UserIDs[0], Ctx: ctx, Cancel: cancel, HttpClient: &http.Client{ diff --git a/test/stress-test/main.go b/test/stress-test/main.go index 6adbd12ee..d7822e51b 100755 --- a/test/stress-test/main.go +++ b/test/stress-test/main.go @@ -319,7 +319,7 @@ func main() { Share: *share, Api: *apiConfig, }, - AdminUserID: share.IMAdminUserID[0], + AdminUserID: share.IMAdminUser.UserIDs[0], Ctx: ctx, Cancel: cancel, HttpClient: &http.Client{ From bd56bd23b80796ac40d91e3985f053dc901672b0 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Tue, 8 Jul 2025 16:33:25 +0800 Subject: [PATCH 53/59] fix: update log level in crontask dist look. (#3440) --- internal/tools/cron/dist_look.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/internal/tools/cron/dist_look.go b/internal/tools/cron/dist_look.go index d1d2d1cb0..e46b9206c 100644 --- a/internal/tools/cron/dist_look.go +++ b/internal/tools/cron/dist_look.go @@ -52,15 +52,12 @@ func (e *EtcdLocker) ExecuteWithLock(ctx context.Context, taskName string, task err = mutex.TryLock(ctxWithTimeout) if err != nil { - if err == context.DeadlineExceeded { - log.ZDebug(ctx, "Task is being executed by another instance, skipping", - "taskName", taskName, - "instanceID", e.instanceID) - } else { - log.ZWarn(ctx, "Failed to acquire task lock", err, - "taskName", taskName, - "instanceID", e.instanceID) - } + // errors.Is(err, concurrency.ErrLocked) + log.ZDebug(ctx, "Task is being executed by another instance, skipping", + "taskName", taskName, + "instanceID", e.instanceID, + "error", err.Error()) + return } From 5e813ef07986bfef8ee33a4d9f5f95d015a58774 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Tue, 8 Jul 2025 16:33:39 +0800 Subject: [PATCH 54/59] fix: solve `createTime` not set in setConversation and Create Conversation. (#3447) --- internal/rpc/conversation/conversation.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/rpc/conversation/conversation.go b/internal/rpc/conversation/conversation.go index cf5a2b9c6..a2b72ddfb 100644 --- a/internal/rpc/conversation/conversation.go +++ b/internal/rpc/conversation/conversation.go @@ -31,7 +31,6 @@ import ( "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo" - "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" dbModel "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" "github.com/openimsdk/open-im-server/v3/pkg/common/webhook" "github.com/openimsdk/open-im-server/v3/pkg/localcache" @@ -244,11 +243,14 @@ func (c *conversationServer) getConversations(ctx context.Context, ownerUserID s return convert.ConversationsDB2Pb(conversations), nil } +// Deprecated func (c *conversationServer) SetConversation(ctx context.Context, req *pbconversation.SetConversationReq) (*pbconversation.SetConversationResp, error) { if err := authverify.CheckAccess(ctx, req.GetConversation().GetUserID()); err != nil { return nil, err } var conversation dbModel.Conversation + conversation.CreateTime = time.Now() + if err := datautil.CopyStructFields(&conversation, req.Conversation); err != nil { return nil, err } @@ -300,6 +302,7 @@ func (c *conversationServer) SetConversations(ctx context.Context, req *pbconver conversation.ConversationType = req.Conversation.ConversationType conversation.UserID = req.Conversation.UserID conversation.GroupID = req.Conversation.GroupID + conversation.CreateTime = time.Now() m, conversation, err := UpdateConversationsMap(ctx, req) if err != nil { @@ -365,6 +368,8 @@ func (c *conversationServer) UpdateConversationsByUser(ctx context.Context, req // create conversation without notification for msg redis transfer. func (c *conversationServer) CreateSingleChatConversations(ctx context.Context, req *pbconversation.CreateSingleChatConversationsReq) (*pbconversation.CreateSingleChatConversationsResp, error) { var conversation dbModel.Conversation + conversation.CreateTime = time.Now() + switch req.ConversationType { case constant.SingleChatType: // sendUser create @@ -372,6 +377,7 @@ func (c *conversationServer) CreateSingleChatConversations(ctx context.Context, conversation.ConversationType = req.ConversationType conversation.OwnerUserID = req.SendID conversation.UserID = req.RecvID + if err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue { return nil, err } @@ -387,6 +393,7 @@ func (c *conversationServer) CreateSingleChatConversations(ctx context.Context, conversation2 := conversation conversation2.OwnerUserID = req.RecvID conversation2.UserID = req.SendID + if err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue { return nil, err } @@ -402,6 +409,7 @@ func (c *conversationServer) CreateSingleChatConversations(ctx context.Context, conversation.ConversationType = req.ConversationType conversation.OwnerUserID = req.RecvID conversation.UserID = req.SendID + if err := c.webhookBeforeCreateSingleChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateSingleChatConversations, &conversation); err != nil && err != servererrs.ErrCallbackContinue { return nil, err } @@ -423,6 +431,7 @@ func (c *conversationServer) CreateGroupChatConversations(ctx context.Context, r conversation.ConversationID = msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, req.GroupID) conversation.GroupID = req.GroupID conversation.ConversationType = constant.ReadGroupChatType + conversation.CreateTime = time.Now() if err := c.webhookBeforeCreateGroupChatConversations(ctx, &c.config.WebhooksConfig.BeforeCreateGroupChatConversations, &conversation); err != nil { return nil, err @@ -708,7 +717,7 @@ func (c *conversationServer) GetConversationsNeedClearMsg(ctx context.Context, _ maxPage := (num + batchNum - 1) / batchNum - temp := make([]*model.Conversation, 0, maxPage*batchNum) + temp := make([]*dbModel.Conversation, 0, maxPage*batchNum) for pageNumber := 0; pageNumber < int(maxPage); pageNumber++ { pagination := &sdkws.RequestPagination{ From b1659ef84d0b6459794c37400094ff3101cea48e Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Tue, 22 Jul 2025 10:24:01 +0800 Subject: [PATCH 55/59] refactor: support modified config and args in mage. (#3466) * refactor: support modified config and args in mage. * fix: update go make version. --- go.mod | 2 +- go.sum | 4 ++-- magefile.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 2a05b14b6..badf4ffaa 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/kelindar/bitmap v1.5.2 github.com/likexian/gokit v0.25.13 - github.com/openimsdk/gomake v0.0.15-alpha.5 + github.com/openimsdk/gomake v0.0.15-alpha.11 github.com/redis/go-redis/v9 v9.4.0 github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil v3.21.11+incompatible diff --git a/go.sum b/go.sum index db68ca960..ab0499ba0 100644 --- a/go.sum +++ b/go.sum @@ -345,8 +345,8 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/openimsdk/gomake v0.0.15-alpha.5 h1:eEZCEHm+NsmcO3onXZPIUbGFCYPYbsX5beV3ZyOsGhY= -github.com/openimsdk/gomake v0.0.15-alpha.5/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= +github.com/openimsdk/gomake v0.0.15-alpha.11 h1:PQudYDRESYeYlUYrrLLJhYIlUPO5x7FAx+o5El9U/Bw= +github.com/openimsdk/gomake v0.0.15-alpha.11/go.mod h1:PndCozNc2IsQIciyn9mvEblYWZwJmAI+06z94EY+csI= github.com/openimsdk/protocol v0.0.73-alpha.12 h1:2NYawXeHChYUeSme6QJ9pOLh+Empce2WmwEtbP4JvKk= github.com/openimsdk/protocol v0.0.73-alpha.12/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw= github.com/openimsdk/tools v0.0.50-alpha.92 h1:hWfykMhmi7EQEiwgQccJqbgggIuhun/PrVkBnjmj9Ec= diff --git a/magefile.go b/magefile.go index d0e77f6fc..b9861bd5e 100644 --- a/magefile.go +++ b/magefile.go @@ -12,15 +12,48 @@ import ( var Default = Build +var Aliases = map[string]any{ + "buildcc": BuildWithCustomConfig, + "startcc": StartWithCustomConfig, +} + +var ( + customRootDir = "." // workDir in mage, default is "./"(project root directory) + customSrcDir = "cmd" // source code directory, default is "cmd" + customOutputDir = "_output" // output directory, default is "_output" + customConfigDir = "config" // configuration directory, default is "config" + customToolsDir = "tools" // tools source code directory, default is "tools" +) + + +// Build support specifical binary build. +// +// Example: `mage build openim-api openim-rpc-user seq` func Build() { flag.Parse() + bin := flag.Args() + if len(bin) != 0 { + bin = bin[1:] + } + mageutil.Build(bin, nil) +} + +func BuildWithCustomConfig() { + flag.Parse() bin := flag.Args() if len(bin) != 0 { bin = bin[1:] } - mageutil.Build(bin) + config := &mageutil.PathOptions{ + RootDir: &customRootDir, + OutputDir: &customOutputDir, + SrcDir: &customSrcDir, + ToolsDir: &customToolsDir, + } + + mageutil.Build(bin, config) } func Start() { @@ -30,7 +63,37 @@ func Start() { mageutil.PrintRed("setMaxOpenFiles failed " + err.Error()) os.Exit(1) } - mageutil.StartToolsAndServices() + + flag.Parse() + bin := flag.Args() + if len(bin) != 0 { + bin = bin[1:] + } + + mageutil.StartToolsAndServices(bin, nil) +} + +func StartWithCustomConfig() { + mageutil.InitForSSC() + err := setMaxOpenFiles() + if err != nil { + mageutil.PrintRed("setMaxOpenFiles failed " + err.Error()) + os.Exit(1) + } + + flag.Parse() + bin := flag.Args() + if len(bin) != 0 { + bin = bin[1:] + } + + config := &mageutil.PathOptions{ + RootDir: &customRootDir, + OutputDir: &customOutputDir, + ConfigDir: &customConfigDir, + } + + mageutil.StartToolsAndServices(bin, config) } func Stop() { From 4ae05d2e38f3f6e3bcc62be59706f7298f7da84a Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Fri, 25 Jul 2025 15:02:49 +0800 Subject: [PATCH 56/59] fix: use safe submodule init in workflows. (#3468) * fix: use safe submodule init in workflows. * update to latest version. --- .../update-version-file-on-release.yml | 101 ++++++++++++------ 1 file changed, 68 insertions(+), 33 deletions(-) diff --git a/.github/workflows/update-version-file-on-release.yml b/.github/workflows/update-version-file-on-release.yml index 113537fd9..b7e2a4409 100644 --- a/.github/workflows/update-version-file-on-release.yml +++ b/.github/workflows/update-version-file-on-release.yml @@ -8,19 +8,40 @@ jobs: update-version: runs-on: ubuntu-latest env: - TAG_VERSION: ${{ github.event.release.tag_name }} + TAG_VERSION: ${{ github.event.release.tag_name }} steps: # Step 1: Checkout the original repository's code - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 + # submodules: "recursive" + + - name: Safe submodule initialization + run: | + echo "Checking for submodules..." + if [ -f .gitmodules ]; then + if [ -s .gitmodules ]; then + echo "Initializing submodules..." + if git submodule sync --recursive 2>/dev/null; then + git submodule update --init --force --recursive || { + echo "Warning: Some submodules failed to initialize, continuing anyway..." + } + else + echo "Warning: Submodule sync failed, continuing without submodules..." + fi + else + echo ".gitmodules exists but is empty, skipping submodule initialization" + fi + else + echo "No .gitmodules file found, no submodules to initialize" + fi # Step 2: Set up Git with official account - name: Set up Git run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" # Step 3: Check and delete existing tag - name: Check and delete existing tag @@ -33,7 +54,8 @@ jobs: # Step 4: Update version file - name: Update version file run: | - echo "${{ env.TAG_VERSION }}" > version/version + mkdir -p version + echo -n "${{ env.TAG_VERSION }}" > version/version # Step 5: Commit and push changes - name: Commit and push changes @@ -42,43 +64,56 @@ jobs: run: | git add version/version git commit -m "Update version to ${{ env.TAG_VERSION }}" - git push origin HEAD:${{ github.ref }} - # Step 6: Create and push tag - - name: Create and push tag - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Step 6: Update tag + - name: Update tag run: | - git tag ${{ env.TAG_VERSION }} - git push origin ${{ env.TAG_VERSION }} + git tag -fa ${{ env.TAG_VERSION }} -m "Update version to ${{ env.TAG_VERSION }}" + git push origin ${{ env.TAG_VERSION }} --force # Step 7: Find and Publish Draft Release - name: Find and Publish Draft Release - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - // Get the list of releases - const releases = await github.rest.repos.listReleases({ - owner: context.repo.owner, - repo: context.repo.repo - }); - - // Find the draft release where the title and tag_name are the same - const draftRelease = releases.data.find(release => - release.draft && release.name === release.tag_name - ); + const { owner, repo } = context.repo; + const tagName = process.env.TAG_VERSION; - if (draftRelease) { - // Publish the draft release using the release_id + try { + let release; + try { + const response = await github.rest.repos.getReleaseByTag({ + owner, + repo, + tag: tagName + }); + release = response.data; + } catch (tagError) { + core.info(`Release not found by tag, searching all releases...`); + const releases = await github.rest.repos.listReleases({ + owner, + repo, + per_page: 100 + }); + + release = releases.data.find(r => r.draft && r.tag_name === tagName); + if (!release) { + throw new Error(`No release found with tag ${tagName}`); + } + } + await github.rest.repos.updateRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: draftRelease.id, // Use release_id - draft: false + owner, + repo, + release_id: release.id, + draft: false, + prerelease: release.prerelease }); - - core.info(`Draft Release ${draftRelease.tag_name} published successfully.`); - } else { - core.info("No matching draft release found."); - } \ No newline at end of file + + const status = release.draft ? "was draft" : "was already published"; + core.info(`Release ${tagName} ensured to be published (${status}).`); + + } catch (error) { + core.warning(`Could not find or update release for tag ${tagName}: ${error.message}`); + } From 230c93cc16d8be8d6196ded250f9d774fa778eae Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Mon, 28 Jul 2025 10:46:23 +0800 Subject: [PATCH 57/59] docs: update slack link. (#3479) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/other.yml | 3 +- .github/workflows/auto-invite-comment.yml | 11 ++- .github/workflows/help-comment-issue.yml | 2 +- .github/workflows/user-first-interaction.yml | 10 +-- README.md | 40 +++++----- README_zh_CN.md | 54 ++++++------- docs/readme/README_cs.md | 36 ++++----- docs/readme/README_da.md | 45 +++++------ docs/readme/README_el.md | 39 +++++---- docs/readme/README_es.md | 48 +++++------ docs/readme/README_fa.md | 38 ++++----- docs/readme/README_fr.md | 42 +++++----- docs/readme/README_hu.md | 36 ++++----- docs/readme/README_ja.md | 83 +++++++++----------- docs/readme/README_ko.md | 45 +++++------ docs/readme/README_tr.md | 40 ++++------ docs/readme/README_uk.md | 36 ++++----- docs/readme/README_vi.md | 41 +++++----- install.sh | 2 +- 20 files changed, 288 insertions(+), 365 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index deb899083..6c91a8522 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,7 +4,7 @@ contact_links: # description: "Report a bug in the project" # file: "bug-report.yml" - name: 📢 Connect on slack - url: https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg + url: https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A about: Support OpenIM-related requests or issues, get in touch with developers and help on slack - name: 🌐 OpenIM Blog url: https://www.openim.io/ diff --git a/.github/ISSUE_TEMPLATE/other.yml b/.github/ISSUE_TEMPLATE/other.yml index 025440229..3a5690e8d 100644 --- a/.github/ISSUE_TEMPLATE/other.yml +++ b/.github/ISSUE_TEMPLATE/other.yml @@ -4,7 +4,6 @@ title: "[Other]: " labels: ["other"] # assignees: [] - body: - type: markdown attributes: @@ -26,5 +25,5 @@ body: - type: markdown attributes: value: | - You can also join our Discord community [here](https://join.slack.com/t/openimsdk/shared_invite/zt-1tmoj26uf-_FDy3dowVHBiGvLk9e5Xkg) + You can also join our Discord community [here](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) Feel free to check out other cool repositories of the openim Community [here](https://github.com/openimsdk) diff --git a/.github/workflows/auto-invite-comment.yml b/.github/workflows/auto-invite-comment.yml index 76fbcdfd3..dde36eaf1 100644 --- a/.github/workflows/auto-invite-comment.yml +++ b/.github/workflows/auto-invite-comment.yml @@ -11,7 +11,6 @@ jobs: permissions: issues: write steps: - - name: Invite user to join OpenIM Community uses: peter-evans/create-or-update-comment@v4 with: @@ -20,11 +19,11 @@ jobs: body: | We value close connections with our users, developers, and contributors here at Open-IM-Server. With a large community and maintainer team, we're always here to help and support you. Whether you're looking to join our community or have any questions or suggestions, we welcome you to get in touch with us. - Our most recommended way to get in touch is through [Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q). Even if you're in China, Slack is usually not blocked by firewalls, making it an easy way to connect with us. Our Slack community is the ideal place to discuss and share ideas and suggestions with other users and developers of Open-IM-Server. You can ask technical questions, seek help, or share your experiences with other users of Open-IM-Server. - + Our most recommended way to get in touch is through [Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A). Even if you're in China, Slack is usually not blocked by firewalls, making it an easy way to connect with us. Our Slack community is the ideal place to discuss and share ideas and suggestions with other users and developers of Open-IM-Server. You can ask technical questions, seek help, or share your experiences with other users of Open-IM-Server. + In addition to Slack, we also offer the following ways to get in touch: - - + We also have Slack channels for you to communicate and discuss. To join, visit https://slack.com/ and join our [👀 Open-IM-Server slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) team channel. + + + We also have Slack channels for you to communicate and discuss. To join, visit https://slack.com/ and join our [👀 Open-IM-Server slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) team channel. + Get in touch with us on [Gmail](https://mail.google.com/mail/u/0/?fs=1&tf=cm&to=winxu81@gmail.com). If you have any questions or issues that need resolving, or any suggestions and feedback for our open source projects, please feel free to contact us via email. + Read our [blog](https://doc.rentsoft.cn/). Our blog is a great place to stay up-to-date with Open-IM-Server projects and trends. On the blog, we share our latest developments, tech trends, and other interesting information. + Add [Wechat](https://github.com/OpenIMSDK/OpenIM-Docs/blob/main/docs/images/WechatIMG20.jpeg) and indicate that you are a user or developer of Open-IM-Server. We will process your request as soon as possible. @@ -36,4 +35,4 @@ jobs: # issue-number: ${{ github.event.issue.number }} # comment: 🤖 Auto-closing issue, if you still need help please reopen the issue or ask for help in the community above # labels: | - # accepted \ No newline at end of file + # accepted diff --git a/.github/workflows/help-comment-issue.yml b/.github/workflows/help-comment-issue.yml index b1cc62182..ce8f12b3b 100644 --- a/.github/workflows/help-comment-issue.yml +++ b/.github/workflows/help-comment-issue.yml @@ -32,5 +32,5 @@ jobs: token: ${{ secrets.BOT_TOKEN }} body: | This issue is available for anyone to work on. **Make sure to reference this issue in your pull request.** :sparkles: Thank you for your contribution! :sparkles: - [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) to connect and communicate with our developers. + [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) to connect and communicate with our developers. If you wish to accept this assignment, please leave a comment in the comments section: `/accept`.🎯 diff --git a/.github/workflows/user-first-interaction.yml b/.github/workflows/user-first-interaction.yml index 6999889eb..4271702a6 100644 --- a/.github/workflows/user-first-interaction.yml +++ b/.github/workflows/user-first-interaction.yml @@ -17,12 +17,12 @@ jobs: repo-token: ${{ secrets.BOT_TOKEN }} pr-message: | Hello! Thank you for your contribution. - + If you are fixing a bug, please reference the issue number in the description. If you are implementing a feature request, please check with the maintainers that the feature will be accepted first. - [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) to connect and communicate with our developers. + [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) to connect and communicate with our developers. Please leave your information in the [✨ discussions](https://github.com/orgs/OpenIMSDK/discussions/426), we expect anyone to join OpenIM developer community. @@ -30,6 +30,6 @@ jobs: Hello! Thank you for filing an issue. If this is a bug report, please include relevant logs to help us debug the problem. - - [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) to connect and communicate with our developers. - continue-on-error: true \ No newline at end of file + + [Join slack 🤖](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) to connect and communicate with our developers. + continue-on-error: true diff --git a/README.md b/README.md index e59bf20c4..3463e83b2 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,12 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20OpenIM%20Guru-006BFF?style=for-the-badge)](https://gurubase.io/g/openim) -

English · 中文 · @@ -47,16 +46,15 @@ Türkçe

-

## :busts_in_silhouette: Join Our Community -+ 💬 [Follow us on Twitter](https://twitter.com/founder_im63606) -+ 🚀 [Join our Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) -+ :eyes: [Join our WeChat Group](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 💬 [Follow us on Twitter](https://twitter.com/founder_im63606) +- 🚀 [Join our Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Join our WeChat Group](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## Ⓜ️ About OpenIM @@ -68,13 +66,14 @@ Unlike standalone chat applications such as Telegram, Signal, and Rocket.Chat, O **OpenIMSDK**, designed for **OpenIMServer**, is an IM SDK created specifically for integration into client applications. It supports various functionalities and modules: -+ 🌟 Main Features: +- 🌟 Main Features: + - 📦 Local Storage - 🔔 Listener Callbacks - 🛡️ API Wrapping - 🌐 Connection Management -+ 📚 Main Modules: +- 📚 Main Modules: 1. 🚀 Initialization and Login 2. 👤 User Management 3. 👫 Friends Management @@ -85,18 +84,18 @@ Built with Golang and supports cross-platform deployment to ensure a consistent 👉 **[Explore the GO SDK](https://github.com/openimsdk/openim-sdk-core)** -## 🌐 Introduction to OpenIMServer +## 🌐 Introduction to OpenIMServer -+ **OpenIMServer** features include: +- **OpenIMServer** features include: - 🌐 Microservices Architecture: Supports cluster mode, including a gateway and multiple rpc services. - 🚀 Diverse Deployment Options: Supports source code, Kubernetes, or Docker deployment. - Massive User Support: Supports large-scale groups with hundreds of thousands, millions of users, and billions of messages. ### Enhanced Business Functions: -+ **REST API**: Provides a REST API for business systems to enhance functionality, such as group creation and message pushing through backend interfaces. +- **REST API**: Provides a REST API for business systems to enhance functionality, such as group creation and message pushing through backend interfaces. -+ **Webhooks**: Expands business forms through callbacks, sending requests to business servers before or after certain events. +- **Webhooks**: Expands business forms through callbacks, sending requests to business servers before or after certain events. ![Overall Architecture](./docs/images/architecture-layers.png) @@ -108,8 +107,8 @@ Experience online for iOS/Android/H5/PC/Web: To facilitate user experience, we offer various deployment solutions. You can choose your preferred deployment method from the list below: -+ **[Source Code Deployment Guide](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Docker Deployment Guide](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Source Code Deployment Guide](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Docker Deployment Guide](https://docs.openim.io/guides/gettingStarted/dockerCompose)** ## System Support @@ -117,16 +116,16 @@ Supports Linux, Windows, Mac systems, and ARM and AMD CPU architectures. ## :link: Links - + **[Developer Manual](https://docs.openim.io/)** - + **[Changelog](https://github.com/openimsdk/open-im-server/blob/main/CHANGELOG.md)** +- **[Developer Manual](https://docs.openim.io/)** +- **[Changelog](https://github.com/openimsdk/open-im-server/blob/main/CHANGELOG.md)** ## :writing_hand: How to Contribute We welcome contributions of any kind! Please make sure to read our [Contributor Documentation](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md) before submitting a Pull Request. - + **[Report a Bug](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=bug&template=bug_report.md&title=)** - + **[Suggest a Feature](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)** - + **[Submit a Pull Request](https://github.com/openimsdk/open-im-server/pulls)** +- **[Report a Bug](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=bug&template=bug_report.md&title=)** +- **[Suggest a Feature](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)** +- **[Submit a Pull Request](https://github.com/openimsdk/open-im-server/pulls)** Thank you for contributing to building a powerful instant messaging solution! @@ -134,9 +133,6 @@ Thank you for contributing to building a powerful instant messaging solution! This software is licensed under the Apache License 2.0 - - - ## 🔮 Thanks to our contributors! diff --git a/README_zh_CN.md b/README_zh_CN.md index 2147c0243..7728c4904 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,34 +45,34 @@ Türkçe

-

## :busts_in_silhouette: 加入我们的社区 -+ 💬 [关注我们的 Twitter](https://twitter.com/founder_im63606) -+ 🚀 [加入我们的 Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2hljfom5u-9ZuzP3NfEKW~BJKbpLm0Hw) -+ :eyes: [加入我们的微信群](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 💬 [关注我们的 Twitter](https://twitter.com/founder_im63606) +- 🚀 [加入我们的 Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2hljfom5u-9ZuzP3NfEKW~BJKbpLm0Hw) +- :eyes: [加入我们的微信群](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## Ⓜ️ 关于 OpenIM -与Telegram、Signal、Rocket.Chat等独立聊天应用不同,OpenIM提供了专为开发者设计的开源即时通讯解决方案,而不是直接安装使用的独立聊天应用。OpenIM由OpenIM SDK和OpenIM Server两大部分组成,为开发者提供了一整套集成即时通讯功能的工具和服务,包括消息发送接收、用户管理和群组管理等。总体来说,OpenIM旨在为开发者提供必要的工具和框架,帮助他们在自己的应用中实现高效的即时通讯解决方案。 +与 Telegram、Signal、Rocket.Chat 等独立聊天应用不同,OpenIM 提供了专为开发者设计的开源即时通讯解决方案,而不是直接安装使用的独立聊天应用。OpenIM 由 OpenIM SDK 和 OpenIM Server 两大部分组成,为开发者提供了一整套集成即时通讯功能的工具和服务,包括消息发送接收、用户管理和群组管理等。总体来说,OpenIM 旨在为开发者提供必要的工具和框架,帮助他们在自己的应用中实现高效的即时通讯解决方案。 ![App-OpenIM 关系](./docs/images/oepnim-design.png) ## 🚀 OpenIMSDK 介绍 -**OpenIMSDK** 是为 **OpenIMServer** 设计的IM SDK,专为集成到客户端应用而生。它支持多种功能和模块: +**OpenIMSDK** 是为 **OpenIMServer** 设计的 IM SDK,专为集成到客户端应用而生。它支持多种功能和模块: + +- 🌟 主要功能: -+ 🌟 主要功能: - 📦 本地存储 - 🔔 监听器回调 - - 🛡️ API封装 + - 🛡️ API 封装 - 🌐 连接管理 -+ 📚 主要模块: +- 📚 主要模块: 1. 🚀 初始化及登录 2. 👤 用户管理 3. 👫 好友管理 @@ -86,31 +85,29 @@ ## 🌐 OpenIMServer 介绍 -+ **OpenIMServer** 的特点包括: - - 🌐 微服务架构:支持集群模式,包括网关(gateway)和多个rpc服务。 - - 🚀 多样的部署方式:支持源代码、Kubernetes或Docker部署。 +- **OpenIMServer** 的特点包括: + - 🌐 微服务架构:支持集群模式,包括网关(gateway)和多个 rpc 服务。 + - 🚀 多样的部署方式:支持源代码、Kubernetes 或 Docker 部署。 - 海量用户支持:支持十万级超大群组,千万级用户和百亿级消息。 ### 增强的业务功能: -+ **REST API**:为业务系统提供REST API,增加群组创建、消息推送等后台接口功能。 +- **REST API**:为业务系统提供 REST API,增加群组创建、消息推送等后台接口功能。 -+ **Webhooks**:通过事件前后的回调,向业务服务器发送请求,扩展更多的业务形态。 +- **Webhooks**:通过事件前后的回调,向业务服务器发送请求,扩展更多的业务形态。 ![整体架构](./docs/images/architecture-layers.png) - - ## :rocket: 快速入门 -在线体验iOS/Android/H5/PC/Web: +在线体验 iOS/Android/H5/PC/Web: -👉 **[OpenIM在线演示](https://www.openim.io/en/commercial)** +👉 **[OpenIM 在线演示](https://www.openim.io/en/commercial)** 为了便于用户体验,我们提供了多种部署解决方案,您可以根据以下列表选择适合您的部署方式: -+ **[源代码部署指南](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Docker 部署指南](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[源代码部署指南](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Docker 部署指南](https://docs.openim.io/guides/gettingStarted/dockerCompose)** ## 系统支持 @@ -118,24 +115,23 @@ ## :link: 相关链接 - + **[开发手册](https://docs.openim.io/)** - + **[更新日志](https://github.com/openimsdk/open-im-server/blob/main/CHANGELOG.md)** +- **[开发手册](https://docs.openim.io/)** +- **[更新日志](https://github.com/openimsdk/open-im-server/blob/main/CHANGELOG.md)** ## :writing_hand: 如何贡献 我们欢迎任何形式的贡献!在提交 Pull Request 之前,请确保阅读我们的[贡献者文档](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md) - + **[报告 Bug](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=bug&template=bug_report.md&title=)** - + **[提出新特性](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)** - + **[提交 Pull Request](https://github.com/openimsdk/open-im-server/pulls)** +- **[报告 Bug](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=bug&template=bug_report.md&title=)** +- **[提出新特性](https://github.com/openimsdk/open-im-server/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=)** +- **[提交 Pull Request](https://github.com/openimsdk/open-im-server/pulls)** 感谢您的贡献,一起来打造强大的即时通讯解决方案! -## :closed_book: 开源许可证 License +## :closed_book: 开源许可证 License This software is licensed under the Apache License 2.0 - ## 🔮 Thanks to our contributors! diff --git a/docs/readme/README_cs.md b/docs/readme/README_cs.md index 63f730a51..c771a55e4 100644 --- a/docs/readme/README_cs.md +++ b/docs/readme/README_cs.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,7 +45,6 @@ Türkçe

-

@@ -61,14 +59,14 @@ OpenIM je platforma služeb speciálně navržená pro integraci chatu, audio-vi **OpenIMSDK** je IM SDK navržený pro**OpenIMServer**, vytvořený speciálně pro vkládání do klientských aplikací. Jeho hlavní vlastnosti a moduly jsou následující: -+ 🌟 Hlavní vlastnosti: +- 🌟 Hlavní vlastnosti: - 📦 Místní úložiště - 🔔 Zpětná volání posluchačů - 🛡️ API obalování - 🌐 Správa připojení -+ 📚 hlavní moduly: +- 📚 hlavní moduly: 1. 🚀 Inicializace a přihlášení 2. 👤 Správa uživatelů @@ -82,15 +80,15 @@ Je postaven pomocí Golang a podporuje nasazení napříč platformami, což zaj ## 🌐 O OpenIMServeru -+ **OpenIMServer** má následující vlastnosti: +- **OpenIMServer** má následující vlastnosti: - 🌐 Architektura mikroslužeb: Podporuje režim clusteru, včetně brány a více služeb RPC. - 🚀 Různé metody nasazení: Podporuje nasazení prostřednictvím zdrojového kódu, Kubernetes nebo Docker. - Podpora masivní uživatelské základny: Super velké skupiny se stovkami tisíc uživatelů, desítkami milionů uživatelů a miliardami zpráv. ### Vylepšené obchodní funkce: -+ **REST API**: OpenIMServer nabízí REST API pro podnikové systémy, jejichž cílem je poskytnout podnikům více funkcí, jako je vytváření skupin a odesílání push zpráv přes backendová rozhraní. -+ **Webhooks**: OpenIMServer poskytuje možnosti zpětného volání pro rozšíření více obchodních formulářů. Zpětné volání znamená, že OpenIMServer odešle požadavek na obchodní server před nebo po určité události, jako jsou zpětná volání před nebo po odeslání zprávy. +- **REST API**: OpenIMServer nabízí REST API pro podnikové systémy, jejichž cílem je poskytnout podnikům více funkcí, jako je vytváření skupin a odesílání push zpráv přes backendová rozhraní. +- **Webhooks**: OpenIMServer poskytuje možnosti zpětného volání pro rozšíření více obchodních formulářů. Zpětné volání znamená, že OpenIMServer odešle požadavek na obchodní server před nebo po určité události, jako jsou zpětná volání před nebo po odeslání zprávy. 👉 **[Další informace](https://docs.openim.io/guides/introduction/product)** @@ -100,7 +98,6 @@ Ponořte se do srdce funkčnosti Open-IM-Server s naším diagramem architektury ![Overall Architecture](../images/architecture-layers.png) - ## :rocket: Rychlý start Podporujeme mnoho platforem. Zde jsou adresy pro rychlou práci na webové stránce: @@ -109,10 +106,10 @@ Podporujeme mnoho platforem. Zde jsou adresy pro rychlou práci na webové strá 🤲 Pro usnadnění uživatelské zkušenosti nabízíme různá řešení nasazení. Způsob nasazení si můžete vybrat ze seznamu níže: -+ **[Průvodce nasazením zdrojového kódu](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Docker Deployment Guide](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Průvodce nasazením Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Průvodce nasazením pro vývojáře Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Průvodce nasazením zdrojového kódu](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Docker Deployment Guide](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Průvodce nasazením Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Průvodce nasazením pro vývojáře Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: Chcete-li začít vyvíjet OpenIM @@ -122,7 +119,7 @@ OpenIM Naším cílem je vybudovat špičkovou open source komunitu. Máme soubo Pokud byste chtěli přispět do tohoto úložiště Open-IM-Server, přečtěte si naši [dokumentaci pro přispěvatele](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md). -Než začnete, ujistěte se, že jsou vaše změny vyžadovány. Nejlepší pro to je vytvořit [nová diskuze](https://github.com/openimsdk/open-im-server/discussions/new/choose) NEBO [Slack Communication](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q), nebo pokud narazíte na problém, [nahlásit jej](https://github.com/openimsdk/open-im-server/issues/new/choose) jako první. +Než začnete, ujistěte se, že jsou vaše změny vyžadovány. Nejlepší pro to je vytvořit [nová diskuze](https://github.com/openimsdk/open-im-server/discussions/new/choose) NEBO [Slack Communication](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), nebo pokud narazíte na problém, [nahlásit jej](https://github.com/openimsdk/open-im-server/issues/new/choose) jako první. - [OpenIM API Reference](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [Protokolování OpenIM Bash](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -154,19 +151,18 @@ Než začnete, ujistěte se, že jsou vaše změny vyžadovány. Nejlepší pro - [Spravovat backend a monitorovat nasazení](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Průvodce nasazením pro vývojáře Mac pro OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: Společenství -+ 📚 [Komunita OpenIM](https://github.com/OpenIMSDK/community) -+ 💕 [Zájmová skupina OpenIM](https://github.com/Openim-sigs) -+ 🚀 [Připojte se k naší komunitě Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [Připojte se k našemu wechatu](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [Komunita OpenIM](https://github.com/OpenIMSDK/community) +- 💕 [Zájmová skupina OpenIM](https://github.com/Openim-sigs) +- 🚀 [Připojte se k naší komunitě Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Připojte se k našemu wechatu](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: Komunitní setkání Chceme, aby se do naší komunity a přispívání kódu zapojil kdokoli, nabízíme dárky a odměny a vítáme vás, abyste se k nám připojili každý čtvrtek večer. -Naše konference je v [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, pak můžete vyhledat kanál Open-IM-Server a připojit se +Naše konference je v [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, pak můžete vyhledat kanál Open-IM-Server a připojit se Zaznamenáváme si každou [dvoutýdenní schůzku](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)do [diskuzí na GitHubu](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), naše historické poznámky ze schůzek a také záznamy schůzek jsou k dispozici na [Dokumenty Google :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing). diff --git a/docs/readme/README_da.md b/docs/readme/README_da.md index 60d97348a..4ed991ef1 100644 --- a/docs/readme/README_da.md +++ b/docs/readme/README_da.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,35 +45,30 @@ Türkçe

-

- ## :busts_in_silhouette: Fællesskab -+ 📚 [OpenIM-fællesskab](https://github.com/OpenIMSDK/community) -+ 💕 [OpenIM-interessegruppe](https://github.com/Openim-sigs) -+ 🚀 [Deltag i vores Slack-fællesskab](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [Deltag i vores WeChat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) -+ 👫 [Deltag i vores Reddit](https://www.reddit.com/r/OpenIMessaging) -+ 💬 [Følg vores Twitter-konto](https://twitter.com/openimsdk) - +- 📚 [OpenIM-fællesskab](https://github.com/OpenIMSDK/community) +- 💕 [OpenIM-interessegruppe](https://github.com/Openim-sigs) +- 🚀 [Deltag i vores Slack-fællesskab](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Deltag i vores WeChat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 👫 [Deltag i vores Reddit](https://www.reddit.com/r/OpenIMessaging) +- 💬 [Følg vores Twitter-konto](https://twitter.com/openimsdk) ## Ⓜ️ Om OpenIM OpenIM er en serviceplatform designet specifikt til integration af chat, lyd-videoopkald, notifikationer og AI-chatbots i applikationer. Den tilbyder en række kraftfulde API'er og Webhooks, som gør det let for udviklere at integrere disse interaktive funktioner i deres applikationer. OpenIM er ikke en selvstændig chatapplikation, men fungerer snarere som en platform, der understøtter andre applikationer i at opnå omfattende kommunikationsfunktionaliteter. Følgende diagram illustrerer interaktionen mellem AppServer, AppClient, OpenIMServer og OpenIMSDK for at forklare detaljeret. - - ![App-OpenIM Relationship](../images/oepnim-design.png) ## 🚀 Om OpenIMSDK - **OpenIMSDK** er en IM SDK designet til **OpenIMServer**, skabt specifikt til indlejring i klientapplikationer. Dens vigtigste funktioner og moduler er som følger: +**OpenIMSDK** er en IM SDK designet til **OpenIMServer**, skabt specifikt til indlejring i klientapplikationer. Dens vigtigste funktioner og moduler er som følger: -+ 🌟 Hovedfunktioner: +- 🌟 Hovedfunktioner: - 📦 Lokal lagring - 🔔 Lytter-callbacks @@ -95,15 +89,15 @@ Det er bygget ved hjælp af Golang og understøtter tværplatformsudrulning, hvi ## 🌐 Om OpenIMServer -+ **OpenIMServer** har følgende karakteristika: +- **OpenIMServer** har følgende karakteristika: - 🌐 Mikroservicarkitektur: Understøtter klyngetilstand, inklusive en gateway og flere rpc-tjenester. - 🚀 Forskellige udrulningsmetoder: Understøtter udrulning via kildekode, Kubernetes eller Docker. - Støtte til massiv brugerbase: Super store grupper med hundredtusinder af brugere, titusinder af brugere og milliarder af beskeder. ### Forbedret forretningsfunktionalitet: -+ **REST API**:OpenIMServer tilbyder REST API'er til forretningssystemer, med det formål at give virksomheder flere funktioner, såsom at oprette grupper og sende push-beskeder gennem backend-grænseflader. -+ **Webhooks**:OpenIMServer giver mulighed for callback-funktionalitet for at udvide flere forretningsformer. Et callback betyder, at OpenIMServer sender en anmodning til forretningsserveren før eller efter en bestemt begivenhed, som callbacks før eller efter at have sendt en besked. +- **REST API**:OpenIMServer tilbyder REST API'er til forretningssystemer, med det formål at give virksomheder flere funktioner, såsom at oprette grupper og sende push-beskeder gennem backend-grænseflader. +- **Webhooks**:OpenIMServer giver mulighed for callback-funktionalitet for at udvide flere forretningsformer. Et callback betyder, at OpenIMServer sender en anmodning til forretningsserveren før eller efter en bestemt begivenhed, som callbacks før eller efter at have sendt en besked. 👉 **[Lær mere](https://docs.openim.io/guides/introduction/product)** @@ -121,10 +115,10 @@ Vi understøtter mange platforme. Her er adresserne for hurtig oplevelse på web 🤲 For at lette brugeroplevelsen tilbyder vi forskellige udrulningsløsninger. Du kan vælge din udrulningsmetode fra listen nedenfor: -+ **[Vejledning til udrulning af kildekode](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Vejledning til Docker-udrulning](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Vejledning til Kubernetes-udrulning](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Vejledning til Mac-udviklerudrulning](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Vejledning til udrulning af kildekode](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Vejledning til Docker-udrulning](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Vejledning til Kubernetes-udrulning](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Vejledning til Mac-udviklerudrulning](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: For at starte udviklingen af OpenIM @@ -134,7 +128,7 @@ OpenIM Vores mål er at bygge et topniveau åben kildekode-fællesskab. Vi har e Hvis du gerne vil bidrage til dette Open-IM-Server-repositorium, bedes du læse vores [dokumentation for bidragydere](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md). -Før du starter, skal du sikre dig, at dine ændringer er efterspurgte. Det bedste for det er at oprette en [ny diskussion](https://github.com/openimsdk/open-im-server/discussions/new/choose) ELLER [Slack-kommunikation](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q), eller hvis du finder et problem, [rapportere det](https://github.com/openimsdk/open-im-server/issues/new/choose) først. +Før du starter, skal du sikre dig, at dine ændringer er efterspurgte. Det bedste for det er at oprette en [ny diskussion](https://github.com/openimsdk/open-im-server/discussions/new/choose) ELLER [Slack-kommunikation](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), eller hvis du finder et problem, [rapportere det](https://github.com/openimsdk/open-im-server/issues/new/choose) først. - [OpenIM API-referencer](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [OpenIM Bash-logging](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -166,12 +160,11 @@ Før du starter, skal du sikre dig, at dine ændringer er efterspurgte. Det beds - [Administrer backend og overvåg udrulning](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Mac-udviklerudrulningsguide for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :calendar: Fællesskabsmøder Vi ønsker, at alle involverer sig i vores fællesskab og bidrager med kode, vi tilbyder gaver og belønninger, og vi byder dig velkommen til at deltage hver torsdag aften. -Vores konference er på [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, derefter kan du søge Open-IM-Server pipeline for at deltage. +Vores konference er på [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, derefter kan du søge Open-IM-Server pipeline for at deltage. Vi tager [notater](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) af hvert fjortendages møde i [GitHub-diskussioner](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Vores historiske mødenotater samt genudsendelser af møderne er tilgængelige på [Google Docs](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) 📑. @@ -189,4 +182,4 @@ OpenIM-logoet, inklusive dets variationer og animerede versioner, vist i dette r - \ No newline at end of file + diff --git a/docs/readme/README_el.md b/docs/readme/README_el.md index da01fcb47..9d31e8267 100644 --- a/docs/readme/README_el.md +++ b/docs/readme/README_el.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,7 +45,6 @@ Türkçe

-

@@ -61,14 +59,14 @@ Το **OpenIMSDK** είναι ένα SDK για αμεση ανταλλαγή μηνυμάτων σχεδιασμένο για το **OpenIMServer**, δημιουργήθηκε ειδικά για ενσωμάτωση σε εφαρμογές πελατών. Οι κύριες δυνατότητες και μονάδες του είναι οι εξής: -+ 🌟 Κύριες Δυνατότητες: +- 🌟 Κύριες Δυνατότητες: - 📦 Τοπική αποθήκευση - 🔔 Callbacks ακροατών - 🛡️ Περιτύλιγμα API - 🌐 Διαχείριση σύνδεσης -+ 📚 Κύριες Μονάδες: +- 📚 Κύριες Μονάδες: 1. 🚀 Αρχικοποίηση και Σύνδεση 2. 👤 Διαχείριση Χρηστών @@ -82,15 +80,15 @@ ## 🌐 Σχετικά με το OpenIMServer -+ Το **OpenIMServer** έχει τις ακόλουθες χαρακτηριστικές: +- Το **OpenIMServer** έχει τις ακόλουθες χαρακτηριστικές: - 🌐 Αρχιτεκτονική μικροϋπηρεσιών: Υποστηρίζει λειτουργία σε σύμπλεγμα, περιλαμβάνοντας έναν πύλη και πολλαπλές υπηρεσίες rpc. - 🚀 Διάφοροι τρόποι ανάπτυξης: Υποστηρίζει ανάπτυξη μέσω πηγαίου κώδικα, Kubernetes, ή Docker. - Υποστήριξη για τεράστια βάση χρηστών: Πολύ μεγάλες ομάδες με εκατοντάδες χιλιάδες χρήστες, δεκάδες εκατομμύρια χρήστες και δισεκατομμύρια μηνύματα. ### Ενισχυμένη Επιχειρηματική Λειτουργικότητα: -+ **REST API**: Το OpenIMServer προσφέρει REST APIs για επιχειρηματικά συστήματα, με στόχο την ενδυνάμωση των επιχειρήσεων με περισσότερες λειτουργικότητες, όπως η δημιουργία ομάδων και η αποστολή μηνυμάτων push μέσω backend διεπαφών. -+ **Webhooks**: Το OpenIMServer παρέχει δυνατότητες επανάκλησης για την επέκταση περισσότερων επιχειρηματικών μορφών. Μια επανάκληση σημαίνει ότι το OpenIMServer στέλνει ένα αίτημα στον επιχειρηματικό διακομιστή πριν ή μετά από ένα συγκεκριμένο γεγονός, όπως επανακλήσεις πριν ή μετά την αποστολή ενός μηνύματος. +- **REST API**: Το OpenIMServer προσφέρει REST APIs για επιχειρηματικά συστήματα, με στόχο την ενδυνάμωση των επιχειρήσεων με περισσότερες λειτουργικότητες, όπως η δημιουργία ομάδων και η αποστολή μηνυμάτων push μέσω backend διεπαφών. +- **Webhooks**: Το OpenIMServer παρέχει δυνατότητες επανάκλησης για την επέκταση περισσότερων επιχειρηματικών μορφών. Μια επανάκληση σημαίνει ότι το OpenIMServer στέλνει ένα αίτημα στον επιχειρηματικό διακομιστή πριν ή μετά από ένα συγκεκριμένο γεγονός, όπως επανακλήσεις πριν ή μετά την αποστολή ενός μηνύματος. 👉 **[Μάθετε περισσότερα](https://docs.openim.io/guides/introduction/product)** @@ -100,7 +98,6 @@ ![Overall Architecture](../../docs/images/architecture-layers.png) - ## :rocket: Γρήγορη Εκκίνηση Υποστηρίζουμε πολλές πλατφόρμες. Εδώ είναι οι διευθύνσεις για γρήγορη εμπειρία στην πλευρά του διαδικτύου: @@ -109,10 +106,10 @@ 🤲 Για να διευκολύνουμε την εμπειρία του χρήστη, προσφέρουμε διάφορες λύσεις ανάπτυξης. Μπορείτε να επιλέξετε τη μέθοδο ανάπτυξης σας από την παρακάτω λίστα: -+ **[Οδηγός Ανάπτυξης Κώδικα Πηγής](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[δηγός Ανάπτυξης μέσω Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Οδηγός Ανάπτυξης Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Οδηγός Ανάπτυξης για Αναπτυξιακούς στο Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Οδηγός Ανάπτυξης Κώδικα Πηγής](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[δηγός Ανάπτυξης μέσω Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Οδηγός Ανάπτυξης Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Οδηγός Ανάπτυξης για Αναπτυξιακούς στο Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: Για να Αρχίσετε την Ανάπτυξη του OpenIM @@ -122,7 +119,7 @@ OpenIM Στόχος μας είναι να δημιουργήσουμε μια Εάν θέλετε να συνεισφέρετε σε αυτό το αποθετήριο Open-IM-Server, παρακαλούμε διαβάστε την [τεκμηρίωση συνεισφέροντος](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md). -Πριν ξεκινήσετε, παρακαλούμε βεβαιωθείτε ότι οι αλλαγές σας είναι ζητούμενες. Το καλύτερο για αυτό είναι να δημιουργήσετε ένα [νέα συζήτηση](https://github.com/openimsdk/open-im-server/discussions/new/choose) ή [Επικοινωνία Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q), ή αν βρείτε ένα ζήτημα, [αναφέρετέ το](https://github.com/openimsdk/open-im-server/issues/new/choose) πρώτα. +Πριν ξεκινήσετε, παρακαλούμε βεβαιωθείτε ότι οι αλλαγές σας είναι ζητούμενες. Το καλύτερο για αυτό είναι να δημιουργήσετε ένα [νέα συζήτηση](https://github.com/openimsdk/open-im-server/discussions/new/choose) ή [Επικοινωνία Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), ή αν βρείτε ένα ζήτημα, [αναφέρετέ το](https://github.com/openimsdk/open-im-server/issues/new/choose) πρώτα. - [Αναφορά API του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [Καταγραφή Bash του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -154,28 +151,28 @@ OpenIM Στόχος μας είναι να δημιουργήσουμε μια - [Διαχείριση backend και παρακολούθηση ανάπτυξης](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Οδηγός Ανάπτυξης για Προγραμματιστές Mac του OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: Κοινότητα -+ 📚 [Κοινότητα OpenIM](https://github.com/OpenIMSDK/community) -+ 💕 [Ομάδα Ενδιαφέροντος OpenIM](https://github.com/Openim-sigs) -+ 🚀 [Εγγραφείτε στην κοινότητα Slack μας](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [γγραφείτε στην ομάδα μας wechat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [Κοινότητα OpenIM](https://github.com/OpenIMSDK/community) +- 💕 [Ομάδα Ενδιαφέροντος OpenIM](https://github.com/Openim-sigs) +- 🚀 [Εγγραφείτε στην κοινότητα Slack μας](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [γγραφείτε στην ομάδα μας wechat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: Συναντήσεις της κοινότητας Θέλουμε οποιονδήποτε να εμπλακεί στην κοινότητά μας και να συνεισφέρει κώδικα. Προσφέρουμε δώρα και ανταμοιβές και σας καλωσορίζουμε να μας ενταχθείτε κάθε Πέμπτη βράδυ. -Η διάσκεψή μας είναι στο [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, στη συνέχεια μπορείτε να αναζητήσετε τη διαδικασία Open-IM-Server για να συμμετάσχετε +Η διάσκεψή μας είναι στο [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, στη συνέχεια μπορείτε να αναζητήσετε τη διαδικασία Open-IM-Server για να συμμετάσχετε Κάνουμε σημειώσεις για κάθε μια [Σημειώνουμε κάθε διμηνιαία συνάντηση](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) στις [συζητήσεις του GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Οι ιστορικές μας σημειώσεις συναντήσεων, καθώς και οι επαναλήψεις των συναντήσεων είναι διαθέσιμες στο[Έγγραφα της Google :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing). ## :eyes: Ποιοί Χρησιμοποιούν το OpenIM Ελέγξτε τη σελίδα με τις [μελέτες περίπτωσης χρήσης ](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) μας για μια λίστα των χρηστών του έργου. Μην διστάσετε να αφήσετε ένα[📝σχόλιο](https://github.com/openimsdk/open-im-server/issues/379) και να μοιραστείτε την περίπτωση χρήσης σας. + ## :page_facing_up: Άδεια Χρήσης -Το OpenIM διατίθεται υπό την άδεια Apache 2.0. Δείτε τη [ΑΔΕΙΑ ΧΡΗΣΗΣ](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) για το πλήρες κείμενο της άδειας. +Το OpenIM διατίθεται υπό την άδεια Apache 2.0. Δείτε τη [ΑΔΕΙΑ ΧΡΗΣΗΣ](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) για το πλήρες κείμενο της άδειας. Το λογότυπο του OpenIM, συμπεριλαμβανομένων των παραλλαγών και των κινούμενων εικόνων, που εμφανίζονται σε αυτό το αποθετήριο[OpenIM](https://github.com/openimsdk/open-im-server) υπό τις διευθύνσεις [assets/logo](../../assets/logo) και [assets/logo-gif](../../assets/logo-gif) προστατεύονται από τους νόμους περί πνευματικής ιδιοκτησίας. diff --git a/docs/readme/README_es.md b/docs/readme/README_es.md index f123b85c3..360aeb190 100644 --- a/docs/readme/README_es.md +++ b/docs/readme/README_es.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,7 +45,6 @@ Türkçe

-

@@ -61,14 +59,14 @@ OpenIM es una plataforma de servicio diseñada específicamente para integrar ch **OpenIMSDK** es un SDK de mensajería instantánea diseñado para **OpenIMServer**, creado específicamente para su incorporación en aplicaciones cliente. Sus principales características y módulos son los siguientes: -+ 🌟 Características Principales: +- 🌟 Características Principales: - 📦 Almacenamiento local - 🔔 Callbacks de escuchas - 🛡️ Envoltura de API - 🌐 Gestión de conexiones -+ 📚 Módulos Principales: +- 📚 Módulos Principales: 1. 🚀 Inicialización y acceso 2. 👤 Gestión de usuarios @@ -82,17 +80,15 @@ Está construido con Golang y soporta despliegue multiplataforma, asegurando una ## 🌐 Acerca de OpenIMServer -+ **OpenIMServer** tiene las siguientes características: +- **OpenIMServer** tiene las siguientes características: - 🌐 Arquitectura de microservicios: Soporta modo cluster, incluyendo un gateway y múltiples servicios rpc. - 🚀 Métodos de despliegue diversos: Soporta el despliegue a través de código fuente, Kubernetes o Docker. - Soporte para una base de usuarios masiva: Grupos super grandes con cientos de miles de usuarios, decenas de millones de usuarios y miles de millones de mensajes. - - ### Funcionalidad Empresarial Mejorada: -+ **API REST**: OpenIMServer ofrece APIs REST para sistemas empresariales, destinadas a empoderar a las empresas con más funcionalidades, como la creación de grupos y el envío de mensajes push a través de interfaces de backend. -+ **Webhooks**: OpenIMServer proporciona capacidades de callback para extender más formas de negocio. Un callback significa que OpenIMServer envía una solicitud al servidor empresarial antes o después de un cierto evento, como callbacks antes o después de enviar un mensaje. +- **API REST**: OpenIMServer ofrece APIs REST para sistemas empresariales, destinadas a empoderar a las empresas con más funcionalidades, como la creación de grupos y el envío de mensajes push a través de interfaces de backend. +- **Webhooks**: OpenIMServer proporciona capacidades de callback para extender más formas de negocio. Un callback significa que OpenIMServer envía una solicitud al servidor empresarial antes o después de un cierto evento, como callbacks antes o después de enviar un mensaje. 👉 **[Aprende más](https://docs.openim.io/guides/introduction/product)** @@ -102,10 +98,8 @@ Adéntrate en el corazón de la funcionalidad de Open-IM-Server con nuestro diag ![Arquitectura General](../../docs/images/architecture-layers.png) - ## :rocket: Inicio Rápido - :rocket: Inicio Rápido Apoyamos muchas plataformas. Aquí están las direcciones para una experiencia rápida en el lado web: @@ -113,22 +107,21 @@ Apoyamos muchas plataformas. Aquí están las direcciones para una experiencia r 🤲 Para facilitar la experiencia del usuario, ofrecemos varias soluciones de despliegue. Puedes elegir tu método de despliegue de la lista a continuación: -+ **[Guía de Despliegue de Código Fuente](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Guía de Despliegue con Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Guía de Despliegue con Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Guía de Despliegue para Desarrolladores en Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Guía de Despliegue de Código Fuente](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Guía de Despliegue con Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Guía de Despliegue con Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Guía de Despliegue para Desarrolladores en Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: Para Comenzar a Desarrollar en OpenIM [![Abrir en Contenedor de Desarrollo](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server) -Nuestro objetivo en OpenIM es construir una comunidad de código abierto de nivel superior. Tenemos un conjunto de estándares, +Nuestro objetivo en OpenIM es construir una comunidad de código abierto de nivel superior. Tenemos un conjunto de estándares, en el [repositorio de la Comunidad.](https://github.com/OpenIMSDK/community). Si te gustaría contribuir a este repositorio de Open-IM-Server, por favor lee nuestra [documentación para colaboradores](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md). - -Antes de comenzar, asegúrate de que tus cambios sean demandados. Lo mejor para eso es crear una [nueva discusión](https://github.com/openimsdk/open-im-server/discussions/new/choose) O [Comunicación en Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q), o si encuentras un problema, [repórtalo](https://github.com/openimsdk/open-im-server/issues/new/choose) primero. +Antes de comenzar, asegúrate de que tus cambios sean demandados. Lo mejor para eso es crear una [nueva discusión](https://github.com/openimsdk/open-im-server/discussions/new/choose) O [Comunicación en Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), o si encuentras un problema, [repórtalo](https://github.com/openimsdk/open-im-server/issues/new/choose) primero. - [Referencia de API de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [Registro de Bash de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -160,32 +153,31 @@ Antes de comenzar, asegúrate de que tus cambios sean demandados. Lo mejor para - [Gestión de backend y despliegue de monitoreo](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Guía de Despliegue para Desarrolladores Mac de OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: Comunidad -+ 📚 [Comunidad de OpenIM](https://github.com/OpenIMSDK/community) -+ 💕 [Grupo de Interés de OpenIM](https://github.com/Openim-sigs) -+ 🚀 [Únete a nuestra comunidad de Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [Únete a nuestro wechat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [Comunidad de OpenIM](https://github.com/OpenIMSDK/community) +- 💕 [Grupo de Interés de OpenIM](https://github.com/Openim-sigs) +- 🚀 [Únete a nuestra comunidad de Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Únete a nuestro wechat (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: Reuniones de la Comunidad Queremos que cualquiera se involucre en nuestra comunidad y contribuya con código, ofrecemos regalos y recompensas, y te damos la bienvenida para que te unas a nosotros cada jueves por la noche. -Nuestra conferencia está en [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, luego puedes buscar el pipeline de Open-IM-Server para unirte +Nuestra conferencia está en [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, luego puedes buscar el pipeline de Open-IM-Server para unirte Tomamos notas de cada [reunión quincenal](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) en [discusiones de GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Nuestras notas de reuniones históricas, así como las repeticiones de las reuniones están disponibles en [Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing). ## :eyes: Quiénes Están Usando OpenIM Consulta nuestros [estudios de caso de usuarios](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) página para obtener una lista de los usuarios del proyecto. No dudes en dejar un [📝comentario](https://github.com/openimsdk/open-im-server/issues/379) y compartir tu caso de uso. -## :page_facing_up: Licencia +## :page_facing_up: Licencia -OpenIM está bajo la licencia Apache 2.0. Consulta [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) para ver el texto completo de la licencia. - +OpenIM está bajo la licencia Apache 2.0. Consulta [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) para ver el texto completo de la licencia. El logotipo de OpenIM, incluyendo sus variaciones y versiones animadas, que se muestran en este repositorio [OpenIM](https://github.com/openimsdk/open-im-server) en los directorios [assets/logo](../../assets/logo) y [assets/logo-gif](assets/logo-gif) están protegidos por las leyes de derechos de autor. + ## 🔮 iGracias a nuestros colaboradores! diff --git a/docs/readme/README_fa.md b/docs/readme/README_fa.md index 7a1512e84..9b54a14d8 100644 --- a/docs/readme/README_fa.md +++ b/docs/readme/README_fa.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,7 +45,6 @@ Türkçe

-

@@ -61,14 +59,14 @@ OpenIM یک پلتفرم خدماتی است که به طور خاص برای ا **OpenIMSDK** یک IM SDK است که برای **OpenIMServer** طراحی شده است که به طور خاص برای جاسازی در برنامه های مشتری ایجاد شده است. ویژگی ها و ماژول های اصلی آن به شرح زیر است: -+ 🌟 ویژگی های اصلی: +- 🌟 ویژگی های اصلی: - 📦 ذخیره سازی محلی - 🔔 پاسخ تماس شنونده - 🛡️ بسته بندی API - 🌐 مدیریت اتصال -+ 📚 ماژول های اصلی: +- 📚 ماژول های اصلی: 1. 🚀 مقداردهی اولیه و ورود 2. 👤 مدیریت کاربر @@ -82,15 +80,15 @@ OpenIM یک پلتفرم خدماتی است که به طور خاص برای ا ## 🌐 درباره OpenIMServer -+ **OpenIMServer** دارای ویژگی های زیر است: +- **OpenIMServer** دارای ویژگی های زیر است: - 🌐 معماری Microservice: از حالت کلاستر، از جمله یک دروازه و چندین سرویس rpc پشتیبانی می کند. - 🚀 روش‌های استقرار متنوع: از استقرار از طریق کد منبع، Kubernetes یا Docker پشتیبانی می‌کند. - پشتیبانی از پایگاه عظیم کاربران: گروه های فوق العاده بزرگ با صدها هزار کاربر، ده ها میلیون کاربر و میلیاردها پیام. ### عملکردهای تجاری پیشرفته: -+ **REST API**: OpenIMServer APIهای REST را برای سیستم‌های تجاری ارائه می‌کند، با هدف توانمندسازی کسب‌وکارها با قابلیت‌های بیشتر، مانند ایجاد گروه‌ها و ارسال پیام‌های فشار از طریق رابط‌های باطنی. -+ **Webhooks**: OpenIMServer قابلیت های پاسخ به تماس را برای گسترش بیشتر فرم های تجاری ارائه می دهد. پاسخ به تماس به این معنی است که OpenIMServer درخواستی را قبل یا بعد از یک رویداد خاص به سرور تجاری ارسال می کند، مانند تماس های قبل یا بعد از ارسال یک پیام. +- **REST API**: OpenIMServer APIهای REST را برای سیستم‌های تجاری ارائه می‌کند، با هدف توانمندسازی کسب‌وکارها با قابلیت‌های بیشتر، مانند ایجاد گروه‌ها و ارسال پیام‌های فشار از طریق رابط‌های باطنی. +- **Webhooks**: OpenIMServer قابلیت های پاسخ به تماس را برای گسترش بیشتر فرم های تجاری ارائه می دهد. پاسخ به تماس به این معنی است که OpenIMServer درخواستی را قبل یا بعد از یک رویداد خاص به سرور تجاری ارسال می کند، مانند تماس های قبل یا بعد از ارسال یک پیام. 👉 **[بیشتر بدانید](https://docs.openim.io/guides/introduction/product)** @@ -100,7 +98,6 @@ OpenIM یک پلتفرم خدماتی است که به طور خاص برای ا ![Overall Architecture](../images/architecture-layers.png) - ## :rocket: شروع سریع ما از بسیاری از پلتفرم ها پشتیبانی می کنیم. در اینجا آدرس هایی برای تجربه سریع در سمت وب آمده است: @@ -109,10 +106,10 @@ OpenIM یک پلتفرم خدماتی است که به طور خاص برای ا 🤲 برای تسهیل تجربه کاربر، ما راه حل های مختلف استقرار را ارائه می دهیم. می توانید روش استقرار خود را از لیست زیر انتخاب کنید: -+ **[راهنمای استقرار کد منبع](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[راهنمای استقرار داکر](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[راهنمای استقرار Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[راهنمای استقرار توسعه دهنده مک](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[راهنمای استقرار کد منبع](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[راهنمای استقرار داکر](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[راهنمای استقرار Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[راهنمای استقرار توسعه دهنده مک](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: برای شروع توسعه OpenIM @@ -122,7 +119,7 @@ OpenIM هدف ما ایجاد یک جامعه منبع باز سطح بالا ا اگر می‌خواهید در این مخزن Open-IM-Server مشارکت کنید، لطفاً [مستندات مشارکت‌کننده](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md) ما را بخوانید. -قبل از شروع، لطفاً مطمئن شوید که تغییرات شما مورد تقاضا هستند. بهترین کار برای آن این است که یک [بحث جدید](https://github.com/openimsdk/open-im-server/discussions/new/choose) یا [ارتباط اسلک](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) ایجاد کنید، یا اگر مشکلی پیدا کردید، ابتدا [آن را گزارش کنید](https://github.com/openimsdk/open-im-server/issues/new/choose). +قبل از شروع، لطفاً مطمئن شوید که تغییرات شما مورد تقاضا هستند. بهترین کار برای آن این است که یک [بحث جدید](https://github.com/openimsdk/open-im-server/discussions/new/choose) یا [ارتباط اسلک](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) ایجاد کنید، یا اگر مشکلی پیدا کردید، ابتدا [آن را گزارش کنید](https://github.com/openimsdk/open-im-server/issues/new/choose). - [مرجع OpenIM API](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [OpenIM Bash Logging](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -154,21 +151,20 @@ OpenIM هدف ما ایجاد یک جامعه منبع باز سطح بالا ا - [مدیریت استقرار باطن و نظارت](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [راهنمای استقرار توسعه دهنده مک برای OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: انجمن -+ 📚 [انجمن OpenIM](https://github.com/OpenIMSDK/community) -+ 💕 [گروه علاقه OpenIM](https://github.com/Openim-sigs) -+ 🚀 [به انجمن Slack ما بپیوندید](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [به وی چت ما بپیوندید](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [انجمن OpenIM](https://github.com/OpenIMSDK/community) +- 💕 [گروه علاقه OpenIM](https://github.com/Openim-sigs) +- 🚀 [به انجمن Slack ما بپیوندید](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [به وی چت ما بپیوندید](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: جلسات جامعه ما می‌خواهیم هر کسی در انجمن ما مشارکت کند و در کد مشارکت کند، ما هدایا و جوایزی ارائه می‌کنیم، و از شما استقبال می‌کنیم که هر پنجشنبه شب به ما بپیوندید. -کنفرانس ما در [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯 است، سپس می توانید خط لوله Open-IM-Server را برای پیوستن جستجو کنید. +کنفرانس ما در [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯 است، سپس می توانید خط لوله Open-IM-Server را برای پیوستن جستجو کنید. -ما از هر [جلسه دو هفته‌ای](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) در [بحث‌های GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting) یادداشت‌برداری می‌کنیم، یادداشت‌های جلسه تاریخی ما، و همچنین بازپخش جلسات در [Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) موجود است. +ما از هر [جلسه دو هفته‌ای](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) در [بحث‌های GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting) یادداشت‌برداری می‌کنیم، یادداشت‌های جلسه تاریخی ما، و همچنین بازپخش جلسات در [Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) موجود است. ## :eyes: چه کسانی از OpenIM استفاده می کنند diff --git a/docs/readme/README_fr.md b/docs/readme/README_fr.md index aaf7a9bd4..107fc1e3f 100644 --- a/docs/readme/README_fr.md +++ b/docs/readme/README_fr.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,25 +45,21 @@ Türkçe

-

- ## Ⓜ️ À propos de OpenIM OpenIM est une plateforme de services conçue spécifiquement pour intégrer des fonctionnalités de communication telles que le chat, les appels audio et vidéo, les notifications, ainsi que les robots de chat IA dans les applications. Elle offre une série d'API puissantes et de Webhooks, permettant aux développeurs d'incorporer facilement ces caractéristiques interactives dans leurs applications. OpenIM n'est pas en soi une application de chat autonome, mais sert de plateforme supportant d'autres applications pour réaliser des fonctionnalités de communication enrichies. L'image ci-dessous montre les relations d'interaction entre AppServer, AppClient, OpenIMServer et OpenIMSDK pour illustrer spécifiquement. - - ![Relation App-OpenIM](../../images/oepnim-design.png) ## 🚀 À propos de OpenIMSDK -**OpenIMSDK** est un SDK IM conçu pour **OpenIMServer** spécialement créé pour être intégré dans les applications clientes. Ses principales fonctionnalités et modules comprennent : +**OpenIMSDK** est un SDK IM conçu pour **OpenIMServer** spécialement créé pour être intégré dans les applications clientes. Ses principales fonctionnalités et modules comprennent : -+ 🌟 Fonctionnalités clés : +- 🌟 Fonctionnalités clés : - 📦 Stockage local - 🔔 Rappels de l'écouteur @@ -85,15 +80,15 @@ Il est construit avec Golang et supporte le déploiement multiplateforme, assura ## 🌐 À propos de OpenIMServer -+ **OpenIMServer** présente les caractéristiques suivantes : +- **OpenIMServer** présente les caractéristiques suivantes : - 🌐 Architecture microservices : prend en charge le mode cluster, incluant le gateway (passerelle) et plusieurs services rpc。 - 🚀 Divers modes de déploiement : supporte le déploiement via le code source, Kubernetes ou Docker。 - Support d'une masse d'utilisateurs : plus de cent mille pour les super grands groupes, des millions d'utilisateurs, et des milliards de messages。 ### Fonctionnalités commerciales améliorées : -+ **REST API**:OpenIMServer fournit une REST API pour les systèmes commerciaux, visant à accorder plus de fonctionnalités, telles que la création de groupes via l'interface backend, l'envoi de messages push, etc。 -+ **Webhooks**:OpenIMServer offre des capacités de rappel pour étendre davantage les formes d'entreprise. Un rappel signifie que OpenIMServer enverra une requête au serveur d'entreprise avant ou après qu'un événement se soit produit, comme un rappel avant ou après l'envoi d'un message。 +- **REST API**:OpenIMServer fournit une REST API pour les systèmes commerciaux, visant à accorder plus de fonctionnalités, telles que la création de groupes via l'interface backend, l'envoi de messages push, etc。 +- **Webhooks**:OpenIMServer offre des capacités de rappel pour étendre davantage les formes d'entreprise. Un rappel signifie que OpenIMServer enverra une requête au serveur d'entreprise avant ou après qu'un événement se soit produit, comme un rappel avant ou après l'envoi d'un message。 👉 **[En savoir plus](https://docs.openim.io/guides/introduction/product)** @@ -103,7 +98,6 @@ Plongez dans le cœur de la fonctionnalité d'Open-IM-Server avec notre diagramm ![Architecture globale](../../images/architecture-layers.png) - ## :rocket: Démarrage rapide Nous prenons en charge de nombreuses plateformes. Voici les adresses pour une expérience rapide du côté web : @@ -112,17 +106,18 @@ Nous prenons en charge de nombreuses plateformes. Voici les adresses pour une ex 🤲 Pour faciliter l'expérience utilisateur, nous proposons plusieurs solutions de déploiement. Vous pouvez choisir votre méthode de déploiement selon la liste ci-dessous : -+ **[Guide de déploiement du code source](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Guide de déploiement Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Guide de déploiement Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Guide de déploiement pour développeur Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Guide de déploiement du code source](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Guide de déploiement Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Guide de déploiement Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Guide de déploiement pour développeur Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** -## :hammer_and_wrench: Commencer à développer avec OpenIM +## :hammer_and_wrench: Commencer à développer avec OpenIM Chez OpenIM, notre objectif est de construire une communauté open source de premier plan. Nous avons un ensemble de standards, disponibles dans le[ dépôt communautaire](https://github.com/OpenIMSDK/community)。 Si vous souhaitez contribuer à ce dépôt Open-IM-Server, veuillez lire notre[ document pour les contributeurs](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)。 -Avant de commencer, assurez-vous que vos modifications sont nécessaires. La meilleure manière est de créer une[ nouvelle discussion ](https://github.com/openimsdk/open-im-server/discussions/new/choose) ou une [ communication Slack,](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q),ou si vous identifiez un problème, de[ signaler d'abord ](https://github.com/openimsdk/open-im-server/issues/new/choose)。 +Avant de commencer, assurez-vous que vos modifications sont nécessaires. La meilleure manière est de créer une[ nouvelle discussion ](https://github.com/openimsdk/open-im-server/discussions/new/choose) ou une [ communication Slack,](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A),ou si vous identifiez un problème, de[ signaler d'abord ](https://github.com/openimsdk/open-im-server/issues/new/choose)。 + - [Référence de l'API OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [Journalisation Bash OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) - [Actions CI/CD OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md) @@ -153,13 +148,12 @@ Avant de commencer, assurez-vous que vos modifications sont nécessaires. La mei - [Gérer le déploiement du backend et la surveillance](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Guide de déploiement pour développeur Mac pour OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ->## :calendar: Réunions de la Communauté +> ## :calendar: Réunions de la Communauté Nous voulons que tout le monde s'implique dans notre communauté et contribue au code, nous offrons des cadeaux et des récompenses, et nous vous invitons à nous rejoindre chaque jeudi soir. -Notre conférence se trouve dans le [ Slack OpenIM ](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, ensuite vous pouvez rechercher le pipeline Open-IM-Server pour rejoindre +Notre conférence se trouve dans le [ Slack OpenIM ](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, ensuite vous pouvez rechercher le pipeline Open-IM-Server pour rejoindre -Nous prenons des notes de chaque [réunion bihebdomadaire ](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) dans les [discussions GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Nos notes de réunion historiques, ainsi que les rediffusions des réunions sont disponibles sur [ Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing). +Nous prenons des notes de chaque [réunion bihebdomadaire ](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) dans les [discussions GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Nos notes de réunion historiques, ainsi que les rediffusions des réunions sont disponibles sur [ Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing). ## :eyes: Qui Utilise OpenIM @@ -167,9 +161,9 @@ Consultez notre page [ études de cas d'utilisateurs ](https://github.com/OpenIM ## :page_facing_up: License -OpenIM est sous licence Apache 2.0. Voir [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) pour le texte complet de la licence. +OpenIM est sous licence Apache 2.0. Voir [LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE) pour le texte complet de la licence. -Le logo OpenIM, y compris ses variations et versions animées, affiché dans ce dépôt[OpenIM](https://github.com/openimsdk/open-im-server) sous les répertoires [assets/logo](../../assets/logo) et [assets/logo-gif](assets/logo-gif) sont protégés par les lois sur le droit d'auteur. +Le logo OpenIM, y compris ses variations et versions animées, affiché dans ce dépôt[OpenIM](https://github.com/openimsdk/open-im-server) sous les répertoires [assets/logo](../../assets/logo) et [assets/logo-gif](assets/logo-gif) sont protégés par les lois sur le droit d'auteur. ## 🔮 Merci à nos contributeurs ! diff --git a/docs/readme/README_hu.md b/docs/readme/README_hu.md index 61013c334..d6cf340c8 100644 --- a/docs/readme/README_hu.md +++ b/docs/readme/README_hu.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,7 +45,6 @@ Türkçe

-

@@ -61,14 +59,14 @@ Az OpenIM egy szolgáltatási platform, amelyet kifejezetten a csevegés, az aud Az **OpenIMSDK** egy **OpenIMServer** számára készült azonnali üzenetküldő SDK, amelyet kifejezetten ügyfélalkalmazásokba való beágyazáshoz hoztak létre. Fő jellemzői és moduljai a következők: -+ 🌟 Főbb jellemzők: +- 🌟 Főbb jellemzők: - 📦 Helyi raktár - 🔔 Hallgatói visszahívások - 🛡️ API-csomagolás - 🌐 Kapcsolatkezelés -+ 📚 Fő modulok: +- 📚 Fő modulok: 1. 🚀 Inicializálás és bejelentkezés 2. 👤 Felhasználókezelés @@ -82,15 +80,15 @@ Golang használatával készült, és támogatja a többplatformos telepítést, ## 🌐 Az OpenIMServerről -+ **OpenIMServer** a következő jellemzőkkel rendelkezik: +- **OpenIMServer** a következő jellemzőkkel rendelkezik: - 🌐 Mikroszolgáltatási architektúra: Támogatja a fürt módot, beleértve az átjárót és több rpc szolgáltatást. - 🚀 Változatos telepítési módszerek: Támogatja a forráskódon, Kubernetesen vagy Dockeren keresztül történő telepítést. - Hatalmas felhasználói bázis támogatása: Szuper nagy csoportok több százezer felhasználóval, több tízmillió felhasználóval és több milliárd üzenettel. ### Továbbfejlesztett üzleti funkcionalitás: -+ **REST API**: Az OpenIMServer REST API-kat kínál az üzleti rendszerek számára, amelyek célja, hogy a vállalkozásokat több funkcióval ruházza fel, mint például csoportok létrehozása és push üzenetek küldése háttérfelületeken keresztül. -+ **Webhooks**: Az OpenIMServer visszahívási lehetőségeket biztosít több üzleti forma kiterjesztéséhez. A visszahívás azt jelenti, hogy az OpenIMServer kérelmet küld az üzleti szervernek egy bizonyos esemény előtt vagy után, például visszahívásokat üzenet küldése előtt vagy után. +- **REST API**: Az OpenIMServer REST API-kat kínál az üzleti rendszerek számára, amelyek célja, hogy a vállalkozásokat több funkcióval ruházza fel, mint például csoportok létrehozása és push üzenetek küldése háttérfelületeken keresztül. +- **Webhooks**: Az OpenIMServer visszahívási lehetőségeket biztosít több üzleti forma kiterjesztéséhez. A visszahívás azt jelenti, hogy az OpenIMServer kérelmet küld az üzleti szervernek egy bizonyos esemény előtt vagy után, például visszahívásokat üzenet küldése előtt vagy után. 👉 **[Tudj meg többet](https://docs.openim.io/guides/introduction/product)** @@ -100,7 +98,6 @@ Merüljön el az Open-IM-Server funkcióinak szívében az architektúra diagram ![Overall Architecture](../images/architecture-layers.png) - ## :rocket: Gyors indítás Számos platformot támogatunk. Íme a címek a gyors weboldali használathoz: @@ -109,10 +106,10 @@ Számos platformot támogatunk. Íme a címek a gyors weboldali használathoz: 🤲 A felhasználói élmény megkönnyítése érdekében különféle telepítési megoldásokat kínálunk. Az alábbi listából választhatja ki a telepítési módot: -+ **[Forráskód-telepítési útmutató](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Docker telepítési útmutató](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Kubernetes telepítési útmutató](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Mac fejlesztői telepítési útmutató](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Forráskód-telepítési útmutató](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Docker telepítési útmutató](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Kubernetes telepítési útmutató](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Mac fejlesztői telepítési útmutató](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: Az OpenIM fejlesztésének megkezdéséhez @@ -122,7 +119,7 @@ OpenIM Célunk egy felső szintű nyílt forráskódú közösség felépítése Ha hozzá szeretne járulni ehhez az Open-IM-Server adattárhoz, kérjük, olvassa el [közreműködői dokumentációnkat](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md). -Mielőtt elkezdené, győződjön meg arról, hogy a változtatásokra van-e igény. Erre a legjobb egy [új beszélgetés](https://github.com/openimsdk/open-im-server/discussions/new/choose) VAGY [Slack Communication](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q)létrehozása, vagy ha problémát talál, először [jelentse](https://github.com/openimsdk/open-im-server/issues/new/choose) first. +Mielőtt elkezdené, győződjön meg arról, hogy a változtatásokra van-e igény. Erre a legjobb egy [új beszélgetés](https://github.com/openimsdk/open-im-server/discussions/new/choose) VAGY [Slack Communication](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)létrehozása, vagy ha problémát talál, először [jelentse](https://github.com/openimsdk/open-im-server/issues/new/choose) first. - [OpenIM API referencia](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [OpenIM Bash naplózás](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -154,19 +151,18 @@ Mielőtt elkezdené, győződjön meg arról, hogy a változtatásokra van-e ig - [A háttérrendszer kezelése és a telepítés figyelése](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Mac Developer Deployment Guide for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: Közösség -+ 📚 [OpenIM közösség](https://github.com/OpenIMSDK/community) -+ 💕 [OpenIM érdeklődési csoport](https://github.com/Openim-sigs) -+ 🚀 [Csatlakozz a Slack közösségünkhöz](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [Csatlakozz a wechathez](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [OpenIM közösség](https://github.com/OpenIMSDK/community) +- 💕 [OpenIM érdeklődési csoport](https://github.com/Openim-sigs) +- 🚀 [Csatlakozz a Slack közösségünkhöz](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Csatlakozz a wechathez](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: Közösségi Találkozók Szeretnénk, ha bárki bekapcsolódna közösségünkbe és hozzájárulna kódunkhoz, ajándékokat és jutalmakat kínálunk, és szeretettel várjuk, hogy csatlakozzon hozzánk minden csütörtök este. -Konferenciánk az [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯alatt van, akkor kereshet az Open-IM-Server folyamatban a csatlakozáshoz +Konferenciánk az [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯alatt van, akkor kereshet az Open-IM-Server folyamatban a csatlakozáshoz A [GitHub-beszélgetések](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)minden [kéthetente történő megbeszélésről](https://github.com/openimsdk/open-im-server/discussions/categories/meeting) jegyzeteket készítünk. A találkozók történeti feljegyzései, valamint az értekezletek visszajátszásai a [Google Dokumentumok :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) webhelyen érhetők el. diff --git a/docs/readme/README_ja.md b/docs/readme/README_ja.md index 5a083c1bf..3c633e102 100644 --- a/docs/readme/README_ja.md +++ b/docs/readme/README_ja.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,29 +45,25 @@ Türkçe

-

+## Ⓜ️ OpenIM について -## Ⓜ️ OpenIMについて - -OpenIMは、アプリケーション内でチャット、音声通話、通知、AIチャットボットなどの通信機能を統合するために特別に設計されたサービスプラットフォームです。一連の強力なAPIとWebhooksを提供することで、開発者はアプリケーションに簡単にこれらの通信機能を統合できます。OpenIM自体は独立したチャットアプリではなく、アプリケーションにサポートを提供し、豊富な通信機能を実現するプラットフォームです。以下の図は、AppServer、AppClient、OpenIMServer、OpenIMSDK間の相互作用を示しています。 - - +OpenIM は、アプリケーション内でチャット、音声通話、通知、AI チャットボットなどの通信機能を統合するために特別に設計されたサービスプラットフォームです。一連の強力な API と Webhooks を提供することで、開発者はアプリケーションに簡単にこれらの通信機能を統合できます。OpenIM 自体は独立したチャットアプリではなく、アプリケーションにサポートを提供し、豊富な通信機能を実現するプラットフォームです。以下の図は、AppServer、AppClient、OpenIMServer、OpenIMSDK 間の相互作用を示しています。 ![App-OpenIM Relationship](../images/oepnim-design.png) -## 🚀 OpenIMSDKについて +## 🚀 OpenIMSDK について - **OpenIMSDK**は、**OpenIMServer**用に設計されたIM SDKで、クライアントアプリケーションに組み込むためのものです。主な機能とモジュールは以下の通りです: +**OpenIMSDK**は、**OpenIMServer**用に設計された IM SDK で、クライアントアプリケーションに組み込むためのものです。主な機能とモジュールは以下の通りです: -+ 🌟 主な機能: +- 🌟 主な機能: - - 📦 ローカルストレージ + - 📦 ローカルストレージ - 🔔 リスナーコールバック - - 🛡️ APIのラッピング + - 🛡️ API のラッピング - 🌐 接続管理 ## 📚 主なモジュール: @@ -79,54 +74,54 @@ OpenIMは、アプリケーション内でチャット、音声通話、通知 4. 🤖 グループ機能 5. 💬 会話処理 -Golangを使用して構築され、クロスプラットフォームの導入をサポートし、すべてのプラットフォームで一貫したアクセス体験を提供します。 +Golang を使用して構築され、クロスプラットフォームの導入をサポートし、すべてのプラットフォームで一貫したアクセス体験を提供します。 -👉 **[GO SDKを探索する](https://github.com/openimsdk/openim-sdk-core)** +👉 **[GO SDK を探索する](https://github.com/openimsdk/openim-sdk-core)** -## 🌐 OpenIMServerについて +## 🌐 OpenIMServer について -+ **OpenIMServer** には以下の特徴があります: - - 🌐 マイクロサービスアーキテクチャ:クラスターモードをサポートし、ゲートウェイ(gateway)と複数のrpcサービスを含みます。 - - 🚀 多様なデプロイメント方法:ソースコード、kubernetes、またはdockerでのデプロイメントをサポートします。 +- **OpenIMServer** には以下の特徴があります: + - 🌐 マイクロサービスアーキテクチャ:クラスターモードをサポートし、ゲートウェイ(gateway)と複数の rpc サービスを含みます。 + - 🚀 多様なデプロイメント方法:ソースコード、kubernetes、または docker でのデプロイメントをサポートします。 - 海量ユーザーサポート:十万人規模の超大型グループ、千万人のユーザー、および百億のメッセージ ### 強化されたビジネス機能: -+ **REST API**:OpenIMServerは、ビジネスシステム用のREST APIを提供しており、ビジネスにさらに多くの機能を提供することを目指しています。たとえば、バックエンドインターフェースを通じてグループを作成したり、プッシュメッセージを送信したりするなどです。 -+ **Webhooks**:OpenIMServerは、より多くのビジネス形態を拡張するためのコールバック機能を提供しています。コールバックとは、特定のイベントが発生する前後に、OpenIMServerがビジネスサーバーにリクエストを送信することを意味します。例えば、メッセージ送信の前後のコールバックなどです。 +- **REST API**:OpenIMServer は、ビジネスシステム用の REST API を提供しており、ビジネスにさらに多くの機能を提供することを目指しています。たとえば、バックエンドインターフェースを通じてグループを作成したり、プッシュメッセージを送信したりするなどです。 +- **Webhooks**:OpenIMServer は、より多くのビジネス形態を拡張するためのコールバック機能を提供しています。コールバックとは、特定のイベントが発生する前後に、OpenIMServer がビジネスサーバーにリクエストを送信することを意味します。例えば、メッセージ送信の前後のコールバックなどです。 👉 **[もっと詳しく知る](https://docs.openim.io/guides/introduction/product)** ## :building_construction: 全体のアーキテクチャ -Open-IM-Serverの機能の核心に迫るために、アーキテクチャダイアグラムをご覧ください。 +Open-IM-Server の機能の核心に迫るために、アーキテクチャダイアグラムをご覧ください。 ![Overall Architecture](../images/architecture-layers.png) ## :rocket: クイックスタート -iOS/Android/H5/PC/Webでのオンライン体験: +iOS/Android/H5/PC/Web でのオンライン体験: 👉 **[OpenIM online demo](https://www.openim.io/zh/commercial)** 🤲 ユーザー体験を容易にするために、私たちは様々なデプロイメントソリューションを提供しています。以下のリストから、ご自身のデプロイメント方法を選択できます: -+ **[ソースコードデプロイメントガイド](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Docker デプロイメントガイド](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Kubernetes デプロイメントガイド](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Mac 開発者向けデプロイメントガイド](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[ソースコードデプロイメントガイド](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Docker デプロイメントガイド](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Kubernetes デプロイメントガイド](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Mac 開発者向けデプロイメントガイド](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** -## :hammer_and_wrench: OpenIMの開発を始める +## :hammer_and_wrench: OpenIM の開発を始める [![Open in Dev Container](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server) OpenIM 私たちの目標は、トップレベルのオープンソースコミュニティを構築することです。[コミュニティリポジトリ](https://github.com/OpenIMSDK/community)には一連の基準があります。 -このOpen-IM-Serverリポジトリに貢献したい場合は、[貢献者ドキュメントをお読みください](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)。 +この Open-IM-Server リポジトリに貢献したい場合は、[貢献者ドキュメントをお読みください](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)。 -始める前に、変更に必要があることを確認してください。最良の方法は、[新しいディスカッション](https://github.com/openimsdk/open-im-server/discussions/new/choose)や[Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q)での通信を作成すること、または問題を発見した場合は、まずそれを[報告](https://github.com/openimsdk/open-im-server/issues/new/choose)することです。 +始める前に、変更に必要があることを確認してください。最良の方法は、[新しいディスカッション](https://github.com/openimsdk/open-im-server/discussions/new/choose)や[Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)での通信を作成すること、または問題を発見した場合は、まずそれを[報告](https://github.com/openimsdk/open-im-server/issues/new/choose)することです。 -- [OpenIM APIリファレンス](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) +- [OpenIM API リファレンス](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [OpenIM Bash ロギング](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) - [OpenIM CI/CD アクション](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/cicd-actions.md) - [OpenIM コード規約](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/code-conventions.md) @@ -149,37 +144,35 @@ OpenIM 私たちの目標は、トップレベルのオープンソースコミ - [OpenIM オフラインデプロイメント](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/offline-deployment.md) - [OpenIM Protoc ツール](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/protoc-tools.md) - [OpenIM テスティングガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/test.md) -- [OpenIM ユーティリティGo](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md) +- [OpenIM ユーティリティ Go](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-go.md) - [OpenIM Makefile ユーティリティ](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-makefile.md) - [OpenIM スクリプトユーティリティ](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/util-scripts.md) - [OpenIM バージョニング](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/version.md) - [バックエンド管理とモニターデプロイメント](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) -- [OpenIM用Mac開発者デプロイメントガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - +- [OpenIM 用 Mac 開発者デプロイメントガイド](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) ## :busts_in_silhouette: コミュニティ -+ 📚 [OpenIM コミュニティ](https://github.com/OpenIMSDK/community) -+ 💕 [OpenIM 興味グループ](https://github.com/Openim-sigs) -+ 🚀 [私たちのSlackコミュニティに参加する](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [私たちのWeChat(微信群)に参加する](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [OpenIM コミュニティ](https://github.com/OpenIMSDK/community) +- 💕 [OpenIM 興味グループ](https://github.com/Openim-sigs) +- 🚀 [私たちの Slack コミュニティに参加する](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [私たちの WeChat(微信群)に参加する](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: コミュニティミーティング 私たちは、誰もがコミュニティに参加し、コードに貢献してもらいたいと考えています。私たちは、ギフトや報酬を提供し、毎週木曜日の夜に参加していただくことを歓迎します。 -私たちの会議は[OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q)🎯で行われます。そこでOpen-IM-Serverパイプラインを検索して参加できます。 - +私たちの会議は[OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)🎯 で行われます。そこで Open-IM-Server パイプラインを検索して参加できます。 -私たちは[隔週の会議](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)のメモを[GitHubディスカッション](https://github.com/openimsdk/open-im-server/discussions/categories/meeting)に記録しています。歴史的な会議のメモや会議のリプレイは[Google Docs📑](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing)で利用可能です。 +私たちは[隔週の会議](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)のメモを[GitHub ディスカッション](https://github.com/openimsdk/open-im-server/discussions/categories/meeting)に記録しています。歴史的な会議のメモや会議のリプレイは[Google Docs📑](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing)で利用可能です。 -## :eyes: OpenIMを使用している人たち +## :eyes: OpenIM を使用している人たち -プロジェクトユーザーのリストについては、[ユーザーケーススタディ](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md)ページをご覧ください。[コメント📝](https://github.com/openimsdk/open-im-server/issues/379)を残して、あなたの使用例を共有することを躊躇しないでください。 +プロジェクトユーザーのリストについては、[ユーザーケーススタディ](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md)ページをご覧ください。[コメント 📝](https://github.com/openimsdk/open-im-server/issues/379)を残して、あなたの使用例を共有することを躊躇しないでください。 ## :page_facing_up: ライセンス -OpenIMはApache 2.0ライセンスの下でライセンスされています。完全なライセンステキストについては、[LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE)を参照してください。 +OpenIM は Apache 2.0 ライセンスの下でライセンスされています。完全なライセンステキストについては、[LICENSE](https://github.com/openimsdk/open-im-server/tree/main/LICENSE)を参照してください。 このリポジトリに表示される[OpenIM](https://github.com/openimsdk/open-im-server)ロゴ、そのバリエーション、およびアニメーションバージョン([assets/logo](./assets/logo)および[assets/logo-gif](assets/logo-gif)ディレクトリ内)は、著作権法によって保護されています。 @@ -187,4 +180,4 @@ OpenIMはApache 2.0ライセンスの下でライセンスされています。 - \ No newline at end of file + diff --git a/docs/readme/README_ko.md b/docs/readme/README_ko.md index ebcdd71ee..9b61e260d 100644 --- a/docs/readme/README_ko.md +++ b/docs/readme/README_ko.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,26 +45,21 @@ Türkçe

-

- ## Ⓜ️ OpenIM에 대하여 OpenIM은 채팅, 오디오-비디오 통화, 알림 및 AI 챗봇을 애플리케이션에 통합하기 위해 특별히 설계된 서비스 플랫폼입니다. 이 플랫폼은 강력한 API와 웹훅을 제공하여 개발자가 이러한 상호작용 기능을 애플리케이션에 쉽게 통합할 수 있게 합니다. OpenIM은 독립 실행형 채팅 애플리케이션이 아니라, 다른 애플리케이션들이 풍부한 커뮤니케이션 기능을 달성할 수 있도록 지원하는 플랫폼으로서의 역할을 합니다. 다음 다이어그램은 AppServer, AppClient, OpenIMServer, 및 OpenIMSDK 간의 상호작용을 자세히 설명하기 위해 제시되었습니다. - - ![App-OpenIM Relationship](../images/oepnim-design.png) ## 🚀 OpenIMSDK에 대하여 - **OpenIMSDK**는**OpenIMServer**를 위해 특별히 제작된 IM SDK로, 클라이언트 애플리케이션 내에 내장하기 위해 설계되었습니다. 그 주요 기능 및 모듈은 다음과 같습니다: +**OpenIMSDK**는**OpenIMServer**를 위해 특별히 제작된 IM SDK로, 클라이언트 애플리케이션 내에 내장하기 위해 설계되었습니다. 그 주요 기능 및 모듈은 다음과 같습니다: - -+ 🌟 주요 기능: +- 🌟 주요 기능: - 📦 로컬 스토리지 - 🔔 리스너 콜백 @@ -86,15 +80,15 @@ OpenIM은 채팅, 오디오-비디오 통화, 알림 및 AI 챗봇을 애플리 ## 🌐 OpenIMServer에 대하여 -+ **OpenIMServer** 는 다음과 같은 특성을 가지고 있습니다: +- **OpenIMServer** 는 다음과 같은 특성을 가지고 있습니다: - 🌐 마이크로서비스 아키텍처: 게이트웨이 및 다수의 rpc 서비스를 포함하는 클러스터 모드를 지원합니다. - - 🚀 다양한 배포 방법: 소스 코드, 쿠버네티스 또는 도커를 통한 배포를 지원합니다. + - 🚀 다양한 배포 방법: 소스 코드, 쿠버네티스 또는 도커를 통한 배포를 지원합니다. - 대규모 사용자 기반 지원: 수십만 명의 사용자를 포함하는 초대형 그룹, 수천만 명의 사용자 및 수십억 건의 메시지를 지원합니다. ### 강화된 비즈니스 기능: -+ **REST API**:OpenIMServer는 비즈니스 시스템을 위한 REST API를 제공하여, 백엔드 인터페이스를 통해 그룹 생성 및 푸시 메시지 전송과 같은 더 많은 기능을 비즈니스에 제공하기 위해 설계되었습니다. -+ **Webhooks**:OpenIMServer는 더 많은 비즈니스 형태를 확장할 수 있는 콜백 기능을 제공합니다. 콜백이란 메시지 전송 전후와 같은 특정 이벤트 전후에 OpenIMServer가 비즈니스 서버로 요청을 보내는 것을 의미합니다. +- **REST API**:OpenIMServer는 비즈니스 시스템을 위한 REST API를 제공하여, 백엔드 인터페이스를 통해 그룹 생성 및 푸시 메시지 전송과 같은 더 많은 기능을 비즈니스에 제공하기 위해 설계되었습니다. +- **Webhooks**:OpenIMServer는 더 많은 비즈니스 형태를 확장할 수 있는 콜백 기능을 제공합니다. 콜백이란 메시지 전송 전후와 같은 특정 이벤트 전후에 OpenIMServer가 비즈니스 서버로 요청을 보내는 것을 의미합니다. 👉 **[더 알아보기](https://docs.openim.io/guides/introduction/product)** @@ -112,10 +106,10 @@ Open-IM-Server의 기능의 핵심으로 들어가 우리의 아키텍처 다이 🤲 사용자 경험을 용이하게 하기 위해, 다양한 배포 솔루션을 제공합니다. 아래 목록에서 배포 방법을 선택할 수 있습니다: -+ **[소스 코드 배포 가이드](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[docker 배포 가이드](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Kubernetes 배포 가이드](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Mac 개발자 배포 가이드](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[소스 코드 배포 가이드](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[docker 배포 가이드](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Kubernetes 배포 가이드](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Mac 개발자 배포 가이드](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: OpenIM 개발 시작하기 @@ -125,7 +119,7 @@ OpenIM의 목표는 최상위 수준의 오픈 소스 커뮤니티를 구축하 이 Open-IM-Server 리포지토리에 기여하고 싶다면, 우리의 [기여자 문서](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md)를 읽어주세요. -시작하기 전에, 변경 사항이 필요한지 확인해 주세요. 가장 좋은 방법은 [새로운 토론](https://github.com/openimsdk/open-im-server/discussions/new/choose)을 생성하거나 [Slack 통신을](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 하거나, 문제를 발견했다면 먼저 [보고](https://github.com/openimsdk/open-im-server/issues/new/choose)하는 것입니다. +시작하기 전에, 변경 사항이 필요한지 확인해 주세요. 가장 좋은 방법은 [새로운 토론](https://github.com/openimsdk/open-im-server/discussions/new/choose)을 생성하거나 [Slack 통신을](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 하거나, 문제를 발견했다면 먼저 [보고](https://github.com/openimsdk/open-im-server/issues/new/choose)하는 것입니다. - [OpenIM API 참조](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [OpenIM Bash 로깅](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -157,23 +151,21 @@ OpenIM의 목표는 최상위 수준의 오픈 소스 커뮤니티를 구축하 - [백엔드 관리 및 모니터 배포](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [맥 개발자 배포 가이드 for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: 커뮤니티 -+ 📚 [OpenIM 커뮤니티](https://github.com/OpenIMSDK/community) -+ 💕 [OpenIM 관심 그룹](https://github.com/Openim-sigs) -+ 🚀 [우리의 Slack 커뮤니티에 가입하기](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [우리의 위챗(微信群)에 가입하기](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [OpenIM 커뮤니티](https://github.com/OpenIMSDK/community) +- 💕 [OpenIM 관심 그룹](https://github.com/Openim-sigs) +- 🚀 [우리의 Slack 커뮤니티에 가입하기](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [우리의 위챗(微信群)에 가입하기](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: 커뮤니티 미팅 우리는 누구나 커뮤니티에 참여하고 코드를 기여할 수 있도록 하며, 선물과 보상을 제공하며, 매주 목요일 밤에 여러분을 환영합니다. -우리의 회의는 [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯에서 이루어지며, Open-IM-Server 파이프라인을 검색하여 참여할 수 있습니다. +우리의 회의는 [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯에서 이루어지며, Open-IM-Server 파이프라인을 검색하여 참여할 수 있습니다. 우리는 격주 회의의 메모를 [GitHub 토론](https://github.com/openimsdk/open-im-server/discussions/categories/meeting)에서 기록하며, 우리의 역사적 [회의](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) 노트와 회의 재생은 [Google Docs 📑](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing)에서 이용할 수 있습니다. - ## :eyes: OpenIM을 사용하는 사람들 프로젝트 사용자 목록을 위한 우리의 [사용자 사례 연구](https://github.com/OpenIMSDK/community/blob/main/ADOPTERS.md) 페이지를 확인하세요. 사용 사례를 공유하고 싶다면 주저하지 말고 [📝코멘트](https://github.com/openimsdk/open-im-server/issues/379)를 남겨주세요. @@ -184,9 +176,8 @@ OpenIM은 Apache 2.0 라이선스에 따라 라이선스가 부여됩니다. 전 이 리포지토리 [OpenIM](https://github.com/openimsdk/open-im-server)에 표시된 OpenIM 로고, 그 변형 및 애니메이션 버전은 [assets/logo](../../assets/logo) 및 [assets/logo-gif](../../assets/logo-gif) 디렉토리 아래에 있으며, 저작권 법에 의해 보호됩니다. - ## 🔮 우리의 기여자들에게 감사합니다! - \ No newline at end of file + diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md index 3cf19f537..ec2eed8bb 100644 --- a/docs/readme/README_tr.md +++ b/docs/readme/README_tr.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,25 +45,21 @@ Türkçe

-

- ## Ⓜ️ OpenIM Hakkında OpenIM, uygulamalara sohbet, sesli-görüntülü aramalar, bildirimler ve AI sohbet robotları entegre etmek için özel olarak tasarlanmış bir hizmet platformudur. Güçlü API'ler ve Webhook'lar sunarak, geliştiricilerin bu etkileşimli özellikleri uygulamalarına kolayca dahil etmelerini sağlar. OpenIM bağımsız bir sohbet uygulaması değildir, ancak zengin iletişim işlevselliği sağlama amacıyla diğer uygulamaları destekleyen bir platform olarak hizmet verir. Aşağıdaki diyagram, AppServer, AppClient, OpenIMServer ve OpenIMSDK arasındaki etkileşimi detaylandırmak için açıklar. - - ![App-OpenIM Relationship](../images/oepnim-design.png) ## 🚀 OpenIMSDK Hakkında - **OpenIMSDK**, müşteri uygulamalarına gömülmek üzere özel olarak oluşturulan **OpenIMServer** için tasarlanmış bir IM SDK'sıdır. Ana özellikleri ve modülleri aşağıdaki gibidir: +**OpenIMSDK**, müşteri uygulamalarına gömülmek üzere özel olarak oluşturulan **OpenIMServer** için tasarlanmış bir IM SDK'sıdır. Ana özellikleri ve modülleri aşağıdaki gibidir: -+ 🌟 Ana Özellikler: +- 🌟 Ana Özellikler: - 📦 Yerel depolama - 🔔 Dinleyici geri çağırmaları @@ -85,15 +80,15 @@ Golang kullanılarak inşa edilmiş ve tüm platformlarda tutarlı bir erişim d ## 🌐 OpenIMServer Hakkında -+ **OpenIMServer** aşağıdaki özelliklere sahiptir: +- **OpenIMServer** aşağıdaki özelliklere sahiptir: - 🌐 Mikroservis mimarisi: Bir kapı ve çoklu rpc servisleri içeren küme modunu destekler. - 🚀 Çeşitli dağıtım yöntemleri: Kaynak kodu, Kubernetes veya Docker aracılığıyla dağıtımı destekler. - Büyük kullanıcı tabanı desteği: Yüz binlerce kullanıcısı olan süper büyük gruplar, on milyonlarca kullanıcı ve milyarlarca mesaj. ### Geliştirilmiş İşlevsellik: -+ **REST API**:OpenIMServer, işletmeleri gruplar oluşturma ve arka plan arayüzleri aracılığıyla itme mesajları gönderme gibi daha fazla işlevsellikle güçlendirmeyi amaçlayan iş sistemleri için REST API'leri sunar. -+ **Webhooks**:OpenIMServer, daha fazla iş formunu genişletme yetenekleri sağlayan geri çağırma özellikleri sunar. Geri çağırma, OpenIMServer'ın belirli bir olaydan önce veya sonra, örneğin bir mesaj göndermeden önce veya sonra iş sunucusuna bir istek göndermesi anlamına gelir. +- **REST API**:OpenIMServer, işletmeleri gruplar oluşturma ve arka plan arayüzleri aracılığıyla itme mesajları gönderme gibi daha fazla işlevsellikle güçlendirmeyi amaçlayan iş sistemleri için REST API'leri sunar. +- **Webhooks**:OpenIMServer, daha fazla iş formunu genişletme yetenekleri sağlayan geri çağırma özellikleri sunar. Geri çağırma, OpenIMServer'ın belirli bir olaydan önce veya sonra, örneğin bir mesaj göndermeden önce veya sonra iş sunucusuna bir istek göndermesi anlamına gelir. 👉 **[Daha fazla bilgi edinin](https://docs.openim.io/guides/introduction/product)** @@ -111,10 +106,10 @@ Birçok platformu destekliyoruz. Web tarafında hızlı deneyim için adresler 🤲 Kullanıcı deneyimini kolaylaştırmak için çeşitli dağıtım çözümleri sunuyoruz. Aşağıdaki listeden dağıtım yönteminizi seçebilirsiniz: -+ **[Kaynak Kodu Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Docker Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Kubernetes Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Mac Geliştirici Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Kaynak Kodu Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Docker Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Kubernetes Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Mac Geliştirici Dağıtım Kılavuzu](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: OpenIM Geliştirmeye Başlamak @@ -124,7 +119,7 @@ OpenIM Amacımız, üst düzey bir açık kaynak topluluğu oluşturmaktır. [To Bu Open-IM-Server deposuna katkıda bulunmak istiyorsanız, lütfen katkıda bulunanlar için [dokümantasyonumuzu](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md) okuyun. -Başlamadan önce, lütfen değişikliklerinizin talep edildiğinden emin olun. Bunun için en iyisi, [yeni bir tartışma OLUŞTURMAK](https://github.com/openimsdk/open-im-server/discussions/new/choose) veya [Slack İletişimi](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) kurmak, ya da bir sorun bulursanız, önce bunu [rapor](https://github.com/openimsdk/open-im-server/issues/new/choose) etmektir. +Başlamadan önce, lütfen değişikliklerinizin talep edildiğinden emin olun. Bunun için en iyisi, [yeni bir tartışma OLUŞTURMAK](https://github.com/openimsdk/open-im-server/discussions/new/choose) veya [Slack İletişimi](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) kurmak, ya da bir sorun bulursanız, önce bunu [rapor](https://github.com/openimsdk/open-im-server/issues/new/choose) etmektir. - [OpenIM API Referansı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [OpenIM Bash Günlüğü](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -156,19 +151,18 @@ Başlamadan önce, lütfen değişikliklerinizin talep edildiğinden emin olun. - [Arka uç yönetimi ve izleme dağıtımı](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Mac Geliştirici Dağıtım Kılavuzu for OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: Topluluk -+ 📚 [OpenIM Topluluğu](https://github.com/OpenIMSDK/community) -+ 💕 [OpenIM İlgi Grubu](https://github.com/Openim-sigs) -+ 🚀 [Slack topluluğumuza katılın](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [Wechat grubumuza katılın (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [OpenIM Topluluğu](https://github.com/OpenIMSDK/community) +- 💕 [OpenIM İlgi Grubu](https://github.com/Openim-sigs) +- 🚀 [Slack topluluğumuza katılın](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Wechat grubumuza katılın (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: Topluluk Toplantıları Topluluğumuza herkesin katılmasını ve kod katkısında bulunmasını istiyoruz, hediyeler ve ödüller sunuyoruz ve sizi her Perşembe gecesi bize katılmaya davet ediyoruz. -Konferansımız [OpenIM Slack'te](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, ardından Open-IM-Server boru hattını arayıp katılabilirsiniz. +Konferansımız [OpenIM Slack'te](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, ardından Open-IM-Server boru hattını arayıp katılabilirsiniz. İki haftada bir yapılan toplantının [notlarını](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) [GitHub tartışmalarında alıyoruz](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), Tarihi toplantı notlarımız ve toplantıların tekrarları [Google Docs'ta](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing) 📑 mevcut. @@ -186,4 +180,4 @@ Bu depoda, [assets/logo](../../assets/logo) ve [assets/logo-gif](../../assets/lo - \ No newline at end of file + diff --git a/docs/readme/README_uk.md b/docs/readme/README_uk.md index 81820590b..a14905bc5 100644 --- a/docs/readme/README_uk.md +++ b/docs/readme/README_uk.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,7 +45,6 @@ Türkçe

-

@@ -61,14 +59,14 @@ OpenIM — це сервісна платформа, спеціально роз **OpenIMSDK** – це пакет IM SDK, розроблений для **OpenIMServer**, створений спеціально для вбудовування в клієнтські програми. Його основні функції та модулі такі: -+ 🌟 Основні характеристики: +- 🌟 Основні характеристики: - 📦 Локальне сховище - 🔔 Зворотні виклики слухача - 🛡️ Обгортка API - 🌐 Керування підключенням -+ 📚 Основні модулі: +- 📚 Основні модулі: 1. 🚀 Ініціалізація та вхід 2. 👤 Керування користувачами @@ -82,15 +80,15 @@ OpenIM — це сервісна платформа, спеціально роз ## 🌐 Про OpenIMServer -+ **OpenIMServer** має такі характеристики: +- **OpenIMServer** має такі характеристики: - 🌐 Архітектура мікросервісу: підтримує режим кластера, включаючи шлюз і кілька служб rpc. - 🚀 Різноманітні методи розгортання: підтримує розгортання через вихідний код, Kubernetes або Docker. - Підтримка величезної бази користувачів: надвеликі групи із сотнями тисяч користувачів, десятками мільйонів користувачів і мільярдами повідомлень. ### Розширена бізнес-функціональність: -+ **REST API**: OpenIMServer пропонує REST API для бізнес-систем, спрямованих на надання компаніям додаткових можливостей, таких як створення груп і надсилання push-повідомлень через серверні інтерфейси. -+ **Веб-перехоплення**: OpenIMServer надає можливості зворотного виклику, щоб розширити більше бізнес-форм. Зворотний виклик означає, що OpenIMServer надсилає запит на бізнес-сервер до або після певної події, як зворотні виклики до або після надсилання повідомлення. +- **REST API**: OpenIMServer пропонує REST API для бізнес-систем, спрямованих на надання компаніям додаткових можливостей, таких як створення груп і надсилання push-повідомлень через серверні інтерфейси. +- **Веб-перехоплення**: OpenIMServer надає можливості зворотного виклику, щоб розширити більше бізнес-форм. Зворотний виклик означає, що OpenIMServer надсилає запит на бізнес-сервер до або після певної події, як зворотні виклики до або після надсилання повідомлення. 👉 **[Докладніше](https://docs.openim.io/guides/introduction/product)** @@ -100,7 +98,6 @@ OpenIM — це сервісна платформа, спеціально роз ![Overall Architecture](../images/architecture-layers.png) - ## :rocket: Швидкий початок Ми підтримуємо багато платформ. Ось адреси для швидкого використання веб-сайту: @@ -109,10 +106,10 @@ OpenIM — це сервісна платформа, спеціально роз 🤲 Щоб полегшити роботу користувача, ми пропонуємо різні рішення для розгортання. Ви можете вибрати спосіб розгортання зі списку нижче: -+ **[Посібник із розгортання вихідного коду](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Посібник із розгортання Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Посібник із розгортання Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Посібник із розгортання розробника Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Посібник із розгортання вихідного коду](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Посібник із розгортання Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Посібник із розгортання Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Посібник із розгортання розробника Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** ## :hammer_and_wrench: Щоб розпочати розробку OpenIM @@ -122,7 +119,7 @@ OpenIM. Наша мета — побудувати спільноту з від Якщо ви хочете внести свій внесок у це сховище Open-IM-Server, прочитайте нашу [документацію для учасників](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md). -Перш ніж почати, переконайтеся, що ваші зміни затребувані. Найкраще для цього створити [нове обговорення](https://github.com/openimsdk/open-im-server/discussions/new/choose) АБО [Нездійснене спілкування](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q)або, якщо ви виявите проблему, спершу [повідомити про неї](https://github.com/openimsdk/open-im-server/issues/new/choose). +Перш ніж почати, переконайтеся, що ваші зміни затребувані. Найкраще для цього створити [нове обговорення](https://github.com/openimsdk/open-im-server/discussions/new/choose) АБО [Нездійснене спілкування](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A)або, якщо ви виявите проблему, спершу [повідомити про неї](https://github.com/openimsdk/open-im-server/issues/new/choose). - [Довідка щодо API OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [Ведення журналу OpenIM Bash](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -154,19 +151,18 @@ OpenIM. Наша мета — побудувати спільноту з від - [Керування серверною частиною та моніторинг розгортання](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Посібник із розгортання розробника Mac для OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: Спільнота -+ 📚 [Спільнота OpenIM](https://github.com/OpenIMSDK/community) -+ 💕 [Група інтересів OpenIM](https://github.com/Openim-sigs) -+ 🚀 [Приєднайтеся до нашої спільноти Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [Приєднайтеся до нашого wechat](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [Спільнота OpenIM](https://github.com/OpenIMSDK/community) +- 💕 [Група інтересів OpenIM](https://github.com/Openim-sigs) +- 🚀 [Приєднайтеся до нашої спільноти Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Приєднайтеся до нашого wechat](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: Збори громади Ми хочемо, щоб будь-хто долучився до нашої спільноти та додав код, ми пропонуємо подарунки та нагороди, і ми запрошуємо вас приєднатися до нас щочетверга ввечері. -Наша конференція знаходиться в [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, тоді ви можете шукати конвеєр Open-IM-Server, щоб приєднатися. +Наша конференція знаходиться в [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, тоді ви можете шукати конвеєр Open-IM-Server, щоб приєднатися. Ми робимо нотатки про кожну [двотижневу зустріч](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting)в [обговореннях GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting). Наші історичні нотатки зустрічей, а також повтори зустрічей доступні в[Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing). diff --git a/docs/readme/README_vi.md b/docs/readme/README_vi.md index a6ab39253..f8894c87f 100644 --- a/docs/readme/README_vi.md +++ b/docs/readme/README_vi.md @@ -12,12 +12,11 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/openimsdk/open-im-server?style=for-the-badge)](https://goreportcard.com/report/github.com/openimsdk/open-im-server) [![Go Reference](https://img.shields.io/badge/Go%20Reference-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://pkg.go.dev/github.com/openimsdk/open-im-server/v3) [![License](https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge)](https://github.com/openimsdk/open-im-server/blob/main/LICENSE) -[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) +[![Slack](https://img.shields.io/badge/Slack-500%2B-blueviolet?style=for-the-badge&logo=slack&logoColor=white)](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) [![Best Practices](https://img.shields.io/badge/Best%20Practices-purple?style=for-the-badge)](https://www.bestpractices.dev/projects/8045) [![Good First Issues](https://img.shields.io/github/issues/openimsdk/open-im-server/good%20first%20issue?style=for-the-badge&logo=github)](https://github.com/openimsdk/open-im-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) [![Language](https://img.shields.io/badge/Language-Go-blue.svg?style=for-the-badge&logo=go&logoColor=white)](https://golang.org/) -

English · 中文 · @@ -46,7 +45,6 @@ Türkçe

-

@@ -61,14 +59,14 @@ OpenIM là một nền tảng dịch vụ được thiết kế đặc biệt ch **OpenIMSDK** là một SDK IM được thiết kế cho **OpenIMServer**, được tạo ra đặc biệt để nhúng vào các ứng dụng khách. Các tính năng chính và các mô-đun của nó như sau: -+ 🌟 Các Tính Năng Chính: +- 🌟 Các Tính Năng Chính: - 📦 Lưu trữ cục bộ - 🔔 Gọi lại sự kiện (Listener callbacks) - 🛡️ Bọc API - 🌐 Quản lý kết nối -+ 📚 Các Mô-đun Chính: +- 📚 Các Mô-đun Chính: 1. 🚀 Khởi tạo và Đăng nhập 2. 👤 Quản lý Người dùng @@ -82,25 +80,24 @@ Nó được xây dựng bằng Golang và hỗ trợ triển khai đa nền t ## 🌐 Về OpenIMServer -+ **OpenIMServer** có những đặc điểm sau: +- **OpenIMServer** có những đặc điểm sau: - 🌐 Kiến trúc vi dịch vụ: Hỗ trợ chế độ cluster, bao gồm một gateway và nhiều dịch vụ rpc. - 🚀 Phương pháp triển khai đa dạng: Hỗ trợ triển khai qua mã nguồn, Kubernetes hoặc Docker. - Hỗ trợ cho cơ sở người dùng lớn: Nhóm siêu lớn với hàng trăm nghìn người dùng, hàng chục triệu người dùng và hàng tỷ tin nhắn. ### Tăng cường Chức năng Kinh doanh: -+ **REST API**: OpenIMServer cung cấp REST APIs cho các hệ thống kinh doanh, nhằm tăng cường khả năng cho doanh nghiệp với nhiều chức năng hơn, như tạo nhóm và gửi tin nhắn đẩy qua giao diện backend. -+ **Webhooks**: OpenIMServer cung cấp khả năng gọi lại để mở rộng thêm hình thức kinh doanh. Một gọi lại có nghĩa là OpenIMServer gửi một yêu cầu đến máy chủ kinh doanh trước hoặc sau một sự kiện nhất định, giống như gọi lại trước hoặc sau khi gửi một tin nhắn. +- **REST API**: OpenIMServer cung cấp REST APIs cho các hệ thống kinh doanh, nhằm tăng cường khả năng cho doanh nghiệp với nhiều chức năng hơn, như tạo nhóm và gửi tin nhắn đẩy qua giao diện backend. +- **Webhooks**: OpenIMServer cung cấp khả năng gọi lại để mở rộng thêm hình thức kinh doanh. Một gọi lại có nghĩa là OpenIMServer gửi một yêu cầu đến máy chủ kinh doanh trước hoặc sau một sự kiện nhất định, giống như gọi lại trước hoặc sau khi gửi một tin nhắn. 👉 **[Learn more](https://docs.openim.io/guides/introduction/product)** ## :building_construction: Kiến trúc tổng thể - Làm sâu sắc vào trái tim của chức năng Open-IM-Server với sơ đồ kiến trúc của chúng tôi. +Làm sâu sắc vào trái tim của chức năng Open-IM-Server với sơ đồ kiến trúc của chúng tôi. ![Overall Architecture](../../docs/images/architecture-layers.png) - ## :rocket: Bắt đầu nhanh Chúng tôi hỗ trợ nhiều nền tảng. Dưới đây là các địa chỉ để trải nghiệm nhanh trên phía web: @@ -109,12 +106,12 @@ Chúng tôi hỗ trợ nhiều nền tảng. Dưới đây là các địa chỉ 🤲 Để tạo thuận lợi cho trải nghiệm người dùng, chúng tôi cung cấp các giải pháp triển khai đa dạng. Bạn có thể chọn phương thức triển khai từ danh sách dưới đây: -+ **[Hướng dẫn Triển khai Mã Nguồn](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** -+ **[Hướng dẫn Triển khai Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** -+ **[Hướng dẫn Triển khai Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** -+ **[Hướng dẫn Triển khai cho Nhà Phát Triển Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** +- **[Hướng dẫn Triển khai Mã Nguồn](https://docs.openim.io/guides/gettingStarted/imSourceCodeDeployment)** +- **[Hướng dẫn Triển khai Docker](https://docs.openim.io/guides/gettingStarted/dockerCompose)** +- **[Hướng dẫn Triển khai Kubernetes](https://docs.openim.io/guides/gettingStarted/k8s-deployment)** +- **[Hướng dẫn Triển khai cho Nhà Phát Triển Mac](https://docs.openim.io/guides/gettingstarted/mac-deployment-guide)** -## :hammer_and_wrench: Để Bắt Đầu Phát Triển OpenIM +## :hammer_and_wrench: Để Bắt Đầu Phát Triển OpenIM [![Mở trong Dev Contain](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/github/openimsdk/open-im-server) @@ -122,8 +119,7 @@ Mục tiêu của OpenIM là xây dựng một cộng đồng mã nguồn mở c Nếu bạn muốn đóng góp cho kho lưu trữ Open-IM-Server này, vui lòng đọc [tài liệu hướng dẫn cho người đóng góp](https://github.com/openimsdk/open-im-server/blob/main/CONTRIBUTING.md). - -Trước khi bạn bắt đầu, hãy chắc chắn rằng các thay đổi của bạn được yêu cầu. Cách tốt nhất là tạo một [cuộc thảo luận mới](https://github.com/openimsdk/open-im-server/discussions/new/choose) hoặc [Giao tiếp Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q), hoặc nếu bạn tìm thấy một vấn đề, [báo cáo nó ](https://github.com/openimsdk/open-im-server/issues/new/choose) trước. +Trước khi bạn bắt đầu, hãy chắc chắn rằng các thay đổi của bạn được yêu cầu. Cách tốt nhất là tạo một [cuộc thảo luận mới](https://github.com/openimsdk/open-im-server/discussions/new/choose) hoặc [Giao tiếp Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A), hoặc nếu bạn tìm thấy một vấn đề, [báo cáo nó ](https://github.com/openimsdk/open-im-server/issues/new/choose) trước. - [Tham khảo API OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/api.md) - [Nhật ký Bash OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/bash-log.md) @@ -155,19 +151,18 @@ Trước khi bạn bắt đầu, hãy chắc chắn rằng các thay đổi củ - [Quản lý triển khai và giám sát backend](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/prometheus-grafana.md) - [Hướng dẫn Triển khai cho Nhà Phát triển Mac OpenIM](https://github.com/openimsdk/open-im-server/tree/main/docs/contrib/mac-developer-deployment-guide.md) - ## :busts_in_silhouette: Cộng đồng -+ 📚 [Cộng đồng OpenIM](https://github.com/OpenIMSDK/community) -+ 💕 [Nhóm Quan tâm OpenIM](https://github.com/Openim-sigs) -+ 🚀 [Tham gia cộng đồng Slack của chúng tôi](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) -+ :eyes: [Tham gia nhóm WeChat của chúng tôi (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) +- 📚 [Cộng đồng OpenIM](https://github.com/OpenIMSDK/community) +- 💕 [Nhóm Quan tâm OpenIM](https://github.com/Openim-sigs) +- 🚀 [Tham gia cộng đồng Slack của chúng tôi](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) +- :eyes: [Tham gia nhóm WeChat của chúng tôi (微信群)](https://openim-1253691595.cos.ap-nanjing.myqcloud.com/WechatIMG20.jpeg) ## :calendar: Cuộc họp Cộng đồng Chúng tôi muốn bất kỳ ai cũng có thể tham gia cộng đồng và đóng góp mã nguồn, chúng tôi cung cấp quà tặng và phần thưởng, và chúng tôi chào đón bạn tham gia cùng chúng tôi mỗi tối thứ Năm. -Hội nghị của chúng tôi được tổ chức trên Slack của [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q) 🎯, sau đó bạn có thể tìm kiếm pipeline Open-IM-Server để tham gia +Hội nghị của chúng tôi được tổ chức trên Slack của [OpenIM Slack](https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A) 🎯, sau đó bạn có thể tìm kiếm pipeline Open-IM-Server để tham gia Chúng tôi ghi chú mỗi [cuộc họp hai tuần một lần](https://github.com/orgs/OpenIMSDK/discussions/categories/meeting) trong [các cuộc thảo luận GitHub](https://github.com/openimsdk/open-im-server/discussions/categories/meeting), ghi chú cuộc họp lịch sử của chúng tôi cũng như các bản ghi lại của cuộc họp có sẵn tại [Google Docs :bookmark_tabs:](https://docs.google.com/document/d/1nx8MDpuG74NASx081JcCpxPgDITNTpIIos0DS6Vr9GU/edit?usp=sharing). diff --git a/install.sh b/install.sh index 4b8e74c55..d07f97338 100755 --- a/install.sh +++ b/install.sh @@ -531,7 +531,7 @@ O:::::::OOO:::::::O p:::::ppppp:::::::pe::::::::e n::::n n::::nII: # Set text color to yellow for the Slack link echo -e "\033[1;33m" - print_with_delay "Join our developer community on Slack: https://join.slack.com/t/openimsdk/shared_invite/zt-22720d66b-o_FvKxMTGXtcnnnHiMqe9Q" 0.01 + print_with_delay "Join our developer community on Slack: https://join.slack.com/t/openimsdk/shared_invite/zt-2ijy1ys1f-O0aEDCr7ExRZ7mwsHAVg9A" 0.01 # Reset text color back to normal echo -e "\033[0m" From 3ad24d95214a910face80fceba3f3f68db073c56 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:52:25 +0800 Subject: [PATCH 58/59] Update CHANGELOG for release v3.8.3-patch.1 (#3164) Co-authored-by: withchao <48119764+withchao@users.noreply.github.com> --- CHANGELOG/CHANGELOG-3.8.md | 19 +++++++++++++++++++ CHANGELOG/README.md | 4 ++++ 2 files changed, 23 insertions(+) create mode 100644 CHANGELOG/CHANGELOG-3.8.md create mode 100644 CHANGELOG/README.md diff --git a/CHANGELOG/CHANGELOG-3.8.md b/CHANGELOG/CHANGELOG-3.8.md new file mode 100644 index 000000000..6ebb1de95 --- /dev/null +++ b/CHANGELOG/CHANGELOG-3.8.md @@ -0,0 +1,19 @@ +## [v3.8.3-patch.1](https://github.com/openimsdk/open-im-server/releases/tag/v3.8.3-patch.1) (2025-02-25) + +### New Features +* feat: add backup volume && optimize log print [Created [#3121](https://github.com/openimsdk/open-im-server/pull/3121) + +### Bug Fixes +* fix: seq conversion failed without exiting [Created [#3120](https://github.com/openimsdk/open-im-server/pull/3120) +* fix: check error in BatchSetTokenMapByUidPid [Created [#3123](https://github.com/openimsdk/open-im-server/pull/3123) +* fix: DeleteDoc crash [Created [#3124](https://github.com/openimsdk/open-im-server/pull/3124) +* fix: the abnormal message has no sending time, causing the SDK to be abnormal [Created [#3126](https://github.com/openimsdk/open-im-server/pull/3126) +* fix: crash caused [#3127](https://github.com/openimsdk/open-im-server/pull/3127) +* fix: the user sets the conversation timer cleanup timestamp unit incorrectly [Created [#3128](https://github.com/openimsdk/open-im-server/pull/3128) +* fix: seq conversion not reading env in docker environment [Created [#3131](https://github.com/openimsdk/open-im-server/pull/3131) + +### Builds +* build: improve workflows contents. [Created [#3125](https://github.com/openimsdk/open-im-server/pull/3125) + +**Full Changelog**: [v3.8.3-e-v1.1.5...v3.8.3-patch.1-e-v1.1.5](https://github.com/openimsdk/open-im-server-enterprise/compare/v3.8.3-e-v1.1.5...v3.8.3-patch.1-e-v1.1.5) + diff --git a/CHANGELOG/README.md b/CHANGELOG/README.md new file mode 100644 index 000000000..204194de1 --- /dev/null +++ b/CHANGELOG/README.md @@ -0,0 +1,4 @@ +# CHANGELOGs + +- [CHANGELOG-3.8.md](./CHANGELOG-3.8.md) + From 097e0333476653801abfb18745577d2f6a0ee534 Mon Sep 17 00:00:00 2001 From: Monet Lee Date: Mon, 28 Jul 2025 10:57:44 +0800 Subject: [PATCH 59/59] fix: fix incorrect kicked logic. (#3480) * fix: fix incorrect ws check logic. * optimize checkSameToken logic. --- internal/msggateway/hub_server.go | 1 + internal/msggateway/ws_server.go | 37 +++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/internal/msggateway/hub_server.go b/internal/msggateway/hub_server.go index 3d0d1e3f9..8374af06d 100644 --- a/internal/msggateway/hub_server.go +++ b/internal/msggateway/hub_server.go @@ -249,6 +249,7 @@ func (s *Server) MultiTerminalLoginCheck(ctx context.Context, req *msggateway.Mu tempUserCtx.SetOperationID(mcontext.GetOperationID(ctx)) client := &Client{} client.ctx = tempUserCtx + client.token = req.Token client.UserID = req.UserID client.PlatformID = int(req.PlatformID) i := &kickHandler{ diff --git a/internal/msggateway/ws_server.go b/internal/msggateway/ws_server.go index 9b7343938..bc7a2fa5f 100644 --- a/internal/msggateway/ws_server.go +++ b/internal/msggateway/ws_server.go @@ -334,6 +334,27 @@ func (ws *WsServer) multiTerminalLoginChecker(clientOK bool, oldClients []*Clien } } + // If reconnect: When multiple msgGateway instances are deployed, a client may disconnect from instance A and reconnect to instance B. + // During this process, instance A might still be executing, resulting in two clients with the same token existing simultaneously. + // This situation needs to be filtered to prevent duplicate clients. + checkSameTokenFunc := func(oldClients []*Client) []*Client { + var clientsNeedToKick []*Client + + for _, c := range oldClients { + if c.token == newClient.token { + log.ZDebug(newClient.ctx, "token is same, not kick", + "userID", newClient.UserID, + "platformID", newClient.PlatformID, + "token", newClient.token) + continue + } + + clientsNeedToKick = append(clientsNeedToKick, c) + } + + return clientsNeedToKick + } + switch ws.msgGatewayConfig.Share.MultiLogin.Policy { case constant.DefalutNotKick: case constant.PCAndOther: @@ -349,11 +370,15 @@ func (ws *WsServer) multiTerminalLoginChecker(clientOK bool, oldClients []*Clien } oldClients = append(oldClients, c) } + fallthrough case constant.AllLoginButSameTermKick: if !clientOK { return } + + oldClients = checkSameTokenFunc(oldClients) + ws.clients.DeleteClients(newClient.UserID, oldClients) for _, c := range oldClients { err := c.KickOnlineMessage() @@ -361,6 +386,7 @@ func (ws *WsServer) multiTerminalLoginChecker(clientOK bool, oldClients []*Clien log.ZWarn(c.ctx, "KickOnlineMessage", err) } } + ctx := mcontext.WithMustInfoCtx( []string{newClient.ctx.GetOperationID(), newClient.ctx.GetUserID(), constant.PlatformIDToName(newClient.PlatformID), newClient.ctx.GetConnID()}, @@ -379,14 +405,17 @@ func (ws *WsServer) multiTerminalLoginChecker(clientOK bool, oldClients []*Clien if !ok { return } - var ( - kickClients []*Client - ) + + var kickClients []*Client for _, client := range clients { if constant.PlatformIDToClass(client.PlatformID) == constant.PlatformIDToClass(newClient.PlatformID) { - kickClients = append(kickClients, client) + { + kickClients = append(kickClients, client) + } } } + kickClients = checkSameTokenFunc(kickClients) + kickTokenFunc(kickClients) } }