复制项目
This commit is contained in:
311
internal/rpc/auth/auth.go
Normal file
311
internal/rpc/auth/auth.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// Copyright © 2023 OpenIM. 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/mcache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/localcache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpccache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
redis2 "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/prommetrics"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
pbauth "git.imall.cloud/openim/protocol/auth"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/msggateway"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/tokenverify"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type authServer struct {
|
||||
pbauth.UnimplementedAuthServer
|
||||
authDatabase controller.AuthDatabase
|
||||
AuthLocalCache *rpccache.AuthLocalCache
|
||||
RegisterCenter discovery.Conn
|
||||
config *Config
|
||||
userClient *rpcli.UserClient
|
||||
adminUserIDs []string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Auth
|
||||
RedisConfig config.Redis
|
||||
MongoConfig config.Mongo
|
||||
Share config.Share
|
||||
LocalCacheConfig config.LocalCache
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
var token cache.TokenModel
|
||||
if rdb == nil {
|
||||
mdb, err := dbb.Mongo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mc, err := mgo.NewCacheMgo(mdb.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token = mcache.NewTokenCacheModel(mc, config.RpcConfig.TokenPolicy.Expire)
|
||||
} else {
|
||||
token = redis2.NewTokenCacheModel(rdb, &config.LocalCacheConfig, config.RpcConfig.TokenPolicy.Expire)
|
||||
}
|
||||
userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authConn, err := client.GetConn(ctx, config.Discovery.RpcService.Auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localcache.InitLocalCache(&config.LocalCacheConfig)
|
||||
|
||||
pbauth.RegisterAuthServer(server, &authServer{
|
||||
RegisterCenter: client,
|
||||
authDatabase: controller.NewAuthDatabase(
|
||||
token,
|
||||
config.Share.Secret,
|
||||
config.RpcConfig.TokenPolicy.Expire,
|
||||
config.Share.MultiLogin,
|
||||
config.Share.IMAdminUser.UserIDs,
|
||||
),
|
||||
AuthLocalCache: rpccache.NewAuthLocalCache(rpcli.NewAuthClient(authConn), &config.LocalCacheConfig, rdb),
|
||||
config: config,
|
||||
userClient: rpcli.NewUserClient(userConn),
|
||||
adminUserIDs: config.Share.IMAdminUser.UserIDs,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authServer) GetAdminToken(ctx context.Context, req *pbauth.GetAdminTokenReq) (*pbauth.GetAdminTokenResp, error) {
|
||||
resp := pbauth.GetAdminTokenResp{}
|
||||
if req.Secret != s.config.Share.Secret {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("secret invalid")
|
||||
}
|
||||
|
||||
if !datautil.Contain(req.UserID, s.adminUserIDs...) {
|
||||
return nil, errs.ErrArgs.WrapMsg("userID is error.", "userID", req.UserID, "adminUserID", s.adminUserIDs)
|
||||
|
||||
}
|
||||
|
||||
if err := s.userClient.CheckUser(ctx, []string{req.UserID}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(constant.AdminPlatformID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prommetrics.UserLoginCounter.Inc()
|
||||
|
||||
resp.Token = token
|
||||
resp.ExpireTimeSeconds = s.config.RpcConfig.TokenPolicy.Expire * 24 * 60 * 60
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenReq) (*pbauth.GetUserTokenResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.PlatformID == constant.AdminPlatformID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("platformID invalid. platformID must not be adminPlatformID")
|
||||
}
|
||||
|
||||
resp := pbauth.GetUserTokenResp{}
|
||||
|
||||
if authverify.CheckUserIsAdmin(ctx, req.UserID) {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("don't get Admin token")
|
||||
}
|
||||
user, err := s.userClient.GetUserInfo(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.AppMangerLevel >= constant.AppNotificationAdmin {
|
||||
return nil, errs.ErrArgs.WrapMsg("app account can`t get token")
|
||||
}
|
||||
token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(req.PlatformID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prommetrics.UserLoginCounter.Inc()
|
||||
|
||||
resp.Token = token
|
||||
resp.ExpireTimeSeconds = s.config.RpcConfig.TokenPolicy.Expire * 24 * 60 * 60
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (s *authServer) GetExistingToken(ctx context.Context, req *pbauth.GetExistingTokenReq) (*pbauth.GetExistingTokenResp, error) {
|
||||
m, err := s.authDatabase.GetTokensWithoutError(ctx, req.UserID, int(req.PlatformID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pbauth.GetExistingTokenResp{
|
||||
TokenStates: convert.TokenMapDB2Pb(m),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *authServer) parseToken(ctx context.Context, tokensString string) (claims *tokenverify.Claims, err error) {
|
||||
claims, err = tokenverify.GetClaimFromToken(tokensString, authverify.Secret(s.config.Share.Secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := s.AuthLocalCache.GetExistingToken(ctx, claims.UserID, claims.PlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(m) == 0 {
|
||||
isAdmin := authverify.CheckUserIsAdmin(ctx, claims.UserID)
|
||||
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 {
|
||||
switch v {
|
||||
case constant.NormalToken:
|
||||
return claims, nil
|
||||
case constant.KickedToken:
|
||||
return nil, servererrs.ErrTokenKicked.Wrap()
|
||||
default:
|
||||
return nil, errs.Wrap(errs.ErrTokenUnknown)
|
||||
}
|
||||
} else {
|
||||
isAdmin := authverify.CheckUserIsAdmin(ctx, claims.UserID)
|
||||
if isAdmin {
|
||||
if err = s.authDatabase.GetTemporaryTokensWithoutError(ctx, claims.UserID, claims.PlatformID, tokensString); err == nil {
|
||||
return claims, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, servererrs.ErrTokenNotExist.Wrap()
|
||||
}
|
||||
|
||||
func (s *authServer) ParseToken(ctx context.Context, req *pbauth.ParseTokenReq) (resp *pbauth.ParseTokenResp, err error) {
|
||||
resp = &pbauth.ParseTokenResp{}
|
||||
claims, err := s.parseToken(ctx, req.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.UserID = claims.UserID
|
||||
resp.PlatformID = int32(claims.PlatformID)
|
||||
resp.ExpireTimeSeconds = claims.ExpiresAt.Unix()
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *authServer) ForceLogout(ctx context.Context, req *pbauth.ForceLogoutReq) (*pbauth.ForceLogoutResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.forceKickOff(ctx, req.UserID, req.PlatformID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbauth.ForceLogoutResp{}, nil
|
||||
}
|
||||
|
||||
func (s *authServer) forceKickOff(ctx context.Context, userID string, platformID int32) error {
|
||||
conns, err := s.RegisterCenter.GetConns(ctx, s.config.Discovery.RpcService.MessageGateway)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range conns {
|
||||
log.ZDebug(ctx, "forceKickOff", "userID", userID, "platformID", platformID)
|
||||
client := msggateway.NewMsgGatewayClient(v)
|
||||
kickReq := &msggateway.KickUserOfflineReq{KickUserIDList: []string{userID}, PlatformID: platformID}
|
||||
_, err := client.KickUserOffline(ctx, kickReq)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "forceKickOff", err, "kickReq", kickReq)
|
||||
}
|
||||
}
|
||||
|
||||
m, err := s.authDatabase.GetTokensWithoutError(ctx, userID, int(platformID))
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return err
|
||||
}
|
||||
for k := range m {
|
||||
m[k] = constant.KickedToken
|
||||
log.ZDebug(ctx, "set token map is ", "token map", m, "userID",
|
||||
userID, "token", k)
|
||||
|
||||
err = s.authDatabase.SetTokenMapByUidPid(ctx, userID, int(platformID), m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authServer) InvalidateToken(ctx context.Context, req *pbauth.InvalidateTokenReq) (*pbauth.InvalidateTokenResp, error) {
|
||||
m, err := s.authDatabase.GetTokensWithoutError(ctx, req.UserID, int(req.PlatformID))
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
if m == nil {
|
||||
return nil, errs.New("token map is empty").Wrap()
|
||||
}
|
||||
log.ZDebug(ctx, "get token from redis", "userID", req.UserID, "platformID",
|
||||
req.PlatformID, "tokenMap", m)
|
||||
|
||||
for k := range m {
|
||||
if k != req.GetPreservedToken() {
|
||||
m[k] = constant.KickedToken
|
||||
}
|
||||
}
|
||||
log.ZDebug(ctx, "set token map is ", "token map", m, "userID",
|
||||
req.UserID, "token", req.GetPreservedToken())
|
||||
err = s.authDatabase.SetTokenMapByUidPid(ctx, req.UserID, int(req.PlatformID), m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbauth.InvalidateTokenResp{}, nil
|
||||
}
|
||||
|
||||
func (s *authServer) KickTokens(ctx context.Context, req *pbauth.KickTokensReq) (*pbauth.KickTokensResp, error) {
|
||||
if err := s.authDatabase.BatchSetTokenMapByUidPid(ctx, req.Tokens); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbauth.KickTokensResp{}, nil
|
||||
}
|
||||
117
internal/rpc/conversation/callback.go
Normal file
117
internal/rpc/conversation/callback.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
dbModel "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/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
|
||||
}
|
||||
865
internal/rpc/conversation/conversation.go
Normal file
865
internal/rpc/conversation/conversation.go
Normal file
@@ -0,0 +1,865 @@
|
||||
// Copyright © 2023 OpenIM. 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 conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
|
||||
dbModel "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/localcache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/msgprocessor"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbconversation "git.imall.cloud/openim/protocol/conversation"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
type conversationServer struct {
|
||||
pbconversation.UnimplementedConversationServer
|
||||
conversationDatabase controller.ConversationDatabase
|
||||
|
||||
conversationNotificationSender *ConversationNotificationSender
|
||||
config *Config
|
||||
|
||||
webhookClient *webhook.Client
|
||||
userClient *rpcli.UserClient
|
||||
msgClient *rpcli.MsgClient
|
||||
groupClient *rpcli.GroupClient
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Conversation
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
NotificationConfig config.Notification
|
||||
Share config.Share
|
||||
WebhooksConfig config.Webhooks
|
||||
LocalCacheConfig config.LocalCache
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
rdb, err := dbb.Redis(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conversationDB, err := mgo.NewConversationMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgClient := rpcli.NewMsgClient(msgConn)
|
||||
|
||||
// 初始化webhook配置管理器(支持从数据库读取配置)
|
||||
var webhookClient *webhook.Client
|
||||
systemConfigDB, err := mgo.NewSystemConfigMongo(mgocli.GetDB())
|
||||
if err == nil {
|
||||
// 如果SystemConfig数据库初始化成功,使用配置管理器
|
||||
webhookConfigManager := webhook.NewConfigManager(systemConfigDB, &config.WebhooksConfig)
|
||||
if err := webhookConfigManager.Start(ctx); err != nil {
|
||||
log.ZWarn(ctx, "failed to start webhook config manager, using default config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
} else {
|
||||
webhookClient = webhook.NewWebhookClientWithManager(webhookConfigManager)
|
||||
}
|
||||
} else {
|
||||
// 如果SystemConfig数据库初始化失败,使用默认配置
|
||||
log.ZWarn(ctx, "failed to init system config db, using default webhook config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
}
|
||||
|
||||
cs := conversationServer{
|
||||
config: config,
|
||||
webhookClient: webhookClient,
|
||||
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, &cs)
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if len(conversations) < 1 {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("conversation not found")
|
||||
}
|
||||
resp := &pbconversation.GetConversationResp{Conversation: &pbconversation.Conversation{}}
|
||||
resp.Conversation = convert.ConversationDB2Pb(conversations[0])
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Deprecated: Use `GetConversations` instead.
|
||||
func (c *conversationServer) GetSortedConversationList(ctx context.Context, req *pbconversation.GetSortedConversationListReq) (resp *pbconversation.GetSortedConversationListResp, err error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
conversationIDs = req.ConversationIDs
|
||||
}
|
||||
|
||||
conversations, err := c.conversationDatabase.FindConversations(ctx, req.UserID, conversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(conversations) == 0 {
|
||||
return nil, errs.ErrRecordNotFound.Wrap()
|
||||
}
|
||||
maxSeqs, err := c.msgClient.GetMaxSeqs(ctx, conversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chatLogs, err := c.msgClient.GetMsgByConversationIDs(ctx, conversationIDs, maxSeqs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conversationMsg, err := c.getConversationInfo(ctx, chatLogs, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasReadSeqs, err := c.msgClient.GetHasReadSeqs(ctx, conversationIDs, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unreadTotal int64
|
||||
conversation_unreadCount := make(map[string]int64)
|
||||
for conversationID, maxSeq := range maxSeqs {
|
||||
unreadCount := maxSeq - hasReadSeqs[conversationID]
|
||||
conversation_unreadCount[conversationID] = unreadCount
|
||||
unreadTotal += unreadCount
|
||||
}
|
||||
|
||||
conversation_isPinTime := make(map[int64]string)
|
||||
conversation_notPinTime := make(map[int64]string)
|
||||
|
||||
for _, v := range conversations {
|
||||
conversationID := v.ConversationID
|
||||
var time int64
|
||||
if _, ok := conversationMsg[conversationID]; ok {
|
||||
time = conversationMsg[conversationID].MsgInfo.LatestMsgRecvTime
|
||||
} else {
|
||||
conversationMsg[conversationID] = &pbconversation.ConversationElem{
|
||||
ConversationID: conversationID,
|
||||
IsPinned: v.IsPinned,
|
||||
MsgInfo: nil,
|
||||
}
|
||||
time = v.CreateTime.UnixMilli()
|
||||
}
|
||||
|
||||
conversationMsg[conversationID].RecvMsgOpt = v.RecvMsgOpt
|
||||
if v.IsPinned {
|
||||
conversationMsg[conversationID].IsPinned = v.IsPinned
|
||||
conversation_isPinTime[time] = conversationID
|
||||
continue
|
||||
}
|
||||
conversation_notPinTime[time] = conversationID
|
||||
}
|
||||
resp = &pbconversation.GetSortedConversationListResp{
|
||||
ConversationTotal: int64(len(chatLogs)),
|
||||
ConversationElems: []*pbconversation.ConversationElem{},
|
||||
UnreadTotal: unreadTotal,
|
||||
}
|
||||
|
||||
c.conversationSort(conversation_isPinTime, resp, conversation_unreadCount, conversationMsg)
|
||||
c.conversationSort(conversation_notPinTime, resp, conversation_unreadCount, conversationMsg)
|
||||
|
||||
resp.ConversationElems = datautil.Paginate(resp.ConversationElems, int(req.Pagination.GetPageNumber()), int(req.Pagination.GetShowNumber()))
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
resp := &pbconversation.GetAllConversationsResp{Conversations: []*pbconversation.Conversation{}}
|
||||
resp.Conversations = convert.ConversationsDB2Pb(conversations)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &pbconversation.GetConversationsResp{
|
||||
Conversations: conversations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) getConversations(ctx context.Context, ownerUserID string, conversationIDs []string) ([]*pbconversation.Conversation, error) {
|
||||
conversations, err := c.conversationDatabase.FindConversations(ctx, ownerUserID, conversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &pbconversation.GetConversationsResp{Conversations: []*pbconversation.Conversation{}}
|
||||
resp.Conversations = convert.ConversationsDB2Pb(conversations)
|
||||
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
|
||||
}
|
||||
err := c.conversationDatabase.SetUserConversations(ctx, req.Conversation.OwnerUserID, []*dbModel.Conversation{&conversation})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.conversationNotificationSender.ConversationChangeNotification(ctx, req.Conversation.OwnerUserID, []string{req.Conversation.ConversationID})
|
||||
resp := &pbconversation.SetConversationResp{}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) SetConversations(ctx context.Context, req *pbconversation.SetConversationsReq) (*pbconversation.SetConversationsResp, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if groupInfo == nil {
|
||||
return nil, servererrs.ErrGroupIDNotFound.WrapMsg(req.Conversation.GroupID)
|
||||
}
|
||||
if groupInfo.Status == constant.GroupStatusDismissed {
|
||||
return nil, servererrs.ErrDismissedAlready.WrapMsg("group dismissed")
|
||||
}
|
||||
}
|
||||
|
||||
conversationMap := make(map[string]*dbModel.Conversation)
|
||||
var needUpdateUsersList []string
|
||||
|
||||
for _, userID := range req.UserIDs {
|
||||
conversationList, err := c.conversationDatabase.FindConversations(ctx, userID, []string{req.Conversation.ConversationID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(conversationList) != 0 {
|
||||
conversationMap[userID] = conversationList[0]
|
||||
} else {
|
||||
needUpdateUsersList = append(needUpdateUsersList, userID)
|
||||
}
|
||||
}
|
||||
|
||||
var conversation dbModel.Conversation
|
||||
conversation.ConversationID = req.Conversation.ConversationID
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for userID := range conversationMap {
|
||||
unequal := UserUpdateCheckMap(ctx, userID, req.Conversation, conversationMap[userID])
|
||||
|
||||
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 _, 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 {
|
||||
transConversation := conversation
|
||||
transConversation.OwnerUserID = ownerUserID
|
||||
transConversation.IsPrivateChat = req.Conversation.IsPrivateChat.Value
|
||||
conversations = append(conversations, &transConversation)
|
||||
}
|
||||
|
||||
if err := c.conversationDatabase.SyncPeerUserPrivateConversationTx(ctx, conversations); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, userID := range req.UserIDs {
|
||||
c.conversationNotificationSender.ConversationSetPrivateNotification(ctx, userID, req.Conversation.UserID,
|
||||
req.Conversation.IsPrivateChat.Value, req.Conversation.ConversationID)
|
||||
}
|
||||
}
|
||||
|
||||
return &pbconversation.SetConversationsResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if len(m) > 0 {
|
||||
if err := c.conversationDatabase.UpdateUserConversations(ctx, req.UserID, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pbconversation.UpdateConversationsByUserResp{}, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
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:
|
||||
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) {
|
||||
var conversation dbModel.Conversation
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err := c.conversationDatabase.CreateGroupChatConversation(ctx, req.GroupID, req.UserIDs, &conversation)
|
||||
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
|
||||
}
|
||||
|
||||
func (c *conversationServer) SetConversationMaxSeq(ctx context.Context, req *pbconversation.SetConversationMaxSeqReq) (*pbconversation.SetConversationMaxSeqResp, error) {
|
||||
if err := c.msgClient.SetUserConversationMaxSeq(ctx, req.ConversationID, req.OwnerUserID, req.MaxSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.conversationDatabase.UpdateUsersConversationField(ctx, req.OwnerUserID, req.ConversationID,
|
||||
map[string]any{"max_seq": req.MaxSeq}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, userID := range req.OwnerUserID {
|
||||
c.conversationNotificationSender.ConversationChangeNotification(ctx, userID, []string{req.ConversationID})
|
||||
}
|
||||
return &pbconversation.SetConversationMaxSeqResp{}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) SetConversationMinSeq(ctx context.Context, req *pbconversation.SetConversationMinSeqReq) (*pbconversation.SetConversationMinSeqResp, error) {
|
||||
if err := c.msgClient.SetUserConversationMin(ctx, req.ConversationID, req.OwnerUserID, req.MinSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.conversationDatabase.UpdateUsersConversationField(ctx, req.OwnerUserID, req.ConversationID,
|
||||
map[string]any{"min_seq": req.MinSeq}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, userID := range req.OwnerUserID {
|
||||
c.conversationNotificationSender.ConversationChangeNotification(ctx, userID, []string{req.ConversationID})
|
||||
}
|
||||
return &pbconversation.SetConversationMinSeqResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &pbconversation.GetConversationIDsResp{ConversationIDs: conversationIDs}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &pbconversation.GetUserConversationIDsHashResp{Hash: hash}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &pbconversation.GetConversationsByConversationIDResp{Conversations: convert.ConversationsDB2Pb(conversations)}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) GetConversationOfflinePushUserIDs(ctx context.Context, req *pbconversation.GetConversationOfflinePushUserIDsReq) (*pbconversation.GetConversationOfflinePushUserIDsResp, error) {
|
||||
if req.ConversationID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("conversationID is empty")
|
||||
}
|
||||
if len(req.UserIDs) == 0 {
|
||||
return &pbconversation.GetConversationOfflinePushUserIDsResp{}, nil
|
||||
}
|
||||
userIDs, err := c.conversationDatabase.GetConversationNotReceiveMessageUserIDs(ctx, req.ConversationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(userIDs) == 0 {
|
||||
return &pbconversation.GetConversationOfflinePushUserIDsResp{UserIDs: req.UserIDs}, nil
|
||||
}
|
||||
userIDSet := make(map[string]struct{})
|
||||
for _, userID := range req.UserIDs {
|
||||
userIDSet[userID] = struct{}{}
|
||||
}
|
||||
for _, userID := range userIDs {
|
||||
delete(userIDSet, userID)
|
||||
}
|
||||
return &pbconversation.GetConversationOfflinePushUserIDsResp{UserIDs: datautil.Keys(userIDSet)}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) conversationSort(conversations map[int64]string, resp *pbconversation.GetSortedConversationListResp, conversation_unreadCount map[string]int64, conversationMsg map[string]*pbconversation.ConversationElem) {
|
||||
keys := []int64{}
|
||||
for key := range conversations {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
return keys[i] > keys[j]
|
||||
})
|
||||
index := 0
|
||||
|
||||
cons := make([]*pbconversation.ConversationElem, len(conversations))
|
||||
for _, v := range keys {
|
||||
conversationID := conversations[v]
|
||||
conversationElem := conversationMsg[conversationID]
|
||||
conversationElem.UnreadCount = conversation_unreadCount[conversationID]
|
||||
cons[index] = conversationElem
|
||||
index++
|
||||
}
|
||||
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) {
|
||||
var (
|
||||
sendIDs []string
|
||||
groupIDs []string
|
||||
sendMap = make(map[string]*sdkws.UserInfo)
|
||||
groupMap = make(map[string]*sdkws.GroupInfo)
|
||||
conversationMsg = make(map[string]*pbconversation.ConversationElem)
|
||||
)
|
||||
for _, chatLog := range chatLogs {
|
||||
switch chatLog.SessionType {
|
||||
case constant.SingleChatType:
|
||||
if chatLog.SendID == userID {
|
||||
sendIDs = append(sendIDs, chatLog.RecvID)
|
||||
}
|
||||
sendIDs = append(sendIDs, chatLog.SendID)
|
||||
case constant.WriteGroupChatType, constant.ReadGroupChatType:
|
||||
groupIDs = append(groupIDs, chatLog.GroupID)
|
||||
sendIDs = append(sendIDs, chatLog.SendID)
|
||||
}
|
||||
}
|
||||
if len(sendIDs) != 0 {
|
||||
sendInfos, err := c.userClient.GetUsersInfo(ctx, sendIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sendInfo := range sendInfos {
|
||||
sendMap[sendInfo.UserID] = sendInfo
|
||||
}
|
||||
}
|
||||
if len(groupIDs) != 0 {
|
||||
groupInfos, err := c.groupClient.GetGroupsInfo(ctx, groupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, groupInfo := range groupInfos {
|
||||
groupMap[groupInfo.GroupID] = groupInfo
|
||||
}
|
||||
}
|
||||
for conversationID, chatLog := range chatLogs {
|
||||
pbchatLog := &pbconversation.ConversationElem{}
|
||||
msgInfo := &pbconversation.MsgInfo{}
|
||||
if err := datautil.CopyStructFields(msgInfo, chatLog); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch chatLog.SessionType {
|
||||
case constant.SingleChatType:
|
||||
if chatLog.SendID == userID {
|
||||
if recv, ok := sendMap[chatLog.RecvID]; ok {
|
||||
msgInfo.FaceURL = recv.FaceURL
|
||||
msgInfo.SenderName = recv.Nickname
|
||||
}
|
||||
break
|
||||
}
|
||||
if send, ok := sendMap[chatLog.SendID]; ok {
|
||||
msgInfo.FaceURL = send.FaceURL
|
||||
msgInfo.SenderName = send.Nickname
|
||||
}
|
||||
case constant.WriteGroupChatType, constant.ReadGroupChatType:
|
||||
msgInfo.GroupID = chatLog.GroupID
|
||||
if group, ok := groupMap[chatLog.GroupID]; ok {
|
||||
msgInfo.GroupName = group.GroupName
|
||||
msgInfo.GroupFaceURL = group.FaceURL
|
||||
msgInfo.GroupMemberCount = group.MemberCount
|
||||
msgInfo.GroupType = group.GroupType
|
||||
}
|
||||
if send, ok := sendMap[chatLog.SendID]; ok {
|
||||
msgInfo.SenderName = send.Nickname
|
||||
}
|
||||
}
|
||||
pbchatLog.ConversationID = conversationID
|
||||
msgInfo.LatestMsgRecvTime = chatLog.SendTime
|
||||
pbchatLog.MsgInfo = msgInfo
|
||||
conversationMsg[conversationID] = pbchatLog
|
||||
}
|
||||
return conversationMsg, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) GetConversationNotReceiveMessageUserIDs(ctx context.Context, req *pbconversation.GetConversationNotReceiveMessageUserIDsReq) (*pbconversation.GetConversationNotReceiveMessageUserIDsResp, error) {
|
||||
userIDs, err := c.conversationDatabase.GetConversationNotReceiveMessageUserIDs(ctx, req.ConversationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbconversation.GetConversationNotReceiveMessageUserIDsResp{UserIDs: userIDs}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if req.AttachedInfo != nil {
|
||||
m["attached_info"] = req.AttachedInfo.Value
|
||||
}
|
||||
if req.Ex != nil {
|
||||
m["ex"] = req.Ex.Value
|
||||
}
|
||||
if req.IsPinned != nil {
|
||||
m["is_pinned"] = req.IsPinned.Value
|
||||
}
|
||||
if req.GroupAtType != nil {
|
||||
m["group_at_type"] = req.GroupAtType.Value
|
||||
}
|
||||
if req.MsgDestructTime != nil {
|
||||
m["msg_destruct_time"] = req.MsgDestructTime.Value
|
||||
}
|
||||
if req.IsMsgDestruct != nil {
|
||||
m["is_msg_destruct"] = req.IsMsgDestruct.Value
|
||||
}
|
||||
if req.BurnDuration != nil {
|
||||
m["burn_duration"] = req.BurnDuration.Value
|
||||
}
|
||||
if req.IsPrivateChat != nil {
|
||||
m["is_private_chat"] = req.IsPrivateChat.Value
|
||||
}
|
||||
if req.MinSeq != nil {
|
||||
m["min_seq"] = req.MinSeq.Value
|
||||
}
|
||||
if req.MaxSeq != nil {
|
||||
m["max_seq"] = req.MaxSeq.Value
|
||||
}
|
||||
if req.LatestMsgDestructTime != nil {
|
||||
m["latest_msg_destruct_time"] = time.UnixMilli(req.LatestMsgDestructTime.Value)
|
||||
}
|
||||
if len(m) > 0 {
|
||||
if err := c.conversationDatabase.UpdateUsersConversationField(ctx, req.UserIDs, req.ConversationID, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pbconversation.UpdateConversationResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &pbconversation.GetOwnerConversationResp{
|
||||
Total: total,
|
||||
Conversations: convert.ConversationsDB2Pb(conversations),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) GetConversationsNeedClearMsg(ctx context.Context, _ *pbconversation.GetConversationsNeedClearMsgReq) (*pbconversation.GetConversationsNeedClearMsgResp, error) {
|
||||
num, err := c.conversationDatabase.GetAllConversationIDsNumber(ctx)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "GetAllConversationIDsNumber failed", err)
|
||||
return nil, err
|
||||
}
|
||||
const batchNum = 100
|
||||
|
||||
if num == 0 {
|
||||
return nil, errs.New("Need Destruct Msg is nil").Wrap()
|
||||
}
|
||||
|
||||
maxPage := (num + batchNum - 1) / batchNum
|
||||
|
||||
temp := make([]*dbModel.Conversation, 0, maxPage*batchNum)
|
||||
|
||||
for pageNumber := 0; pageNumber < int(maxPage); pageNumber++ {
|
||||
pagination := &sdkws.RequestPagination{
|
||||
PageNumber: int32(pageNumber),
|
||||
ShowNumber: batchNum,
|
||||
}
|
||||
|
||||
conversationIDs, err := c.conversationDatabase.PageConversationIDs(ctx, pagination)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "PageConversationIDs failed", err, "pageNumber", pageNumber)
|
||||
continue
|
||||
}
|
||||
|
||||
// log.ZDebug(ctx, "PageConversationIDs success", "pageNumber", pageNumber, "conversationIDsNum", len(conversationIDs), "conversationIDs", conversationIDs)
|
||||
if len(conversationIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
conversations, err := c.conversationDatabase.GetConversationsByConversationID(ctx, conversationIDs)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "GetConversationsByConversationID failed", err, "conversationIDs", conversationIDs)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, conversation := range conversations {
|
||||
if conversation.IsMsgDestruct && conversation.MsgDestructTime != 0 && ((time.Now().UnixMilli() > (conversation.MsgDestructTime + conversation.LatestMsgDestructTime.UnixMilli() + 8*60*60)) || // 8*60*60 is UTC+8
|
||||
conversation.LatestMsgDestructTime.IsZero()) {
|
||||
temp = append(temp, conversation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &pbconversation.GetConversationsNeedClearMsgResp{Conversations: convert.ConversationsDB2Pb(temp)}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &pbconversation.GetNotNotifyConversationIDsResp{ConversationIDs: conversationIDs}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &pbconversation.GetPinnedConversationIDsResp{ConversationIDs: conversationIDs}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) DeleteConversations(ctx context.Context, req *pbconversation.DeleteConversationsReq) (*pbconversation.DeleteConversationsResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.conversationDatabase.DeleteUsersConversations(ctx, req.OwnerUserID, req.ConversationIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbconversation.DeleteConversationsResp{}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) ClearUserConversationMsg(ctx context.Context, req *pbconversation.ClearUserConversationMsgReq) (*pbconversation.ClearUserConversationMsgResp, error) {
|
||||
conversations, err := c.conversationDatabase.FindRandConversation(ctx, req.Timestamp, int(req.Limit))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
latestMsgDestructTime := time.UnixMilli(req.Timestamp)
|
||||
for i, conversation := range conversations {
|
||||
if conversation.IsMsgDestruct == false || conversation.MsgDestructTime == 0 {
|
||||
continue
|
||||
}
|
||||
seq, err := c.msgClient.GetLastMessageSeqByTime(ctx, conversation.ConversationID, req.Timestamp-(conversation.MsgDestructTime*1000))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seq <= 0 {
|
||||
log.ZDebug(ctx, "ClearUserConversationMsg GetLastMessageSeqByTime seq <= 0", "index", i, "conversationID", conversation.ConversationID, "ownerUserID", conversation.OwnerUserID, "msgDestructTime", conversation.MsgDestructTime, "seq", seq)
|
||||
if err := c.setConversationMinSeqAndLatestMsgDestructTime(ctx, conversation.ConversationID, conversation.OwnerUserID, -1, latestMsgDestructTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
seq++
|
||||
if err := c.setConversationMinSeqAndLatestMsgDestructTime(ctx, conversation.ConversationID, conversation.OwnerUserID, seq, latestMsgDestructTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.ZDebug(ctx, "ClearUserConversationMsg set min seq", "index", i, "conversationID", conversation.ConversationID, "ownerUserID", conversation.OwnerUserID, "seq", seq, "msgDestructTime", conversation.MsgDestructTime)
|
||||
}
|
||||
return &pbconversation.ClearUserConversationMsgResp{Count: int32(len(conversations))}, nil
|
||||
}
|
||||
|
||||
func (c *conversationServer) setConversationMinSeqAndLatestMsgDestructTime(ctx context.Context, conversationID string, ownerUserID string, minSeq int64, latestMsgDestructTime time.Time) error {
|
||||
update := map[string]any{
|
||||
"latest_msg_destruct_time": latestMsgDestructTime,
|
||||
}
|
||||
if minSeq >= 0 {
|
||||
if err := c.msgClient.SetUserConversationMin(ctx, conversationID, []string{ownerUserID}, minSeq); err != nil {
|
||||
return err
|
||||
}
|
||||
update["min_seq"] = minSeq
|
||||
}
|
||||
|
||||
if err := c.conversationDatabase.UpdateUsersConversationField(ctx, []string{ownerUserID}, conversationID, update); err != nil {
|
||||
return err
|
||||
}
|
||||
c.conversationNotificationSender.ConversationChangeNotification(ctx, ownerUserID, []string{conversationID})
|
||||
return nil
|
||||
}
|
||||
85
internal/rpc/conversation/db_map.go
Normal file
85
internal/rpc/conversation/db_map.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
dbModel "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/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
|
||||
}
|
||||
75
internal/rpc/conversation/notification.go
Normal file
75
internal/rpc/conversation/notification.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright © 2023 OpenIM. 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 conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
)
|
||||
|
||||
type ConversationNotificationSender struct {
|
||||
*notification.NotificationSender
|
||||
}
|
||||
|
||||
func NewConversationNotificationSender(conf *config.Notification, msgClient *rpcli.MsgClient) *ConversationNotificationSender {
|
||||
return &ConversationNotificationSender{notification.NewNotificationSender(conf, notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {
|
||||
return msgClient.SendMsg(ctx, req)
|
||||
}))}
|
||||
}
|
||||
|
||||
// SetPrivate invote.
|
||||
func (c *ConversationNotificationSender) ConversationSetPrivateNotification(ctx context.Context, sendID, recvID string,
|
||||
isPrivateChat bool, conversationID string,
|
||||
) {
|
||||
tips := &sdkws.ConversationSetPrivateTips{
|
||||
RecvID: recvID,
|
||||
SendID: sendID,
|
||||
IsPrivate: isPrivateChat,
|
||||
ConversationID: conversationID,
|
||||
}
|
||||
|
||||
c.Notification(ctx, sendID, recvID, constant.ConversationPrivateChatNotification, tips)
|
||||
}
|
||||
|
||||
func (c *ConversationNotificationSender) ConversationChangeNotification(ctx context.Context, userID string, conversationIDs []string) {
|
||||
tips := &sdkws.ConversationUpdateTips{
|
||||
UserID: userID,
|
||||
ConversationIDList: conversationIDs,
|
||||
}
|
||||
|
||||
c.Notification(ctx, userID, userID, constant.ConversationChangeNotification, tips)
|
||||
}
|
||||
|
||||
func (c *ConversationNotificationSender) ConversationUnreadChangeNotification(
|
||||
ctx context.Context,
|
||||
userID, conversationID string,
|
||||
unreadCountTime, hasReadSeq int64,
|
||||
) {
|
||||
tips := &sdkws.ConversationHasReadTips{
|
||||
UserID: userID,
|
||||
ConversationID: conversationID,
|
||||
HasReadSeq: hasReadSeq,
|
||||
UnreadCountTime: unreadCountTime,
|
||||
}
|
||||
|
||||
c.Notification(ctx, userID, userID, constant.ConversationUnreadNotification, tips)
|
||||
}
|
||||
63
internal/rpc/conversation/sync.go
Normal file
63
internal/rpc/conversation/sync.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package conversation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/internal/rpc/incrversion"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/util/hashutil"
|
||||
"git.imall.cloud/openim/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
|
||||
}
|
||||
conversationIDs, err := c.conversationDatabase.GetConversationIDs(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idHash := hashutil.IdHash(conversationIDs)
|
||||
if req.IdHash == idHash {
|
||||
conversationIDs = nil
|
||||
}
|
||||
return &conversation.GetFullOwnerConversationIDsResp{
|
||||
Version: idHash,
|
||||
VersionID: vl.ID.Hex(),
|
||||
Equal: req.IdHash == idHash,
|
||||
ConversationIDs: conversationIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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,
|
||||
VersionID: req.VersionID,
|
||||
VersionNumber: req.Version,
|
||||
Version: c.conversationDatabase.FindConversationUserVersion,
|
||||
CacheMaxVersion: c.conversationDatabase.FindMaxConversationUserVersionCache,
|
||||
Find: func(ctx context.Context, conversationIDs []string) ([]*conversation.Conversation, error) {
|
||||
return c.getConversations(ctx, req.UserID, conversationIDs)
|
||||
},
|
||||
Resp: func(version *model.VersionLog, delIDs []string, insertList, updateList []*conversation.Conversation, full bool) *conversation.GetIncrementalConversationResp {
|
||||
return &conversation.GetIncrementalConversationResp{
|
||||
VersionID: version.ID.Hex(),
|
||||
Version: uint64(version.Version),
|
||||
Full: full,
|
||||
Delete: delIDs,
|
||||
Insert: insertList,
|
||||
Update: updateList,
|
||||
}
|
||||
},
|
||||
}
|
||||
return opt.Build()
|
||||
}
|
||||
46
internal/rpc/group/cache.go
Normal file
46
internal/rpc/group/cache.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright © 2023 OpenIM. 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 group
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
pbgroup "git.imall.cloud/openim/protocol/group"
|
||||
)
|
||||
|
||||
// GetGroupInfoCache get group info from cache.
|
||||
func (g *groupServer) GetGroupInfoCache(ctx context.Context, req *pbgroup.GetGroupInfoCacheReq) (*pbgroup.GetGroupInfoCacheResp, error) {
|
||||
group, err := g.db.TakeGroup(ctx, req.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbgroup.GetGroupInfoCacheResp{
|
||||
GroupInfo: convert.Db2PbGroupInfo(group, "", 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *groupServer) GetGroupMemberCache(ctx context.Context, req *pbgroup.GetGroupMemberCacheReq) (*pbgroup.GetGroupMemberCacheResp, error) {
|
||||
if err := g.checkAdminOrInGroup(ctx, req.GroupID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members, err := g.db.TakeGroupMember(ctx, req.GroupID, req.GroupMemberID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbgroup.GetGroupMemberCacheResp{
|
||||
Member: convert.Db2PbGroupMember(members),
|
||||
}, nil
|
||||
}
|
||||
476
internal/rpc/group/callback.go
Normal file
476
internal/rpc/group/callback.go
Normal file
@@ -0,0 +1,476 @@
|
||||
// Copyright © 2023 OpenIM. 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 group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/group"
|
||||
"git.imall.cloud/openim/protocol/wrapperspb"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
// CallbackBeforeCreateGroup callback before create group.
|
||||
func (g *groupServer) webhookBeforeCreateGroup(ctx context.Context, before *config.BeforeConfig, req *group.CreateGroupReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &callbackstruct.CallbackBeforeCreateGroupReq{
|
||||
CallbackCommand: callbackstruct.CallbackBeforeCreateGroupCommand,
|
||||
OperationID: mcontext.GetOperationID(ctx),
|
||||
GroupInfo: req.GroupInfo,
|
||||
}
|
||||
cbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{
|
||||
UserID: req.OwnerUserID,
|
||||
RoleLevel: constant.GroupOwner,
|
||||
})
|
||||
for _, userID := range req.AdminUserIDs {
|
||||
cbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{
|
||||
UserID: userID,
|
||||
RoleLevel: constant.GroupAdmin,
|
||||
})
|
||||
}
|
||||
for _, userID := range req.MemberUserIDs {
|
||||
cbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{
|
||||
UserID: userID,
|
||||
RoleLevel: constant.GroupOrdinaryUsers,
|
||||
})
|
||||
}
|
||||
resp := &callbackstruct.CallbackBeforeCreateGroupResp{}
|
||||
|
||||
if err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datautil.NotNilReplace(&req.GroupInfo.GroupID, resp.GroupID)
|
||||
datautil.NotNilReplace(&req.GroupInfo.GroupName, resp.GroupName)
|
||||
datautil.NotNilReplace(&req.GroupInfo.Notification, resp.Notification)
|
||||
datautil.NotNilReplace(&req.GroupInfo.Introduction, resp.Introduction)
|
||||
datautil.NotNilReplace(&req.GroupInfo.FaceURL, resp.FaceURL)
|
||||
datautil.NotNilReplace(&req.GroupInfo.OwnerUserID, resp.OwnerUserID)
|
||||
datautil.NotNilReplace(&req.GroupInfo.Ex, resp.Ex)
|
||||
datautil.NotNilReplace(&req.GroupInfo.Status, resp.Status)
|
||||
datautil.NotNilReplace(&req.GroupInfo.CreatorUserID, resp.CreatorUserID)
|
||||
datautil.NotNilReplace(&req.GroupInfo.GroupType, resp.GroupType)
|
||||
datautil.NotNilReplace(&req.GroupInfo.NeedVerification, resp.NeedVerification)
|
||||
datautil.NotNilReplace(&req.GroupInfo.LookMemberInfo, resp.LookMemberInfo)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterCreateGroup(ctx context.Context, after *config.AfterConfig, req *group.CreateGroupReq) {
|
||||
cbReq := &callbackstruct.CallbackAfterCreateGroupReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterCreateGroupCommand,
|
||||
GroupInfo: req.GroupInfo,
|
||||
}
|
||||
cbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{
|
||||
UserID: req.OwnerUserID,
|
||||
RoleLevel: constant.GroupOwner,
|
||||
})
|
||||
for _, userID := range req.AdminUserIDs {
|
||||
cbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{
|
||||
UserID: userID,
|
||||
RoleLevel: constant.GroupAdmin,
|
||||
})
|
||||
}
|
||||
for _, userID := range req.MemberUserIDs {
|
||||
cbReq.InitMemberList = append(cbReq.InitMemberList, &apistruct.GroupAddMemberInfo{
|
||||
UserID: userID,
|
||||
RoleLevel: constant.GroupOrdinaryUsers,
|
||||
})
|
||||
}
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterCreateGroupResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookBeforeMembersJoinGroup(ctx context.Context, before *config.BeforeConfig, groupMembers []*model.GroupMember, groupID string, groupEx string) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
groupMembersMap := datautil.SliceToMap(groupMembers, func(e *model.GroupMember) string {
|
||||
return e.UserID
|
||||
})
|
||||
var groupMembersCallback []*callbackstruct.CallbackGroupMember
|
||||
|
||||
for _, member := range groupMembers {
|
||||
groupMembersCallback = append(groupMembersCallback, &callbackstruct.CallbackGroupMember{
|
||||
UserID: member.UserID,
|
||||
Ex: member.Ex,
|
||||
})
|
||||
}
|
||||
|
||||
cbReq := &callbackstruct.CallbackBeforeMembersJoinGroupReq{
|
||||
CallbackCommand: callbackstruct.CallbackBeforeMembersJoinGroupCommand,
|
||||
GroupID: groupID,
|
||||
MembersList: groupMembersCallback,
|
||||
GroupEx: groupEx,
|
||||
}
|
||||
resp := &callbackstruct.CallbackBeforeMembersJoinGroupResp{}
|
||||
|
||||
if err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录webhook响应,用于排查自动禁言问题
|
||||
if len(resp.MemberCallbackList) > 0 {
|
||||
log.ZInfo(ctx, "webhookBeforeMembersJoinGroup: webhook response received",
|
||||
"groupID", groupID,
|
||||
"memberCallbackListCount", len(resp.MemberCallbackList),
|
||||
"memberCallbackList", resp.MemberCallbackList)
|
||||
}
|
||||
|
||||
for _, memberCallbackResp := range resp.MemberCallbackList {
|
||||
if _, ok := groupMembersMap[(*memberCallbackResp.UserID)]; ok {
|
||||
if memberCallbackResp.MuteEndTime != nil {
|
||||
muteEndTimeTimestamp := *memberCallbackResp.MuteEndTime
|
||||
now := time.Now()
|
||||
nowUnixMilli := now.UnixMilli()
|
||||
|
||||
// 检查时间戳是否合理(防止时间戳单位错误导致自动禁言)
|
||||
// 如果时间戳小于1000000000000(2001-09-09),可能是秒级时间戳,需要转换为毫秒
|
||||
// 如果时间戳大于当前时间+10年,可能是时间戳单位错误
|
||||
var muteEndTime time.Time
|
||||
if muteEndTimeTimestamp < 1000000000000 {
|
||||
// 可能是秒级时间戳,转换为毫秒
|
||||
log.ZWarn(ctx, "webhookBeforeMembersJoinGroup: MuteEndTime appears to be in seconds, converting to milliseconds", nil,
|
||||
"groupID", groupID,
|
||||
"userID", *memberCallbackResp.UserID,
|
||||
"originalTimestamp", muteEndTimeTimestamp)
|
||||
muteEndTime = time.Unix(muteEndTimeTimestamp, 0)
|
||||
} else if muteEndTimeTimestamp > nowUnixMilli+10*365*24*3600*1000 {
|
||||
// 时间戳超过当前时间10年,可能是单位错误,忽略
|
||||
log.ZWarn(ctx, "webhookBeforeMembersJoinGroup: MuteEndTime is too far in the future, ignoring", nil,
|
||||
"groupID", groupID,
|
||||
"userID", *memberCallbackResp.UserID,
|
||||
"muteEndTimeTimestamp", muteEndTimeTimestamp,
|
||||
"nowUnixMilli", nowUnixMilli)
|
||||
continue
|
||||
} else {
|
||||
muteEndTime = time.UnixMilli(muteEndTimeTimestamp)
|
||||
}
|
||||
|
||||
// 记录webhook返回的禁言时间,用于排查自动禁言问题
|
||||
log.ZInfo(ctx, "webhookBeforeMembersJoinGroup: webhook returned MuteEndTime",
|
||||
"groupID", groupID,
|
||||
"userID", *memberCallbackResp.UserID,
|
||||
"muteEndTimeTimestamp", muteEndTimeTimestamp,
|
||||
"muteEndTime", muteEndTime.Format(time.RFC3339),
|
||||
"now", now.Format(time.RFC3339),
|
||||
"isMuted", muteEndTime.After(now),
|
||||
"mutedDurationSeconds", muteEndTime.Sub(now).Seconds())
|
||||
groupMembersMap[(*memberCallbackResp.UserID)].MuteEndTime = muteEndTime
|
||||
}
|
||||
|
||||
datautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].FaceURL, memberCallbackResp.FaceURL)
|
||||
datautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].Ex, memberCallbackResp.Ex)
|
||||
datautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].Nickname, memberCallbackResp.Nickname)
|
||||
datautil.NotNilReplace(&groupMembersMap[(*memberCallbackResp.UserID)].RoleLevel, memberCallbackResp.RoleLevel)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookBeforeSetGroupMemberInfo(ctx context.Context, before *config.BeforeConfig, req *group.SetGroupMemberInfo) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := callbackstruct.CallbackBeforeSetGroupMemberInfoReq{
|
||||
CallbackCommand: callbackstruct.CallbackBeforeSetGroupMemberInfoCommand,
|
||||
GroupID: req.GroupID,
|
||||
UserID: req.UserID,
|
||||
}
|
||||
if req.Nickname != nil {
|
||||
cbReq.Nickname = &req.Nickname.Value
|
||||
}
|
||||
if req.FaceURL != nil {
|
||||
cbReq.FaceURL = &req.FaceURL.Value
|
||||
}
|
||||
if req.RoleLevel != nil {
|
||||
cbReq.RoleLevel = &req.RoleLevel.Value
|
||||
}
|
||||
if req.Ex != nil {
|
||||
cbReq.Ex = &req.Ex.Value
|
||||
}
|
||||
resp := &callbackstruct.CallbackBeforeSetGroupMemberInfoResp{}
|
||||
if err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.FaceURL != nil {
|
||||
req.FaceURL = wrapperspb.String(*resp.FaceURL)
|
||||
}
|
||||
if resp.Nickname != nil {
|
||||
req.Nickname = wrapperspb.String(*resp.Nickname)
|
||||
}
|
||||
if resp.RoleLevel != nil {
|
||||
req.RoleLevel = wrapperspb.Int32(*resp.RoleLevel)
|
||||
}
|
||||
if resp.Ex != nil {
|
||||
req.Ex = wrapperspb.String(*resp.Ex)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterSetGroupMemberInfo(ctx context.Context, after *config.AfterConfig, req *group.SetGroupMemberInfo) {
|
||||
cbReq := callbackstruct.CallbackAfterSetGroupMemberInfoReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterSetGroupMemberInfoCommand,
|
||||
GroupID: req.GroupID,
|
||||
UserID: req.UserID,
|
||||
}
|
||||
if req.Nickname != nil {
|
||||
cbReq.Nickname = &req.Nickname.Value
|
||||
}
|
||||
if req.FaceURL != nil {
|
||||
cbReq.FaceURL = &req.FaceURL.Value
|
||||
}
|
||||
if req.RoleLevel != nil {
|
||||
cbReq.RoleLevel = &req.RoleLevel.Value
|
||||
}
|
||||
if req.Ex != nil {
|
||||
cbReq.Ex = &req.Ex.Value
|
||||
}
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterSetGroupMemberInfoResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterQuitGroup(ctx context.Context, after *config.AfterConfig, req *group.QuitGroupReq) {
|
||||
cbReq := &callbackstruct.CallbackQuitGroupReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterQuitGroupCommand,
|
||||
GroupID: req.GroupID,
|
||||
UserID: req.UserID,
|
||||
}
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackQuitGroupResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterKickGroupMember(ctx context.Context, after *config.AfterConfig, req *group.KickGroupMemberReq) {
|
||||
cbReq := &callbackstruct.CallbackKillGroupMemberReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterKickGroupCommand,
|
||||
GroupID: req.GroupID,
|
||||
KickedUserIDs: req.KickedUserIDs,
|
||||
Reason: req.Reason,
|
||||
}
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackKillGroupMemberResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterDismissGroup(ctx context.Context, after *config.AfterConfig, req *callbackstruct.CallbackDisMissGroupReq) {
|
||||
req.CallbackCommand = callbackstruct.CallbackAfterDisMissGroupCommand
|
||||
g.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &callbackstruct.CallbackDisMissGroupResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookBeforeApplyJoinGroup(ctx context.Context, before *config.BeforeConfig, req *callbackstruct.CallbackJoinGroupReq) (err error) {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
req.CallbackCommand = callbackstruct.CallbackBeforeJoinGroupCommand
|
||||
resp := &callbackstruct.CallbackJoinGroupResp{}
|
||||
if err := g.webhookClient.SyncPost(ctx, req.GetCallbackCommand(), req, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterTransferGroupOwner(ctx context.Context, after *config.AfterConfig, req *group.TransferGroupOwnerReq) {
|
||||
cbReq := &callbackstruct.CallbackTransferGroupOwnerReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterTransferGroupOwnerCommand,
|
||||
GroupID: req.GroupID,
|
||||
OldOwnerUserID: req.OldOwnerUserID,
|
||||
NewOwnerUserID: req.NewOwnerUserID,
|
||||
}
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackTransferGroupOwnerResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookBeforeInviteUserToGroup(ctx context.Context, before *config.BeforeConfig, req *group.InviteUserToGroupReq) (err error) {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &callbackstruct.CallbackBeforeInviteUserToGroupReq{
|
||||
CallbackCommand: callbackstruct.CallbackBeforeInviteJoinGroupCommand,
|
||||
OperationID: mcontext.GetOperationID(ctx),
|
||||
GroupID: req.GroupID,
|
||||
Reason: req.Reason,
|
||||
InvitedUserIDs: req.InvitedUserIDs,
|
||||
}
|
||||
|
||||
resp := &callbackstruct.CallbackBeforeInviteUserToGroupResp{}
|
||||
if err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle the scenario where certain members are refused
|
||||
// You might want to update the req.Members list or handle it as per your business logic
|
||||
|
||||
// if len(resp.RefusedMembersAccount) > 0 {
|
||||
// implement members are refused
|
||||
// }
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterJoinGroup(ctx context.Context, after *config.AfterConfig, req *group.JoinGroupReq) {
|
||||
cbReq := &callbackstruct.CallbackAfterJoinGroupReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterJoinGroupCommand,
|
||||
OperationID: mcontext.GetOperationID(ctx),
|
||||
GroupID: req.GroupID,
|
||||
ReqMessage: req.ReqMessage,
|
||||
JoinSource: req.JoinSource,
|
||||
InviterUserID: req.InviterUserID,
|
||||
}
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterJoinGroupResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookBeforeSetGroupInfo(ctx context.Context, before *config.BeforeConfig, req *group.SetGroupInfoReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &callbackstruct.CallbackBeforeSetGroupInfoReq{
|
||||
CallbackCommand: callbackstruct.CallbackBeforeSetGroupInfoCommand,
|
||||
GroupID: req.GroupInfoForSet.GroupID,
|
||||
Notification: req.GroupInfoForSet.Notification,
|
||||
Introduction: req.GroupInfoForSet.Introduction,
|
||||
FaceURL: req.GroupInfoForSet.FaceURL,
|
||||
GroupName: req.GroupInfoForSet.GroupName,
|
||||
}
|
||||
if req.GroupInfoForSet.Ex != nil {
|
||||
cbReq.Ex = req.GroupInfoForSet.Ex.Value
|
||||
}
|
||||
log.ZDebug(ctx, "debug CallbackBeforeSetGroupInfo", "ex", cbReq.Ex)
|
||||
if req.GroupInfoForSet.NeedVerification != nil {
|
||||
cbReq.NeedVerification = req.GroupInfoForSet.NeedVerification.Value
|
||||
}
|
||||
if req.GroupInfoForSet.LookMemberInfo != nil {
|
||||
cbReq.LookMemberInfo = req.GroupInfoForSet.LookMemberInfo.Value
|
||||
}
|
||||
if req.GroupInfoForSet.ApplyMemberFriend != nil {
|
||||
cbReq.ApplyMemberFriend = req.GroupInfoForSet.ApplyMemberFriend.Value
|
||||
}
|
||||
resp := &callbackstruct.CallbackBeforeSetGroupInfoResp{}
|
||||
|
||||
if err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Ex != nil {
|
||||
req.GroupInfoForSet.Ex = wrapperspb.String(*resp.Ex)
|
||||
}
|
||||
if resp.NeedVerification != nil {
|
||||
req.GroupInfoForSet.NeedVerification = wrapperspb.Int32(*resp.NeedVerification)
|
||||
}
|
||||
if resp.LookMemberInfo != nil {
|
||||
req.GroupInfoForSet.LookMemberInfo = wrapperspb.Int32(*resp.LookMemberInfo)
|
||||
}
|
||||
if resp.ApplyMemberFriend != nil {
|
||||
req.GroupInfoForSet.ApplyMemberFriend = wrapperspb.Int32(*resp.ApplyMemberFriend)
|
||||
}
|
||||
datautil.NotNilReplace(&req.GroupInfoForSet.GroupID, &resp.GroupID)
|
||||
datautil.NotNilReplace(&req.GroupInfoForSet.GroupName, &resp.GroupName)
|
||||
datautil.NotNilReplace(&req.GroupInfoForSet.FaceURL, &resp.FaceURL)
|
||||
datautil.NotNilReplace(&req.GroupInfoForSet.Introduction, &resp.Introduction)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterSetGroupInfo(ctx context.Context, after *config.AfterConfig, req *group.SetGroupInfoReq) {
|
||||
cbReq := &callbackstruct.CallbackAfterSetGroupInfoReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterSetGroupInfoCommand,
|
||||
GroupID: req.GroupInfoForSet.GroupID,
|
||||
Notification: req.GroupInfoForSet.Notification,
|
||||
Introduction: req.GroupInfoForSet.Introduction,
|
||||
FaceURL: req.GroupInfoForSet.FaceURL,
|
||||
GroupName: req.GroupInfoForSet.GroupName,
|
||||
}
|
||||
if req.GroupInfoForSet.Ex != nil {
|
||||
cbReq.Ex = &req.GroupInfoForSet.Ex.Value
|
||||
}
|
||||
if req.GroupInfoForSet.NeedVerification != nil {
|
||||
cbReq.NeedVerification = &req.GroupInfoForSet.NeedVerification.Value
|
||||
}
|
||||
if req.GroupInfoForSet.LookMemberInfo != nil {
|
||||
cbReq.LookMemberInfo = &req.GroupInfoForSet.LookMemberInfo.Value
|
||||
}
|
||||
if req.GroupInfoForSet.ApplyMemberFriend != nil {
|
||||
cbReq.ApplyMemberFriend = &req.GroupInfoForSet.ApplyMemberFriend.Value
|
||||
}
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterSetGroupInfoResp{}, after)
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookBeforeSetGroupInfoEx(ctx context.Context, before *config.BeforeConfig, req *group.SetGroupInfoExReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &callbackstruct.CallbackBeforeSetGroupInfoExReq{
|
||||
CallbackCommand: callbackstruct.CallbackBeforeSetGroupInfoExCommand,
|
||||
GroupID: req.GroupID,
|
||||
GroupName: req.GroupName,
|
||||
Notification: req.Notification,
|
||||
Introduction: req.Introduction,
|
||||
FaceURL: req.FaceURL,
|
||||
}
|
||||
|
||||
if req.Ex != nil {
|
||||
cbReq.Ex = req.Ex
|
||||
}
|
||||
log.ZDebug(ctx, "debug CallbackBeforeSetGroupInfoEx", "ex", cbReq.Ex)
|
||||
|
||||
if req.NeedVerification != nil {
|
||||
cbReq.NeedVerification = req.NeedVerification
|
||||
}
|
||||
if req.LookMemberInfo != nil {
|
||||
cbReq.LookMemberInfo = req.LookMemberInfo
|
||||
}
|
||||
if req.ApplyMemberFriend != nil {
|
||||
cbReq.ApplyMemberFriend = req.ApplyMemberFriend
|
||||
}
|
||||
|
||||
resp := &callbackstruct.CallbackBeforeSetGroupInfoExResp{}
|
||||
|
||||
if err := g.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datautil.NotNilReplace(&req.GroupID, &resp.GroupID)
|
||||
datautil.NotNilReplace(&req.GroupName, &resp.GroupName)
|
||||
datautil.NotNilReplace(&req.FaceURL, &resp.FaceURL)
|
||||
datautil.NotNilReplace(&req.Introduction, &resp.Introduction)
|
||||
datautil.NotNilReplace(&req.Ex, &resp.Ex)
|
||||
datautil.NotNilReplace(&req.NeedVerification, &resp.NeedVerification)
|
||||
datautil.NotNilReplace(&req.LookMemberInfo, &resp.LookMemberInfo)
|
||||
datautil.NotNilReplace(&req.ApplyMemberFriend, &resp.ApplyMemberFriend)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *groupServer) webhookAfterSetGroupInfoEx(ctx context.Context, after *config.AfterConfig, req *group.SetGroupInfoExReq) {
|
||||
cbReq := &callbackstruct.CallbackAfterSetGroupInfoExReq{
|
||||
CallbackCommand: callbackstruct.CallbackAfterSetGroupInfoExCommand,
|
||||
GroupID: req.GroupID,
|
||||
GroupName: req.GroupName,
|
||||
Notification: req.Notification,
|
||||
Introduction: req.Introduction,
|
||||
FaceURL: req.FaceURL,
|
||||
}
|
||||
|
||||
if req.Ex != nil {
|
||||
cbReq.Ex = req.Ex
|
||||
}
|
||||
if req.NeedVerification != nil {
|
||||
cbReq.NeedVerification = req.NeedVerification
|
||||
}
|
||||
if req.LookMemberInfo != nil {
|
||||
cbReq.LookMemberInfo = req.LookMemberInfo
|
||||
}
|
||||
if req.ApplyMemberFriend != nil {
|
||||
cbReq.ApplyMemberFriend = req.ApplyMemberFriend
|
||||
}
|
||||
|
||||
g.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &callbackstruct.CallbackAfterSetGroupInfoExResp{}, after)
|
||||
}
|
||||
63
internal/rpc/group/convert.go
Normal file
63
internal/rpc/group/convert.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2023 OpenIM. 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 group
|
||||
|
||||
import (
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
)
|
||||
|
||||
func (g *groupServer) groupDB2PB(group *model.Group, ownerUserID string, memberCount uint32) *sdkws.GroupInfo {
|
||||
return &sdkws.GroupInfo{
|
||||
GroupID: group.GroupID,
|
||||
GroupName: group.GroupName,
|
||||
Notification: group.Notification,
|
||||
Introduction: group.Introduction,
|
||||
FaceURL: group.FaceURL,
|
||||
OwnerUserID: ownerUserID,
|
||||
CreateTime: group.CreateTime.UnixMilli(),
|
||||
MemberCount: memberCount,
|
||||
Ex: group.Ex,
|
||||
Status: group.Status,
|
||||
CreatorUserID: group.CreatorUserID,
|
||||
GroupType: group.GroupType,
|
||||
NeedVerification: group.NeedVerification,
|
||||
LookMemberInfo: group.LookMemberInfo,
|
||||
ApplyMemberFriend: group.ApplyMemberFriend,
|
||||
NotificationUpdateTime: group.NotificationUpdateTime.UnixMilli(),
|
||||
NotificationUserID: group.NotificationUserID,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *groupServer) groupMemberDB2PB(member *model.GroupMember, appMangerLevel int32) *sdkws.GroupMemberFullInfo {
|
||||
return &sdkws.GroupMemberFullInfo{
|
||||
GroupID: member.GroupID,
|
||||
UserID: member.UserID,
|
||||
RoleLevel: member.RoleLevel,
|
||||
JoinTime: member.JoinTime.UnixMilli(),
|
||||
Nickname: member.Nickname,
|
||||
FaceURL: member.FaceURL,
|
||||
AppMangerLevel: appMangerLevel,
|
||||
JoinSource: member.JoinSource,
|
||||
OperatorUserID: member.OperatorUserID,
|
||||
Ex: member.Ex,
|
||||
MuteEndTime: member.MuteEndTime.UnixMilli(),
|
||||
InviterUserID: member.InviterUserID,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *groupServer) groupMemberDB2PB2(member *model.GroupMember) *sdkws.GroupMemberFullInfo {
|
||||
return g.groupMemberDB2PB(member, 0)
|
||||
}
|
||||
134
internal/rpc/group/db_map.go
Normal file
134
internal/rpc/group/db_map.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright © 2023 OpenIM. 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 group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pbgroup "git.imall.cloud/openim/protocol/group"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
func UpdateGroupInfoMap(ctx context.Context, group *sdkws.GroupInfoForSet) map[string]any {
|
||||
m := make(map[string]any)
|
||||
if group.GroupName != "" {
|
||||
m["group_name"] = group.GroupName
|
||||
}
|
||||
if group.Notification != "" {
|
||||
m["notification"] = group.Notification
|
||||
m["notification_update_time"] = time.Now()
|
||||
m["notification_user_id"] = mcontext.GetOpUserID(ctx)
|
||||
}
|
||||
if group.Introduction != "" {
|
||||
m["introduction"] = group.Introduction
|
||||
}
|
||||
if group.FaceURL != "" {
|
||||
m["face_url"] = group.FaceURL
|
||||
}
|
||||
if group.NeedVerification != nil {
|
||||
m["need_verification"] = group.NeedVerification.Value
|
||||
}
|
||||
if group.LookMemberInfo != nil {
|
||||
m["look_member_info"] = group.LookMemberInfo.Value
|
||||
}
|
||||
if group.ApplyMemberFriend != nil {
|
||||
m["apply_member_friend"] = group.ApplyMemberFriend.Value
|
||||
}
|
||||
if group.Ex != nil {
|
||||
m["ex"] = group.Ex.Value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
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 strings.TrimSpace(group.GroupName.Value) != "" {
|
||||
m["group_name"] = group.GroupName.Value
|
||||
groupNameFlag = true
|
||||
} else {
|
||||
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_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, normalFlag, groupNameFlag, notificationFlag, nil
|
||||
}
|
||||
|
||||
func UpdateGroupStatusMap(status int) map[string]any {
|
||||
return map[string]any{
|
||||
"status": status,
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateGroupMemberMutedTimeMap(t time.Time) map[string]any {
|
||||
return map[string]any{
|
||||
"mute_end_time": t,
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateGroupMemberMap(req *pbgroup.SetGroupMemberInfo) map[string]any {
|
||||
m := make(map[string]any)
|
||||
if req.Nickname != nil {
|
||||
m["nickname"] = req.Nickname.Value
|
||||
}
|
||||
if req.FaceURL != nil {
|
||||
m["face_url"] = req.FaceURL.Value
|
||||
}
|
||||
if req.RoleLevel != nil {
|
||||
m["role_level"] = req.RoleLevel.Value
|
||||
}
|
||||
if req.Ex != nil {
|
||||
m["ex"] = req.Ex.Value
|
||||
}
|
||||
return m
|
||||
}
|
||||
25
internal/rpc/group/fill.go
Normal file
25
internal/rpc/group/fill.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright © 2023 OpenIM. 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 group
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
relationtb "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
)
|
||||
|
||||
func (g *groupServer) PopulateGroupMember(ctx context.Context, members ...*relationtb.GroupMember) error {
|
||||
return g.notification.PopulateGroupMember(ctx, members...)
|
||||
}
|
||||
2096
internal/rpc/group/group.go
Normal file
2096
internal/rpc/group/group.go
Normal file
File diff suppressed because it is too large
Load Diff
930
internal/rpc/group/notification.go
Normal file
930
internal/rpc/group/notification.go
Normal file
@@ -0,0 +1,930 @@
|
||||
// Copyright © 2023 OpenIM. 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 group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/versionctx"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/msgprocessor"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification/common_user"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbgroup "git.imall.cloud/openim/protocol/group"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/openimsdk/tools/utils/jsonutil"
|
||||
"github.com/openimsdk/tools/utils/stringutil"
|
||||
)
|
||||
|
||||
// GroupApplicationReceiver
|
||||
const (
|
||||
applicantReceiver = iota
|
||||
adminReceiver
|
||||
)
|
||||
|
||||
func NewNotificationSender(db controller.GroupDatabase, config *Config, userClient *rpcli.UserClient, msgClient *rpcli.MsgClient, conversationClient *rpcli.ConversationClient) *NotificationSender {
|
||||
return &NotificationSender{
|
||||
NotificationSender: notification.NewNotificationSender(&config.NotificationConfig,
|
||||
notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {
|
||||
return msgClient.SendMsg(ctx, req)
|
||||
}),
|
||||
notification.WithUserRpcClient(userClient.GetUserInfo),
|
||||
),
|
||||
getUsersInfo: func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error) {
|
||||
users, err := userClient.GetUsersInfo(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return datautil.Slice(users, func(e *sdkws.UserInfo) common_user.CommonUser { return e }), nil
|
||||
},
|
||||
db: db,
|
||||
config: config,
|
||||
msgClient: msgClient,
|
||||
conversationClient: conversationClient,
|
||||
}
|
||||
}
|
||||
|
||||
type NotificationSender struct {
|
||||
*notification.NotificationSender
|
||||
getUsersInfo func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error)
|
||||
db controller.GroupDatabase
|
||||
config *Config
|
||||
msgClient *rpcli.MsgClient
|
||||
conversationClient *rpcli.ConversationClient
|
||||
}
|
||||
|
||||
func (g *NotificationSender) PopulateGroupMember(ctx context.Context, members ...*model.GroupMember) error {
|
||||
if len(members) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 收集所有需要填充用户信息的UserID
|
||||
userIDsMap := make(map[string]struct{})
|
||||
for _, member := range members {
|
||||
userIDsMap[member.UserID] = struct{}{}
|
||||
}
|
||||
|
||||
// 获取所有用户信息
|
||||
users, err := g.getUsersInfo(ctx, datautil.Keys(userIDsMap))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建用户信息map
|
||||
userMap := make(map[string]common_user.CommonUser)
|
||||
for i, user := range users {
|
||||
userMap[user.GetUserID()] = users[i]
|
||||
}
|
||||
|
||||
// 填充群成员信息
|
||||
for i, member := range members {
|
||||
user, ok := userMap[member.UserID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// 填充昵称和头像
|
||||
if member.Nickname == "" {
|
||||
members[i].Nickname = user.GetNickname()
|
||||
}
|
||||
if member.FaceURL == "" {
|
||||
members[i].FaceURL = user.GetFaceURL()
|
||||
}
|
||||
|
||||
// 填充UserType和UserFlag到Ex字段
|
||||
// 先解析现有的Ex字段(如果有的话)
|
||||
var exData map[string]interface{}
|
||||
if members[i].Ex != "" {
|
||||
_ = jsonutil.JsonUnmarshal([]byte(members[i].Ex), &exData)
|
||||
}
|
||||
if exData == nil {
|
||||
exData = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// 添加userType和userFlag
|
||||
exData["userType"] = user.GetUserType()
|
||||
exData["userFlag"] = user.GetUserFlag()
|
||||
|
||||
exJSON, _ := jsonutil.JsonMarshal(exData)
|
||||
members[i].Ex = string(exJSON)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) getUser(ctx context.Context, userID string) (*sdkws.PublicUserInfo, error) {
|
||||
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))
|
||||
}
|
||||
return &sdkws.PublicUserInfo{
|
||||
UserID: users[0].GetUserID(),
|
||||
Nickname: users[0].GetNickname(),
|
||||
FaceURL: users[0].GetFaceURL(),
|
||||
Ex: users[0].GetEx(),
|
||||
UserType: users[0].GetUserType(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) getGroupInfo(ctx context.Context, groupID string) (*sdkws.GroupInfo, error) {
|
||||
gm, err := g.db.TakeGroup(ctx, groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
num, err := g.db.FindGroupMemberNum(ctx, groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ownerUserIDs, err := g.db.GetGroupRoleLevelMemberIDs(ctx, groupID, constant.GroupOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ownerUserID string
|
||||
if len(ownerUserIDs) > 0 {
|
||||
ownerUserID = ownerUserIDs[0]
|
||||
}
|
||||
|
||||
return convert.Db2PbGroupInfo(gm, ownerUserID, num), nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) getGroupMembers(ctx context.Context, groupID string, userIDs []string) ([]*sdkws.GroupMemberFullInfo, error) {
|
||||
members, err := g.db.FindGroupMembers(ctx, groupID, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := g.PopulateGroupMember(ctx, members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.ZDebug(ctx, "getGroupMembers", "members", members)
|
||||
res := make([]*sdkws.GroupMemberFullInfo, 0, len(members))
|
||||
for _, member := range members {
|
||||
res = append(res, g.groupMemberDB2PB(member, 0))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) getGroupMemberMap(ctx context.Context, groupID string, userIDs []string) (map[string]*sdkws.GroupMemberFullInfo, error) {
|
||||
members, err := g.getGroupMembers(ctx, groupID, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*sdkws.GroupMemberFullInfo)
|
||||
for i, member := range members {
|
||||
m[member.UserID] = members[i]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) getGroupMember(ctx context.Context, groupID string, userID string) (*sdkws.GroupMemberFullInfo, error) {
|
||||
members, err := g.getGroupMembers(ctx, groupID, []string{userID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(members) == 0 {
|
||||
return nil, errs.ErrInternalServer.WrapMsg(fmt.Sprintf("group %s member %s not found", groupID, userID))
|
||||
}
|
||||
return members[0], nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) getGroupOwnerAndAdminUserID(ctx context.Context, groupID string) ([]string, error) {
|
||||
members, err := g.db.FindGroupMemberRoleLevels(ctx, groupID, []int32{constant.GroupOwner, constant.GroupAdmin})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := g.PopulateGroupMember(ctx, members...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fn := func(e *model.GroupMember) string { return e.UserID }
|
||||
return datautil.Slice(members, fn), nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) groupMemberDB2PB(member *model.GroupMember, appMangerLevel int32) *sdkws.GroupMemberFullInfo {
|
||||
return &sdkws.GroupMemberFullInfo{
|
||||
GroupID: member.GroupID,
|
||||
UserID: member.UserID,
|
||||
RoleLevel: member.RoleLevel,
|
||||
JoinTime: member.JoinTime.UnixMilli(),
|
||||
Nickname: member.Nickname,
|
||||
FaceURL: member.FaceURL,
|
||||
AppMangerLevel: appMangerLevel,
|
||||
JoinSource: member.JoinSource,
|
||||
OperatorUserID: member.OperatorUserID,
|
||||
Ex: member.Ex,
|
||||
MuteEndTime: member.MuteEndTime.UnixMilli(),
|
||||
InviterUserID: member.InviterUserID,
|
||||
}
|
||||
}
|
||||
|
||||
/* func (g *NotificationSender) getUsersInfoMap(ctx context.Context, userIDs []string) (map[string]*sdkws.UserInfo, error) {
|
||||
users, err := g.getUsersInfo(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]*sdkws.UserInfo)
|
||||
for _, user := range users {
|
||||
result[user.GetUserID()] = user.(*sdkws.UserInfo)
|
||||
}
|
||||
return result, nil
|
||||
} */
|
||||
|
||||
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) 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.CheckUserIsAdmin(ctx, userID) {
|
||||
*targetUser = &sdkws.GroupMemberFullInfo{
|
||||
GroupID: groupID,
|
||||
UserID: userID,
|
||||
RoleLevel: constant.GroupAdmin,
|
||||
AppMangerLevel: constant.AppAdmin,
|
||||
}
|
||||
} else {
|
||||
member, err := g.db.TakeGroupMember(ctx, groupID, userID)
|
||||
if err == nil {
|
||||
*targetUser = g.groupMemberDB2PB(member, 0)
|
||||
} else if !(errors.Is(err, mongo.ErrNoDocuments) || errs.ErrRecordNotFound.Is(err)) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
user, err := g.getUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *targetUser == nil {
|
||||
*targetUser = &sdkws.GroupMemberFullInfo{
|
||||
GroupID: groupID,
|
||||
UserID: userID,
|
||||
Nickname: user.Nickname,
|
||||
FaceURL: user.FaceURL,
|
||||
OperatorUserID: userID,
|
||||
}
|
||||
} else {
|
||||
if (*targetUser).Nickname == "" {
|
||||
(*targetUser).Nickname = user.Nickname
|
||||
}
|
||||
if (*targetUser).FaceURL == "" {
|
||||
(*targetUser).FaceURL = user.FaceURL
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) setVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string) {
|
||||
versions := versionctx.GetVersionLog(ctx).Get()
|
||||
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()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *NotificationSender) setSortVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string, sortVersion *uint64) {
|
||||
versions := versionctx.GetVersionLog(ctx).Get()
|
||||
for _, coll := range versions {
|
||||
if coll.Name == collName && coll.Doc.DID == id {
|
||||
*version = uint64(coll.Doc.Version)
|
||||
*versionID = coll.Doc.ID.Hex()
|
||||
for _, elem := range coll.Doc.Logs {
|
||||
if elem.EID == model.VersionSortChangeID {
|
||||
*sortVersion = uint64(elem.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupCreatedNotification(ctx context.Context, tips *sdkws.GroupCreatedTips, SendMessage *bool) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
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.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupCreatedNotification, tips, notification.WithSendMessage(SendMessage))
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupInfoSetNotification(ctx context.Context, tips *sdkws.GroupInfoSetTips) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
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.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetNotification, tips, notification.WithRpcGetUserName())
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupInfoSetNameNotification(ctx context.Context, tips *sdkws.GroupInfoSetNameTips) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
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.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetNameNotification, tips)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupInfoSetAnnouncementNotification(ctx context.Context, tips *sdkws.GroupInfoSetAnnouncementTips, sendMessage *bool) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
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.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupInfoSetAnnouncementNotification, tips, notification.WithRpcGetUserName(), notification.WithSendMessage(sendMessage))
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
var user *sdkws.PublicUserInfo
|
||||
user, err = g.getUser(ctx, req.InviterUserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userIDs, err := g.getGroupOwnerAndAdminUserID(ctx, req.GroupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userIDs = append(userIDs, req.InviterUserID, mcontext.GetOpUserID(ctx))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *NotificationSender) MemberQuitNotification(ctx context.Context, member *sdkws.GroupMemberFullInfo) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, member.GroupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tips := &sdkws.MemberQuitTips{Group: group, QuitUser: member}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, member.GroupID)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), member.GroupID, constant.MemberQuitNotification, tips)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupApplicationAcceptedNotification(ctx context.Context, req *pbgroup.GroupApplicationResponseReq) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
var userIDs []string
|
||||
userIDs, err = g.getGroupOwnerAndAdminUserID(ctx, req.GroupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var opUser *sdkws.GroupMemberFullInfo
|
||||
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) {
|
||||
if userID == req.FromUserID {
|
||||
tips.ReceiverAs = applicantReceiver
|
||||
} else {
|
||||
tips.ReceiverAs = adminReceiver
|
||||
}
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), userID, constant.GroupApplicationAcceptedNotification, tips)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupApplicationRejectedNotification(ctx context.Context, req *pbgroup.GroupApplicationResponseReq) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
var userIDs []string
|
||||
userIDs, err = g.getGroupOwnerAndAdminUserID(ctx, req.GroupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var opUser *sdkws.GroupMemberFullInfo
|
||||
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) {
|
||||
if userID == req.FromUserID {
|
||||
tips.ReceiverAs = applicantReceiver
|
||||
} else {
|
||||
tips.ReceiverAs = adminReceiver
|
||||
}
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), userID, constant.GroupApplicationRejectedNotification, tips)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupOwnerTransferredNotification(ctx context.Context, req *pbgroup.TransferGroupOwnerReq) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, req.GroupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
opUserID := mcontext.GetOpUserID(ctx)
|
||||
var member map[string]*sdkws.GroupMemberFullInfo
|
||||
member, err = g.getGroupMemberMap(ctx, req.GroupID, []string{opUserID, req.NewOwnerUserID, req.OldOwnerUserID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tips := &sdkws.GroupOwnerTransferredTips{
|
||||
Group: group,
|
||||
OpUser: member[opUserID],
|
||||
NewGroupOwner: member[req.NewOwnerUserID],
|
||||
OldGroupOwnerInfo: member[req.OldOwnerUserID],
|
||||
}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, req.GroupID)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupOwnerTransferredNotification, tips)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) MemberKickedNotification(ctx context.Context, tips *sdkws.MemberKickedTips, SendMessage *bool) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
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.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.MemberKickedNotification, tips, notification.WithSendMessage(SendMessage))
|
||||
}
|
||||
|
||||
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 {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if !g.config.RpcConfig.EnableHistoryForNewMembers {
|
||||
conversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID)
|
||||
maxSeq, err := g.msgClient.GetConversationMaxSeq(ctx, conversationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.msgClient.SetUserConversationsMinSeq(ctx, conversationID, entrantUserID, maxSeq+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := g.conversationClient.CreateGroupChatConversations(ctx, groupID, entrantUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users, err := g.getGroupMembers(ctx, groupID, entrantUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tips := &sdkws.MemberInvitedTips{
|
||||
Group: group,
|
||||
InvitedUserList: users,
|
||||
}
|
||||
opUserID := mcontext.GetOpUserID(ctx)
|
||||
if err = g.fillUserByUserID(ctx, opUserID, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return nil
|
||||
}
|
||||
if invitedOpUserID == opUserID {
|
||||
tips.InviterUser = tips.OpUser
|
||||
} else {
|
||||
if err = g.fillUserByUserID(ctx, invitedOpUserID, &tips.InviterUser, tips.Group.GroupID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.MemberInvitedNotification, tips, notification.WithSendMessage(SendMessage))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) MemberEnterNotification(ctx context.Context, groupID string, entrantUserID string) error {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if !g.config.RpcConfig.EnableHistoryForNewMembers {
|
||||
conversationID := msgprocessor.GetConversationIDBySessionType(constant.ReadGroupChatType, groupID)
|
||||
maxSeq, err := g.msgClient.GetConversationMaxSeq(ctx, conversationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.msgClient.SetUserConversationsMinSeq(ctx, conversationID, []string{entrantUserID}, maxSeq+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := g.conversationClient.CreateGroupChatConversations(ctx, groupID, []string{entrantUserID}); err != nil {
|
||||
return err
|
||||
}
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := g.getGroupMember(ctx, groupID, entrantUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tips := &sdkws.MemberEnterTips{
|
||||
Group: group,
|
||||
EntrantUser: user,
|
||||
OperationTime: time.Now().UnixMilli(),
|
||||
}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)
|
||||
// 群内广播:通知所有群成员有人入群
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.MemberEnterNotification, tips)
|
||||
// 给入群本人发送一条系统通知(单聊),便于客户端展示“你已加入群聊”
|
||||
g.NotificationWithSessionType(ctx, mcontext.GetOpUserID(ctx), entrantUserID,
|
||||
constant.MemberEnterNotification, constant.SingleChatType, tips)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupDismissedNotification(ctx context.Context, tips *sdkws.GroupDismissedTips, SendMessage *bool) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), tips.Group.GroupID, constant.GroupDismissedNotification, tips, notification.WithSendMessage(SendMessage))
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupMemberMutedNotification(ctx context.Context, groupID, groupMemberUserID string, mutedSeconds uint32) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
log.ZDebug(ctx, "GroupMemberMutedNotification start", "groupID", groupID, "groupMemberUserID", groupMemberUserID, "mutedSeconds", mutedSeconds, "opUserID", mcontext.GetOpUserID(ctx))
|
||||
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "GroupMemberMutedNotification getGroupInfo failed", err, "groupID", groupID)
|
||||
return
|
||||
}
|
||||
log.ZDebug(ctx, "GroupMemberMutedNotification got group info", "groupID", groupID, "groupName", group.GroupName)
|
||||
|
||||
var user map[string]*sdkws.GroupMemberFullInfo
|
||||
user, err = g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "GroupMemberMutedNotification getGroupMemberMap failed", err, "groupID", groupID)
|
||||
return
|
||||
}
|
||||
log.ZDebug(ctx, "GroupMemberMutedNotification got user map", "opUser", user[mcontext.GetOpUserID(ctx)], "mutedUser", user[groupMemberUserID])
|
||||
|
||||
tips := &sdkws.GroupMemberMutedTips{
|
||||
Group: group, MutedSeconds: mutedSeconds,
|
||||
OpUser: user[mcontext.GetOpUserID(ctx)], MutedUser: user[groupMemberUserID],
|
||||
}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
log.ZWarn(ctx, "GroupMemberMutedNotification fillOpUser failed", err)
|
||||
return
|
||||
}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)
|
||||
// 在群聊中通知,但推送服务会过滤只推送给群主、管理员和被禁言成员本人
|
||||
log.ZInfo(ctx, "GroupMemberMutedNotification sending notification", "groupID", groupID, "recvID", group.GroupID, "contentType", constant.GroupMemberMutedNotification, "mutedUserID", groupMemberUserID, "mutedSeconds", mutedSeconds)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberMutedNotification, tips)
|
||||
log.ZDebug(ctx, "GroupMemberMutedNotification notification sent", "groupID", groupID)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupMemberCancelMutedNotification(ctx context.Context, groupID, groupMemberUserID string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
log.ZDebug(ctx, "GroupMemberCancelMutedNotification start", "groupID", groupID, "groupMemberUserID", groupMemberUserID, "opUserID", mcontext.GetOpUserID(ctx))
|
||||
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "GroupMemberCancelMutedNotification getGroupInfo failed", err, "groupID", groupID)
|
||||
return
|
||||
}
|
||||
log.ZDebug(ctx, "GroupMemberCancelMutedNotification got group info", "groupID", groupID, "groupName", group.GroupName)
|
||||
|
||||
var user map[string]*sdkws.GroupMemberFullInfo
|
||||
user, err = g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "GroupMemberCancelMutedNotification getGroupMemberMap failed", err, "groupID", groupID)
|
||||
return
|
||||
}
|
||||
log.ZDebug(ctx, "GroupMemberCancelMutedNotification got user map", "opUser", user[mcontext.GetOpUserID(ctx)], "mutedUser", user[groupMemberUserID])
|
||||
|
||||
tips := &sdkws.GroupMemberCancelMutedTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], MutedUser: user[groupMemberUserID]}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
log.ZWarn(ctx, "GroupMemberCancelMutedNotification fillOpUser failed", err)
|
||||
return
|
||||
}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID)
|
||||
// 在群聊中通知,但推送服务会过滤只推送给群主、管理员和被取消禁言成员本人
|
||||
log.ZInfo(ctx, "GroupMemberCancelMutedNotification sending notification", "groupID", groupID, "recvID", group.GroupID, "contentType", constant.GroupMemberCancelMutedNotification, "cancelMutedUserID", groupMemberUserID)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberCancelMutedNotification, tips)
|
||||
log.ZDebug(ctx, "GroupMemberCancelMutedNotification notification sent", "groupID", groupID)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupMutedNotification(ctx context.Context, groupID string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var users []*sdkws.GroupMemberFullInfo
|
||||
users, err = g.getGroupMembers(ctx, groupID, []string{mcontext.GetOpUserID(ctx)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tips := &sdkws.GroupMutedTips{Group: group}
|
||||
if len(users) > 0 {
|
||||
tips.OpUser = users[0]
|
||||
}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, groupID)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMutedNotification, tips)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupCancelMutedNotification(ctx context.Context, groupID string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var users []*sdkws.GroupMemberFullInfo
|
||||
users, err = g.getGroupMembers(ctx, groupID, []string{mcontext.GetOpUserID(ctx)})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tips := &sdkws.GroupCancelMutedTips{Group: group}
|
||||
if len(users) > 0 {
|
||||
tips.OpUser = users[0]
|
||||
}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
g.setVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, groupID)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupCancelMutedNotification, tips)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupMemberInfoSetNotification(ctx context.Context, groupID, groupMemberUserID string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var user map[string]*sdkws.GroupMemberFullInfo
|
||||
user, err = g.getGroupMemberMap(ctx, groupID, []string{groupMemberUserID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tips := &sdkws.GroupMemberInfoSetTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], ChangedUser: user[groupMemberUserID]}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
g.setSortVersion(ctx, &tips.GroupMemberVersion, &tips.GroupMemberVersionID, database.GroupMemberVersionName, tips.Group.GroupID, &tips.GroupSortVersion)
|
||||
g.Notification(ctx, mcontext.GetOpUserID(ctx), group.GroupID, constant.GroupMemberInfoSetNotification, tips)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupMemberSetToAdminNotification(ctx context.Context, groupID, groupMemberUserID string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
user, err := g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tips := &sdkws.GroupMemberInfoSetTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], ChangedUser: user[groupMemberUserID]}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func (g *NotificationSender) GroupMemberSetToOrdinaryUserNotification(ctx context.Context, groupID, groupMemberUserID string) {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.ZError(ctx, stringutil.GetFuncName(1)+" failed", err)
|
||||
}
|
||||
}()
|
||||
var group *sdkws.GroupInfo
|
||||
group, err = g.getGroupInfo(ctx, groupID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var user map[string]*sdkws.GroupMemberFullInfo
|
||||
user, err = g.getGroupMemberMap(ctx, groupID, []string{mcontext.GetOpUserID(ctx), groupMemberUserID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tips := &sdkws.GroupMemberInfoSetTips{Group: group, OpUser: user[mcontext.GetOpUserID(ctx)], ChangedUser: user[groupMemberUserID]}
|
||||
if err = g.fillOpUser(ctx, &tips.OpUser, tips.Group.GroupID); err != nil {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
47
internal/rpc/group/statistics.go
Normal file
47
internal/rpc/group/statistics.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright © 2023 OpenIM. 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 group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/protocol/group"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
func (g *groupServer) GroupCreateCount(ctx context.Context, req *group.GroupCreateCountReq) (*group.GroupCreateCountResp, error) {
|
||||
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
|
||||
}
|
||||
start := time.UnixMilli(req.Start)
|
||||
before, err := g.db.CountTotal(ctx, &start)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count, err := g.db.CountRangeEverydayTotal(ctx, start, time.UnixMilli(req.End))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &group.GroupCreateCountResp{Total: total, Before: before, Count: count}, nil
|
||||
}
|
||||
197
internal/rpc/group/sync.go
Normal file
197
internal/rpc/group/sync.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/internal/rpc/incrversion"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/util/hashutil"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbgroup "git.imall.cloud/openim/protocol/group"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
const versionSyncLimit = 500
|
||||
|
||||
func (g *groupServer) GetFullGroupMemberUserIDs(ctx context.Context, req *pbgroup.GetFullGroupMemberUserIDsReq) (*pbgroup.GetFullGroupMemberUserIDsResp, error) {
|
||||
userIDs, err := g.db.FindGroupMemberUserID(ctx, req.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := authverify.CheckAccessIn(ctx, userIDs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vl, err := g.db.FindMaxGroupMemberVersionCache(ctx, req.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idHash := hashutil.IdHash(userIDs)
|
||||
if req.IdHash == idHash {
|
||||
userIDs = nil
|
||||
}
|
||||
return &pbgroup.GetFullGroupMemberUserIDsResp{
|
||||
Version: idHash,
|
||||
VersionID: vl.ID.Hex(),
|
||||
Equal: req.IdHash == idHash,
|
||||
UserIDs: userIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
groupIDs, err := g.db.FindJoinGroupID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idHash := hashutil.IdHash(groupIDs)
|
||||
if req.IdHash == idHash {
|
||||
groupIDs = nil
|
||||
}
|
||||
return &pbgroup.GetFullJoinGroupIDsResp{
|
||||
Version: idHash,
|
||||
VersionID: vl.ID.Hex(),
|
||||
Equal: req.IdHash == idHash,
|
||||
GroupIDs: groupIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if group.Status == constant.GroupStatusDismissed {
|
||||
return nil, servererrs.ErrDismissedAlready.Wrap()
|
||||
}
|
||||
var (
|
||||
hasGroupUpdate bool
|
||||
sortVersion uint64
|
||||
)
|
||||
opt := incrversion.Option[*sdkws.GroupMemberFullInfo, pbgroup.GetIncrementalGroupMemberResp]{
|
||||
Ctx: ctx,
|
||||
VersionKey: req.GroupID,
|
||||
VersionID: req.VersionID,
|
||||
VersionNumber: req.Version,
|
||||
Version: func(ctx context.Context, groupID string, version uint, limit int) (*model.VersionLog, error) {
|
||||
vl, err := g.db.FindMemberIncrVersion(ctx, groupID, version, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs := make([]model.VersionLogElem, 0, len(vl.Logs))
|
||||
for i, log := range vl.Logs {
|
||||
switch log.EID {
|
||||
case model.VersionGroupChangeID:
|
||||
vl.LogLen--
|
||||
hasGroupUpdate = true
|
||||
case model.VersionSortChangeID:
|
||||
vl.LogLen--
|
||||
sortVersion = uint64(log.Version)
|
||||
default:
|
||||
logs = append(logs, vl.Logs[i])
|
||||
}
|
||||
}
|
||||
vl.Logs = logs
|
||||
if vl.LogLen > 0 {
|
||||
hasGroupUpdate = true
|
||||
}
|
||||
return vl, nil
|
||||
},
|
||||
CacheMaxVersion: g.db.FindMaxGroupMemberVersionCache,
|
||||
Find: func(ctx context.Context, ids []string) ([]*sdkws.GroupMemberFullInfo, error) {
|
||||
return g.getGroupMembersInfo(ctx, req.GroupID, ids)
|
||||
},
|
||||
Resp: func(version *model.VersionLog, delIDs []string, insertList, updateList []*sdkws.GroupMemberFullInfo, full bool) *pbgroup.GetIncrementalGroupMemberResp {
|
||||
return &pbgroup.GetIncrementalGroupMemberResp{
|
||||
VersionID: version.ID.Hex(),
|
||||
Version: uint64(version.Version),
|
||||
Full: full,
|
||||
Delete: delIDs,
|
||||
Insert: insertList,
|
||||
Update: updateList,
|
||||
SortVersion: sortVersion,
|
||||
}
|
||||
},
|
||||
}
|
||||
resp, err := opt.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Full || hasGroupUpdate {
|
||||
count, err := g.db.FindGroupMemberNum(ctx, group.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner, err := g.db.TakeGroupOwner(ctx, group.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Group = g.groupDB2PB(group, owner.UserID, count)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (g *groupServer) GetIncrementalJoinGroup(ctx context.Context, req *pbgroup.GetIncrementalJoinGroupReq) (*pbgroup.GetIncrementalJoinGroupResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt := incrversion.Option[*sdkws.GroupInfo, pbgroup.GetIncrementalJoinGroupResp]{
|
||||
Ctx: ctx,
|
||||
VersionKey: req.UserID,
|
||||
VersionID: req.VersionID,
|
||||
VersionNumber: req.Version,
|
||||
Version: g.db.FindJoinIncrVersion,
|
||||
CacheMaxVersion: g.db.FindMaxJoinGroupVersionCache,
|
||||
Find: g.getGroupsInfo,
|
||||
Resp: func(version *model.VersionLog, delIDs []string, insertList, updateList []*sdkws.GroupInfo, full bool) *pbgroup.GetIncrementalJoinGroupResp {
|
||||
return &pbgroup.GetIncrementalJoinGroupResp{
|
||||
VersionID: version.ID.Hex(),
|
||||
Version: uint64(version.Version),
|
||||
Full: full,
|
||||
Delete: delIDs,
|
||||
Insert: insertList,
|
||||
Update: updateList,
|
||||
}
|
||||
},
|
||||
}
|
||||
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 {
|
||||
if errors.Is(err, servererrs.ErrDismissedAlready) {
|
||||
log.ZWarn(ctx, "Failed to get incremental group member", err, "groupID", memberReq.GroupID, "request", memberReq)
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
207
internal/rpc/incrversion/batch_option.go
Normal file
207
internal/rpc/incrversion/batch_option.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package incrversion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type BatchOption[A, B any] struct {
|
||||
Ctx context.Context
|
||||
TargetKeys []string
|
||||
VersionIDs []string
|
||||
VersionNumbers []uint64
|
||||
//SyncLimit int
|
||||
Versions func(ctx context.Context, dIds []string, versions []uint64, limits []int) (map[string]*model.VersionLog, error)
|
||||
CacheMaxVersions func(ctx context.Context, dIds []string) (map[string]*model.VersionLog, error)
|
||||
Find func(ctx context.Context, dId string, ids []string) (A, error)
|
||||
Resp func(versionsMap map[string]*model.VersionLog, deleteIdsMap map[string][]string, insertListMap, updateListMap map[string]A, fullMap map[string]bool) *B
|
||||
}
|
||||
|
||||
func (o *BatchOption[A, B]) newError(msg string) error {
|
||||
return errs.ErrInternalServer.WrapMsg(msg)
|
||||
}
|
||||
|
||||
func (o *BatchOption[A, B]) check() error {
|
||||
if o.Ctx == nil {
|
||||
return o.newError("opt ctx is nil")
|
||||
}
|
||||
if len(o.TargetKeys) == 0 {
|
||||
return o.newError("targetKeys is empty")
|
||||
}
|
||||
if o.Versions == nil {
|
||||
return o.newError("func versions is nil")
|
||||
}
|
||||
if o.Find == nil {
|
||||
return o.newError("func find is nil")
|
||||
}
|
||||
if o.Resp == nil {
|
||||
return o.newError("func resp is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *BatchOption[A, B]) validVersions() []bool {
|
||||
valids := make([]bool, len(o.VersionIDs))
|
||||
for i, versionID := range o.VersionIDs {
|
||||
objID, err := primitive.ObjectIDFromHex(versionID)
|
||||
valids[i] = (err == nil && (!objID.IsZero()) && o.VersionNumbers[i] > 0)
|
||||
}
|
||||
return valids
|
||||
}
|
||||
|
||||
func (o *BatchOption[A, B]) equalIDs(objIDs []primitive.ObjectID) []bool {
|
||||
equals := make([]bool, len(o.VersionIDs))
|
||||
for i, versionID := range o.VersionIDs {
|
||||
equals[i] = versionID == objIDs[i].Hex()
|
||||
}
|
||||
return equals
|
||||
}
|
||||
|
||||
func (o *BatchOption[A, B]) getVersions(tags *[]int) (versions map[string]*model.VersionLog, err error) {
|
||||
var dIDs []string
|
||||
var versionNums []uint64
|
||||
var limits []int
|
||||
|
||||
valids := o.validVersions()
|
||||
|
||||
if o.CacheMaxVersions == nil {
|
||||
for i, valid := range valids {
|
||||
if valid {
|
||||
(*tags)[i] = tagQuery
|
||||
dIDs = append(dIDs, o.TargetKeys[i])
|
||||
versionNums = append(versionNums, o.VersionNumbers[i])
|
||||
limits = append(limits, syncLimit)
|
||||
} else {
|
||||
(*tags)[i] = tagFull
|
||||
dIDs = append(dIDs, o.TargetKeys[i])
|
||||
versionNums = append(versionNums, 0)
|
||||
limits = append(limits, 0)
|
||||
}
|
||||
}
|
||||
|
||||
versions, err = o.Versions(o.Ctx, dIDs, versionNums, limits)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return versions, nil
|
||||
|
||||
} else {
|
||||
caches, err := o.CacheMaxVersions(o.Ctx, o.TargetKeys)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
objIDs := make([]primitive.ObjectID, len(o.VersionIDs))
|
||||
|
||||
for i, versionID := range o.VersionIDs {
|
||||
objID, _ := primitive.ObjectIDFromHex(versionID)
|
||||
objIDs[i] = objID
|
||||
}
|
||||
|
||||
equals := o.equalIDs(objIDs)
|
||||
for i, valid := range valids {
|
||||
if !valid {
|
||||
(*tags)[i] = tagFull
|
||||
} else if !equals[i] {
|
||||
(*tags)[i] = tagFull
|
||||
} else if o.VersionNumbers[i] == uint64(caches[o.TargetKeys[i]].Version) {
|
||||
(*tags)[i] = tagEqual
|
||||
} else {
|
||||
(*tags)[i] = tagQuery
|
||||
dIDs = append(dIDs, o.TargetKeys[i])
|
||||
versionNums = append(versionNums, o.VersionNumbers[i])
|
||||
limits = append(limits, syncLimit)
|
||||
|
||||
delete(caches, o.TargetKeys[i])
|
||||
}
|
||||
}
|
||||
|
||||
if dIDs != nil {
|
||||
versionMap, err := o.Versions(o.Ctx, dIDs, versionNums, limits)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
for k, v := range versionMap {
|
||||
caches[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
versions = caches
|
||||
}
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (o *BatchOption[A, B]) Build() (*B, error) {
|
||||
if err := o.check(); err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
tags := make([]int, len(o.TargetKeys))
|
||||
versions, err := o.getVersions(&tags)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
fullMap := make(map[string]bool)
|
||||
for i, tag := range tags {
|
||||
switch tag {
|
||||
case tagQuery:
|
||||
vLog := versions[o.TargetKeys[i]]
|
||||
fullMap[o.TargetKeys[i]] = vLog.ID.Hex() != o.VersionIDs[i] || uint64(vLog.Version) < o.VersionNumbers[i] || len(vLog.Logs) != vLog.LogLen
|
||||
case tagFull:
|
||||
fullMap[o.TargetKeys[i]] = true
|
||||
case tagEqual:
|
||||
fullMap[o.TargetKeys[i]] = false
|
||||
default:
|
||||
panic(fmt.Errorf("undefined tag %d", tag))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
insertIdsMap = make(map[string][]string)
|
||||
deleteIdsMap = make(map[string][]string)
|
||||
updateIdsMap = make(map[string][]string)
|
||||
)
|
||||
|
||||
for _, targetKey := range o.TargetKeys {
|
||||
if !fullMap[targetKey] {
|
||||
version := versions[targetKey]
|
||||
insertIds, deleteIds, updateIds := version.DeleteAndChangeIDs()
|
||||
insertIdsMap[targetKey] = insertIds
|
||||
deleteIdsMap[targetKey] = deleteIds
|
||||
updateIdsMap[targetKey] = updateIds
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
insertListMap = make(map[string]A)
|
||||
updateListMap = make(map[string]A)
|
||||
)
|
||||
|
||||
for targetKey, insertIds := range insertIdsMap {
|
||||
if len(insertIds) > 0 {
|
||||
insertList, err := o.Find(o.Ctx, targetKey, insertIds)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
insertListMap[targetKey] = insertList
|
||||
}
|
||||
}
|
||||
|
||||
for targetKey, updateIds := range updateIdsMap {
|
||||
if len(updateIds) > 0 {
|
||||
updateList, err := o.Find(o.Ctx, targetKey, updateIds)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
updateListMap[targetKey] = updateList
|
||||
}
|
||||
}
|
||||
|
||||
return o.Resp(versions, deleteIdsMap, insertListMap, updateListMap, fullMap), nil
|
||||
}
|
||||
153
internal/rpc/incrversion/option.go
Normal file
153
internal/rpc/incrversion/option.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package incrversion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
//func Limit(maxSync int, version uint64) int {
|
||||
// if version == 0 {
|
||||
// return 0
|
||||
// }
|
||||
// return maxSync
|
||||
//}
|
||||
|
||||
const syncLimit = 200
|
||||
|
||||
const (
|
||||
tagQuery = iota + 1
|
||||
tagFull
|
||||
tagEqual
|
||||
)
|
||||
|
||||
type Option[A, B any] struct {
|
||||
Ctx context.Context
|
||||
VersionKey string
|
||||
VersionID string
|
||||
VersionNumber uint64
|
||||
//SyncLimit int
|
||||
CacheMaxVersion func(ctx context.Context, dId string) (*model.VersionLog, error)
|
||||
Version func(ctx context.Context, dId string, version uint, limit int) (*model.VersionLog, error)
|
||||
//SortID func(ctx context.Context, dId string) ([]string, error)
|
||||
Find func(ctx context.Context, ids []string) ([]A, error)
|
||||
Resp func(version *model.VersionLog, deleteIds []string, insertList, updateList []A, full bool) *B
|
||||
}
|
||||
|
||||
func (o *Option[A, B]) newError(msg string) error {
|
||||
return errs.ErrInternalServer.WrapMsg(msg)
|
||||
}
|
||||
|
||||
func (o *Option[A, B]) check() error {
|
||||
if o.Ctx == nil {
|
||||
return o.newError("opt ctx is nil")
|
||||
}
|
||||
if o.VersionKey == "" {
|
||||
return o.newError("versionKey is empty")
|
||||
}
|
||||
//if o.SyncLimit <= 0 {
|
||||
// return o.newError("invalid synchronization quantity")
|
||||
//}
|
||||
if o.Version == nil {
|
||||
return o.newError("func version is nil")
|
||||
}
|
||||
//if o.SortID == nil {
|
||||
// return o.newError("func allID is nil")
|
||||
//}
|
||||
if o.Find == nil {
|
||||
return o.newError("func find is nil")
|
||||
}
|
||||
if o.Resp == nil {
|
||||
return o.newError("func resp is nil")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Option[A, B]) validVersion() bool {
|
||||
objID, err := primitive.ObjectIDFromHex(o.VersionID)
|
||||
return err == nil && (!objID.IsZero()) && o.VersionNumber > 0
|
||||
}
|
||||
|
||||
func (o *Option[A, B]) equalID(objID primitive.ObjectID) bool {
|
||||
return o.VersionID == objID.Hex()
|
||||
}
|
||||
|
||||
func (o *Option[A, B]) getVersion(tag *int) (*model.VersionLog, error) {
|
||||
if o.CacheMaxVersion == nil {
|
||||
if o.validVersion() {
|
||||
*tag = tagQuery
|
||||
return o.Version(o.Ctx, o.VersionKey, uint(o.VersionNumber), syncLimit)
|
||||
}
|
||||
*tag = tagFull
|
||||
return o.Version(o.Ctx, o.VersionKey, 0, 0)
|
||||
} else {
|
||||
cache, err := o.CacheMaxVersion(o.Ctx, o.VersionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !o.validVersion() {
|
||||
*tag = tagFull
|
||||
return cache, nil
|
||||
}
|
||||
if !o.equalID(cache.ID) {
|
||||
*tag = tagFull
|
||||
return cache, nil
|
||||
}
|
||||
if o.VersionNumber == uint64(cache.Version) {
|
||||
*tag = tagEqual
|
||||
return cache, nil
|
||||
}
|
||||
*tag = tagQuery
|
||||
return o.Version(o.Ctx, o.VersionKey, uint(o.VersionNumber), syncLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Option[A, B]) Build() (*B, error) {
|
||||
if err := o.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tag int
|
||||
version, err := o.getVersion(&tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var full bool
|
||||
switch tag {
|
||||
case tagQuery:
|
||||
full = version.ID.Hex() != o.VersionID || uint64(version.Version) < o.VersionNumber || len(version.Logs) != version.LogLen
|
||||
case tagFull:
|
||||
full = true
|
||||
case tagEqual:
|
||||
full = false
|
||||
default:
|
||||
panic(fmt.Errorf("undefined tag %d", tag))
|
||||
}
|
||||
var (
|
||||
insertIds []string
|
||||
deleteIds []string
|
||||
updateIds []string
|
||||
)
|
||||
if !full {
|
||||
insertIds, deleteIds, updateIds = version.DeleteAndChangeIDs()
|
||||
}
|
||||
var (
|
||||
insertList []A
|
||||
updateList []A
|
||||
)
|
||||
if len(insertIds) > 0 {
|
||||
insertList, err = o.Find(o.Ctx, insertIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(updateIds) > 0 {
|
||||
updateList, err = o.Find(o.Ctx, updateIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return o.Resp(version, deleteIds, insertList, updateList, full), nil
|
||||
}
|
||||
231
internal/rpc/msg/as_read.go
Normal file
231
internal/rpc/msg/as_read.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
cbapi "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
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
|
||||
conversationIDs, err = m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
conversationIDs = req.ConversationIDs
|
||||
}
|
||||
|
||||
hasReadSeqs, err := m.MsgDatabase.GetHasReadSeqs(ctx, req.UserID, conversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conversations, err := m.ConversationLocalCache.GetConversations(ctx, req.UserID, conversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conversationMaxSeqMap := make(map[string]int64)
|
||||
for _, conversation := range conversations {
|
||||
if conversation.MaxSeq != 0 {
|
||||
conversationMaxSeqMap[conversation.ConversationID] = conversation.MaxSeq
|
||||
}
|
||||
}
|
||||
maxSeqs, err := m.MsgDatabase.GetMaxSeqsWithTime(ctx, conversationIDs)
|
||||
if err != nil {
|
||||
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],
|
||||
MaxSeq: maxSeq.Seq,
|
||||
MaxSeqTime: maxSeq.Time,
|
||||
}
|
||||
if v, ok := conversationMaxSeqMap[conversationID]; ok {
|
||||
resp.Seqs[conversationID].MaxSeq = v
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if req.HasReadSeq > maxSeq {
|
||||
return nil, errs.ErrArgs.WrapMsg("hasReadSeq must not be bigger than maxSeq")
|
||||
}
|
||||
if err := m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.sendMarkAsReadNotification(ctx, req.ConversationID, constant.SingleChatType, req.UserID, req.UserID, nil, req.HasReadSeq)
|
||||
return &msg.SetConversationHasReadSeqResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) MarkMsgsAsRead(ctx context.Context, req *msg.MarkMsgsAsReadReq) (*msg.MarkMsgsAsReadResp, 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
|
||||
}
|
||||
hasReadSeq := req.Seqs[len(req.Seqs)-1]
|
||||
if hasReadSeq > maxSeq {
|
||||
return nil, errs.ErrArgs.WrapMsg("hasReadSeq must not be bigger than maxSeq")
|
||||
}
|
||||
conversation, err := m.ConversationLocalCache.GetConversation(ctx, req.UserID, req.ConversationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
webhookCfg := m.webhookConfig()
|
||||
if err := m.MsgDatabase.MarkSingleChatMsgsAsRead(ctx, req.UserID, req.ConversationID, req.Seqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentHasReadSeq, err := m.MsgDatabase.GetHasReadSeq(ctx, req.UserID, req.ConversationID)
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
if hasReadSeq > currentHasReadSeq {
|
||||
err = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, hasReadSeq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reqCallback := &cbapi.CallbackSingleMsgReadReq{
|
||||
ConversationID: conversation.ConversationID,
|
||||
UserID: req.UserID,
|
||||
Seqs: req.Seqs,
|
||||
ContentType: conversation.ConversationType,
|
||||
}
|
||||
m.webhookAfterSingleMsgRead(ctx, &webhookCfg.AfterSingleMsgRead, reqCallback)
|
||||
m.sendMarkAsReadNotification(ctx, req.ConversationID, conversation.ConversationType, req.UserID,
|
||||
m.conversationAndGetRecvID(conversation, req.UserID), req.Seqs, hasReadSeq)
|
||||
return &msg.MarkMsgsAsReadResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
webhookCfg := m.webhookConfig()
|
||||
hasReadSeq, err := m.MsgDatabase.GetHasReadSeq(ctx, req.UserID, req.ConversationID)
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
var seqs []int64
|
||||
|
||||
log.ZDebug(ctx, "MarkConversationAsRead", "hasReadSeq", hasReadSeq, "req.HasReadSeq", req.HasReadSeq)
|
||||
if conversation.ConversationType == constant.SingleChatType {
|
||||
for i := hasReadSeq + 1; i <= req.HasReadSeq; i++ {
|
||||
seqs = append(seqs, i)
|
||||
}
|
||||
// avoid client missed call MarkConversationMessageAsRead by order
|
||||
for _, val := range req.Seqs {
|
||||
if !datautil.Contain(val, seqs...) {
|
||||
seqs = append(seqs, val)
|
||||
}
|
||||
}
|
||||
if len(seqs) > 0 {
|
||||
log.ZDebug(ctx, "MarkConversationAsRead", "seqs", seqs, "conversationID", req.ConversationID)
|
||||
if err = m.MsgDatabase.MarkSingleChatMsgsAsRead(ctx, req.UserID, req.ConversationID, seqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if req.HasReadSeq > hasReadSeq {
|
||||
err = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasReadSeq = req.HasReadSeq
|
||||
}
|
||||
m.sendMarkAsReadNotification(ctx, req.ConversationID, conversation.ConversationType, req.UserID,
|
||||
m.conversationAndGetRecvID(conversation, req.UserID), seqs, hasReadSeq)
|
||||
} else if conversation.ConversationType == constant.ReadGroupChatType ||
|
||||
conversation.ConversationType == constant.NotificationChatType {
|
||||
if req.HasReadSeq > hasReadSeq {
|
||||
err = m.MsgDatabase.SetHasReadSeq(ctx, req.UserID, req.ConversationID, req.HasReadSeq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasReadSeq = req.HasReadSeq
|
||||
}
|
||||
m.sendMarkAsReadNotification(ctx, req.ConversationID, constant.SingleChatType, req.UserID,
|
||||
req.UserID, seqs, hasReadSeq)
|
||||
}
|
||||
|
||||
if conversation.ConversationType == constant.SingleChatType {
|
||||
reqCall := &cbapi.CallbackSingleMsgReadReq{
|
||||
ConversationID: conversation.ConversationID,
|
||||
UserID: conversation.OwnerUserID,
|
||||
Seqs: req.Seqs,
|
||||
ContentType: conversation.ConversationType,
|
||||
}
|
||||
m.webhookAfterSingleMsgRead(ctx, &webhookCfg.AfterSingleMsgRead, reqCall)
|
||||
} else if conversation.ConversationType == constant.ReadGroupChatType {
|
||||
reqCall := &cbapi.CallbackGroupMsgReadReq{
|
||||
SendID: conversation.OwnerUserID,
|
||||
ReceiveID: req.UserID,
|
||||
UnreadMsgNum: req.HasReadSeq,
|
||||
ContentType: int64(conversation.ConversationType),
|
||||
}
|
||||
m.webhookAfterGroupMsgRead(ctx, &webhookCfg.AfterGroupMsgRead, reqCall)
|
||||
}
|
||||
return &msg.MarkConversationAsReadResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) sendMarkAsReadNotification(ctx context.Context, conversationID string, sessionType int32, sendID, recvID string, seqs []int64, hasReadSeq int64) {
|
||||
tips := &sdkws.MarkAsReadTips{
|
||||
MarkAsReadUserID: sendID,
|
||||
ConversationID: conversationID,
|
||||
Seqs: seqs,
|
||||
HasReadSeq: hasReadSeq,
|
||||
}
|
||||
m.notificationSender.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips)
|
||||
}
|
||||
236
internal/rpc/msg/callback.go
Normal file
236
internal/rpc/msg/callback.go
Normal file
@@ -0,0 +1,236 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
|
||||
cbapi "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbchat "git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/openimsdk/tools/utils/stringutil"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func toCommonCallback(ctx context.Context, msg *pbchat.SendMsgReq, command string) cbapi.CommonCallbackReq {
|
||||
return cbapi.CommonCallbackReq{
|
||||
SendID: msg.MsgData.SendID,
|
||||
ServerMsgID: msg.MsgData.ServerMsgID,
|
||||
CallbackCommand: command,
|
||||
ClientMsgID: msg.MsgData.ClientMsgID,
|
||||
OperationID: mcontext.GetOperationID(ctx),
|
||||
SenderPlatformID: msg.MsgData.SenderPlatformID,
|
||||
SenderNickname: msg.MsgData.SenderNickname,
|
||||
SessionType: msg.MsgData.SessionType,
|
||||
MsgFrom: msg.MsgData.MsgFrom,
|
||||
ContentType: msg.MsgData.ContentType,
|
||||
Status: msg.MsgData.Status,
|
||||
SendTime: msg.MsgData.SendTime,
|
||||
CreateTime: msg.MsgData.CreateTime,
|
||||
AtUserIDList: msg.MsgData.AtUserIDList,
|
||||
SenderFaceURL: msg.MsgData.SenderFaceURL,
|
||||
Content: GetContent(msg.MsgData),
|
||||
Seq: uint32(msg.MsgData.Seq),
|
||||
Ex: msg.MsgData.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 (m *msgServer) webhookBeforeSendSingleMsg(ctx context.Context, before *config.BeforeConfig, msg *pbchat.SendMsgReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
if msg.MsgData.ContentType == constant.Typing {
|
||||
return nil
|
||||
}
|
||||
if !filterBeforeMsg(msg, before) {
|
||||
return nil
|
||||
}
|
||||
cbReq := &cbapi.CallbackBeforeSendSingleMsgReq{
|
||||
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeSendSingleMsgCommand),
|
||||
RecvID: msg.MsgData.RecvID,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeSendSingleMsgResp{}
|
||||
if err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
if !filterBeforeMsg(msg, before) {
|
||||
return nil
|
||||
}
|
||||
if msg.MsgData.ContentType == constant.Typing {
|
||||
return nil
|
||||
}
|
||||
cbReq := &cbapi.CallbackBeforeSendGroupMsgReq{
|
||||
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeSendGroupMsgCommand),
|
||||
GroupID: msg.MsgData.GroupID,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeSendGroupMsgResp{}
|
||||
if err := m.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *msgServer) webhookAfterSendGroupMsg(ctx context.Context, after *config.AfterConfig, msg *pbchat.SendMsgReq) {
|
||||
if after == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if msg.MsgData.ContentType == constant.Typing {
|
||||
log.ZDebug(ctx, "webhook skipped: typing message", "contentType", msg.MsgData.ContentType)
|
||||
return
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "webhook afterSendGroupMsg checking", "enable", after.Enable, "groupID", msg.MsgData.GroupID, "contentType", msg.MsgData.ContentType, "attentionIds", after.AttentionIds, "deniedTypes", after.DeniedTypes)
|
||||
|
||||
if !filterAfterMsg(msg, after) {
|
||||
log.ZDebug(ctx, "webhook filtered out by filterAfterMsg", "groupID", msg.MsgData.GroupID)
|
||||
return
|
||||
}
|
||||
|
||||
if !after.Enable {
|
||||
log.ZDebug(ctx, "webhook afterSendGroupMsg disabled, skipping", "enable", after.Enable)
|
||||
return
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "webhook afterSendGroupMsg sending", "groupID", msg.MsgData.GroupID, "sendID", msg.MsgData.SendID, "contentType", msg.MsgData.ContentType)
|
||||
|
||||
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 {
|
||||
//if msg.MsgData.ContentType != constant.Text {
|
||||
// return nil
|
||||
//}
|
||||
if !filterBeforeMsg(msg, before) {
|
||||
return nil
|
||||
}
|
||||
cbReq := &cbapi.CallbackMsgModifyCommandReq{
|
||||
CommonCallbackReq: toCommonCallback(ctx, msg, cbapi.CallbackBeforeMsgModifyCommand),
|
||||
}
|
||||
resp := &cbapi.CallbackMsgModifyCommandResp{}
|
||||
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)
|
||||
datautil.NotNilReplace(&msg.MsgData.GroupID, resp.GroupID)
|
||||
datautil.NotNilReplace(&msg.MsgData.ClientMsgID, resp.ClientMsgID)
|
||||
datautil.NotNilReplace(&msg.MsgData.ServerMsgID, resp.ServerMsgID)
|
||||
datautil.NotNilReplace(&msg.MsgData.SenderPlatformID, resp.SenderPlatformID)
|
||||
datautil.NotNilReplace(&msg.MsgData.SenderNickname, resp.SenderNickname)
|
||||
datautil.NotNilReplace(&msg.MsgData.SenderFaceURL, resp.SenderFaceURL)
|
||||
datautil.NotNilReplace(&msg.MsgData.SessionType, resp.SessionType)
|
||||
datautil.NotNilReplace(&msg.MsgData.MsgFrom, resp.MsgFrom)
|
||||
datautil.NotNilReplace(&msg.MsgData.ContentType, resp.ContentType)
|
||||
datautil.NotNilReplace(&msg.MsgData.Status, resp.Status)
|
||||
datautil.NotNilReplace(&msg.MsgData.Options, resp.Options)
|
||||
datautil.NotNilReplace(&msg.MsgData.AtUserIDList, resp.AtUserIDList)
|
||||
datautil.NotNilReplace(&msg.MsgData.AttachedInfo, resp.AttachedInfo)
|
||||
datautil.NotNilReplace(&msg.MsgData.Ex, resp.Ex)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *msgServer) webhookAfterGroupMsgRead(ctx context.Context, after *config.AfterConfig, req *cbapi.CallbackGroupMsgReadReq) {
|
||||
req.CallbackCommand = cbapi.CallbackAfterGroupMsgReadCommand
|
||||
m.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CallbackGroupMsgReadResp{}, after)
|
||||
}
|
||||
|
||||
func (m *msgServer) webhookAfterSingleMsgRead(ctx context.Context, after *config.AfterConfig, req *cbapi.CallbackSingleMsgReadReq) {
|
||||
|
||||
req.CallbackCommand = cbapi.CallbackAfterSingleMsgReadCommand
|
||||
|
||||
m.webhookClient.AsyncPost(ctx, req.GetCallbackCommand(), req, &cbapi.CallbackSingleMsgReadResp{}, after)
|
||||
|
||||
}
|
||||
|
||||
func (m *msgServer) webhookAfterRevokeMsg(ctx context.Context, after *config.AfterConfig, req *pbchat.RevokeMsgReq) {
|
||||
callbackReq := &cbapi.CallbackAfterRevokeMsgReq{
|
||||
CallbackCommand: cbapi.CallbackAfterRevokeMsgCommand,
|
||||
ConversationID: req.ConversationID,
|
||||
Seq: req.Seq,
|
||||
UserID: req.UserID,
|
||||
}
|
||||
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)),
|
||||
}
|
||||
}
|
||||
61
internal/rpc/msg/clear.go
Normal file
61
internal/rpc/msg/clear.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
// DestructMsgs hard delete in Database.
|
||||
func (m *msgServer) DestructMsgs(ctx context.Context, req *msg.DestructMsgsReq) (*msg.DestructMsgsResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs, err := m.MsgDatabase.GetRandBeforeMsg(ctx, req.Timestamp, int(req.Limit))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, doc := range docs {
|
||||
if err := m.MsgDatabase.DeleteDoc(ctx, doc.DocID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.ZDebug(ctx, "DestructMsgs delete doc", "index", i, "docID", doc.DocID)
|
||||
index := strings.LastIndex(doc.DocID, ":")
|
||||
if index < 0 {
|
||||
continue
|
||||
}
|
||||
var minSeq int64
|
||||
for _, model := range doc.Msg {
|
||||
if model.Msg == nil {
|
||||
continue
|
||||
}
|
||||
if model.Msg.Seq > minSeq {
|
||||
minSeq = model.Msg.Seq
|
||||
}
|
||||
}
|
||||
if minSeq <= 0 {
|
||||
continue
|
||||
}
|
||||
conversationID := doc.DocID[:index]
|
||||
if conversationID == "" {
|
||||
continue
|
||||
}
|
||||
minSeq++
|
||||
if err := m.MsgDatabase.SetMinSeq(ctx, conversationID, minSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.ZDebug(ctx, "DestructMsgs delete doc set min seq", "index", i, "docID", doc.DocID, "conversationID", conversationID, "setMinSeq", minSeq)
|
||||
}
|
||||
return &msg.DestructMsgsResp{Count: int32(len(docs))}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetLastMessageSeqByTime(ctx context.Context, req *msg.GetLastMessageSeqByTimeReq) (*msg.GetLastMessageSeqByTimeResp, error) {
|
||||
seq, err := m.MsgDatabase.GetLastMessageSeqByTime(ctx, req.ConversationID, req.Time)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &msg.GetLastMessageSeqByTimeResp{Seq: seq}, nil
|
||||
}
|
||||
251
internal/rpc/msg/delete.go
Normal file
251
internal/rpc/msg/delete.go
Normal file
@@ -0,0 +1,251 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/conversation"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/openimsdk/tools/utils/timeutil"
|
||||
)
|
||||
|
||||
func (m *msgServer) getMinSeqs(maxSeqs map[string]int64) map[string]int64 {
|
||||
minSeqs := make(map[string]int64)
|
||||
for k, v := range maxSeqs {
|
||||
minSeqs[k] = v + 1
|
||||
}
|
||||
return minSeqs
|
||||
}
|
||||
|
||||
func (m *msgServer) validateDeleteSyncOpt(opt *msg.DeleteSyncOpt) (isSyncSelf, isSyncOther bool) {
|
||||
if opt == nil {
|
||||
return
|
||||
}
|
||||
return opt.IsSyncSelf, opt.IsSyncOther
|
||||
}
|
||||
|
||||
func (m *msgServer) ClearConversationsMsg(ctx context.Context, req *msg.ClearConversationsMsgReq) (*msg.ClearConversationsMsgResp, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return &msg.ClearConversationsMsgResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) UserClearAllMsg(ctx context.Context, req *msg.UserClearAllMsgReq) (*msg.UserClearAllMsgResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.clearConversation(ctx, conversationIDs, req.UserID, req.DeleteSyncOpt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &msg.UserClearAllMsgResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) DeleteMsgs(ctx context.Context, req *msg.DeleteMsgsReq) (*msg.DeleteMsgsResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取要删除的消息信息,用于权限检查
|
||||
_, _, msgs, err := m.MsgDatabase.GetMsgBySeqs(ctx, req.UserID, req.ConversationID, req.Seqs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("messages not found")
|
||||
}
|
||||
|
||||
// 权限检查:如果不是管理员,需要检查删除权限
|
||||
if !authverify.IsAdmin(ctx) {
|
||||
// 收集所有消息的发送者ID
|
||||
sendIDs := make([]string, 0, len(msgs))
|
||||
for _, msg := range msgs {
|
||||
if msg != nil && msg.SendID != "" {
|
||||
sendIDs = append(sendIDs, msg.SendID)
|
||||
}
|
||||
}
|
||||
sendIDs = datautil.Distinct(sendIDs)
|
||||
|
||||
// 检查第一条消息的会话类型(假设所有消息来自同一会话)
|
||||
sessionType := msgs[0].SessionType
|
||||
switch sessionType {
|
||||
case constant.SingleChatType:
|
||||
// 单聊:只能删除自己发送的消息
|
||||
for _, msg := range msgs {
|
||||
if msg != nil && msg.SendID != req.UserID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("can only delete own messages in single chat")
|
||||
}
|
||||
}
|
||||
case constant.ReadGroupChatType:
|
||||
// 群聊:检查权限
|
||||
groupID := msgs[0].GroupID
|
||||
if groupID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("groupID is empty")
|
||||
}
|
||||
|
||||
// 获取操作者和所有消息发送者的群成员信息
|
||||
allUserIDs := append([]string{req.UserID}, sendIDs...)
|
||||
members, err := m.GroupLocalCache.GetGroupMemberInfoMap(ctx, groupID, datautil.Distinct(allUserIDs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查操作者的角色
|
||||
opMember, ok := members[req.UserID]
|
||||
if !ok {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("user not in group")
|
||||
}
|
||||
|
||||
// 检查每条消息的删除权限
|
||||
for _, msg := range msgs {
|
||||
if msg == nil || msg.SendID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果是自己发送的消息,可以删除
|
||||
if msg.SendID == req.UserID {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果不是自己发送的消息,需要检查权限
|
||||
switch opMember.RoleLevel {
|
||||
case constant.GroupOwner:
|
||||
// 群主可以删除任何人的消息
|
||||
case constant.GroupAdmin:
|
||||
// 管理员只能删除普通成员的消息
|
||||
sendMember, ok := members[msg.SendID]
|
||||
if !ok {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("message sender not in group")
|
||||
}
|
||||
if sendMember.RoleLevel != constant.GroupOrdinaryUsers {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("group admin can only delete messages from ordinary members")
|
||||
}
|
||||
default:
|
||||
// 普通成员只能删除自己的消息
|
||||
return nil, errs.ErrNoPermission.WrapMsg("can only delete own messages")
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, errs.ErrInternalServer.WrapMsg("sessionType not supported", "sessionType", sessionType)
|
||||
}
|
||||
}
|
||||
|
||||
isSyncSelf, isSyncOther := m.validateDeleteSyncOpt(req.DeleteSyncOpt)
|
||||
if isSyncOther {
|
||||
if err := m.MsgDatabase.DeleteMsgsPhysicalBySeqs(ctx, req.ConversationID, req.Seqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conv, err := m.conversationClient.GetConversationsByConversationID(ctx, req.ConversationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tips := &sdkws.DeleteMsgsTips{UserID: req.UserID, ConversationID: req.ConversationID, Seqs: req.Seqs}
|
||||
m.notificationSender.NotificationWithSessionType(ctx, req.UserID, m.conversationAndGetRecvID(conv, req.UserID),
|
||||
constant.DeleteMsgsNotification, conv.ConversationType, tips)
|
||||
} else {
|
||||
if err := m.MsgDatabase.DeleteUserMsgsBySeqs(ctx, req.UserID, req.ConversationID, req.Seqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isSyncSelf {
|
||||
tips := &sdkws.DeleteMsgsTips{UserID: req.UserID, ConversationID: req.ConversationID, Seqs: req.Seqs}
|
||||
m.notificationSender.NotificationWithSessionType(ctx, req.UserID, req.UserID, constant.DeleteMsgsNotification, constant.SingleChatType, tips)
|
||||
}
|
||||
}
|
||||
return &msg.DeleteMsgsResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &msg.DeleteMsgPhysicalBySeqResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) DeleteMsgPhysical(ctx context.Context, req *msg.DeleteMsgPhysicalReq) (*msg.DeleteMsgPhysicalResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remainTime := timeutil.GetCurrentTimestampBySecond() - req.Timestamp
|
||||
if _, err := m.DestructMsgs(ctx, &msg.DestructMsgsReq{Timestamp: remainTime, Limit: 9999}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &msg.DeleteMsgPhysicalResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) clearConversation(ctx context.Context, conversationIDs []string, userID string, deleteSyncOpt *msg.DeleteSyncOpt) error {
|
||||
conversations, err := m.conversationClient.GetConversationsByConversationIDs(ctx, conversationIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var existConversations []*conversation.Conversation
|
||||
var existConversationIDs []string
|
||||
for _, conversation := range conversations {
|
||||
existConversations = append(existConversations, conversation)
|
||||
existConversationIDs = append(existConversationIDs, conversation.ConversationID)
|
||||
}
|
||||
log.ZDebug(ctx, "ClearConversationsMsg", "existConversationIDs", existConversationIDs)
|
||||
maxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, existConversationIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isSyncSelf, isSyncOther := m.validateDeleteSyncOpt(deleteSyncOpt)
|
||||
if !isSyncOther {
|
||||
setSeqs := m.getMinSeqs(maxSeqs)
|
||||
if err := m.MsgDatabase.SetUserConversationsMinSeqs(ctx, userID, setSeqs); err != nil {
|
||||
return err
|
||||
}
|
||||
ownerUserIDs := []string{userID}
|
||||
for conversationID, seq := range setSeqs {
|
||||
if err := m.conversationClient.SetConversationMinSeq(ctx, conversationID, ownerUserIDs, seq); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// notification 2 self
|
||||
if isSyncSelf {
|
||||
tips := &sdkws.ClearConversationTips{UserID: userID, ConversationIDs: existConversationIDs}
|
||||
m.notificationSender.NotificationWithSessionType(ctx, userID, userID, constant.ClearConversationNotification, constant.SingleChatType, tips)
|
||||
}
|
||||
} else {
|
||||
if err := m.MsgDatabase.SetMinSeqs(ctx, m.getMinSeqs(maxSeqs)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, conversation := range existConversations {
|
||||
tips := &sdkws.ClearConversationTips{UserID: userID, ConversationIDs: []string{conversation.ConversationID}}
|
||||
m.notificationSender.NotificationWithSessionType(ctx, userID, m.conversationAndGetRecvID(conversation, userID), constant.ClearConversationNotification, conversation.ConversationType, tips)
|
||||
}
|
||||
}
|
||||
if err := m.MsgDatabase.UserSetHasReadSeqs(ctx, userID, maxSeqs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
106
internal/rpc/msg/filter.go
Normal file
106
internal/rpc/msg/filter.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbchat "git.imall.cloud/openim/protocol/msg"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
const (
|
||||
separator = "-"
|
||||
)
|
||||
|
||||
func filterAfterMsg(msg *pbchat.SendMsgReq, after *config.AfterConfig) bool {
|
||||
result := filterMsg(msg, after.AttentionIds, after.DeniedTypes)
|
||||
// 添加调试日志
|
||||
if !result {
|
||||
// 只在过滤掉时记录,避免日志过多
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func filterBeforeMsg(msg *pbchat.SendMsgReq, before *config.BeforeConfig) bool {
|
||||
return filterMsg(msg, nil, before.DeniedTypes)
|
||||
}
|
||||
|
||||
func filterMsg(msg *pbchat.SendMsgReq, attentionIds []string, deniedTypes []int32) bool {
|
||||
// According to the attentionIds configuration, only some users are sent
|
||||
// 注意:对于群消息,应该检查GroupID而不是RecvID
|
||||
if len(attentionIds) != 0 {
|
||||
// 单聊消息检查RecvID,群聊消息检查GroupID
|
||||
if msg.MsgData.SessionType == constant.SingleChatType {
|
||||
if !datautil.Contain(msg.MsgData.RecvID, attentionIds...) {
|
||||
return false
|
||||
}
|
||||
} else if msg.MsgData.SessionType == constant.ReadGroupChatType {
|
||||
if !datautil.Contain(msg.MsgData.GroupID, attentionIds...) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if defaultDeniedTypes(msg.MsgData.ContentType) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(deniedTypes) != 0 && datautil.Contain(msg.MsgData.ContentType, deniedTypes...) {
|
||||
return false
|
||||
}
|
||||
//if len(allowedTypes) != 0 && !isInInterval(msg.MsgData.ContentType, allowedTypes) {
|
||||
// return false
|
||||
//}
|
||||
//if len(deniedTypes) != 0 && isInInterval(msg.MsgData.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
|
||||
}
|
||||
|
||||
// isInInterval if data is in interval
|
||||
// Supports two formats: a single type or a range. The range is defined by the lower and upper bounds connected with a hyphen ("-")
|
||||
// e.g. [1, 100, 200-500, 600-700] means that only data within the range
|
||||
// {1, 100} ∪ [200, 500] ∪ [600, 700] will return true.
|
||||
func isInInterval(data int32, interval []string) bool {
|
||||
for _, v := range interval {
|
||||
if strings.Contains(v, separator) {
|
||||
// is interval
|
||||
bounds := strings.Split(v, separator)
|
||||
if len(bounds) != 2 {
|
||||
continue
|
||||
}
|
||||
bottom, err := strconv.Atoi(bounds[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
top, err := strconv.Atoi(bounds[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if datautil.BetweenEq(int(data), bottom, top) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
iv, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if int(data) == iv {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
44
internal/rpc/msg/msg_status.go
Normal file
44
internal/rpc/msg/msg_status.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbmsg "git.imall.cloud/openim/protocol/msg"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
func (m *msgServer) SetSendMsgStatus(ctx context.Context, req *pbmsg.SetSendMsgStatusReq) (*pbmsg.SetSendMsgStatusResp, error) {
|
||||
resp := &pbmsg.SetSendMsgStatusResp{}
|
||||
if err := m.MsgDatabase.SetSendMsgStatus(ctx, mcontext.GetOperationID(ctx), req.Status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetSendMsgStatus(ctx context.Context, req *pbmsg.GetSendMsgStatusReq) (*pbmsg.GetSendMsgStatusResp, error) {
|
||||
resp := &pbmsg.GetSendMsgStatusResp{}
|
||||
status, err := m.MsgDatabase.GetSendMsgStatus(ctx, mcontext.GetOperationID(ctx))
|
||||
if IsNotFound(err) {
|
||||
resp.Status = constant.MsgStatusNotExist
|
||||
return resp, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Status = status
|
||||
return resp, nil
|
||||
}
|
||||
50
internal/rpc/msg/notification.go
Normal file
50
internal/rpc/msg/notification.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
)
|
||||
|
||||
type MsgNotificationSender struct {
|
||||
*notification.NotificationSender
|
||||
}
|
||||
|
||||
func NewMsgNotificationSender(config *Config, opts ...notification.NotificationSenderOptions) *MsgNotificationSender {
|
||||
return &MsgNotificationSender{notification.NewNotificationSender(&config.NotificationConfig, opts...)}
|
||||
}
|
||||
|
||||
func (m *MsgNotificationSender) UserDeleteMsgsNotification(ctx context.Context, userID, conversationID string, seqs []int64) {
|
||||
tips := sdkws.DeleteMsgsTips{
|
||||
UserID: userID,
|
||||
ConversationID: conversationID,
|
||||
Seqs: seqs,
|
||||
}
|
||||
m.Notification(ctx, userID, userID, constant.DeleteMsgsNotification, &tips)
|
||||
}
|
||||
|
||||
func (m *MsgNotificationSender) MarkAsReadNotification(ctx context.Context, conversationID string, sessionType int32, sendID, recvID string, seqs []int64, hasReadSeq int64) {
|
||||
tips := &sdkws.MarkAsReadTips{
|
||||
MarkAsReadUserID: sendID,
|
||||
ConversationID: conversationID,
|
||||
Seqs: seqs,
|
||||
HasReadSeq: hasReadSeq,
|
||||
}
|
||||
m.NotificationWithSessionType(ctx, sendID, recvID, constant.HasReadReceipt, sessionType, tips)
|
||||
}
|
||||
971
internal/rpc/msg/qrcode_decoder.go
Normal file
971
internal/rpc/msg/qrcode_decoder.go
Normal file
@@ -0,0 +1,971 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"math"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/makiuchi-d/gozxing"
|
||||
"github.com/makiuchi-d/gozxing/qrcode"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
// openImageFile 打开图片文件
|
||||
func openImageFile(imagePath string) (*os.File, error) {
|
||||
file, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法打开文件: %v", err)
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// QRDecoder 二维码解码器接口
|
||||
type QRDecoder interface {
|
||||
Name() string
|
||||
Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) // 返回是否检测到二维码
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QuircDecoder - Quirc解码器包装
|
||||
// ============================================================================
|
||||
|
||||
// QuircDecoder 使用 Quirc 库的解码器
|
||||
type QuircDecoder struct {
|
||||
detectFunc func([]uint8, int, int) (bool, error)
|
||||
}
|
||||
|
||||
// NewQuircDecoder 创建Quirc解码器
|
||||
func NewQuircDecoder(detectFunc func([]uint8, int, int) (bool, error)) *QuircDecoder {
|
||||
return &QuircDecoder{detectFunc: detectFunc}
|
||||
}
|
||||
|
||||
func (d *QuircDecoder) Name() string {
|
||||
return "quirc"
|
||||
}
|
||||
|
||||
func (d *QuircDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
|
||||
if d.detectFunc == nil {
|
||||
return false, fmt.Errorf("quirc 解码器未启用")
|
||||
}
|
||||
|
||||
// 打开并解码图片
|
||||
file, err := openImageFile(imagePath)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "打开图片文件失败", err, "imagePath", imagePath)
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "解码图片失败", err, "imagePath", imagePath)
|
||||
return false, fmt.Errorf("无法解码图片: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
|
||||
// 转换为灰度图
|
||||
grayData := convertToGrayscale(img, width, height)
|
||||
|
||||
// 调用Quirc检测
|
||||
hasQRCode, err := d.detectFunc(grayData, width, height)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "Quirc检测失败", err, "width", width, "height", height)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return hasQRCode, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CustomQRDecoder - 自定义解码器(兼容圆形角)
|
||||
// ============================================================================
|
||||
|
||||
// CustomQRDecoder 自定义二维码解码器,兼容圆形角等特殊格式
|
||||
type CustomQRDecoder struct{}
|
||||
|
||||
func (d *CustomQRDecoder) Name() string {
|
||||
return "custom (圆形角兼容)"
|
||||
}
|
||||
|
||||
// Decode 解码二维码,返回是否检测到二维码
|
||||
func (d *CustomQRDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
|
||||
file, err := openImageFile(imagePath)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "打开图片文件失败", err, "imagePath", imagePath)
|
||||
return false, fmt.Errorf("无法打开文件: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "解码图片失败", err, "imagePath", imagePath)
|
||||
return false, fmt.Errorf("无法解码图片: %v", err)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
|
||||
reader := qrcode.NewQRCodeReader()
|
||||
hints := make(map[gozxing.DecodeHintType]interface{})
|
||||
hints[gozxing.DecodeHintType_TRY_HARDER] = true
|
||||
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
|
||||
hints[gozxing.DecodeHintType_CHARACTER_SET] = "UTF-8"
|
||||
|
||||
// 尝试直接解码
|
||||
bitmap, err := gozxing.NewBinaryBitmapFromImage(img)
|
||||
if err == nil {
|
||||
if _, err := reader.Decode(bitmap, hints); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 尝试不使用PURE_BARCODE
|
||||
delete(hints, gozxing.DecodeHintType_PURE_BARCODE)
|
||||
if _, err := reader.Decode(bitmap, hints); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
|
||||
}
|
||||
|
||||
// 尝试多尺度缩放
|
||||
scales := []float64{1.0, 1.5, 2.0, 0.75, 0.5}
|
||||
for _, scale := range scales {
|
||||
scaledImg := scaleImage(img, width, height, scale)
|
||||
if scaledImg == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
scaledBitmap, err := gozxing.NewBinaryBitmapFromImage(scaledImg)
|
||||
if err == nil {
|
||||
if _, err := reader.Decode(scaledBitmap, hints); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为灰度图进行预处理
|
||||
grayData := convertToGrayscale(img, width, height)
|
||||
|
||||
// 尝试多种预处理方法
|
||||
preprocessMethods := []struct {
|
||||
name string
|
||||
fn func([]byte, int, int) []byte
|
||||
}{
|
||||
{"Otsu二值化", enhanceImageOtsu},
|
||||
{"标准增强", enhanceImage},
|
||||
{"强对比度", enhanceImageStrong},
|
||||
{"圆形角处理", enhanceImageForRoundedCorners},
|
||||
{"去噪+锐化", enhanceImageDenoiseSharpen},
|
||||
{"高斯模糊+锐化", enhanceImageGaussianSharpen},
|
||||
}
|
||||
|
||||
scalesForPreprocessed := []float64{1.0, 2.0, 1.5, 1.2, 0.8}
|
||||
|
||||
for _, method := range preprocessMethods {
|
||||
processed := method.fn(grayData, width, height)
|
||||
|
||||
// 快速检测定位图案
|
||||
corners := detectCornersFast(processed, width, height)
|
||||
if len(corners) < 2 {
|
||||
// 如果没有检测到足够的定位图案,仍然尝试解码
|
||||
}
|
||||
|
||||
processedImg := createImageFromGrayscale(processed, width, height)
|
||||
bitmap2, err := gozxing.NewBinaryBitmapFromImage(processedImg)
|
||||
if err == nil {
|
||||
if _, err := reader.Decode(bitmap2, hints); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
delete(hints, gozxing.DecodeHintType_PURE_BARCODE)
|
||||
if _, err := reader.Decode(bitmap2, hints); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
|
||||
}
|
||||
|
||||
// 对预处理后的图像进行多尺度缩放
|
||||
for _, scale := range scalesForPreprocessed {
|
||||
scaledProcessed := scaleGrayscaleImage(processed, width, height, scale)
|
||||
if scaledProcessed == nil {
|
||||
continue
|
||||
}
|
||||
scaledImg := createImageFromGrayscale(scaledProcessed.data, scaledProcessed.width, scaledProcessed.height)
|
||||
scaledBitmap, err := gozxing.NewBinaryBitmapFromImage(scaledImg)
|
||||
if err == nil {
|
||||
if _, err := reader.Decode(scaledBitmap, hints); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
delete(hints, gozxing.DecodeHintType_PURE_BARCODE)
|
||||
if _, err := reader.Decode(scaledBitmap, hints); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
hints[gozxing.DecodeHintType_PURE_BARCODE] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ParallelQRDecoder - 并行解码器
|
||||
// ============================================================================
|
||||
|
||||
// ParallelQRDecoder 并行解码器,同时运行 quirc 和 custom 解码器
|
||||
type ParallelQRDecoder struct {
|
||||
quircDecoder QRDecoder
|
||||
customDecoder QRDecoder
|
||||
}
|
||||
|
||||
// NewParallelQRDecoder 创建并行解码器
|
||||
func NewParallelQRDecoder(quircDecoder, customDecoder QRDecoder) *ParallelQRDecoder {
|
||||
return &ParallelQRDecoder{
|
||||
quircDecoder: quircDecoder,
|
||||
customDecoder: customDecoder,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ParallelQRDecoder) Name() string {
|
||||
return "parallel (quirc + custom)"
|
||||
}
|
||||
|
||||
// Decode 并行解码:同时运行 quirc 和 custom,任一成功立即返回
|
||||
func (d *ParallelQRDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
|
||||
type decodeResult struct {
|
||||
hasQRCode bool
|
||||
err error
|
||||
name string
|
||||
}
|
||||
|
||||
resultChan := make(chan decodeResult, 2)
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
var quircErr error
|
||||
var customErr error
|
||||
|
||||
// 启动Quirc解码
|
||||
if d.quircDecoder != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
hasQRCode, err := d.quircDecoder.Decode(ctx, imagePath, logPrefix)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
quircErr = err
|
||||
}
|
||||
mu.Unlock()
|
||||
resultChan <- decodeResult{
|
||||
hasQRCode: hasQRCode,
|
||||
err: err,
|
||||
name: d.quircDecoder.Name(),
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 启动Custom解码
|
||||
if d.customDecoder != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
hasQRCode, err := d.customDecoder.Decode(ctx, imagePath, logPrefix)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
customErr = err
|
||||
}
|
||||
mu.Unlock()
|
||||
resultChan <- decodeResult{
|
||||
hasQRCode: hasQRCode,
|
||||
err: err,
|
||||
name: d.customDecoder.Name(),
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 等待第一个结果
|
||||
var firstResult decodeResult
|
||||
var secondResult decodeResult
|
||||
|
||||
firstResult = <-resultChan
|
||||
if firstResult.hasQRCode {
|
||||
// 如果检测到二维码,立即返回
|
||||
go func() {
|
||||
<-resultChan
|
||||
wg.Wait()
|
||||
}()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 等待第二个结果
|
||||
if d.quircDecoder != nil && d.customDecoder != nil {
|
||||
secondResult = <-resultChan
|
||||
if secondResult.hasQRCode {
|
||||
wg.Wait()
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 如果都失败,返回错误
|
||||
if firstResult.err != nil && secondResult.err != nil {
|
||||
log.ZError(ctx, "并行解码失败,两个解码器都失败", fmt.Errorf("quirc错误=%v, custom错误=%v", quircErr, customErr),
|
||||
"quircError", quircErr,
|
||||
"customError", customErr)
|
||||
return false, fmt.Errorf("quirc 和 custom 都解码失败: quirc错误=%v, custom错误=%v", quircErr, customErr)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助函数
|
||||
// ============================================================================
|
||||
|
||||
// convertToGrayscale 转换为灰度图
|
||||
func convertToGrayscale(img image.Image, width, height int) []byte {
|
||||
grayData := make([]byte, width*height)
|
||||
bounds := img.Bounds()
|
||||
|
||||
if ycbcr, ok := img.(*image.YCbCr); ok {
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
yi := ycbcr.YOffset(x+bounds.Min.X, y+bounds.Min.Y)
|
||||
grayData[y*width+x] = ycbcr.Y[yi]
|
||||
}
|
||||
}
|
||||
return grayData
|
||||
}
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
r, g, b, _ := img.At(x+bounds.Min.X, y+bounds.Min.Y).RGBA()
|
||||
r8 := uint8(r >> 8)
|
||||
g8 := uint8(g >> 8)
|
||||
b8 := uint8(b >> 8)
|
||||
gray := byte((uint16(r8)*299 + uint16(g8)*587 + uint16(b8)*114) / 1000)
|
||||
grayData[y*width+x] = gray
|
||||
}
|
||||
}
|
||||
|
||||
return grayData
|
||||
}
|
||||
|
||||
// enhanceImage 图像增强(标准方法)
|
||||
func enhanceImage(data []byte, width, height int) []byte {
|
||||
enhanced := make([]byte, len(data))
|
||||
copy(enhanced, data)
|
||||
|
||||
minVal := uint8(255)
|
||||
maxVal := uint8(0)
|
||||
for _, v := range data {
|
||||
if v < minVal {
|
||||
minVal = v
|
||||
}
|
||||
if v > maxVal {
|
||||
maxVal = v
|
||||
}
|
||||
}
|
||||
|
||||
if maxVal-minVal < 50 {
|
||||
rangeVal := maxVal - minVal
|
||||
if rangeVal == 0 {
|
||||
rangeVal = 1
|
||||
}
|
||||
for i, v := range data {
|
||||
stretched := uint8((uint16(v-minVal) * 255) / uint16(rangeVal))
|
||||
enhanced[i] = stretched
|
||||
}
|
||||
}
|
||||
|
||||
return enhanced
|
||||
}
|
||||
|
||||
// enhanceImageStrong 强对比度增强
|
||||
func enhanceImageStrong(data []byte, width, height int) []byte {
|
||||
enhanced := make([]byte, len(data))
|
||||
|
||||
histogram := make([]int, 256)
|
||||
for _, v := range data {
|
||||
histogram[v]++
|
||||
}
|
||||
|
||||
cdf := make([]int, 256)
|
||||
cdf[0] = histogram[0]
|
||||
for i := 1; i < 256; i++ {
|
||||
cdf[i] = cdf[i-1] + histogram[i]
|
||||
}
|
||||
|
||||
total := len(data)
|
||||
for i, v := range data {
|
||||
if total > 0 {
|
||||
enhanced[i] = uint8((cdf[v] * 255) / total)
|
||||
}
|
||||
}
|
||||
|
||||
return enhanced
|
||||
}
|
||||
|
||||
// enhanceImageForRoundedCorners 针对圆形角的特殊处理
|
||||
func enhanceImageForRoundedCorners(data []byte, width, height int) []byte {
|
||||
enhanced := make([]byte, len(data))
|
||||
copy(enhanced, data)
|
||||
|
||||
minVal := uint8(255)
|
||||
maxVal := uint8(0)
|
||||
for _, v := range data {
|
||||
if v < minVal {
|
||||
minVal = v
|
||||
}
|
||||
if v > maxVal {
|
||||
maxVal = v
|
||||
}
|
||||
}
|
||||
|
||||
if maxVal-minVal < 100 {
|
||||
rangeVal := maxVal - minVal
|
||||
if rangeVal == 0 {
|
||||
rangeVal = 1
|
||||
}
|
||||
for i, v := range data {
|
||||
stretched := uint8((uint16(v-minVal) * 255) / uint16(rangeVal))
|
||||
enhanced[i] = stretched
|
||||
}
|
||||
}
|
||||
|
||||
// 形态学操作:先腐蚀后膨胀
|
||||
dilated := make([]byte, len(enhanced))
|
||||
kernelSize := 3
|
||||
halfKernel := kernelSize / 2
|
||||
|
||||
// 腐蚀(最小值滤波)
|
||||
for y := halfKernel; y < height-halfKernel; y++ {
|
||||
for x := halfKernel; x < width-halfKernel; x++ {
|
||||
minVal := uint8(255)
|
||||
for ky := -halfKernel; ky <= halfKernel; ky++ {
|
||||
for kx := -halfKernel; kx <= halfKernel; kx++ {
|
||||
idx := (y+ky)*width + (x + kx)
|
||||
if enhanced[idx] < minVal {
|
||||
minVal = enhanced[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
dilated[y*width+x] = minVal
|
||||
}
|
||||
}
|
||||
|
||||
// 膨胀(最大值滤波)
|
||||
for y := halfKernel; y < height-halfKernel; y++ {
|
||||
for x := halfKernel; x < width-halfKernel; x++ {
|
||||
maxVal := uint8(0)
|
||||
for ky := -halfKernel; ky <= halfKernel; ky++ {
|
||||
for kx := -halfKernel; kx <= halfKernel; kx++ {
|
||||
idx := (y+ky)*width + (x + kx)
|
||||
if dilated[idx] > maxVal {
|
||||
maxVal = dilated[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
enhanced[y*width+x] = maxVal
|
||||
}
|
||||
}
|
||||
|
||||
// 边界保持原值
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel {
|
||||
enhanced[y*width+x] = data[y*width+x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enhanced
|
||||
}
|
||||
|
||||
// enhanceImageDenoiseSharpen 去噪+锐化处理
|
||||
func enhanceImageDenoiseSharpen(data []byte, width, height int) []byte {
|
||||
denoised := medianFilter(data, width, height, 3)
|
||||
sharpened := sharpenImage(denoised, width, height)
|
||||
return sharpened
|
||||
}
|
||||
|
||||
// medianFilter 中值滤波去噪
|
||||
func medianFilter(data []byte, width, height, kernelSize int) []byte {
|
||||
filtered := make([]byte, len(data))
|
||||
halfKernel := kernelSize / 2
|
||||
kernelArea := kernelSize * kernelSize
|
||||
values := make([]byte, kernelArea)
|
||||
|
||||
for y := halfKernel; y < height-halfKernel; y++ {
|
||||
for x := halfKernel; x < width-halfKernel; x++ {
|
||||
idx := 0
|
||||
for ky := -halfKernel; ky <= halfKernel; ky++ {
|
||||
for kx := -halfKernel; kx <= halfKernel; kx++ {
|
||||
values[idx] = data[(y+ky)*width+(x+kx)]
|
||||
idx++
|
||||
}
|
||||
}
|
||||
filtered[y*width+x] = quickSelectMedian(values)
|
||||
}
|
||||
}
|
||||
|
||||
// 边界保持原值
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel {
|
||||
filtered[y*width+x] = data[y*width+x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// quickSelectMedian 快速选择中值
|
||||
func quickSelectMedian(arr []byte) byte {
|
||||
n := len(arr)
|
||||
if n <= 7 {
|
||||
// 小数组使用插入排序
|
||||
for i := 1; i < n; i++ {
|
||||
key := arr[i]
|
||||
j := i - 1
|
||||
for j >= 0 && arr[j] > key {
|
||||
arr[j+1] = arr[j]
|
||||
j--
|
||||
}
|
||||
arr[j+1] = key
|
||||
}
|
||||
return arr[n/2]
|
||||
}
|
||||
return quickSelect(arr, 0, n-1, n/2)
|
||||
}
|
||||
|
||||
// quickSelect 快速选择第k小的元素
|
||||
func quickSelect(arr []byte, left, right, k int) byte {
|
||||
if left == right {
|
||||
return arr[left]
|
||||
}
|
||||
pivotIndex := partition(arr, left, right)
|
||||
if k == pivotIndex {
|
||||
return arr[k]
|
||||
} else if k < pivotIndex {
|
||||
return quickSelect(arr, left, pivotIndex-1, k)
|
||||
}
|
||||
return quickSelect(arr, pivotIndex+1, right, k)
|
||||
}
|
||||
|
||||
func partition(arr []byte, left, right int) int {
|
||||
pivot := arr[right]
|
||||
i := left
|
||||
for j := left; j < right; j++ {
|
||||
if arr[j] <= pivot {
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
arr[i], arr[right] = arr[right], arr[i]
|
||||
return i
|
||||
}
|
||||
|
||||
// sharpenImage 锐化处理
|
||||
func sharpenImage(data []byte, width, height int) []byte {
|
||||
sharpened := make([]byte, len(data))
|
||||
kernel := []int{0, -1, 0, -1, 5, -1, 0, -1, 0}
|
||||
|
||||
for y := 1; y < height-1; y++ {
|
||||
for x := 1; x < width-1; x++ {
|
||||
sum := 0
|
||||
idx := 0
|
||||
for ky := -1; ky <= 1; ky++ {
|
||||
for kx := -1; kx <= 1; kx++ {
|
||||
pixelIdx := (y+ky)*width + (x + kx)
|
||||
sum += int(data[pixelIdx]) * kernel[idx]
|
||||
idx++
|
||||
}
|
||||
}
|
||||
if sum < 0 {
|
||||
sum = 0
|
||||
}
|
||||
if sum > 255 {
|
||||
sum = 255
|
||||
}
|
||||
sharpened[y*width+x] = uint8(sum)
|
||||
}
|
||||
}
|
||||
|
||||
// 边界保持原值
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
if y == 0 || y == height-1 || x == 0 || x == width-1 {
|
||||
sharpened[y*width+x] = data[y*width+x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sharpened
|
||||
}
|
||||
|
||||
// enhanceImageOtsu Otsu自适应阈值二值化
|
||||
func enhanceImageOtsu(data []byte, width, height int) []byte {
|
||||
threshold := calculateOtsuThreshold(data)
|
||||
binary := make([]byte, len(data))
|
||||
for i := range data {
|
||||
if data[i] > threshold {
|
||||
binary[i] = 255
|
||||
}
|
||||
}
|
||||
return binary
|
||||
}
|
||||
|
||||
// calculateOtsuThreshold 计算Otsu自适应阈值
|
||||
func calculateOtsuThreshold(data []byte) uint8 {
|
||||
histogram := make([]int, 256)
|
||||
for _, v := range data {
|
||||
histogram[v]++
|
||||
}
|
||||
|
||||
total := len(data)
|
||||
if total == 0 {
|
||||
return 128
|
||||
}
|
||||
|
||||
var threshold uint8
|
||||
var maxVar float64
|
||||
var sum int
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
sum += i * histogram[i]
|
||||
}
|
||||
|
||||
var sum1 int
|
||||
var wB int
|
||||
for i := 0; i < 256; i++ {
|
||||
wB += histogram[i]
|
||||
if wB == 0 {
|
||||
continue
|
||||
}
|
||||
wF := total - wB
|
||||
if wF == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
sum1 += i * histogram[i]
|
||||
mB := float64(sum1) / float64(wB)
|
||||
mF := float64(sum-sum1) / float64(wF)
|
||||
|
||||
varBetween := float64(wB) * float64(wF) * (mB - mF) * (mB - mF)
|
||||
|
||||
if varBetween > maxVar {
|
||||
maxVar = varBetween
|
||||
threshold = uint8(i)
|
||||
}
|
||||
}
|
||||
|
||||
return threshold
|
||||
}
|
||||
|
||||
// enhanceImageGaussianSharpen 高斯模糊+锐化
|
||||
func enhanceImageGaussianSharpen(data []byte, width, height int) []byte {
|
||||
blurred := gaussianBlur(data, width, height, 1.0)
|
||||
sharpened := sharpenImage(blurred, width, height)
|
||||
enhanced := enhanceImage(sharpened, width, height)
|
||||
return enhanced
|
||||
}
|
||||
|
||||
// gaussianBlur 高斯模糊
|
||||
func gaussianBlur(data []byte, width, height int, sigma float64) []byte {
|
||||
blurred := make([]byte, len(data))
|
||||
kernelSize := 5
|
||||
halfKernel := kernelSize / 2
|
||||
|
||||
kernel := make([]float64, kernelSize*kernelSize)
|
||||
sum := 0.0
|
||||
for y := -halfKernel; y <= halfKernel; y++ {
|
||||
for x := -halfKernel; x <= halfKernel; x++ {
|
||||
idx := (y+halfKernel)*kernelSize + (x + halfKernel)
|
||||
val := math.Exp(-(float64(x*x+y*y) / (2 * sigma * sigma)))
|
||||
kernel[idx] = val
|
||||
sum += val
|
||||
}
|
||||
}
|
||||
|
||||
for i := range kernel {
|
||||
kernel[i] /= sum
|
||||
}
|
||||
|
||||
for y := halfKernel; y < height-halfKernel; y++ {
|
||||
for x := halfKernel; x < width-halfKernel; x++ {
|
||||
var val float64
|
||||
idx := 0
|
||||
for ky := -halfKernel; ky <= halfKernel; ky++ {
|
||||
for kx := -halfKernel; kx <= halfKernel; kx++ {
|
||||
pixelIdx := (y+ky)*width + (x + kx)
|
||||
val += float64(data[pixelIdx]) * kernel[idx]
|
||||
idx++
|
||||
}
|
||||
}
|
||||
blurred[y*width+x] = uint8(val)
|
||||
}
|
||||
}
|
||||
|
||||
// 边界保持原值
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel {
|
||||
blurred[y*width+x] = data[y*width+x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blurred
|
||||
}
|
||||
|
||||
// createImageFromGrayscale 从灰度数据创建图像
|
||||
func createImageFromGrayscale(data []byte, width, height int) image.Image {
|
||||
img := image.NewGray(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
rowStart := y * width
|
||||
rowEnd := rowStart + width
|
||||
if rowEnd > len(data) {
|
||||
rowEnd = len(data)
|
||||
}
|
||||
copy(img.Pix[y*img.Stride:], data[rowStart:rowEnd])
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// scaledImage 缩放后的图像数据
|
||||
type scaledImage struct {
|
||||
data []byte
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
// scaleImage 缩放图像
|
||||
func scaleImage(img image.Image, origWidth, origHeight int, scale float64) image.Image {
|
||||
if scale == 1.0 {
|
||||
return img
|
||||
}
|
||||
|
||||
newWidth := int(float64(origWidth) * scale)
|
||||
newHeight := int(float64(origHeight) * scale)
|
||||
|
||||
if newWidth < 50 || newHeight < 50 {
|
||||
return nil
|
||||
}
|
||||
if newWidth > 1500 || newHeight > 1500 {
|
||||
return nil
|
||||
}
|
||||
|
||||
scaled := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
||||
bounds := img.Bounds()
|
||||
|
||||
for y := 0; y < newHeight; y++ {
|
||||
for x := 0; x < newWidth; x++ {
|
||||
srcX := float64(x) / scale
|
||||
srcY := float64(y) / scale
|
||||
|
||||
x1 := int(srcX)
|
||||
y1 := int(srcY)
|
||||
x2 := x1 + 1
|
||||
y2 := y1 + 1
|
||||
|
||||
if x2 >= bounds.Dx() {
|
||||
x2 = bounds.Dx() - 1
|
||||
}
|
||||
if y2 >= bounds.Dy() {
|
||||
y2 = bounds.Dy() - 1
|
||||
}
|
||||
|
||||
fx := srcX - float64(x1)
|
||||
fy := srcY - float64(y1)
|
||||
|
||||
c11 := getPixelColor(img, bounds.Min.X+x1, bounds.Min.Y+y1)
|
||||
c12 := getPixelColor(img, bounds.Min.X+x1, bounds.Min.Y+y2)
|
||||
c21 := getPixelColor(img, bounds.Min.X+x2, bounds.Min.Y+y1)
|
||||
c22 := getPixelColor(img, bounds.Min.X+x2, bounds.Min.Y+y2)
|
||||
|
||||
r := uint8(float64(c11.R)*(1-fx)*(1-fy) + float64(c21.R)*fx*(1-fy) + float64(c12.R)*(1-fx)*fy + float64(c22.R)*fx*fy)
|
||||
g := uint8(float64(c11.G)*(1-fx)*(1-fy) + float64(c21.G)*fx*(1-fy) + float64(c12.G)*(1-fx)*fy + float64(c22.G)*fx*fy)
|
||||
b := uint8(float64(c11.B)*(1-fx)*(1-fy) + float64(c21.B)*fx*(1-fy) + float64(c12.B)*(1-fx)*fy + float64(c22.B)*fx*fy)
|
||||
|
||||
scaled.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
return scaled
|
||||
}
|
||||
|
||||
// getPixelColor 获取像素颜色
|
||||
func getPixelColor(img image.Image, x, y int) color.RGBA {
|
||||
r, g, b, _ := img.At(x, y).RGBA()
|
||||
return color.RGBA{
|
||||
R: uint8(r >> 8),
|
||||
G: uint8(g >> 8),
|
||||
B: uint8(b >> 8),
|
||||
A: 255,
|
||||
}
|
||||
}
|
||||
|
||||
// scaleGrayscaleImage 缩放灰度图像
|
||||
func scaleGrayscaleImage(data []byte, origWidth, origHeight int, scale float64) *scaledImage {
|
||||
if scale == 1.0 {
|
||||
return &scaledImage{data: data, width: origWidth, height: origHeight}
|
||||
}
|
||||
|
||||
newWidth := int(float64(origWidth) * scale)
|
||||
newHeight := int(float64(origHeight) * scale)
|
||||
|
||||
if newWidth < 21 || newHeight < 21 || newWidth > 2000 || newHeight > 2000 {
|
||||
return nil
|
||||
}
|
||||
|
||||
scaled := make([]byte, newWidth*newHeight)
|
||||
|
||||
for y := 0; y < newHeight; y++ {
|
||||
for x := 0; x < newWidth; x++ {
|
||||
srcX := float64(x) / scale
|
||||
srcY := float64(y) / scale
|
||||
|
||||
x1 := int(srcX)
|
||||
y1 := int(srcY)
|
||||
x2 := x1 + 1
|
||||
y2 := y1 + 1
|
||||
|
||||
if x2 >= origWidth {
|
||||
x2 = origWidth - 1
|
||||
}
|
||||
if y2 >= origHeight {
|
||||
y2 = origHeight - 1
|
||||
}
|
||||
|
||||
fx := srcX - float64(x1)
|
||||
fy := srcY - float64(y1)
|
||||
|
||||
v11 := float64(data[y1*origWidth+x1])
|
||||
v12 := float64(data[y2*origWidth+x1])
|
||||
v21 := float64(data[y1*origWidth+x2])
|
||||
v22 := float64(data[y2*origWidth+x2])
|
||||
|
||||
val := v11*(1-fx)*(1-fy) + v21*fx*(1-fy) + v12*(1-fx)*fy + v22*fx*fy
|
||||
scaled[y*newWidth+x] = uint8(val)
|
||||
}
|
||||
}
|
||||
|
||||
return &scaledImage{data: scaled, width: newWidth, height: newHeight}
|
||||
}
|
||||
|
||||
// Point 表示一个点
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// Corner 表示检测到的定位图案角点
|
||||
type Corner struct {
|
||||
Center Point
|
||||
Size int
|
||||
Type int
|
||||
}
|
||||
|
||||
// detectCornersFast 快速检测定位图案
|
||||
func detectCornersFast(data []byte, width, height int) []Corner {
|
||||
var corners []Corner
|
||||
|
||||
scanStep := max(2, min(width, height)/80)
|
||||
if scanStep < 1 {
|
||||
scanStep = 1
|
||||
}
|
||||
|
||||
for y := scanStep * 3; y < height-scanStep*3; y += scanStep {
|
||||
for x := scanStep * 3; x < width-scanStep*3; x += scanStep {
|
||||
if isFinderPatternFast(data, width, height, x, y) {
|
||||
corners = append(corners, Corner{
|
||||
Center: Point{X: x, Y: y},
|
||||
Size: 20,
|
||||
})
|
||||
if len(corners) >= 3 {
|
||||
return corners
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return corners
|
||||
}
|
||||
|
||||
// isFinderPatternFast 快速检测定位图案
|
||||
func isFinderPatternFast(data []byte, width, height, x, y int) bool {
|
||||
centerIdx := y*width + x
|
||||
if centerIdx < 0 || centerIdx >= len(data) {
|
||||
return false
|
||||
}
|
||||
if data[centerIdx] > 180 {
|
||||
return false
|
||||
}
|
||||
|
||||
radius := min(width, height) / 15
|
||||
if radius < 3 {
|
||||
radius = 3
|
||||
}
|
||||
if radius > 30 {
|
||||
radius = 30
|
||||
}
|
||||
|
||||
directions := []struct{ dx, dy int }{{radius, 0}, {-radius, 0}, {0, radius}, {0, -radius}}
|
||||
blackCount := 0
|
||||
whiteCount := 0
|
||||
|
||||
for _, dir := range directions {
|
||||
nx := x + dir.dx
|
||||
ny := y + dir.dy
|
||||
if nx >= 0 && nx < width && ny >= 0 && ny < height {
|
||||
idx := ny*width + nx
|
||||
if idx >= 0 && idx < len(data) {
|
||||
if data[idx] < 128 {
|
||||
blackCount++
|
||||
} else {
|
||||
whiteCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blackCount >= 2 && whiteCount >= 2
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
191
internal/rpc/msg/qrcode_detect.go
Normal file
191
internal/rpc/msg/qrcode_detect.go
Normal file
@@ -0,0 +1,191 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
// PictureElem 用于解析图片消息内容
|
||||
type PictureElem struct {
|
||||
SourcePicture struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"sourcePicture"`
|
||||
BigPicture struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"bigPicture"`
|
||||
SnapshotPicture struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"snapshotPicture"`
|
||||
}
|
||||
|
||||
// checkImageContainsQRCode 检测图片中是否包含二维码
|
||||
// userType: 0-普通用户(不能发送包含二维码的图片),1-特殊用户(可以发送)
|
||||
func (m *msgServer) checkImageContainsQRCode(ctx context.Context, msgData *sdkws.MsgData, userType int32) error {
|
||||
// userType=1 的用户可以发送包含二维码的图片,不进行检测
|
||||
if userType == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 只检测图片类型的消息
|
||||
if msgData.ContentType != constant.Picture {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析图片消息内容
|
||||
var pictureElem PictureElem
|
||||
if err := json.Unmarshal(msgData.Content, &pictureElem); err != nil {
|
||||
// 如果解析失败,记录警告但不拦截
|
||||
log.ZWarn(ctx, "failed to parse picture message", err, "content", string(msgData.Content))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取图片URL(优先使用原图,如果没有则使用大图)
|
||||
imageURL := pictureElem.SourcePicture.URL
|
||||
if imageURL == "" {
|
||||
imageURL = pictureElem.BigPicture.URL
|
||||
}
|
||||
if imageURL == "" {
|
||||
imageURL = pictureElem.SnapshotPicture.URL
|
||||
}
|
||||
if imageURL == "" {
|
||||
// 没有有效的图片URL,无法检测
|
||||
log.ZWarn(ctx, "no valid image URL found in picture message", nil, "pictureElem", pictureElem)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 下载图片并检测二维码
|
||||
hasQRCode, err := m.detectQRCodeInImage(ctx, imageURL, "")
|
||||
if err != nil {
|
||||
// 检测失败时,记录错误但不拦截(避免误拦截)
|
||||
log.ZWarn(ctx, "QR code detection failed", err, "imageURL", imageURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasQRCode {
|
||||
log.ZWarn(ctx, "检测到二维码,拒绝发送", nil, "imageURL", imageURL, "userType", userType)
|
||||
return servererrs.ErrImageContainsQRCode.WrapMsg("userType=0的用户不能发送包含二维码的图片")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectQRCodeInImage 下载图片并检测是否包含二维码
|
||||
func (m *msgServer) detectQRCodeInImage(ctx context.Context, imageURL string, logPrefix string) (bool, error) {
|
||||
// 创建带超时的HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "创建HTTP请求失败", err, "imageURL", imageURL)
|
||||
return false, errs.WrapMsg(err, "failed to create request")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "下载图片失败", err, "imageURL", imageURL)
|
||||
return false, errs.WrapMsg(err, "failed to download image")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.ZError(ctx, "下载图片状态码异常", nil, "statusCode", resp.StatusCode, "imageURL", imageURL)
|
||||
return false, errs.WrapMsg(fmt.Errorf("unexpected status code: %d", resp.StatusCode), "failed to download image")
|
||||
}
|
||||
|
||||
// 限制图片大小(最大10MB)
|
||||
const maxImageSize = 10 * 1024 * 1024
|
||||
limitedReader := io.LimitReader(resp.Body, maxImageSize+1)
|
||||
|
||||
// 创建临时文件
|
||||
tmpFile, err := os.CreateTemp("", "qrcode_detect_*.tmp")
|
||||
if err != nil {
|
||||
log.ZError(ctx, "创建临时文件失败", err)
|
||||
return false, errs.WrapMsg(err, "failed to create temp file")
|
||||
}
|
||||
tmpFilePath := tmpFile.Name()
|
||||
|
||||
// 确保检测完成后删除临时文件(无论成功还是失败)
|
||||
defer func() {
|
||||
// 确保文件已关闭后再删除
|
||||
if tmpFile != nil {
|
||||
_ = tmpFile.Close()
|
||||
}
|
||||
// 删除临时文件,忽略文件不存在的错误
|
||||
if err := os.Remove(tmpFilePath); err != nil && !os.IsNotExist(err) {
|
||||
log.ZWarn(ctx, "删除临时文件失败", err, "tmpFile", tmpFilePath)
|
||||
}
|
||||
}()
|
||||
|
||||
// 保存图片到临时文件
|
||||
written, err := io.Copy(tmpFile, limitedReader)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "保存图片到临时文件失败", err, "tmpFile", tmpFilePath)
|
||||
return false, errs.WrapMsg(err, "failed to save image")
|
||||
}
|
||||
|
||||
// 关闭文件以便后续读取
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
log.ZError(ctx, "关闭临时文件失败", err, "tmpFile", tmpFilePath)
|
||||
return false, errs.WrapMsg(err, "failed to close temp file")
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if written > maxImageSize {
|
||||
log.ZWarn(ctx, "图片过大", nil, "size", written, "maxSize", maxImageSize)
|
||||
return false, errs.WrapMsg(fmt.Errorf("image too large: %d bytes", written), "image size exceeds limit")
|
||||
}
|
||||
|
||||
// 使用优化的并行解码器检测二维码
|
||||
hasQRCode, err := m.detectQRCodeWithDecoder(ctx, tmpFilePath, "")
|
||||
if err != nil {
|
||||
log.ZError(ctx, "二维码检测失败", err, "tmpFile", tmpFilePath)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return hasQRCode, nil
|
||||
}
|
||||
|
||||
// detectQRCodeWithDecoder 使用优化的解码器检测二维码
|
||||
func (m *msgServer) detectQRCodeWithDecoder(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
|
||||
// 使用Custom解码器(已移除Quirc解码器依赖)
|
||||
customDecoder := &CustomQRDecoder{}
|
||||
|
||||
// 执行解码
|
||||
hasQRCode, err := customDecoder.Decode(ctx, imagePath, logPrefix)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "解码器检测失败", err, "decoder", customDecoder.Name())
|
||||
return false, err
|
||||
}
|
||||
|
||||
return hasQRCode, nil
|
||||
}
|
||||
134
internal/rpc/msg/revoke.go
Normal file
134
internal/rpc/msg/revoke.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
func (m *msgServer) RevokeMsg(ctx context.Context, req *msg.RevokeMsgReq) (*msg.RevokeMsgResp, error) {
|
||||
if req.UserID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("user_id is empty")
|
||||
}
|
||||
if req.ConversationID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("conversation_id is empty")
|
||||
}
|
||||
if req.Seq < 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("seq is invalid")
|
||||
}
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := m.UserLocalCache.GetUserInfo(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _, msgs, err := m.MsgDatabase.GetMsgBySeqs(ctx, req.UserID, req.ConversationID, []int64{req.Seq})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(msgs) == 0 || msgs[0] == nil {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("msg not found")
|
||||
}
|
||||
if msgs[0].ContentType == constant.MsgRevokeNotification {
|
||||
return nil, servererrs.ErrMsgAlreadyRevoke.WrapMsg("msg already revoke")
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(msgs[0])
|
||||
log.ZDebug(ctx, "GetMsgBySeqs", "conversationID", req.ConversationID, "seq", req.Seq, "msg", string(data))
|
||||
var role int32
|
||||
if !authverify.IsAdmin(ctx) {
|
||||
sessionType := msgs[0].SessionType
|
||||
switch sessionType {
|
||||
case constant.SingleChatType:
|
||||
if err := authverify.CheckAccess(ctx, msgs[0].SendID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role = user.AppMangerLevel
|
||||
case constant.ReadGroupChatType:
|
||||
members, err := m.GroupLocalCache.GetGroupMemberInfoMap(ctx, msgs[0].GroupID, datautil.Distinct([]string{req.UserID, msgs[0].SendID}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.UserID != msgs[0].SendID {
|
||||
switch members[req.UserID].RoleLevel {
|
||||
case constant.GroupOwner:
|
||||
case constant.GroupAdmin:
|
||||
if sendMember, ok := members[msgs[0].SendID]; ok {
|
||||
if sendMember.RoleLevel != constant.GroupOrdinaryUsers {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("no permission")
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, errs.ErrNoPermission.WrapMsg("no permission")
|
||||
}
|
||||
}
|
||||
if member := members[req.UserID]; member != nil {
|
||||
role = member.RoleLevel
|
||||
}
|
||||
default:
|
||||
return nil, errs.ErrInternalServer.WrapMsg("msg sessionType not supported", "sessionType", sessionType)
|
||||
}
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
err = m.MsgDatabase.RevokeMsg(ctx, req.ConversationID, req.Seq, &model.RevokeModel{
|
||||
Role: role,
|
||||
UserID: req.UserID,
|
||||
Nickname: user.Nickname,
|
||||
Time: now,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
revokerUserID := mcontext.GetOpUserID(ctx)
|
||||
var flag bool
|
||||
|
||||
if len(m.config.Share.IMAdminUser.UserIDs) > 0 {
|
||||
flag = datautil.Contain(revokerUserID, m.adminUserIDs...)
|
||||
}
|
||||
tips := sdkws.RevokeMsgTips{
|
||||
RevokerUserID: revokerUserID,
|
||||
ClientMsgID: msgs[0].ClientMsgID,
|
||||
RevokeTime: now,
|
||||
Seq: req.Seq,
|
||||
SesstionType: msgs[0].SessionType,
|
||||
ConversationID: req.ConversationID,
|
||||
IsAdminRevoke: flag,
|
||||
}
|
||||
var recvID string
|
||||
if msgs[0].SessionType == constant.ReadGroupChatType {
|
||||
recvID = msgs[0].GroupID
|
||||
} else {
|
||||
recvID = msgs[0].RecvID
|
||||
}
|
||||
m.notificationSender.NotificationWithSessionType(ctx, req.UserID, recvID, constant.MsgRevokeNotification, msgs[0].SessionType, &tips)
|
||||
webhookCfg := m.webhookConfig()
|
||||
m.webhookAfterRevokeMsg(ctx, &webhookCfg.AfterRevokeMsg, req)
|
||||
return &msg.RevokeMsgResp{}, nil
|
||||
}
|
||||
231
internal/rpc/msg/send.go
Normal file
231
internal/rpc/msg/send.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/prommetrics"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/msgprocessor"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/util/conversationutil"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbconv "git.imall.cloud/openim/protocol/conversation"
|
||||
pbmsg "git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"git.imall.cloud/openim/protocol/wrapperspb"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
func (m *msgServer) SendMsg(ctx context.Context, req *pbmsg.SendMsgReq) (*pbmsg.SendMsgResp, error) {
|
||||
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 {
|
||||
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)
|
||||
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, before **sdkws.MsgData) (resp *pbmsg.SendMsgResp, err error) {
|
||||
if err = m.messageVerification(ctx, req); err != nil {
|
||||
prommetrics.GroupChatMsgProcessFailedCounter.Inc()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webhookCfg := m.webhookConfig()
|
||||
|
||||
if err = m.webhookBeforeSendGroupMsg(ctx, &webhookCfg.BeforeSendGroupMsg, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.webhookBeforeMsgModify(ctx, &webhookCfg.BeforeMsgModify, req, before); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.MsgDatabase.MsgToMQ(ctx, conversationutil.GenConversationUniqueKeyForGroup(req.MsgData.GroupID), req.MsgData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.MsgData.ContentType == constant.AtText {
|
||||
go m.setConversationAtInfo(ctx, req.MsgData)
|
||||
}
|
||||
|
||||
// 获取webhook配置(优先从配置管理器获取)
|
||||
m.webhookAfterSendGroupMsg(ctx, &webhookCfg.AfterSendGroupMsg, req)
|
||||
|
||||
prommetrics.GroupChatMsgProcessSuccessCounter.Inc()
|
||||
resp = &pbmsg.SendMsgResp{}
|
||||
resp.SendTime = req.MsgData.SendTime
|
||||
resp.ServerMsgID = req.MsgData.ServerMsgID
|
||||
resp.ClientMsgID = req.MsgData.ClientMsgID
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) setConversationAtInfo(nctx context.Context, msg *sdkws.MsgData) {
|
||||
|
||||
log.ZDebug(nctx, "setConversationAtInfo", "msg", msg)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.ZPanic(nctx, "setConversationAtInfo Panic", errs.ErrPanic(r))
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := mcontext.NewCtx("@@@" + mcontext.GetOperationID(nctx))
|
||||
|
||||
var atUserID []string
|
||||
|
||||
conversation := &pbconv.ConversationReq{
|
||||
ConversationID: msgprocessor.GetConversationIDByMsg(msg),
|
||||
ConversationType: msg.SessionType,
|
||||
GroupID: msg.GroupID,
|
||||
}
|
||||
memberUserIDList, err := m.GroupLocalCache.GetGroupMemberIDs(ctx, msg.GroupID)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "GetGroupMemberIDs", err)
|
||||
return
|
||||
}
|
||||
|
||||
tagAll := datautil.Contain(constant.AtAllString, msg.AtUserIDList...)
|
||||
if tagAll {
|
||||
|
||||
memberUserIDList = datautil.DeleteElems(memberUserIDList, msg.SendID)
|
||||
|
||||
atUserID = datautil.Single([]string{constant.AtAllString}, msg.AtUserIDList)
|
||||
|
||||
if len(atUserID) == 0 { // just @everyone
|
||||
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAll}
|
||||
} else { // @Everyone and @other people
|
||||
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAllAtMe}
|
||||
atUserID = datautil.SliceIntersectFuncs(atUserID, memberUserIDList, func(a string) string { return a }, func(b string) string {
|
||||
return b
|
||||
})
|
||||
if err := m.conversationClient.SetConversations(ctx, atUserID, conversation); err != nil {
|
||||
log.ZWarn(ctx, "SetConversations", err, "userID", atUserID, "conversation", conversation)
|
||||
}
|
||||
memberUserIDList = datautil.Single(atUserID, memberUserIDList)
|
||||
}
|
||||
|
||||
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtAll}
|
||||
if err := m.conversationClient.SetConversations(ctx, memberUserIDList, conversation); err != nil {
|
||||
log.ZWarn(ctx, "SetConversations", err, "userID", memberUserIDList, "conversation", conversation)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
atUserID = datautil.SliceIntersectFuncs(msg.AtUserIDList, memberUserIDList, func(a string) string { return a }, func(b string) string {
|
||||
return b
|
||||
})
|
||||
conversation.GroupAtType = &wrapperspb.Int32Value{Value: constant.AtMe}
|
||||
|
||||
if err := m.conversationClient.SetConversations(ctx, atUserID, conversation); err != nil {
|
||||
log.ZWarn(ctx, "SetConversations", err, atUserID, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
resp = &pbmsg.SendMsgResp{
|
||||
ServerMsgID: req.MsgData.ServerMsgID,
|
||||
ClientMsgID: req.MsgData.ClientMsgID,
|
||||
SendTime: req.MsgData.SendTime,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
webhookCfg := m.webhookConfig()
|
||||
isSend := true
|
||||
isNotification := msgprocessor.IsNotificationByMsg(req.MsgData)
|
||||
if !isNotification {
|
||||
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
|
||||
}
|
||||
}
|
||||
if !isSend {
|
||||
prommetrics.SingleChatMsgProcessFailedCounter.Inc()
|
||||
return nil, errs.ErrArgs.WrapMsg("message is not sent")
|
||||
} else {
|
||||
if err := m.webhookBeforeMsgModify(ctx, &webhookCfg.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
|
||||
}
|
||||
|
||||
m.webhookAfterSendSingleMsg(ctx, &webhookCfg.AfterSendSingleMsg, req)
|
||||
prommetrics.SingleChatMsgProcessSuccessCounter.Inc()
|
||||
return &pbmsg.SendMsgResp{
|
||||
ServerMsgID: req.MsgData.ServerMsgID,
|
||||
ClientMsgID: req.MsgData.ClientMsgID,
|
||||
SendTime: req.MsgData.SendTime,
|
||||
}, 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
|
||||
}
|
||||
105
internal/rpc/msg/seq.go
Normal file
105
internal/rpc/msg/seq.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
pbmsg "git.imall.cloud/openim/protocol/msg"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func (m *msgServer) GetConversationMaxSeq(ctx context.Context, req *pbmsg.GetConversationMaxSeqReq) (*pbmsg.GetConversationMaxSeqResp, error) {
|
||||
maxSeq, err := m.MsgDatabase.GetMaxSeq(ctx, req.ConversationID)
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
return &pbmsg.GetConversationMaxSeqResp{MaxSeq: maxSeq}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetMaxSeqs(ctx context.Context, req *pbmsg.GetMaxSeqsReq) (*pbmsg.SeqsInfoResp, error) {
|
||||
maxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, req.ConversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbmsg.SeqsInfoResp{MaxSeqs: maxSeqs}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetHasReadSeqs(ctx context.Context, req *pbmsg.GetHasReadSeqsReq) (*pbmsg.SeqsInfoResp, error) {
|
||||
hasReadSeqs, err := m.MsgDatabase.GetHasReadSeqs(ctx, req.UserID, req.ConversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbmsg.SeqsInfoResp{MaxSeqs: hasReadSeqs}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetMsgByConversationIDs(ctx context.Context, req *pbmsg.GetMsgByConversationIDsReq) (*pbmsg.GetMsgByConversationIDsResp, error) {
|
||||
Msgs, err := m.MsgDatabase.FindOneByDocIDs(ctx, req.ConversationIDs, req.MaxSeqs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbmsg.GetMsgByConversationIDsResp{MsgDatas: Msgs}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) SetUserConversationsMinSeq(ctx context.Context, req *pbmsg.SetUserConversationsMinSeqReq) (*pbmsg.SetUserConversationsMinSeqResp, error) {
|
||||
for _, userID := range req.UserIDs {
|
||||
if err := m.MsgDatabase.SetUserConversationsMinSeqs(ctx, userID, map[string]int64{req.ConversationID: req.Seq}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pbmsg.SetUserConversationsMinSeqResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetActiveConversation(ctx context.Context, req *pbmsg.GetActiveConversationReq) (*pbmsg.GetActiveConversationResp, error) {
|
||||
res, err := m.MsgDatabase.GetCacheMaxSeqWithTime(ctx, req.ConversationIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conversations := make([]*pbmsg.ActiveConversation, 0, len(res))
|
||||
for conversationID, val := range res {
|
||||
conversations = append(conversations, &pbmsg.ActiveConversation{
|
||||
MaxSeq: val.Seq,
|
||||
LastTime: val.Time,
|
||||
ConversationID: conversationID,
|
||||
})
|
||||
}
|
||||
if req.Limit > 0 {
|
||||
sort.Sort(activeConversations(conversations))
|
||||
if len(conversations) > int(req.Limit) {
|
||||
conversations = conversations[:req.Limit]
|
||||
}
|
||||
}
|
||||
return &pbmsg.GetActiveConversationResp{Conversations: conversations}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) SetUserConversationMaxSeq(ctx context.Context, req *pbmsg.SetUserConversationMaxSeqReq) (*pbmsg.SetUserConversationMaxSeqResp, error) {
|
||||
for _, userID := range req.OwnerUserID {
|
||||
if err := m.MsgDatabase.SetUserConversationsMaxSeq(ctx, req.ConversationID, userID, req.MaxSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pbmsg.SetUserConversationMaxSeqResp{}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) SetUserConversationMinSeq(ctx context.Context, req *pbmsg.SetUserConversationMinSeqReq) (*pbmsg.SetUserConversationMinSeqResp, error) {
|
||||
for _, userID := range req.OwnerUserID {
|
||||
if err := m.MsgDatabase.SetUserConversationsMinSeq(ctx, req.ConversationID, userID, req.MinSeq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pbmsg.SetUserConversationMinSeqResp{}, nil
|
||||
}
|
||||
218
internal/rpc/msg/server.go
Normal file
218
internal/rpc/msg/server.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/mcache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/localcache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/mqbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpccache"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/conversation"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
type MessageInterceptorFunc func(ctx context.Context, globalConfig *Config, req *msg.SendMsgReq) (*sdkws.MsgData, error)
|
||||
|
||||
// MessageInterceptorChain defines a chain of message interceptor functions.
|
||||
type MessageInterceptorChain []MessageInterceptorFunc
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Msg
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
KafkaConfig config.Kafka
|
||||
NotificationConfig config.Notification
|
||||
Share config.Share
|
||||
WebhooksConfig config.Webhooks
|
||||
LocalCacheConfig config.LocalCache
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
// 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.
|
||||
UserLocalCache *rpccache.UserLocalCache // Local cache for user data.
|
||||
FriendLocalCache *rpccache.FriendLocalCache // Local cache for friend data.
|
||||
GroupLocalCache *rpccache.GroupLocalCache // Local cache for group data.
|
||||
ConversationLocalCache *rpccache.ConversationLocalCache // Local cache for conversation data.
|
||||
Handlers MessageInterceptorChain // Chain of handlers for processing messages.
|
||||
notificationSender *notification.NotificationSender // RPC client for sending notifications.
|
||||
msgNotificationSender *MsgNotificationSender // RPC client for sending msg notifications.
|
||||
config *Config // Global configuration settings.
|
||||
webhookClient *webhook.Client
|
||||
webhookConfigManager *webhook.ConfigManager // Webhook配置管理器(支持从数据库读取)
|
||||
conversationClient *rpcli.ConversationClient
|
||||
redPacketDB database.RedPacket // Database for red packet records.
|
||||
redPacketReceiveDB database.RedPacketReceive // Database for red packet receive records.
|
||||
|
||||
adminUserIDs []string
|
||||
}
|
||||
|
||||
func (m *msgServer) addInterceptorHandler(interceptorFunc ...MessageInterceptorFunc) {
|
||||
m.Handlers = append(m.Handlers, interceptorFunc...)
|
||||
|
||||
}
|
||||
|
||||
// webhookConfig returns the latest webhook config from the client/manager with fallback.
|
||||
func (m *msgServer) webhookConfig() *config.Webhooks {
|
||||
if m.webhookClient == nil {
|
||||
return &m.config.WebhooksConfig
|
||||
}
|
||||
return m.webhookClient.GetConfig(&m.config.WebhooksConfig)
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
dbb := dbbuild.NewBuilder(&config.MongodbConfig, &config.RedisConfig)
|
||||
mgocli, err := dbb.Mongo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rdb, err := dbb.Redis(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgDocModel, err := mgo.NewMsgMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var msgModel cache.MsgCache
|
||||
if rdb == nil {
|
||||
cm, err := mgo.NewCacheMgo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgModel = mcache.NewMsgCache(cm, msgDocModel)
|
||||
} else {
|
||||
msgModel = redis.NewMsgCache(rdb, msgDocModel)
|
||||
}
|
||||
seqConversation, err := mgo.NewSeqConversationMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seqConversationCache := redis.NewSeqConversationCacheRedis(rdb, seqConversation)
|
||||
seqUser, err := mgo.NewSeqUserMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seqUserCache := redis.NewSeqUserCacheRedis(rdb, seqUser)
|
||||
redPacketDB, err := mgo.NewRedPacketMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
redPacketReceiveDB, err := mgo.NewRedPacketReceiveMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
friendConn, err := client.GetConn(ctx, config.Discovery.RpcService.Friend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conversationConn, err := client.GetConn(ctx, config.Discovery.RpcService.Conversation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conversationClient := rpcli.NewConversationClient(conversationConn)
|
||||
msgDatabase := controller.NewCommonMsgDatabase(msgDocModel, msgModel, seqUserCache, seqConversationCache, redisProducer)
|
||||
localcache.InitLocalCache(&config.LocalCacheConfig)
|
||||
|
||||
// 初始化webhook配置管理器(支持从数据库读取配置)
|
||||
var webhookClient *webhook.Client
|
||||
var webhookConfigManager *webhook.ConfigManager
|
||||
systemConfigDB, err := mgo.NewSystemConfigMongo(mgocli.GetDB())
|
||||
if err == nil {
|
||||
// 如果SystemConfig数据库初始化成功,使用配置管理器
|
||||
webhookConfigManager = webhook.NewConfigManager(systemConfigDB, &config.WebhooksConfig)
|
||||
if err := webhookConfigManager.Start(ctx); err != nil {
|
||||
log.ZWarn(ctx, "failed to start webhook config manager, using default config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
} else {
|
||||
webhookClient = webhook.NewWebhookClientWithManager(webhookConfigManager)
|
||||
}
|
||||
} else {
|
||||
// 如果SystemConfig数据库初始化失败,使用默认配置
|
||||
log.ZWarn(ctx, "failed to init system config db, using default webhook config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
}
|
||||
|
||||
s := &msgServer{
|
||||
MsgDatabase: msgDatabase,
|
||||
RegisterCenter: client,
|
||||
UserLocalCache: rpccache.NewUserLocalCache(rpcli.NewUserClient(userConn), &config.LocalCacheConfig, rdb),
|
||||
GroupLocalCache: rpccache.NewGroupLocalCache(rpcli.NewGroupClient(groupConn), &config.LocalCacheConfig, rdb),
|
||||
ConversationLocalCache: rpccache.NewConversationLocalCache(conversationClient, &config.LocalCacheConfig, rdb),
|
||||
FriendLocalCache: rpccache.NewFriendLocalCache(rpcli.NewRelationClient(friendConn), &config.LocalCacheConfig, rdb),
|
||||
config: config,
|
||||
webhookClient: webhookClient,
|
||||
webhookConfigManager: webhookConfigManager,
|
||||
conversationClient: conversationClient,
|
||||
redPacketDB: redPacketDB,
|
||||
redPacketReceiveDB: redPacketReceiveDB,
|
||||
adminUserIDs: config.Share.IMAdminUser.UserIDs,
|
||||
}
|
||||
|
||||
s.notificationSender = notification.NewNotificationSender(&config.NotificationConfig, notification.WithLocalSendMsg(s.SendMsg))
|
||||
s.msgNotificationSender = NewMsgNotificationSender(config, notification.WithLocalSendMsg(s.SendMsg))
|
||||
|
||||
msg.RegisterMsgServer(server, s)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *msgServer) conversationAndGetRecvID(conversation *conversation.Conversation, userID string) string {
|
||||
if conversation.ConversationType == constant.SingleChatType ||
|
||||
conversation.ConversationType == constant.NotificationChatType {
|
||||
if userID == conversation.OwnerUserID {
|
||||
return conversation.UserID
|
||||
} else {
|
||||
return conversation.OwnerUserID
|
||||
}
|
||||
} else if conversation.ConversationType == constant.ReadGroupChatType {
|
||||
return conversation.GroupID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
107
internal/rpc/msg/statistics.go
Normal file
107
internal/rpc/msg/statistics.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/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
|
||||
}
|
||||
var pbUsers []*msg.ActiveUser
|
||||
if len(users) > 0 {
|
||||
userIDs := datautil.Slice(users, func(e *model.UserCount) string { return e.UserID })
|
||||
userMap, err := m.UserLocalCache.GetUsersInfoMap(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pbUsers = make([]*msg.ActiveUser, 0, len(users))
|
||||
for _, user := range users {
|
||||
pbUser := userMap[user.UserID]
|
||||
if pbUser == nil {
|
||||
pbUser = &sdkws.UserInfo{
|
||||
UserID: user.UserID,
|
||||
Nickname: user.UserID,
|
||||
}
|
||||
}
|
||||
pbUsers = append(pbUsers, &msg.ActiveUser{
|
||||
User: pbUser,
|
||||
Count: user.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &msg.GetActiveUserResp{
|
||||
MsgCount: msgCount,
|
||||
UserCount: userCount,
|
||||
DateCount: dateCount,
|
||||
Users: pbUsers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
var pbgroups []*msg.ActiveGroup
|
||||
if len(groups) > 0 {
|
||||
groupIDs := datautil.Slice(groups, func(e *model.GroupCount) string { return e.GroupID })
|
||||
resp, err := m.GroupLocalCache.GetGroupInfos(ctx, groupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupMap := make(map[string]*sdkws.GroupInfo, len(groups))
|
||||
for i, group := range groups {
|
||||
groupMap[group.GroupID] = resp[i]
|
||||
}
|
||||
pbgroups = make([]*msg.ActiveGroup, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
pbgroup := groupMap[group.GroupID]
|
||||
if pbgroup == nil {
|
||||
pbgroup = &sdkws.GroupInfo{
|
||||
GroupID: group.GroupID,
|
||||
GroupName: group.GroupID,
|
||||
}
|
||||
}
|
||||
pbgroups = append(pbgroups, &msg.ActiveGroup{
|
||||
Group: pbgroup,
|
||||
Count: group.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &msg.GetActiveGroupResp{
|
||||
MsgCount: msgCount,
|
||||
GroupCount: groupCount,
|
||||
DateCount: dateCount,
|
||||
Groups: pbgroups,
|
||||
}, nil
|
||||
}
|
||||
658
internal/rpc/msg/sync_msg.go
Normal file
658
internal/rpc/msg/sync_msg.go
Normal file
@@ -0,0 +1,658 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/apistruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/msgprocessor"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/util/conversationutil"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/openimsdk/tools/utils/jsonutil"
|
||||
"github.com/openimsdk/tools/utils/timeutil"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
// 设置请求超时,防止大量数据拉取导致pod异常
|
||||
// 对于大量大群用户,需要更长的超时时间(60秒)
|
||||
queryTimeout := 60 * time.Second
|
||||
queryCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 参数验证:限制 SeqRanges 数量和每个范围的大小,防止内存溢出
|
||||
const maxSeqRanges = 100
|
||||
const maxSeqRangeSize = 10000
|
||||
// 限制响应总大小,防止pod内存溢出(估算:每条消息平均1KB,最多50MB)
|
||||
const maxTotalMessages = 50000
|
||||
if len(req.SeqRanges) > maxSeqRanges {
|
||||
log.ZWarn(ctx, "SeqRanges count exceeds limit", nil, "count", len(req.SeqRanges), "limit", maxSeqRanges)
|
||||
return nil, errs.ErrArgs.WrapMsg("too many seq ranges", "count", len(req.SeqRanges), "limit", maxSeqRanges)
|
||||
}
|
||||
for _, seq := range req.SeqRanges {
|
||||
// 验证每个 seq range 的合理性
|
||||
if seq.Begin < 0 || seq.End < 0 {
|
||||
log.ZWarn(ctx, "invalid seq range: negative values", nil, "begin", seq.Begin, "end", seq.End)
|
||||
continue
|
||||
}
|
||||
if seq.End < seq.Begin {
|
||||
log.ZWarn(ctx, "invalid seq range: end < begin", nil, "begin", seq.Begin, "end", seq.End)
|
||||
continue
|
||||
}
|
||||
seqRangeSize := seq.End - seq.Begin + 1
|
||||
if seqRangeSize > maxSeqRangeSize {
|
||||
log.ZWarn(ctx, "seq range size exceeds limit, will be limited", nil, "conversationID", seq.ConversationID, "begin", seq.Begin, "end", seq.End, "size", seqRangeSize, "limit", maxSeqRangeSize)
|
||||
}
|
||||
}
|
||||
resp := &sdkws.PullMessageBySeqsResp{}
|
||||
resp.Msgs = make(map[string]*sdkws.PullMsgs)
|
||||
resp.NotificationMsgs = make(map[string]*sdkws.PullMsgs)
|
||||
|
||||
var totalMessages int
|
||||
for _, seq := range req.SeqRanges {
|
||||
// 检查总消息数,防止内存溢出
|
||||
if totalMessages >= maxTotalMessages {
|
||||
log.ZWarn(ctx, "total messages count exceeds limit, stopping", nil, "totalMessages", totalMessages, "limit", maxTotalMessages)
|
||||
break
|
||||
}
|
||||
if !msgprocessor.IsNotification(seq.ConversationID) {
|
||||
conversation, err := m.ConversationLocalCache.GetConversation(queryCtx, req.UserID, seq.ConversationID)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "GetConversation error", err, "conversationID", seq.ConversationID)
|
||||
continue
|
||||
}
|
||||
minSeq, maxSeq, msgs, err := m.MsgDatabase.GetMsgBySeqsRange(queryCtx, req.UserID, seq.ConversationID,
|
||||
seq.Begin, seq.End, seq.Num, conversation.MaxSeq)
|
||||
if err != nil {
|
||||
// 如果是超时错误,记录更详细的日志
|
||||
if queryCtx.Err() == context.DeadlineExceeded {
|
||||
log.ZWarn(ctx, "GetMsgBySeqsRange timeout", err, "conversationID", seq.ConversationID, "seq", seq, "timeout", queryTimeout)
|
||||
return nil, errs.ErrInternalServer.WrapMsg("message pull timeout, data too large or query too slow")
|
||||
}
|
||||
log.ZWarn(ctx, "GetMsgBySeqsRange error", err, "conversationID", seq.ConversationID, "seq", seq)
|
||||
continue
|
||||
}
|
||||
totalMessages += len(msgs)
|
||||
var isEnd bool
|
||||
switch req.Order {
|
||||
case sdkws.PullOrder_PullOrderAsc:
|
||||
isEnd = maxSeq <= seq.End
|
||||
case sdkws.PullOrder_PullOrderDesc:
|
||||
isEnd = seq.Begin <= minSeq
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
log.ZWarn(ctx, "not have msgs", nil, "conversationID", seq.ConversationID, "seq", seq)
|
||||
continue
|
||||
}
|
||||
// 过滤禁言通知消息(只保留群主、管理员和被禁言成员本人可以看到的)
|
||||
msgs = m.filterMuteNotificationMsgs(ctx, req.UserID, seq.ConversationID, msgs)
|
||||
// 填充红包消息的领取信息
|
||||
msgs = m.enrichRedPacketMessages(ctx, req.UserID, msgs)
|
||||
resp.Msgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: msgs, IsEnd: isEnd}
|
||||
} else {
|
||||
// 限制通知消息的查询范围,防止内存溢出
|
||||
const maxNotificationSeqRange = 5000
|
||||
var seqs []int64
|
||||
seqRange := seq.End - seq.Begin + 1
|
||||
if seqRange > maxNotificationSeqRange {
|
||||
log.ZWarn(ctx, "notification seq range too large, limiting", nil, "conversationID", seq.ConversationID, "begin", seq.Begin, "end", seq.End, "range", seqRange, "limit", maxNotificationSeqRange)
|
||||
// 只取最后 maxNotificationSeqRange 条
|
||||
for i := seq.End - maxNotificationSeqRange + 1; i <= seq.End; i++ {
|
||||
seqs = append(seqs, i)
|
||||
}
|
||||
} else {
|
||||
for i := seq.Begin; i <= seq.End; i++ {
|
||||
seqs = append(seqs, i)
|
||||
}
|
||||
}
|
||||
minSeq, maxSeq, notificationMsgs, err := m.MsgDatabase.GetMsgBySeqs(queryCtx, req.UserID, seq.ConversationID, seqs)
|
||||
if err != nil {
|
||||
// 如果是超时错误,记录更详细的日志
|
||||
if queryCtx.Err() == context.DeadlineExceeded {
|
||||
log.ZWarn(ctx, "GetMsgBySeqs timeout", err, "conversationID", seq.ConversationID, "seq", seq, "timeout", queryTimeout)
|
||||
return nil, errs.ErrInternalServer.WrapMsg("notification message pull timeout, data too large or query too slow")
|
||||
}
|
||||
log.ZWarn(ctx, "GetMsgBySeqs error", err, "conversationID", seq.ConversationID, "seq", seq)
|
||||
continue
|
||||
}
|
||||
totalMessages += len(notificationMsgs)
|
||||
var isEnd bool
|
||||
switch req.Order {
|
||||
case sdkws.PullOrder_PullOrderAsc:
|
||||
isEnd = maxSeq <= seq.End
|
||||
case sdkws.PullOrder_PullOrderDesc:
|
||||
isEnd = seq.Begin <= minSeq
|
||||
}
|
||||
if len(notificationMsgs) == 0 {
|
||||
log.ZWarn(ctx, "not have notificationMsgs", nil, "conversationID", seq.ConversationID, "seq", seq)
|
||||
|
||||
continue
|
||||
}
|
||||
// 过滤禁言通知消息(只保留群主、管理员和被禁言成员本人可以看到的)
|
||||
notificationMsgs = m.filterMuteNotificationMsgs(ctx, req.UserID, seq.ConversationID, notificationMsgs)
|
||||
// 填充红包消息的领取信息
|
||||
notificationMsgs = m.enrichRedPacketMessages(ctx, req.UserID, notificationMsgs)
|
||||
resp.NotificationMsgs[seq.ConversationID] = &sdkws.PullMsgs{Msgs: notificationMsgs, IsEnd: isEnd}
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetSeqMessage(ctx context.Context, req *msg.GetSeqMessageReq) (*msg.GetSeqMessageResp, error) {
|
||||
resp := &msg.GetSeqMessageResp{
|
||||
Msgs: make(map[string]*sdkws.PullMsgs),
|
||||
NotificationMsgs: make(map[string]*sdkws.PullMsgs),
|
||||
}
|
||||
for _, conv := range req.Conversations {
|
||||
isEnd, endSeq, msgs, err := m.MsgDatabase.GetMessagesBySeqWithBounds(ctx, req.UserID, conv.ConversationID, conv.Seqs, req.GetOrder())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pullMsgs *sdkws.PullMsgs
|
||||
if ok := false; conversationutil.IsNotificationConversationID(conv.ConversationID) {
|
||||
pullMsgs, ok = resp.NotificationMsgs[conv.ConversationID]
|
||||
if !ok {
|
||||
pullMsgs = &sdkws.PullMsgs{}
|
||||
resp.NotificationMsgs[conv.ConversationID] = pullMsgs
|
||||
}
|
||||
} else {
|
||||
pullMsgs, ok = resp.Msgs[conv.ConversationID]
|
||||
if !ok {
|
||||
pullMsgs = &sdkws.PullMsgs{}
|
||||
resp.Msgs[conv.ConversationID] = pullMsgs
|
||||
}
|
||||
}
|
||||
// 过滤禁言通知消息(只保留群主、管理员和被禁言成员本人可以看到的)
|
||||
filteredMsgs := m.filterMuteNotificationMsgs(ctx, req.UserID, conv.ConversationID, msgs)
|
||||
// 填充红包消息的领取信息
|
||||
filteredMsgs = m.enrichRedPacketMessages(ctx, req.UserID, filteredMsgs)
|
||||
pullMsgs.Msgs = append(pullMsgs.Msgs, filteredMsgs...)
|
||||
pullMsgs.IsEnd = isEnd
|
||||
pullMsgs.EndSeq = endSeq
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// filterMuteNotificationMsgs 过滤禁言、取消禁言、踢出群聊、退出群聊、进入群聊、群成员信息设置和角色变更通知消息,只保留群主、管理员和相关成员本人可以看到的消息
|
||||
func (m *msgServer) filterMuteNotificationMsgs(ctx context.Context, userID, conversationID string, msgs []*sdkws.MsgData) []*sdkws.MsgData {
|
||||
// 如果不是群聊会话,直接返回
|
||||
if !conversationutil.IsGroupConversationID(conversationID) {
|
||||
return msgs
|
||||
}
|
||||
|
||||
// 提取群ID
|
||||
groupID := conversationutil.GetGroupIDFromConversationID(conversationID)
|
||||
if groupID == "" {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: invalid group conversationID", nil, "conversationID", conversationID)
|
||||
return msgs
|
||||
}
|
||||
|
||||
var filteredMsgs []*sdkws.MsgData
|
||||
var needCheckPermission bool
|
||||
|
||||
// 先检查是否有需要过滤的消息
|
||||
for _, msg := range msgs {
|
||||
if msg.ContentType == constant.GroupMemberMutedNotification ||
|
||||
msg.ContentType == constant.GroupMemberCancelMutedNotification ||
|
||||
msg.ContentType == constant.MemberKickedNotification ||
|
||||
msg.ContentType == constant.MemberQuitNotification ||
|
||||
msg.ContentType == constant.MemberInvitedNotification ||
|
||||
msg.ContentType == constant.MemberEnterNotification ||
|
||||
msg.ContentType == constant.GroupMemberInfoSetNotification ||
|
||||
msg.ContentType == constant.GroupMemberSetToAdminNotification ||
|
||||
msg.ContentType == constant.GroupMemberSetToOrdinaryUserNotification {
|
||||
needCheckPermission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有需要过滤的消息,直接返回
|
||||
if !needCheckPermission {
|
||||
return msgs
|
||||
}
|
||||
|
||||
// 对于被踢出的用户,可能无法获取成员信息,需要特殊处理
|
||||
// 先收集所有被踢出的用户ID,以便后续判断
|
||||
var allKickedUserIDs []string
|
||||
if needCheckPermission {
|
||||
for _, msg := range msgs {
|
||||
if msg.ContentType == constant.MemberKickedNotification {
|
||||
var tips sdkws.MemberKickedTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err == nil {
|
||||
kickedUserIDs := datautil.Slice(tips.KickedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })
|
||||
allKickedUserIDs = append(allKickedUserIDs, kickedUserIDs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
allKickedUserIDs = datautil.Distinct(allKickedUserIDs)
|
||||
}
|
||||
|
||||
// 检查用户是否是被踢出的用户(即使已经被踢出,也应该能看到自己被踢出的通知)
|
||||
isKickedUserInMsgs := datautil.Contain(userID, allKickedUserIDs...)
|
||||
|
||||
// 获取当前用户在群中的角色(如果用户已经被踢出,这里会返回错误)
|
||||
// 添加超时保护,防止大群查询阻塞(3秒超时)
|
||||
memberCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
member, err := m.GroupLocalCache.GetGroupMember(memberCtx, groupID, userID)
|
||||
isOwnerOrAdmin := false
|
||||
if err != nil {
|
||||
if memberCtx.Err() == context.DeadlineExceeded {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: GetGroupMember timeout", err, "groupID", groupID, "userID", userID)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GetGroupMember failed (user may be kicked)", err, "groupID", groupID, "userID", userID, "isKickedUserInMsgs", isKickedUserInMsgs)
|
||||
}
|
||||
// 如果获取失败(可能已经被踢出或超时),仍然需要检查是否是相关成员本人
|
||||
// 继续处理,但 isOwnerOrAdmin 保持为 false
|
||||
// 如果是被踢出的用户,仍然可以查看自己被踢出的通知
|
||||
} else {
|
||||
isOwnerOrAdmin = member.RoleLevel == constant.GroupOwner || member.RoleLevel == constant.GroupAdmin
|
||||
}
|
||||
|
||||
// 过滤消息
|
||||
for _, msg := range msgs {
|
||||
|
||||
if msg.ContentType == constant.GroupMemberMutedNotification {
|
||||
var tips sdkws.GroupMemberMutedTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberMutedTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
mutedUserID := tips.MutedUser.UserID
|
||||
if isOwnerOrAdmin || userID == mutedUserID {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberMutedNotification allowed", "userID", userID, "mutedUserID", mutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberMutedNotification filtered", "userID", userID, "mutedUserID", mutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
}
|
||||
} else if msg.ContentType == constant.GroupMemberCancelMutedNotification {
|
||||
var tips sdkws.GroupMemberCancelMutedTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberCancelMutedTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
cancelMutedUserID := tips.MutedUser.UserID
|
||||
if isOwnerOrAdmin || userID == cancelMutedUserID {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberCancelMutedNotification allowed", "userID", userID, "cancelMutedUserID", cancelMutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberCancelMutedNotification filtered", "userID", userID, "cancelMutedUserID", cancelMutedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
}
|
||||
} else if msg.ContentType == constant.MemberQuitNotification {
|
||||
var tips sdkws.MemberQuitTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberQuitTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
quitUserID := tips.QuitUser.UserID
|
||||
// 退出群聊通知只允许群主和管理员看到,退出者本人不通知
|
||||
if isOwnerOrAdmin {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberQuitNotification allowed", "userID", userID, "quitUserID", quitUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberQuitNotification filtered", "userID", userID, "quitUserID", quitUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
}
|
||||
} else if msg.ContentType == constant.MemberInvitedNotification {
|
||||
var tips sdkws.MemberInvitedTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberInvitedTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
// 获取被邀请的用户ID列表
|
||||
invitedUserIDs := datautil.Slice(tips.InvitedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })
|
||||
isInvitedUser := datautil.Contain(userID, invitedUserIDs...)
|
||||
// 邀请入群通知:允许群主、管理员和被邀请的用户本人看到
|
||||
if isOwnerOrAdmin || isInvitedUser {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberInvitedNotification allowed", "userID", userID, "invitedUserIDs", invitedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isInvitedUser", isInvitedUser)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberInvitedNotification filtered", "userID", userID, "invitedUserIDs", invitedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isInvitedUser", isInvitedUser)
|
||||
}
|
||||
} else if msg.ContentType == constant.MemberEnterNotification {
|
||||
var tips sdkws.MemberEnterTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberEnterTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
entrantUserID := tips.EntrantUser.UserID
|
||||
// 进入群聊通知只允许群主和管理员看到
|
||||
if isOwnerOrAdmin {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberEnterNotification allowed", "userID", userID, "entrantUserID", entrantUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberEnterNotification filtered", "userID", userID, "entrantUserID", entrantUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
}
|
||||
} else if msg.ContentType == constant.MemberKickedNotification {
|
||||
var tips sdkws.MemberKickedTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal MemberKickedTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
// 获取被踢出的用户ID列表
|
||||
kickedUserIDs := datautil.Slice(tips.KickedUserList, func(e *sdkws.GroupMemberFullInfo) string { return e.UserID })
|
||||
isKickedUser := datautil.Contain(userID, kickedUserIDs...)
|
||||
// 被踢出群聊通知:允许群主、管理员和被踢出的用户本人看到
|
||||
if isOwnerOrAdmin || isKickedUser {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberKickedNotification allowed", "userID", userID, "kickedUserIDs", kickedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isKickedUser", isKickedUser)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: MemberKickedNotification filtered", "userID", userID, "kickedUserIDs", kickedUserIDs, "isOwnerOrAdmin", isOwnerOrAdmin, "isKickedUser", isKickedUser)
|
||||
}
|
||||
} else if msg.ContentType == constant.GroupMemberInfoSetNotification {
|
||||
var tips sdkws.GroupMemberInfoSetTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberInfoSetTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
changedUserID := tips.ChangedUser.UserID
|
||||
// 群成员信息设置通知(如背景音)只允许群主和管理员看到
|
||||
if isOwnerOrAdmin {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberInfoSetNotification allowed", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberInfoSetNotification filtered", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin)
|
||||
}
|
||||
} else if msg.ContentType == constant.GroupMemberSetToAdminNotification || msg.ContentType == constant.GroupMemberSetToOrdinaryUserNotification {
|
||||
var tips sdkws.GroupMemberInfoSetTips
|
||||
if err := unmarshalNotificationContent(string(msg.Content), &tips); err != nil {
|
||||
log.ZWarn(ctx, "filterMuteNotificationMsgs: unmarshal GroupMemberInfoSetTips failed", err)
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
continue
|
||||
}
|
||||
changedUserID := tips.ChangedUser.UserID
|
||||
// 设置为管理员/普通用户通知:允许群主、管理员和本人看到
|
||||
if isOwnerOrAdmin || userID == changedUserID {
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberSetToAdmin/OrdinaryUserNotification allowed", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin, "contentType", msg.ContentType)
|
||||
} else {
|
||||
log.ZDebug(ctx, "filterMuteNotificationMsgs: GroupMemberSetToAdmin/OrdinaryUserNotification filtered", "userID", userID, "changedUserID", changedUserID, "isOwnerOrAdmin", isOwnerOrAdmin, "contentType", msg.ContentType)
|
||||
}
|
||||
} else {
|
||||
// 其他消息直接通过
|
||||
filteredMsgs = append(filteredMsgs, msg)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredMsgs
|
||||
}
|
||||
|
||||
// unmarshalNotificationContent 解析通知消息内容
|
||||
func unmarshalNotificationContent(content string, v interface{}) error {
|
||||
var notificationElem sdkws.NotificationElem
|
||||
if err := json.Unmarshal([]byte(content), ¬ificationElem); err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonutil.JsonUnmarshal([]byte(notificationElem.Detail), v)
|
||||
}
|
||||
|
||||
// enrichRedPacketMessages 填充红包消息的领取信息和状态
|
||||
func (m *msgServer) enrichRedPacketMessages(ctx context.Context, userID string, msgs []*sdkws.MsgData) []*sdkws.MsgData {
|
||||
if m.redPacketReceiveDB == nil || m.redPacketDB == nil {
|
||||
return msgs
|
||||
}
|
||||
|
||||
for _, msg := range msgs {
|
||||
// 只处理自定义消息类型
|
||||
if msg.ContentType != constant.Custom {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析自定义消息内容
|
||||
var customElem apistruct.CustomElem
|
||||
if err := json.Unmarshal(msg.Content, &customElem); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否为红包消息(通过description字段判断二级类型)
|
||||
if customElem.Description != "redpacket" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析红包消息内容(从data字段中解析)
|
||||
var redPacketElem apistruct.RedPacketElem
|
||||
if err := json.Unmarshal([]byte(customElem.Data), &redPacketElem); err != nil {
|
||||
log.ZWarn(ctx, "enrichRedPacketMessages: failed to unmarshal red packet data", err, "redPacketID", redPacketElem.RedPacketID)
|
||||
continue
|
||||
}
|
||||
|
||||
// 查询红包记录
|
||||
redPacket, err := m.redPacketDB.Take(ctx, redPacketElem.RedPacketID)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "enrichRedPacketMessages: failed to get red packet", err, "redPacketID", redPacketElem.RedPacketID)
|
||||
// 如果查询失败,保持原有状态,不填充信息
|
||||
continue
|
||||
}
|
||||
|
||||
// 填充红包状态信息
|
||||
redPacketElem.Status = redPacket.Status
|
||||
|
||||
// 判断是否已过期(检查过期时间和状态)
|
||||
now := time.Now()
|
||||
isExpired := redPacket.Status == model.RedPacketStatusExpired || (redPacket.ExpireTime.Before(now) && redPacket.Status == model.RedPacketStatusActive)
|
||||
redPacketElem.IsExpired = isExpired
|
||||
|
||||
// 判断是否已领完
|
||||
isFinished := redPacket.Status == model.RedPacketStatusFinished || redPacket.RemainCount <= 0
|
||||
redPacketElem.IsFinished = isFinished
|
||||
|
||||
// 如果已过期,更新状态
|
||||
if isExpired && redPacket.Status == model.RedPacketStatusActive {
|
||||
redPacket.Status = model.RedPacketStatusExpired
|
||||
redPacketElem.Status = model.RedPacketStatusExpired
|
||||
}
|
||||
|
||||
// 查询用户是否已领取
|
||||
receive, err := m.redPacketReceiveDB.FindByUserAndRedPacketID(ctx, userID, redPacketElem.RedPacketID)
|
||||
if err != nil {
|
||||
// 如果查询失败或未找到记录,说明未领取
|
||||
redPacketElem.IsReceived = false
|
||||
redPacketElem.ReceiveInfo = nil
|
||||
} else {
|
||||
// 已领取,填充领取信息(包括金额)
|
||||
redPacketElem.IsReceived = true
|
||||
redPacketElem.ReceiveInfo = &apistruct.RedPacketReceiveInfo{
|
||||
Amount: receive.Amount,
|
||||
ReceiveTime: receive.ReceiveTime.UnixMilli(),
|
||||
IsLucky: false, // 已去掉手气最佳功能,始终返回 false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新自定义消息的data字段(包含领取信息和状态)
|
||||
redPacketData := jsonutil.StructToJsonString(redPacketElem)
|
||||
customElem.Data = redPacketData
|
||||
|
||||
// 重新序列化并更新消息内容
|
||||
newContent, err := json.Marshal(customElem)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "enrichRedPacketMessages: failed to marshal custom elem", err, "redPacketID", redPacketElem.RedPacketID)
|
||||
continue
|
||||
}
|
||||
msg.Content = newContent
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
func (m *msgServer) GetMaxSeq(ctx context.Context, req *sdkws.GetMaxSeqReq) (*sdkws.GetMaxSeqResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conversationIDs, err := m.ConversationLocalCache.GetConversationIDs(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, conversationID := range conversationIDs {
|
||||
conversationIDs = append(conversationIDs, conversationutil.GetNotificationConversationIDByConversationID(conversationID))
|
||||
}
|
||||
conversationIDs = append(conversationIDs, conversationutil.GetSelfNotificationConversationID(req.UserID))
|
||||
log.ZDebug(ctx, "GetMaxSeq", "conversationIDs", conversationIDs)
|
||||
maxSeqs, err := m.MsgDatabase.GetMaxSeqs(ctx, conversationIDs)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "GetMaxSeqs error", err, "conversationIDs", conversationIDs, "maxSeqs", maxSeqs)
|
||||
return nil, err
|
||||
}
|
||||
// avoid pulling messages from sessions with a large number of max seq values of 0
|
||||
for conversationID, seq := range maxSeqs {
|
||||
if seq == 0 {
|
||||
delete(maxSeqs, conversationID)
|
||||
}
|
||||
}
|
||||
resp := new(sdkws.GetMaxSeqResp)
|
||||
resp.MaxSeqs = maxSeqs
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) SearchMessage(ctx context.Context, req *msg.SearchMessageReq) (resp *msg.SearchMessageResp, err error) {
|
||||
// var chatLogs []*sdkws.MsgData
|
||||
var chatLogs []*msg.SearchedMsgData
|
||||
var total int64
|
||||
resp = &msg.SearchMessageResp{}
|
||||
if total, chatLogs, err = m.MsgDatabase.SearchMessage(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
sendIDs []string
|
||||
recvIDs []string
|
||||
groupIDs []string
|
||||
sendNameMap = make(map[string]string)
|
||||
sendFaceMap = make(map[string]string)
|
||||
recvMap = make(map[string]string)
|
||||
groupMap = make(map[string]*sdkws.GroupInfo)
|
||||
seenSendIDs = make(map[string]struct{})
|
||||
seenRecvIDs = make(map[string]struct{})
|
||||
seenGroupIDs = make(map[string]struct{})
|
||||
)
|
||||
|
||||
for _, chatLog := range chatLogs {
|
||||
if chatLog.MsgData.SenderNickname == "" || chatLog.MsgData.SenderFaceURL == "" {
|
||||
if _, ok := seenSendIDs[chatLog.MsgData.SendID]; !ok {
|
||||
seenSendIDs[chatLog.MsgData.SendID] = struct{}{}
|
||||
sendIDs = append(sendIDs, chatLog.MsgData.SendID)
|
||||
}
|
||||
}
|
||||
switch chatLog.MsgData.SessionType {
|
||||
case constant.SingleChatType, constant.NotificationChatType:
|
||||
if _, ok := seenRecvIDs[chatLog.MsgData.RecvID]; !ok {
|
||||
seenRecvIDs[chatLog.MsgData.RecvID] = struct{}{}
|
||||
recvIDs = append(recvIDs, chatLog.MsgData.RecvID)
|
||||
}
|
||||
case constant.WriteGroupChatType, constant.ReadGroupChatType:
|
||||
if _, ok := seenGroupIDs[chatLog.MsgData.GroupID]; !ok {
|
||||
seenGroupIDs[chatLog.MsgData.GroupID] = struct{}{}
|
||||
groupIDs = append(groupIDs, chatLog.MsgData.GroupID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve sender and receiver information
|
||||
if len(sendIDs) != 0 {
|
||||
sendInfos, err := m.UserLocalCache.GetUsersInfo(ctx, sendIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sendInfo := range sendInfos {
|
||||
sendNameMap[sendInfo.UserID] = sendInfo.Nickname
|
||||
sendFaceMap[sendInfo.UserID] = sendInfo.FaceURL
|
||||
}
|
||||
}
|
||||
|
||||
if len(recvIDs) != 0 {
|
||||
recvInfos, err := m.UserLocalCache.GetUsersInfo(ctx, recvIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, recvInfo := range recvInfos {
|
||||
recvMap[recvInfo.UserID] = recvInfo.Nickname
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve group information including member counts
|
||||
if len(groupIDs) != 0 {
|
||||
groupInfos, err := m.GroupLocalCache.GetGroupInfos(ctx, groupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, groupInfo := range groupInfos {
|
||||
groupMap[groupInfo.GroupID] = groupInfo
|
||||
// Get actual member count
|
||||
memberIDs, err := m.GroupLocalCache.GetGroupMemberIDs(ctx, groupInfo.GroupID)
|
||||
if err == nil {
|
||||
groupInfo.MemberCount = uint32(len(memberIDs)) // Update the member count with actual number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct response with updated information
|
||||
for _, chatLog := range chatLogs {
|
||||
pbchatLog := &msg.ChatLog{}
|
||||
datautil.CopyStructFields(pbchatLog, chatLog.MsgData)
|
||||
pbchatLog.SendTime = chatLog.MsgData.SendTime
|
||||
pbchatLog.CreateTime = chatLog.MsgData.CreateTime
|
||||
if chatLog.MsgData.SenderNickname == "" {
|
||||
pbchatLog.SenderNickname = sendNameMap[chatLog.MsgData.SendID]
|
||||
}
|
||||
if chatLog.MsgData.SenderFaceURL == "" {
|
||||
pbchatLog.SenderFaceURL = sendFaceMap[chatLog.MsgData.SendID]
|
||||
}
|
||||
switch chatLog.MsgData.SessionType {
|
||||
case constant.SingleChatType, constant.NotificationChatType:
|
||||
pbchatLog.RecvNickname = recvMap[chatLog.MsgData.RecvID]
|
||||
case constant.ReadGroupChatType:
|
||||
groupInfo := groupMap[chatLog.MsgData.GroupID]
|
||||
pbchatLog.GroupMemberCount = groupInfo.MemberCount // Reflects actual member count
|
||||
pbchatLog.RecvID = groupInfo.GroupID
|
||||
pbchatLog.GroupName = groupInfo.GroupName
|
||||
pbchatLog.GroupOwner = groupInfo.OwnerUserID
|
||||
pbchatLog.GroupType = groupInfo.GroupType
|
||||
}
|
||||
searchChatLog := &msg.SearchChatLog{ChatLog: pbchatLog, IsRevoked: chatLog.IsRevoked}
|
||||
|
||||
resp.ChatLogs = append(resp.ChatLogs, searchChatLog)
|
||||
}
|
||||
resp.ChatLogsNum = int32(total)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetServerTime(ctx context.Context, _ *msg.GetServerTimeReq) (*msg.GetServerTimeResp, error) {
|
||||
return &msg.GetServerTimeResp{ServerTime: timeutil.GetCurrentTimestampByMill()}, nil
|
||||
}
|
||||
|
||||
func (m *msgServer) GetLastMessage(ctx context.Context, req *msg.GetLastMessageReq) (*msg.GetLastMessageResp, error) {
|
||||
msgs, err := m.MsgDatabase.GetLastMessage(ctx, req.ConversationIDs, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &msg.GetLastMessageResp{Msgs: msgs}, nil
|
||||
}
|
||||
91
internal/rpc/msg/utils.go
Normal file
91
internal/rpc/msg/utils.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
switch errs.Unwrap(err) {
|
||||
case redis.Nil, mongo.ErrNoDocuments:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type activeConversations []*msg.ActiveConversation
|
||||
|
||||
func (s activeConversations) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s activeConversations) Less(i, j int) bool {
|
||||
return s[i].LastTime > s[j].LastTime
|
||||
}
|
||||
|
||||
func (s activeConversations) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
//type seqTime struct {
|
||||
// ConversationID string
|
||||
// Seq int64
|
||||
// Time int64
|
||||
// Unread int64
|
||||
// Pinned bool
|
||||
//}
|
||||
//
|
||||
//func (s seqTime) String() string {
|
||||
// return fmt.Sprintf("<Time_%d,Unread_%d,Pinned_%t>", s.Time, s.Unread, s.Pinned)
|
||||
//}
|
||||
//
|
||||
//type seqTimes []seqTime
|
||||
//
|
||||
//func (s seqTimes) Len() int {
|
||||
// return len(s)
|
||||
//}
|
||||
//
|
||||
//// Less sticky priority, unread priority, time descending
|
||||
//func (s seqTimes) Less(i, j int) bool {
|
||||
// iv, jv := s[i], s[j]
|
||||
// if iv.Pinned && (!jv.Pinned) {
|
||||
// return true
|
||||
// }
|
||||
// if jv.Pinned && (!iv.Pinned) {
|
||||
// return false
|
||||
// }
|
||||
// if iv.Unread > 0 && jv.Unread == 0 {
|
||||
// return true
|
||||
// }
|
||||
// if jv.Unread > 0 && iv.Unread == 0 {
|
||||
// return false
|
||||
// }
|
||||
// return iv.Time > jv.Time
|
||||
//}
|
||||
//
|
||||
//func (s seqTimes) Swap(i, j int) {
|
||||
// s[i], s[j] = s[j], s[i]
|
||||
//}
|
||||
//
|
||||
//type conversationStatus struct {
|
||||
// ConversationID string
|
||||
// Pinned bool
|
||||
// Recv bool
|
||||
//}
|
||||
405
internal/rpc/msg/verify.go
Normal file
405
internal/rpc/msg/verify.go
Normal file
@@ -0,0 +1,405 @@
|
||||
// Copyright © 2023 OpenIM. 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 msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/openimsdk/tools/utils/encrypt"
|
||||
"github.com/openimsdk/tools/utils/timeutil"
|
||||
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
var ExcludeContentType = []int{constant.HasReadReceipt}
|
||||
|
||||
// URL正则表达式,匹配常见的URL格式
|
||||
// 匹配以下格式的链接:
|
||||
// 1. http:// 或 https:// 开头的完整URL
|
||||
// 2. // 开头的协议相对URL(如 //s.yam.com/MvQzr)
|
||||
// 3. www. 开头的链接
|
||||
// 4. 直接包含域名的链接(如 s.yam.com/MvQzr、xxx.cc/csd、t.cn/AX4fYkFZ),匹配包含至少一个点的域名格式,后面可跟路径
|
||||
// 域名格式:至少包含一个点和一个2位以上的顶级域名,如 xxx.com、xxx.cn、s.yam.com、xxx.cc、t.cn 等
|
||||
// 注意:匹配 // 后面必须跟着域名格式,避免误匹配其他 // 开头的文本
|
||||
// 修复:支持单字符域名(如 t.cn),移除了点之前必须有两个字符的限制
|
||||
var urlRegex = regexp.MustCompile(`(?i)(https?://[^\s<>"{}|\\^` + "`" + `\[\]]+|//[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}(/[^\s<>"{}|\\^` + "`" + `\[\]]*)?|www\.[^\s<>"{}|\\^` + "`" + `\[\]]+|[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}(/[^\s<>"{}|\\^` + "`" + `\[\]]*)?)`)
|
||||
|
||||
type Validator interface {
|
||||
validate(pb *msg.SendMsgReq) (bool, int32, string)
|
||||
}
|
||||
|
||||
// TextElem 用于解析文本消息内容
|
||||
type TextElem struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// AtElem 用于解析@消息内容
|
||||
type AtElem struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// checkMessageContainsLink 检测消息内容中是否包含链接
|
||||
// userType: 0-普通用户(不能发送链接),1-特殊用户(可以发送链接)
|
||||
func (m *msgServer) checkMessageContainsLink(msgData *sdkws.MsgData, userType int32) error {
|
||||
// userType=1 的用户可以发送链接,不进行检测
|
||||
if userType == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 只检测文本类型的消息
|
||||
if msgData.ContentType != constant.Text && msgData.ContentType != constant.AtText {
|
||||
return nil
|
||||
}
|
||||
|
||||
var textContent string
|
||||
var err error
|
||||
|
||||
// 解析消息内容
|
||||
if msgData.ContentType == constant.Text {
|
||||
var textElem TextElem
|
||||
if err = json.Unmarshal(msgData.Content, &textElem); err != nil {
|
||||
// 如果解析失败,尝试直接使用字符串
|
||||
textContent = string(msgData.Content)
|
||||
} else {
|
||||
textContent = textElem.Content
|
||||
}
|
||||
} else if msgData.ContentType == constant.AtText {
|
||||
var atElem AtElem
|
||||
if err = json.Unmarshal(msgData.Content, &atElem); err != nil {
|
||||
// 如果解析失败,尝试直接使用字符串
|
||||
textContent = string(msgData.Content)
|
||||
} else {
|
||||
textContent = atElem.Text
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否包含链接
|
||||
if urlRegex.MatchString(textContent) {
|
||||
return servererrs.ErrMessageContainsLink.WrapMsg("userType=0的用户不能发送包含链接的消息")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type MessageRevoked struct {
|
||||
RevokerID string `json:"revokerID"`
|
||||
RevokerRole int32 `json:"revokerRole"`
|
||||
ClientMsgID string `json:"clientMsgID"`
|
||||
RevokerNickname string `json:"revokerNickname"`
|
||||
RevokeTime int64 `json:"revokeTime"`
|
||||
SourceMessageSendTime int64 `json:"sourceMessageSendTime"`
|
||||
SourceMessageSendID string `json:"sourceMessageSendID"`
|
||||
SourceMessageSenderNickname string `json:"sourceMessageSenderNickname"`
|
||||
SessionType int32 `json:"sessionType"`
|
||||
Seq uint32 `json:"seq"`
|
||||
}
|
||||
|
||||
func (m *msgServer) messageVerification(ctx context.Context, data *msg.SendMsgReq) error {
|
||||
webhookCfg := m.webhookConfig()
|
||||
|
||||
switch data.MsgData.SessionType {
|
||||
case constant.SingleChatType:
|
||||
if datautil.Contain(data.MsgData.SendID, m.adminUserIDs...) {
|
||||
return nil
|
||||
}
|
||||
if data.MsgData.ContentType <= constant.NotificationEnd &&
|
||||
data.MsgData.ContentType >= constant.NotificationBegin {
|
||||
return nil
|
||||
}
|
||||
if err := m.webhookBeforeSendSingleMsg(ctx, &webhookCfg.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
|
||||
}
|
||||
// userType=1 的用户可以发送链接和二维码,不进行检测
|
||||
userType := u.GetUserType()
|
||||
|
||||
if userType != 1 {
|
||||
// 检测userType=0的用户是否发送了包含链接的消息
|
||||
if err := m.checkMessageContainsLink(data.MsgData, userType); err != nil {
|
||||
return err
|
||||
}
|
||||
// 检测userType=0的用户是否发送了包含二维码的图片
|
||||
if err := m.checkImageContainsQRCode(ctx, data.MsgData, userType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 单聊中:不限制文件发送权限,所有用户都可以发送文件
|
||||
|
||||
black, err := m.FriendLocalCache.IsBlack(ctx, data.MsgData.SendID, data.MsgData.RecvID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if black {
|
||||
return servererrs.ErrBlockedByPeer.Wrap()
|
||||
}
|
||||
if m.config.RpcConfig.FriendVerify {
|
||||
friend, err := m.FriendLocalCache.IsFriend(ctx, data.MsgData.SendID, data.MsgData.RecvID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !friend {
|
||||
return servererrs.ErrNotPeersFriend.Wrap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case constant.ReadGroupChatType:
|
||||
groupInfo, err := m.GroupLocalCache.GetGroupInfo(ctx, data.MsgData.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if groupInfo.Status == constant.GroupStatusDismissed &&
|
||||
data.MsgData.ContentType != constant.GroupDismissedNotification {
|
||||
return servererrs.ErrDismissedAlready.Wrap()
|
||||
}
|
||||
// 检查是否为系统管理员,系统管理员跳过部分检查
|
||||
isSystemAdmin := datautil.Contain(data.MsgData.SendID, m.adminUserIDs...)
|
||||
|
||||
// 通知消息类型跳过检查
|
||||
if data.MsgData.ContentType <= constant.NotificationEnd &&
|
||||
data.MsgData.ContentType >= constant.NotificationBegin {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SuperGroup 跳过部分检查,但仍需检查文件权限
|
||||
if groupInfo.GroupType == constant.SuperGroup {
|
||||
// SuperGroup 也需要检查文件发送权限
|
||||
if data.MsgData.ContentType == constant.File {
|
||||
if isSystemAdmin {
|
||||
// 系统管理员可以发送文件
|
||||
return nil
|
||||
}
|
||||
memberIDs, err := m.GroupLocalCache.GetGroupMemberIDMap(ctx, data.MsgData.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := memberIDs[data.MsgData.SendID]; !ok {
|
||||
return servererrs.ErrNotInGroupYet.Wrap()
|
||||
}
|
||||
groupMemberInfo, err := m.GroupLocalCache.GetGroupMember(ctx, data.MsgData.GroupID, data.MsgData.SendID)
|
||||
if err != nil {
|
||||
if errs.ErrRecordNotFound.Is(err) {
|
||||
return servererrs.ErrNotInGroupYet.WrapMsg(err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
u, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isGroupOwner := groupMemberInfo.RoleLevel == constant.GroupOwner
|
||||
isGroupAdmin := groupMemberInfo.RoleLevel == constant.GroupAdmin
|
||||
canSendFile := u.GetUserType() == 1 || isGroupOwner || isGroupAdmin
|
||||
if !canSendFile {
|
||||
return servererrs.ErrNoPermission.WrapMsg("only group owner, admin, or userType=1 can send files in group chat")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 先获取用户信息,用于检查userType(系统管理员和userType=1的用户可以发送文件)
|
||||
memberIDs, err := m.GroupLocalCache.GetGroupMemberIDMap(ctx, data.MsgData.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := memberIDs[data.MsgData.SendID]; !ok {
|
||||
return servererrs.ErrNotInGroupYet.Wrap()
|
||||
}
|
||||
|
||||
groupMemberInfo, err := m.GroupLocalCache.GetGroupMember(ctx, data.MsgData.GroupID, data.MsgData.SendID)
|
||||
if err != nil {
|
||||
if errs.ErrRecordNotFound.Is(err) {
|
||||
return servererrs.ErrNotInGroupYet.WrapMsg(err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
// 获取用户信息以获取userType
|
||||
u, err := m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 群聊中:检查文件发送权限(优先检查,确保不会被其他逻辑跳过)
|
||||
// 只有群主、管理员、userType=1的用户可以发送文件
|
||||
// 系统管理员也可以发送文件
|
||||
if data.MsgData.ContentType == constant.File {
|
||||
isGroupOwner := groupMemberInfo.RoleLevel == constant.GroupOwner
|
||||
isGroupAdmin := groupMemberInfo.RoleLevel == constant.GroupAdmin
|
||||
userType := u.GetUserType()
|
||||
|
||||
// 如果是文件消息且 userType=0 且不是群主/管理员,可能是缓存问题,尝试清除缓存并重新获取
|
||||
if userType == 0 && !isGroupOwner && !isGroupAdmin && !isSystemAdmin {
|
||||
// 清除本地缓存
|
||||
m.UserLocalCache.DelUserInfo(ctx, data.MsgData.SendID)
|
||||
// 重新获取用户信息
|
||||
u, err = m.UserLocalCache.GetUserInfo(ctx, data.MsgData.SendID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userType = u.GetUserType()
|
||||
}
|
||||
|
||||
canSendFile := isSystemAdmin || userType == 1 || isGroupOwner || isGroupAdmin
|
||||
if !canSendFile {
|
||||
return servererrs.ErrNoPermission.WrapMsg("only group owner, admin, or userType=1 can send files in group chat")
|
||||
}
|
||||
}
|
||||
|
||||
if isSystemAdmin {
|
||||
// 系统管理员跳过大部分检查
|
||||
return nil
|
||||
}
|
||||
|
||||
// 群聊中:userType=1、群主、群管理员可以发送链接
|
||||
isGroupOwner := groupMemberInfo.RoleLevel == constant.GroupOwner
|
||||
isGroupAdmin := groupMemberInfo.RoleLevel == constant.GroupAdmin
|
||||
canSendLink := u.GetUserType() == 1 || isGroupOwner || isGroupAdmin
|
||||
|
||||
// 如果不符合发送链接的条件,进行链接检测
|
||||
if !canSendLink {
|
||||
if err := m.checkMessageContainsLink(data.MsgData, u.GetUserType()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 群聊中:检测userType=0的普通成员是否发送了包含二维码的图片
|
||||
// userType=1、群主、群管理员可以发送包含二维码的图片,不进行检测
|
||||
if !canSendLink {
|
||||
if err := m.checkImageContainsQRCode(ctx, data.MsgData, u.GetUserType()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isGroupOwner {
|
||||
return nil
|
||||
} else {
|
||||
nowUnixMilli := time.Now().UnixMilli()
|
||||
// 记录禁言检查信息,用于排查自动禁言问题
|
||||
if groupMemberInfo.MuteEndTime > 0 {
|
||||
muteEndTime := time.UnixMilli(groupMemberInfo.MuteEndTime)
|
||||
isMuted := groupMemberInfo.MuteEndTime >= nowUnixMilli
|
||||
log.ZInfo(ctx, "messageVerification: checking mute status",
|
||||
"groupID", data.MsgData.GroupID,
|
||||
"userID", data.MsgData.SendID,
|
||||
"muteEndTimeTimestamp", groupMemberInfo.MuteEndTime,
|
||||
"muteEndTime", muteEndTime.Format(time.RFC3339),
|
||||
"now", time.UnixMilli(nowUnixMilli).Format(time.RFC3339),
|
||||
"isMuted", isMuted,
|
||||
"mutedDurationSeconds", (groupMemberInfo.MuteEndTime-nowUnixMilli)/1000,
|
||||
"roleLevel", groupMemberInfo.RoleLevel)
|
||||
}
|
||||
if groupMemberInfo.MuteEndTime >= nowUnixMilli {
|
||||
return servererrs.ErrMutedInGroup.Wrap()
|
||||
}
|
||||
if groupInfo.Status == constant.GroupStatusMuted && !isGroupAdmin {
|
||||
return servererrs.ErrMutedGroup.Wrap()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *msgServer) encapsulateMsgData(msg *sdkws.MsgData) {
|
||||
msg.ServerMsgID = GetMsgID(msg.SendID)
|
||||
if msg.SendTime == 0 {
|
||||
msg.SendTime = timeutil.GetCurrentTimestampByMill()
|
||||
}
|
||||
switch msg.ContentType {
|
||||
case constant.Text, constant.Picture, constant.Voice, constant.Video,
|
||||
constant.File, constant.AtText, constant.Merger, constant.Card,
|
||||
constant.Location, constant.Custom, constant.Quote, constant.AdvancedText, constant.MarkdownText:
|
||||
case constant.Revoke:
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)
|
||||
case constant.HasReadReceipt:
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsConversationUpdate, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsSenderConversationUpdate, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)
|
||||
case constant.Typing:
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsHistory, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsPersistent, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsSenderSync, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsConversationUpdate, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsSenderConversationUpdate, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsUnreadCount, false)
|
||||
datautil.SetSwitchFromOptions(msg.Options, constant.IsOfflinePush, false)
|
||||
}
|
||||
}
|
||||
|
||||
func GetMsgID(sendID string) string {
|
||||
t := timeutil.GetCurrentTimeFormatted()
|
||||
return encrypt.Md5(t + "-" + sendID + "-" + strconv.Itoa(rand.Int()))
|
||||
}
|
||||
|
||||
func (m *msgServer) modifyMessageByUserMessageReceiveOpt(ctx context.Context, userID, conversationID string, sessionType int, pb *msg.SendMsgReq) (bool, error) {
|
||||
opt, err := m.UserLocalCache.GetUserGlobalMsgRecvOpt(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch opt {
|
||||
case constant.ReceiveMessage:
|
||||
case constant.NotReceiveMessage:
|
||||
return false, nil
|
||||
case constant.ReceiveNotNotifyMessage:
|
||||
if pb.MsgData.Options == nil {
|
||||
pb.MsgData.Options = make(map[string]bool, 10)
|
||||
}
|
||||
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
|
||||
return true, nil
|
||||
}
|
||||
singleOpt, err := m.ConversationLocalCache.GetSingleConversationRecvMsgOpt(ctx, userID, conversationID)
|
||||
if errs.ErrRecordNotFound.Is(err) {
|
||||
return true, nil
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch singleOpt {
|
||||
case constant.ReceiveMessage:
|
||||
return true, nil
|
||||
case constant.NotReceiveMessage:
|
||||
if datautil.Contain(int(pb.MsgData.ContentType), ExcludeContentType...) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
case constant.ReceiveNotNotifyMessage:
|
||||
if pb.MsgData.Options == nil {
|
||||
pb.MsgData.Options = make(map[string]bool, 10)
|
||||
}
|
||||
datautil.SetSwitchFromOptions(pb.MsgData.Options, constant.IsOfflinePush, false)
|
||||
return true, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
165
internal/rpc/relation/black.go
Normal file
165
internal/rpc/relation/black.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright © 2023 OpenIM. 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 relation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/protocol/relation"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
func (s *friendServer) GetPaginationBlacks(ctx context.Context, req *relation.GetPaginationBlacksReq) (resp *relation.GetPaginationBlacksResp, err error) {
|
||||
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
|
||||
}
|
||||
resp = &relation.GetPaginationBlacksResp{}
|
||||
resp.Blacks, err = convert.BlackDB2Pb(ctx, blacks, s.userClient.GetUsersInfoMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Total = int32(total)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
resp := &relation.IsBlackResp{}
|
||||
resp.InUser1Blacks = in1
|
||||
resp.InUser2Blacks = in2
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) RemoveBlack(ctx context.Context, req *relation.RemoveBlackReq) (*relation.RemoveBlackResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.blackDatabase.Delete(ctx, []*model.Black{{OwnerUserID: req.OwnerUserID, BlockUserID: req.BlackUserID}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.notificationSender.BlackDeletedNotification(ctx, req)
|
||||
s.webhookAfterRemoveBlack(ctx, &s.config.WebhooksConfig.AfterRemoveBlack, req)
|
||||
|
||||
return &relation.RemoveBlackResp{}, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) AddBlack(ctx context.Context, req *relation.AddBlackReq) (*relation.AddBlackResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.webhookBeforeAddBlack(ctx, &s.config.WebhooksConfig.BeforeAddBlack, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.userClient.CheckUser(ctx, []string{req.OwnerUserID, req.BlackUserID}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
black := model.Black{
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
BlockUserID: req.BlackUserID,
|
||||
OperatorUserID: mcontext.GetOpUserID(ctx),
|
||||
CreateTime: time.Now(),
|
||||
Ex: req.Ex,
|
||||
}
|
||||
|
||||
if err := s.blackDatabase.Create(ctx, []*model.Black{&black}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.notificationSender.BlackAddedNotification(ctx, req)
|
||||
return &relation.AddBlackResp{}, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) GetSpecifiedBlacks(ctx context.Context, req *relation.GetSpecifiedBlacksReq) (*relation.GetSpecifiedBlacksResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(req.UserIDList) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("userIDList is empty")
|
||||
}
|
||||
|
||||
if datautil.Duplicate(req.UserIDList) {
|
||||
return nil, errs.ErrArgs.WrapMsg("userIDList repeated")
|
||||
}
|
||||
|
||||
userMap, err := s.userClient.GetUsersInfoMap(ctx, req.UserIDList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blacks, err := s.blackDatabase.FindBlackInfos(ctx, req.OwnerUserID, req.UserIDList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blackMap := datautil.SliceToMap(blacks, func(e *model.Black) string {
|
||||
return e.BlockUserID
|
||||
})
|
||||
|
||||
resp := &relation.GetSpecifiedBlacksResp{
|
||||
Blacks: make([]*sdkws.BlackInfo, 0, len(req.UserIDList)),
|
||||
}
|
||||
|
||||
toPublcUser := func(userID string) *sdkws.PublicUserInfo {
|
||||
v, ok := userMap[userID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &sdkws.PublicUserInfo{
|
||||
UserID: v.UserID,
|
||||
Nickname: v.Nickname,
|
||||
FaceURL: v.FaceURL,
|
||||
Ex: v.Ex,
|
||||
UserType: v.UserType,
|
||||
}
|
||||
}
|
||||
|
||||
for _, userID := range req.UserIDList {
|
||||
if black := blackMap[userID]; black != nil {
|
||||
resp.Blacks = append(resp.Blacks,
|
||||
&sdkws.BlackInfo{
|
||||
OwnerUserID: black.OwnerUserID,
|
||||
CreateTime: black.CreateTime.UnixMilli(),
|
||||
BlackUserInfo: toPublcUser(userID),
|
||||
AddSource: black.AddSource,
|
||||
OperatorUserID: black.OperatorUserID,
|
||||
Ex: black.Ex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resp.Total = int32(len(resp.Blacks))
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
169
internal/rpc/relation/callback.go
Normal file
169
internal/rpc/relation/callback.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright © 2023 OpenIM. 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 relation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
|
||||
cbapi "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/protocol/relation"
|
||||
)
|
||||
|
||||
func (s *friendServer) webhookAfterDeleteFriend(ctx context.Context, after *config.AfterConfig, req *relation.DeleteFriendReq) {
|
||||
cbReq := &cbapi.CallbackAfterDeleteFriendReq{
|
||||
CallbackCommand: cbapi.CallbackAfterDeleteFriendCommand,
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
FriendUserID: req.FriendUserID,
|
||||
}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterDeleteFriendResp{}, after)
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookBeforeAddFriend(ctx context.Context, before *config.BeforeConfig, req *relation.ApplyToAddFriendReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeAddFriendReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeAddFriendCommand,
|
||||
FromUserID: req.FromUserID,
|
||||
ToUserID: req.ToUserID,
|
||||
ReqMsg: req.ReqMsg,
|
||||
Ex: req.Ex,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeAddFriendResp{}
|
||||
|
||||
if err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookAfterAddFriend(ctx context.Context, after *config.AfterConfig, req *relation.ApplyToAddFriendReq) {
|
||||
cbReq := &cbapi.CallbackAfterAddFriendReq{
|
||||
CallbackCommand: cbapi.CallbackAfterAddFriendCommand,
|
||||
FromUserID: req.FromUserID,
|
||||
ToUserID: req.ToUserID,
|
||||
ReqMsg: req.ReqMsg,
|
||||
}
|
||||
resp := &cbapi.CallbackAfterAddFriendResp{}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookAfterSetFriendRemark(ctx context.Context, after *config.AfterConfig, req *relation.SetFriendRemarkReq) {
|
||||
cbReq := &cbapi.CallbackAfterSetFriendRemarkReq{
|
||||
CallbackCommand: cbapi.CallbackAfterSetFriendRemarkCommand,
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
FriendUserID: req.FriendUserID,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
resp := &cbapi.CallbackAfterSetFriendRemarkResp{}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookAfterImportFriends(ctx context.Context, after *config.AfterConfig, req *relation.ImportFriendReq) {
|
||||
cbReq := &cbapi.CallbackAfterImportFriendsReq{
|
||||
CallbackCommand: cbapi.CallbackAfterImportFriendsCommand,
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
FriendUserIDs: req.FriendUserIDs,
|
||||
}
|
||||
resp := &cbapi.CallbackAfterImportFriendsResp{}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookAfterRemoveBlack(ctx context.Context, after *config.AfterConfig, req *relation.RemoveBlackReq) {
|
||||
cbReq := &cbapi.CallbackAfterRemoveBlackReq{
|
||||
CallbackCommand: cbapi.CallbackAfterRemoveBlackCommand,
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
BlackUserID: req.BlackUserID,
|
||||
}
|
||||
resp := &cbapi.CallbackAfterRemoveBlackResp{}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookBeforeSetFriendRemark(ctx context.Context, before *config.BeforeConfig, req *relation.SetFriendRemarkReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeSetFriendRemarkReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeSetFriendRemarkCommand,
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
FriendUserID: req.FriendUserID,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeSetFriendRemarkResp{}
|
||||
if err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Remark != "" {
|
||||
req.Remark = resp.Remark
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookBeforeAddBlack(ctx context.Context, before *config.BeforeConfig, req *relation.AddBlackReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeAddBlackReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeAddBlackCommand,
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
BlackUserID: req.BlackUserID,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeAddBlackResp{}
|
||||
return s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookBeforeAddFriendAgree(ctx context.Context, before *config.BeforeConfig, req *relation.RespondFriendApplyReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeAddFriendAgreeReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeAddFriendAgreeCommand,
|
||||
FromUserID: req.FromUserID,
|
||||
ToUserID: req.ToUserID,
|
||||
HandleMsg: req.HandleMsg,
|
||||
HandleResult: req.HandleResult,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeAddFriendAgreeResp{}
|
||||
return s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookAfterAddFriendAgree(ctx context.Context, after *config.AfterConfig, req *relation.RespondFriendApplyReq) {
|
||||
cbReq := &cbapi.CallbackAfterAddFriendAgreeReq{
|
||||
CallbackCommand: cbapi.CallbackAfterAddFriendAgreeCommand,
|
||||
FromUserID: req.FromUserID,
|
||||
ToUserID: req.ToUserID,
|
||||
HandleMsg: req.HandleMsg,
|
||||
HandleResult: req.HandleResult,
|
||||
}
|
||||
resp := &cbapi.CallbackAfterAddFriendAgreeResp{}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, after)
|
||||
}
|
||||
|
||||
func (s *friendServer) webhookBeforeImportFriends(ctx context.Context, before *config.BeforeConfig, req *relation.ImportFriendReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeImportFriendsReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeImportFriendsCommand,
|
||||
OwnerUserID: req.OwnerUserID,
|
||||
FriendUserIDs: req.FriendUserIDs,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeImportFriendsResp{}
|
||||
if err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resp.FriendUserIDs) > 0 {
|
||||
req.FriendUserIDs = resp.FriendUserIDs
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
597
internal/rpc/relation/friend.go
Normal file
597
internal/rpc/relation/friend.go
Normal file
@@ -0,0 +1,597 @@
|
||||
// Copyright © 2023 OpenIM. 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 relation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification/common_user"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
|
||||
"github.com/openimsdk/tools/mq/memamq"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/localcache"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/relation"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type friendServer struct {
|
||||
relation.UnimplementedFriendServer
|
||||
db controller.FriendDatabase
|
||||
blackDatabase controller.BlackDatabase
|
||||
notificationSender *FriendNotificationSender
|
||||
RegisterCenter discovery.Conn
|
||||
config *Config
|
||||
webhookClient *webhook.Client
|
||||
queue *memamq.MemoryQueue
|
||||
userClient *rpcli.UserClient
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Friend
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
// ZookeeperConfig config.ZooKeeper
|
||||
NotificationConfig config.Notification
|
||||
Share config.Share
|
||||
WebhooksConfig config.Webhooks
|
||||
LocalCacheConfig config.LocalCache
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
rdb, err := dbb.Redis(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
friendMongoDB, err := mgo.NewFriendMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
friendRequestMongoDB, err := mgo.NewFriendRequestMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blackMongoDB, err := mgo.NewBlackMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgConn, err := client.GetConn(ctx, config.Discovery.RpcService.Msg)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
// 初始化webhook配置管理器(支持从数据库读取配置)
|
||||
var webhookClient *webhook.Client
|
||||
systemConfigDB, err := mgo.NewSystemConfigMongo(mgocli.GetDB())
|
||||
if err == nil {
|
||||
// 如果SystemConfig数据库初始化成功,使用配置管理器
|
||||
webhookConfigManager := webhook.NewConfigManager(systemConfigDB, &config.WebhooksConfig)
|
||||
if err := webhookConfigManager.Start(ctx); err != nil {
|
||||
log.ZWarn(ctx, "failed to start webhook config manager, using default config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
} else {
|
||||
webhookClient = webhook.NewWebhookClientWithManager(webhookConfigManager)
|
||||
}
|
||||
} else {
|
||||
// 如果SystemConfig数据库初始化失败,使用默认配置
|
||||
log.ZWarn(ctx, "failed to init system config db, using default webhook config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
}
|
||||
|
||||
// Register Friend server with refactored MongoDB and Redis integrations
|
||||
relation.RegisterFriendServer(server, &friendServer{
|
||||
db: database,
|
||||
blackDatabase: controller.NewBlackDatabase(
|
||||
blackMongoDB,
|
||||
redis.NewBlackCacheRedis(rdb, &config.LocalCacheConfig, blackMongoDB),
|
||||
),
|
||||
notificationSender: notificationSender,
|
||||
RegisterCenter: client,
|
||||
config: config,
|
||||
webhookClient: webhookClient,
|
||||
queue: memamq.NewMemoryQueue(16, 1024*1024),
|
||||
userClient: userClient,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ok.
|
||||
func (s *friendServer) ApplyToAddFriend(ctx context.Context, req *relation.ApplyToAddFriendReq) (resp *relation.ApplyToAddFriendResp, err error) {
|
||||
resp = &relation.ApplyToAddFriendResp{}
|
||||
if err := authverify.CheckAccess(ctx, req.FromUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.ToUserID == req.FromUserID {
|
||||
return nil, servererrs.ErrCanNotAddYourself.WrapMsg("req.ToUserID", req.ToUserID)
|
||||
}
|
||||
if err = s.webhookBeforeAddFriend(ctx, &s.config.WebhooksConfig.BeforeAddFriend, req); err != nil && err != servererrs.ErrCallbackContinue {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.userClient.CheckUser(ctx, []string{req.ToUserID, req.FromUserID}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
in1, in2, err := s.db.CheckIn(ctx, req.FromUserID, req.ToUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in1 && in2 {
|
||||
return nil, servererrs.ErrRelationshipAlready.WrapMsg("already friends has f")
|
||||
}
|
||||
if err = s.db.AddFriendRequest(ctx, req.FromUserID, req.ToUserID, req.ReqMsg, req.Ex); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.notificationSender.FriendApplicationAddNotification(ctx, req)
|
||||
s.webhookAfterAddFriend(ctx, &s.config.WebhooksConfig.AfterAddFriend, req)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ok.
|
||||
func (s *friendServer) ImportFriends(ctx context.Context, req *relation.ImportFriendReq) (resp *relation.ImportFriendResp, err error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.userClient.CheckUser(ctx, append([]string{req.OwnerUserID}, req.FriendUserIDs...)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if datautil.Contain(req.OwnerUserID, req.FriendUserIDs...) {
|
||||
return nil, servererrs.ErrCanNotAddYourself.WrapMsg("can not add yourself")
|
||||
}
|
||||
if datautil.Duplicate(req.FriendUserIDs) {
|
||||
return nil, errs.ErrArgs.WrapMsg("friend userID repeated")
|
||||
}
|
||||
|
||||
if err := s.webhookBeforeImportFriends(ctx, &s.config.WebhooksConfig.BeforeImportFriends, req); err != nil && err != servererrs.ErrCallbackContinue {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.db.BecomeFriends(ctx, req.OwnerUserID, req.FriendUserIDs, constant.BecomeFriendByImport); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, userID := range req.FriendUserIDs {
|
||||
s.notificationSender.FriendApplicationAgreedNotification(ctx, &relation.RespondFriendApplyReq{
|
||||
FromUserID: req.OwnerUserID,
|
||||
ToUserID: userID,
|
||||
HandleResult: constant.FriendResponseAgree,
|
||||
}, false)
|
||||
}
|
||||
|
||||
s.webhookAfterImportFriends(ctx, &s.config.WebhooksConfig.AfterImportFriends, req)
|
||||
return &relation.ImportFriendResp{}, nil
|
||||
}
|
||||
|
||||
// ok.
|
||||
func (s *friendServer) RespondFriendApply(ctx context.Context, req *relation.RespondFriendApplyReq) (resp *relation.RespondFriendApplyResp, err error) {
|
||||
resp = &relation.RespondFriendApplyResp{}
|
||||
if err := authverify.CheckAccess(ctx, req.ToUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
friendRequest := model.FriendRequest{
|
||||
FromUserID: req.FromUserID,
|
||||
ToUserID: req.ToUserID,
|
||||
HandleMsg: req.HandleMsg,
|
||||
HandleResult: req.HandleResult,
|
||||
}
|
||||
if req.HandleResult == constant.FriendResponseAgree {
|
||||
if err := s.webhookBeforeAddFriendAgree(ctx, &s.config.WebhooksConfig.BeforeAddFriendAgree, req); err != nil && err != servererrs.ErrCallbackContinue {
|
||||
return nil, err
|
||||
}
|
||||
err := s.db.AgreeFriendRequest(ctx, &friendRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.webhookAfterAddFriendAgree(ctx, &s.config.WebhooksConfig.AfterAddFriendAgree, req)
|
||||
s.notificationSender.FriendApplicationAgreedNotification(ctx, req, true)
|
||||
return resp, nil
|
||||
}
|
||||
if req.HandleResult == constant.FriendResponseRefuse {
|
||||
err := s.db.RefuseFriendRequest(ctx, &friendRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.notificationSender.FriendApplicationRefusedNotification(ctx, req)
|
||||
return resp, nil
|
||||
}
|
||||
return nil, errs.ErrArgs.WrapMsg("req.HandleResult != -1/1")
|
||||
}
|
||||
|
||||
// ok.
|
||||
func (s *friendServer) DeleteFriend(ctx context.Context, req *relation.DeleteFriendReq) (resp *relation.DeleteFriendResp, err error) {
|
||||
if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = s.db.FindFriendsWithError(ctx, req.OwnerUserID, []string{req.FriendUserID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.db.Delete(ctx, req.OwnerUserID, []string{req.FriendUserID}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.notificationSender.FriendDeletedNotification(ctx, req)
|
||||
s.webhookAfterDeleteFriend(ctx, &s.config.WebhooksConfig.AfterDeleteFriend, req)
|
||||
|
||||
return &relation.DeleteFriendResp{}, nil
|
||||
}
|
||||
|
||||
// ok.
|
||||
func (s *friendServer) SetFriendRemark(ctx context.Context, req *relation.SetFriendRemarkReq) (resp *relation.SetFriendRemarkResp, err error) {
|
||||
if err = s.webhookBeforeSetFriendRemark(ctx, &s.config.WebhooksConfig.BeforeSetFriendRemark, req); err != nil && err != servererrs.ErrCallbackContinue {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := authverify.CheckAccess(ctx, req.OwnerUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = s.db.FindFriendsWithError(ctx, req.OwnerUserID, []string{req.FriendUserID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.db.UpdateRemark(ctx, req.OwnerUserID, req.FriendUserID, req.Remark); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.webhookAfterSetFriendRemark(ctx, &s.config.WebhooksConfig.AfterSetFriendRemark, req)
|
||||
s.notificationSender.FriendRemarkSetNotification(ctx, req.OwnerUserID, req.FriendUserID)
|
||||
|
||||
return &relation.SetFriendRemarkResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &relation.GetFriendInfoResp{FriendInfos: convert.FriendOnlyDB2PbOnly(friends)}, nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
friends, err := s.getFriend(ctx, req.OwnerUserID, req.FriendUserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &relation.GetDesignatedFriendsResp{
|
||||
FriendsInfo: friends,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) getFriend(ctx context.Context, ownerUserID string, friendUserIDs []string) ([]*sdkws.FriendInfo, error) {
|
||||
if len(friendUserIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
friends, err := s.db.FindFriendsWithError(ctx, ownerUserID, friendUserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convert.FriendsDB2Pb(ctx, friends, s.userClient.GetUsersInfoMap)
|
||||
}
|
||||
|
||||
// Get the list of friend requests sent out proactively.
|
||||
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
|
||||
}
|
||||
resp = &relation.GetDesignatedFriendsApplyResp{}
|
||||
resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 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.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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.getCommonUserMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = int32(total)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) GetPaginationFriendsApplyFrom(ctx context.Context, req *relation.GetPaginationFriendsApplyFromReq) (resp *relation.GetPaginationFriendsApplyFromResp, err error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 = &relation.GetPaginationFriendsApplyFromResp{}
|
||||
resp.FriendRequests, err = convert.FriendRequestDB2Pb(ctx, friendRequests, s.getCommonUserMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = int32(total)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) GetPaginationFriends(ctx context.Context, req *relation.GetPaginationFriendsReq) (resp *relation.GetPaginationFriendsResp, err error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total, friends, err := s.db.PageOwnerFriends(ctx, req.UserID, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = &relation.GetPaginationFriendsResp{}
|
||||
resp.FriendsInfo, err = convert.FriendsDB2Pb(ctx, friends, s.userClient.GetUsersInfoMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Total = int32(total)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) GetFriendIDs(ctx context.Context, req *relation.GetFriendIDsReq) (resp *relation.GetFriendIDsResp, err error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = &relation.GetFriendIDsResp{}
|
||||
resp.FriendIDs, err = s.db.FindFriendUserIDs(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) GetSpecifiedFriendsInfo(ctx context.Context, req *relation.GetSpecifiedFriendsInfoReq) (*relation.GetSpecifiedFriendsInfoResp, error) {
|
||||
if len(req.UserIDList) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("userIDList is empty")
|
||||
}
|
||||
|
||||
if datautil.Duplicate(req.UserIDList) {
|
||||
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
|
||||
}
|
||||
|
||||
friends, err := s.db.FindFriendsWithError(ctx, req.OwnerUserID, req.UserIDList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blacks, err := s.blackDatabase.FindBlackInfos(ctx, req.OwnerUserID, req.UserIDList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
friendMap := datautil.SliceToMap(friends, func(e *model.Friend) string {
|
||||
return e.FriendUserID
|
||||
})
|
||||
|
||||
blackMap := datautil.SliceToMap(blacks, func(e *model.Black) string {
|
||||
return e.BlockUserID
|
||||
})
|
||||
|
||||
resp := &relation.GetSpecifiedFriendsInfoResp{
|
||||
Infos: make([]*relation.GetSpecifiedFriendsInfoInfo, 0, len(req.UserIDList)),
|
||||
}
|
||||
|
||||
for _, userID := range req.UserIDList {
|
||||
user := userMap[userID]
|
||||
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var friendInfo *sdkws.FriendInfo
|
||||
if friend := friendMap[userID]; friend != nil {
|
||||
friendInfo = &sdkws.FriendInfo{
|
||||
OwnerUserID: friend.OwnerUserID,
|
||||
Remark: friend.Remark,
|
||||
CreateTime: friend.CreateTime.UnixMilli(),
|
||||
AddSource: friend.AddSource,
|
||||
OperatorUserID: friend.OperatorUserID,
|
||||
Ex: friend.Ex,
|
||||
IsPinned: friend.IsPinned,
|
||||
}
|
||||
}
|
||||
|
||||
var blackInfo *sdkws.BlackInfo
|
||||
if black := blackMap[userID]; black != nil {
|
||||
blackInfo = &sdkws.BlackInfo{
|
||||
OwnerUserID: black.OwnerUserID,
|
||||
CreateTime: black.CreateTime.UnixMilli(),
|
||||
AddSource: black.AddSource,
|
||||
OperatorUserID: black.OperatorUserID,
|
||||
Ex: black.Ex,
|
||||
}
|
||||
}
|
||||
|
||||
resp.Infos = append(resp.Infos, &relation.GetSpecifiedFriendsInfoInfo{
|
||||
UserInfo: user,
|
||||
FriendInfo: friendInfo,
|
||||
BlackInfo: blackInfo,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
if datautil.Duplicate(req.FriendUserIDs) {
|
||||
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
|
||||
}
|
||||
|
||||
val := make(map[string]any)
|
||||
|
||||
if req.IsPinned != nil {
|
||||
val["is_pinned"] = req.IsPinned.Value
|
||||
}
|
||||
if req.Remark != nil {
|
||||
val["remark"] = req.Remark.Value
|
||||
}
|
||||
if req.Ex != nil {
|
||||
val["ex"] = req.Ex.Value
|
||||
}
|
||||
if err = s.db.UpdateFriends(ctx, req.OwnerUserID, req.FriendUserIDs, val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &relation.UpdateFriendsResp{}
|
||||
|
||||
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
|
||||
}
|
||||
303
internal/rpc/relation/notification.go
Normal file
303
internal/rpc/relation/notification.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Copyright © 2023 OpenIM. 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 relation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/versionctx"
|
||||
|
||||
relationtb "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification/common_user"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/relation"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
type FriendNotificationSender struct {
|
||||
*notification.NotificationSender
|
||||
// Target not found err
|
||||
getUsersInfo func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error)
|
||||
// db controller
|
||||
db controller.FriendDatabase
|
||||
}
|
||||
|
||||
type friendNotificationSenderOptions func(*FriendNotificationSender)
|
||||
|
||||
func WithFriendDB(db controller.FriendDatabase) friendNotificationSenderOptions {
|
||||
return func(s *FriendNotificationSender) {
|
||||
s.db = db
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
result = append(result, user)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
s.getUsersInfo = f
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
result = append(result, user)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
s.getUsersInfo = f
|
||||
}
|
||||
}
|
||||
|
||||
func NewFriendNotificationSender(conf *config.Notification, msgClient *rpcli.MsgClient, opts ...friendNotificationSenderOptions) *FriendNotificationSender {
|
||||
f := &FriendNotificationSender{
|
||||
NotificationSender: notification.NewNotificationSender(conf, notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {
|
||||
return msgClient.SendMsg(ctx, req)
|
||||
})),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
result := make(map[string]*sdkws.UserInfo)
|
||||
for _, user := range users {
|
||||
result[user.GetUserID()] = user.(*sdkws.UserInfo)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
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
|
||||
}
|
||||
return users[fromUserID].Nickname, users[toUserID].Nickname, nil
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) UserInfoUpdatedNotification(ctx context.Context, changedUserID string) {
|
||||
tips := sdkws.UserInfoUpdatedTips{UserID: changedUserID}
|
||||
f.Notification(ctx, mcontext.GetOpUserID(ctx), changedUserID, constant.UserInfoUpdatedNotification, &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) 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) 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) 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{
|
||||
FromUserID: req.FromUserID,
|
||||
ToUserID: req.ToUserID,
|
||||
},
|
||||
HandleMsg: req.HandleMsg,
|
||||
Request: request,
|
||||
}
|
||||
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 {
|
||||
log.ZError(ctx, "FriendApplicationRefusedNotification get friend request", err, "fromUserID", req.FromUserID, "toUserID", req.ToUserID)
|
||||
return
|
||||
}
|
||||
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,
|
||||
ToUserID: req.FriendUserID,
|
||||
}}
|
||||
f.Notification(ctx, req.OwnerUserID, req.FriendUserID, constant.FriendDeletedNotification, &tips)
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) setVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string) {
|
||||
versions := versionctx.GetVersionLog(ctx).Get()
|
||||
for _, coll := range versions {
|
||||
if coll.Name == collName && coll.Doc.DID == id {
|
||||
*version = uint64(coll.Doc.Version)
|
||||
*versionID = coll.Doc.ID.Hex()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) setSortVersion(ctx context.Context, version *uint64, versionID *string, collName string, id string, sortVersion *uint64) {
|
||||
versions := versionctx.GetVersionLog(ctx).Get()
|
||||
for _, coll := range versions {
|
||||
if coll.Name == collName && coll.Doc.DID == id {
|
||||
*version = uint64(coll.Doc.Version)
|
||||
*versionID = coll.Doc.ID.Hex()
|
||||
for _, elem := range coll.Doc.Logs {
|
||||
if elem.EID == relationtb.VersionSortChangeID {
|
||||
*sortVersion = uint64(elem.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) FriendRemarkSetNotification(ctx context.Context, fromUserID, toUserID string) {
|
||||
tips := sdkws.FriendInfoChangedTips{FromToUserID: &sdkws.FromToUserID{}}
|
||||
tips.FromToUserID.FromUserID = fromUserID
|
||||
tips.FromToUserID.ToUserID = toUserID
|
||||
f.setSortVersion(ctx, &tips.FriendVersion, &tips.FriendVersionID, database.FriendVersionName, toUserID, &tips.FriendSortVersion)
|
||||
f.Notification(ctx, fromUserID, toUserID, constant.FriendRemarkSetNotification, &tips)
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) FriendsInfoUpdateNotification(ctx context.Context, toUserID string, friendIDs []string) {
|
||||
tips := sdkws.FriendsInfoUpdateTips{FromToUserID: &sdkws.FromToUserID{}}
|
||||
tips.FromToUserID.ToUserID = toUserID
|
||||
tips.FriendIDs = friendIDs
|
||||
f.Notification(ctx, toUserID, toUserID, constant.FriendsInfoUpdateNotification, &tips)
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) BlackAddedNotification(ctx context.Context, req *relation.AddBlackReq) {
|
||||
tips := sdkws.BlackAddedTips{FromToUserID: &sdkws.FromToUserID{}}
|
||||
tips.FromToUserID.FromUserID = req.OwnerUserID
|
||||
tips.FromToUserID.ToUserID = req.BlackUserID
|
||||
f.Notification(ctx, req.OwnerUserID, req.BlackUserID, constant.BlackAddedNotification, &tips)
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) BlackDeletedNotification(ctx context.Context, req *relation.RemoveBlackReq) {
|
||||
blackDeletedTips := sdkws.BlackDeletedTips{FromToUserID: &sdkws.FromToUserID{
|
||||
FromUserID: req.OwnerUserID,
|
||||
ToUserID: req.BlackUserID,
|
||||
}}
|
||||
f.Notification(ctx, req.OwnerUserID, req.BlackUserID, constant.BlackDeletedNotification, &blackDeletedTips)
|
||||
}
|
||||
|
||||
func (f *FriendNotificationSender) FriendInfoUpdatedNotification(ctx context.Context, changedUserID string, needNotifiedUserID string) {
|
||||
tips := sdkws.UserInfoUpdatedTips{UserID: changedUserID}
|
||||
f.Notification(ctx, mcontext.GetOpUserID(ctx), needNotifiedUserID, constant.FriendInfoUpdatedNotification, &tips)
|
||||
}
|
||||
108
internal/rpc/relation/sync.go
Normal file
108
internal/rpc/relation/sync.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package relation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/util/hashutil"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/log"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/internal/rpc/incrversion"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/protocol/relation"
|
||||
)
|
||||
|
||||
func (s *friendServer) NotificationUserInfoUpdate(ctx context.Context, req *relation.NotificationUserInfoUpdateReq) (*relation.NotificationUserInfoUpdateResp, error) {
|
||||
userIDs, err := s.db.FindFriendUserIDs(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(userIDs) > 0 {
|
||||
friendUserIDs := []string{req.UserID}
|
||||
noCancelCtx := context.WithoutCancel(ctx)
|
||||
err := s.queue.PushCtx(ctx, func() {
|
||||
for _, userID := range userIDs {
|
||||
if err := s.db.OwnerIncrVersion(noCancelCtx, userID, friendUserIDs, model.VersionStateUpdate); err != nil {
|
||||
log.ZError(ctx, "OwnerIncrVersion", err, "userID", userID, "friendUserIDs", friendUserIDs)
|
||||
}
|
||||
}
|
||||
for _, userID := range userIDs {
|
||||
s.notificationSender.FriendInfoUpdatedNotification(noCancelCtx, req.UserID, userID)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.ZError(ctx, "NotificationUserInfoUpdate timeout", err, "userID", req.UserID)
|
||||
}
|
||||
}
|
||||
return &relation.NotificationUserInfoUpdateResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
userIDs, err := s.db.FindFriendUserIDs(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idHash := hashutil.IdHash(userIDs)
|
||||
if req.IdHash == idHash {
|
||||
userIDs = nil
|
||||
}
|
||||
return &relation.GetFullFriendUserIDsResp{
|
||||
Version: idHash,
|
||||
VersionID: vl.ID.Hex(),
|
||||
Equal: req.IdHash == idHash,
|
||||
UserIDs: userIDs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *friendServer) GetIncrementalFriends(ctx context.Context, req *relation.GetIncrementalFriendsReq) (*relation.GetIncrementalFriendsResp, error) {
|
||||
if err := authverify.CheckAccess(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sortVersion uint64
|
||||
opt := incrversion.Option[*sdkws.FriendInfo, relation.GetIncrementalFriendsResp]{
|
||||
Ctx: ctx,
|
||||
VersionKey: req.UserID,
|
||||
VersionID: req.VersionID,
|
||||
VersionNumber: req.Version,
|
||||
Version: func(ctx context.Context, ownerUserID string, version uint, limit int) (*model.VersionLog, error) {
|
||||
vl, err := s.db.FindFriendIncrVersion(ctx, ownerUserID, version, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vl.Logs = slices.DeleteFunc(vl.Logs, func(elem model.VersionLogElem) bool {
|
||||
if elem.EID == model.VersionSortChangeID {
|
||||
vl.LogLen--
|
||||
sortVersion = uint64(elem.Version)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return vl, nil
|
||||
},
|
||||
CacheMaxVersion: s.db.FindMaxFriendVersionCache,
|
||||
Find: func(ctx context.Context, ids []string) ([]*sdkws.FriendInfo, error) {
|
||||
return s.getFriend(ctx, req.UserID, ids)
|
||||
},
|
||||
Resp: func(version *model.VersionLog, deleteIds []string, insertList, updateList []*sdkws.FriendInfo, full bool) *relation.GetIncrementalFriendsResp {
|
||||
return &relation.GetIncrementalFriendsResp{
|
||||
VersionID: version.ID.Hex(),
|
||||
Version: uint64(version.Version),
|
||||
Full: full,
|
||||
Delete: deleteIds,
|
||||
Insert: insertList,
|
||||
Update: updateList,
|
||||
SortVersion: sortVersion,
|
||||
}
|
||||
},
|
||||
}
|
||||
return opt.Build()
|
||||
}
|
||||
233
internal/rpc/third/log.go
Normal file
233
internal/rpc/third/log.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// Copyright © 2023 OpenIM. 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 third
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
relationtb "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/third"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
func genLogID() string {
|
||||
const dataLen = 10
|
||||
data := make([]byte, dataLen)
|
||||
rand.Read(data)
|
||||
chars := []byte("0123456789")
|
||||
for i := 0; i < len(data); i++ {
|
||||
if i == 0 {
|
||||
data[i] = chars[1:][data[i]%9]
|
||||
} else {
|
||||
data[i] = chars[data[i]%10]
|
||||
}
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// extractKeyFromLogURL 从日志URL中提取S3的key
|
||||
// URL格式: https://s3.jizhying.com/images/openim/data/hash/{hash}?...
|
||||
// 或: https://chatall.oss-ap-southeast-1.aliyuncs.com/openim%2Fdata%2Fhash%2F{hash}
|
||||
// key格式: openim/data/hash/{hash}(不包含bucket名称)
|
||||
// bucket名称在URL路径的第一段(如images),需要去掉
|
||||
func extractKeyFromLogURL(logURL string, bucketName string) string {
|
||||
if logURL == "" {
|
||||
return ""
|
||||
}
|
||||
parsedURL, err := url.Parse(logURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// 获取路径部分,去掉开头的'/'
|
||||
path := strings.TrimPrefix(parsedURL.Path, "/")
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 如果配置了bucket名称,且路径以bucket名称开头,则去掉bucket名称前缀
|
||||
if bucketName != "" && strings.HasPrefix(path, bucketName+"/") {
|
||||
path = strings.TrimPrefix(path, bucketName+"/")
|
||||
} else {
|
||||
// 如果没有匹配到bucket名称,尝试去掉路径的第一段(可能是bucket名称)
|
||||
// 这种情况下,假设路径的第一段是bucket名称
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) > 1 {
|
||||
path = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// URL.Path已经是解码后的路径,所以直接返回即可
|
||||
return path
|
||||
}
|
||||
|
||||
func (t *thirdServer) UploadLogs(ctx context.Context, req *third.UploadLogsReq) (*third.UploadLogsResp, error) {
|
||||
var dbLogs []*relationtb.Log
|
||||
userID := mcontext.GetOpUserID(ctx)
|
||||
platform := constant.PlatformID2Name[int(req.Platform)]
|
||||
for _, fileURL := range req.FileURLs {
|
||||
log := relationtb.Log{
|
||||
Platform: platform,
|
||||
UserID: userID,
|
||||
CreateTime: time.Now(),
|
||||
Url: fileURL.URL,
|
||||
FileName: fileURL.Filename,
|
||||
AppFramework: req.AppFramework,
|
||||
Version: req.Version,
|
||||
Ex: req.Ex,
|
||||
}
|
||||
for i := 0; i < 20; i++ {
|
||||
id := genLogID()
|
||||
logs, err := t.thirdDatabase.GetLogs(ctx, []string{id}, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
log.LogID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
if log.LogID == "" {
|
||||
return nil, servererrs.ErrData.WrapMsg("Log id gen error")
|
||||
}
|
||||
dbLogs = append(dbLogs, &log)
|
||||
}
|
||||
err := t.thirdDatabase.UploadLogs(ctx, dbLogs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &third.UploadLogsResp{}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) DeleteLogs(ctx context.Context, req *third.DeleteLogsReq) (*third.DeleteLogsResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userID := ""
|
||||
logs, err := t.thirdDatabase.GetLogs(ctx, req.LogIDs, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var logIDs []string
|
||||
for _, log := range logs {
|
||||
logIDs = append(logIDs, log.LogID)
|
||||
}
|
||||
if ids := datautil.Single(req.LogIDs, logIDs); len(ids) > 0 {
|
||||
return nil, errs.ErrRecordNotFound.WrapMsg("logIDs not found", "logIDs", ids)
|
||||
}
|
||||
|
||||
// 在删除日志记录前,先删除对应的S3文件
|
||||
engine := t.config.RpcConfig.Object.Enable
|
||||
if engine != "" && t.s3 != nil {
|
||||
// 获取bucket名称(从minio配置中)
|
||||
bucketName := ""
|
||||
if engine == "minio" {
|
||||
bucketName = t.config.MinioConfig.Bucket
|
||||
}
|
||||
|
||||
for _, logRecord := range logs {
|
||||
if logRecord.Url == "" {
|
||||
continue
|
||||
}
|
||||
// 从URL中提取S3的key(不包含bucket名称)
|
||||
key := extractKeyFromLogURL(logRecord.Url, bucketName)
|
||||
if key == "" {
|
||||
log.ZDebug(ctx, "DeleteLogs: cannot extract key from URL, skipping S3 deletion", "logID", logRecord.LogID, "url", logRecord.Url)
|
||||
continue
|
||||
}
|
||||
// 直接使用key删除S3文件
|
||||
log.ZInfo(ctx, "DeleteLogs: attempting to delete S3 file", "logID", logRecord.LogID, "url", logRecord.Url, "key", key, "bucket", bucketName, "engine", engine)
|
||||
if err := t.s3.DeleteObject(ctx, key); err != nil {
|
||||
// S3文件删除失败,返回错误,不删除数据库记录
|
||||
log.ZError(ctx, "DeleteLogs: S3 file delete failed", err, "logID", logRecord.LogID, "url", logRecord.Url, "key", key, "bucket", bucketName, "engine", engine)
|
||||
return nil, errs.WrapMsg(err, "failed to delete S3 file for log", "logID", logRecord.LogID, "url", logRecord.Url, "key", key)
|
||||
}
|
||||
log.ZInfo(ctx, "DeleteLogs: S3 file delete command executed successfully", "logID", logRecord.LogID, "url", logRecord.Url, "key", key, "bucket", bucketName, "engine", engine)
|
||||
}
|
||||
}
|
||||
|
||||
err = t.thirdDatabase.DeleteLogs(ctx, req.LogIDs, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &third.DeleteLogsResp{}, nil
|
||||
}
|
||||
|
||||
func dbToPbLogInfos(logs []*relationtb.Log) []*third.LogInfo {
|
||||
db2pbForLogInfo := func(log *relationtb.Log) *third.LogInfo {
|
||||
return &third.LogInfo{
|
||||
Filename: log.FileName,
|
||||
UserID: log.UserID,
|
||||
Platform: log.Platform,
|
||||
Url: log.Url,
|
||||
CreateTime: log.CreateTime.UnixMilli(),
|
||||
LogID: log.LogID,
|
||||
SystemType: log.SystemType,
|
||||
Version: log.Version,
|
||||
Ex: log.Ex,
|
||||
}
|
||||
}
|
||||
return datautil.Slice(logs, db2pbForLogInfo)
|
||||
}
|
||||
|
||||
func (t *thirdServer) SearchLogs(ctx context.Context, req *third.SearchLogsReq) (*third.SearchLogsResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
resp third.SearchLogsResp
|
||||
userIDs []string
|
||||
)
|
||||
if req.StartTime > req.EndTime {
|
||||
return nil, errs.ErrArgs.WrapMsg("startTime>endTime")
|
||||
}
|
||||
if req.StartTime == 0 && req.EndTime == 0 {
|
||||
t := time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
timestampMills := t.UnixNano() / int64(time.Millisecond)
|
||||
req.StartTime = timestampMills
|
||||
req.EndTime = time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
total, logs, err := t.thirdDatabase.SearchLogs(ctx, req.Keyword, time.UnixMilli(req.StartTime), time.UnixMilli(req.EndTime), req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pbLogs := dbToPbLogInfos(logs)
|
||||
for _, log := range logs {
|
||||
userIDs = append(userIDs, log.UserID)
|
||||
}
|
||||
userMap, err := t.userClient.GetUsersInfoMap(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, pbLog := range pbLogs {
|
||||
if user, ok := userMap[pbLog.UserID]; ok {
|
||||
pbLog.Nickname = user.Nickname
|
||||
}
|
||||
}
|
||||
resp.LogsInfos = pbLogs
|
||||
resp.Total = uint32(total)
|
||||
return &resp, nil
|
||||
}
|
||||
446
internal/rpc/third/r2.go
Normal file
446
internal/rpc/third/r2.go
Normal file
@@ -0,0 +1,446 @@
|
||||
// Copyright © 2023 OpenIM. 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 third
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
aws3 "github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/openimsdk/tools/s3"
|
||||
)
|
||||
|
||||
const (
|
||||
minPartSize int64 = 1024 * 1024 * 5 // 5MB
|
||||
maxPartSize int64 = 1024 * 1024 * 1024 * 5 // 5GB
|
||||
maxNumSize int64 = 10000
|
||||
)
|
||||
|
||||
type R2Config struct {
|
||||
Endpoint string
|
||||
Region string
|
||||
Bucket string
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
SessionToken string
|
||||
}
|
||||
|
||||
// NewR2 创建支持 Cloudflare R2 的 S3 客户端
|
||||
func NewR2(conf R2Config) (*R2, error) {
|
||||
if conf.Endpoint == "" {
|
||||
return nil, errors.New("endpoint is required for R2")
|
||||
}
|
||||
|
||||
// 创建 HTTP 客户端,设置合理的超时
|
||||
httpClient := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
cfg := aws.Config{
|
||||
Region: conf.Region,
|
||||
Credentials: credentials.NewStaticCredentialsProvider(conf.AccessKeyID, conf.SecretAccessKey, conf.SessionToken),
|
||||
HTTPClient: httpClient,
|
||||
}
|
||||
|
||||
// 创建 S3 客户端,启用路径风格访问(R2 要求)并设置自定义 endpoint
|
||||
client := aws3.NewFromConfig(cfg, func(o *aws3.Options) {
|
||||
o.BaseEndpoint = aws.String(conf.Endpoint)
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
|
||||
r2 := &R2{
|
||||
bucket: conf.Bucket,
|
||||
client: client,
|
||||
presign: aws3.NewPresignClient(client),
|
||||
}
|
||||
|
||||
// 测试连接:尝试列出 bucket(验证 bucket 存在且有权限),设置 5 秒超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
fmt.Printf("[R2] Testing connection to bucket '%s' at endpoint '%s'...\n", conf.Bucket, conf.Endpoint)
|
||||
_, err := client.ListObjectsV2(ctx, &aws3.ListObjectsV2Input{
|
||||
Bucket: aws.String(conf.Bucket),
|
||||
MaxKeys: aws.Int32(1),
|
||||
})
|
||||
if err != nil {
|
||||
// 详细的错误信息
|
||||
var respErr *awshttp.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
fmt.Printf("[R2] Bucket verification HTTP error:\n")
|
||||
fmt.Printf(" Status Code: %d\n", respErr.Response.StatusCode)
|
||||
fmt.Printf(" Status: %s\n", respErr.Response.Status)
|
||||
}
|
||||
fmt.Printf("[R2] Warning: failed to verify R2 bucket '%s' at endpoint '%s': %v\n", conf.Bucket, conf.Endpoint, err)
|
||||
fmt.Printf("[R2] Please ensure:\n")
|
||||
fmt.Printf(" 1. Bucket '%s' exists in your R2 account\n", conf.Bucket)
|
||||
fmt.Printf(" 2. API credentials have correct permissions (Object Read & Write)\n")
|
||||
fmt.Printf(" 3. Account ID in endpoint matches your R2 account\n")
|
||||
} else {
|
||||
fmt.Printf("[R2] Successfully connected to bucket '%s'\n", conf.Bucket)
|
||||
}
|
||||
|
||||
return r2, nil
|
||||
}
|
||||
|
||||
type R2 struct {
|
||||
bucket string
|
||||
client *aws3.Client
|
||||
presign *aws3.PresignClient
|
||||
}
|
||||
|
||||
func (r *R2) Engine() string {
|
||||
return "aws"
|
||||
}
|
||||
|
||||
func (r *R2) PartLimit() (*s3.PartLimit, error) {
|
||||
return &s3.PartLimit{
|
||||
MinPartSize: minPartSize,
|
||||
MaxPartSize: maxPartSize,
|
||||
MaxNumSize: maxNumSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *R2) formatETag(etag string) string {
|
||||
return strings.Trim(etag, `"`)
|
||||
}
|
||||
|
||||
func (r *R2) PartSize(ctx context.Context, size int64) (int64, error) {
|
||||
if size <= 0 {
|
||||
return 0, errors.New("size must be greater than 0")
|
||||
}
|
||||
if size > maxPartSize*maxNumSize {
|
||||
return 0, fmt.Errorf("size must be less than the maximum allowed limit")
|
||||
}
|
||||
if size <= minPartSize*maxNumSize {
|
||||
return minPartSize, nil
|
||||
}
|
||||
partSize := size / maxNumSize
|
||||
if size%maxNumSize != 0 {
|
||||
partSize++
|
||||
}
|
||||
return partSize, nil
|
||||
}
|
||||
|
||||
func (r *R2) IsNotFound(err error) bool {
|
||||
var respErr *awshttp.ResponseError
|
||||
if !errors.As(err, &respErr) {
|
||||
return false
|
||||
}
|
||||
if respErr == nil || respErr.Response == nil {
|
||||
return false
|
||||
}
|
||||
return respErr.Response.StatusCode == http.StatusNotFound
|
||||
}
|
||||
|
||||
func (r *R2) PresignedPutObject(ctx context.Context, name string, expire time.Duration, opt *s3.PutOption) (*s3.PresignedPutResult, error) {
|
||||
res, err := r.presign.PresignPutObject(ctx, &aws3.PutObjectInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
}, aws3.WithPresignExpires(expire), withDisableHTTPPresignerHeaderV4(nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s3.PresignedPutResult{URL: res.URL}, nil
|
||||
}
|
||||
|
||||
func (r *R2) DeleteObject(ctx context.Context, name string) error {
|
||||
_, err := r.client.DeleteObject(ctx, &aws3.DeleteObjectInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *R2) CopyObject(ctx context.Context, src string, dst string) (*s3.CopyObjectInfo, error) {
|
||||
res, err := r.client.CopyObject(ctx, &aws3.CopyObjectInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
CopySource: aws.String(r.bucket + "/" + src),
|
||||
Key: aws.String(dst),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.CopyObjectResult == nil || res.CopyObjectResult.ETag == nil || *res.CopyObjectResult.ETag == "" {
|
||||
return nil, errors.New("CopyObject etag is nil")
|
||||
}
|
||||
return &s3.CopyObjectInfo{
|
||||
Key: dst,
|
||||
ETag: r.formatETag(*res.CopyObjectResult.ETag),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *R2) StatObject(ctx context.Context, name string) (*s3.ObjectInfo, error) {
|
||||
res, err := r.client.HeadObject(ctx, &aws3.HeadObjectInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.ETag == nil || *res.ETag == "" {
|
||||
return nil, errors.New("GetObjectAttributes etag is nil")
|
||||
}
|
||||
if res.ContentLength == nil {
|
||||
return nil, errors.New("GetObjectAttributes object size is nil")
|
||||
}
|
||||
info := &s3.ObjectInfo{
|
||||
ETag: r.formatETag(*res.ETag),
|
||||
Key: name,
|
||||
Size: *res.ContentLength,
|
||||
}
|
||||
if res.LastModified == nil {
|
||||
info.LastModified = time.Unix(0, 0)
|
||||
} else {
|
||||
info.LastModified = *res.LastModified
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (r *R2) InitiateMultipartUpload(ctx context.Context, name string, opt *s3.PutOption) (*s3.InitiateMultipartUploadResult, error) {
|
||||
startTime := time.Now()
|
||||
fmt.Printf("[R2] InitiateMultipartUpload start: bucket=%s, key=%s\n", r.bucket, name)
|
||||
|
||||
input := &aws3.CreateMultipartUploadInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
}
|
||||
|
||||
// 如果提供了 ContentType,添加到请求中
|
||||
if opt != nil && opt.ContentType != "" {
|
||||
input.ContentType = aws.String(opt.ContentType)
|
||||
fmt.Printf("[R2] ContentType: %s\n", opt.ContentType)
|
||||
}
|
||||
|
||||
res, err := r.client.CreateMultipartUpload(ctx, input)
|
||||
duration := time.Since(startTime)
|
||||
|
||||
if err != nil {
|
||||
// 详细错误信息
|
||||
var respErr *awshttp.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
fmt.Printf("[R2] HTTP Response Error after %v:\n", duration)
|
||||
fmt.Printf(" Status Code: %d\n", respErr.Response.StatusCode)
|
||||
fmt.Printf(" Status: %s\n", respErr.Response.Status)
|
||||
if respErr.Response.Body != nil {
|
||||
body := make([]byte, 1024)
|
||||
n, _ := respErr.Response.Body.Read(body)
|
||||
fmt.Printf(" Body: %s\n", string(body[:n]))
|
||||
}
|
||||
}
|
||||
fmt.Printf("[R2] InitiateMultipartUpload failed after %v: %v\n", duration, err)
|
||||
return nil, fmt.Errorf("CreateMultipartUpload failed (bucket=%s, key=%s): %w", r.bucket, name, err)
|
||||
}
|
||||
if res.UploadId == nil || *res.UploadId == "" {
|
||||
return nil, errors.New("CreateMultipartUpload upload id is nil")
|
||||
}
|
||||
|
||||
fmt.Printf("[R2] InitiateMultipartUpload success after %v: uploadID=%s\n", duration, *res.UploadId)
|
||||
return &s3.InitiateMultipartUploadResult{
|
||||
Key: name,
|
||||
Bucket: r.bucket,
|
||||
UploadID: *res.UploadId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *R2) CompleteMultipartUpload(ctx context.Context, uploadID string, name string, parts []s3.Part) (*s3.CompleteMultipartUploadResult, error) {
|
||||
params := &aws3.CompleteMultipartUploadInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
UploadId: aws.String(uploadID),
|
||||
MultipartUpload: &types.CompletedMultipartUpload{
|
||||
Parts: make([]types.CompletedPart, 0, len(parts)),
|
||||
},
|
||||
}
|
||||
for _, part := range parts {
|
||||
params.MultipartUpload.Parts = append(params.MultipartUpload.Parts, types.CompletedPart{
|
||||
ETag: aws.String(part.ETag),
|
||||
PartNumber: aws.Int32(int32(part.PartNumber)),
|
||||
})
|
||||
}
|
||||
res, err := r.client.CompleteMultipartUpload(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.ETag == nil || *res.ETag == "" {
|
||||
return nil, errors.New("CompleteMultipartUpload etag is nil")
|
||||
}
|
||||
info := &s3.CompleteMultipartUploadResult{
|
||||
Key: name,
|
||||
Bucket: r.bucket,
|
||||
ETag: r.formatETag(*res.ETag),
|
||||
}
|
||||
if res.Location != nil {
|
||||
info.Location = *res.Location
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (r *R2) AbortMultipartUpload(ctx context.Context, uploadID string, name string) error {
|
||||
_, err := r.client.AbortMultipartUpload(ctx, &aws3.AbortMultipartUploadInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
UploadId: aws.String(uploadID),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *R2) ListUploadedParts(ctx context.Context, uploadID string, name string, partNumberMarker int, maxParts int) (*s3.ListUploadedPartsResult, error) {
|
||||
params := &aws3.ListPartsInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
UploadId: aws.String(uploadID),
|
||||
PartNumberMarker: aws.String(strconv.Itoa(partNumberMarker)),
|
||||
MaxParts: aws.Int32(int32(maxParts)),
|
||||
}
|
||||
res, err := r.client.ListParts(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info := &s3.ListUploadedPartsResult{
|
||||
Key: name,
|
||||
UploadID: uploadID,
|
||||
UploadedParts: make([]s3.UploadedPart, 0, len(res.Parts)),
|
||||
}
|
||||
if res.MaxParts != nil {
|
||||
info.MaxParts = int(*res.MaxParts)
|
||||
}
|
||||
if res.NextPartNumberMarker != nil {
|
||||
info.NextPartNumberMarker, _ = strconv.Atoi(*res.NextPartNumberMarker)
|
||||
}
|
||||
for _, part := range res.Parts {
|
||||
var val s3.UploadedPart
|
||||
if part.PartNumber != nil {
|
||||
val.PartNumber = int(*part.PartNumber)
|
||||
}
|
||||
if part.LastModified != nil {
|
||||
val.LastModified = *part.LastModified
|
||||
}
|
||||
if part.Size != nil {
|
||||
val.Size = *part.Size
|
||||
}
|
||||
info.UploadedParts = append(info.UploadedParts, val)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (r *R2) AuthSign(ctx context.Context, uploadID string, name string, expire time.Duration, partNumbers []int) (*s3.AuthSignResult, error) {
|
||||
res := &s3.AuthSignResult{
|
||||
Parts: make([]s3.SignPart, 0, len(partNumbers)),
|
||||
}
|
||||
params := &aws3.UploadPartInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
UploadId: aws.String(uploadID),
|
||||
}
|
||||
opt := aws3.WithPresignExpires(expire)
|
||||
for _, number := range partNumbers {
|
||||
params.PartNumber = aws.Int32(int32(number))
|
||||
val, err := r.presign.PresignUploadPart(ctx, params, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := url.Parse(val.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := u.Query()
|
||||
u.RawQuery = ""
|
||||
urlstr := u.String()
|
||||
if res.URL == "" {
|
||||
res.URL = urlstr
|
||||
}
|
||||
if res.URL == urlstr {
|
||||
urlstr = ""
|
||||
}
|
||||
res.Parts = append(res.Parts, s3.SignPart{
|
||||
PartNumber: number,
|
||||
URL: urlstr,
|
||||
Query: query,
|
||||
Header: val.SignedHeader,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *R2) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (string, error) {
|
||||
params := &aws3.GetObjectInput{
|
||||
Bucket: aws.String(r.bucket),
|
||||
Key: aws.String(name),
|
||||
}
|
||||
res, err := r.presign.PresignGetObject(ctx, params, aws3.WithPresignExpires(expire), withDisableHTTPPresignerHeaderV4(opt))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.URL, nil
|
||||
}
|
||||
|
||||
func (r *R2) FormData(ctx context.Context, name string, size int64, contentType string, duration time.Duration) (*s3.FormData, error) {
|
||||
return nil, errors.New("R2 does not currently support form data file uploads")
|
||||
}
|
||||
|
||||
func withDisableHTTPPresignerHeaderV4(opt *s3.AccessURLOption) func(options *aws3.PresignOptions) {
|
||||
return func(options *aws3.PresignOptions) {
|
||||
options.Presigner = &disableHTTPPresignerHeaderV4{
|
||||
opt: opt,
|
||||
presigner: options.Presigner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type disableHTTPPresignerHeaderV4 struct {
|
||||
opt *s3.AccessURLOption
|
||||
presigner aws3.HTTPPresignerV4
|
||||
}
|
||||
|
||||
func (d *disableHTTPPresignerHeaderV4) PresignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(*v4.SignerOptions)) (url string, signedHeader http.Header, err error) {
|
||||
optFns = append(optFns, func(options *v4.SignerOptions) {
|
||||
options.DisableHeaderHoisting = true
|
||||
})
|
||||
r.Header.Del("Amz-Sdk-Request")
|
||||
d.setOption(r.URL)
|
||||
return d.presigner.PresignHTTP(ctx, credentials, r, payloadHash, service, region, signingTime, optFns...)
|
||||
}
|
||||
|
||||
func (d *disableHTTPPresignerHeaderV4) setOption(u *url.URL) {
|
||||
if d.opt == nil {
|
||||
return
|
||||
}
|
||||
query := u.Query()
|
||||
if d.opt.ContentType != "" {
|
||||
query.Set("response-content-type", d.opt.ContentType)
|
||||
}
|
||||
if d.opt.Filename != "" {
|
||||
query.Set("response-content-disposition", `attachment; filename*=UTF-8''`+url.PathEscape(d.opt.Filename))
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
}
|
||||
352
internal/rpc/third/s3.go
Normal file
352
internal/rpc/third/s3.go
Normal file
@@ -0,0 +1,352 @@
|
||||
// Copyright © 2023 OpenIM. 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 third
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/protocol/third"
|
||||
"github.com/google/uuid"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"github.com/openimsdk/tools/s3"
|
||||
"github.com/openimsdk/tools/s3/cont"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
func (t *thirdServer) PartLimit(ctx context.Context, req *third.PartLimitReq) (*third.PartLimitResp, error) {
|
||||
limit, err := t.s3dataBase.PartLimit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &third.PartLimitResp{
|
||||
MinPartSize: limit.MinPartSize,
|
||||
MaxPartSize: limit.MaxPartSize,
|
||||
MaxNumSize: int32(limit.MaxNumSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) PartSize(ctx context.Context, req *third.PartSizeReq) (*third.PartSizeResp, error) {
|
||||
size, err := t.s3dataBase.PartSize(ctx, req.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &third.PartSizeResp{Size: size}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) InitiateMultipartUpload(ctx context.Context, req *third.InitiateMultipartUploadReq) (*third.InitiateMultipartUploadResp, error) {
|
||||
if err := t.checkUploadName(ctx, req.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expireTime := time.Now().Add(t.defaultExpire)
|
||||
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{
|
||||
Name: req.Name,
|
||||
UserID: mcontext.GetOpUserID(ctx),
|
||||
Hash: req.Hash,
|
||||
Key: haErr.Object.Key,
|
||||
Size: haErr.Object.Size,
|
||||
ContentType: req.ContentType,
|
||||
Group: req.Cause,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
if err := t.s3dataBase.SetObject(ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取 OSS 的真实 URL
|
||||
_, rawURL, err := t.s3dataBase.AccessURL(ctx, obj.Name, t.defaultExpire, nil)
|
||||
if err != nil {
|
||||
// 如果获取 OSS URL 失败,则使用配置的 URL
|
||||
rawURL = t.apiAddress(req.UrlPrefix, obj.Name)
|
||||
}
|
||||
|
||||
return &third.InitiateMultipartUploadResp{
|
||||
Url: rawURL,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var sign *third.AuthSignParts
|
||||
if result.Sign != nil && len(result.Sign.Parts) > 0 {
|
||||
sign = &third.AuthSignParts{
|
||||
Url: result.Sign.URL,
|
||||
Query: toPbMapArray(result.Sign.Query),
|
||||
Header: toPbMapArray(result.Sign.Header),
|
||||
Parts: make([]*third.SignPart, len(result.Sign.Parts)),
|
||||
}
|
||||
for i, part := range result.Sign.Parts {
|
||||
sign.Parts[i] = &third.SignPart{
|
||||
PartNumber: int32(part.PartNumber),
|
||||
Url: part.URL,
|
||||
Query: toPbMapArray(part.Query),
|
||||
Header: toPbMapArray(part.Header),
|
||||
}
|
||||
}
|
||||
}
|
||||
return &third.InitiateMultipartUploadResp{
|
||||
Upload: &third.UploadInfo{
|
||||
UploadID: result.UploadID,
|
||||
PartSize: result.PartSize,
|
||||
Sign: sign,
|
||||
ExpireTime: expireTime.UnixMilli(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) AuthSign(ctx context.Context, req *third.AuthSignReq) (*third.AuthSignResp, error) {
|
||||
partNumbers := datautil.Slice(req.PartNumbers, func(partNumber int32) int { return int(partNumber) })
|
||||
result, err := t.s3dataBase.AuthSign(ctx, req.UploadID, partNumbers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &third.AuthSignResp{
|
||||
Url: result.URL,
|
||||
Query: toPbMapArray(result.Query),
|
||||
Header: toPbMapArray(result.Header),
|
||||
Parts: make([]*third.SignPart, len(result.Parts)),
|
||||
}
|
||||
for i, part := range result.Parts {
|
||||
resp.Parts[i] = &third.SignPart{
|
||||
PartNumber: int32(part.PartNumber),
|
||||
Url: part.URL,
|
||||
Query: toPbMapArray(part.Query),
|
||||
Header: toPbMapArray(part.Header),
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) CompleteMultipartUpload(ctx context.Context, req *third.CompleteMultipartUploadReq) (*third.CompleteMultipartUploadResp, error) {
|
||||
if err := t.checkUploadName(ctx, req.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := t.s3dataBase.CompleteMultipartUpload(ctx, req.UploadID, req.Parts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj := &model.Object{
|
||||
Name: req.Name,
|
||||
UserID: mcontext.GetOpUserID(ctx),
|
||||
Hash: result.Hash,
|
||||
Key: result.Key,
|
||||
Size: result.Size,
|
||||
ContentType: req.ContentType,
|
||||
Group: req.Cause,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
if err := t.s3dataBase.SetObject(ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取 OSS 的真实 URL
|
||||
_, rawURL, err := t.s3dataBase.AccessURL(ctx, obj.Name, t.defaultExpire, nil)
|
||||
if err != nil {
|
||||
// 如果获取 OSS URL 失败,则使用配置的 URL
|
||||
rawURL = t.apiAddress(req.UrlPrefix, obj.Name)
|
||||
}
|
||||
|
||||
return &third.CompleteMultipartUploadResp{
|
||||
Url: rawURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) AccessURL(ctx context.Context, req *third.AccessURLReq) (*third.AccessURLResp, error) {
|
||||
opt := &s3.AccessURLOption{}
|
||||
if len(req.Query) > 0 {
|
||||
switch req.Query["type"] {
|
||||
case "":
|
||||
case "image":
|
||||
opt.Image = &s3.Image{}
|
||||
opt.Image.Format = req.Query["format"]
|
||||
opt.Image.Width, _ = strconv.Atoi(req.Query["width"])
|
||||
opt.Image.Height, _ = strconv.Atoi(req.Query["height"])
|
||||
log.ZDebug(ctx, "AccessURL image", "name", req.Name, "option", opt.Image)
|
||||
default:
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid query type")
|
||||
}
|
||||
}
|
||||
expireTime, rawURL, err := t.s3dataBase.AccessURL(ctx, req.Name, t.defaultExpire, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &third.AccessURLResp{
|
||||
Url: rawURL,
|
||||
ExpireTime: expireTime.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) InitiateFormData(ctx context.Context, req *third.InitiateFormDataReq) (*third.InitiateFormDataResp, error) {
|
||||
if req.Name == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("name is empty")
|
||||
}
|
||||
if req.Size <= 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("size must be greater than 0")
|
||||
}
|
||||
if err := t.checkUploadName(ctx, req.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var duration time.Duration
|
||||
opUserID := mcontext.GetOpUserID(ctx)
|
||||
var key string
|
||||
if authverify.CheckUserIsAdmin(ctx, opUserID) {
|
||||
if req.Millisecond <= 0 {
|
||||
duration = time.Minute * 10
|
||||
} else {
|
||||
duration = time.Millisecond * time.Duration(req.Millisecond)
|
||||
}
|
||||
if req.Absolute {
|
||||
key = req.Name
|
||||
}
|
||||
} else {
|
||||
duration = time.Minute * 10
|
||||
}
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errs.WrapMsg(err, "uuid NewRandom failed")
|
||||
}
|
||||
if key == "" {
|
||||
date := time.Now().Format("20060102")
|
||||
key = path.Join(cont.DirectPath, date, opUserID, hex.EncodeToString(uid[:])+path.Ext(req.Name))
|
||||
}
|
||||
mate := FormDataMate{
|
||||
Name: req.Name,
|
||||
Size: req.Size,
|
||||
ContentType: req.ContentType,
|
||||
Group: req.Group,
|
||||
Key: key,
|
||||
}
|
||||
mateData, err := json.Marshal(&mate)
|
||||
if err != nil {
|
||||
return nil, errs.WrapMsg(err, "marshal failed")
|
||||
}
|
||||
resp, err := t.s3dataBase.FormData(ctx, key, req.Size, req.ContentType, duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &third.InitiateFormDataResp{
|
||||
Id: base64.RawStdEncoding.EncodeToString(mateData),
|
||||
Url: resp.URL,
|
||||
File: resp.File,
|
||||
Header: toPbMapArray(resp.Header),
|
||||
FormData: resp.FormData,
|
||||
Expires: resp.Expires.UnixMilli(),
|
||||
SuccessCodes: datautil.Slice(resp.SuccessCodes, func(code int) int32 {
|
||||
return int32(code)
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) CompleteFormData(ctx context.Context, req *third.CompleteFormDataReq) (*third.CompleteFormDataResp, error) {
|
||||
if req.Id == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("id is empty")
|
||||
}
|
||||
data, err := base64.RawStdEncoding.DecodeString(req.Id)
|
||||
if err != nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid id " + err.Error())
|
||||
}
|
||||
var mate FormDataMate
|
||||
if err := json.Unmarshal(data, &mate); err != nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid id " + err.Error())
|
||||
}
|
||||
if err := t.checkUploadName(ctx, mate.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := t.s3dataBase.StatObject(ctx, mate.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Size > 0 && info.Size != mate.Size {
|
||||
return nil, servererrs.ErrData.WrapMsg("file size mismatch")
|
||||
}
|
||||
obj := &model.Object{
|
||||
Name: mate.Name,
|
||||
UserID: mcontext.GetOpUserID(ctx),
|
||||
Hash: "etag_" + info.ETag,
|
||||
Key: info.Key,
|
||||
Size: info.Size,
|
||||
ContentType: mate.ContentType,
|
||||
Group: mate.Group,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
if err := t.s3dataBase.SetObject(ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取 OSS 的真实 URL
|
||||
_, rawURL, err := t.s3dataBase.AccessURL(ctx, mate.Name, t.defaultExpire, nil)
|
||||
if err != nil {
|
||||
// 如果获取 OSS URL 失败,则使用配置的 URL
|
||||
rawURL = t.apiAddress(req.UrlPrefix, mate.Name)
|
||||
}
|
||||
|
||||
return &third.CompleteFormDataResp{Url: rawURL}, nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) apiAddress(prefix, name string) string {
|
||||
return prefix + name
|
||||
}
|
||||
|
||||
func (t *thirdServer) DeleteOutdatedData(ctx context.Context, req *third.DeleteOutdatedDataReq) (*third.DeleteOutdatedDataResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine := t.config.RpcConfig.Object.Enable
|
||||
expireTime := time.UnixMilli(req.ExpireTime)
|
||||
// Find all expired data in S3 database
|
||||
models, err := t.s3dataBase.FindExpirationObject(ctx, engine, expireTime, req.ObjectGroup, int64(req.Limit))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, obj := range models {
|
||||
if err := t.s3dataBase.DeleteSpecifiedData(ctx, engine, []string{obj.Name}); err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
if err := t.s3dataBase.DelS3Key(ctx, engine, obj.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count, err := t.s3dataBase.GetKeyCount(ctx, engine, obj.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.ZDebug(ctx, "delete s3 object record", "index", i, "s3", obj, "count", count)
|
||||
if count == 0 {
|
||||
if err := t.s3.DeleteObject(ctx, obj.Key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return &third.DeleteOutdatedDataResp{Count: int32(len(models))}, nil
|
||||
}
|
||||
|
||||
type FormDataMate struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ContentType string `json:"contentType"`
|
||||
Group string `json:"group"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
175
internal/rpc/third/third.go
Normal file
175
internal/rpc/third/third.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright © 2023 OpenIM. 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 third
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/mcache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
"github.com/openimsdk/tools/s3/disable"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/localcache"
|
||||
"github.com/openimsdk/tools/s3/aws"
|
||||
"github.com/openimsdk/tools/s3/kodo"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/protocol/third"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/s3"
|
||||
"github.com/openimsdk/tools/s3/cos"
|
||||
"github.com/openimsdk/tools/s3/minio"
|
||||
"github.com/openimsdk/tools/s3/oss"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type thirdServer struct {
|
||||
third.UnimplementedThirdServer
|
||||
thirdDatabase controller.ThirdDatabase
|
||||
s3dataBase controller.S3Database
|
||||
defaultExpire time.Duration
|
||||
config *Config
|
||||
s3 s3.Interface
|
||||
userClient *rpcli.UserClient
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Third
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
NotificationConfig config.Notification
|
||||
Share config.Share
|
||||
MinioConfig config.Minio
|
||||
LocalCacheConfig config.LocalCache
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
rdb, err := dbb.Redis(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logdb, err := mgo.NewLogMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s3db, err := mgo.NewS3Mongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var thirdCache cache.ThirdCache
|
||||
if rdb == nil {
|
||||
tc, err := mgo.NewCacheMgo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
thirdCache = mcache.NewThirdCache(tc)
|
||||
} else {
|
||||
thirdCache = redis.NewThirdCache(rdb)
|
||||
}
|
||||
// Select the oss method according to the profile policy
|
||||
var o s3.Interface
|
||||
switch enable := config.RpcConfig.Object.Enable; enable {
|
||||
case "minio":
|
||||
var minioCache minio.Cache
|
||||
if rdb == nil {
|
||||
mc, err := mgo.NewCacheMgo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
minioCache = mcache.NewMinioCache(mc)
|
||||
} else {
|
||||
minioCache = redis.NewMinioCache(rdb)
|
||||
}
|
||||
o, err = minio.NewMinio(ctx, minioCache, *config.MinioConfig.Build())
|
||||
case "cos":
|
||||
o, err = cos.NewCos(*config.RpcConfig.Object.Cos.Build())
|
||||
case "oss":
|
||||
o, err = oss.NewOSS(*config.RpcConfig.Object.Oss.Build())
|
||||
case "kodo":
|
||||
o, err = kodo.NewKodo(*config.RpcConfig.Object.Kodo.Build())
|
||||
case "aws":
|
||||
// 使用自定义 R2 客户端支持 Cloudflare R2(需要自定义 endpoint)
|
||||
awsConf := config.RpcConfig.Object.Aws
|
||||
if awsConf.Endpoint != "" {
|
||||
// 如果配置了 endpoint,使用 R2 客户端
|
||||
o, err = NewR2(R2Config{
|
||||
Endpoint: awsConf.Endpoint,
|
||||
Region: awsConf.Region,
|
||||
Bucket: awsConf.Bucket,
|
||||
AccessKeyID: awsConf.AccessKeyID,
|
||||
SecretAccessKey: awsConf.SecretAccessKey,
|
||||
SessionToken: awsConf.SessionToken,
|
||||
})
|
||||
} else {
|
||||
// 标准 AWS S3
|
||||
o, err = aws.NewAws(*awsConf.Build())
|
||||
}
|
||||
case "":
|
||||
o = disable.NewDisable()
|
||||
default:
|
||||
err = fmt.Errorf("invalid object enable: %s", enable)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localcache.InitLocalCache(&config.LocalCacheConfig)
|
||||
third.RegisterThirdServer(server, &thirdServer{
|
||||
thirdDatabase: controller.NewThirdDatabase(thirdCache, logdb),
|
||||
s3dataBase: controller.NewS3Database(rdb, o, s3db),
|
||||
defaultExpire: time.Hour * 24 * 7,
|
||||
config: config,
|
||||
s3: o,
|
||||
userClient: rpcli.NewUserClient(userConn),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *thirdServer) FcmUpdateToken(ctx context.Context, req *third.FcmUpdateTokenReq) (resp *third.FcmUpdateTokenResp, err error) {
|
||||
err = t.thirdDatabase.FcmUpdateToken(ctx, req.Account, int(req.PlatformID), req.FcmToken, req.ExpireTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &third.FcmUpdateTokenResp{}, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return &third.SetAppBadgeResp{}, nil
|
||||
}
|
||||
88
internal/rpc/third/tool.go
Normal file
88
internal/rpc/third/tool.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright © 2023 OpenIM. 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 third
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/protocol/third"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
func toPbMapArray(m map[string][]string) []*third.KeyValues {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]*third.KeyValues, 0, len(m))
|
||||
for key := range m {
|
||||
res = append(res, &third.KeyValues{
|
||||
Key: key,
|
||||
Values: m[key],
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (t *thirdServer) checkUploadName(ctx context.Context, name string) error {
|
||||
if name == "" {
|
||||
return errs.ErrArgs.WrapMsg("name is empty")
|
||||
}
|
||||
if name[0] == '/' {
|
||||
return errs.ErrArgs.WrapMsg("name cannot start with `/`")
|
||||
}
|
||||
if err := checkValidObjectName(name); err != nil {
|
||||
return errs.ErrArgs.WrapMsg(err.Error())
|
||||
}
|
||||
opUserID := mcontext.GetOpUserID(ctx)
|
||||
if opUserID == "" {
|
||||
return errs.ErrNoPermission.WrapMsg("opUserID is empty")
|
||||
}
|
||||
if !authverify.CheckUserIsAdmin(ctx, opUserID) {
|
||||
if !strings.HasPrefix(name, opUserID+"/") {
|
||||
return errs.ErrNoPermission.WrapMsg(fmt.Sprintf("name must start with `%s/`", opUserID))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkValidObjectNamePrefix(objectName string) error {
|
||||
if len(objectName) > 1024 {
|
||||
return errs.New("object name cannot be longer than 1024 characters")
|
||||
}
|
||||
if !utf8.ValidString(objectName) {
|
||||
return errs.New("object name with non UTF-8 strings are not supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkValidObjectName(objectName string) error {
|
||||
if strings.TrimSpace(objectName) == "" {
|
||||
return errs.New("object name cannot be empty")
|
||||
}
|
||||
return checkValidObjectNamePrefix(objectName)
|
||||
}
|
||||
|
||||
func putUpdate[T any](update map[string]any, name string, val interface{ GetValuePtr() *T }) {
|
||||
ptrVal := val.GetValuePtr()
|
||||
if ptrVal == nil {
|
||||
return
|
||||
}
|
||||
update[name] = *ptrVal
|
||||
}
|
||||
127
internal/rpc/user/callback.go
Normal file
127
internal/rpc/user/callback.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright © 2023 OpenIM. 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 user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
|
||||
cbapi "git.imall.cloud/openim/open-im-server-deploy/pkg/callbackstruct"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
pbuser "git.imall.cloud/openim/protocol/user"
|
||||
)
|
||||
|
||||
func (s *userServer) webhookBeforeUpdateUserInfo(ctx context.Context, before *config.BeforeConfig, req *pbuser.UpdateUserInfoReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeUpdateUserInfoReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeUpdateUserInfoCommand,
|
||||
UserID: req.UserInfo.UserID,
|
||||
FaceURL: &req.UserInfo.FaceURL,
|
||||
Nickname: &req.UserInfo.Nickname,
|
||||
UserType: &req.UserInfo.UserType,
|
||||
UserFlag: &req.UserInfo.UserFlag,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeUpdateUserInfoResp{}
|
||||
if err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datautil.NotNilReplace(&req.UserInfo.FaceURL, resp.FaceURL)
|
||||
datautil.NotNilReplace(&req.UserInfo.Ex, resp.Ex)
|
||||
datautil.NotNilReplace(&req.UserInfo.Nickname, resp.Nickname)
|
||||
datautil.NotNilReplace(&req.UserInfo.UserType, resp.UserType)
|
||||
datautil.NotNilReplace(&req.UserInfo.UserFlag, resp.UserFlag)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServer) webhookAfterUpdateUserInfo(ctx context.Context, after *config.AfterConfig, req *pbuser.UpdateUserInfoReq) {
|
||||
cbReq := &cbapi.CallbackAfterUpdateUserInfoReq{
|
||||
CallbackCommand: cbapi.CallbackAfterUpdateUserInfoCommand,
|
||||
UserID: req.UserInfo.UserID,
|
||||
FaceURL: req.UserInfo.FaceURL,
|
||||
Nickname: req.UserInfo.Nickname,
|
||||
UserType: req.UserInfo.UserType,
|
||||
UserFlag: req.UserInfo.UserFlag,
|
||||
}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterUpdateUserInfoResp{}, after)
|
||||
}
|
||||
|
||||
func (s *userServer) webhookBeforeUpdateUserInfoEx(ctx context.Context, before *config.BeforeConfig, req *pbuser.UpdateUserInfoExReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeUpdateUserInfoExReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeUpdateUserInfoExCommand,
|
||||
UserID: req.UserInfo.UserID,
|
||||
FaceURL: req.UserInfo.FaceURL,
|
||||
Nickname: req.UserInfo.Nickname,
|
||||
UserType: req.UserInfo.UserType,
|
||||
UserFlag: req.UserInfo.UserFlag,
|
||||
}
|
||||
resp := &cbapi.CallbackBeforeUpdateUserInfoExResp{}
|
||||
if err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datautil.NotNilReplace(req.UserInfo.FaceURL, resp.FaceURL)
|
||||
datautil.NotNilReplace(req.UserInfo.Ex, resp.Ex)
|
||||
datautil.NotNilReplace(req.UserInfo.Nickname, resp.Nickname)
|
||||
datautil.NotNilReplace(req.UserInfo.UserType, resp.UserType)
|
||||
datautil.NotNilReplace(req.UserInfo.UserFlag, resp.UserFlag)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServer) webhookAfterUpdateUserInfoEx(ctx context.Context, after *config.AfterConfig, req *pbuser.UpdateUserInfoExReq) {
|
||||
cbReq := &cbapi.CallbackAfterUpdateUserInfoExReq{
|
||||
CallbackCommand: cbapi.CallbackAfterUpdateUserInfoExCommand,
|
||||
UserID: req.UserInfo.UserID,
|
||||
FaceURL: req.UserInfo.FaceURL,
|
||||
Nickname: req.UserInfo.Nickname,
|
||||
UserType: req.UserInfo.UserType,
|
||||
UserFlag: req.UserInfo.UserFlag,
|
||||
}
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterUpdateUserInfoExResp{}, after)
|
||||
}
|
||||
|
||||
func (s *userServer) webhookBeforeUserRegister(ctx context.Context, before *config.BeforeConfig, req *pbuser.UserRegisterReq) error {
|
||||
return webhook.WithCondition(ctx, before, func(ctx context.Context) error {
|
||||
cbReq := &cbapi.CallbackBeforeUserRegisterReq{
|
||||
CallbackCommand: cbapi.CallbackBeforeUserRegisterCommand,
|
||||
Users: req.Users,
|
||||
}
|
||||
|
||||
resp := &cbapi.CallbackBeforeUserRegisterResp{}
|
||||
|
||||
if err := s.webhookClient.SyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, resp, before); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resp.Users) != 0 {
|
||||
req.Users = resp.Users
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServer) webhookAfterUserRegister(ctx context.Context, after *config.AfterConfig, req *pbuser.UserRegisterReq) {
|
||||
cbReq := &cbapi.CallbackAfterUserRegisterReq{
|
||||
CallbackCommand: cbapi.CallbackAfterUserRegisterCommand,
|
||||
Users: req.Users,
|
||||
}
|
||||
|
||||
s.webhookClient.AsyncPost(ctx, cbReq.GetCallbackCommand(), cbReq, &cbapi.CallbackAfterUserRegisterResp{}, after)
|
||||
}
|
||||
71
internal/rpc/user/config.go
Normal file
71
internal/rpc/user/config.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
pbuser "git.imall.cloud/openim/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.CheckAccess(ctx, req.UserID); 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); 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); 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); 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
|
||||
}
|
||||
126
internal/rpc/user/notification.go
Normal file
126
internal/rpc/user/notification.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright © 2023 OpenIM. 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 user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
"git.imall.cloud/openim/protocol/msg"
|
||||
|
||||
relationtb "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification/common_user"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/notification"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
)
|
||||
|
||||
type UserNotificationSender struct {
|
||||
*notification.NotificationSender
|
||||
getUsersInfo func(ctx context.Context, userIDs []string) ([]common_user.CommonUser, error)
|
||||
// db controller
|
||||
db controller.UserDatabase
|
||||
}
|
||||
|
||||
type userNotificationSenderOptions func(*UserNotificationSender)
|
||||
|
||||
func WithUserDB(db controller.UserDatabase) userNotificationSenderOptions {
|
||||
return func(u *UserNotificationSender) {
|
||||
u.db = db
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserFunc(
|
||||
fn func(ctx context.Context, userIDs []string) (users []*relationtb.User, err error),
|
||||
) userNotificationSenderOptions {
|
||||
return func(u *UserNotificationSender) {
|
||||
f := func(ctx context.Context, userIDs []string) (result []common_user.CommonUser, err error) {
|
||||
users, err := fn(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
result = append(result, user)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
u.getUsersInfo = f
|
||||
}
|
||||
}
|
||||
|
||||
func NewUserNotificationSender(config *Config, msgClient *rpcli.MsgClient, opts ...userNotificationSenderOptions) *UserNotificationSender {
|
||||
f := &UserNotificationSender{
|
||||
NotificationSender: notification.NewNotificationSender(&config.NotificationConfig, notification.WithRpcClient(func(ctx context.Context, req *msg.SendMsgReq) (*msg.SendMsgResp, error) {
|
||||
return msgClient.SendMsg(ctx, req)
|
||||
})),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
/* func (u *UserNotificationSender) getUsersInfoMap(
|
||||
ctx context.Context,
|
||||
userIDs []string,
|
||||
) (map[string]*sdkws.UserInfo, error) {
|
||||
users, err := u.getUsersInfo(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]*sdkws.UserInfo)
|
||||
for _, user := range users {
|
||||
result[user.GetUserID()] = user.(*sdkws.UserInfo)
|
||||
}
|
||||
return result, nil
|
||||
} */
|
||||
|
||||
/* func (u *UserNotificationSender) getFromToUserNickname(
|
||||
ctx context.Context,
|
||||
fromUserID, toUserID string,
|
||||
) (string, string, error) {
|
||||
users, err := u.getUsersInfoMap(ctx, []string{fromUserID, toUserID})
|
||||
if err != nil {
|
||||
return "", "", nil
|
||||
}
|
||||
return users[fromUserID].Nickname, users[toUserID].Nickname, nil
|
||||
} */
|
||||
|
||||
func (u *UserNotificationSender) UserStatusChangeNotification(
|
||||
ctx context.Context,
|
||||
tips *sdkws.UserStatusChangeTips,
|
||||
) {
|
||||
u.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserStatusChangeNotification, tips)
|
||||
}
|
||||
func (u *UserNotificationSender) UserCommandUpdateNotification(
|
||||
ctx context.Context,
|
||||
tips *sdkws.UserCommandUpdateTips,
|
||||
) {
|
||||
u.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserCommandUpdateNotification, tips)
|
||||
}
|
||||
func (u *UserNotificationSender) UserCommandAddNotification(
|
||||
ctx context.Context,
|
||||
tips *sdkws.UserCommandAddTips,
|
||||
) {
|
||||
u.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserCommandAddNotification, tips)
|
||||
}
|
||||
func (u *UserNotificationSender) UserCommandDeleteNotification(
|
||||
ctx context.Context,
|
||||
tips *sdkws.UserCommandDeleteTips,
|
||||
) {
|
||||
u.Notification(ctx, tips.FromUserID, tips.ToUserID, constant.UserCommandDeleteNotification, tips)
|
||||
}
|
||||
104
internal/rpc/user/online.go
Normal file
104
internal/rpc/user/online.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
pbuser "git.imall.cloud/openim/protocol/user"
|
||||
)
|
||||
|
||||
func (s *userServer) getUserOnlineStatus(ctx context.Context, userID string) (*pbuser.OnlineStatus, error) {
|
||||
platformIDs, err := s.online.GetOnline(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := pbuser.OnlineStatus{
|
||||
UserID: userID,
|
||||
PlatformIDs: platformIDs,
|
||||
}
|
||||
if len(platformIDs) > 0 {
|
||||
status.Status = constant.Online
|
||||
} else {
|
||||
status.Status = constant.Offline
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (s *userServer) getUsersOnlineStatus(ctx context.Context, userIDs []string) ([]*pbuser.OnlineStatus, error) {
|
||||
res := make([]*pbuser.OnlineStatus, 0, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
status, err := s.getUserOnlineStatus(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, status)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SubscribeOrCancelUsersStatus Subscribe online or cancel online users.
|
||||
func (s *userServer) SubscribeOrCancelUsersStatus(ctx context.Context, req *pbuser.SubscribeOrCancelUsersStatusReq) (*pbuser.SubscribeOrCancelUsersStatusResp, error) {
|
||||
return &pbuser.SubscribeOrCancelUsersStatusResp{}, nil
|
||||
}
|
||||
|
||||
// GetUserStatus Get the online status of the user.
|
||||
func (s *userServer) GetUserStatus(ctx context.Context, req *pbuser.GetUserStatusReq) (*pbuser.GetUserStatusResp, error) {
|
||||
res, err := s.getUsersOnlineStatus(ctx, req.UserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.GetUserStatusResp{StatusList: res}, nil
|
||||
}
|
||||
|
||||
// SetUserStatus Synchronize user's online status.
|
||||
func (s *userServer) SetUserStatus(ctx context.Context, req *pbuser.SetUserStatusReq) (*pbuser.SetUserStatusResp, error) {
|
||||
var (
|
||||
online []int32
|
||||
offline []int32
|
||||
)
|
||||
switch req.Status {
|
||||
case constant.Online:
|
||||
online = []int32{req.PlatformID}
|
||||
case constant.Offline:
|
||||
offline = []int32{req.PlatformID}
|
||||
}
|
||||
if err := s.online.SetUserOnline(ctx, req.UserID, online, offline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.SetUserStatusResp{}, nil
|
||||
}
|
||||
|
||||
// GetSubscribeUsersStatus Get the online status of subscribers.
|
||||
func (s *userServer) GetSubscribeUsersStatus(ctx context.Context, req *pbuser.GetSubscribeUsersStatusReq) (*pbuser.GetSubscribeUsersStatusResp, error) {
|
||||
return &pbuser.GetSubscribeUsersStatusResp{}, nil
|
||||
}
|
||||
|
||||
func (s *userServer) SetUserOnlineStatus(ctx context.Context, req *pbuser.SetUserOnlineStatusReq) (*pbuser.SetUserOnlineStatusResp, error) {
|
||||
for _, status := range req.Status {
|
||||
if err := s.online.SetUserOnline(ctx, status.UserID, status.Online, status.Offline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &pbuser.SetUserOnlineStatusResp{}, nil
|
||||
}
|
||||
|
||||
func (s *userServer) GetAllOnlineUsers(ctx context.Context, req *pbuser.GetAllOnlineUsersReq) (*pbuser.GetAllOnlineUsersResp, error) {
|
||||
resMap, nextCursor, err := s.online.GetAllOnlineUsers(ctx, req.Cursor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &pbuser.GetAllOnlineUsersResp{
|
||||
StatusList: make([]*pbuser.OnlineStatus, 0, len(resMap)),
|
||||
NextCursor: nextCursor,
|
||||
}
|
||||
for userID, plats := range resMap {
|
||||
resp.StatusList = append(resp.StatusList, &pbuser.OnlineStatus{
|
||||
UserID: userID,
|
||||
Status: int32(datautil.If(len(plats) > 0, constant.Online, constant.Offline)),
|
||||
PlatformIDs: plats,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
43
internal/rpc/user/statistics.go
Normal file
43
internal/rpc/user/statistics.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright © 2023 OpenIM. 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 user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
pbuser "git.imall.cloud/openim/protocol/user"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
func (s *userServer) UserRegisterCount(ctx context.Context, req *pbuser.UserRegisterCountReq) (*pbuser.UserRegisterCountResp, error) {
|
||||
if req.Start > req.End {
|
||||
return nil, errs.ErrArgs.WrapMsg("start > end")
|
||||
}
|
||||
total, err := s.db.CountTotal(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.UnixMilli(req.Start)
|
||||
before, err := s.db.CountTotal(ctx, &start)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count, err := s.db.CountRangeEverydayTotal(ctx, start, time.UnixMilli(req.End))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.UserRegisterCountResp{Total: total, Before: before, Count: count}, nil
|
||||
}
|
||||
730
internal/rpc/user/user.go
Normal file
730
internal/rpc/user/user.go
Normal file
@@ -0,0 +1,730 @@
|
||||
// Copyright © 2023 OpenIM. 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 user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/open-im-server-deploy/internal/rpc/relation"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/authverify"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/config"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/convert"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/prommetrics"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/redis"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/controller"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/database/mgo"
|
||||
tablerelation "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/model"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/webhook"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/dbbuild"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/localcache"
|
||||
"git.imall.cloud/openim/open-im-server-deploy/pkg/rpcli"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/group"
|
||||
friendpb "git.imall.cloud/openim/protocol/relation"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
pbuser "git.imall.cloud/openim/protocol/user"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSecret = "openIM123"
|
||||
)
|
||||
|
||||
type userServer struct {
|
||||
pbuser.UnimplementedUserServer
|
||||
online cache.OnlineCache
|
||||
db controller.UserDatabase
|
||||
friendNotificationSender *relation.FriendNotificationSender
|
||||
userNotificationSender *UserNotificationSender
|
||||
RegisterCenter discovery.Conn
|
||||
config *Config
|
||||
webhookClient *webhook.Client
|
||||
groupClient *rpcli.GroupClient
|
||||
relationClient *rpcli.RelationClient
|
||||
clientConfig controller.ClientConfigDatabase
|
||||
|
||||
adminUserIDs []string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.User
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
KafkaConfig config.Kafka
|
||||
NotificationConfig config.Notification
|
||||
Share config.Share
|
||||
WebhooksConfig config.Webhooks
|
||||
LocalCacheConfig config.LocalCache
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
rdb, err := dbb.Redis(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users := make([]*tablerelation.User, 0)
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
groupConn, err := client.GetConn(ctx, config.Discovery.RpcService.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
friendConn, err := client.GetConn(ctx, config.Discovery.RpcService.Friend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgClient := rpcli.NewMsgClient(msgConn)
|
||||
userCache := redis.NewUserCacheRedis(rdb, &config.LocalCacheConfig, userDB, redis.GetRocksCacheOptions())
|
||||
database := controller.NewUserDatabase(userDB, userCache, mgocli.GetTx())
|
||||
localcache.InitLocalCache(&config.LocalCacheConfig)
|
||||
|
||||
// 初始化webhook配置管理器(支持从数据库读取配置)
|
||||
var webhookClient *webhook.Client
|
||||
log.ZInfo(ctx, "initializing webhook config manager...", "default_url", config.WebhooksConfig.URL)
|
||||
systemConfigDB, err := mgo.NewSystemConfigMongo(mgocli.GetDB())
|
||||
if err == nil {
|
||||
// 如果SystemConfig数据库初始化成功,使用配置管理器
|
||||
log.ZInfo(ctx, "system config db initialized successfully, creating webhook config manager")
|
||||
webhookConfigManager := webhook.NewConfigManager(systemConfigDB, &config.WebhooksConfig)
|
||||
if err := webhookConfigManager.Start(ctx); err != nil {
|
||||
log.ZWarn(ctx, "failed to start webhook config manager, using default config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
} else {
|
||||
log.ZInfo(ctx, "webhook config manager started, using dynamic config")
|
||||
webhookClient = webhook.NewWebhookClientWithManager(webhookConfigManager)
|
||||
}
|
||||
} else {
|
||||
// 如果SystemConfig数据库初始化失败,使用默认配置
|
||||
log.ZWarn(ctx, "failed to init system config db, using default webhook config", err)
|
||||
webhookClient = webhook.NewWebhookClient(config.WebhooksConfig.URL)
|
||||
}
|
||||
|
||||
u := &userServer{
|
||||
online: redis.NewUserOnline(rdb),
|
||||
db: database,
|
||||
RegisterCenter: client,
|
||||
friendNotificationSender: relation.NewFriendNotificationSender(&config.NotificationConfig, msgClient, relation.WithDBFunc(database.FindWithError)),
|
||||
userNotificationSender: NewUserNotificationSender(config, msgClient, WithUserFunc(database.FindWithError)),
|
||||
config: config,
|
||||
webhookClient: webhookClient,
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *userServer) GetDesignateUsers(ctx context.Context, req *pbuser.GetDesignateUsersReq) (resp *pbuser.GetDesignateUsersResp, err error) {
|
||||
resp = &pbuser.GetDesignateUsersResp{}
|
||||
users, err := s.db.Find(ctx, req.UserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.UsersInfo = convert.UsersDB2Pb(users)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// deprecated:
|
||||
// UpdateUserInfo
|
||||
func (s *userServer) UpdateUserInfo(ctx context.Context, req *pbuser.UpdateUserInfoReq) (resp *pbuser.UpdateUserInfoResp, err error) {
|
||||
resp = &pbuser.UpdateUserInfoResp{}
|
||||
err = authverify.CheckAccess(ctx, req.UserInfo.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.webhookBeforeUpdateUserInfo(ctx, &s.config.WebhooksConfig.BeforeUpdateUserInfo, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := convert.UserPb2DBMap(req.UserInfo)
|
||||
oldUser, err := s.db.GetUserByID(ctx, req.UserInfo.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.UpdateByMap(ctx, req.UserInfo.UserID, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserInfo.UserID)
|
||||
|
||||
s.webhookAfterUpdateUserInfo(ctx, &s.config.WebhooksConfig.AfterUpdateUserInfo, req)
|
||||
if err = s.NotificationUserInfoUpdate(ctx, req.UserInfo.UserID, oldUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *userServer) UpdateUserInfoEx(ctx context.Context, req *pbuser.UpdateUserInfoExReq) (resp *pbuser.UpdateUserInfoExResp, err error) {
|
||||
resp = &pbuser.UpdateUserInfoExResp{}
|
||||
err = authverify.CheckAccess(ctx, req.UserInfo.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.webhookBeforeUpdateUserInfoEx(ctx, &s.config.WebhooksConfig.BeforeUpdateUserInfoEx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldUser, err := s.db.GetUserByID(ctx, req.UserInfo.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := convert.UserPb2DBMapEx(req.UserInfo)
|
||||
if err = s.db.UpdateByMap(ctx, req.UserInfo.UserID, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserInfo.UserID)
|
||||
|
||||
s.webhookAfterUpdateUserInfoEx(ctx, &s.config.WebhooksConfig.AfterUpdateUserInfoEx, req)
|
||||
if err := s.NotificationUserInfoUpdate(ctx, req.UserInfo.UserID, oldUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
func (s *userServer) SetGlobalRecvMessageOpt(ctx context.Context, req *pbuser.SetGlobalRecvMessageOptReq) (resp *pbuser.SetGlobalRecvMessageOptResp, err error) {
|
||||
resp = &pbuser.SetGlobalRecvMessageOptResp{}
|
||||
if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]any, 1)
|
||||
m["global_recv_msg_opt"] = req.GlobalRecvMsgOpt
|
||||
if err := s.db.UpdateByMap(ctx, req.UserID, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.friendNotificationSender.UserInfoUpdatedNotification(ctx, req.UserID)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *userServer) AccountCheck(ctx context.Context, req *pbuser.AccountCheckReq) (resp *pbuser.AccountCheckResp, err error) {
|
||||
resp = &pbuser.AccountCheckResp{}
|
||||
if datautil.Duplicate(req.CheckUserIDs) {
|
||||
return nil, errs.ErrArgs.WrapMsg("userID repeated")
|
||||
}
|
||||
if err = authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := s.db.Find(ctx, req.CheckUserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs := make(map[string]any, 0)
|
||||
for _, v := range users {
|
||||
userIDs[v.UserID] = nil
|
||||
}
|
||||
for _, v := range req.CheckUserIDs {
|
||||
temp := &pbuser.AccountCheckRespSingleUserStatus{UserID: v}
|
||||
if _, ok := userIDs[v]; ok {
|
||||
temp.AccountStatus = constant.Registered
|
||||
} else {
|
||||
temp.AccountStatus = constant.UnRegistered
|
||||
}
|
||||
resp.Results = append(resp.Results, temp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *userServer) GetPaginationUsers(ctx context.Context, req *pbuser.GetPaginationUsersReq) (resp *pbuser.GetPaginationUsersResp, err error) {
|
||||
if req.UserID == "" && req.NickName == "" {
|
||||
total, users, err := s.db.PageFindUser(ctx, constant.IMOrdinaryUser, constant.AppOrdinaryUsers, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.GetPaginationUsersResp{Total: int32(total), Users: convert.UsersDB2Pb(users)}, err
|
||||
} else {
|
||||
total, users, err := s.db.PageFindUserWithKeyword(ctx, constant.IMOrdinaryUser, constant.AppOrdinaryUsers, req.UserID, req.NickName, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.GetPaginationUsersResp{Total: int32(total), Users: convert.UsersDB2Pb(users)}, err
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *userServer) UserRegister(ctx context.Context, req *pbuser.UserRegisterReq) (resp *pbuser.UserRegisterResp, err error) {
|
||||
resp = &pbuser.UserRegisterResp{}
|
||||
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); 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")
|
||||
}
|
||||
userIDs := make([]string, 0)
|
||||
for _, user := range req.Users {
|
||||
if user.UserID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("userID is empty")
|
||||
}
|
||||
if strings.Contains(user.UserID, ":") {
|
||||
return nil, errs.ErrArgs.WrapMsg("userID contains ':' is invalid userID")
|
||||
}
|
||||
userIDs = append(userIDs, user.UserID)
|
||||
}
|
||||
exist, err := s.db.IsExist(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
return nil, servererrs.ErrRegisteredAlready.WrapMsg("userID registered already")
|
||||
}
|
||||
if err := s.webhookBeforeUserRegister(ctx, &s.config.WebhooksConfig.BeforeUserRegister, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
users := make([]*tablerelation.User, 0, len(req.Users))
|
||||
for _, user := range req.Users {
|
||||
users = append(users, &tablerelation.User{
|
||||
UserID: user.UserID,
|
||||
Nickname: user.Nickname,
|
||||
FaceURL: user.FaceURL,
|
||||
Ex: user.Ex,
|
||||
CreateTime: now,
|
||||
AppMangerLevel: user.AppMangerLevel,
|
||||
GlobalRecvMsgOpt: user.GlobalRecvMsgOpt,
|
||||
})
|
||||
}
|
||||
if err := s.db.Create(ctx, users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prommetrics.UserRegisterCounter.Add(float64(len(users)))
|
||||
|
||||
s.webhookAfterUserRegister(ctx, &s.config.WebhooksConfig.AfterUserRegister, req)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *userServer) GetGlobalRecvMessageOpt(ctx context.Context, req *pbuser.GetGlobalRecvMessageOptReq) (resp *pbuser.GetGlobalRecvMessageOptResp, err error) {
|
||||
user, err := s.db.FindWithError(ctx, []string{req.UserID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.GetGlobalRecvMessageOptResp{GlobalRecvMsgOpt: user[0].GlobalRecvMsgOpt}, nil
|
||||
}
|
||||
|
||||
// GetAllUserID Get user account by page.
|
||||
func (s *userServer) GetAllUserID(ctx context.Context, req *pbuser.GetAllUserIDReq) (resp *pbuser.GetAllUserIDResp, err error) {
|
||||
total, userIDs, err := s.db.GetAllUserID(ctx, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.GetAllUserIDResp{Total: int32(total), UserIDs: userIDs}, nil
|
||||
}
|
||||
|
||||
// ProcessUserCommandAdd user general function add.
|
||||
func (s *userServer) ProcessUserCommandAdd(ctx context.Context, req *pbuser.ProcessUserCommandAddReq) (*pbuser.ProcessUserCommandAddResp, error) {
|
||||
err := authverify.CheckAccess(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var value string
|
||||
if req.Value != nil {
|
||||
value = req.Value.Value
|
||||
}
|
||||
var ex string
|
||||
if req.Ex != nil {
|
||||
value = req.Ex.Value
|
||||
}
|
||||
// Assuming you have a method in s.storage to add a user command
|
||||
err = s.db.AddUserCommand(ctx, req.UserID, req.Type, req.Uuid, value, ex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tips := &sdkws.UserCommandAddTips{
|
||||
FromUserID: req.UserID,
|
||||
ToUserID: req.UserID,
|
||||
}
|
||||
s.userNotificationSender.UserCommandAddNotification(ctx, tips)
|
||||
return &pbuser.ProcessUserCommandAddResp{}, nil
|
||||
}
|
||||
|
||||
// ProcessUserCommandDelete user general function delete.
|
||||
func (s *userServer) ProcessUserCommandDelete(ctx context.Context, req *pbuser.ProcessUserCommandDeleteReq) (*pbuser.ProcessUserCommandDeleteResp, error) {
|
||||
err := authverify.CheckAccess(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.db.DeleteUserCommand(ctx, req.UserID, req.Type, req.Uuid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tips := &sdkws.UserCommandDeleteTips{
|
||||
FromUserID: req.UserID,
|
||||
ToUserID: req.UserID,
|
||||
}
|
||||
s.userNotificationSender.UserCommandDeleteNotification(ctx, tips)
|
||||
return &pbuser.ProcessUserCommandDeleteResp{}, nil
|
||||
}
|
||||
|
||||
// ProcessUserCommandUpdate user general function update.
|
||||
func (s *userServer) ProcessUserCommandUpdate(ctx context.Context, req *pbuser.ProcessUserCommandUpdateReq) (*pbuser.ProcessUserCommandUpdateResp, error) {
|
||||
err := authverify.CheckAccess(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val := make(map[string]any)
|
||||
|
||||
// Map fields from eax to val
|
||||
if req.Value != nil {
|
||||
val["value"] = req.Value.Value
|
||||
}
|
||||
if req.Ex != nil {
|
||||
val["ex"] = req.Ex.Value
|
||||
}
|
||||
|
||||
// Assuming you have a method in s.storage to update a user command
|
||||
err = s.db.UpdateUserCommand(ctx, req.UserID, req.Type, req.Uuid, val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tips := &sdkws.UserCommandUpdateTips{
|
||||
FromUserID: req.UserID,
|
||||
ToUserID: req.UserID,
|
||||
}
|
||||
s.userNotificationSender.UserCommandUpdateNotification(ctx, tips)
|
||||
return &pbuser.ProcessUserCommandUpdateResp{}, nil
|
||||
}
|
||||
|
||||
func (s *userServer) ProcessUserCommandGet(ctx context.Context, req *pbuser.ProcessUserCommandGetReq) (*pbuser.ProcessUserCommandGetResp, error) {
|
||||
|
||||
err := authverify.CheckAccess(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Fetch user commands from the database
|
||||
commands, err := s.db.GetUserCommands(ctx, req.UserID, req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize commandInfoSlice as an empty slice
|
||||
commandInfoSlice := make([]*pbuser.CommandInfoResp, 0, len(commands))
|
||||
|
||||
for _, command := range commands {
|
||||
// No need to use index since command is already a pointer
|
||||
commandInfoSlice = append(commandInfoSlice, &pbuser.CommandInfoResp{
|
||||
Type: command.Type,
|
||||
Uuid: command.Uuid,
|
||||
Value: command.Value,
|
||||
CreateTime: command.CreateTime,
|
||||
Ex: command.Ex,
|
||||
})
|
||||
}
|
||||
|
||||
// Return the response with the slice
|
||||
return &pbuser.ProcessUserCommandGetResp{CommandResp: commandInfoSlice}, nil
|
||||
}
|
||||
|
||||
func (s *userServer) ProcessUserCommandGetAll(ctx context.Context, req *pbuser.ProcessUserCommandGetAllReq) (*pbuser.ProcessUserCommandGetAllResp, error) {
|
||||
err := authverify.CheckAccess(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Fetch user commands from the database
|
||||
commands, err := s.db.GetAllUserCommands(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize commandInfoSlice as an empty slice
|
||||
commandInfoSlice := make([]*pbuser.AllCommandInfoResp, 0, len(commands))
|
||||
|
||||
for _, command := range commands {
|
||||
// No need to use index since command is already a pointer
|
||||
commandInfoSlice = append(commandInfoSlice, &pbuser.AllCommandInfoResp{
|
||||
Type: command.Type,
|
||||
Uuid: command.Uuid,
|
||||
Value: command.Value,
|
||||
CreateTime: command.CreateTime,
|
||||
Ex: command.Ex,
|
||||
})
|
||||
}
|
||||
|
||||
// Return the response with the slice
|
||||
return &pbuser.ProcessUserCommandGetAllResp{CommandResp: commandInfoSlice}, nil
|
||||
}
|
||||
|
||||
func (s *userServer) AddNotificationAccount(ctx context.Context, req *pbuser.AddNotificationAccountReq) (*pbuser.AddNotificationAccountResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.AppMangerLevel < constant.AppNotificationAdmin {
|
||||
return nil, errs.ErrArgs.WithDetail("app level not supported")
|
||||
}
|
||||
if req.UserID == "" {
|
||||
for i := 0; i < 20; i++ {
|
||||
userId := s.genUserID()
|
||||
_, err := s.db.FindWithError(ctx, []string{userId})
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
req.UserID = userId
|
||||
break
|
||||
}
|
||||
if req.UserID == "" {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("gen user id failed")
|
||||
}
|
||||
} else {
|
||||
_, err := s.db.FindWithError(ctx, []string{req.UserID})
|
||||
if err == nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("userID is used")
|
||||
}
|
||||
}
|
||||
|
||||
user := &tablerelation.User{
|
||||
UserID: req.UserID,
|
||||
Nickname: req.NickName,
|
||||
FaceURL: req.FaceURL,
|
||||
CreateTime: time.Now(),
|
||||
AppMangerLevel: req.AppMangerLevel,
|
||||
}
|
||||
if err := s.db.Create(ctx, []*tablerelation.User{user}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pbuser.AddNotificationAccountResp{
|
||||
UserID: req.UserID,
|
||||
NickName: req.NickName,
|
||||
FaceURL: req.FaceURL,
|
||||
AppMangerLevel: req.AppMangerLevel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *userServer) UpdateNotificationAccountInfo(ctx context.Context, req *pbuser.UpdateNotificationAccountInfoReq) (*pbuser.UpdateNotificationAccountInfoResp, error) {
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := s.db.FindWithError(ctx, []string{req.UserID}); err != nil {
|
||||
return nil, errs.ErrArgs.Wrap()
|
||||
}
|
||||
|
||||
user := map[string]interface{}{}
|
||||
|
||||
if req.NickName != "" {
|
||||
user["nickname"] = req.NickName
|
||||
}
|
||||
|
||||
if req.FaceURL != "" {
|
||||
user["face_url"] = req.FaceURL
|
||||
}
|
||||
|
||||
if err := s.db.UpdateByMap(ctx, req.UserID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pbuser.UpdateNotificationAccountInfoResp{}, nil
|
||||
}
|
||||
|
||||
func (s *userServer) SearchNotificationAccount(ctx context.Context, req *pbuser.SearchNotificationAccountReq) (*pbuser.SearchNotificationAccountResp, error) {
|
||||
// Check if user is an admin
|
||||
if err := authverify.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []*tablerelation.User
|
||||
var err error
|
||||
|
||||
// If a keyword is provided in the request
|
||||
if req.Keyword != "" {
|
||||
// Find users by keyword
|
||||
users, err = s.db.Find(ctx, []string{req.Keyword})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert users to response format
|
||||
resp := s.userModelToResp(users, req.Pagination, req.AppManagerLevel)
|
||||
if resp.Total != 0 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Find users by nickname if no users found by keyword
|
||||
users, err = s.db.FindByNickname(ctx, req.Keyword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = s.userModelToResp(users, req.Pagination, req.AppManagerLevel)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// If no keyword, find users with notification settings
|
||||
if req.AppManagerLevel != nil {
|
||||
users, err = s.db.FindNotification(ctx, int64(*req.AppManagerLevel))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
users, err = s.db.FindSystemAccount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp := s.userModelToResp(users, req.Pagination, req.AppManagerLevel)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *userServer) GetNotificationAccount(ctx context.Context, req *pbuser.GetNotificationAccountReq) (*pbuser.GetNotificationAccountResp, error) {
|
||||
if req.UserID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("userID is empty")
|
||||
}
|
||||
user, err := s.db.GetUserByID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, servererrs.ErrUserIDNotFound.Wrap()
|
||||
}
|
||||
if user.AppMangerLevel >= constant.AppAdmin {
|
||||
return &pbuser.GetNotificationAccountResp{Account: &pbuser.NotificationAccountInfo{
|
||||
UserID: user.UserID,
|
||||
FaceURL: user.FaceURL,
|
||||
NickName: user.Nickname,
|
||||
AppMangerLevel: user.AppMangerLevel,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
return nil, errs.ErrNoPermission.WrapMsg("notification messages cannot be sent for this ID")
|
||||
}
|
||||
|
||||
func (s *userServer) genUserID() string {
|
||||
const l = 10
|
||||
data := make([]byte, l)
|
||||
rand.Read(data)
|
||||
chars := []byte("0123456789")
|
||||
for i := 0; i < len(data); i++ {
|
||||
if i == 0 {
|
||||
data[i] = chars[1:][data[i]%9]
|
||||
} else {
|
||||
data[i] = chars[data[i]%10]
|
||||
}
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (s *userServer) userModelToResp(users []*tablerelation.User, pagination pagination.Pagination, appManagerLevel *int32) *pbuser.SearchNotificationAccountResp {
|
||||
accounts := make([]*pbuser.NotificationAccountInfo, 0)
|
||||
var total int64
|
||||
for _, v := range users {
|
||||
if v.AppMangerLevel >= constant.AppNotificationAdmin && !datautil.Contain(v.UserID, s.adminUserIDs...) {
|
||||
if appManagerLevel != nil {
|
||||
if v.AppMangerLevel != *appManagerLevel {
|
||||
continue
|
||||
}
|
||||
}
|
||||
temp := &pbuser.NotificationAccountInfo{
|
||||
UserID: v.UserID,
|
||||
FaceURL: v.FaceURL,
|
||||
NickName: v.Nickname,
|
||||
AppMangerLevel: v.AppMangerLevel,
|
||||
}
|
||||
accounts = append(accounts, temp)
|
||||
total += 1
|
||||
}
|
||||
}
|
||||
|
||||
notificationAccounts := datautil.Paginate(accounts, int(pagination.GetPageNumber()), int(pagination.GetShowNumber()))
|
||||
|
||||
return &pbuser.SearchNotificationAccountResp{Total: total, NotificationAccounts: notificationAccounts}
|
||||
}
|
||||
|
||||
func (s *userServer) NotificationUserInfoUpdate(ctx context.Context, userID string, oldUser *tablerelation.User) error {
|
||||
user, err := s.db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Nickname == oldUser.Nickname && user.FaceURL == oldUser.FaceURL && user.Ex == oldUser.Ex {
|
||||
return nil
|
||||
}
|
||||
oldUserInfo := convert.UserDB2Pb(oldUser)
|
||||
newUserInfo := convert.UserDB2Pb(user)
|
||||
var wg sync.WaitGroup
|
||||
var es [2]error
|
||||
wg.Add(len(es))
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, es[0] = s.groupClient.NotificationUserInfoUpdate(ctx, &group.NotificationUserInfoUpdateReq{
|
||||
UserID: userID,
|
||||
OldUserInfo: oldUserInfo,
|
||||
NewUserInfo: newUserInfo,
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, es[1] = s.relationClient.NotificationUserInfoUpdate(ctx, &friendpb.NotificationUserInfoUpdateReq{
|
||||
UserID: userID,
|
||||
OldUserInfo: oldUserInfo,
|
||||
NewUserInfo: newUserInfo,
|
||||
})
|
||||
}()
|
||||
wg.Wait()
|
||||
return errors.Join(es[:]...)
|
||||
}
|
||||
|
||||
func (s *userServer) SortQuery(ctx context.Context, req *pbuser.SortQueryReq) (*pbuser.SortQueryResp, error) {
|
||||
users, err := s.db.SortQuery(ctx, req.UserIDName, req.Asc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pbuser.SortQueryResp{Users: convert.UsersDB2Pb(users)}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user