mirror of
				https://github.com/openimsdk/open-im-server.git
				synced 2025-10-26 21:22:16 +08:00 
			
		
		
		
	1
This commit is contained in:
		
							parent
							
								
									04bac6ddb7
								
							
						
					
					
						commit
						65dbe8ef6a
					
				| @ -7,8 +7,11 @@ import ( | |||||||
| 
 | 
 | ||||||
| 	"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush" | 	"github.com/openimsdk/open-im-server/v3/internal/push/offlinepush" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/config" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/config" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/dbbuild" | 	"github.com/openimsdk/open-im-server/v3/pkg/dbbuild" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/mqbuild" | 	"github.com/openimsdk/open-im-server/v3/pkg/mqbuild" | ||||||
| 	pbpush "github.com/openimsdk/protocol/push" | 	pbpush "github.com/openimsdk/protocol/push" | ||||||
| @ -28,6 +31,7 @@ type pushServer struct { | |||||||
| type Config struct { | type Config struct { | ||||||
| 	RpcConfig          config.Push | 	RpcConfig          config.Push | ||||||
| 	RedisConfig        config.Redis | 	RedisConfig        config.Redis | ||||||
|  | 	MongoConfig        config.Mongo | ||||||
| 	KafkaConfig        config.Kafka | 	KafkaConfig        config.Kafka | ||||||
| 	NotificationConfig config.Notification | 	NotificationConfig config.Notification | ||||||
| 	Share              config.Share | 	Share              config.Share | ||||||
| @ -46,12 +50,25 @@ func (p pushServer) DelUserPushToken(ctx context.Context, | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { | func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { | ||||||
| 	dbb := dbbuild.NewBuilder(nil, &config.RedisConfig) | 	dbb := dbbuild.NewBuilder(&config.MongoConfig, &config.RedisConfig) | ||||||
| 	rdb, err := dbb.Redis(ctx) | 	rdb, err := dbb.Redis(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	cacheModel := redis.NewThirdCache(rdb) | 	var cacheModel cache.ThirdCache | ||||||
|  | 	if rdb == nil { | ||||||
|  | 		mdb, err := dbb.Mongo(ctx) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		mc, err := mgo.NewCacheMgo(mdb.GetDB()) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		cacheModel = mcache.NewThirdCache(mc) | ||||||
|  | 	} else { | ||||||
|  | 		cacheModel = redis.NewThirdCache(rdb) | ||||||
|  | 	} | ||||||
| 	offlinePusher, err := offlinepush.NewOfflinePusher(&config.RpcConfig, cacheModel, string(config.FcmConfigPath)) | 	offlinePusher, err := offlinepush.NewOfflinePusher(&config.RpcConfig, cacheModel, string(config.FcmConfigPath)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | |||||||
| @ -18,6 +18,9 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database/mgo" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/dbbuild" | 	"github.com/openimsdk/open-im-server/v3/pkg/dbbuild" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/rpcli" | 	"github.com/openimsdk/open-im-server/v3/pkg/rpcli" | ||||||
| 
 | 
 | ||||||
| @ -51,16 +54,31 @@ type authServer struct { | |||||||
| type Config struct { | type Config struct { | ||||||
| 	RpcConfig   config.Auth | 	RpcConfig   config.Auth | ||||||
| 	RedisConfig config.Redis | 	RedisConfig config.Redis | ||||||
|  | 	MongoConfig config.Mongo | ||||||
| 	Share       config.Share | 	Share       config.Share | ||||||
| 	Discovery   config.Discovery | 	Discovery   config.Discovery | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { | func Start(ctx context.Context, config *Config, client discovery.Conn, server grpc.ServiceRegistrar) error { | ||||||
| 	dbb := dbbuild.NewBuilder(nil, &config.RedisConfig) | 	dbb := dbbuild.NewBuilder(&config.MongoConfig, &config.RedisConfig) | ||||||
| 	rdb, err := dbb.Redis(ctx) | 	rdb, err := dbb.Redis(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		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.RpcConfig.TokenPolicy.Expire) | ||||||
|  | 	} | ||||||
| 	userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User) | 	userConn, err := client.GetConn(ctx, config.Discovery.RpcService.User) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -68,7 +86,7 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr | |||||||
| 	pbauth.RegisterAuthServer(server, &authServer{ | 	pbauth.RegisterAuthServer(server, &authServer{ | ||||||
| 		RegisterCenter: client, | 		RegisterCenter: client, | ||||||
| 		authDatabase: controller.NewAuthDatabase( | 		authDatabase: controller.NewAuthDatabase( | ||||||
| 			redis2.NewTokenCacheModel(rdb, config.RpcConfig.TokenPolicy.Expire), | 			token, | ||||||
| 			config.Share.Secret, | 			config.Share.Secret, | ||||||
| 			config.RpcConfig.TokenPolicy.Expire, | 			config.RpcConfig.TokenPolicy.Expire, | ||||||
| 			config.Share.MultiLogin, | 			config.Share.MultiLogin, | ||||||
|  | |||||||
| @ -19,11 +19,12 @@ import ( | |||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/authverify" |  | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/authverify" | ||||||
|  | 
 | ||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" | ||||||
| @ -37,7 +38,10 @@ import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (t *thirdServer) PartLimit(ctx context.Context, req *third.PartLimitReq) (*third.PartLimitResp, error) { | func (t *thirdServer) PartLimit(ctx context.Context, req *third.PartLimitReq) (*third.PartLimitResp, error) { | ||||||
| 	limit := t.s3dataBase.PartLimit() | 	limit, err := t.s3dataBase.PartLimit() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
| 	return &third.PartLimitResp{ | 	return &third.PartLimitResp{ | ||||||
| 		MinPartSize: limit.MinPartSize, | 		MinPartSize: limit.MinPartSize, | ||||||
| 		MaxPartSize: limit.MaxPartSize, | 		MaxPartSize: limit.MaxPartSize, | ||||||
|  | |||||||
| @ -19,8 +19,11 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/dbbuild" | 	"github.com/openimsdk/open-im-server/v3/pkg/dbbuild" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/rpcli" | 	"github.com/openimsdk/open-im-server/v3/pkg/rpcli" | ||||||
|  | 	"github.com/openimsdk/tools/s3/disable" | ||||||
| 
 | 
 | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/config" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/config" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/redis" | ||||||
| @ -79,15 +82,31 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		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 | 	// Select the oss method according to the profile policy | ||||||
| 	enable := config.RpcConfig.Object.Enable | 	var o s3.Interface | ||||||
| 	var ( | 	switch enable := config.RpcConfig.Object.Enable; enable { | ||||||
| 		o s3.Interface |  | ||||||
| 	) |  | ||||||
| 	switch enable { |  | ||||||
| 	case "minio": | 	case "minio": | ||||||
| 		o, err = minio.NewMinio(ctx, redis.NewMinioCache(rdb), *config.MinioConfig.Build()) | 		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": | 	case "cos": | ||||||
| 		o, err = cos.NewCos(*config.RpcConfig.Object.Cos.Build()) | 		o, err = cos.NewCos(*config.RpcConfig.Object.Cos.Build()) | ||||||
| 	case "oss": | 	case "oss": | ||||||
| @ -96,6 +115,8 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr | |||||||
| 		o, err = kodo.NewKodo(*config.RpcConfig.Object.Kodo.Build()) | 		o, err = kodo.NewKodo(*config.RpcConfig.Object.Kodo.Build()) | ||||||
| 	case "aws": | 	case "aws": | ||||||
| 		o, err = aws.NewAws(*config.RpcConfig.Object.Aws.Build()) | 		o, err = aws.NewAws(*config.RpcConfig.Object.Aws.Build()) | ||||||
|  | 	case "": | ||||||
|  | 		o = disable.NewDisable() | ||||||
| 	default: | 	default: | ||||||
| 		err = fmt.Errorf("invalid object enable: %s", enable) | 		err = fmt.Errorf("invalid object enable: %s", enable) | ||||||
| 	} | 	} | ||||||
| @ -108,7 +129,7 @@ func Start(ctx context.Context, config *Config, client discovery.Conn, server gr | |||||||
| 	} | 	} | ||||||
| 	localcache.InitLocalCache(&config.LocalCacheConfig) | 	localcache.InitLocalCache(&config.LocalCacheConfig) | ||||||
| 	third.RegisterThirdServer(server, &thirdServer{ | 	third.RegisterThirdServer(server, &thirdServer{ | ||||||
| 		thirdDatabase: controller.NewThirdDatabase(redis.NewThirdCache(rdb), logdb), | 		thirdDatabase: controller.NewThirdDatabase(thirdCache, logdb), | ||||||
| 		s3dataBase:    controller.NewS3Database(rdb, o, s3db), | 		s3dataBase:    controller.NewS3Database(rdb, o, s3db), | ||||||
| 		defaultExpire: time.Hour * 24 * 7, | 		defaultExpire: time.Hour * 24 * 7, | ||||||
| 		config:        config, | 		config:        config, | ||||||
|  | |||||||
| @ -38,6 +38,7 @@ func NewAuthRpcCmd() *AuthRpcCmd { | |||||||
| 	ret.configMap = map[string]any{ | 	ret.configMap = map[string]any{ | ||||||
| 		config.OpenIMRPCAuthCfgFileName: &authConfig.RpcConfig, | 		config.OpenIMRPCAuthCfgFileName: &authConfig.RpcConfig, | ||||||
| 		config.RedisConfigFileName:      &authConfig.RedisConfig, | 		config.RedisConfigFileName:      &authConfig.RedisConfig, | ||||||
|  | 		config.MongodbConfigFileName:    &authConfig.MongoConfig, | ||||||
| 		config.ShareFileName:            &authConfig.Share, | 		config.ShareFileName:            &authConfig.Share, | ||||||
| 		config.DiscoveryConfigFilename:  &authConfig.Discovery, | 		config.DiscoveryConfigFilename:  &authConfig.Discovery, | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -38,6 +38,7 @@ func NewPushRpcCmd() *PushRpcCmd { | |||||||
| 	ret.configMap = map[string]any{ | 	ret.configMap = map[string]any{ | ||||||
| 		config.OpenIMPushCfgFileName:    &pushConfig.RpcConfig, | 		config.OpenIMPushCfgFileName:    &pushConfig.RpcConfig, | ||||||
| 		config.RedisConfigFileName:      &pushConfig.RedisConfig, | 		config.RedisConfigFileName:      &pushConfig.RedisConfig, | ||||||
|  | 		config.MongodbConfigFileName:    &pushConfig.MongoConfig, | ||||||
| 		config.KafkaConfigFileName:      &pushConfig.KafkaConfig, | 		config.KafkaConfigFileName:      &pushConfig.KafkaConfig, | ||||||
| 		config.ShareFileName:            &pushConfig.Share, | 		config.ShareFileName:            &pushConfig.Share, | ||||||
| 		config.NotificationFileName:     &pushConfig.NotificationConfig, | 		config.NotificationFileName:     &pushConfig.NotificationConfig, | ||||||
|  | |||||||
| @ -1,15 +0,0 @@ | |||||||
| // Copyright © 2024 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 redispubsub // import "github.com/openimsdk/open-im-server/v3/pkg/common/redispubsub" |  | ||||||
| @ -1,30 +0,0 @@ | |||||||
| // Copyright © 2024 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 redispubsub |  | ||||||
| 
 |  | ||||||
| import "github.com/redis/go-redis/v9" |  | ||||||
| 
 |  | ||||||
| type Publisher struct { |  | ||||||
| 	client  redis.UniversalClient |  | ||||||
| 	channel string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewPublisher(client redis.UniversalClient, channel string) *Publisher { |  | ||||||
| 	return &Publisher{client: client, channel: channel} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (p *Publisher) Publish(message string) error { |  | ||||||
| 	return p.client.Publish(ctx, p.channel, message).Err() |  | ||||||
| } |  | ||||||
| @ -1,49 +0,0 @@ | |||||||
| // Copyright © 2024 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 redispubsub |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 
 |  | ||||||
| 	"github.com/redis/go-redis/v9" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ctx = context.Background() |  | ||||||
| 
 |  | ||||||
| type Subscriber struct { |  | ||||||
| 	client  redis.UniversalClient |  | ||||||
| 	channel string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewSubscriber(client redis.UniversalClient, channel string) *Subscriber { |  | ||||||
| 	return &Subscriber{client: client, channel: channel} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Subscriber) OnMessage(ctx context.Context, callback func(string)) error { |  | ||||||
| 	messageChannel := s.client.Subscribe(ctx, s.channel).Channel() |  | ||||||
| 
 |  | ||||||
| 	go func() { |  | ||||||
| 		for { |  | ||||||
| 			select { |  | ||||||
| 			case <-ctx.Done(): |  | ||||||
| 				return |  | ||||||
| 			case msg := <-messageChannel: |  | ||||||
| 				callback(msg.Payload) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
							
								
								
									
										105
									
								
								pkg/common/storage/cache/mcache/minio.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								pkg/common/storage/cache/mcache/minio.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | |||||||
|  | package mcache | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" | ||||||
|  | 	"github.com/openimsdk/tools/log" | ||||||
|  | 	"github.com/openimsdk/tools/s3/minio" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewMinioCache(cache database.Cache) minio.Cache { | ||||||
|  | 	return &minioCache{ | ||||||
|  | 		cache:      cache, | ||||||
|  | 		expireTime: time.Hour * 24 * 7, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type minioCache struct { | ||||||
|  | 	cache      database.Cache | ||||||
|  | 	expireTime time.Duration | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (g *minioCache) getObjectImageInfoKey(key string) string { | ||||||
|  | 	return cachekey.GetObjectImageInfoKey(key) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (g *minioCache) getMinioImageThumbnailKey(key string, format string, width int, height int) string { | ||||||
|  | 	return cachekey.GetMinioImageThumbnailKey(key, format, width, height) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (g *minioCache) DelObjectImageInfoKey(ctx context.Context, keys ...string) error { | ||||||
|  | 	ks := make([]string, 0, len(keys)) | ||||||
|  | 	for _, key := range keys { | ||||||
|  | 		ks = append(ks, g.getObjectImageInfoKey(key)) | ||||||
|  | 	} | ||||||
|  | 	return g.cache.Del(ctx, ks) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (g *minioCache) DelImageThumbnailKey(ctx context.Context, key string, format string, width int, height int) error { | ||||||
|  | 	return g.cache.Del(ctx, []string{g.getMinioImageThumbnailKey(key, format, width, height)}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (g *minioCache) GetImageObjectKeyInfo(ctx context.Context, key string, fn func(ctx context.Context) (*minio.ImageInfo, error)) (*minio.ImageInfo, error) { | ||||||
|  | 	return getCache[*minio.ImageInfo](ctx, g.cache, g.getObjectImageInfoKey(key), g.expireTime, fn) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (g *minioCache) GetThumbnailKey(ctx context.Context, key string, format string, width int, height int, minioCache func(ctx context.Context) (string, error)) (string, error) { | ||||||
|  | 	return getCache[string](ctx, g.cache, g.getMinioImageThumbnailKey(key, format, width, height), g.expireTime, minioCache) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getCache[V any](ctx context.Context, cache database.Cache, key string, expireTime time.Duration, fn func(ctx context.Context) (V, error)) (V, error) { | ||||||
|  | 	getDB := func() (V, bool, error) { | ||||||
|  | 		res, err := cache.Get(ctx, []string{key}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			var val V | ||||||
|  | 			return val, false, err | ||||||
|  | 		} | ||||||
|  | 		var val V | ||||||
|  | 		if str, ok := res[key]; ok { | ||||||
|  | 			if json.Unmarshal([]byte(str), &val) != nil { | ||||||
|  | 				return val, false, err | ||||||
|  | 			} | ||||||
|  | 			return val, true, nil | ||||||
|  | 		} | ||||||
|  | 		return val, false, nil | ||||||
|  | 	} | ||||||
|  | 	dbVal, ok, err := getDB() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return dbVal, err | ||||||
|  | 	} | ||||||
|  | 	if ok { | ||||||
|  | 		return dbVal, nil | ||||||
|  | 	} | ||||||
|  | 	lockValue, err := cache.Lock(ctx, key, time.Minute) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return dbVal, err | ||||||
|  | 	} | ||||||
|  | 	defer func() { | ||||||
|  | 		if err := cache.Unlock(ctx, key, lockValue); err != nil { | ||||||
|  | 			log.ZError(ctx, "unlock cache key", err, "key", key, "value", lockValue) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	dbVal, ok, err = getDB() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return dbVal, err | ||||||
|  | 	} | ||||||
|  | 	if ok { | ||||||
|  | 		return dbVal, nil | ||||||
|  | 	} | ||||||
|  | 	val, err := fn(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return val, err | ||||||
|  | 	} | ||||||
|  | 	data, err := json.Marshal(val) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return val, err | ||||||
|  | 	} | ||||||
|  | 	if err := cache.Set(ctx, key, string(data), expireTime); err != nil { | ||||||
|  | 		return val, err | ||||||
|  | 	} | ||||||
|  | 	return val, nil | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								pkg/common/storage/cache/mcache/online.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								pkg/common/storage/cache/mcache/online.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | |||||||
|  | package mcache | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"sync" | ||||||
|  | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewOnlineCache() cache.OnlineCache { | ||||||
|  | 	return &onlineCache{ | ||||||
|  | 		user: make(map[string]map[int32]struct{}), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type onlineCache struct { | ||||||
|  | 	lock sync.RWMutex | ||||||
|  | 	user map[string]map[int32]struct{} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *onlineCache) GetOnline(ctx context.Context, userID string) ([]int32, error) { | ||||||
|  | 	x.lock.RLock() | ||||||
|  | 	defer x.lock.RUnlock() | ||||||
|  | 	pSet, ok := x.user[userID] | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	res := make([]int32, 0, len(pSet)) | ||||||
|  | 	for k := range pSet { | ||||||
|  | 		res = append(res, k) | ||||||
|  | 	} | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *onlineCache) SetUserOnline(ctx context.Context, userID string, online, offline []int32) error { | ||||||
|  | 	x.lock.Lock() | ||||||
|  | 	defer x.lock.Unlock() | ||||||
|  | 	pSet, ok := x.user[userID] | ||||||
|  | 	if ok { | ||||||
|  | 		for _, p := range offline { | ||||||
|  | 			delete(pSet, p) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(online) > 0 { | ||||||
|  | 		if !ok { | ||||||
|  | 			pSet = make(map[int32]struct{}) | ||||||
|  | 			x.user[userID] = pSet | ||||||
|  | 		} | ||||||
|  | 		for _, p := range online { | ||||||
|  | 			pSet[p] = struct{}{} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(pSet) == 0 { | ||||||
|  | 		delete(x.user, userID) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *onlineCache) GetAllOnlineUsers(ctx context.Context, cursor uint64) (map[string][]int32, uint64, error) { | ||||||
|  | 	if cursor != 0 { | ||||||
|  | 		return nil, 0, nil | ||||||
|  | 	} | ||||||
|  | 	x.lock.RLock() | ||||||
|  | 	defer x.lock.RUnlock() | ||||||
|  | 	res := make(map[string][]int32) | ||||||
|  | 	for k, v := range x.user { | ||||||
|  | 		pSet := make([]int32, 0, len(v)) | ||||||
|  | 		for p := range v { | ||||||
|  | 			pSet = append(pSet, p) | ||||||
|  | 		} | ||||||
|  | 		res[k] = pSet | ||||||
|  | 	} | ||||||
|  | 	return res, 0, nil | ||||||
|  | } | ||||||
							
								
								
									
										98
									
								
								pkg/common/storage/cache/mcache/third.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								pkg/common/storage/cache/mcache/third.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | |||||||
|  | package mcache | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" | ||||||
|  | 	"github.com/openimsdk/tools/errs" | ||||||
|  | 	"github.com/redis/go-redis/v9" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewThirdCache(cache database.Cache) cache.ThirdCache { | ||||||
|  | 	return &thirdCache{ | ||||||
|  | 		cache: cache, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type thirdCache struct { | ||||||
|  | 	cache database.Cache | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) getGetuiTokenKey() string { | ||||||
|  | 	return cachekey.GetGetuiTokenKey() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) getGetuiTaskIDKey() string { | ||||||
|  | 	return cachekey.GetGetuiTaskIDKey() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) getUserBadgeUnreadCountSumKey(userID string) string { | ||||||
|  | 	return cachekey.GetUserBadgeUnreadCountSumKey(userID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) getFcmAccountTokenKey(account string, platformID int) string { | ||||||
|  | 	return cachekey.GetFcmAccountTokenKey(account, platformID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) get(ctx context.Context, key string) (string, error) { | ||||||
|  | 	res, err := c.cache.Get(ctx, []string{key}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	if val, ok := res[key]; ok { | ||||||
|  | 		return val, nil | ||||||
|  | 	} | ||||||
|  | 	return "", errs.Wrap(redis.Nil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) SetFcmToken(ctx context.Context, account string, platformID int, fcmToken string, expireTime int64) (err error) { | ||||||
|  | 	return errs.Wrap(c.cache.Set(ctx, c.getFcmAccountTokenKey(account, platformID), fcmToken, time.Duration(expireTime)*time.Second)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) GetFcmToken(ctx context.Context, account string, platformID int) (string, error) { | ||||||
|  | 	return c.get(ctx, c.getFcmAccountTokenKey(account, platformID)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) DelFcmToken(ctx context.Context, account string, platformID int) error { | ||||||
|  | 	return c.cache.Del(ctx, []string{c.getFcmAccountTokenKey(account, platformID)}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) IncrUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error) { | ||||||
|  | 	return c.cache.Incr(ctx, c.getUserBadgeUnreadCountSumKey(userID), 1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) SetUserBadgeUnreadCountSum(ctx context.Context, userID string, value int) error { | ||||||
|  | 	return c.cache.Set(ctx, c.getUserBadgeUnreadCountSumKey(userID), strconv.Itoa(value), 0) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) GetUserBadgeUnreadCountSum(ctx context.Context, userID string) (int, error) { | ||||||
|  | 	str, err := c.get(ctx, c.getUserBadgeUnreadCountSumKey(userID)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	val, err := strconv.Atoi(str) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, errs.WrapMsg(err, "strconv.Atoi", "str", str) | ||||||
|  | 	} | ||||||
|  | 	return val, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) SetGetuiToken(ctx context.Context, token string, expireTime int64) error { | ||||||
|  | 	return c.cache.Set(ctx, c.getGetuiTokenKey(), token, time.Duration(expireTime)*time.Second) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) GetGetuiToken(ctx context.Context) (string, error) { | ||||||
|  | 	return c.get(ctx, c.getGetuiTokenKey()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) SetGetuiTaskID(ctx context.Context, taskID string, expireTime int64) error { | ||||||
|  | 	return c.cache.Set(ctx, c.getGetuiTaskIDKey(), taskID, time.Duration(expireTime)*time.Second) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c *thirdCache) GetGetuiTaskID(ctx context.Context) (string, error) { | ||||||
|  | 	return c.get(ctx, c.getGetuiTaskIDKey()) | ||||||
|  | } | ||||||
							
								
								
									
										130
									
								
								pkg/common/storage/cache/mcache/token.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								pkg/common/storage/cache/mcache/token.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,130 @@ | |||||||
|  | package mcache | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" | ||||||
|  | 	"github.com/openimsdk/tools/errs" | ||||||
|  | 	"github.com/openimsdk/tools/log" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewTokenCacheModel(cache database.Cache, accessExpire int64) cache.TokenModel { | ||||||
|  | 	c := &tokenCache{cache: cache} | ||||||
|  | 	c.accessExpire = c.getExpireTime(accessExpire) | ||||||
|  | 	return c | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type tokenCache struct { | ||||||
|  | 	cache        database.Cache | ||||||
|  | 	accessExpire time.Duration | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) getTokenKey(userID string, platformID int, token string) string { | ||||||
|  | 	return cachekey.GetTokenKey(userID, platformID) + ":" + token | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) SetTokenFlag(ctx context.Context, userID string, platformID int, token string, flag int) error { | ||||||
|  | 	return x.cache.Set(ctx, x.getTokenKey(userID, platformID, token), strconv.Itoa(flag), x.accessExpire) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetTokenFlagEx set token and flag with expire time | ||||||
|  | func (x *tokenCache) SetTokenFlagEx(ctx context.Context, userID string, platformID int, token string, flag int) error { | ||||||
|  | 	return x.SetTokenFlag(ctx, userID, platformID, token, flag) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) GetTokensWithoutError(ctx context.Context, userID string, platformID int) (map[string]int, error) { | ||||||
|  | 	prefix := x.getTokenKey(userID, platformID, "") | ||||||
|  | 	m, err := x.cache.Prefix(ctx, prefix) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errs.Wrap(err) | ||||||
|  | 	} | ||||||
|  | 	mm := make(map[string]int) | ||||||
|  | 	for k, v := range m { | ||||||
|  | 		state, err := strconv.Atoi(v) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.ZError(ctx, "token value is not int", err, "value", v, "userID", userID, "platformID", platformID) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		mm[strings.TrimPrefix(k, prefix)] = state | ||||||
|  | 	} | ||||||
|  | 	return mm, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) GetAllTokensWithoutError(ctx context.Context, userID string) (map[int]map[string]int, error) { | ||||||
|  | 	prefix := cachekey.UidPidToken + userID + ":" | ||||||
|  | 	tokens, err := x.cache.Prefix(ctx, prefix) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	res := make(map[int]map[string]int) | ||||||
|  | 	for key, flagStr := range tokens { | ||||||
|  | 		flag, err := strconv.Atoi(flagStr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.ZError(ctx, "token value is not int", err, "key", key, "value", flagStr, "userID", userID) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		arr := strings.SplitN(strings.TrimPrefix(key, prefix), ":", 2) | ||||||
|  | 		if len(arr) != 2 { | ||||||
|  | 			log.ZError(ctx, "token value is not int", err, "key", key, "value", flagStr, "userID", userID) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		platformID, err := strconv.Atoi(arr[0]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.ZError(ctx, "token value is not int", err, "key", key, "value", flagStr, "userID", userID) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		token := arr[1] | ||||||
|  | 		if token == "" { | ||||||
|  | 			log.ZError(ctx, "token value is not int", err, "key", key, "value", flagStr, "userID", userID) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		tk, ok := res[platformID] | ||||||
|  | 		if !ok { | ||||||
|  | 			tk = make(map[string]int) | ||||||
|  | 			res[platformID] = tk | ||||||
|  | 		} | ||||||
|  | 		tk[token] = flag | ||||||
|  | 	} | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) SetTokenMapByUidPid(ctx context.Context, userID string, platformID int, m map[string]int) error { | ||||||
|  | 	for token, flag := range m { | ||||||
|  | 		err := x.SetTokenFlag(ctx, userID, platformID, token, flag) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) BatchSetTokenMapByUidPid(ctx context.Context, tokens map[string]map[string]any) error { | ||||||
|  | 	for prefix, tokenFlag := range tokens { | ||||||
|  | 		for token, flag := range tokenFlag { | ||||||
|  | 			flagStr := fmt.Sprintf("%v", flag) | ||||||
|  | 			if err := x.cache.Set(ctx, prefix+":"+token, flagStr, x.accessExpire); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) DeleteTokenByUidPid(ctx context.Context, userID string, platformID int, fields []string) error { | ||||||
|  | 	keys := make([]string, 0, len(fields)) | ||||||
|  | 	for _, token := range fields { | ||||||
|  | 		keys = append(keys, x.getTokenKey(userID, platformID, token)) | ||||||
|  | 	} | ||||||
|  | 	return x.cache.Del(ctx, keys) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *tokenCache) getExpireTime(t int64) time.Duration { | ||||||
|  | 	return time.Hour * 24 * time.Duration(t) | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								pkg/common/storage/cache/redis/online.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								pkg/common/storage/cache/redis/online.go
									
									
									
									
										vendored
									
									
								
							| @ -3,18 +3,23 @@ package redis | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache" | ||||||
| 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/cachekey" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/cache/mcache" | ||||||
| 	"github.com/openimsdk/protocol/constant" | 	"github.com/openimsdk/protocol/constant" | ||||||
| 	"github.com/openimsdk/tools/errs" | 	"github.com/openimsdk/tools/errs" | ||||||
| 	"github.com/openimsdk/tools/log" | 	"github.com/openimsdk/tools/log" | ||||||
| 	"github.com/redis/go-redis/v9" | 	"github.com/redis/go-redis/v9" | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func NewUserOnline(rdb redis.UniversalClient) cache.OnlineCache { | func NewUserOnline(rdb redis.UniversalClient) cache.OnlineCache { | ||||||
|  | 	if rdb == nil { | ||||||
|  | 		return mcache.NewOnlineCache() | ||||||
|  | 	} | ||||||
| 	return &userOnline{ | 	return &userOnline{ | ||||||
| 		rdb:         rdb, | 		rdb:         rdb, | ||||||
| 		expire:      cachekey.OnlineExpire, | 		expire:      cachekey.OnlineExpire, | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								pkg/common/storage/cache/redis/todo.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								pkg/common/storage/cache/redis/todo.go
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +0,0 @@ | |||||||
| package redis |  | ||||||
| 
 |  | ||||||
| // todo: token online third minio |  | ||||||
| @ -60,10 +60,10 @@ func (a *authDatabase) BatchSetTokenMapByUidPid(ctx context.Context, tokens []st | |||||||
| 	setMap := make(map[string]map[string]any) | 	setMap := make(map[string]map[string]any) | ||||||
| 	for _, token := range tokens { | 	for _, token := range tokens { | ||||||
| 		claims, err := tokenverify.GetClaimFromToken(token, authverify.Secret(a.accessSecret)) | 		claims, err := tokenverify.GetClaimFromToken(token, authverify.Secret(a.accessSecret)) | ||||||
| 		key := cachekey.GetTokenKey(claims.UserID, claims.PlatformID) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			continue | 			continue | ||||||
| 		} else { | 		} else { | ||||||
|  | 			key := cachekey.GetTokenKey(claims.UserID, claims.PlatformID) | ||||||
| 			if v, ok := setMap[key]; ok { | 			if v, ok := setMap[key]; ok { | ||||||
| 				v[token] = constant.KickedToken | 				v[token] = constant.KickedToken | ||||||
| 			} else { | 			} else { | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type S3Database interface { | type S3Database interface { | ||||||
| 	PartLimit() *s3.PartLimit | 	PartLimit() (*s3.PartLimit, error) | ||||||
| 	PartSize(ctx context.Context, size int64) (int64, error) | 	PartSize(ctx context.Context, size int64) (int64, error) | ||||||
| 	AuthSign(ctx context.Context, uploadID string, partNumbers []int) (*s3.AuthSignResult, error) | 	AuthSign(ctx context.Context, uploadID string, partNumbers []int) (*s3.AuthSignResult, error) | ||||||
| 	InitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int) (*cont.InitiateUploadResult, error) | 	InitiateMultipartUpload(ctx context.Context, hash string, size int64, expire time.Duration, maxParts int) (*cont.InitiateUploadResult, error) | ||||||
| @ -65,7 +65,7 @@ func (s *s3Database) PartSize(ctx context.Context, size int64) (int64, error) { | |||||||
| 	return s.s3.PartSize(ctx, size) | 	return s.s3.PartSize(ctx, size) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *s3Database) PartLimit() *s3.PartLimit { | func (s *s3Database) PartLimit() (*s3.PartLimit, error) { | ||||||
| 	return s.s3.PartLimit() | 	return s.s3.PartLimit() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										16
									
								
								pkg/common/storage/database/cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								pkg/common/storage/database/cache.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | package database | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type Cache interface { | ||||||
|  | 	Get(ctx context.Context, key []string) (map[string]string, error) | ||||||
|  | 	Prefix(ctx context.Context, prefix string) (map[string]string, error) | ||||||
|  | 	Set(ctx context.Context, key string, value string, expireAt time.Duration) error | ||||||
|  | 	Incr(ctx context.Context, key string, value int) (int, error) | ||||||
|  | 	Del(ctx context.Context, key []string) error | ||||||
|  | 	Lock(ctx context.Context, key string, duration time.Duration) (string, error) | ||||||
|  | 	Unlock(ctx context.Context, key string, value string) error | ||||||
|  | } | ||||||
							
								
								
									
										183
									
								
								pkg/common/storage/database/mgo/cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								pkg/common/storage/database/mgo/cache.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,183 @@ | |||||||
|  | package mgo | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database" | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" | ||||||
|  | 	"github.com/openimsdk/tools/db/mongoutil" | ||||||
|  | 	"github.com/openimsdk/tools/errs" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo/options" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func NewCacheMgo(db *mongo.Database) (*CacheMgo, error) { | ||||||
|  | 	coll := db.Collection(database.CacheName) | ||||||
|  | 	_, err := coll.Indexes().CreateMany(context.Background(), []mongo.IndexModel{ | ||||||
|  | 		{ | ||||||
|  | 			Keys: bson.D{ | ||||||
|  | 				{Key: "key", Value: 1}, | ||||||
|  | 			}, | ||||||
|  | 			Options: options.Index().SetUnique(true), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Keys: bson.D{ | ||||||
|  | 				{Key: "expire_at", Value: 1}, | ||||||
|  | 			}, | ||||||
|  | 			Options: options.Index().SetExpireAfterSeconds(0), | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errs.Wrap(err) | ||||||
|  | 	} | ||||||
|  | 	return &CacheMgo{coll: coll}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type CacheMgo struct { | ||||||
|  | 	coll *mongo.Collection | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) findToMap(res []model.Cache, now time.Time) map[string]string { | ||||||
|  | 	kv := make(map[string]string) | ||||||
|  | 	for _, re := range res { | ||||||
|  | 		if re.ExpireAt != nil && re.ExpireAt.Before(now) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		kv[re.Key] = re.Value | ||||||
|  | 	} | ||||||
|  | 	return kv | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) Get(ctx context.Context, key []string) (map[string]string, error) { | ||||||
|  | 	if len(key) == 0 { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	now := time.Now() | ||||||
|  | 	res, err := mongoutil.Find[model.Cache](ctx, x.coll, bson.M{ | ||||||
|  | 		"key": bson.M{"$in": key}, | ||||||
|  | 		"$or": []bson.M{ | ||||||
|  | 			{"expire_at": bson.M{"$gt": now}}, | ||||||
|  | 			{"expire_at": nil}, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return x.findToMap(res, now), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) Prefix(ctx context.Context, prefix string) (map[string]string, error) { | ||||||
|  | 	now := time.Now() | ||||||
|  | 	res, err := mongoutil.Find[model.Cache](ctx, x.coll, bson.M{ | ||||||
|  | 		"key": bson.M{"$regex": "^" + prefix}, | ||||||
|  | 		"$or": []bson.M{ | ||||||
|  | 			{"expire_at": bson.M{"$gt": now}}, | ||||||
|  | 			{"expire_at": nil}, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return x.findToMap(res, now), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) Set(ctx context.Context, key string, value string, expireAt time.Duration) error { | ||||||
|  | 	cv := &model.Cache{ | ||||||
|  | 		Key:   key, | ||||||
|  | 		Value: value, | ||||||
|  | 	} | ||||||
|  | 	if expireAt > 0 { | ||||||
|  | 		now := time.Now().Add(expireAt) | ||||||
|  | 		cv.ExpireAt = &now | ||||||
|  | 	} | ||||||
|  | 	opt := options.Update().SetUpsert(true) | ||||||
|  | 	return mongoutil.UpdateOne(ctx, x.coll, bson.M{"key": key}, bson.M{"$set": cv}, false, opt) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) Incr(ctx context.Context, key string, value int) (int, error) { | ||||||
|  | 	pipeline := mongo.Pipeline{ | ||||||
|  | 		{ | ||||||
|  | 			{"$set", bson.M{ | ||||||
|  | 				"value": bson.M{ | ||||||
|  | 					"$toString": bson.M{ | ||||||
|  | 						"$add": bson.A{ | ||||||
|  | 							bson.M{"$toInt": "$value"}, | ||||||
|  | 							value, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	opt := options.FindOneAndUpdate().SetReturnDocument(options.After) | ||||||
|  | 	res, err := mongoutil.FindOneAndUpdate[model.Cache](ctx, x.coll, bson.M{"key": key}, pipeline, opt) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return strconv.Atoi(res.Value) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) Del(ctx context.Context, key []string) error { | ||||||
|  | 	if len(key) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	_, err := x.coll.DeleteMany(ctx, bson.M{"key": bson.M{"$in": key}}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) lockKey(key string) string { | ||||||
|  | 	return "LOCK_" + key | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) Lock(ctx context.Context, key string, duration time.Duration) (string, error) { | ||||||
|  | 	tmp, err := uuid.NewUUID() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	if duration <= 0 || duration > time.Minute*10 { | ||||||
|  | 		duration = time.Minute * 10 | ||||||
|  | 	} | ||||||
|  | 	cv := &model.Cache{ | ||||||
|  | 		Key:      x.lockKey(key), | ||||||
|  | 		Value:    tmp.String(), | ||||||
|  | 		ExpireAt: nil, | ||||||
|  | 	} | ||||||
|  | 	ctx, cancel := context.WithTimeout(ctx, time.Second*30) | ||||||
|  | 	defer cancel() | ||||||
|  | 	wait := func() error { | ||||||
|  | 		timeout := time.NewTimer(time.Millisecond * 100) | ||||||
|  | 		defer timeout.Stop() | ||||||
|  | 		select { | ||||||
|  | 		case <-ctx.Done(): | ||||||
|  | 			return ctx.Err() | ||||||
|  | 		case <-timeout.C: | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for { | ||||||
|  | 		if err := mongoutil.DeleteOne(ctx, x.coll, bson.M{"key": key, "expire_at": bson.M{"$lt": time.Now()}}); err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 		expireAt := time.Now().Add(duration) | ||||||
|  | 		cv.ExpireAt = &expireAt | ||||||
|  | 		if err := mongoutil.InsertMany[*model.Cache](ctx, x.coll, []*model.Cache{cv}); err != nil { | ||||||
|  | 			if mongo.IsDuplicateKeyError(err) { | ||||||
|  | 				if err := wait(); err != nil { | ||||||
|  | 					return "", err | ||||||
|  | 				} | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 		return cv.Value, nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (x *CacheMgo) Unlock(ctx context.Context, key string, value string) error { | ||||||
|  | 	return mongoutil.DeleteOne(ctx, x.coll, bson.M{"key": x.lockKey(key), "value": value}) | ||||||
|  | } | ||||||
							
								
								
									
										133
									
								
								pkg/common/storage/database/mgo/cache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								pkg/common/storage/database/mgo/cache_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,133 @@ | |||||||
|  | package mgo | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" | ||||||
|  | 	"github.com/openimsdk/tools/db/mongoutil" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo/options" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestName1111(t *testing.T) { | ||||||
|  | 	coll := Mongodb().Collection("temp") | ||||||
|  | 
 | ||||||
|  | 	//updatePipeline := mongo.Pipeline{ | ||||||
|  | 	//	{ | ||||||
|  | 	//		{"$set", bson.M{ | ||||||
|  | 	//			"age": bson.M{ | ||||||
|  | 	//				"$toString": bson.M{ | ||||||
|  | 	//					"$add": bson.A{ | ||||||
|  | 	//						bson.M{"$toInt": "$age"}, | ||||||
|  | 	//						1, | ||||||
|  | 	//					}, | ||||||
|  | 	//				}, | ||||||
|  | 	//			}, | ||||||
|  | 	//		}}, | ||||||
|  | 	//	}, | ||||||
|  | 	//} | ||||||
|  | 
 | ||||||
|  | 	pipeline := mongo.Pipeline{ | ||||||
|  | 		{ | ||||||
|  | 			{"$set", bson.M{ | ||||||
|  | 				"value": bson.M{ | ||||||
|  | 					"$toString": bson.M{ | ||||||
|  | 						"$add": bson.A{ | ||||||
|  | 							bson.M{"$toInt": "$value"}, | ||||||
|  | 							1, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	opt := options.FindOneAndUpdate().SetUpsert(false).SetReturnDocument(options.After) | ||||||
|  | 	res, err := mongoutil.FindOneAndUpdate[model.Cache](context.Background(), coll, bson.M{"key": "123456"}, pipeline, opt) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	t.Log(res) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestName33333(t *testing.T) { | ||||||
|  | 	c, err := NewCacheMgo(Mongodb()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	if err := c.Set(context.Background(), "123456", "123456", time.Hour); err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := c.Set(context.Background(), "123666", "123666", time.Hour); err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	res1, err := c.Get(context.Background(), []string{"123456"}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	t.Log(res1) | ||||||
|  | 
 | ||||||
|  | 	res2, err := c.Prefix(context.Background(), "123") | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	t.Log(res2) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestName1111aa(t *testing.T) { | ||||||
|  | 
 | ||||||
|  | 	c, err := NewCacheMgo(Mongodb()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	var count int | ||||||
|  | 
 | ||||||
|  | 	key := "123456" | ||||||
|  | 
 | ||||||
|  | 	doFunc := func() { | ||||||
|  | 		value, err := c.Lock(context.Background(), key, time.Second*30) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Log("Lock error", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		tmp := count | ||||||
|  | 		tmp++ | ||||||
|  | 		count = tmp | ||||||
|  | 		t.Log("count", tmp) | ||||||
|  | 		if err := c.Unlock(context.Background(), key, value); err != nil { | ||||||
|  | 			t.Log("Unlock error", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := c.Lock(context.Background(), key, time.Second*10); err != nil { | ||||||
|  | 		t.Log(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var wg sync.WaitGroup | ||||||
|  | 	for i := 0; i < 32; i++ { | ||||||
|  | 		wg.Add(1) | ||||||
|  | 		go func() { | ||||||
|  | 			defer wg.Done() | ||||||
|  | 			for i := 0; i < 100; i++ { | ||||||
|  | 				doFunc() | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	wg.Wait() | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestName111111a(t *testing.T) { | ||||||
|  | 	arr := strings.SplitN("1:testkakskdask:1111", ":", 2) | ||||||
|  | 	t.Log(arr) | ||||||
|  | } | ||||||
| @ -132,19 +132,19 @@ func TestName5(t *testing.T) { | |||||||
| 	t.Log(res) | 	t.Log(res) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestName6(t *testing.T) { | //func TestName6(t *testing.T) { | ||||||
| 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*300) | //	ctx, cancel := context.WithTimeout(context.Background(), time.Second*300) | ||||||
| 	defer cancel() | //	defer cancel() | ||||||
| 	cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second))) | //	cli := Result(mongo.Connect(ctx, options.Client().ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100").SetConnectTimeout(5*time.Second))) | ||||||
| 
 | // | ||||||
| 	tmp, err := NewMsgMongo(cli.Database("openim_v3")) | //	tmp, err := NewMsgMongo(cli.Database("openim_v3")) | ||||||
| 	if err != nil { | //	if err != nil { | ||||||
| 		panic(err) | //		panic(err) | ||||||
| 	} | //	} | ||||||
| 	msg := tmp.(*MsgMgo) | //	msg := tmp.(*MsgMgo) | ||||||
| 	seq, sendTime, err := msg.findBeforeSendTime(ctx, "si_4924054191_9511766539", 1144) | //	seq, sendTime, err := msg.findBeforeSendTime(ctx, "si_4924054191_9511766539", 1144) | ||||||
| 	if err != nil { | //	if err != nil { | ||||||
| 		panic(err) | //		panic(err) | ||||||
| 	} | //	} | ||||||
| 	t.Log(seq, sendTime) | //	t.Log(seq, sendTime) | ||||||
| } | //} | ||||||
|  | |||||||
| @ -2,10 +2,11 @@ package mgo | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"go.mongodb.org/mongo-driver/mongo" |  | ||||||
| 	"go.mongodb.org/mongo-driver/mongo/options" |  | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo/options" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func Result[V any](val V, err error) V { | func Result[V any](val V, err error) V { | ||||||
| @ -19,7 +20,7 @@ func Mongodb() *mongo.Database { | |||||||
| 	return Result( | 	return Result( | ||||||
| 		mongo.Connect(context.Background(), | 		mongo.Connect(context.Background(), | ||||||
| 			options.Client(). | 			options.Client(). | ||||||
| 				ApplyURI("mongodb://openIM:openIM123@172.16.8.48:37017/openim_v3?maxPoolSize=100"). | 				ApplyURI("mongodb://openIM:openIM123@172.16.8.135:37017/openim_v3?maxPoolSize=100"). | ||||||
| 				SetConnectTimeout(5*time.Second)), | 				SetConnectTimeout(5*time.Second)), | ||||||
| 	).Database("openim_v3") | 	).Database("openim_v3") | ||||||
| } | } | ||||||
|  | |||||||
| @ -18,4 +18,5 @@ const ( | |||||||
| 	SeqConversationName     = "seq" | 	SeqConversationName     = "seq" | ||||||
| 	SeqUserName             = "seq_user" | 	SeqUserName             = "seq_user" | ||||||
| 	StreamMsgName           = "stream_msg" | 	StreamMsgName           = "stream_msg" | ||||||
|  | 	CacheName               = "cache" | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								pkg/common/storage/model/cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								pkg/common/storage/model/cache.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | package model | ||||||
|  | 
 | ||||||
|  | import "time" | ||||||
|  | 
 | ||||||
|  | type Cache struct { | ||||||
|  | 	Key      string     `bson:"key"` | ||||||
|  | 	Value    string     `bson:"value"` | ||||||
|  | 	ExpireAt *time.Time `bson:"expire_at"` | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user