diff --git a/internal/api/router.go b/internal/api/router.go index 52190ecce..af9c69aed 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -210,6 +210,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co friendRouterGroup.POST("/get_full_friend_user_ids", f.GetFullFriendUserIDs) friendRouterGroup.POST("/get_self_unhandled_apply_count", f.GetSelfUnhandledApplyCount) friendRouterGroup.POST("/get_pinned_friend_ids", f.GetPinnedFriendIDs) + friendRouterGroup.POST("/add_oneway_friend", f.AddOnewayFriend) } g := NewGroupApi(group.NewGroupClient(groupConn)) diff --git a/internal/rpc/relation/friend.go b/internal/rpc/relation/friend.go index d39bb2c4e..28417e61c 100644 --- a/internal/rpc/relation/friend.go +++ b/internal/rpc/relation/friend.go @@ -700,6 +700,12 @@ func (s *friendServer) AddOnewayFriend(ctx context.Context, req *relation.ApplyT if err := s.db.BecomeOnewayFriend(ctx, req.FromUserID, req.ToUserID, becomeFriendByOneway); err != nil { return nil, err } + + // Silently notify only A (FromUserID) to trigger an incremental friend-list sync + // so the remark is reflected in the conversation list. + // B (ToUserID) receives no notification of any kind. + s.notificationSender.FriendAddedOnewayNotification(ctx, req.FromUserID, req.ToUserID) + // Notify only A (FromUserID) so incremental friend sync is triggered // without notifying B (ToUserID). //tips := sdkws.FriendApplicationApprovedTips{ diff --git a/internal/rpc/relation/notification.go b/internal/rpc/relation/notification.go index 4ee45e197..d9d773c76 100644 --- a/internal/rpc/relation/notification.go +++ b/internal/rpc/relation/notification.go @@ -282,6 +282,19 @@ func (f *FriendNotificationSender) FriendsInfoUpdateNotification(ctx context.Con f.Notification(ctx, toUserID, toUserID, constant.FriendsInfoUpdateNotification, &tips) } +// FriendAddedOnewayNotification silently notifies ownerUserID that friendUserID has been added +// to their friend list (one-way, no consent from friendUserID required). +// isSendMsg=false ensures no visible message appears in either user's conversation list. +func (f *FriendNotificationSender) FriendAddedOnewayNotification(ctx context.Context, ownerUserID, friendUserID string) { + tips := sdkws.FriendsInfoUpdateTips{ + FromToUserID: &sdkws.FromToUserID{ToUserID: ownerUserID}, + FriendIDs: []string{friendUserID}, + } + f.setSortVersion(ctx, &tips.FriendVersion, &tips.FriendVersionID, + database.FriendVersionName, ownerUserID, &tips.FriendSortVersion) + f.Notification(ctx, ownerUserID, ownerUserID, constant.FriendsInfoUpdateNotification, &tips) +} + func (f *FriendNotificationSender) BlackAddedNotification(ctx context.Context, req *relation.AddBlackReq) { tips := sdkws.BlackAddedTips{FromToUserID: &sdkws.FromToUserID{}} tips.FromToUserID.FromUserID = req.OwnerUserID diff --git a/internal/rpc/rtc/signal.go b/internal/rpc/rtc/signal.go index b20e02d36..393f1f2bc 100644 --- a/internal/rpc/rtc/signal.go +++ b/internal/rpc/rtc/signal.go @@ -108,6 +108,19 @@ func (s *rtcServer) handleInvite(ctx context.Context, req *rtc.SignalInviteReq, } } + // 从主叫用户资料获取铃声 URL,注入到邀请信息中,被叫方收到后播放主叫方铃声 + if inviterInfo, err := s.userClient.GetUserInfo(ctx, req.UserID); err == nil && inviterInfo.CallRingtoneURL != "" { + inv.CallerRingtoneURL = inviterInfo.CallRingtoneURL + } + + // 查询被叫方铃声 URL,供主叫方在等待时播放 + var calleeRingtoneURL string + if len(inv.InviteeUserIDList) > 0 { + if inviteeInfo, err := s.userClient.GetUserInfo(ctx, inv.InviteeUserIDList[0]); err == nil { + calleeRingtoneURL = inviteeInfo.CallRingtoneURL + } + } + if _, err := s.roomClient.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: inv.RoomID}); err != nil { log.ZError(ctx, "handleInvite", err, "r", err.Error()) return nil, errs.WrapMsg(err, "LiveKit CreateRoom failed", "roomID", inv.RoomID) @@ -147,9 +160,10 @@ func (s *rtcServer) handleInvite(ctx context.Context, req *rtc.SignalInviteReq, log.ZDebug(ctx, "handleInvite", "token", token, "roomID", inv.RoomID, "liveURL", s.config.RpcConfig.LiveKit.ExternalAddress) return &rtc.SignalInviteResp{ - Token: token, - RoomID: inv.RoomID, - LiveURL: s.config.RpcConfig.LiveKit.ExternalAddress, + Token: token, + RoomID: inv.RoomID, + LiveURL: s.config.RpcConfig.LiveKit.ExternalAddress, + CalleeRingtoneURL: calleeRingtoneURL, }, nil } @@ -164,6 +178,19 @@ func (s *rtcServer) handleInviteInGroup(ctx context.Context, req *rtc.SignalInvi inv.InviterUserID = req.UserID inv.InitiateTime = time.Now().UnixMilli() + // 从主叫用户资料获取铃声 URL,注入到邀请信息中,被叫方收到后播放主叫方铃声 + if inviterInfo, err := s.userClient.GetUserInfo(ctx, req.UserID); err == nil && inviterInfo.CallRingtoneURL != "" { + inv.CallerRingtoneURL = inviterInfo.CallRingtoneURL + } + + // 查询第一位被叫的铃声 URL,供主叫方在等待时播放 + var calleeRingtoneURL string + if len(inv.InviteeUserIDList) > 0 { + if inviteeInfo, err := s.userClient.GetUserInfo(ctx, inv.InviteeUserIDList[0]); err == nil { + calleeRingtoneURL = inviteeInfo.CallRingtoneURL + } + } + if _, err := s.roomClient.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: inv.RoomID}); err != nil { return nil, errs.WrapMsg(err, "LiveKit CreateRoom failed", "roomID", inv.RoomID) } @@ -206,9 +233,10 @@ func (s *rtcServer) handleInviteInGroup(ctx context.Context, req *rtc.SignalInvi } return &rtc.SignalInviteInGroupResp{ - Token: token, - RoomID: inv.RoomID, - LiveURL: s.config.RpcConfig.LiveKit.ExternalAddress, + Token: token, + RoomID: inv.RoomID, + LiveURL: s.config.RpcConfig.LiveKit.ExternalAddress, + CalleeRingtoneURL: calleeRingtoneURL, }, nil } diff --git a/pkg/common/convert/user.go b/pkg/common/convert/user.go index 4cf094c24..2c4063cbf 100644 --- a/pkg/common/convert/user.go +++ b/pkg/common/convert/user.go @@ -46,9 +46,11 @@ func UserDB2Pb(user *relationtb.User) *sdkws.UserInfo { FirstName: user.FirstName, LastName: user.LastName, Phone: user.Phone, + AreaCode: user.AreaCode, PhoneVisibility: user.PhoneVisibility, CallAcceptSetting: user.CallAcceptSetting, MsgReceiveSetting: user.MsgReceiveSetting, + CallRingtoneURL: user.CallRingtoneURL, } } @@ -59,16 +61,18 @@ func UsersDB2Pb(users []*relationtb.User) []*sdkws.UserInfo { func UserPb2DB(user *sdkws.UserInfo) *relationtb.User { fullName := BuildFullName(user.FirstName, user.LastName) return &relationtb.User{ - UserID: user.UserID, - Nickname: user.Nickname, - FaceURL: user.FaceURL, - Ex: user.Ex, - CreateTime: time.UnixMilli(user.CreateTime), - AppMangerLevel: user.AppMangerLevel, + UserID: user.UserID, + Nickname: user.Nickname, + FaceURL: user.FaceURL, + Ex: user.Ex, + CreateTime: time.UnixMilli(user.CreateTime), + AppMangerLevel: user.AppMangerLevel, GlobalRecvMsgOpt: user.GlobalRecvMsgOpt, - FirstName: user.FirstName, - LastName: user.LastName, - FullName: fullName, + FirstName: user.FirstName, + LastName: user.LastName, + FullName: fullName, + AreaCode: user.AreaCode, + CallRingtoneURL: user.CallRingtoneURL, } } @@ -83,8 +87,10 @@ func UserPb2DBMap(user *sdkws.UserInfo) map[string]any { "ex": user.Ex, "first_name": user.FirstName, "last_name": user.LastName, + "area_code": user.AreaCode, "app_manager_level": user.AppMangerLevel, "global_recv_msg_opt": user.GlobalRecvMsgOpt, + "call_ringtone_url": user.CallRingtoneURL, } for key, value := range fields { if v, ok := value.(string); ok && v != "" { @@ -115,12 +121,32 @@ func UserPb2DBMapEx(user *sdkws.UserInfoWithEx) map[string]any { if user.Ex != nil { val["ex"] = user.Ex.Value } + if user.FirstName != nil { + val["first_name"] = user.FirstName.Value + } + if user.LastName != nil { + val["last_name"] = user.LastName.Value + } + if user.FirstName != nil || user.LastName != nil { + firstName := "" + lastName := "" + if user.FirstName != nil { + firstName = user.FirstName.Value + } + if user.LastName != nil { + lastName = user.LastName.Value + } + val["full_name"] = BuildFullName(firstName, lastName) + } if user.GlobalRecvMsgOpt != nil { val["global_recv_msg_opt"] = user.GlobalRecvMsgOpt.Value } if user.Phone != nil { val["phone"] = user.Phone.Value } + if user.AreaCode != nil { + val["area_code"] = user.AreaCode.Value + } if user.PhoneVisibility != nil { val["phone_visibility"] = user.PhoneVisibility.Value } @@ -130,7 +156,8 @@ func UserPb2DBMapEx(user *sdkws.UserInfoWithEx) map[string]any { if user.MsgReceiveSetting != nil { val["msg_receive_setting"] = user.MsgReceiveSetting.Value } - // TODO: Add FirstName/LastName support to UserInfoWithEx proto when regenerated - + if user.CallRingtoneURL != nil { + val["call_ringtone_url"] = user.CallRingtoneURL.Value + } return val } diff --git a/pkg/common/storage/database/mgo/user.go b/pkg/common/storage/database/mgo/user.go index 5a2dc7e34..d5f64a5ab 100644 --- a/pkg/common/storage/database/mgo/user.go +++ b/pkg/common/storage/database/mgo/user.go @@ -16,9 +16,10 @@ 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" - "time" "github.com/openimsdk/protocol/user" "github.com/openimsdk/tools/db/mongoutil" @@ -63,7 +64,38 @@ func (u *UserMgo) UpdateByMap(ctx context.Context, userID string, args map[strin if len(args) == 0 { return nil } - return mongoutil.UpdateOne(ctx, u.coll, bson.M{"user_id": userID}, bson.M{"$set": args}, true) + filter := bson.M{"user_id": userID} + update := bson.M{"$set": args} + if err := mongoutil.UpdateOne(ctx, u.coll, filter, update, true); err != nil { + return err + } + // Keep user attributes in sync for consumers that read from the "attribute" collection. + // Only sync the allowed attribute fields. + attributeSet := make(map[string]any) + for _, key := range []string{ + "nickname", + "first_name", + "last_name", + "full_name", + "remark", + "face_url", + "phone_number", + "area_code", + } { + if v, ok := args[key]; ok { + attributeSet[key] = v + } + } + //// user collection uses "phone"; attribute collection uses "phone_number". + if v, ok := args["phone"]; ok { + attributeSet["phone_number"] = v + } + if len(attributeSet) == 0 { + return nil + } + + attributeColl := u.coll.Database().Collection("attribute") + return mongoutil.UpdateOne(ctx, attributeColl, filter, bson.M{"$set": attributeSet}, true) } func (u *UserMgo) Find(ctx context.Context, userIDs []string) (users []*model.User, err error) { diff --git a/pkg/common/storage/model/user.go b/pkg/common/storage/model/user.go index dc02fa5cf..4290f1828 100644 --- a/pkg/common/storage/model/user.go +++ b/pkg/common/storage/model/user.go @@ -62,9 +62,12 @@ type User struct { LastName string `bson:"last_name"` FullName string `bson:"full_name"` Phone string `bson:"phone"` + AreaCode string `bson:"area_code"` PhoneVisibility int32 `bson:"phone_visibility"` CallAcceptSetting int32 `bson:"call_accept_setting"` MsgReceiveSetting int32 `bson:"msg_receive_setting"` + // CallRingtoneURL 用户自定义来电铃声 URL;对方来电时播放此铃声 + CallRingtoneURL string `bson:"call_ringtone_url"` // Status 账号状态:0=正常,1=冻结,2=黑名单 Status int32 `bson:"status"` } diff --git a/protocol b/protocol index 7f613eb71..3b211f91d 160000 --- a/protocol +++ b/protocol @@ -1 +1 @@ -Subproject commit 7f613eb71f23a69730cfb3c3abd1515da0fb17cf +Subproject commit 3b211f91d0e6b98797f91ba34fa64a7b47df5645