mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-06-26 20:38:25 +08:00
Merge branch 'develop/tom' into feature/captcha
This commit is contained in:
commit
f73aa064ce
@ -18,3 +18,4 @@ prometheus:
|
||||
|
||||
|
||||
enableHistoryForNewMembers: true
|
||||
commonGroupsLimitWithFriend: 3
|
||||
|
||||
2
go.mod
2
go.mod
@ -2,6 +2,8 @@ module github.com/openimsdk/open-im-server/v3
|
||||
|
||||
go 1.25.0
|
||||
|
||||
replace github.com/openimsdk/protocol => ./protocol
|
||||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.14.1
|
||||
github.com/dtm-labs/rockscache v0.1.1
|
||||
|
||||
2
go.sum
2
go.sum
@ -356,8 +356,6 @@ github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
|
||||
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
|
||||
github.com/openimsdk/gomake v0.0.17 h1:q8haP48VOH45WhJRiLj1YSBJyUFJqD8CTedH65i1YH8=
|
||||
github.com/openimsdk/gomake v0.0.17/go.mod h1:nnjS8yCtrPJAt1knMbyPiUwCH2gpyBzj/EZAONfUOXg=
|
||||
github.com/openimsdk/protocol v0.0.73-alpha.12 h1:2NYawXeHChYUeSme6QJ9pOLh+Empce2WmwEtbP4JvKk=
|
||||
github.com/openimsdk/protocol v0.0.73-alpha.12/go.mod h1:WF7EuE55vQvpyUAzDXcqg+B+446xQyEba0X35lTINmw=
|
||||
github.com/openimsdk/tools v0.0.50-alpha.113 h1:rhLWaSJuhjgJFNVzmpChLCG7dPXS0+bte+CPI0008Us=
|
||||
github.com/openimsdk/tools v0.0.50-alpha.113/go.mod h1:x9i/e+WJFW4tocy6RNJQ9NofQiP3KJ1Y576/06TqOG4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
|
||||
@ -169,3 +169,7 @@ func (o *GroupApi) GetFullJoinGroupIDs(c *gin.Context) {
|
||||
func (o *GroupApi) GetGroupApplicationUnhandledCount(c *gin.Context) {
|
||||
a2r.Call(c, group.GroupClient.GetGroupApplicationUnhandledCount, o.Client)
|
||||
}
|
||||
|
||||
func (o *GroupApi) GetCommonGroupsWithFriend(c *gin.Context) {
|
||||
a2r.Call(c, group.GroupClient.GetCommonGroupsWithFriend, o.Client)
|
||||
}
|
||||
|
||||
@ -30,9 +30,10 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
API config.API
|
||||
Share config.Share
|
||||
Discovery config.Discovery
|
||||
API config.API
|
||||
Share config.Share
|
||||
Discovery config.Discovery
|
||||
MongodbConfig config.Mongo
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, index int, cfg *Config) error {
|
||||
|
||||
@ -24,8 +24,11 @@ import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/prommetrics"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
|
||||
"github.com/openimsdk/protocol/constant"
|
||||
"github.com/openimsdk/tools/apiresp"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mw"
|
||||
@ -54,6 +57,20 @@ func prommetricsGin() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, config *Config) (*gin.Engine, error) {
|
||||
mgocli, err := mongoutil.NewMongoDB(ctx, config.MongodbConfig.Build())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userGlobalBlackDB, err := mgo.NewUserGlobalBlackMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userDB, err := mgo.NewUserMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blacklistCtrl := controller.NewUserGlobalBlackDatabase(userGlobalBlackDB)
|
||||
|
||||
authConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.Auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -104,6 +121,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
||||
u := NewUserApi(user.NewUserClient(userConn), client, config.Share.RpcRegisterName)
|
||||
m := NewMessageApi(msg.NewMsgClient(msgConn), rpcli.NewUserClient(userConn), config.Share.IMAdminUserID)
|
||||
cp := NewCaptchaApi(pbcaptcha.NewCaptchaClient(captchaConn))
|
||||
bl := NewUserGlobalBlackApi(blacklistCtrl, userDB, config.Share.IMAdminUserID, rpcli.NewAuthClient(authConn))
|
||||
userRouterGroup := r.Group("/user")
|
||||
{
|
||||
userRouterGroup.POST("/user_register", u.UserRegister)
|
||||
@ -129,6 +147,11 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
||||
userRouterGroup.POST("/add_notification_account", u.AddNotificationAccount)
|
||||
userRouterGroup.POST("/update_notification_account", u.UpdateNotificationAccountInfo)
|
||||
userRouterGroup.POST("/search_notification_account", u.SearchNotificationAccount)
|
||||
|
||||
// 全局黑名单管理(仅管理员)
|
||||
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
|
||||
userRouterGroup.POST("/remove_global_blacklist", bl.RemoveGlobalBlacklist)
|
||||
userRouterGroup.POST("/get_global_blacklist", bl.GetGlobalBlacklist)
|
||||
}
|
||||
// friend routing group
|
||||
{
|
||||
@ -193,6 +216,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
||||
groupRouterGroup.POST("/get_full_group_member_user_ids", g.GetFullGroupMemberUserIDs)
|
||||
groupRouterGroup.POST("/get_full_join_group_ids", g.GetFullJoinGroupIDs)
|
||||
groupRouterGroup.POST("/get_group_application_unhandled_count", g.GetGroupApplicationUnhandledCount)
|
||||
groupRouterGroup.POST("/get_common_groups_with_friend", g.GetCommonGroupsWithFriend)
|
||||
}
|
||||
// certificate
|
||||
{
|
||||
|
||||
157
internal/api/user_global_black.go
Normal file
157
internal/api/user_global_black.go
Normal file
@ -0,0 +1,157 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/rpcli"
|
||||
"github.com/openimsdk/protocol/constant"
|
||||
"github.com/openimsdk/protocol/sdkws"
|
||||
"github.com/openimsdk/tools/apiresp"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
)
|
||||
|
||||
type UserGlobalBlackApi struct {
|
||||
blacklistDB controller.UserGlobalBlackDatabase
|
||||
userDB database.User
|
||||
imAdminUserIDs []string
|
||||
authClient *rpcli.AuthClient
|
||||
}
|
||||
|
||||
func NewUserGlobalBlackApi(blacklistDB controller.UserGlobalBlackDatabase, userDB database.User, imAdminUserIDs []string, authClient *rpcli.AuthClient) UserGlobalBlackApi {
|
||||
return UserGlobalBlackApi{blacklistDB: blacklistDB, userDB: userDB, imAdminUserIDs: imAdminUserIDs, authClient: authClient}
|
||||
}
|
||||
|
||||
type addGlobalBlacklistReq struct {
|
||||
UserIDs []string `json:"userIDs" binding:"required,min=1"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type removeGlobalBlacklistReq struct {
|
||||
UserIDs []string `json:"userIDs" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
type getGlobalBlacklistReq struct {
|
||||
Pagination *sdkws.RequestPagination `json:"pagination" binding:"required"`
|
||||
}
|
||||
|
||||
type globalBlackItem struct {
|
||||
UserID string `json:"userID"`
|
||||
Nickname string `json:"nickname"`
|
||||
OperatorID string `json:"operatorID"`
|
||||
Reason string `json:"reason"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
}
|
||||
|
||||
type getGlobalBlacklistResp struct {
|
||||
Total int64 `json:"total"`
|
||||
Blacks []globalBlackItem `json:"blacks"`
|
||||
}
|
||||
|
||||
// AddGlobalBlacklist 管理员将用户加入全局黑名单,并立即踢下线(所有平台 token 标记 KickedToken)
|
||||
func (b *UserGlobalBlackApi) AddGlobalBlacklist(c *gin.Context) {
|
||||
var req addGlobalBlacklistReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := authverify.CheckAdmin(c, b.imAdminUserIDs); err != nil {
|
||||
apiresp.GinError(c, err)
|
||||
return
|
||||
}
|
||||
operatorID := mcontext.GetOpUserID(c)
|
||||
foundUsers, err := b.userDB.Find(c, req.UserIDs)
|
||||
if err != nil {
|
||||
apiresp.GinError(c, err)
|
||||
return
|
||||
}
|
||||
userMap := make(map[string]*model.User, len(foundUsers))
|
||||
for _, u := range foundUsers {
|
||||
userMap[u.UserID] = u
|
||||
}
|
||||
blacks := make([]*model.UserGlobalBlack, 0, len(req.UserIDs))
|
||||
for _, userID := range req.UserIDs {
|
||||
u, ok := userMap[userID]
|
||||
if !ok {
|
||||
apiresp.GinError(c, errs.ErrRecordNotFound.WrapMsg("userID not found", "userID", userID))
|
||||
return
|
||||
}
|
||||
blacks = append(blacks, &model.UserGlobalBlack{
|
||||
UserID: u.UserID,
|
||||
Nickname: u.Nickname,
|
||||
OperatorID: operatorID,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
}
|
||||
if err := b.blacklistDB.AddBlack(c, blacks); err != nil {
|
||||
apiresp.GinError(c, err)
|
||||
return
|
||||
}
|
||||
// 黑名单写入成功后,对每个被封禁用户的所有非管理员平台执行 force_logout:
|
||||
// 1. 断开 WS 长连接(msggateway.KickUserOffline)
|
||||
// 2. 将 Redis 中该平台的所有 token 标记为 KickedToken
|
||||
for _, black := range blacks {
|
||||
for platformID := range constant.PlatformID2Name {
|
||||
if int32(platformID) == constant.AdminPlatformID {
|
||||
continue
|
||||
}
|
||||
if err := b.authClient.ForceLogout(c, black.UserID, int32(platformID)); err != nil {
|
||||
// 踢下线失败不阻断主流程,记录警告即可
|
||||
log.ZWarn(c, "AddGlobalBlacklist: ForceLogout failed", err,
|
||||
"userID", black.UserID, "platformID", platformID)
|
||||
}
|
||||
}
|
||||
}
|
||||
apiresp.GinSuccess(c, nil)
|
||||
}
|
||||
|
||||
// RemoveGlobalBlacklist 管理员从全局黑名单移除用户
|
||||
func (b *UserGlobalBlackApi) RemoveGlobalBlacklist(c *gin.Context) {
|
||||
var req removeGlobalBlacklistReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := authverify.CheckAdmin(c, b.imAdminUserIDs); err != nil {
|
||||
apiresp.GinError(c, err)
|
||||
return
|
||||
}
|
||||
if err := b.blacklistDB.RemoveBlack(c, req.UserIDs); err != nil {
|
||||
apiresp.GinError(c, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(c, nil)
|
||||
}
|
||||
|
||||
// GetGlobalBlacklist 管理员分页查询全局黑名单
|
||||
func (b *UserGlobalBlackApi) GetGlobalBlacklist(c *gin.Context) {
|
||||
var req getGlobalBlacklistReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := authverify.CheckAdmin(c, b.imAdminUserIDs); err != nil {
|
||||
apiresp.GinError(c, err)
|
||||
return
|
||||
}
|
||||
total, blacks, err := b.blacklistDB.GetBlackList(c, req.Pagination)
|
||||
if err != nil {
|
||||
apiresp.GinError(c, err)
|
||||
return
|
||||
}
|
||||
items := make([]globalBlackItem, 0, len(blacks))
|
||||
for _, blk := range blacks {
|
||||
items = append(items, globalBlackItem{
|
||||
UserID: blk.UserID,
|
||||
Nickname: blk.Nickname,
|
||||
OperatorID: blk.OperatorID,
|
||||
Reason: blk.Reason,
|
||||
CreateTime: blk.CreateTime.UnixMilli(),
|
||||
})
|
||||
}
|
||||
apiresp.GinSuccess(c, getGlobalBlacklistResp{Total: total, Blacks: items})
|
||||
}
|
||||
@ -22,6 +22,8 @@ import (
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||
redis2 "github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/redisutil"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@ -42,6 +44,7 @@ import (
|
||||
|
||||
type authServer struct {
|
||||
pbauth.UnimplementedAuthServer
|
||||
blacklistDB controller.UserGlobalBlackDatabase
|
||||
authDatabase controller.AuthDatabase
|
||||
RegisterCenter discovery.SvcDiscoveryRegistry
|
||||
config *Config
|
||||
@ -49,10 +52,11 @@ type authServer struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Auth
|
||||
RedisConfig config.Redis
|
||||
Share config.Share
|
||||
Discovery config.Discovery
|
||||
RpcConfig config.Auth
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
Share config.Share
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server *grpc.Server) error {
|
||||
@ -60,6 +64,14 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mgocli, err := mongoutil.NewMongoDB(ctx, config.MongodbConfig.Build())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userGlobalBlackDB, err := mgo.NewUserGlobalBlackMongo(mgocli.GetDB())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.User)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -73,8 +85,9 @@ func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryReg
|
||||
config.Share.MultiLogin,
|
||||
config.Share.IMAdminUserID,
|
||||
),
|
||||
config: config,
|
||||
userClient: rpcli.NewUserClient(userConn),
|
||||
config: config,
|
||||
blacklistDB: controller.NewUserGlobalBlackDatabase(userGlobalBlackDB),
|
||||
userClient: rpcli.NewUserClient(userConn),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@ -126,6 +139,16 @@ func (s *authServer) GetUserToken(ctx context.Context, req *pbauth.GetUserTokenR
|
||||
if user.AppMangerLevel >= constant.AppNotificationAdmin {
|
||||
return nil, errs.ErrArgs.WrapMsg("app account can`t get token")
|
||||
}
|
||||
|
||||
blocked, _ := s.blacklistDB.IsBlocked(ctx, req.UserID)
|
||||
if blocked {
|
||||
// Blacklisted users should be actively kicked to invalidate existing sessions.
|
||||
if kickErr := s.forceKickOffAllPlatforms(ctx, req.UserID); kickErr != nil {
|
||||
log.ZWarn(ctx, "GetUserToken forceKickOffAllPlatforms failed", kickErr, "userID", req.UserID)
|
||||
}
|
||||
log.ZWarn(ctx, "GetUserToken is blocked", errors.New("user is in global blacklist, userID="+req.UserID), "userID", req.UserID, "blocked", blocked)
|
||||
return nil, servererrs.ErrUserBlocked.WithDetail("user is in global blacklist, userID=" + req.UserID)
|
||||
}
|
||||
token, err := s.authDatabase.CreateToken(ctx, req.UserID, int(req.PlatformID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -144,6 +167,16 @@ func (s *authServer) parseToken(ctx context.Context, tokensString string) (claim
|
||||
if isAdmin {
|
||||
return claims, nil
|
||||
}
|
||||
// 非管理员用户检查全局黑名单
|
||||
blocked, _ := s.blacklistDB.IsBlocked(ctx, claims.UserID)
|
||||
if blocked {
|
||||
// Blacklisted users should be actively kicked to invalidate existing sessions.
|
||||
if kickErr := s.forceKickOffAllPlatforms(ctx, claims.UserID); kickErr != nil {
|
||||
log.ZWarn(ctx, "parseToken forceKickOffAllPlatforms failed", kickErr, "userID", claims.UserID)
|
||||
}
|
||||
log.ZWarn(ctx, "parseToken is blocked", errors.New("user is in global blacklist, userID="+claims.UserID), "userID", claims.UserID, "blocked", blocked)
|
||||
return nil, servererrs.ErrUserBlocked.WithDetail("user is in global blacklist, userID=" + claims.UserID)
|
||||
}
|
||||
m, err := s.authDatabase.GetTokensWithoutError(ctx, claims.UserID, claims.PlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -218,6 +251,18 @@ func (s *authServer) forceKickOff(ctx context.Context, userID string, platformID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authServer) forceKickOffAllPlatforms(ctx context.Context, userID string) error {
|
||||
for platformID := range constant.PlatformID2Name {
|
||||
if int32(platformID) == constant.AdminPlatformID {
|
||||
continue
|
||||
}
|
||||
if err := s.forceKickOff(ctx, userID, int32(platformID)); 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) {
|
||||
|
||||
@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -358,6 +359,68 @@ func (s *groupServer) GetJoinedGroupList(ctx context.Context, req *pbgroup.GetJo
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (g *groupServer) GetCommonGroupsWithFriend(ctx context.Context, req *pbgroup.GetCommonGroupsWithFriendReq) (*pbgroup.GetCommonGroupsWithFriendResp, error) {
|
||||
if req.FriendUserID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("friendUserID empty")
|
||||
}
|
||||
opUserID := mcontext.GetOpUserID(ctx)
|
||||
if opUserID == "" {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("op user id empty")
|
||||
}
|
||||
|
||||
selfGroupIDs, err := g.db.FindJoinGroupID(ctx, opUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(selfGroupIDs) == 0 {
|
||||
return &pbgroup.GetCommonGroupsWithFriendResp{
|
||||
Total: 0,
|
||||
Groups: []*sdkws.GroupInfo{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
friendMembers, err := g.db.FindGroupMemberUser(ctx, selfGroupIDs, req.FriendUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(friendMembers) == 0 {
|
||||
return &pbgroup.GetCommonGroupsWithFriendResp{
|
||||
Total: 0,
|
||||
Groups: []*sdkws.GroupInfo{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
commonGroupIDs := datautil.Distinct(datautil.Slice(friendMembers, func(e *model.GroupMember) string {
|
||||
return e.GroupID
|
||||
}))
|
||||
|
||||
groups, err := g.getGroupsInfo(ctx, commonGroupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Keep response deterministic by sorting common groups with member count descending.
|
||||
sort.SliceStable(groups, func(i, j int) bool {
|
||||
return groups[i].MemberCount > groups[j].MemberCount
|
||||
})
|
||||
total := len(groups)
|
||||
|
||||
limit := g.config.RpcConfig.CommonGroupsLimitWithFriend
|
||||
if limit <= 0 {
|
||||
limit = 3
|
||||
}
|
||||
if len(groups) > limit {
|
||||
groups = groups[:limit]
|
||||
}
|
||||
|
||||
return &pbgroup.GetCommonGroupsWithFriendResp{
|
||||
Total: uint32(total),
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *groupServer) InviteUserToGroup(ctx context.Context, req *pbgroup.InviteUserToGroupReq) (*pbgroup.InviteUserToGroupResp, error) {
|
||||
if len(req.InvitedUserIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("user empty")
|
||||
|
||||
@ -37,6 +37,7 @@ func NewApiCmd() *ApiCmd {
|
||||
OpenIMAPICfgFileName: &apiConfig.API,
|
||||
ShareFileName: &apiConfig.Share,
|
||||
DiscoveryConfigFilename: &apiConfig.Discovery,
|
||||
MongodbConfigFileName: &apiConfig.MongodbConfig,
|
||||
}
|
||||
ret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))
|
||||
ret.ctx = context.WithValue(context.Background(), "version", version.Version)
|
||||
|
||||
@ -38,6 +38,7 @@ func NewAuthRpcCmd() *AuthRpcCmd {
|
||||
OpenIMRPCAuthCfgFileName: &authConfig.RpcConfig,
|
||||
RedisConfigFileName: &authConfig.RedisConfig,
|
||||
ShareFileName: &authConfig.Share,
|
||||
MongodbConfigFileName: &authConfig.MongodbConfig,
|
||||
DiscoveryConfigFilename: &authConfig.Discovery,
|
||||
}
|
||||
ret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))
|
||||
|
||||
@ -290,6 +290,7 @@ type Group struct {
|
||||
} `mapstructure:"rpc"`
|
||||
Prometheus Prometheus `mapstructure:"prometheus"`
|
||||
EnableHistoryForNewMembers bool `mapstructure:"enableHistoryForNewMembers"`
|
||||
CommonGroupsLimitWithFriend int `mapstructure:"commonGroupsLimitWithFriend"`
|
||||
}
|
||||
|
||||
type Msg struct {
|
||||
|
||||
@ -54,6 +54,7 @@ const (
|
||||
// Account error codes.
|
||||
UserIDNotFoundError = 1101 // UserID does not exist or is not registered
|
||||
RegisteredAlreadyError = 1102 // user is already registered
|
||||
UserBlockedError = 1103 // user is blocked (global blacklist)
|
||||
|
||||
// Group error codes.
|
||||
GroupIDNotFoundError = 1201 // GroupID does not exist
|
||||
|
||||
@ -29,6 +29,7 @@ var (
|
||||
ErrRecordNotFound = errs.NewCodeError(RecordNotFoundError, "RecordNotFoundError")
|
||||
|
||||
ErrUserIDNotFound = errs.NewCodeError(UserIDNotFoundError, "UserIDNotFoundError")
|
||||
ErrUserBlocked = errs.NewCodeError(UserBlockedError, "UserBlockedError")
|
||||
ErrGroupIDNotFound = errs.NewCodeError(GroupIDNotFoundError, "GroupIDNotFoundError")
|
||||
ErrGroupIDExisted = errs.NewCodeError(GroupIDExisted, "GroupIDExisted")
|
||||
|
||||
|
||||
45
pkg/common/storage/controller/user_global_black.go
Normal file
45
pkg/common/storage/controller/user_global_black.go
Normal file
@ -0,0 +1,45 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
)
|
||||
|
||||
// UserGlobalBlackDatabase 全局黑名单业务接口
|
||||
type UserGlobalBlackDatabase interface {
|
||||
// AddBlack 将用户加入全局黑名单
|
||||
AddBlack(ctx context.Context, blacks []*model.UserGlobalBlack) error
|
||||
// RemoveBlack 按 userID 将用户从全局黑名单移除
|
||||
RemoveBlack(ctx context.Context, userIDs []string) error
|
||||
// IsBlocked 检查用户是否在全局黑名单
|
||||
IsBlocked(ctx context.Context, userID string) (bool, error)
|
||||
// GetBlackList 分页获取黑名单列表
|
||||
GetBlackList(ctx context.Context, pagination pagination.Pagination) (count int64, blacks []*model.UserGlobalBlack, err error)
|
||||
}
|
||||
|
||||
type userGlobalBlackDatabase struct {
|
||||
db database.UserGlobalBlack
|
||||
}
|
||||
|
||||
func NewUserGlobalBlackDatabase(db database.UserGlobalBlack) UserGlobalBlackDatabase {
|
||||
return &userGlobalBlackDatabase{db: db}
|
||||
}
|
||||
|
||||
func (u *userGlobalBlackDatabase) AddBlack(ctx context.Context, blacks []*model.UserGlobalBlack) error {
|
||||
return u.db.Add(ctx, blacks)
|
||||
}
|
||||
|
||||
func (u *userGlobalBlackDatabase) RemoveBlack(ctx context.Context, userIDs []string) error {
|
||||
return u.db.Remove(ctx, userIDs)
|
||||
}
|
||||
|
||||
func (u *userGlobalBlackDatabase) IsBlocked(ctx context.Context, userID string) (bool, error) {
|
||||
return u.db.IsBlocked(ctx, userID)
|
||||
}
|
||||
|
||||
func (u *userGlobalBlackDatabase) GetBlackList(ctx context.Context, pagination pagination.Pagination) (int64, []*model.UserGlobalBlack, error) {
|
||||
return u.db.Page(ctx, pagination)
|
||||
}
|
||||
89
pkg/common/storage/database/mgo/user_global_black.go
Normal file
89
pkg/common/storage/database/mgo/user_global_black.go
Normal file
@ -0,0 +1,89 @@
|
||||
package mgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func NewUserGlobalBlackMongo(db *mongo.Database) (database.UserGlobalBlack, error) {
|
||||
coll := db.Collection(database.UserGlobalBlackName)
|
||||
_, err := coll.Indexes().CreateOne(context.Background(), mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "user_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return &UserGlobalBlackMgo{coll: coll}, nil
|
||||
}
|
||||
|
||||
type UserGlobalBlackMgo struct {
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func (u *UserGlobalBlackMgo) Add(ctx context.Context, blacks []*model.UserGlobalBlack) error {
|
||||
for _, b := range blacks {
|
||||
if b.CreateTime.IsZero() {
|
||||
b.CreateTime = time.Now()
|
||||
}
|
||||
}
|
||||
// 使用 upsert 避免重复插入报错
|
||||
for _, b := range blacks {
|
||||
filter := bson.M{"user_id": b.UserID}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"nickname": b.Nickname,
|
||||
"operator_id": b.OperatorID,
|
||||
"reason": b.Reason,
|
||||
},
|
||||
"$setOnInsert": bson.M{
|
||||
"user_id": b.UserID,
|
||||
"create_time": b.CreateTime,
|
||||
},
|
||||
}
|
||||
opts := options.Update().SetUpsert(true)
|
||||
if _, err := u.coll.UpdateOne(ctx, filter, update, opts); err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserGlobalBlackMgo) Remove(ctx context.Context, users []string) error {
|
||||
if len(users) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := u.coll.DeleteMany(ctx, bson.M{"user_id": bson.M{"$in": users}})
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
func (u *UserGlobalBlackMgo) Find(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return mongoutil.Find[*model.UserGlobalBlack](ctx, u.coll, bson.M{"user_id": bson.M{"$in": userIDs}})
|
||||
}
|
||||
|
||||
func (u *UserGlobalBlackMgo) IsBlocked(ctx context.Context, userID string) (bool, error) {
|
||||
count, err := u.coll.CountDocuments(ctx, bson.M{"user_id": userID})
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "IsBlocked failed", err, "collection", database.UserGlobalBlackName, "userID", userID, "count", count)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (u *UserGlobalBlackMgo) Page(ctx context.Context, pagination pagination.Pagination) (int64, []*model.UserGlobalBlack, error) {
|
||||
return mongoutil.FindPage[*model.UserGlobalBlack](ctx, u.coll, bson.M{}, pagination)
|
||||
}
|
||||
@ -17,4 +17,5 @@ const (
|
||||
UserName = "user"
|
||||
SeqConversationName = "seq"
|
||||
SeqUserName = "seq_user"
|
||||
UserGlobalBlackName = "user_global_black_list"
|
||||
)
|
||||
|
||||
22
pkg/common/storage/database/user_global_black.go
Normal file
22
pkg/common/storage/database/user_global_black.go
Normal file
@ -0,0 +1,22 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model"
|
||||
"github.com/openimsdk/tools/db/pagination"
|
||||
)
|
||||
|
||||
// UserGlobalBlack 全局黑名单持久化接口
|
||||
type UserGlobalBlack interface {
|
||||
// Add 批量添加用户到全局黑名单
|
||||
Add(ctx context.Context, blacks []*model.UserGlobalBlack) error
|
||||
// Remove 按 userID 从全局黑名单移除用户
|
||||
Remove(ctx context.Context, userIDs []string) error
|
||||
// Find 查询指定用户是否在黑名单(返回在黑名单中的记录)
|
||||
Find(ctx context.Context, userIDs []string) ([]*model.UserGlobalBlack, error)
|
||||
// IsBlocked 检查单个用户是否在黑名单
|
||||
IsBlocked(ctx context.Context, userID string) (bool, error)
|
||||
// Page 分页查询黑名单列表
|
||||
Page(ctx context.Context, pagination pagination.Pagination) (count int64, blacks []*model.UserGlobalBlack, err error)
|
||||
}
|
||||
12
pkg/common/storage/model/user_global_black.go
Normal file
12
pkg/common/storage/model/user_global_black.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// UserGlobalBlack 全局黑名单记录,被加入黑名单的用户无法登录
|
||||
type UserGlobalBlack struct {
|
||||
UserID string `bson:"user_id"`
|
||||
Nickname string `bson:"nickname"`
|
||||
OperatorID string `bson:"operator_id"`
|
||||
Reason string `bson:"reason"`
|
||||
CreateTime time.Time `bson:"create_time"`
|
||||
}
|
||||
@ -2,6 +2,7 @@ package rpcli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/protocol/auth"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@ -28,3 +29,7 @@ func (x *AuthClient) InvalidateToken(ctx context.Context, req *auth.InvalidateTo
|
||||
func (x *AuthClient) ParseToken(ctx context.Context, token string) (*auth.ParseTokenResp, error) {
|
||||
return x.AuthClient.ParseToken(ctx, &auth.ParseTokenReq{Token: token})
|
||||
}
|
||||
|
||||
func (x *AuthClient) ForceLogout(ctx context.Context, userID string, platformID int32) error {
|
||||
return ignoreResp(x.AuthClient.ForceLogout(ctx, &auth.ForceLogoutReq{UserID: userID, PlatformID: platformID}))
|
||||
}
|
||||
|
||||
146
scripts/get_common_group.sh
Executable file
146
scripts/get_common_group.sh
Executable file
@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ====== 按需修改 ======
|
||||
API_BASE="${API_BASE:-http://127.0.0.1:10002}" # 你的 open-im-api 地址
|
||||
SELF_USER_ID="${SELF_USER_ID:-5694418935}" # 当前登录用户(拿 token 的用户)
|
||||
#FRIEND_USER_ID="${FRIEND_USER_ID:-1971806090}" # 要查询共同群的好友
|
||||
FRIEND_USER_ID="${FRIEND_USER_ID:-1011009748}" # 要查询共同群的好友
|
||||
PLATFORM_ID="${PLATFORM_ID:-2}" # 1=iOS, 2=Android, 3=Windows...
|
||||
ADMIN_USER_ID="${ADMIN_USER_ID:-imAdmin}" # 管理员账号(用于签发用户 token)
|
||||
ADMIN_SECRET="${ADMIN_SECRET:-openIM123}" # 配置中的 share.secret
|
||||
DEBUG="${DEBUG:-1}" # DEBUG=1 打印请求/响应明细
|
||||
# RecordNotFoundError(errCode=1004)常见于 get_user_token:
|
||||
# 服务端会查用户是否存在(user RPC GetDesignateUsers);若 SELF_USER_ID 未注册,
|
||||
# 返回空列表后 rpcli.firstValue 会包装为 ErrRecordNotFound(errDlt: record not found)。
|
||||
# 处理:先注册该用户,或 export SELF_USER_ID=已存在用户,或 export TOKEN=已有用户 token 跳过拉 token。
|
||||
#
|
||||
# HTTP 404 + 响应体 "404 page not found"(Gin):当前连上的 API 进程路由表里没有该路径。
|
||||
# 本仓库已注册 POST /group/get_common_groups_with_friend(见 internal/api/router.go)。
|
||||
# 处理:用当前代码重新编译/替换镜像并重启 openim-api,或确认 API_BASE 指向的就是带该路由的实例(无错误路径前缀/反代截断)。
|
||||
# =====================
|
||||
|
||||
debug_log() {
|
||||
if [[ "${DEBUG}" == "1" ]]; then
|
||||
echo "[DEBUG] $*"
|
||||
fi
|
||||
}
|
||||
|
||||
print_json_safe() {
|
||||
local raw="${1:-}"
|
||||
if echo "${raw}" | jq -e . >/dev/null 2>&1; then
|
||||
echo "${raw}" | jq .
|
||||
else
|
||||
echo "${raw}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 1) 先拿 user token(如果你已有 token,可跳过这一步,直接 export TOKEN=xxx)
|
||||
if [[ -z "${TOKEN:-}" ]]; then
|
||||
if [[ -z "${ADMIN_SECRET}" ]]; then
|
||||
echo "缺少 ADMIN_SECRET,请先导出:export ADMIN_SECRET='你的share.secret'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "获取管理员 token: ${ADMIN_USER_ID}"
|
||||
OP_ID_ADMIN="op_admin_$(date +%s)"
|
||||
debug_log "POST ${API_BASE}/auth/get_admin_token"
|
||||
debug_log "operationID: ${OP_ID_ADMIN}"
|
||||
debug_log "admin req body: {\"userID\":\"${ADMIN_USER_ID}\",\"secret\":\"***\"}"
|
||||
ADMIN_RESP=$(
|
||||
curl -sS -X POST "${API_BASE}/auth/get_admin_token" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "operationID: ${OP_ID_ADMIN}" \
|
||||
-d "$(cat <<JSON
|
||||
{
|
||||
"userID": "${ADMIN_USER_ID}",
|
||||
"secret": "${ADMIN_SECRET}"
|
||||
}
|
||||
JSON
|
||||
)"
|
||||
)
|
||||
debug_log "admin raw resp: ${ADMIN_RESP}"
|
||||
ADMIN_TOKEN="$(echo "${ADMIN_RESP}" | jq -r '.data.token // empty')"
|
||||
debug_log "admin token parsed: ${ADMIN_TOKEN:-<empty>}"
|
||||
if [[ -z "${ADMIN_TOKEN}" ]]; then
|
||||
echo "获取管理员 token 失败,响应如下:"
|
||||
print_json_safe "${ADMIN_RESP}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "获取用户 token: ${SELF_USER_ID}"
|
||||
OP_ID_USER="op_user_$(date +%s)"
|
||||
debug_log "POST ${API_BASE}/auth/get_user_token"
|
||||
debug_log "operationID: ${OP_ID_USER}"
|
||||
debug_log "user req body: {\"userID\":\"${SELF_USER_ID}\",\"platformID\":${PLATFORM_ID}}"
|
||||
USER_RESP=$(
|
||||
curl -sS -X POST "${API_BASE}/auth/get_user_token" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "operationID: ${OP_ID_USER}" \
|
||||
-H "token: ${ADMIN_TOKEN}" \
|
||||
-d "$(cat <<JSON
|
||||
{
|
||||
"userID": "${SELF_USER_ID}",
|
||||
"platformID": ${PLATFORM_ID}
|
||||
}
|
||||
JSON
|
||||
)"
|
||||
)
|
||||
debug_log "user raw resp: ${USER_RESP}"
|
||||
TOKEN="$(echo "${USER_RESP}" | jq -r '.data.token // empty')"
|
||||
debug_log "user token parsed: ${TOKEN:-<empty>}"
|
||||
fi
|
||||
|
||||
if [[ -z "${TOKEN}" ]]; then
|
||||
echo "获取用户 token 失败,响应如下:"
|
||||
print_json_safe "${USER_RESP:-}"
|
||||
USER_ERR_CODE="$(echo "${USER_RESP:-}" | jq -r '.errCode // empty')"
|
||||
if [[ "${USER_ERR_CODE}" == "1004" ]]; then
|
||||
echo ""
|
||||
echo "【排查】errCode 1004 (RecordNotFoundError):当前请求的 userID 在用户库中不存在。"
|
||||
echo " - 服务端路径:auth GetUserToken → user GetDesignateUsers → 未命中则空结果 → record not found"
|
||||
echo " - 请先将 SELF_USER_ID=${SELF_USER_ID} 注册进系统,或改用已存在用户,或: export TOKEN='你的用户token'"
|
||||
else
|
||||
echo "提示:请确认 SELF_USER_ID 已注册、ADMIN_SECRET 与部署一致,或手动 export TOKEN 后重试。"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OP_ID="op_$(date +%s)"
|
||||
|
||||
# 2) 调共同群接口
|
||||
echo "查询共同群: self=${SELF_USER_ID}, friend=${FRIEND_USER_ID}"
|
||||
REQ_BODY="$(cat <<JSON
|
||||
{
|
||||
"friendUserID": "${FRIEND_USER_ID}"
|
||||
}
|
||||
JSON
|
||||
)"
|
||||
debug_log "POST ${API_BASE}/group/get_common_groups_with_friend"
|
||||
debug_log "operationID: ${OP_ID}"
|
||||
debug_log "group req body: ${REQ_BODY}"
|
||||
GROUP_BODY="$(mktemp)"
|
||||
GROUP_HTTP_CODE="$(
|
||||
curl -sS -o "${GROUP_BODY}" -w "%{http_code}" -X POST "${API_BASE}/group/get_common_groups_with_friend" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H "token: ${TOKEN}" \
|
||||
-H "operationID: ${OP_ID}" \
|
||||
-d "${REQ_BODY}"
|
||||
)"
|
||||
GROUP_RESP="$(cat "${GROUP_BODY}")"
|
||||
rm -f "${GROUP_BODY}"
|
||||
debug_log "group HTTP status: ${GROUP_HTTP_CODE}"
|
||||
debug_log "group raw resp: ${GROUP_RESP}"
|
||||
print_json_safe "${GROUP_RESP}"
|
||||
if [[ "${GROUP_HTTP_CODE}" == "404" ]] || [[ "${GROUP_RESP}" == "404 page not found" ]]; then
|
||||
echo ""
|
||||
echo "【排查】HTTP 404:Gin 未匹配到路由,通常表示当前运行的 openim-api 版本过旧,不含 get_common_groups_with_friend。"
|
||||
echo " - 期望路径: POST ${API_BASE}/group/get_common_groups_with_friend"
|
||||
echo " - 请用本仓库代码重新构建并重启 API,或核对 API_BASE / 网关是否多删、少拼了路径前缀。"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${GROUP_HTTP_CODE}" != "200" ]]; then
|
||||
echo ""
|
||||
echo "【提示】HTTP 状态码: ${GROUP_HTTP_CODE}(非 200),请结合响应体与网关/鉴权配置排查。"
|
||||
exit 1
|
||||
fi
|
||||
171
scripts/global_blacklist_api.sh
Executable file
171
scripts/global_blacklist_api.sh
Executable file
@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 统一通过 API 新链路管理全局黑名单(按 userID)
|
||||
#
|
||||
# 用法:
|
||||
# 1) 添加
|
||||
# ./scripts/global_blacklist_api.sh add "user001,user002" [reason]
|
||||
#
|
||||
# 2) 删除
|
||||
# ./scripts/global_blacklist_api.sh remove "user001,user002"
|
||||
#
|
||||
# 3) 查询
|
||||
# ./scripts/global_blacklist_api.sh list [pageNumber] [showNumber]
|
||||
#
|
||||
# 环境变量(可覆盖):
|
||||
# OPENIM_API_ADDR 默认: http://127.0.0.1:10002
|
||||
# ADMIN_TOKEN 管理员 token(如未提供则自动调用 /auth/get_admin_token 获取)
|
||||
# OPENIM_SECRET 获取管理员 token 所需 secret,默认: openIM123
|
||||
# ADMIN_USER_ID 获取管理员 token 所需 userID,默认: imAdmin
|
||||
|
||||
OPENIM_API_ADDR="${OPENIM_API_ADDR:-http://127.0.0.1:10002}"
|
||||
ADMIN_TOKEN="${ADMIN_TOKEN:-}"
|
||||
OPENIM_SECRET="${OPENIM_SECRET:-openIM123}"
|
||||
ADMIN_USER_ID="${ADMIN_USER_ID:-imAdmin}"
|
||||
OPERATION_ID="${OPERATION_ID:-gb_$(date +%s)_$RANDOM}"
|
||||
|
||||
ACTION="${1:-}"
|
||||
USERIDS_RAW="${2:-}"
|
||||
REASON="${3:-manual_by_api_script}"
|
||||
PAGE_NUMBER="${2:-1}"
|
||||
SHOW_NUMBER="${3:-20}"
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
local s="$1"
|
||||
s="${s#"${s%%[![:space:]]*}"}"
|
||||
s="${s%"${s##*[![:space:]]}"}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
userids_csv_to_json_array() {
|
||||
local csv="$1"
|
||||
local arr_json="["
|
||||
local first=1
|
||||
local item
|
||||
|
||||
IFS=',' read -r -a _items <<< "$csv"
|
||||
for item in "${_items[@]}"; do
|
||||
item="$(trim "$item")"
|
||||
[[ -z "$item" ]] && continue
|
||||
if [[ $first -eq 1 ]]; then
|
||||
arr_json="${arr_json}\"${item}\""
|
||||
first=0
|
||||
else
|
||||
arr_json="${arr_json},\"${item}\""
|
||||
fi
|
||||
done
|
||||
arr_json="${arr_json}]"
|
||||
|
||||
if [[ "$arr_json" == "[]" ]]; then
|
||||
die "userIDs 为空,请传入逗号分隔的 userID,如 \"user001,user002\""
|
||||
fi
|
||||
printf '%s' "$arr_json"
|
||||
}
|
||||
|
||||
get_admin_token() {
|
||||
local uid body resp token last_resp
|
||||
local -a candidates=("${ADMIN_USER_ID}" "openIM123456" "imAdmin")
|
||||
last_resp=""
|
||||
|
||||
for uid in "${candidates[@]}"; do
|
||||
body="{\"secret\":\"${OPENIM_SECRET}\",\"userID\":\"${uid}\"}"
|
||||
resp="$(curl -sS -X POST "${OPENIM_API_ADDR}/auth/get_admin_token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "operationID: ${OPERATION_ID}" \
|
||||
-d "$body")"
|
||||
last_resp="$resp"
|
||||
|
||||
token="$(python3 - <<'PY' "$resp"
|
||||
import json
|
||||
import sys
|
||||
|
||||
raw = sys.argv[1]
|
||||
try:
|
||||
obj = json.loads(raw)
|
||||
except Exception:
|
||||
print("")
|
||||
raise SystemExit(0)
|
||||
|
||||
token = ""
|
||||
if isinstance(obj, dict):
|
||||
data = obj.get("data")
|
||||
if isinstance(data, dict):
|
||||
token = data.get("token") or data.get("Token") or ""
|
||||
if not token:
|
||||
token = obj.get("token") or obj.get("Token") or ""
|
||||
print(token)
|
||||
PY
|
||||
)"
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "自动获取管理员 token 成功,userID=${uid}" >&2
|
||||
printf '%s' "$token"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "get_admin_token raw response: $last_resp" >&2
|
||||
die "自动获取管理员 token 失败,请检查 OPENIM_API_ADDR/OPENIM_SECRET/ADMIN_USER_ID(当前: ${ADMIN_USER_ID}),或直接设置 ADMIN_TOKEN"
|
||||
}
|
||||
|
||||
call_api() {
|
||||
local path="$1"
|
||||
local body="$2"
|
||||
local token="$3"
|
||||
|
||||
curl -sS -X POST "${OPENIM_API_ADDR}${path}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "operationID: ${OPERATION_ID}" \
|
||||
-H "token: ${token}" \
|
||||
-d "$body"
|
||||
}
|
||||
|
||||
if [[ -z "$ACTION" ]]; then
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
添加: ./scripts/global_blacklist_api.sh add "user001,user002" [reason]
|
||||
删除: ./scripts/global_blacklist_api.sh remove "user001,user002"
|
||||
查询: ./scripts/global_blacklist_api.sh list [pageNumber] [showNumber]
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$ADMIN_TOKEN" ]]; then
|
||||
echo "ADMIN_TOKEN 未设置,尝试自动获取管理员 token..."
|
||||
ADMIN_TOKEN="$(get_admin_token)"
|
||||
fi
|
||||
|
||||
case "$ACTION" in
|
||||
add)
|
||||
[[ -z "$USERIDS_RAW" ]] && die "add 需要 userIDs 参数"
|
||||
USERIDS_JSON="$(userids_csv_to_json_array "$USERIDS_RAW")"
|
||||
BODY="{\"userIDs\":${USERIDS_JSON},\"reason\":\"${REASON}\"}"
|
||||
echo ">>> POST /user/add_global_blacklist"
|
||||
call_api "/user/add_global_blacklist" "$BODY" "$ADMIN_TOKEN"
|
||||
;;
|
||||
|
||||
remove)
|
||||
[[ -z "$USERIDS_RAW" ]] && die "remove 需要 userIDs 参数"
|
||||
USERIDS_JSON="$(userids_csv_to_json_array "$USERIDS_RAW")"
|
||||
BODY="{\"userIDs\":${USERIDS_JSON}}"
|
||||
echo ">>> POST /user/remove_global_blacklist"
|
||||
call_api "/user/remove_global_blacklist" "$BODY" "$ADMIN_TOKEN"
|
||||
;;
|
||||
|
||||
list)
|
||||
BODY="{\"pagination\":{\"pageNumber\":${PAGE_NUMBER},\"showNumber\":${SHOW_NUMBER}}}"
|
||||
echo ">>> POST /user/get_global_blacklist"
|
||||
call_api "/user/get_global_blacklist" "$BODY" "$ADMIN_TOKEN"
|
||||
;;
|
||||
|
||||
*)
|
||||
die "不支持的 action: ${ACTION}(仅支持 add/remove/list)"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo
|
||||
Loading…
x
Reference in New Issue
Block a user