4510 lines
163 KiB
Go
4510 lines
163 KiB
Go
// Package store provides methods for registering and accessing database adapters.
|
|
package store
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tinode/chat/server/auth"
|
|
adapter "github.com/tinode/chat/server/db"
|
|
"github.com/tinode/chat/server/logs"
|
|
"github.com/tinode/chat/server/media"
|
|
"github.com/tinode/chat/server/store/types"
|
|
"github.com/tinode/chat/server/validate"
|
|
)
|
|
|
|
var adp adapter.Adapter
|
|
var availableAdapters = make(map[string]adapter.Adapter)
|
|
var mediaHandler media.Handler
|
|
var activeTenantIDs sync.Map
|
|
|
|
// Unique ID generator
|
|
var uGen types.UidGenerator
|
|
|
|
type configType struct {
|
|
// 16-byte key for XTEA. Used to initialize types.UidGenerator.
|
|
UidKey []byte `json:"uid_key"`
|
|
// Maximum number of results to return from adapter.
|
|
MaxResults int `json:"max_results"`
|
|
// DB adapter name to use. Should be one of those specified in `Adapters`.
|
|
UseAdapter string `json:"use_adapter"`
|
|
// Configurations for individual adapters.
|
|
Adapters map[string]json.RawMessage `json:"adapters"`
|
|
// Optional Redis configuration for group message seq and unique keys.
|
|
Redis json.RawMessage `json:"redis"`
|
|
}
|
|
|
|
func openAdapter(workerId int, jsonconf json.RawMessage) error {
|
|
var config configType
|
|
if err := json.Unmarshal(jsonconf, &config); err != nil {
|
|
return errors.New("store: failed to parse config: " + err.Error() + "(" + string(jsonconf) + ")")
|
|
}
|
|
|
|
if adp == nil {
|
|
if len(config.UseAdapter) > 0 {
|
|
// Adapter name specified explicitly.
|
|
if ad, ok := availableAdapters[config.UseAdapter]; ok {
|
|
adp = ad
|
|
} else {
|
|
return errors.New("store: " + config.UseAdapter + " adapter is not available in this binary")
|
|
}
|
|
} else if len(availableAdapters) == 1 {
|
|
// Default to the only entry in availableAdapters.
|
|
for _, v := range availableAdapters {
|
|
adp = v
|
|
}
|
|
} else {
|
|
return errors.New("store: db adapter is not specified. Please set `store_config.use_adapter` in `meetingim.conf`")
|
|
}
|
|
}
|
|
|
|
if adp.IsOpen() {
|
|
return errors.New("store: connection is already opened")
|
|
}
|
|
tenantBusiness, ok := adp.(adapter.TenantBusinessAdapter)
|
|
if !ok {
|
|
return errors.New("store: " + adp.GetName() + " adapter does not support tenant-scoped business access")
|
|
}
|
|
if !tenantBusiness.TenantBusinessReady() {
|
|
return errors.New("store: " + adp.GetName() + " adapter tenant-scoped business access is not ready")
|
|
}
|
|
if _, ok := adp.(adapter.TenantAdapter); !ok {
|
|
return errors.New("store: " + adp.GetName() + " adapter does not support tenant resolution")
|
|
}
|
|
if _, ok := adp.(adapter.TenantAuthAdapter); !ok {
|
|
return errors.New("store: " + adp.GetName() + " adapter does not support tenant-scoped authentication")
|
|
}
|
|
|
|
// Initialize snowflake.
|
|
if workerId < 0 || workerId > 1023 {
|
|
return errors.New("store: invalid worker ID")
|
|
}
|
|
|
|
if err := uGen.Init(uint(workerId), config.UidKey); err != nil {
|
|
return errors.New("store: failed to init snowflake: " + err.Error())
|
|
}
|
|
|
|
if err := adp.SetMaxResults(config.MaxResults); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := initGroupMessageRuntime(config.Redis); err != nil {
|
|
return err
|
|
}
|
|
|
|
var adapterConfig json.RawMessage
|
|
if config.Adapters != nil {
|
|
adapterConfig = config.Adapters[adp.GetName()]
|
|
}
|
|
|
|
return adp.Open(adapterConfig)
|
|
}
|
|
|
|
// PersistentStorageInterface defines methods used for interation with persistent storage.
|
|
type PersistentStorageInterface interface {
|
|
Open(workerId int, jsonconf json.RawMessage) error
|
|
Close() error
|
|
IsOpen() bool
|
|
GetAdapter() adapter.Adapter
|
|
GetAdapterName() string
|
|
GetAdapterVersion() int
|
|
GetDbVersion() int
|
|
InitDb(jsonconf json.RawMessage, reset bool) error
|
|
UpgradeDb(jsonconf json.RawMessage) error
|
|
GetUid() types.Uid
|
|
GetUidString() string
|
|
DbStats() func() any
|
|
GetAuthNames() []string
|
|
GetAuthHandler(name string) auth.AuthHandler
|
|
GetLogicalAuthHandler(name string) auth.AuthHandler
|
|
GetValidator(name string) validate.Validator
|
|
GetMediaHandler() media.Handler
|
|
UseMediaHandler(name, config string) error
|
|
}
|
|
|
|
// Store is the main object for interacting with persistent storage.
|
|
var Store PersistentStorageInterface
|
|
|
|
type storeObj struct{}
|
|
|
|
const (
|
|
cacheNamespaceUser = "user"
|
|
cacheNamespaceTopic = "topic"
|
|
cacheNamespaceSub = "sub"
|
|
cacheNamespaceGroupSettings = "group_settings"
|
|
cacheNamespaceGroupMember = "group_member"
|
|
cacheNamespaceVerification = "verification"
|
|
cacheNamespaceActionToken = "action_token"
|
|
cacheNamespaceFriends = "friends"
|
|
cacheNamespaceBlocklist = "blocklist"
|
|
cacheNamespaceUnreadCount = "unread_count"
|
|
cacheNamespaceGroupPage = "group_member_page"
|
|
)
|
|
|
|
// TenantsPersistenceInterface defines tenant registry lookups used before authentication.
|
|
type TenantsPersistenceInterface interface {
|
|
GetByCode(code string) (*types.Tenant, error)
|
|
GetByID(id types.TenantID) (*types.Tenant, error)
|
|
ListActive(limit int) ([]types.Tenant, error)
|
|
}
|
|
|
|
type tenantsMapper struct{}
|
|
|
|
// Tenants is the tenant registry access object.
|
|
var Tenants TenantsPersistenceInterface
|
|
|
|
// AdminInternalConfigProvider optionally resolves hotgo-owned configuration
|
|
// through the Admin service instead of reading the hotgo database directly.
|
|
type AdminInternalConfigProvider interface {
|
|
GetTenantByCode(code string) (*types.Tenant, error)
|
|
GetTenantByID(id types.TenantID) (*types.Tenant, error)
|
|
ListActiveTenants(limit int) ([]types.Tenant, error)
|
|
RegistrationSMSEnabled() (bool, error)
|
|
ThirdPartyConfig(group string, keys ...string) (map[string]string, error)
|
|
}
|
|
|
|
var adminInternalConfigProvider AdminInternalConfigProvider
|
|
|
|
func SetAdminInternalConfigProvider(provider AdminInternalConfigProvider) {
|
|
adminInternalConfigProvider = provider
|
|
}
|
|
|
|
// RegistrationConfigPersistenceInterface exposes global registration policy.
|
|
type RegistrationConfigPersistenceInterface interface {
|
|
SMSEnabled() (bool, error)
|
|
}
|
|
|
|
type registrationConfigMapper struct{}
|
|
|
|
// RegistrationConfig reads registration policy from the control database.
|
|
var RegistrationConfig RegistrationConfigPersistenceInterface
|
|
|
|
func (registrationConfigMapper) SMSEnabled() (bool, error) {
|
|
if enabled, ok := registrationSMSCache.get("im:register_sms_enabled"); ok {
|
|
return enabled, nil
|
|
}
|
|
if adminInternalConfigProvider != nil {
|
|
if enabled, err := adminInternalConfigProvider.RegistrationSMSEnabled(); err == nil {
|
|
registrationSMSCache.set("im:register_sms_enabled", enabled)
|
|
return enabled, nil
|
|
} else {
|
|
logs.Warn.Printf("admin internal registration config fallback failed: %v", err)
|
|
}
|
|
}
|
|
configAdp, ok := adp.(adapter.RegistrationConfigAdapter)
|
|
if !ok {
|
|
return true, types.ErrUnsupported
|
|
}
|
|
enabled, err := configAdp.RegistrationSMSEnabled()
|
|
if err == nil {
|
|
registrationSMSCache.set("im:register_sms_enabled", enabled)
|
|
}
|
|
return enabled, err
|
|
}
|
|
|
|
// ThirdPartyConfigPersistenceInterface exposes global third-party integration settings.
|
|
type ThirdPartyConfigPersistenceInterface interface {
|
|
Get(group string, keys ...string) (map[string]string, error)
|
|
}
|
|
|
|
type thirdPartyConfigMapper struct{}
|
|
|
|
// ThirdPartyConfig reads third-party settings from the control database.
|
|
var ThirdPartyConfig ThirdPartyConfigPersistenceInterface = thirdPartyConfigMapper{}
|
|
|
|
func (thirdPartyConfigMapper) Get(group string, keys ...string) (map[string]string, error) {
|
|
cacheKey := thirdPartyCacheKey(group, keys...)
|
|
if cached, ok := thirdPartyConfigCache.get(cacheKey); ok {
|
|
return cloneStringMap(cached), nil
|
|
}
|
|
if adminInternalConfigProvider != nil {
|
|
if result, err := adminInternalConfigProvider.ThirdPartyConfig(group, keys...); err == nil {
|
|
thirdPartyConfigCache.set(cacheKey, cloneStringMap(result))
|
|
return result, nil
|
|
} else {
|
|
logs.Warn.Printf("admin internal third-party config fallback failed: group=%s err=%v", group, err)
|
|
}
|
|
}
|
|
configAdp, ok := adp.(adapter.ThirdPartyConfigAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
result, err := configAdp.ThirdPartyConfig(group, keys...)
|
|
if err == nil {
|
|
thirdPartyConfigCache.set(cacheKey, cloneStringMap(result))
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
type TenantEntryPersistenceInterface interface {
|
|
CreatePublicUpload(upload *types.PublicUpload) (*types.PublicUpload, error)
|
|
CreateTenantApplication(application *types.TenantApplication) (*types.TenantApplication, error)
|
|
CreateTenantEntryFeedback(feedback *types.TenantEntryFeedback) (*types.TenantEntryFeedback, error)
|
|
}
|
|
|
|
type tenantEntryMapper struct{}
|
|
|
|
var TenantEntry TenantEntryPersistenceInterface
|
|
|
|
func (tenantEntryMapper) CreatePublicUpload(upload *types.PublicUpload) (*types.PublicUpload, error) {
|
|
if upload == nil || strings.TrimSpace(upload.FileID) == "" || strings.TrimSpace(upload.Purpose) == "" ||
|
|
strings.TrimSpace(upload.MimeType) == "" || strings.TrimSpace(upload.Location) == "" || upload.Size <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
entryAdp, ok := adp.(adapter.TenantEntryAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
return entryAdp.PublicUploadCreate(upload)
|
|
}
|
|
|
|
func (tenantEntryMapper) CreateTenantApplication(application *types.TenantApplication) (*types.TenantApplication, error) {
|
|
if application == nil || strings.TrimSpace(application.ApplicationID) == "" ||
|
|
strings.TrimSpace(application.CreditCode) == "" || strings.TrimSpace(application.IdempotencyKey) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
entryAdp, ok := adp.(adapter.TenantEntryAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
return entryAdp.TenantApplicationCreate(application)
|
|
}
|
|
|
|
func (tenantEntryMapper) CreateTenantEntryFeedback(feedback *types.TenantEntryFeedback) (*types.TenantEntryFeedback, error) {
|
|
if feedback == nil || strings.TrimSpace(feedback.FeedbackID) == "" ||
|
|
strings.TrimSpace(feedback.Description) == "" || strings.TrimSpace(feedback.IdempotencyKey) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
entryAdp, ok := adp.(adapter.TenantEntryAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
return entryAdp.TenantEntryFeedbackCreate(feedback)
|
|
}
|
|
|
|
// GetByCode resolves a tenant by its public enterprise code.
|
|
func (tenantsMapper) GetByCode(code string) (*types.Tenant, error) {
|
|
code = strings.TrimSpace(code)
|
|
if tenant, ok := tenantByCodeCache.get(code); ok {
|
|
return cloneTenant(tenant), nil
|
|
}
|
|
if adminInternalConfigProvider != nil {
|
|
if tenant, err := adminInternalConfigProvider.GetTenantByCode(code); err == nil && tenant != nil {
|
|
tenantByCodeCache.set(code, cloneTenant(tenant))
|
|
tenantByIDCache.set(strconv.FormatInt(int64(tenant.ID), 10), cloneTenant(tenant))
|
|
return tenant, nil
|
|
} else if err != nil {
|
|
logs.Warn.Printf("admin internal tenant by code fallback failed: code=%s err=%v", code, err)
|
|
}
|
|
}
|
|
tenantAdp, ok := adp.(adapter.TenantAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
tenant, err := tenantAdp.TenantGetByCode(code)
|
|
if err == nil && tenant != nil {
|
|
tenantByCodeCache.set(code, cloneTenant(tenant))
|
|
tenantByIDCache.set(strconv.FormatInt(int64(tenant.ID), 10), cloneTenant(tenant))
|
|
}
|
|
return tenant, err
|
|
}
|
|
|
|
// GetByID resolves an internal tenant ID to its registry record.
|
|
func (tenantsMapper) GetByID(id types.TenantID) (*types.Tenant, error) {
|
|
if !id.IsValid() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
idKey := strconv.FormatInt(int64(id), 10)
|
|
if tenant, ok := tenantByIDCache.get(idKey); ok {
|
|
return cloneTenant(tenant), nil
|
|
}
|
|
if adminInternalConfigProvider != nil {
|
|
if tenant, err := adminInternalConfigProvider.GetTenantByID(id); err == nil && tenant != nil {
|
|
tenantByIDCache.set(idKey, cloneTenant(tenant))
|
|
tenantByCodeCache.set(tenant.Code, cloneTenant(tenant))
|
|
return tenant, nil
|
|
} else if err != nil {
|
|
logs.Warn.Printf("admin internal tenant by id fallback failed: tenant=%d err=%v", id, err)
|
|
}
|
|
}
|
|
tenantAdp, ok := adp.(adapter.TenantAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
tenant, err := tenantAdp.TenantGetByID(id)
|
|
if err == nil && tenant != nil {
|
|
tenantByIDCache.set(idKey, cloneTenant(tenant))
|
|
tenantByCodeCache.set(tenant.Code, cloneTenant(tenant))
|
|
}
|
|
return tenant, err
|
|
}
|
|
|
|
func (tenantsMapper) ListActive(limit int) ([]types.Tenant, error) {
|
|
if limit <= 0 {
|
|
limit = 1000
|
|
}
|
|
if adminInternalConfigProvider != nil {
|
|
if tenants, err := adminInternalConfigProvider.ListActiveTenants(limit); err == nil {
|
|
for i := range tenants {
|
|
tenant := tenants[i]
|
|
tenantByIDCache.set(strconv.FormatInt(int64(tenant.ID), 10), cloneTenant(&tenant))
|
|
tenantByCodeCache.set(tenant.Code, cloneTenant(&tenant))
|
|
}
|
|
return tenants, nil
|
|
} else {
|
|
logs.Warn.Printf("admin internal active tenants fallback failed: err=%v", err)
|
|
}
|
|
}
|
|
tenantAdp, ok := adp.(adapter.TenantAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
return tenantAdp.TenantListActive(limit)
|
|
}
|
|
|
|
func tenantAuthAdapter(tenantID types.TenantID) (adapter.TenantAuthAdapter, error) {
|
|
if !tenantID.IsValid() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
if routed, ok := adp.(adapter.TenantRoutedAdapter); ok {
|
|
_, authAdp, err := routed.TenantAdapterFor(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return authAdp, nil
|
|
}
|
|
tenantAdp, ok := adp.(adapter.TenantAuthAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
return tenantAdp, nil
|
|
}
|
|
|
|
func tenantBusinessAdapter(tenantID types.TenantID) (adapter.TenantBusinessAdapter, error) {
|
|
if !tenantID.IsValid() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
if routed, ok := adp.(adapter.TenantRoutedAdapter); ok {
|
|
businessAdp, _, err := routed.TenantAdapterFor(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !businessAdp.TenantBusinessReady() {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
activeTenantIDs.Store(tenantID, struct{}{})
|
|
return businessAdp, nil
|
|
}
|
|
tenantAdp, ok := adp.(adapter.TenantBusinessAdapter)
|
|
if !ok {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
if !tenantAdp.TenantBusinessReady() {
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
activeTenantIDs.Store(tenantID, struct{}{})
|
|
return tenantAdp, nil
|
|
}
|
|
|
|
// KnownTenantIDs returns tenants which have used the business Store since this
|
|
// process started. It is used only for tenant-scoped maintenance work.
|
|
func KnownTenantIDs() []types.TenantID {
|
|
var result []types.TenantID
|
|
activeTenantIDs.Range(func(key, _ any) bool {
|
|
result = append(result, key.(types.TenantID))
|
|
return true
|
|
})
|
|
return result
|
|
}
|
|
|
|
// TenantStore is a validated tenant scope. Domain mappers still receive the
|
|
// tenant explicitly so it remains visible at every persistence boundary.
|
|
type TenantStore struct {
|
|
TenantID types.TenantID
|
|
}
|
|
|
|
// ForTenant validates and returns a tenant-scoped Store handle.
|
|
func ForTenant(tenantID types.TenantID) (*TenantStore, error) {
|
|
if _, err := tenantBusinessAdapter(tenantID); err != nil {
|
|
return nil, err
|
|
}
|
|
return &TenantStore{TenantID: tenantID}, nil
|
|
}
|
|
|
|
// Open initializes the persistence system. Adapter holds a connection pool for a database instance.
|
|
//
|
|
// name - name of the adapter rquested in the config file
|
|
// jsonconf - configuration string
|
|
func (storeObj) Open(workerId int, jsonconf json.RawMessage) error {
|
|
if err := openAdapter(workerId, jsonconf); err != nil {
|
|
return err
|
|
}
|
|
|
|
return adp.CheckDbVersion()
|
|
}
|
|
|
|
// Close terminates connection to persistent storage.
|
|
func (storeObj) Close() error {
|
|
closeGroupMessageRuntime()
|
|
if adp.IsOpen() {
|
|
return adp.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsOpen checks if persistent storage connection has been initialized.
|
|
func (storeObj) IsOpen() bool {
|
|
if adp != nil {
|
|
return adp.IsOpen()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetAdapter returns the currently configured adapter.
|
|
func (storeObj) GetAdapter() adapter.Adapter {
|
|
return adp
|
|
}
|
|
|
|
// GetAdapterName returns the name of the current adater.
|
|
func (storeObj) GetAdapterName() string {
|
|
if adp != nil {
|
|
return adp.GetName()
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GetAdapterVersion returns version of the current adater.
|
|
func (storeObj) GetAdapterVersion() int {
|
|
if adp != nil {
|
|
return adp.Version()
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// GetDbVersion returns version of the underlying database.
|
|
func (storeObj) GetDbVersion() int {
|
|
if adp != nil {
|
|
vers, _ := adp.GetDbVersion()
|
|
return vers
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// InitDb creates and configures a new database instance. If 'reset' is true it will first
|
|
// attempt to drop an existing database. If jsconf is nil it will assume that the adapter is
|
|
// already open. If it's non-nil and the adapter is not open, it will use the config string
|
|
// to open the adapter first.
|
|
func (s storeObj) InitDb(jsonconf json.RawMessage, reset bool) error {
|
|
if !s.IsOpen() {
|
|
if err := openAdapter(1, jsonconf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return adp.CreateDb(reset)
|
|
}
|
|
|
|
// UpgradeDb performes an upgrade of the database to the current adapter version.
|
|
// If jsconf is nil it will assume that the adapter is already open. If it's non-nil and the
|
|
// adapter is not open, it will use the config string to open the adapter first.
|
|
func (s storeObj) UpgradeDb(jsonconf json.RawMessage) error {
|
|
if !s.IsOpen() {
|
|
if err := openAdapter(1, jsonconf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return adp.UpgradeDb()
|
|
}
|
|
|
|
// RegisterAdapter makes a persistence adapter available.
|
|
// If Register is called twice or if the adapter is nil, it panics.
|
|
func RegisterAdapter(a adapter.Adapter) {
|
|
if a == nil {
|
|
panic("store: Register adapter is nil")
|
|
}
|
|
|
|
adapterName := a.GetName()
|
|
if _, ok := availableAdapters[adapterName]; ok {
|
|
panic("store: adapter '" + adapterName + "' is already registered")
|
|
}
|
|
availableAdapters[adapterName] = a
|
|
}
|
|
|
|
// GetUid generates a unique ID suitable for use as a primary key.
|
|
func (storeObj) GetUid() types.Uid {
|
|
return uGen.Get()
|
|
}
|
|
|
|
// GetUidString generate unique ID as a string.
|
|
func (storeObj) GetUidString() string {
|
|
return uGen.GetStr()
|
|
}
|
|
|
|
// DecodeUid takes an XTEA encrypted Uid and decrypts it into an int64.
|
|
// This is needed for sql compatibility. Tte original int64 values
|
|
// are generated by snowflake which ensures that the top bit is unset.
|
|
func DecodeUid(uid types.Uid) int64 {
|
|
if uid.IsZero() {
|
|
return 0
|
|
}
|
|
return uGen.DecodeUid(uid)
|
|
}
|
|
|
|
// EncodeUid applies XTEA encryption to an int64 value. It's the inverse of DecodeUid.
|
|
func EncodeUid(id int64) types.Uid {
|
|
if id == 0 {
|
|
return types.ZeroUid
|
|
}
|
|
return uGen.EncodeInt64(id)
|
|
}
|
|
|
|
// DbStats returns a callback returning db connection stats object.
|
|
func (s storeObj) DbStats() func() any {
|
|
if !s.IsOpen() {
|
|
return nil
|
|
}
|
|
return adp.Stats
|
|
}
|
|
|
|
// UsersPersistenceInterface is an interface which defines methods for persistent storage of user records.
|
|
type UsersPersistenceInterface interface {
|
|
Create(tenantID types.TenantID, user *types.User, private any) (*types.User, error)
|
|
GetAuthRecord(tenantID types.TenantID, user types.Uid) (string, auth.Level, []byte, time.Time, error)
|
|
GetAuthByLogin(tenantID types.TenantID, login string) (types.Uid, auth.Level, []byte, time.Time, error)
|
|
AddAuthRecord(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, secret []byte, expires time.Time) error
|
|
UpdateAuthRecord(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, username string, secret []byte, expires time.Time) error
|
|
DeleteAuthRecord(tenantID types.TenantID, uid types.Uid) error
|
|
Get(tenantID types.TenantID, uid types.Uid) (*types.User, error)
|
|
GetAll(tenantID types.TenantID, uid ...types.Uid) ([]types.User, error)
|
|
GetByPhone(tenantID types.TenantID, phone string) (types.Uid, error)
|
|
GetByCred(tenantID types.TenantID, method, value string) (types.Uid, error)
|
|
Delete(tenantID types.TenantID, id types.Uid, hard bool) error
|
|
UpdateLastSeen(tenantID types.TenantID, uid types.Uid, userAgent string, when time.Time) error
|
|
Update(tenantID types.TenantID, uid types.Uid, update map[string]any) error
|
|
UpdateTags(tenantID types.TenantID, uid types.Uid, add, remove, reset []string) ([]string, error)
|
|
UpdateState(tenantID types.TenantID, uid types.Uid, state types.ObjState) error
|
|
GetSubs(tenantID types.TenantID, id types.Uid) ([]types.Subscription, error)
|
|
FindSubs(tenantID types.TenantID, caller types.Uid, prefPrefix string, required [][]string, optional []string, activeOnly bool) ([]types.Subscription, error)
|
|
FindOne(tenantID types.TenantID, tag string) (string, error)
|
|
GetTopics(tenantID types.TenantID, id types.Uid, opts *types.QueryOpt) ([]types.Subscription, error)
|
|
GetTopicsAny(tenantID types.TenantID, id types.Uid, opts *types.QueryOpt) ([]types.Subscription, error)
|
|
GetOwnTopics(tenantID types.TenantID, id types.Uid) ([]string, error)
|
|
GetChannels(tenantID types.TenantID, id types.Uid) ([]string, error)
|
|
UpsertCred(tenantID types.TenantID, cred *types.Credential) (bool, error)
|
|
ConfirmCred(tenantID types.TenantID, id types.Uid, method string) error
|
|
FailCred(tenantID types.TenantID, id types.Uid, method string) error
|
|
GetActiveCred(tenantID types.TenantID, id types.Uid, method string) (*types.Credential, error)
|
|
GetAllCreds(tenantID types.TenantID, id types.Uid, method string, validatedOnly bool) ([]types.Credential, error)
|
|
DelCred(tenantID types.TenantID, id types.Uid, method, value string) error
|
|
GetUnreadCount(tenantID types.TenantID, ids ...types.Uid) (map[types.Uid]int, error)
|
|
GetUnvalidated(tenantID types.TenantID, lastUpdatedBefore time.Time, limit int) ([]types.Uid, error)
|
|
}
|
|
|
|
// usersMapper is a concrete type which implements UsersPersistenceInterface.
|
|
type usersMapper struct{}
|
|
|
|
// Users is a singleton ancor object exporting UsersPersistenceInterface methods.
|
|
var Users UsersPersistenceInterface
|
|
|
|
// Create inserts User object into a database, updates creation time and assigns UID
|
|
func (usersMapper) Create(tenantID types.TenantID, user *types.User, private any) (*types.User, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil || user.TenantID != tenantID {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
|
|
user.SetUid(Store.GetUid())
|
|
user.InitTimes()
|
|
|
|
err = tenantAdp.TenantUserCreate(tenantID, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create user's subscription to 'me' && 'fnd'. These topics are ephemeral, the topic object need not to be
|
|
// inserted.
|
|
err = Subs.Create(tenantID,
|
|
&types.Subscription{
|
|
ObjHeader: types.ObjHeader{CreatedAt: user.CreatedAt},
|
|
TenantID: user.TenantID,
|
|
User: user.Id,
|
|
Topic: user.Uid().UserId(),
|
|
ModeWant: types.ModeCMeFnd,
|
|
ModeGiven: types.ModeCMeFnd,
|
|
Private: private,
|
|
},
|
|
&types.Subscription{
|
|
ObjHeader: types.ObjHeader{CreatedAt: user.CreatedAt},
|
|
TenantID: user.TenantID,
|
|
User: user.Id,
|
|
Topic: user.Uid().FndName(),
|
|
ModeWant: types.ModeCMeFnd,
|
|
ModeGiven: types.ModeCMeFnd,
|
|
Private: nil,
|
|
})
|
|
if err != nil {
|
|
// Best effort to delete incomplete user record. Orphaned user records are not a problem.
|
|
// They just take up space.
|
|
tenantAdp.TenantUserDelete(tenantID, user.Uid(), true)
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetAuthRecord takes a user ID and a authentication scheme name, fetches unique scheme-dependent identifier and
|
|
// authentication secret.
|
|
func (usersMapper) GetAuthRecord(tenantID types.TenantID, user types.Uid) (string, auth.Level, []byte, time.Time, error) {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return "", auth.LevelNone, nil, time.Time{}, err
|
|
}
|
|
return tenantAdp.TenantAuthGetRecord(tenantID, user)
|
|
}
|
|
|
|
// GetAuthByLogin fetches a Basic authentication record by username or phone.
|
|
func (usersMapper) GetAuthByLogin(tenantID types.TenantID, login string) (types.Uid, auth.Level, []byte, time.Time, error) {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return types.ZeroUid, auth.LevelNone, nil, time.Time{}, err
|
|
}
|
|
return tenantAdp.TenantAuthGetByLogin(tenantID, login)
|
|
}
|
|
|
|
// AddAuthRecord creates a new authentication record for the given user.
|
|
func (usersMapper) AddAuthRecord(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, secret []byte,
|
|
expires time.Time) error {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantAuthAddRecord(tenantID, uid, authLvl, secret, expires)
|
|
}
|
|
|
|
// UpdateAuthRecord updates authentication record with a new secret and expiration time.
|
|
func (usersMapper) UpdateAuthRecord(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, username string,
|
|
secret []byte, expires time.Time) error {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantAuthUpdateRecord(tenantID, uid, username, authLvl, secret, expires)
|
|
}
|
|
|
|
// DeleteAuthRecord deletes the user's Basic authentication record.
|
|
func (usersMapper) DeleteAuthRecord(tenantID types.TenantID, uid types.Uid) error {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantAuthDeleteRecord(tenantID, uid)
|
|
}
|
|
|
|
// Get returns a user object for the given user ID or nil if the user is not found.
|
|
func (usersMapper) Get(tenantID types.TenantID, uid types.Uid) (*types.User, error) {
|
|
var cached types.User
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceUser, uid.String(), &cached); err != nil {
|
|
logs.Warn.Printf("cache user[%s]: read failed: %v", uid.UserId(), err)
|
|
} else if ok {
|
|
if cached.TenantID == types.ZeroTenantID {
|
|
cached.TenantID = tenantID
|
|
}
|
|
if cached.TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
return &cached, nil
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user, err := tenantAdp.TenantUserGet(tenantID, uid)
|
|
if err == nil && user != nil && user.TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
if err == nil && user != nil {
|
|
if cacheErr := CacheObject(tenantID, cacheNamespaceUser, uid.String(), user); cacheErr != nil {
|
|
logs.Warn.Printf("cache user[%s]: write failed: %v", uid.UserId(), cacheErr)
|
|
}
|
|
}
|
|
return user, err
|
|
}
|
|
|
|
// GetAll returns a slice of user objects for the given user IDs.
|
|
func (usersMapper) GetAll(tenantID types.TenantID, uid ...types.Uid) ([]types.User, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
users, err := tenantAdp.TenantUserGetAll(tenantID, uid...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range users {
|
|
if users[i].TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
// GetByCred returns user ID for the given validated credential.
|
|
func (usersMapper) GetByCred(tenantID types.TenantID, method, value string) (types.Uid, error) {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return types.ZeroUid, err
|
|
}
|
|
return tenantAdp.TenantUserGetByCred(tenantID, method, value)
|
|
}
|
|
|
|
// GetByPhone resolves an account by the phone identity stored on im_users.
|
|
// It intentionally does not require a completed tel credential.
|
|
func (usersMapper) GetByPhone(tenantID types.TenantID, phone string) (types.Uid, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return types.ZeroUid, err
|
|
}
|
|
return tenantAdp.TenantUserGetByPhone(tenantID, phone)
|
|
}
|
|
|
|
// Delete deletes user records.
|
|
func (usersMapper) Delete(tenantID types.TenantID, id types.Uid, hard bool) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantUserDelete(tenantID, id, hard); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceUser, id.String())
|
|
return nil
|
|
}
|
|
|
|
// UpdateLastSeen updates LastSeen and UserAgent.
|
|
func (usersMapper) UpdateLastSeen(tenantID types.TenantID, uid types.Uid, userAgent string, when time.Time) error {
|
|
return Users.Update(tenantID, uid, map[string]any{"LastSeen": when, "UserAgent": userAgent})
|
|
}
|
|
|
|
// Update is a general-purpose update of user data.
|
|
func (usersMapper) Update(tenantID types.TenantID, uid types.Uid, update map[string]any) error {
|
|
if _, ok := update["UpdatedAt"]; !ok {
|
|
update["UpdatedAt"] = types.TimeNow()
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantUserUpdate(tenantID, uid, update); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceUser, uid.String())
|
|
return nil
|
|
}
|
|
|
|
// UpdateTags either adds, removes, or resets tags to the given slices.
|
|
func (usersMapper) UpdateTags(tenantID types.TenantID, uid types.Uid, add, remove, reset []string) ([]string, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUserUpdateTags(tenantID, uid, add, remove, reset)
|
|
}
|
|
|
|
// UpdateState changes user's state and state of some topics associated with the user.
|
|
func (usersMapper) UpdateState(tenantID types.TenantID, uid types.Uid, state types.ObjState) error {
|
|
update := map[string]any{
|
|
"State": state,
|
|
"StateAt": types.TimeNow()}
|
|
return Users.Update(tenantID, uid, update)
|
|
}
|
|
|
|
// GetSubs loads *all* subscriptions for the given user.
|
|
// Does not load Public/Trusted or Private, does not load deleted subscriptions.
|
|
func (usersMapper) GetSubs(tenantID types.TenantID, id types.Uid) ([]types.Subscription, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantSubsForUser(tenantID, id)
|
|
}
|
|
|
|
// FindSubs find a list of users and topics for the given tags. Results are formatted as subscriptions.
|
|
// `required` specifies an AND of ORs for required terms:
|
|
// at least one element of every sublist in `required` must be present in the object's tags list.
|
|
// `optional` specifies a list of optional terms.
|
|
func (usersMapper) FindSubs(tenantID types.TenantID, caller types.Uid, prefPrefix string, required [][]string, optional []string, activeOnly bool) ([]types.Subscription, error) {
|
|
if len(required) == 0 && len(optional) == 0 {
|
|
// No tags specified, return empty list.
|
|
return nil, nil
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFind(tenantID, caller.UserId(), prefPrefix, required, optional, activeOnly)
|
|
}
|
|
|
|
// Find returns topics and/or users which match the given tag, with optional partial matching.
|
|
func (usersMapper) FindOne(tenantID types.TenantID, tag string) (string, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return tenantAdp.TenantFindOne(tenantID, tag)
|
|
}
|
|
|
|
// GetTopics load a list of user's subscriptions with Public+Trusted fields copied to subscription
|
|
func (usersMapper) GetTopics(tenantID types.TenantID, id types.Uid, opts *types.QueryOpt) ([]types.Subscription, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantTopicsForUser(tenantID, id, false, opts)
|
|
}
|
|
|
|
// GetTopicsAny load a list of user's subscriptions with Public+Trusted fields copied to subscription.
|
|
// Deleted topics are returned too.
|
|
func (usersMapper) GetTopicsAny(tenantID types.TenantID, id types.Uid, opts *types.QueryOpt) ([]types.Subscription, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantTopicsForUser(tenantID, id, true, opts)
|
|
}
|
|
|
|
// GetOwnTopics returns a slice of group topic names where the user is the owner.
|
|
func (usersMapper) GetOwnTopics(tenantID types.TenantID, id types.Uid) ([]string, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantOwnTopics(tenantID, id)
|
|
}
|
|
|
|
// GetChannels returns a slice of group topic names where the user is a channel reader.
|
|
func (usersMapper) GetChannels(tenantID types.TenantID, id types.Uid) ([]string, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantChannelsForUser(tenantID, id)
|
|
}
|
|
|
|
// UpsertCred adds or updates a credential validation request. Return true if the record was inserted, false if updated.
|
|
func (usersMapper) UpsertCred(tenantID types.TenantID, cred *types.Credential) (bool, error) {
|
|
cred.InitTimes()
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return tenantAdp.TenantCredUpsert(tenantID, cred)
|
|
}
|
|
|
|
// ConfirmCred marks credential method as confirmed.
|
|
func (usersMapper) ConfirmCred(tenantID types.TenantID, id types.Uid, method string) error {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantCredConfirm(tenantID, id, method)
|
|
}
|
|
|
|
// FailCred increments fail count for the given credential method.
|
|
func (usersMapper) FailCred(tenantID types.TenantID, id types.Uid, method string) error {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantCredFail(tenantID, id, method)
|
|
}
|
|
|
|
// GetActiveCred gets a the currently active credential for the given user and method.
|
|
func (usersMapper) GetActiveCred(tenantID types.TenantID, id types.Uid, method string) (*types.Credential, error) {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantCredGetActive(tenantID, id, method)
|
|
}
|
|
|
|
// GetAllCreds returns credentials of the given user, all or validated only.
|
|
func (usersMapper) GetAllCreds(tenantID types.TenantID, id types.Uid, method string, validatedOnly bool) ([]types.Credential, error) {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantCredGetAll(tenantID, id, method, validatedOnly)
|
|
}
|
|
|
|
// DelCred deletes user's credentials. If method is "", all credentials are deleted.
|
|
func (usersMapper) DelCred(tenantID types.TenantID, id types.Uid, method, value string) error {
|
|
tenantAdp, err := tenantAuthAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantCredDel(tenantID, id, method, value)
|
|
}
|
|
|
|
// GetUnreadCount returs users' total count of unread messages in all topics with the R permissions.
|
|
func (usersMapper) GetUnreadCount(tenantID types.TenantID, ids ...types.Uid) (map[types.Uid]int, error) {
|
|
counts := make(map[types.Uid]int, len(ids))
|
|
misses := make([]types.Uid, 0, len(ids))
|
|
for _, uid := range ids {
|
|
if uid.IsZero() {
|
|
counts[uid] = 0
|
|
continue
|
|
}
|
|
var cached int
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceUnreadCount, uid.UserId(), &cached); err != nil {
|
|
logs.Warn.Printf("cache unread[%s]: read failed: %v", uid.UserId(), err)
|
|
misses = append(misses, uid)
|
|
} else if ok {
|
|
if cached < 0 {
|
|
cached = 0
|
|
}
|
|
counts[uid] = cached
|
|
} else {
|
|
misses = append(misses, uid)
|
|
}
|
|
}
|
|
if len(misses) == 0 {
|
|
return counts, nil
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbCounts, err := tenantAdp.TenantUserUnreadCount(tenantID, misses...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, uid := range misses {
|
|
count := dbCounts[uid]
|
|
if count < 0 {
|
|
count = 0
|
|
}
|
|
counts[uid] = count
|
|
CacheUserUnreadCount(tenantID, uid, count)
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
func CacheUserUnreadCount(tenantID types.TenantID, uid types.Uid, count int) {
|
|
if uid.IsZero() {
|
|
return
|
|
}
|
|
if count < 0 {
|
|
count = 0
|
|
}
|
|
if err := CacheObject(tenantID, cacheNamespaceUnreadCount, uid.UserId(), count); err != nil {
|
|
logs.Warn.Printf("cache unread[%s]: write failed: %v", uid.UserId(), err)
|
|
}
|
|
}
|
|
|
|
func DeleteCachedUserUnreadCount(tenantID types.TenantID, uid types.Uid) {
|
|
if uid.IsZero() {
|
|
return
|
|
}
|
|
if err := DeleteCachedObject(tenantID, cacheNamespaceUnreadCount, uid.UserId()); err != nil {
|
|
logs.Warn.Printf("cache unread[%s]: delete failed: %v", uid.UserId(), err)
|
|
}
|
|
}
|
|
|
|
// GetUnvalidated returns a list of stale user ids which have unvalidated credentials,
|
|
// their auth levels and a comma-separated list of these credential names.
|
|
func (usersMapper) GetUnvalidated(tenantID types.TenantID, lastUpdatedBefore time.Time, limit int) ([]types.Uid, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUserGetUnvalidated(tenantID, lastUpdatedBefore, limit)
|
|
}
|
|
|
|
// TopicsPersistenceInterface is an interface which defines methods for persistent storage of topics.
|
|
type TopicsPersistenceInterface interface {
|
|
Create(tenantID types.TenantID, topic *types.Topic, owner types.Uid, private any) error
|
|
CreateP2P(tenantID types.TenantID, initiator, invited *types.Subscription) error
|
|
Get(tenantID types.TenantID, topic string) (*types.Topic, error)
|
|
GetUsers(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error)
|
|
GetUsersAny(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error)
|
|
GetSubs(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error)
|
|
GetSubsAny(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error)
|
|
Update(tenantID types.TenantID, topic string, update map[string]any) error
|
|
UpdateSubCnt(tenantID types.TenantID, topic string) error
|
|
OwnerChange(tenantID types.TenantID, topic string, newOwner types.Uid) error
|
|
Delete(tenantID types.TenantID, topic string, isChan, hard bool) error
|
|
}
|
|
|
|
// topicsMapper is a concrete type implementing TopicsPersistenceInterface.
|
|
type topicsMapper struct{}
|
|
|
|
// Topics is a singleton ancor object exporting TopicsPersistenceInterface methods.
|
|
var Topics TopicsPersistenceInterface
|
|
|
|
// Create creates a topic and owner's subscription to it.
|
|
func (topicsMapper) Create(tenantID types.TenantID, topic *types.Topic, owner types.Uid, private any) error {
|
|
if topic == nil || topic.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
topic.InitTimes()
|
|
topic.TouchedAt = topic.CreatedAt
|
|
topic.Owner = owner.String()
|
|
|
|
err = tenantAdp.TenantTopicCreate(tenantID, topic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !owner.IsZero() {
|
|
err = Subs.Create(tenantID, &types.Subscription{
|
|
ObjHeader: types.ObjHeader{CreatedAt: topic.CreatedAt},
|
|
TenantID: tenantID,
|
|
User: owner.String(),
|
|
Topic: topic.Id,
|
|
ModeGiven: types.ModeCFull,
|
|
ModeWant: topic.GetAccess(owner),
|
|
Private: private})
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// CreateP2P creates a P2P topic by generating two user's subsciptions to each other.
|
|
func (topicsMapper) CreateP2P(tenantID types.TenantID, initiator, invited *types.Subscription) error {
|
|
if initiator == nil || invited == nil || initiator.TenantID != tenantID || invited.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
initiator.InitTimes()
|
|
initiator.SetTouchedAt(initiator.CreatedAt)
|
|
invited.InitTimes()
|
|
invited.SetTouchedAt(invited.CreatedAt)
|
|
|
|
return tenantAdp.TenantTopicCreateP2P(tenantID, initiator, invited)
|
|
}
|
|
|
|
// Get a single topic with a list of relevant users de-normalized into it
|
|
func (topicsMapper) Get(tenantID types.TenantID, topic string) (*types.Topic, error) {
|
|
var cached types.Topic
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceTopic, topic, &cached); err != nil {
|
|
logs.Warn.Printf("cache topic[%s]: read failed: %v", topic, err)
|
|
} else if ok {
|
|
if cached.TenantID == types.ZeroTenantID {
|
|
cached.TenantID = tenantID
|
|
}
|
|
if cached.TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
return &cached, nil
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := tenantAdp.TenantTopicGet(tenantID, topic)
|
|
if err == nil && result != nil && result.TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
if err == nil && result != nil {
|
|
if cacheErr := CacheObject(tenantID, cacheNamespaceTopic, topic, result); cacheErr != nil {
|
|
logs.Warn.Printf("cache topic[%s]: write failed: %v", topic, cacheErr)
|
|
}
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// GetUsers loads subscriptions for topic plus loads user.Public+Trusted.
|
|
// Deleted subscriptions are not loaded.
|
|
func (topicsMapper) GetUsers(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUsersForTopic(tenantID, topic, false, opts)
|
|
}
|
|
|
|
// GetUsersAny loads subscriptions for topic plus loads user.Public+Trusted. It's the same as GetUsers,
|
|
// except it loads deleted subscriptions too.
|
|
func (topicsMapper) GetUsersAny(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUsersForTopic(tenantID, topic, true, opts)
|
|
}
|
|
|
|
// GetSubs loads a list of subscriptions to the given topic, user.Public+Trusted and deleted
|
|
// subscriptions are not loaded. Suspended subscriptions are loaded.
|
|
func (topicsMapper) GetSubs(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantSubsForTopic(tenantID, topic, false, opts)
|
|
}
|
|
|
|
// GetSubsAny loads a list of subscriptions to the given topic including deleted subscription.
|
|
// user.Public/Trusted are not loaded
|
|
func (topicsMapper) GetSubsAny(tenantID types.TenantID, topic string, opts *types.QueryOpt) ([]types.Subscription, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantSubsForTopic(tenantID, topic, true, opts)
|
|
}
|
|
|
|
// UpdateSubCnt refreshes subscriber count value denormalized in topic.
|
|
func (topicsMapper) UpdateSubCnt(tenantID types.TenantID, topic string) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantTopicUpdateSubCnt(tenantID, topic)
|
|
}
|
|
|
|
// Update is a generic topic update.
|
|
func (topicsMapper) Update(tenantID types.TenantID, topic string, update map[string]any) error {
|
|
if _, ok := update["UpdatedAt"]; !ok {
|
|
update["UpdatedAt"] = types.TimeNow()
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantTopicUpdate(tenantID, topic, update); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceTopic, topic)
|
|
return nil
|
|
}
|
|
|
|
// OwnerChange replaces the old topic owner with the new owner.
|
|
func (topicsMapper) OwnerChange(tenantID types.TenantID, topic string, newOwner types.Uid) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantTopicOwnerChange(tenantID, topic, newOwner); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceTopic, topic)
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes topic, messages, attachments, and subscriptions.
|
|
func (topicsMapper) Delete(tenantID types.TenantID, topic string, isChan, hard bool) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantTopicDelete(tenantID, topic, isChan, hard); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceTopic, topic)
|
|
return nil
|
|
}
|
|
|
|
// SubsPersistenceInterface is an interface which defines methods for persistent storage of subscriptions.
|
|
type SubsPersistenceInterface interface {
|
|
Create(tenantID types.TenantID, subs ...*types.Subscription) error
|
|
Get(tenantID types.TenantID, topic string, user types.Uid, keepDeleted bool) (*types.Subscription, error)
|
|
Update(tenantID types.TenantID, topic string, user types.Uid, update map[string]any) error
|
|
Delete(tenantID types.TenantID, topic string, user types.Uid) error
|
|
}
|
|
|
|
// subsMapper is a concrete type implementing SubsPersistenceInterface.
|
|
type subsMapper struct{}
|
|
|
|
// Subs is a singleton ancor object exporting SubsPersistenceInterface.
|
|
var Subs SubsPersistenceInterface
|
|
|
|
// Create creates multiple subscriptions.
|
|
func (subsMapper) Create(tenantID types.TenantID, subs ...*types.Subscription) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(subs) == 0 {
|
|
// Nothing to do.
|
|
return nil
|
|
}
|
|
|
|
topic := subs[0].Topic
|
|
if types.IsEphemeralTopic(topic) {
|
|
// Ephemeral topics are not persisted in 'topics' table, don't try to update them.
|
|
// Mixing ephemeral and real topics is not permitted.
|
|
topic = ""
|
|
}
|
|
|
|
for _, sub := range subs {
|
|
if sub == nil || sub.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
sub.InitTimes()
|
|
if topic != "" && sub.Topic != topic {
|
|
return fmt.Errorf("all subscriptions must be for the same topic, got %s vs %s", sub.Topic, topic)
|
|
}
|
|
}
|
|
|
|
return tenantAdp.TenantTopicShare(tenantID, topic, subs)
|
|
}
|
|
|
|
// Get subscription given topic and user ID.
|
|
func (subsMapper) Get(tenantID types.TenantID, topic string, user types.Uid, keepDeleted bool) (*types.Subscription, error) {
|
|
cacheID := subscriptionCacheID(topic, user, keepDeleted)
|
|
var cached types.Subscription
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceSub, cacheID, &cached); err != nil {
|
|
logs.Warn.Printf("cache sub[%s]: read failed: %v", cacheID, err)
|
|
} else if ok {
|
|
if cached.TenantID == types.ZeroTenantID {
|
|
cached.TenantID = tenantID
|
|
}
|
|
if cached.TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
return &cached, nil
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := tenantAdp.TenantSubscriptionGet(tenantID, topic, user, keepDeleted)
|
|
if err == nil && result != nil && result.TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
if err == nil && result != nil {
|
|
if cacheErr := CacheObject(tenantID, cacheNamespaceSub, cacheID, result); cacheErr != nil {
|
|
logs.Warn.Printf("cache sub[%s]: write failed: %v", cacheID, cacheErr)
|
|
}
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// Update values of topic's subscriptions.
|
|
func (subsMapper) Update(tenantID types.TenantID, topic string, user types.Uid, update map[string]any) error {
|
|
update["UpdatedAt"] = types.TimeNow()
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantSubsUpdate(tenantID, topic, user, update); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(topic, user, false))
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(topic, user, true))
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes a subscription.
|
|
// To delete channel subscription the channel name must be explicitly specified.
|
|
func (subsMapper) Delete(tenantID types.TenantID, topic string, user types.Uid) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantSubsDelete(tenantID, topic, user); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(topic, user, false))
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(topic, user, true))
|
|
return nil
|
|
}
|
|
|
|
func subscriptionCacheID(topic string, user types.Uid, keepDeleted bool) string {
|
|
if keepDeleted {
|
|
return topic + ":" + user.String() + ":any"
|
|
}
|
|
return topic + ":" + user.String() + ":active"
|
|
}
|
|
|
|
// MessagesPersistenceInterface is an interface which defines methods for persistent storage of messages.
|
|
type MessagesPersistenceInterface interface {
|
|
Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error)
|
|
PreparePersist(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error)
|
|
PersistPrepared(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error)
|
|
GetOne(tenantID types.TenantID, topic string, seq int64) (*types.Message, error)
|
|
Recall(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error)
|
|
Edit(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, head types.KVMap, content any, reason string, editedAt time.Time) (*types.Message, error)
|
|
DeleteList(tenantID types.TenantID, topic string, delID int, forUser types.Uid, msgDelAge time.Duration, ranges []types.Range) error
|
|
GetAll(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Message, error)
|
|
GetDeleted(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Range, int, error)
|
|
Search(tenantID types.TenantID, topic string, forUser types.Uid, query, messageType, cursor string, limit int) ([]types.Message, string, error)
|
|
}
|
|
|
|
// messagesMapper is a concrete type implementing MessagesPersistenceInterface.
|
|
type messagesMapper struct{}
|
|
|
|
// Messages is a singleton ancor object for exporting MessagesPersistenceInterface.
|
|
var Messages MessagesPersistenceInterface
|
|
|
|
func (messagesMapper) GetOne(tenantID types.TenantID, topic string, seq int64) (*types.Message, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if types.GetTopicCat(topic) == types.TopicCatGrp {
|
|
return tenantAdp.TenantGroupMessageGetOne(tenantID, topic, seq)
|
|
}
|
|
return tenantAdp.TenantMessageGetOne(tenantID, topic, seq)
|
|
}
|
|
|
|
func (messagesMapper) Recall(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if types.GetTopicCat(topic) == types.TopicCatGrp {
|
|
msg, err := tenantAdp.TenantGroupMessageRecall(tenantID, topic, seq, actor, expectedVersion, recalledAt)
|
|
if err == nil && groupMsgRuntime != nil {
|
|
if cacheErr := groupMsgRuntime.CacheHotMessage(tenantID, msg); cacheErr != nil {
|
|
logs.Warn.Printf("topic[%s]: failed to refresh recalled group message cache: %v", topic, cacheErr)
|
|
}
|
|
}
|
|
return msg, err
|
|
}
|
|
return tenantAdp.TenantMessageRecall(tenantID, topic, seq, actor, expectedVersion, recalledAt)
|
|
}
|
|
|
|
func (messagesMapper) Edit(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, head types.KVMap, content any, reason string, editedAt time.Time) (*types.Message, error) {
|
|
if err := validateMessageHeaders(head); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if types.GetTopicCat(topic) == types.TopicCatGrp {
|
|
msg, err := tenantAdp.TenantGroupMessageEdit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
|
if err == nil && groupMsgRuntime != nil {
|
|
if cacheErr := groupMsgRuntime.CacheHotMessage(tenantID, msg); cacheErr != nil {
|
|
logs.Warn.Printf("topic[%s]: failed to refresh edited group message cache: %v", topic, cacheErr)
|
|
}
|
|
}
|
|
return msg, err
|
|
}
|
|
return tenantAdp.TenantMessageEdit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
|
}
|
|
|
|
type ConversationsPersistenceInterface interface {
|
|
EnsureP2P(tenantID types.TenantID, uid, peer types.Uid) (string, error)
|
|
GetSettings(tenantID types.TenantID, uid types.Uid, topic string) (*types.ConversationSettings, error)
|
|
UpdateSettings(tenantID types.TenantID, uid types.Uid, topic string, patch types.ConversationSettingsPatch) (*types.ConversationSettings, error)
|
|
GetDraft(tenantID types.TenantID, uid types.Uid, topic string) (*types.TopicDraft, error)
|
|
PutDraft(tenantID types.TenantID, uid types.Uid, topic, content string, entities []byte, version int) (*types.TopicDraft, error)
|
|
}
|
|
|
|
type conversationsMapper struct{}
|
|
|
|
var Conversations ConversationsPersistenceInterface
|
|
|
|
func (conversationsMapper) EnsureP2P(tenantID types.TenantID, uid, peer types.Uid) (string, error) {
|
|
if uid.IsZero() || peer.IsZero() || uid == peer {
|
|
return "", types.ErrMalformed
|
|
}
|
|
canSend, _, err := Relations.CanSendP2P(tenantID, uid, peer)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !canSend {
|
|
return "", types.ErrPermissionDenied
|
|
}
|
|
topic := uid.P2PName(peer)
|
|
if topic == "" {
|
|
return "", types.ErrMalformed
|
|
}
|
|
now := types.TimeNow()
|
|
initiator := &types.Subscription{
|
|
ObjHeader: types.ObjHeader{CreatedAt: now, UpdatedAt: now},
|
|
TenantID: tenantID,
|
|
User: uid.String(),
|
|
Topic: topic,
|
|
ModeWant: types.ModeCP2P,
|
|
ModeGiven: types.ModeCP2P,
|
|
Private: map[string]any{"peer": peer.UserId(), "created_by": "http"},
|
|
}
|
|
invited := &types.Subscription{
|
|
ObjHeader: types.ObjHeader{CreatedAt: now, UpdatedAt: now},
|
|
TenantID: tenantID,
|
|
User: peer.String(),
|
|
Topic: topic,
|
|
ModeWant: types.ModeCP2P,
|
|
ModeGiven: types.ModeCP2P,
|
|
Private: map[string]any{"peer": uid.UserId(), "created_by": "http"},
|
|
}
|
|
if err = Topics.CreateP2P(tenantID, initiator, invited); err != nil {
|
|
return "", err
|
|
}
|
|
return topic, nil
|
|
}
|
|
|
|
func (conversationsMapper) GetSettings(tenantID types.TenantID, uid types.Uid, topic string) (*types.ConversationSettings, error) {
|
|
if uid.IsZero() || strings.TrimSpace(topic) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
if _, err := Subs.Get(tenantID, strings.TrimSpace(topic), uid, false); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantConversationSettingsGet(tenantID, uid, strings.TrimSpace(topic))
|
|
}
|
|
|
|
func (conversationsMapper) UpdateSettings(tenantID types.TenantID, uid types.Uid, topic string, patch types.ConversationSettingsPatch) (*types.ConversationSettings, error) {
|
|
if uid.IsZero() || strings.TrimSpace(topic) == "" || patch.Version <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
if _, err := Subs.Get(tenantID, strings.TrimSpace(topic), uid, false); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantConversationSettingsUpdate(tenantID, uid, strings.TrimSpace(topic), patch)
|
|
}
|
|
|
|
func (conversationsMapper) GetDraft(tenantID types.TenantID, uid types.Uid, topic string) (*types.TopicDraft, error) {
|
|
if uid.IsZero() || strings.TrimSpace(topic) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
if _, err := Subs.Get(tenantID, strings.TrimSpace(topic), uid, false); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantTopicDraftGet(tenantID, uid, strings.TrimSpace(topic))
|
|
}
|
|
|
|
func (conversationsMapper) PutDraft(tenantID types.TenantID, uid types.Uid, topic, content string, entities []byte, version int) (*types.TopicDraft, error) {
|
|
if uid.IsZero() || strings.TrimSpace(topic) == "" || version < 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
if _, err := Subs.Get(tenantID, strings.TrimSpace(topic), uid, false); err != nil {
|
|
return nil, err
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantTopicDraftPut(tenantID, uid, strings.TrimSpace(topic), content, entities, version)
|
|
}
|
|
|
|
type CallsPersistenceInterface interface {
|
|
Create(tenantID types.TenantID, record *types.CallRecord) error
|
|
Accept(tenantID types.TenantID, callID string, seq int64, acceptedAt time.Time) error
|
|
Finish(tenantID types.TenantID, callID string, seq int64, status string, durationMS int64, finishedAt time.Time) error
|
|
List(tenantID types.TenantID, uid types.Uid, topic, cursor string, limit int) ([]types.CallRecord, string, error)
|
|
}
|
|
|
|
type callsMapper struct{}
|
|
|
|
var Calls CallsPersistenceInterface
|
|
|
|
func (callsMapper) Create(tenantID types.TenantID, record *types.CallRecord) error {
|
|
if record == nil || record.TenantID != tenantID || strings.TrimSpace(record.CallID) == "" ||
|
|
record.Topic == "" || record.SeqID <= 0 || record.CallerID.IsZero() || record.CalleeID.IsZero() ||
|
|
record.CallType == "" {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantCallCreate(tenantID, record)
|
|
}
|
|
|
|
func (callsMapper) Accept(tenantID types.TenantID, callID string, seq int64, acceptedAt time.Time) error {
|
|
if strings.TrimSpace(callID) == "" || seq <= 0 || acceptedAt.IsZero() {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantCallAccept(tenantID, callID, seq, acceptedAt)
|
|
}
|
|
|
|
func (callsMapper) Finish(tenantID types.TenantID, callID string, seq int64, status string, durationMS int64, finishedAt time.Time) error {
|
|
switch status {
|
|
case types.CallStatusFinished, types.CallStatusMissed, types.CallStatusDeclined, types.CallStatusDisconnected:
|
|
default:
|
|
return types.ErrMalformed
|
|
}
|
|
if strings.TrimSpace(callID) == "" || seq <= 0 || finishedAt.IsZero() || durationMS < 0 {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantCallFinish(tenantID, callID, seq, status, durationMS, finishedAt)
|
|
}
|
|
|
|
func (callsMapper) List(tenantID types.TenantID, uid types.Uid, topic, cursor string, limit int) ([]types.CallRecord, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantCallList(tenantID, uid, strings.TrimSpace(topic), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
type GroupBusinessPersistenceInterface interface {
|
|
CreateGroup(types.TenantID, *types.Topic, types.Uid, any, *types.GroupSettings, []types.Uid, any, time.Time) error
|
|
CreateSettings(types.TenantID, *types.GroupSettings) (*types.GroupSettings, error)
|
|
GetSettings(types.TenantID, string) (*types.GroupSettings, error)
|
|
UpdateSettings(types.TenantID, string, types.GroupSettingsPatch) (*types.GroupSettings, error)
|
|
UpsertMember(types.TenantID, *types.GroupMember) (*types.GroupMember, error)
|
|
GetMember(types.TenantID, string, types.Uid) (*types.GroupMember, error)
|
|
ListMembers(types.TenantID, string, types.GroupMemberListOptions) ([]types.GroupMember, string, error)
|
|
UpdateMember(types.TenantID, string, types.Uid, types.GroupMemberPatch) (*types.GroupMember, error)
|
|
CreateInvitation(types.TenantID, *types.GroupInvitation) (*types.GroupInvitation, error)
|
|
AcceptInvitation(types.TenantID, string, types.Uid, any, time.Time) (*types.GroupInvitation, error)
|
|
CreateJoinRequest(types.TenantID, *types.GroupJoinRequest) (*types.GroupJoinRequest, error)
|
|
ListJoinRequests(types.TenantID, string, string, string, int) ([]types.GroupJoinRequest, string, error)
|
|
ResolveJoinRequest(types.TenantID, string, string, types.Uid, string, string, int, any, time.Time) (*types.GroupJoinRequest, error)
|
|
LatestAnnouncement(types.TenantID, string) (*types.GroupAnnouncement, error)
|
|
PutAnnouncement(types.TenantID, *types.GroupAnnouncement, int) (*types.GroupAnnouncement, error)
|
|
ReadAnnouncement(types.TenantID, string, string, types.Uid, time.Time) error
|
|
ListMutes(types.TenantID, string, string, int) ([]types.GroupMute, string, error)
|
|
UpsertMute(types.TenantID, *types.GroupMute) (*types.GroupMute, error)
|
|
DeleteMute(types.TenantID, string, types.Uid, types.Uid, time.Time) error
|
|
ListFiles(types.TenantID, string, string, string, string, int) ([]types.GroupFile, string, error)
|
|
CreateFile(types.TenantID, *types.GroupFile) (*types.GroupFile, error)
|
|
DeleteFile(types.TenantID, string, string, types.Uid, bool, time.Time) (*types.GroupFile, error)
|
|
InviteMember(types.TenantID, string, types.Uid, types.Uid, string, string, any, time.Time) (bool, error)
|
|
Leave(types.TenantID, string, types.Uid, time.Time) error
|
|
UpdateRole(types.TenantID, string, types.Uid, types.Uid, string, types.AccessMode, time.Time) error
|
|
TransferOwner(types.TenantID, string, types.Uid, types.Uid, int, time.Time) error
|
|
CanPost(types.TenantID, string, types.Uid, bool, time.Time) (bool, error)
|
|
ListPendingEvents(types.TenantID, int, time.Time) ([]types.GroupEvent, error)
|
|
MarkEventDelivered(types.TenantID, string, time.Time) error
|
|
}
|
|
|
|
type groupBusinessMapper struct{}
|
|
|
|
var GroupBusiness GroupBusinessPersistenceInterface
|
|
|
|
func (groupBusinessMapper) CreateGroup(tenantID types.TenantID, topic *types.Topic, owner types.Uid, ownerPrivate any,
|
|
settings *types.GroupSettings, members []types.Uid, memberPrivate any, now time.Time) error {
|
|
if topic == nil || topic.TenantID != tenantID || owner.IsZero() || settings == nil || settings.Topic != topic.Id {
|
|
return types.ErrMalformed
|
|
}
|
|
topic.InitTimes()
|
|
topic.TouchedAt = topic.CreatedAt
|
|
topic.Owner = owner.String()
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantGroupCreate(tenantID, topic, owner, ownerPrivate, settings, members, memberPrivate, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) CreateSettings(tenantID types.TenantID, settings *types.GroupSettings) (*types.GroupSettings, error) {
|
|
if settings == nil || settings.Topic == "" || settings.OwnerUserID.IsZero() || settings.CreatedBy.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupSettingsCreate(tenantID, settings)
|
|
}
|
|
|
|
func (groupBusinessMapper) GetSettings(tenantID types.TenantID, topic string) (*types.GroupSettings, error) {
|
|
if strings.TrimSpace(topic) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
topic = strings.TrimSpace(topic)
|
|
var cached types.GroupSettings
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceGroupSettings, topic, &cached); err != nil {
|
|
logs.Warn.Printf("cache group settings[%s]: read failed: %v", topic, err)
|
|
} else if ok {
|
|
return &cached, nil
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
settings, err := tenantAdp.TenantGroupSettingsGet(tenantID, topic)
|
|
if err == nil && settings != nil {
|
|
if cacheErr := CacheObject(tenantID, cacheNamespaceGroupSettings, topic, settings); cacheErr != nil {
|
|
logs.Warn.Printf("cache group settings[%s]: write failed: %v", topic, cacheErr)
|
|
}
|
|
}
|
|
return settings, err
|
|
}
|
|
|
|
func (groupBusinessMapper) UpdateSettings(tenantID types.TenantID, topic string, patch types.GroupSettingsPatch) (*types.GroupSettings, error) {
|
|
if strings.TrimSpace(topic) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
settings, err := tenantAdp.TenantGroupSettingsUpdate(tenantID, strings.TrimSpace(topic), patch)
|
|
if err == nil {
|
|
if settings != nil {
|
|
_ = CacheObject(tenantID, cacheNamespaceGroupSettings, strings.TrimSpace(topic), settings)
|
|
} else {
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupSettings, strings.TrimSpace(topic))
|
|
}
|
|
}
|
|
return settings, err
|
|
}
|
|
|
|
func (groupBusinessMapper) UpsertMember(tenantID types.TenantID, member *types.GroupMember) (*types.GroupMember, error) {
|
|
if member == nil || member.Topic == "" || member.UserID.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
member, err = tenantAdp.TenantGroupMemberUpsert(tenantID, member)
|
|
if err == nil && member != nil {
|
|
_ = CacheObject(tenantID, cacheNamespaceGroupMember, groupMemberCacheID(member.Topic, member.UserID), member)
|
|
}
|
|
return member, err
|
|
}
|
|
|
|
func (groupBusinessMapper) GetMember(tenantID types.TenantID, topic string, uid types.Uid) (*types.GroupMember, error) {
|
|
if strings.TrimSpace(topic) == "" || uid.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
topic = strings.TrimSpace(topic)
|
|
cacheID := groupMemberCacheID(topic, uid)
|
|
var cached types.GroupMember
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceGroupMember, cacheID, &cached); err != nil {
|
|
logs.Warn.Printf("cache group member[%s]: read failed: %v", cacheID, err)
|
|
} else if ok {
|
|
return &cached, nil
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
member, err := tenantAdp.TenantGroupMemberGet(tenantID, topic, uid)
|
|
if err == nil && member != nil {
|
|
if cacheErr := CacheObject(tenantID, cacheNamespaceGroupMember, cacheID, member); cacheErr != nil {
|
|
logs.Warn.Printf("cache group member[%s]: write failed: %v", cacheID, cacheErr)
|
|
}
|
|
}
|
|
return member, err
|
|
}
|
|
|
|
func (groupBusinessMapper) ListMembers(tenantID types.TenantID, topic string, opts types.GroupMemberListOptions) ([]types.GroupMember, string, error) {
|
|
if strings.TrimSpace(topic) == "" {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if opts.Limit <= 0 || opts.Limit > 100 {
|
|
opts.Limit = 30
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantGroupMemberList(tenantID, strings.TrimSpace(topic), opts)
|
|
}
|
|
|
|
func CacheGroupMemberPage(tenantID types.TenantID, key string, payload map[string]any) {
|
|
if strings.TrimSpace(key) == "" || payload == nil {
|
|
return
|
|
}
|
|
if err := CacheObject(tenantID, cacheNamespaceGroupPage, strings.TrimSpace(key), payload); err != nil {
|
|
logs.Warn.Printf("cache group member page[%s]: write failed: %v", key, err)
|
|
}
|
|
}
|
|
|
|
func GetCachedGroupMemberPage(tenantID types.TenantID, key string) (map[string]any, bool) {
|
|
if strings.TrimSpace(key) == "" {
|
|
return nil, false
|
|
}
|
|
var payload map[string]any
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceGroupPage, strings.TrimSpace(key), &payload); err != nil {
|
|
logs.Warn.Printf("cache group member page[%s]: read failed: %v", key, err)
|
|
} else if ok {
|
|
return payload, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (groupBusinessMapper) UpdateMember(tenantID types.TenantID, topic string, uid types.Uid, patch types.GroupMemberPatch) (*types.GroupMember, error) {
|
|
if strings.TrimSpace(topic) == "" || uid.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
member, err := tenantAdp.TenantGroupMemberUpdate(tenantID, strings.TrimSpace(topic), uid, patch)
|
|
if err == nil {
|
|
if member != nil {
|
|
_ = CacheObject(tenantID, cacheNamespaceGroupMember, groupMemberCacheID(strings.TrimSpace(topic), uid), member)
|
|
} else {
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupMember, groupMemberCacheID(strings.TrimSpace(topic), uid))
|
|
}
|
|
}
|
|
return member, err
|
|
}
|
|
|
|
func (groupBusinessMapper) CreateInvitation(tenantID types.TenantID, invitation *types.GroupInvitation) (*types.GroupInvitation, error) {
|
|
if invitation == nil || invitation.Topic == "" || invitation.InviterUserID.IsZero() || invitation.TargetUserID.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupInvitationCreate(tenantID, invitation)
|
|
}
|
|
|
|
func (groupBusinessMapper) AcceptInvitation(tenantID types.TenantID, invitationID string, uid types.Uid, memberPrivate any, now time.Time) (*types.GroupInvitation, error) {
|
|
if strings.TrimSpace(invitationID) == "" || uid.IsZero() || now.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupInvitationAccept(tenantID, strings.TrimSpace(invitationID), uid, memberPrivate, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) CreateJoinRequest(tenantID types.TenantID, req *types.GroupJoinRequest) (*types.GroupJoinRequest, error) {
|
|
if req == nil || req.Topic == "" || req.ApplicantUserID.IsZero() || req.RequestID == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupJoinRequestCreate(tenantID, req)
|
|
}
|
|
|
|
func (groupBusinessMapper) ListJoinRequests(tenantID types.TenantID, topic, status, cursor string, limit int) ([]types.GroupJoinRequest, string, error) {
|
|
if strings.TrimSpace(topic) == "" {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantGroupJoinRequestList(tenantID, strings.TrimSpace(topic), strings.TrimSpace(status), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (groupBusinessMapper) ResolveJoinRequest(tenantID types.TenantID, topic, requestID string, actor types.Uid, action, rejectReason string, version int, memberPrivate any, now time.Time) (*types.GroupJoinRequest, error) {
|
|
if strings.TrimSpace(topic) == "" || strings.TrimSpace(requestID) == "" || actor.IsZero() || now.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupJoinRequestResolve(tenantID, strings.TrimSpace(topic), strings.TrimSpace(requestID), actor, strings.TrimSpace(action), strings.TrimSpace(rejectReason), version, memberPrivate, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) LatestAnnouncement(tenantID types.TenantID, topic string) (*types.GroupAnnouncement, error) {
|
|
if strings.TrimSpace(topic) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupAnnouncementLatest(tenantID, strings.TrimSpace(topic))
|
|
}
|
|
|
|
func (groupBusinessMapper) PutAnnouncement(tenantID types.TenantID, ann *types.GroupAnnouncement, expectedVersion int) (*types.GroupAnnouncement, error) {
|
|
if ann == nil || ann.Topic == "" || ann.AuthorUserID.IsZero() || ann.Content == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupAnnouncementPut(tenantID, ann, expectedVersion)
|
|
}
|
|
|
|
func (groupBusinessMapper) ReadAnnouncement(tenantID types.TenantID, topic, announcementID string, uid types.Uid, readAt time.Time) error {
|
|
if strings.TrimSpace(topic) == "" || strings.TrimSpace(announcementID) == "" || uid.IsZero() || readAt.IsZero() {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantGroupAnnouncementRead(tenantID, strings.TrimSpace(topic), strings.TrimSpace(announcementID), uid, readAt)
|
|
}
|
|
|
|
func (groupBusinessMapper) ListMutes(tenantID types.TenantID, topic, cursor string, limit int) ([]types.GroupMute, string, error) {
|
|
if strings.TrimSpace(topic) == "" {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantGroupMuteList(tenantID, strings.TrimSpace(topic), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (groupBusinessMapper) UpsertMute(tenantID types.TenantID, mute *types.GroupMute) (*types.GroupMute, error) {
|
|
if mute == nil || mute.Topic == "" || mute.UserID.IsZero() || mute.MutedBy.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupMuteUpsert(tenantID, mute)
|
|
}
|
|
|
|
func (groupBusinessMapper) DeleteMute(tenantID types.TenantID, topic string, uid, actor types.Uid, now time.Time) error {
|
|
if strings.TrimSpace(topic) == "" || uid.IsZero() || actor.IsZero() || now.IsZero() {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantGroupMuteDelete(tenantID, strings.TrimSpace(topic), uid, actor, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) ListFiles(tenantID types.TenantID, topic, fileType, query, cursor string, limit int) ([]types.GroupFile, string, error) {
|
|
if strings.TrimSpace(topic) == "" {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantGroupFileList(tenantID, strings.TrimSpace(topic), strings.TrimSpace(fileType), strings.TrimSpace(query), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (groupBusinessMapper) CreateFile(tenantID types.TenantID, file *types.GroupFile) (*types.GroupFile, error) {
|
|
if file == nil || strings.TrimSpace(file.Topic) == "" || strings.TrimSpace(file.FileID) == "" || file.UploaderID.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupFileCreate(tenantID, file)
|
|
}
|
|
|
|
func (groupBusinessMapper) DeleteFile(tenantID types.TenantID, topic, groupFileID string, actor types.Uid, canManage bool, now time.Time) (*types.GroupFile, error) {
|
|
if strings.TrimSpace(topic) == "" || strings.TrimSpace(groupFileID) == "" || actor.IsZero() || now.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupFileDelete(tenantID, strings.TrimSpace(topic), strings.TrimSpace(groupFileID), actor, canManage, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) InviteMember(tenantID types.TenantID, topic string, inviter, target types.Uid,
|
|
message, idempotencyKey string, private any, now time.Time) (bool, error) {
|
|
if strings.TrimSpace(topic) == "" || inviter.IsZero() || target.IsZero() || now.IsZero() {
|
|
return false, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return tenantAdp.TenantGroupInviteMember(tenantID, strings.TrimSpace(topic), inviter, target,
|
|
strings.TrimSpace(message), strings.TrimSpace(idempotencyKey), private, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) Leave(tenantID types.TenantID, topic string, uid types.Uid, now time.Time) error {
|
|
if strings.TrimSpace(topic) == "" || uid.IsZero() || now.IsZero() {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantGroupLeave(tenantID, strings.TrimSpace(topic), uid, now); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupMember, groupMemberCacheID(strings.TrimSpace(topic), uid))
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(strings.TrimSpace(topic), uid, false))
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(strings.TrimSpace(topic), uid, true))
|
|
return nil
|
|
}
|
|
|
|
func (groupBusinessMapper) UpdateRole(tenantID types.TenantID, topic string, actor, target types.Uid,
|
|
role string, modeGiven types.AccessMode, now time.Time) error {
|
|
if strings.TrimSpace(topic) == "" || actor.IsZero() || target.IsZero() || now.IsZero() {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantGroupRoleUpdate(tenantID, strings.TrimSpace(topic), actor, target, strings.TrimSpace(role), modeGiven, now); err != nil {
|
|
return err
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupMember, groupMemberCacheID(strings.TrimSpace(topic), target))
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(strings.TrimSpace(topic), target, false))
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(strings.TrimSpace(topic), target, true))
|
|
return nil
|
|
}
|
|
|
|
func (groupBusinessMapper) TransferOwner(tenantID types.TenantID, topic string, previousOwner, newOwner types.Uid,
|
|
version int, now time.Time) error {
|
|
if strings.TrimSpace(topic) == "" || previousOwner.IsZero() || newOwner.IsZero() || now.IsZero() {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantGroupOwnerTransfer(tenantID, strings.TrimSpace(topic), previousOwner, newOwner, version, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) CanPost(tenantID types.TenantID, topic string, uid types.Uid, admin bool, now time.Time) (bool, error) {
|
|
if strings.TrimSpace(topic) == "" || uid.IsZero() || now.IsZero() {
|
|
return false, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return tenantAdp.TenantGroupCanPost(tenantID, strings.TrimSpace(topic), uid, admin, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) ListPendingEvents(tenantID types.TenantID, limit int, now time.Time) ([]types.GroupEvent, error) {
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantGroupEventListPending(tenantID, limit, now)
|
|
}
|
|
|
|
func (groupBusinessMapper) MarkEventDelivered(tenantID types.TenantID, eventID string, now time.Time) error {
|
|
if strings.TrimSpace(eventID) == "" || now.IsZero() {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantGroupEventMarkDelivered(tenantID, strings.TrimSpace(eventID), now)
|
|
}
|
|
|
|
func groupMemberCacheID(topic string, uid types.Uid) string {
|
|
return strings.TrimSpace(topic) + ":" + uid.String()
|
|
}
|
|
|
|
type WorkbenchPersistenceInterface interface {
|
|
Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error)
|
|
}
|
|
|
|
type workbenchMapper struct{}
|
|
|
|
var Workbench WorkbenchPersistenceInterface
|
|
|
|
func (workbenchMapper) Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error) {
|
|
if uid.IsZero() {
|
|
return nil, 0, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return tenantAdp.TenantWorkbenchModules(tenantID, uid)
|
|
}
|
|
|
|
type OrganizationPersistenceInterface interface {
|
|
ListDepartments(tenantID types.TenantID, uid types.Uid, parentID, cursor string, limit int) ([]types.OrganizationDepartment, string, error)
|
|
ListMembers(tenantID types.TenantID, uid types.Uid, departmentID, cursor string, limit int) ([]types.OrganizationMember, string, error)
|
|
ListAllMembers(tenantID types.TenantID, uid types.Uid, departmentID, queryText, cursor string, limit int) ([]types.OrganizationMember, string, error)
|
|
Search(tenantID types.TenantID, uid types.Uid, queryText, resultTypes, cursor string, limit int) ([]types.OrganizationSearchItem, string, error)
|
|
}
|
|
|
|
type organizationMapper struct{}
|
|
|
|
var Organization OrganizationPersistenceInterface
|
|
|
|
func (organizationMapper) ListDepartments(tenantID types.TenantID, uid types.Uid, parentID, cursor string, limit int) ([]types.OrganizationDepartment, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantOrganizationDepartments(tenantID, uid, strings.TrimSpace(parentID), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (organizationMapper) ListMembers(tenantID types.TenantID, uid types.Uid, departmentID, cursor string, limit int) ([]types.OrganizationMember, string, error) {
|
|
if uid.IsZero() || strings.TrimSpace(departmentID) == "" {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantOrganizationMembers(tenantID, uid, strings.TrimSpace(departmentID), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (organizationMapper) ListAllMembers(tenantID types.TenantID, uid types.Uid, departmentID, queryText, cursor string, limit int) ([]types.OrganizationMember, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantOrganizationMembersAll(tenantID, uid, strings.TrimSpace(departmentID), strings.TrimSpace(queryText), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (organizationMapper) Search(tenantID types.TenantID, uid types.Uid, queryText, resultTypes, cursor string, limit int) ([]types.OrganizationSearchItem, string, error) {
|
|
if uid.IsZero() || strings.TrimSpace(queryText) == "" {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantOrganizationSearch(tenantID, uid, strings.TrimSpace(queryText), strings.TrimSpace(resultTypes), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
type MeetingsPersistenceInterface interface {
|
|
Create(tenantID types.TenantID, uid types.Uid, meeting *types.Meeting, attendeeIDs []types.Uid) (*types.Meeting, error)
|
|
List(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.Meeting, string, error)
|
|
Get(tenantID types.TenantID, uid types.Uid, meetingID string) (*types.Meeting, error)
|
|
Update(tenantID types.TenantID, uid types.Uid, meetingID string, patch types.MeetingPatch, idempotencyKey string) (*types.Meeting, error)
|
|
Cancel(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) (*types.Meeting, error)
|
|
Remind(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) ([]types.MeetingParticipant, error)
|
|
Respond(tenantID types.TenantID, uid types.Uid, meetingID, response string, version int) (*types.Meeting, error)
|
|
}
|
|
|
|
type meetingsMapper struct{}
|
|
|
|
var Meetings MeetingsPersistenceInterface
|
|
|
|
func (meetingsMapper) Create(tenantID types.TenantID, uid types.Uid, meeting *types.Meeting, attendeeIDs []types.Uid) (*types.Meeting, error) {
|
|
if uid.IsZero() || meeting == nil || !validMeetingForCreate(meeting) {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMeetingCreate(tenantID, uid, meeting, attendeeIDs)
|
|
}
|
|
|
|
func (meetingsMapper) List(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.Meeting, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 20
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantMeetingList(tenantID, uid, strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (meetingsMapper) Get(tenantID types.TenantID, uid types.Uid, meetingID string) (*types.Meeting, error) {
|
|
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMeetingGet(tenantID, uid, strings.TrimSpace(meetingID))
|
|
}
|
|
|
|
func (meetingsMapper) Update(tenantID types.TenantID, uid types.Uid, meetingID string, patch types.MeetingPatch, idempotencyKey string) (*types.Meeting, error) {
|
|
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || patch.Version <= 0 || !validMeetingPatch(patch) {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMeetingUpdate(tenantID, uid, strings.TrimSpace(meetingID), patch, strings.TrimSpace(idempotencyKey))
|
|
}
|
|
|
|
func (meetingsMapper) Cancel(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) (*types.Meeting, error) {
|
|
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMeetingCancel(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(idempotencyKey))
|
|
}
|
|
|
|
func (meetingsMapper) Remind(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) ([]types.MeetingParticipant, error) {
|
|
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMeetingRemind(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(idempotencyKey))
|
|
}
|
|
|
|
func (meetingsMapper) Respond(tenantID types.TenantID, uid types.Uid, meetingID, response string, version int) (*types.Meeting, error) {
|
|
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || !validMeetingResponse(response) || version <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMeetingRespond(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(response), version)
|
|
}
|
|
|
|
func validMeetingForCreate(meeting *types.Meeting) bool {
|
|
return strings.TrimSpace(meeting.MeetingID) != "" &&
|
|
strings.TrimSpace(meeting.Title) != "" &&
|
|
!meeting.StartAt.IsZero() &&
|
|
!meeting.EndAt.IsZero() &&
|
|
meeting.EndAt.After(meeting.StartAt)
|
|
}
|
|
|
|
func validMeetingPatch(patch types.MeetingPatch) bool {
|
|
if patch.Title == nil && patch.StartAt == nil && patch.EndAt == nil && patch.Location == nil && patch.ReminderMinutes == nil {
|
|
return false
|
|
}
|
|
if patch.Title != nil && strings.TrimSpace(*patch.Title) == "" {
|
|
return false
|
|
}
|
|
if patch.StartAt != nil && patch.EndAt != nil && !patch.EndAt.After(*patch.StartAt) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validMeetingResponse(response string) bool {
|
|
switch strings.TrimSpace(response) {
|
|
case types.MeetingResponseAccepted, types.MeetingResponseDeclined, types.MeetingResponseTentative:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type DailyReportsPersistenceInterface interface {
|
|
List(tenantID types.TenantID, uid types.Uid, scope, from, to, status, cursor string, limit int) ([]types.DailyReport, string, error)
|
|
CreateDraft(tenantID types.TenantID, uid types.Uid, reportID string, reportDate time.Time) (*types.DailyReport, error)
|
|
Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.DailyReport, error)
|
|
SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, content string, attachments []string, version int) (*types.DailyReport, error)
|
|
Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.DailyReport, error)
|
|
Stats(tenantID types.TenantID, uid types.Uid, date time.Time, departmentID string) (*types.DailyReportStats, error)
|
|
Remind(tenantID types.TenantID, uid types.Uid, date time.Time, userIDs []types.Uid, idempotencyKey string) ([]types.Uid, error)
|
|
}
|
|
|
|
type dailyReportsMapper struct{}
|
|
|
|
var DailyReports DailyReportsPersistenceInterface
|
|
|
|
func (dailyReportsMapper) List(tenantID types.TenantID, uid types.Uid, scope, from, to, status, cursor string, limit int) ([]types.DailyReport, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantDailyReportList(tenantID, uid, strings.TrimSpace(scope), strings.TrimSpace(from), strings.TrimSpace(to), strings.TrimSpace(status), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (dailyReportsMapper) CreateDraft(tenantID types.TenantID, uid types.Uid, reportID string, reportDate time.Time) (*types.DailyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" || reportDate.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantDailyReportCreateDraft(tenantID, uid, strings.TrimSpace(reportID), reportDate)
|
|
}
|
|
|
|
func (dailyReportsMapper) Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.DailyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantDailyReportGet(tenantID, uid, strings.TrimSpace(reportID))
|
|
}
|
|
|
|
func (dailyReportsMapper) SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, content string, attachments []string, version int) (*types.DailyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantDailyReportSaveDraft(tenantID, uid, strings.TrimSpace(reportID), content, attachments, version)
|
|
}
|
|
|
|
func (dailyReportsMapper) Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.DailyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantDailyReportSubmit(tenantID, uid, strings.TrimSpace(reportID), version)
|
|
}
|
|
|
|
func (dailyReportsMapper) Stats(tenantID types.TenantID, uid types.Uid, date time.Time, departmentID string) (*types.DailyReportStats, error) {
|
|
if uid.IsZero() || date.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantDailyReportStats(tenantID, uid, date, strings.TrimSpace(departmentID))
|
|
}
|
|
|
|
func (dailyReportsMapper) Remind(tenantID types.TenantID, uid types.Uid, date time.Time, userIDs []types.Uid, idempotencyKey string) ([]types.Uid, error) {
|
|
if uid.IsZero() || date.IsZero() || strings.TrimSpace(idempotencyKey) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantDailyReportRemind(tenantID, uid, date, userIDs, strings.TrimSpace(idempotencyKey))
|
|
}
|
|
|
|
type MonthlyReportsPersistenceInterface interface {
|
|
List(tenantID types.TenantID, uid types.Uid, scope, fromMonth, toMonth, status, cursor string, limit int) ([]types.MonthlyReport, string, error)
|
|
CreateDraft(tenantID types.TenantID, uid types.Uid, reportID, month string) (*types.MonthlyReport, error)
|
|
Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.MonthlyReport, error)
|
|
SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, workSummary, nextPlan string, attachments []string, version int) (*types.MonthlyReport, error)
|
|
Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error)
|
|
Reopen(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error)
|
|
Stats(tenantID types.TenantID, uid types.Uid, month, departmentID string) (*types.MonthlyReportStats, error)
|
|
}
|
|
|
|
type monthlyReportsMapper struct{}
|
|
|
|
var MonthlyReports MonthlyReportsPersistenceInterface
|
|
|
|
func (monthlyReportsMapper) List(tenantID types.TenantID, uid types.Uid, scope, fromMonth, toMonth, status, cursor string, limit int) ([]types.MonthlyReport, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantMonthlyReportList(tenantID, uid, strings.TrimSpace(scope), strings.TrimSpace(fromMonth), strings.TrimSpace(toMonth), strings.TrimSpace(status), strings.TrimSpace(cursor), limit)
|
|
}
|
|
|
|
func (monthlyReportsMapper) CreateDraft(tenantID types.TenantID, uid types.Uid, reportID, month string) (*types.MonthlyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" || strings.TrimSpace(month) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMonthlyReportCreateDraft(tenantID, uid, strings.TrimSpace(reportID), strings.TrimSpace(month))
|
|
}
|
|
|
|
func (monthlyReportsMapper) Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.MonthlyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMonthlyReportGet(tenantID, uid, strings.TrimSpace(reportID))
|
|
}
|
|
|
|
func (monthlyReportsMapper) SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, workSummary, nextPlan string, attachments []string, version int) (*types.MonthlyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMonthlyReportSaveDraft(tenantID, uid, strings.TrimSpace(reportID), workSummary, nextPlan, attachments, version)
|
|
}
|
|
|
|
func (monthlyReportsMapper) Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMonthlyReportSubmit(tenantID, uid, strings.TrimSpace(reportID), version)
|
|
}
|
|
|
|
func (monthlyReportsMapper) Reopen(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error) {
|
|
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMonthlyReportReopen(tenantID, uid, strings.TrimSpace(reportID), version)
|
|
}
|
|
|
|
func (monthlyReportsMapper) Stats(tenantID types.TenantID, uid types.Uid, month, departmentID string) (*types.MonthlyReportStats, error) {
|
|
if uid.IsZero() || strings.TrimSpace(month) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMonthlyReportStats(tenantID, uid, strings.TrimSpace(month), strings.TrimSpace(departmentID))
|
|
}
|
|
|
|
type MailboxPersistenceInterface interface {
|
|
Config(tenantID types.TenantID, uid types.Uid) (*types.MailboxConfig, error)
|
|
Submit(tenantID types.TenantID, uid types.Uid, submission *types.MailboxSubmission, idempotencyKey string) (*types.MailboxSubmission, error)
|
|
}
|
|
|
|
type mailboxMapper struct{}
|
|
|
|
var Mailbox MailboxPersistenceInterface
|
|
|
|
func (mailboxMapper) Config(tenantID types.TenantID, uid types.Uid) (*types.MailboxConfig, error) {
|
|
if uid.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMailboxConfigGet(tenantID, uid)
|
|
}
|
|
|
|
func (mailboxMapper) Submit(tenantID types.TenantID, uid types.Uid, submission *types.MailboxSubmission, idempotencyKey string) (*types.MailboxSubmission, error) {
|
|
if uid.IsZero() || submission == nil || strings.TrimSpace(submission.SubmissionID) == "" ||
|
|
strings.TrimSpace(submission.Subject) == "" || strings.TrimSpace(submission.Content) == "" ||
|
|
strings.TrimSpace(idempotencyKey) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantMailboxSubmissionCreate(tenantID, uid, submission, strings.TrimSpace(idempotencyKey))
|
|
}
|
|
|
|
type ProfilePersistenceInterface interface {
|
|
Get(tenantID types.TenantID, uid types.Uid) (*types.UserProfile, error)
|
|
Update(tenantID types.TenantID, uid types.Uid, patch types.UserProfilePatch) (*types.UserProfile, error)
|
|
GetPrivacy(tenantID types.TenantID, uid types.Uid) (*types.UserPrivacySettings, error)
|
|
UpdatePrivacy(tenantID types.TenantID, uid types.Uid, patch types.UserPrivacyPatch) (*types.UserPrivacySettings, error)
|
|
}
|
|
|
|
type profileMapper struct{}
|
|
|
|
var Profiles ProfilePersistenceInterface
|
|
|
|
type AppReleasePersistenceInterface interface {
|
|
Latest(tenantID types.TenantID, platform, channel string) (*types.AppRelease, error)
|
|
}
|
|
|
|
type appReleaseMapper struct{}
|
|
|
|
var AppReleases AppReleasePersistenceInterface
|
|
|
|
func (appReleaseMapper) Latest(tenantID types.TenantID, platform, channel string) (*types.AppRelease, error) {
|
|
platform = strings.ToLower(strings.TrimSpace(platform))
|
|
channel = strings.ToLower(strings.TrimSpace(channel))
|
|
if platform == "" || channel == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantAppReleaseLatest(tenantID, platform, channel)
|
|
}
|
|
|
|
type DevicePushTokenPersistenceInterface interface {
|
|
Upsert(tenantID types.TenantID, token *types.DevicePushToken) (*types.DevicePushToken, error)
|
|
}
|
|
|
|
type DevicePushTokenQueryInterface interface {
|
|
GetAll(tenantID types.TenantID, uid ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error)
|
|
}
|
|
|
|
type devicePushTokenMapper struct{}
|
|
|
|
var DevicePushTokens DevicePushTokenPersistenceInterface
|
|
|
|
func (devicePushTokenMapper) Upsert(tenantID types.TenantID, token *types.DevicePushToken) (*types.DevicePushToken, error) {
|
|
if token == nil || token.UserID.IsZero() || strings.TrimSpace(token.DeviceID) == "" ||
|
|
strings.TrimSpace(token.Provider) == "" || strings.TrimSpace(token.Token) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantDevicePushTokenUpsert(tenantID, token)
|
|
}
|
|
|
|
func (devicePushTokenMapper) GetAll(tenantID types.TenantID, uids ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
reader, ok := tenantAdp.(interface {
|
|
TenantDevicePushTokenGetAll(types.TenantID, ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error)
|
|
})
|
|
if !ok {
|
|
return map[types.Uid][]types.DeviceDef{}, 0, nil
|
|
}
|
|
return reader.TenantDevicePushTokenGetAll(tenantID, uids...)
|
|
}
|
|
|
|
type SafetyPersistenceInterface interface {
|
|
CreateRiskChallenge(tenantID types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error)
|
|
ConfirmRiskChallenge(tenantID types.TenantID, uid types.Uid, challengeID, decision, actionTokenID string, now time.Time) (*types.RiskChallenge, error)
|
|
AbuseReasons(tenantID types.TenantID, targetType string) ([]types.AbuseReason, error)
|
|
CreateAbuseReport(tenantID types.TenantID, report *types.AbuseReport, idempotencyKey string) (*types.AbuseReport, error)
|
|
FeedbackConfig(tenantID types.TenantID, uid types.Uid) (*types.FeedbackConfig, error)
|
|
CreateFeedback(tenantID types.TenantID, feedback *types.FeedbackSubmission, idempotencyKey string) (*types.FeedbackSubmission, error)
|
|
}
|
|
|
|
type safetyMapper struct{}
|
|
|
|
var Safety SafetyPersistenceInterface
|
|
|
|
func (safetyMapper) CreateRiskChallenge(tenantID types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error) {
|
|
if challenge == nil || challenge.UserID.IsZero() || strings.TrimSpace(challenge.ChallengeID) == "" ||
|
|
strings.TrimSpace(challenge.Action) == "" || challenge.ExpiresAt.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantRiskChallengeCreate(tenantID, challenge)
|
|
}
|
|
|
|
func (safetyMapper) ConfirmRiskChallenge(tenantID types.TenantID, uid types.Uid, challengeID, decision, actionTokenID string, now time.Time) (*types.RiskChallenge, error) {
|
|
decision = strings.ToLower(strings.TrimSpace(decision))
|
|
if uid.IsZero() || strings.TrimSpace(challengeID) == "" || (decision != "continue" && decision != "cancel") {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantRiskChallengeConfirm(tenantID, uid, strings.TrimSpace(challengeID), decision, strings.TrimSpace(actionTokenID), now)
|
|
}
|
|
|
|
func (safetyMapper) AbuseReasons(tenantID types.TenantID, targetType string) ([]types.AbuseReason, error) {
|
|
targetType = strings.ToLower(strings.TrimSpace(targetType))
|
|
if targetType == "" {
|
|
targetType = "message"
|
|
}
|
|
if targetType != "user" && targetType != "group" && targetType != "message" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantAbuseReasons(tenantID, targetType)
|
|
}
|
|
|
|
func (safetyMapper) CreateAbuseReport(tenantID types.TenantID, report *types.AbuseReport, idempotencyKey string) (*types.AbuseReport, error) {
|
|
if report == nil || report.ReporterID.IsZero() || strings.TrimSpace(report.ReportID) == "" ||
|
|
strings.TrimSpace(report.TargetType) == "" || strings.TrimSpace(report.ReasonCode) == "" ||
|
|
strings.TrimSpace(idempotencyKey) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantAbuseReportCreate(tenantID, report, strings.TrimSpace(idempotencyKey))
|
|
}
|
|
|
|
func (safetyMapper) FeedbackConfig(tenantID types.TenantID, uid types.Uid) (*types.FeedbackConfig, error) {
|
|
if uid.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFeedbackConfigGet(tenantID, uid)
|
|
}
|
|
|
|
func (safetyMapper) CreateFeedback(tenantID types.TenantID, feedback *types.FeedbackSubmission, idempotencyKey string) (*types.FeedbackSubmission, error) {
|
|
if feedback == nil || feedback.UserID.IsZero() || strings.TrimSpace(feedback.FeedbackID) == "" ||
|
|
strings.TrimSpace(feedback.Category) == "" || strings.TrimSpace(feedback.Content) == "" ||
|
|
strings.TrimSpace(idempotencyKey) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFeedbackCreate(tenantID, feedback, strings.TrimSpace(idempotencyKey))
|
|
}
|
|
|
|
func (profileMapper) Get(tenantID types.TenantID, uid types.Uid) (*types.UserProfile, error) {
|
|
if uid.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUserProfileGet(tenantID, uid)
|
|
}
|
|
|
|
func (profileMapper) Update(tenantID types.TenantID, uid types.Uid, patch types.UserProfilePatch) (*types.UserProfile, error) {
|
|
if uid.IsZero() || patch.Version <= 0 || !validUserProfilePatch(patch) {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUserProfileUpdate(tenantID, uid, patch)
|
|
}
|
|
|
|
func (profileMapper) GetPrivacy(tenantID types.TenantID, uid types.Uid) (*types.UserPrivacySettings, error) {
|
|
if uid.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUserPrivacyGet(tenantID, uid)
|
|
}
|
|
|
|
func (profileMapper) UpdatePrivacy(tenantID types.TenantID, uid types.Uid, patch types.UserPrivacyPatch) (*types.UserPrivacySettings, error) {
|
|
if uid.IsZero() || patch.Version <= 0 || !validUserPrivacyPatch(patch) {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantUserPrivacyUpdate(tenantID, uid, patch)
|
|
}
|
|
|
|
func validUserPrivacyPatch(patch types.UserPrivacyPatch) bool {
|
|
if patch.AddMeMethods == nil && patch.ProfileVisibility == nil && patch.LastSeenVisibility == nil && patch.ReadReceipts == nil {
|
|
return false
|
|
}
|
|
for method := range patch.AddMeMethods {
|
|
switch method {
|
|
case "account", "phone", "qr", "group":
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
validVisibility := func(value *string) bool {
|
|
if value == nil {
|
|
return true
|
|
}
|
|
switch strings.TrimSpace(*value) {
|
|
case "everyone", "friends", "nobody":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return validVisibility(patch.ProfileVisibility) && validVisibility(patch.LastSeenVisibility)
|
|
}
|
|
|
|
func validUserProfilePatch(patch types.UserProfilePatch) bool {
|
|
if patch.AvatarFileID == nil && patch.Nickname == nil && patch.BirthDate == nil && patch.Signature == nil && patch.Gender == nil && patch.Region == nil && patch.Email == nil {
|
|
return false
|
|
}
|
|
if patch.Nickname != nil {
|
|
name := strings.TrimSpace(*patch.Nickname)
|
|
if len([]rune(name)) == 0 || len([]rune(name)) > 32 {
|
|
return false
|
|
}
|
|
}
|
|
if patch.Signature != nil && len([]rune(*patch.Signature)) > 120 {
|
|
return false
|
|
}
|
|
if patch.Email != nil && len([]rune(strings.TrimSpace(*patch.Email))) > 128 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type RelationsPersistenceInterface interface {
|
|
CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error)
|
|
ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error)
|
|
CountPendingFriendRequests(tenantID types.TenantID, uid types.Uid) (int, error)
|
|
ResolveFriendRequest(tenantID types.TenantID, requestID string, actor types.Uid, status string, handledAt time.Time) (*types.FriendRequest, error)
|
|
ListFriends(tenantID types.TenantID, uid types.Uid, query, initial, cursor string, limit int) ([]types.FriendListItem, string, error)
|
|
GetRelationshipContext(tenantID types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error)
|
|
CanSendP2P(tenantID types.TenantID, userID, peerID types.Uid) (bool, string, error)
|
|
BlockUser(tenantID types.TenantID, blockerID, blockedID types.Uid, now time.Time) (*types.UserBlock, error)
|
|
ListBlocks(tenantID types.TenantID, blockerID types.Uid, cursor string, limit int) ([]types.BlockListItem, string, error)
|
|
UnblockUser(tenantID types.TenantID, blockerID, blockedID types.Uid) error
|
|
DeleteFriend(tenantID types.TenantID, userID, peerID types.Uid, now time.Time) error
|
|
UpdateFriendRemark(tenantID types.TenantID, userID, peerID types.Uid, remark string, now time.Time) (*types.Friendship, error)
|
|
SearchUsers(tenantID types.TenantID, viewer types.Uid, query, searchType, cursor string, limit int) ([]types.UserSearchItem, string, error)
|
|
}
|
|
|
|
type relationsMapper struct{}
|
|
|
|
var Relations RelationsPersistenceInterface
|
|
|
|
type FavoritesPersistenceInterface interface {
|
|
Create(tenantID types.TenantID, item *types.FavoriteItem) (*types.FavoriteItem, error)
|
|
List(tenantID types.TenantID, uid types.Uid, itemType, query, cursor string, limit int) ([]types.FavoriteItem, string, error)
|
|
Get(tenantID types.TenantID, uid types.Uid, favoriteID string) (*types.FavoriteItem, error)
|
|
Delete(tenantID types.TenantID, uid types.Uid, favoriteID string) error
|
|
}
|
|
|
|
type favoritesMapper struct{}
|
|
|
|
var Favorites FavoritesPersistenceInterface
|
|
|
|
func (favoritesMapper) Create(tenantID types.TenantID, item *types.FavoriteItem) (*types.FavoriteItem, error) {
|
|
if item == nil || item.TenantID != tenantID || item.UserID.IsZero() || item.FavoriteID == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFavoriteCreate(tenantID, item)
|
|
}
|
|
|
|
func (favoritesMapper) List(tenantID types.TenantID, uid types.Uid, itemType, query, cursor string, limit int) ([]types.FavoriteItem, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantFavoriteList(tenantID, uid, itemType, query, cursor, limit)
|
|
}
|
|
|
|
func (favoritesMapper) Get(tenantID types.TenantID, uid types.Uid, favoriteID string) (*types.FavoriteItem, error) {
|
|
if uid.IsZero() || strings.TrimSpace(favoriteID) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFavoriteGet(tenantID, uid, favoriteID)
|
|
}
|
|
|
|
func (favoritesMapper) Delete(tenantID types.TenantID, uid types.Uid, favoriteID string) error {
|
|
if uid.IsZero() || strings.TrimSpace(favoriteID) == "" {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantFavoriteDelete(tenantID, uid, favoriteID)
|
|
}
|
|
|
|
func (relationsMapper) CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error) {
|
|
if req == nil || req.TenantID != tenantID || req.ApplicantID.IsZero() || req.TargetID.IsZero() || req.ApplicantID == req.TargetID {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFriendRequestCreate(tenantID, req)
|
|
}
|
|
|
|
func (relationsMapper) ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
switch direction {
|
|
case "", "inbound", "outbound", "all":
|
|
default:
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
switch status {
|
|
case "", "all", types.FriendRequestPending, types.FriendRequestAccepted, types.FriendRequestRejected, types.FriendRequestExpired:
|
|
default:
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 20
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantFriendRequestList(tenantID, uid, direction, status, cursor, limit)
|
|
}
|
|
|
|
func (relationsMapper) CountPendingFriendRequests(tenantID types.TenantID, uid types.Uid) (int, error) {
|
|
if uid.IsZero() {
|
|
return 0, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return tenantAdp.TenantFriendRequestPendingCount(tenantID, uid)
|
|
}
|
|
|
|
func (relationsMapper) ResolveFriendRequest(tenantID types.TenantID, requestID string, actor types.Uid, status string, handledAt time.Time) (*types.FriendRequest, error) {
|
|
if strings.TrimSpace(requestID) == "" || actor.IsZero() || (status != types.FriendRequestAccepted && status != types.FriendRequestRejected) {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status == types.FriendRequestAccepted {
|
|
req, err := tenantAdp.TenantFriendRequestAccept(tenantID, requestID, actor, handledAt)
|
|
if err == nil && req != nil {
|
|
cacheFriendPair(tenantID, req.ApplicantID, req.TargetID)
|
|
}
|
|
return req, err
|
|
}
|
|
req, err := tenantAdp.TenantFriendRequestResolve(tenantID, requestID, actor, status, handledAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func (relationsMapper) ListFriends(tenantID types.TenantID, uid types.Uid, query, initial, cursor string, limit int) ([]types.FriendListItem, string, error) {
|
|
if uid.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
items, nextCursor, err := tenantAdp.TenantFriendshipList(tenantID, uid, query, initial, cursor, limit)
|
|
if err == nil && query == "" && initial == "" {
|
|
for _, item := range items {
|
|
cacheFriendOneWay(tenantID, uid, item.Friendship.PeerID)
|
|
}
|
|
}
|
|
return items, nextCursor, err
|
|
}
|
|
|
|
func (relationsMapper) GetRelationshipContext(tenantID types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error) {
|
|
if userID.IsZero() || peerID.IsZero() || userID == peerID {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return relationshipContext(tenantAdp, tenantID, userID, peerID, topic)
|
|
}
|
|
|
|
func (relationsMapper) CanSendP2P(tenantID types.TenantID, userID, peerID types.Uid) (bool, string, error) {
|
|
ctx, err := (relationsMapper{}).GetRelationshipContext(tenantID, userID, peerID, "")
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
if ctx.Relationship == types.RelationshipFriend {
|
|
return true, ctx.Relationship, nil
|
|
}
|
|
return false, ctx.Relationship, nil
|
|
}
|
|
|
|
func (relationsMapper) BlockUser(tenantID types.TenantID, blockerID, blockedID types.Uid, now time.Time) (*types.UserBlock, error) {
|
|
if blockerID.IsZero() || blockedID.IsZero() || blockerID == blockedID {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
block, err := tenantAdp.TenantUserBlockUpsert(tenantID, blockerID, blockedID, now)
|
|
if err == nil {
|
|
cacheBlockOneWay(tenantID, blockerID, blockedID)
|
|
}
|
|
return block, err
|
|
}
|
|
|
|
func (relationsMapper) ListBlocks(tenantID types.TenantID, blockerID types.Uid, cursor string, limit int) ([]types.BlockListItem, string, error) {
|
|
if blockerID.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
items, nextCursor, err := tenantAdp.TenantUserBlockList(tenantID, blockerID, cursor, limit)
|
|
if err == nil {
|
|
for _, item := range items {
|
|
cacheBlockOneWay(tenantID, blockerID, item.Block.BlockedID)
|
|
}
|
|
}
|
|
return items, nextCursor, err
|
|
}
|
|
|
|
func (relationsMapper) UnblockUser(tenantID types.TenantID, blockerID, blockedID types.Uid) error {
|
|
if blockerID.IsZero() || blockedID.IsZero() || blockerID == blockedID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = tenantAdp.TenantUserBlockDelete(tenantID, blockerID, blockedID)
|
|
if err == nil {
|
|
uncacheBlockOneWay(tenantID, blockerID, blockedID)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (relationsMapper) DeleteFriend(tenantID types.TenantID, userID, peerID types.Uid, now time.Time) error {
|
|
if userID.IsZero() || peerID.IsZero() || userID == peerID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = tenantAdp.TenantFriendshipDeletePair(tenantID, userID, peerID, now)
|
|
if err == nil {
|
|
uncacheFriendPair(tenantID, userID, peerID)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (relationsMapper) UpdateFriendRemark(tenantID types.TenantID, userID, peerID types.Uid, remark string, now time.Time) (*types.Friendship, error) {
|
|
if userID.IsZero() || peerID.IsZero() || userID == peerID || len([]rune(remark)) > 64 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFriendshipUpdateRemark(tenantID, userID, peerID, remark, now)
|
|
}
|
|
|
|
func (relationsMapper) SearchUsers(tenantID types.TenantID, viewer types.Uid, query, searchType, cursor string, limit int) ([]types.UserSearchItem, string, error) {
|
|
if viewer.IsZero() || strings.TrimSpace(query) == "" {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
switch searchType {
|
|
case "", "account", "exact_account", "phone":
|
|
default:
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 20
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
users, nextCursor, err := tenantAdp.TenantUserSearch(tenantID, viewer, query, searchType, cursor, limit)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
items := make([]types.UserSearchItem, 0, len(users))
|
|
for i := range users {
|
|
uid := users[i].Uid()
|
|
ctx, err := relationshipContext(tenantAdp, tenantID, viewer, uid, viewer.P2PName(uid))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
items = append(items, types.UserSearchItem{
|
|
User: &users[i],
|
|
Relationship: ctx.Relationship,
|
|
RequestID: ctx.RequestID,
|
|
CanAdd: ctx.Relationship == types.RelationshipNone,
|
|
Action: relationshipAction(ctx.Relationship),
|
|
})
|
|
}
|
|
return items, nextCursor, nil
|
|
}
|
|
|
|
func relationshipAction(relationship string) string {
|
|
switch relationship {
|
|
case types.RelationshipFriend:
|
|
return "chat"
|
|
case types.RelationshipPendingInbound, types.RelationshipPendingOutbound:
|
|
return "pending"
|
|
case types.RelationshipBlockedByMe, types.RelationshipBlockedByPeer:
|
|
return "none"
|
|
default:
|
|
return "add"
|
|
}
|
|
}
|
|
|
|
func relationshipContext(tenantAdp adapter.TenantBusinessAdapter, tenantID types.TenantID, userID, peerID types.Uid, topic string) (*types.RelationshipContext, error) {
|
|
ctx := &types.RelationshipContext{
|
|
TenantID: tenantID,
|
|
UserID: userID,
|
|
PeerID: peerID,
|
|
Topic: topic,
|
|
Relationship: types.RelationshipNone,
|
|
}
|
|
peer, err := tenantAdp.TenantUserGet(tenantID, peerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx.Peer = peer
|
|
if cachedBlock, err := cachedBlockContains(tenantID, userID, peerID); err != nil {
|
|
logs.Warn.Printf("cache block[%s:%s]: read failed: %v", userID.UserId(), peerID.UserId(), err)
|
|
} else if cachedBlock {
|
|
ctx.Relationship = types.RelationshipBlockedByMe
|
|
return ctx, nil
|
|
}
|
|
if block, err := tenantAdp.TenantUserBlockGet(tenantID, userID, peerID); err != nil {
|
|
return nil, err
|
|
} else if block != nil {
|
|
cacheBlockOneWay(tenantID, userID, peerID)
|
|
ctx.Relationship = types.RelationshipBlockedByMe
|
|
return ctx, nil
|
|
}
|
|
if cachedBlock, err := cachedBlockContains(tenantID, peerID, userID); err != nil {
|
|
logs.Warn.Printf("cache block[%s:%s]: read failed: %v", peerID.UserId(), userID.UserId(), err)
|
|
} else if cachedBlock {
|
|
ctx.Relationship = types.RelationshipBlockedByPeer
|
|
return ctx, nil
|
|
}
|
|
if block, err := tenantAdp.TenantUserBlockGet(tenantID, peerID, userID); err != nil {
|
|
return nil, err
|
|
} else if block != nil {
|
|
cacheBlockOneWay(tenantID, peerID, userID)
|
|
ctx.Relationship = types.RelationshipBlockedByPeer
|
|
return ctx, nil
|
|
}
|
|
if cachedFriend, err := cachedFriendContains(tenantID, userID, peerID); err != nil {
|
|
logs.Warn.Printf("cache friend[%s:%s]: read failed: %v", userID.UserId(), peerID.UserId(), err)
|
|
} else if cachedFriend {
|
|
ctx.Relationship = types.RelationshipFriend
|
|
ctx.CanSend = true
|
|
if ctx.Topic == "" {
|
|
ctx.Topic = userID.P2PName(peerID)
|
|
}
|
|
return ctx, nil
|
|
}
|
|
if friendship, err := tenantAdp.TenantFriendshipGet(tenantID, userID, peerID); err != nil {
|
|
return nil, err
|
|
} else if friendship != nil && friendship.Status == types.RelationshipFriend {
|
|
cacheFriendOneWay(tenantID, userID, peerID)
|
|
ctx.Relationship = types.RelationshipFriend
|
|
ctx.CanSend = true
|
|
if ctx.Topic == "" {
|
|
ctx.Topic = friendship.Topic
|
|
}
|
|
return ctx, nil
|
|
}
|
|
req, err := tenantAdp.TenantFriendRequestLatestBetween(tenantID, userID, peerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req != nil && req.Status == types.FriendRequestPending && req.ExpiresAt.After(time.Now()) {
|
|
ctx.RequestID = req.RequestID
|
|
ctx.PendingStatus = req.Status
|
|
if req.ApplicantID == userID {
|
|
ctx.Relationship = types.RelationshipPendingOutbound
|
|
} else {
|
|
ctx.Relationship = types.RelationshipPendingInbound
|
|
}
|
|
}
|
|
return ctx, nil
|
|
}
|
|
|
|
func relationSetID(uid types.Uid) string {
|
|
return uid.UserId()
|
|
}
|
|
|
|
func cacheFriendPair(tenantID types.TenantID, uid1, uid2 types.Uid) {
|
|
cacheFriendOneWay(tenantID, uid1, uid2)
|
|
cacheFriendOneWay(tenantID, uid2, uid1)
|
|
}
|
|
|
|
func cacheFriendOneWay(tenantID types.TenantID, uid, peer types.Uid) {
|
|
if err := AddCachedSetMember(tenantID, cacheNamespaceFriends, relationSetID(uid), peer.UserId()); err != nil {
|
|
logs.Warn.Printf("cache friends[%s]: add failed: %v", uid.UserId(), err)
|
|
}
|
|
}
|
|
|
|
func uncacheFriendPair(tenantID types.TenantID, uid1, uid2 types.Uid) {
|
|
if err := RemoveCachedSetMember(tenantID, cacheNamespaceFriends, relationSetID(uid1), uid2.UserId()); err != nil {
|
|
logs.Warn.Printf("cache friends[%s]: remove failed: %v", uid1.UserId(), err)
|
|
}
|
|
if err := RemoveCachedSetMember(tenantID, cacheNamespaceFriends, relationSetID(uid2), uid1.UserId()); err != nil {
|
|
logs.Warn.Printf("cache friends[%s]: remove failed: %v", uid2.UserId(), err)
|
|
}
|
|
}
|
|
|
|
func cachedFriendContains(tenantID types.TenantID, uid, peer types.Uid) (bool, error) {
|
|
return CachedSetContains(tenantID, cacheNamespaceFriends, relationSetID(uid), peer.UserId())
|
|
}
|
|
|
|
func cacheBlockOneWay(tenantID types.TenantID, blocker, blocked types.Uid) {
|
|
if err := AddCachedSetMember(tenantID, cacheNamespaceBlocklist, relationSetID(blocker), blocked.UserId()); err != nil {
|
|
logs.Warn.Printf("cache blocklist[%s]: add failed: %v", blocker.UserId(), err)
|
|
}
|
|
}
|
|
|
|
func uncacheBlockOneWay(tenantID types.TenantID, blocker, blocked types.Uid) {
|
|
if err := RemoveCachedSetMember(tenantID, cacheNamespaceBlocklist, relationSetID(blocker), blocked.UserId()); err != nil {
|
|
logs.Warn.Printf("cache blocklist[%s]: remove failed: %v", blocker.UserId(), err)
|
|
}
|
|
}
|
|
|
|
func cachedBlockContains(tenantID types.TenantID, blocker, blocked types.Uid) (bool, error) {
|
|
return CachedSetContains(tenantID, cacheNamespaceBlocklist, relationSetID(blocker), blocked.UserId())
|
|
}
|
|
|
|
// Save message
|
|
func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
|
saveResult, err := Messages.PreparePersist(tenantID, msg, attachmentURLs, readBySender)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if saveResult != nil && saveResult.Duplicate {
|
|
return saveResult, nil
|
|
}
|
|
return Messages.PersistPrepared(tenantID, msg, attachmentURLs, readBySender)
|
|
}
|
|
|
|
func (messagesMapper) PreparePersist(tenantID types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) {
|
|
if msg == nil || msg.TenantID != tenantID {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientMsgID, err := normalizeClientMsgID(msg.Head)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
msg.ClientMsgID = clientMsgID
|
|
fromUid := types.ParseUid(msg.From)
|
|
isGroupMessage := types.GetTopicCat(msg.Topic) == types.TopicCatGrp
|
|
if isGroupMessage && groupMsgRuntime != nil {
|
|
if clientMsgID != "" {
|
|
claim, claimed, err := groupMsgRuntime.Claim(tenantID, msg.Topic, fromUid, clientMsgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !claimed {
|
|
if claim != nil && claim.Status == "done" && claim.Seq > 0 {
|
|
msg.SeqId = claim.Seq
|
|
return &types.MessageSaveResult{SeqId: claim.Seq, Duplicate: true}, nil
|
|
}
|
|
return nil, types.ErrDuplicate
|
|
}
|
|
}
|
|
seq, err := groupMsgRuntime.NextSeq(tenantID, msg.Topic, msg.SeqId-1)
|
|
if err != nil {
|
|
if clientMsgID != "" {
|
|
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
|
|
}
|
|
return nil, err
|
|
}
|
|
msg.SeqId = seq
|
|
}
|
|
if clientMsgID != "" {
|
|
var duplicate *types.Message
|
|
if isGroupMessage {
|
|
duplicate, err = tenantAdp.TenantGroupMessageGetByClientID(tenantID, msg.Topic, fromUid, clientMsgID)
|
|
} else {
|
|
duplicate, err = tenantAdp.TenantMessageGetByClientID(tenantID, fromUid, clientMsgID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if duplicate != nil {
|
|
msg.ObjHeader = duplicate.ObjHeader
|
|
msg.DeletedAt = duplicate.DeletedAt
|
|
msg.DelId = duplicate.DelId
|
|
msg.DeletedFor = duplicate.DeletedFor
|
|
msg.SeqId = duplicate.SeqId
|
|
msg.Topic = duplicate.Topic
|
|
msg.From = duplicate.From
|
|
msg.ClientMsgID = duplicate.ClientMsgID
|
|
msg.Head = duplicate.Head
|
|
msg.Content = duplicate.Content
|
|
return &types.MessageSaveResult{SeqId: duplicate.SeqId, Duplicate: true}, nil
|
|
}
|
|
}
|
|
msg.InitTimes()
|
|
msg.SetUid(Store.GetUid())
|
|
// Reserve topic's or user's SeqId before optional async persistence.
|
|
err = tenantAdp.TenantTopicUpdateOnMessage(tenantID, msg.Topic, msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &types.MessageSaveResult{SeqId: msg.SeqId}, nil
|
|
}
|
|
|
|
func (messagesMapper) PersistPrepared(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
|
if msg == nil || msg.TenantID != tenantID || msg.Topic == "" || msg.SeqId <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientMsgID, err := normalizeClientMsgID(msg.Head)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
msg.ClientMsgID = clientMsgID
|
|
fromUid := types.ParseUid(msg.From)
|
|
isGroupMessage := types.GetTopicCat(msg.Topic) == types.TopicCatGrp
|
|
if isGroupMessage {
|
|
err = tenantAdp.TenantGroupMessageSave(tenantID, msg)
|
|
} else {
|
|
err = tenantAdp.TenantMessageSave(tenantID, msg)
|
|
if err == types.ErrDuplicate && clientMsgID == "" {
|
|
err = retryTenantMessageSaveAfterSeqConflict(tenantAdp, tenantID, msg)
|
|
}
|
|
}
|
|
if err != nil {
|
|
if isGroupMessage && err == types.ErrDuplicate && clientMsgID != "" {
|
|
duplicate, dupErr := tenantAdp.TenantGroupMessageGetByClientID(tenantID, msg.Topic, fromUid, clientMsgID)
|
|
if dupErr != nil {
|
|
return nil, dupErr
|
|
}
|
|
if duplicate != nil {
|
|
msg.SeqId = duplicate.SeqId
|
|
if groupMsgRuntime != nil {
|
|
_ = groupMsgRuntime.MarkDone(tenantID, msg.Topic, fromUid, clientMsgID, duplicate.SeqId, duplicate.Uid().String())
|
|
}
|
|
return &types.MessageSaveResult{SeqId: duplicate.SeqId, Duplicate: true}, nil
|
|
}
|
|
}
|
|
if isGroupMessage && groupMsgRuntime != nil && clientMsgID != "" {
|
|
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
|
|
}
|
|
return nil, err
|
|
}
|
|
if isGroupMessage && groupMsgRuntime != nil && clientMsgID != "" {
|
|
if err := groupMsgRuntime.MarkDone(tenantID, msg.Topic, fromUid, clientMsgID, msg.SeqId, msg.Uid().String()); err != nil {
|
|
logs.Warn.Printf("topic[%s]: failed to mark group message unique key done: %v", msg.Topic, err)
|
|
}
|
|
}
|
|
if isGroupMessage && groupMsgRuntime != nil {
|
|
if err := groupMsgRuntime.CacheHotMessage(tenantID, msg); err != nil {
|
|
logs.Warn.Printf("topic[%s]: failed to cache hot group message: %v", msg.Topic, err)
|
|
}
|
|
}
|
|
|
|
markedReadBySender := false
|
|
// Mark message as read by the sender.
|
|
if readBySender {
|
|
// Make sure From is valid, otherwise we will reset values for all subscribers.
|
|
if !fromUid.IsZero() {
|
|
// Ignore the error here. It's not a big deal if it fails.
|
|
if subErr := tenantAdp.TenantSubsUpdate(tenantID, msg.Topic, fromUid,
|
|
map[string]any{
|
|
"RecvSeqId": msg.SeqId,
|
|
"ReadSeqId": msg.SeqId}); subErr != nil {
|
|
logs.Warn.Printf("topic[%s]: failed to mark message (seq: %d) read by sender - err: %+v", msg.Topic, msg.SeqId, subErr)
|
|
} else {
|
|
markedReadBySender = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(attachmentURLs) > 0 {
|
|
var attachments []string
|
|
for _, url := range attachmentURLs {
|
|
// Convert attachment URLs to file IDs.
|
|
if fid := mediaHandler.GetIdFromUrl(url); !fid.IsZero() {
|
|
attachments = append(attachments, fid.String())
|
|
}
|
|
}
|
|
if len(attachments) > 0 && isGroupMessage {
|
|
fileType := groupFileTypeFromMessageHead(msg.Head)
|
|
for _, fileID := range attachments {
|
|
if _, err := tenantAdp.TenantGroupFileCreate(tenantID, &types.GroupFile{
|
|
Topic: msg.Topic,
|
|
FileID: fileID,
|
|
SourceTopic: msg.Topic,
|
|
SourceSeq: msg.SeqId,
|
|
UploaderID: fromUid,
|
|
FileType: fileType,
|
|
IdempotencyKey: fmt.Sprintf("%s:%d:%s", msg.Topic, msg.SeqId, fileID),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else if len(attachments) > 0 {
|
|
if err := tenantAdp.TenantFileLinkAttachments(tenantID, "", types.ZeroUid, msg.Uid(), attachments); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return &types.MessageSaveResult{SeqId: msg.SeqId, MarkedReadBySender: markedReadBySender}, nil
|
|
}
|
|
|
|
func PrepareMessagePersist(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
|
return Messages.PreparePersist(tenantID, msg, attachmentURLs, readBySender)
|
|
}
|
|
|
|
func PersistPreparedMessage(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
|
return Messages.PersistPrepared(tenantID, msg, attachmentURLs, readBySender)
|
|
}
|
|
|
|
func retryTenantMessageSaveAfterSeqConflict(tenantAdp adapter.TenantBusinessAdapter, tenantID types.TenantID, msg *types.Message) error {
|
|
latest, err := tenantAdp.TenantMessageGetAll(tenantID, msg.Topic, types.ZeroUid, &types.QueryOpt{Before: 1 << 62, Limit: 1})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(latest) == 0 || latest[0].SeqId < msg.SeqId {
|
|
return types.ErrDuplicate
|
|
}
|
|
msg.SeqId = latest[0].SeqId + 1
|
|
if err = tenantAdp.TenantTopicUpdateOnMessage(tenantID, msg.Topic, msg); err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantMessageSave(tenantID, msg)
|
|
}
|
|
|
|
func normalizeClientMsgID(head types.KVMap) (string, error) {
|
|
if head == nil {
|
|
return "", nil
|
|
}
|
|
if err := validateMessageHeaders(head); err != nil {
|
|
return "", err
|
|
}
|
|
value, ok := head["x-im-client-mid"]
|
|
if !ok {
|
|
return "", nil
|
|
}
|
|
clientMsgID, ok := value.(string)
|
|
if !ok {
|
|
return "", types.ErrMalformed
|
|
}
|
|
clientMsgID = strings.TrimSpace(clientMsgID)
|
|
if len(clientMsgID) > 64 {
|
|
return "", types.ErrMalformed
|
|
}
|
|
return clientMsgID, nil
|
|
}
|
|
|
|
func validateMessageHeaders(head types.KVMap) error {
|
|
if schema, ok := head["x-im-schema"]; ok {
|
|
if !validIMMessageSchema(schema) {
|
|
return types.ErrMalformed
|
|
}
|
|
}
|
|
legacyType, hasLegacyType := messageHeaderString(head, "type")
|
|
productType, hasProductType := messageHeaderString(head, "x-im-type")
|
|
if raw, ok := head["type"]; ok && (!hasLegacyType || !validIMMessageType(legacyType)) {
|
|
_ = raw
|
|
return types.ErrMalformed
|
|
}
|
|
if raw, ok := head["x-im-type"]; ok && (!hasProductType || !validIMMessageType(productType)) {
|
|
_ = raw
|
|
return types.ErrMalformed
|
|
}
|
|
if hasLegacyType && hasProductType {
|
|
if canonicalIMMessageType(legacyType) != canonicalIMMessageType(productType) {
|
|
return types.ErrMalformed
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validIMMessageSchema(value any) bool {
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return strings.TrimSpace(typed) == "1"
|
|
case json.Number:
|
|
parsed, err := typed.Int64()
|
|
return err == nil && parsed == 1
|
|
case float64:
|
|
return typed == 1
|
|
case float32:
|
|
return typed == 1
|
|
case int:
|
|
return typed == 1
|
|
case int8:
|
|
return typed == 1
|
|
case int16:
|
|
return typed == 1
|
|
case int32:
|
|
return typed == 1
|
|
case int64:
|
|
return typed == 1
|
|
case uint:
|
|
return typed == 1
|
|
case uint8:
|
|
return typed == 1
|
|
case uint16:
|
|
return typed == 1
|
|
case uint32:
|
|
return typed == 1
|
|
case uint64:
|
|
return typed == 1
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func messageHeaderString(head types.KVMap, key string) (string, bool) {
|
|
value, ok := head[key]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
text = strings.TrimSpace(text)
|
|
return text, text != ""
|
|
}
|
|
|
|
func validIMMessageHeaderToken(value string, max int) bool {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || len(value) > max {
|
|
return false
|
|
}
|
|
for _, ch := range value {
|
|
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '.' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func validIMMessageType(value string) bool {
|
|
switch strings.TrimSpace(value) {
|
|
case "text", "image", "file", "audio", "voice", "video", "location", "contact", "contact_card",
|
|
"card", "link", "forward_bundle", "sticker", "call", "system", "system_event":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func canonicalIMMessageType(value string) string {
|
|
switch strings.TrimSpace(value) {
|
|
case "audio":
|
|
return "voice"
|
|
case "contact":
|
|
return "contact_card"
|
|
case "system":
|
|
return "system_event"
|
|
default:
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
|
|
func groupFileTypeFromMessageHead(head types.KVMap) string {
|
|
msgType, ok := messageHeaderString(head, "x-im-type")
|
|
if !ok {
|
|
msgType, ok = messageHeaderString(head, "type")
|
|
}
|
|
if !ok {
|
|
return "file"
|
|
}
|
|
switch canonicalIMMessageType(msgType) {
|
|
case "image", "audio", "voice", "video":
|
|
return canonicalIMMessageType(msgType)
|
|
default:
|
|
return "file"
|
|
}
|
|
}
|
|
|
|
// DeleteList deletes multiple messages defined by a list of ranges.
|
|
func (messagesMapper) DeleteList(tenantID types.TenantID, topic string, delID int, forUser types.Uid, msgDelAge time.Duration, ranges []types.Range) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var toDel *types.DelMessage
|
|
if delID > 0 {
|
|
toDel = &types.DelMessage{
|
|
TenantID: tenantID,
|
|
Topic: topic,
|
|
DelId: delID,
|
|
DeletedFor: forUser.String(),
|
|
SeqIdRanges: ranges}
|
|
toDel.SetUid(Store.GetUid())
|
|
toDel.InitTimes()
|
|
if msgDelAge > 0 {
|
|
toDel.SetNewerThan(toDel.CreatedAt.Add(-msgDelAge))
|
|
}
|
|
}
|
|
|
|
isGroupMessage := types.GetTopicCat(topic) == types.TopicCatGrp
|
|
if isGroupMessage {
|
|
err = tenantAdp.TenantGroupMessageDeleteList(tenantID, topic, toDel)
|
|
} else {
|
|
err = tenantAdp.TenantMessageDeleteList(tenantID, topic, toDel)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isGroupMessage && groupMsgRuntime != nil {
|
|
if cacheErr := groupMsgRuntime.InvalidateHotMessages(tenantID, topic, ranges); cacheErr != nil {
|
|
logs.Warn.Printf("topic[%s]: failed to invalidate deleted group message cache: %v", topic, cacheErr)
|
|
}
|
|
}
|
|
|
|
// TODO: move to adapter.
|
|
if delID > 0 {
|
|
// Record ID of the delete transaction
|
|
err = tenantAdp.TenantTopicUpdate(tenantID, topic, map[string]any{"DelId": delID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Soft-deleting will update one subscription, hard-deleting will ipdate all.
|
|
// Soft- or hard- is defined by the forUser being defined.
|
|
err = tenantAdp.TenantSubsUpdate(tenantID, topic, forUser, map[string]any{"DelId": delID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// GetAll returns multiple messages.
|
|
func (messagesMapper) GetAll(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Message, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if types.GetTopicCat(topic) == types.TopicCatGrp {
|
|
if groupMsgRuntime != nil && forUser.IsZero() {
|
|
if msgs, ok, cacheErr := groupMsgRuntime.GetHotMessages(tenantID, topic, opt, 100); cacheErr != nil {
|
|
logs.Warn.Printf("topic[%s]: failed to read hot group messages: %v", topic, cacheErr)
|
|
} else if ok {
|
|
return msgs, nil
|
|
}
|
|
}
|
|
return tenantAdp.TenantGroupMessageGetAll(tenantID, topic, forUser, opt)
|
|
}
|
|
return tenantAdp.TenantMessageGetAll(tenantID, topic, forUser, opt)
|
|
}
|
|
|
|
func (messagesMapper) Search(tenantID types.TenantID, topic string, forUser types.Uid, query, messageType, cursor string, limit int) ([]types.Message, string, error) {
|
|
if topic == "" || forUser.IsZero() {
|
|
return nil, "", types.ErrMalformed
|
|
}
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 50
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantMessageSearch(tenantID, topic, forUser, query, messageType, cursor, limit)
|
|
}
|
|
|
|
// GetDeleted returns the ranges of deleted messages and the largest DelId reported in the list.
|
|
func (messagesMapper) GetDeleted(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Range, int, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
dmsgs, err := tenantAdp.TenantMessageGetDeleted(tenantID, topic, forUser, opt)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var ranges []types.Range
|
|
var maxID int
|
|
// Flatten out the ranges
|
|
for i := range dmsgs {
|
|
dm := &dmsgs[i]
|
|
if dm.DelId > maxID {
|
|
maxID = dm.DelId
|
|
}
|
|
ranges = append(ranges, dm.SeqIdRanges...)
|
|
}
|
|
sort.Sort(types.RangeSorter(ranges))
|
|
ranges = types.RangeSorter(ranges).Normalize()
|
|
|
|
return ranges, maxID, nil
|
|
}
|
|
|
|
// Registered authentication handlers.
|
|
var authHandlers map[string]auth.AuthHandler
|
|
|
|
// Logical auth handler names
|
|
var authHandlerNames map[string]string
|
|
|
|
// RegisterAuthScheme registers an authentication scheme handler.
|
|
// The 'name' must be the hardcoded name, NOT the logical name.
|
|
func RegisterAuthScheme(name string, handler auth.AuthHandler) {
|
|
if name == "" {
|
|
panic("RegisterAuthScheme: empty auth scheme name")
|
|
}
|
|
if handler == nil {
|
|
panic("RegisterAuthScheme: scheme handler is nil")
|
|
}
|
|
|
|
name = strings.ToLower(name)
|
|
if authHandlers == nil {
|
|
authHandlers = make(map[string]auth.AuthHandler)
|
|
}
|
|
if _, dup := authHandlers[name]; dup {
|
|
panic("RegisterAuthScheme: called twice for scheme " + name)
|
|
}
|
|
authHandlers[name] = handler
|
|
}
|
|
|
|
// GetAuthNames returns all addressable auth handler names, logical and hardcoded
|
|
// excluding those which are disabled like "basic:".
|
|
func (s storeObj) GetAuthNames() []string {
|
|
if len(authHandlers) == 0 {
|
|
return nil
|
|
}
|
|
|
|
allNames := make(map[string]struct{})
|
|
for name := range authHandlers {
|
|
allNames[name] = struct{}{}
|
|
}
|
|
for name := range authHandlerNames {
|
|
allNames[name] = struct{}{}
|
|
}
|
|
|
|
var names []string
|
|
for name := range allNames {
|
|
if s.GetLogicalAuthHandler(name) != nil {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
|
|
return names
|
|
|
|
}
|
|
|
|
// GetAuthHandler returns an auth handler by actual hardcoded name irrspectful of logical naming.
|
|
func (storeObj) GetAuthHandler(name string) auth.AuthHandler {
|
|
return authHandlers[strings.ToLower(name)]
|
|
}
|
|
|
|
// GetLogicalAuthHandler returns an auth handler by logical name. If there is no handler by that
|
|
// logical name it tries to find one by the hardcoded name.
|
|
func (storeObj) GetLogicalAuthHandler(name string) auth.AuthHandler {
|
|
name = strings.ToLower(name)
|
|
if len(authHandlerNames) != 0 {
|
|
if lname, ok := authHandlerNames[name]; ok {
|
|
return authHandlers[lname]
|
|
}
|
|
}
|
|
return authHandlers[name]
|
|
}
|
|
|
|
// InitAuthLogicalNames initializes authentication mapping "logical handler name":"actual handler name".
|
|
// Logical name must not be empty, actual name could be an empty string.
|
|
func InitAuthLogicalNames(config json.RawMessage) error {
|
|
if config == nil || string(config) == "null" {
|
|
return nil
|
|
}
|
|
var mapping []string
|
|
if err := json.Unmarshal(config, &mapping); err != nil {
|
|
return errors.New("store: failed to parse logical auth names: " + err.Error() + "(" + string(config) + ")")
|
|
}
|
|
if len(mapping) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if authHandlerNames == nil {
|
|
authHandlerNames = make(map[string]string)
|
|
}
|
|
for _, pair := range mapping {
|
|
if parts := strings.Split(pair, ":"); len(parts) == 2 {
|
|
if parts[0] == "" {
|
|
return errors.New("store: empty logical auth name '" + pair + "'")
|
|
}
|
|
parts[0] = strings.ToLower(parts[0])
|
|
if _, ok := authHandlerNames[parts[0]]; ok {
|
|
return errors.New("store: duplicate mapping for logical auth name '" + pair + "'")
|
|
}
|
|
parts[1] = strings.ToLower(parts[1])
|
|
if parts[1] != "" {
|
|
if _, ok := authHandlers[parts[1]]; !ok {
|
|
return errors.New("store: unknown handler for logical auth name '" + pair + "'")
|
|
}
|
|
}
|
|
if parts[0] == parts[1] {
|
|
// Skip useless identity mapping.
|
|
continue
|
|
}
|
|
authHandlerNames[parts[0]] = parts[1]
|
|
} else {
|
|
return errors.New("store: invalid logical auth mapping '" + pair + "'")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Registered authentication handlers.
|
|
var validators map[string]validate.Validator
|
|
|
|
// RegisterValidator registers validation scheme.
|
|
func RegisterValidator(name string, v validate.Validator) {
|
|
name = strings.ToLower(name)
|
|
if validators == nil {
|
|
validators = make(map[string]validate.Validator)
|
|
}
|
|
|
|
if v == nil {
|
|
panic("RegisterValidator: validator is nil")
|
|
}
|
|
if _, dup := validators[name]; dup {
|
|
panic("RegisterValidator: called twice for validator " + name)
|
|
}
|
|
validators[name] = v
|
|
}
|
|
|
|
// GetValidator returns registered validator by name.
|
|
func (storeObj) GetValidator(name string) validate.Validator {
|
|
return validators[strings.ToLower(name)]
|
|
}
|
|
|
|
// DevicePersistenceInterface is an interface which defines methods used for handling device IDs.
|
|
// Mostly used to generate push notifications.
|
|
type DevicePersistenceInterface interface {
|
|
Update(tenantID types.TenantID, uid types.Uid, oldDeviceID string, dev *types.DeviceDef) error
|
|
GetAll(tenantID types.TenantID, uid ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error)
|
|
Delete(tenantID types.TenantID, uid types.Uid, deviceID string) error
|
|
}
|
|
|
|
// deviceMapper is a concrete type implementing DevicePersistenceInterface.
|
|
type deviceMapper struct{}
|
|
|
|
// Devices is a singleton instance of DevicePersistenceInterface to map methods to.
|
|
var Devices DevicePersistenceInterface
|
|
|
|
// Update updates a device record.
|
|
func (deviceMapper) Update(tenantID types.TenantID, uid types.Uid, oldDeviceID string, dev *types.DeviceDef) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// If the old device Id is specified and it's different from the new ID, delete the old id
|
|
if oldDeviceID != "" && (dev == nil || dev.DeviceId != oldDeviceID) {
|
|
if err := tenantAdp.TenantDeviceDelete(tenantID, uid, oldDeviceID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Insert or update the new DeviceId if one is given.
|
|
if dev != nil && dev.DeviceId != "" {
|
|
if dev.TenantID.IsZero() {
|
|
dev.TenantID = tenantID
|
|
}
|
|
if dev.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
return tenantAdp.TenantDeviceUpsert(tenantID, uid, dev)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAll returns all known device IDs for a given list of user IDs.
|
|
// The second return parameter is the count of found device IDs.
|
|
func (deviceMapper) GetAll(tenantID types.TenantID, uid ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return tenantAdp.TenantDeviceGetAll(tenantID, uid...)
|
|
}
|
|
|
|
// Delete deletes device record for a given user.
|
|
func (deviceMapper) Delete(tenantID types.TenantID, uid types.Uid, deviceID string) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantDeviceDelete(tenantID, uid, deviceID)
|
|
}
|
|
|
|
// Registered media/file handlers.
|
|
var fileHandlers map[string]media.Handler
|
|
|
|
// RegisterMediaHandler saves reference to a media handler (file upload-download handler).
|
|
func RegisterMediaHandler(name string, mh media.Handler) {
|
|
if fileHandlers == nil {
|
|
fileHandlers = make(map[string]media.Handler)
|
|
}
|
|
|
|
if mh == nil {
|
|
panic("RegisterMediaHandler: handler is nil")
|
|
}
|
|
if _, dup := fileHandlers[name]; dup {
|
|
panic("RegisterMediaHandler: called twice for handler " + name)
|
|
}
|
|
fileHandlers[name] = mh
|
|
}
|
|
|
|
// GetMediaHandler returns default media handler.
|
|
func (storeObj) GetMediaHandler() media.Handler {
|
|
return mediaHandler
|
|
}
|
|
|
|
// UseMediaHandler sets specified media handler as default.
|
|
func (storeObj) UseMediaHandler(name, config string) error {
|
|
mediaHandler = fileHandlers[name]
|
|
if mediaHandler == nil {
|
|
panic("UseMediaHandler: unknown handler '" + name + "'")
|
|
}
|
|
return mediaHandler.Init(config)
|
|
}
|
|
|
|
// FilePersistenceInterface is an interface wchich defines methods used for file handling (records or uploaded files).
|
|
type FilePersistenceInterface interface {
|
|
// StartUpload records that the given user initiated a file upload
|
|
StartUpload(tenantID types.TenantID, fd *types.FileDef) error
|
|
// FinishUpload marks started upload as successfully finished.
|
|
FinishUpload(tenantID types.TenantID, fd *types.FileDef, success bool, size int64) (*types.FileDef, error)
|
|
// Get fetches a file record for a unique file id.
|
|
Get(tenantID types.TenantID, fid string) (*types.FileDef, error)
|
|
// DeleteUnused removes unused attachments.
|
|
DeleteUnused(tenantID types.TenantID, olderThan time.Time, limit int) error
|
|
// LinkAttachments connects earlier uploaded attachments to a message or topic to prevent it
|
|
// from being garbage collected.
|
|
LinkAttachments(tenantID types.TenantID, topic string, msgId types.Uid, attachments []string) error
|
|
// UserHasAccess checks if user can download a file through an active upload, user, topic, or message reference.
|
|
UserHasAccess(tenantID types.TenantID, fid string, uid types.Uid) (bool, error)
|
|
}
|
|
|
|
// fileMapper is concrete type which implements FilePersistenceInterface.
|
|
type fileMapper struct{}
|
|
|
|
// Files is a sigleton instance of FilePersistenceInterface to be used for handling file uploads.
|
|
var Files FilePersistenceInterface
|
|
|
|
// StartUpload records that the given user initiated a file upload
|
|
func (fileMapper) StartUpload(tenantID types.TenantID, fd *types.FileDef) error {
|
|
if fd == nil || fd.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fd.Status = types.UploadStarted
|
|
return tenantAdp.TenantFileStartUpload(tenantID, fd)
|
|
}
|
|
|
|
// FinishUpload marks started upload as successfully finished or failed.
|
|
func (fileMapper) FinishUpload(tenantID types.TenantID, fd *types.FileDef, success bool, size int64) (*types.FileDef, error) {
|
|
if fd == nil || fd.TenantID != tenantID {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantFileFinishUpload(tenantID, fd, success, size)
|
|
}
|
|
|
|
// Get fetches a file record for a unique file id.
|
|
func (fileMapper) Get(tenantID types.TenantID, fid string) (*types.FileDef, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fd, err := tenantAdp.TenantFileGet(tenantID, fid)
|
|
if err == nil && fd != nil && fd.TenantID != tenantID {
|
|
return nil, types.ErrNotFound
|
|
}
|
|
return fd, err
|
|
}
|
|
|
|
// DeleteUnused removes unused attachments and avatars.
|
|
func (fileMapper) DeleteUnused(tenantID types.TenantID, olderThan time.Time, limit int) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
toDel, err := tenantAdp.TenantFileDeleteUnused(tenantID, olderThan, limit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(toDel) > 0 {
|
|
logs.Warn.Println("deleting media", toDel)
|
|
return Store.GetMediaHandler().Delete(toDel)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LinkAttachments connects earlier uploaded attachments to a message or topic to prevent it
|
|
// from being garbage collected.
|
|
func (fileMapper) LinkAttachments(tenantID types.TenantID, topic string, msgId types.Uid, attachments []string) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Convert attachment URLs to file IDs.
|
|
var fids []string
|
|
for _, url := range attachments {
|
|
if fid := mediaHandler.GetIdFromUrl(url); !fid.IsZero() {
|
|
fids = append(fids, fid.String())
|
|
}
|
|
}
|
|
|
|
if len(fids) > 0 {
|
|
userId := types.ZeroUid
|
|
if topic != "" && types.GetTopicCat(topic) == types.TopicCatMe {
|
|
userId = types.ParseUserId(topic)
|
|
topic = ""
|
|
}
|
|
return tenantAdp.TenantFileLinkAttachments(tenantID, topic, userId, msgId, fids)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fileMapper) UserHasAccess(tenantID types.TenantID, fid string, uid types.Uid) (bool, error) {
|
|
if fid == "" || uid.IsZero() {
|
|
return false, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return tenantAdp.TenantFileUserHasAccess(tenantID, fid, uid)
|
|
}
|
|
|
|
// PersistentCacheInterface is an interface which defines methods used for accessing persistent key-value cache.
|
|
type PersistentCacheInterface interface {
|
|
// Get reads a persistent cache entry.
|
|
Get(tenantID types.TenantID, key string) (string, error)
|
|
// Upsert creates or updates a persistent cache entry.
|
|
Upsert(tenantID types.TenantID, key string, value string, failOnDuplicate bool) error
|
|
// Delete deletes a single persistent cache entry.
|
|
Delete(tenantID types.TenantID, key string) error
|
|
// Expire expires older entries with the specified key prefix.
|
|
Expire(tenantID types.TenantID, keyPrefix string, olderThan time.Time) error
|
|
}
|
|
|
|
// pcacheMapper is concrete type which implements PersistentCacheInterface.
|
|
type pcacheMapper struct{}
|
|
|
|
var PCache PersistentCacheInterface
|
|
|
|
// VerificationPersistenceInterface defines storage for product verification challenges.
|
|
type VerificationPersistenceInterface interface {
|
|
Create(tenantID types.TenantID, vc *types.VerificationCode) error
|
|
Get(tenantID types.TenantID, challengeID string) (*types.VerificationCode, error)
|
|
GetActive(tenantID types.TenantID, scene, phoneHash string, now time.Time) (*types.VerificationCode, error)
|
|
Fail(tenantID types.TenantID, challengeID string, state types.VerificationState) error
|
|
MarkVerified(tenantID types.TenantID, challengeID, tokenHash string, verifiedAt time.Time) error
|
|
Consume(tenantID types.TenantID, challengeID, tokenHash string, consumedAt time.Time) error
|
|
}
|
|
|
|
type verificationMapper struct{}
|
|
|
|
// Verifications is a singleton for product verification challenges.
|
|
var Verifications VerificationPersistenceInterface
|
|
|
|
// RegistrationInvitePersistenceInterface manages tenant-scoped registration invitations.
|
|
type RegistrationInvitePersistenceInterface interface {
|
|
Create(tenantID types.TenantID, code *types.RegistrationInviteCode) error
|
|
CreateBatch(tenantID types.TenantID, batch *types.RegistrationInviteBatch, codes []types.RegistrationInviteCode) error
|
|
GetByCode(tenantID types.TenantID, code string) (*types.RegistrationInviteCode, error)
|
|
List(tenantID types.TenantID, opts types.RegistrationInviteListOptions) (*types.RegistrationInvitePage, error)
|
|
GetBatch(tenantID types.TenantID, batchID int64) (*types.RegistrationInviteBatch, error)
|
|
GetBatchByIdempotency(tenantID types.TenantID, key string) (*types.RegistrationInviteBatch, error)
|
|
Revoke(tenantID types.TenantID, codeID int64, now time.Time) error
|
|
RevokeBatch(tenantID types.TenantID, batchID int64, now time.Time) (int, error)
|
|
RecordUse(tenantID types.TenantID, code string, uid types.Uid, requestID string, now time.Time) error
|
|
SyncUsage(tenantID types.TenantID, codeID int64) error
|
|
}
|
|
|
|
type registrationInviteMapper struct{}
|
|
|
|
// RegistrationInvites is the tenant-scoped registration invitation store.
|
|
var RegistrationInvites RegistrationInvitePersistenceInterface
|
|
|
|
// UserSecurityPersistenceInterface defines user security state used to
|
|
// invalidate previously issued login tokens.
|
|
type UserSecurityPersistenceInterface interface {
|
|
GetAuthVersion(tenantID types.TenantID, uid types.Uid) (int, error)
|
|
BumpAuthVersion(tenantID types.TenantID, uid types.Uid, updatedAt time.Time) (int, error)
|
|
GetRealName(tenantID types.TenantID, uid types.Uid) (*types.RealNameVerification, error)
|
|
CreateRealNameSubmission(tenantID types.TenantID, rv *types.RealNameVerification) error
|
|
CreateRealNameAppeal(tenantID types.TenantID, appeal *types.RealNameAppeal) (*types.RealNameAppeal, error)
|
|
CreateActionToken(tenantID types.TenantID, token *types.ActionToken) error
|
|
ConsumeActionToken(tenantID types.TenantID, uid types.Uid, token, action, target string, now time.Time) (*types.ActionToken, error)
|
|
GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error)
|
|
UpsertIndependentPassword(tenantID types.TenantID, pass *types.IndependentPassword) error
|
|
UpdateIndependentPasswordAttempts(tenantID types.TenantID, uid types.Uid, purpose string, attempts int, lockedUntil *time.Time, updatedAt time.Time) error
|
|
GetYouthMode(tenantID types.TenantID, uid types.Uid) (*types.YouthModeSettings, error)
|
|
SetYouthMode(tenantID types.TenantID, uid types.Uid, enabled bool, policyVersion int, now time.Time) (*types.YouthModeSettings, error)
|
|
CreateSystemNotification(tenantID types.TenantID, notification *types.SystemNotification) error
|
|
ListSystemNotifications(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.SystemNotification, string, error)
|
|
CountUnreadSystemNotifications(tenantID types.TenantID, uid types.Uid) (int, error)
|
|
MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) error
|
|
}
|
|
|
|
type userSecurityMapper struct{}
|
|
|
|
// Security is a singleton for user security metadata.
|
|
var Security UserSecurityPersistenceInterface
|
|
|
|
func (userSecurityMapper) GetAuthVersion(tenantID types.TenantID, uid types.Uid) (int, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return tenantAdp.TenantUserSecurityGetAuthVersion(tenantID, uid)
|
|
}
|
|
|
|
func (userSecurityMapper) BumpAuthVersion(tenantID types.TenantID, uid types.Uid, updatedAt time.Time) (int, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return tenantAdp.TenantUserSecurityBumpAuthVersion(tenantID, uid, updatedAt)
|
|
}
|
|
|
|
func (userSecurityMapper) GetRealName(tenantID types.TenantID, uid types.Uid) (*types.RealNameVerification, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantRealNameGet(tenantID, uid)
|
|
}
|
|
|
|
func (userSecurityMapper) CreateRealNameSubmission(tenantID types.TenantID, rv *types.RealNameVerification) error {
|
|
if rv == nil || rv.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantRealNameCreate(tenantID, rv)
|
|
}
|
|
|
|
func (userSecurityMapper) CreateRealNameAppeal(tenantID types.TenantID, appeal *types.RealNameAppeal) (*types.RealNameAppeal, error) {
|
|
if appeal == nil || appeal.TenantID != tenantID || appeal.UserID.IsZero() || strings.TrimSpace(appeal.AppealID) == "" {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantRealNameAppealCreate(tenantID, appeal)
|
|
}
|
|
|
|
func (userSecurityMapper) CreateActionToken(tenantID types.TenantID, token *types.ActionToken) error {
|
|
if token == nil || token.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantActionTokenCreate(tenantID, token); err != nil {
|
|
return err
|
|
}
|
|
if cacheErr := CacheObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(token.UserID, token.TokenID, token.Action, token.Target), token); cacheErr != nil {
|
|
logs.Warn.Printf("cache action token[%s]: write failed: %v", token.TokenID, cacheErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (userSecurityMapper) ConsumeActionToken(tenantID types.TenantID, uid types.Uid, token, action, target string, now time.Time) (*types.ActionToken, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
consumed, err := tenantAdp.TenantActionTokenConsume(tenantID, uid, token, action, target, now)
|
|
if err == nil {
|
|
tokenID, _, _ := strings.Cut(token, ".")
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceActionToken, actionTokenCacheID(uid, tokenID, action, target))
|
|
}
|
|
return consumed, err
|
|
}
|
|
|
|
func (userSecurityMapper) GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantIndependentPasswordGet(tenantID, uid, purpose)
|
|
}
|
|
|
|
func (userSecurityMapper) UpsertIndependentPassword(tenantID types.TenantID, pass *types.IndependentPassword) error {
|
|
if pass == nil || pass.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantIndependentPasswordUpsert(tenantID, pass)
|
|
}
|
|
|
|
func (userSecurityMapper) UpdateIndependentPasswordAttempts(tenantID types.TenantID, uid types.Uid, purpose string, attempts int, lockedUntil *time.Time, updatedAt time.Time) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantIndependentPasswordUpdateAttempts(tenantID, uid, purpose, attempts, lockedUntil, updatedAt)
|
|
}
|
|
|
|
func (userSecurityMapper) GetYouthMode(tenantID types.TenantID, uid types.Uid) (*types.YouthModeSettings, error) {
|
|
if uid.IsZero() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantYouthModeGet(tenantID, uid)
|
|
}
|
|
|
|
func (userSecurityMapper) SetYouthMode(tenantID types.TenantID, uid types.Uid, enabled bool, policyVersion int, now time.Time) (*types.YouthModeSettings, error) {
|
|
if uid.IsZero() || policyVersion <= 0 {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantYouthModeSet(tenantID, uid, enabled, policyVersion, now)
|
|
}
|
|
|
|
func (userSecurityMapper) CreateSystemNotification(tenantID types.TenantID, notification *types.SystemNotification) error {
|
|
if notification == nil || notification.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantSystemNotificationCreate(tenantID, notification)
|
|
}
|
|
|
|
func (userSecurityMapper) ListSystemNotifications(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.SystemNotification, string, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return tenantAdp.TenantSystemNotificationList(tenantID, uid, cursor, limit)
|
|
}
|
|
|
|
func (userSecurityMapper) CountUnreadSystemNotifications(tenantID types.TenantID, uid types.Uid) (int, error) {
|
|
if uid.IsZero() {
|
|
return 0, types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return tenantAdp.TenantSystemNotificationUnreadCount(tenantID, uid)
|
|
}
|
|
|
|
func (userSecurityMapper) MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantSystemNotificationMarkRead(tenantID, uid, eventID, readAt)
|
|
}
|
|
|
|
func (verificationMapper) Create(tenantID types.TenantID, vc *types.VerificationCode) error {
|
|
if vc == nil || vc.TenantID != tenantID {
|
|
return types.ErrMalformed
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantVerificationCreate(tenantID, vc); err != nil {
|
|
return err
|
|
}
|
|
if cacheErr := cacheVerificationCode(tenantID, vc); cacheErr != nil {
|
|
logs.Warn.Printf("cache verification[%s]: write failed: %v", vc.ChallengeID, cacheErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (verificationMapper) Get(tenantID types.TenantID, challengeID string) (*types.VerificationCode, error) {
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp.TenantVerificationGet(tenantID, challengeID)
|
|
}
|
|
|
|
func (verificationMapper) GetActive(tenantID types.TenantID, scene, phoneHash string, now time.Time) (*types.VerificationCode, error) {
|
|
activeCacheID := verificationActiveCacheID(scene, phoneHash)
|
|
var challengeID string
|
|
if ok, err := GetCachedObject(tenantID, cacheNamespaceVerification, activeCacheID, &challengeID); err != nil {
|
|
logs.Warn.Printf("cache verification active[%s]: read failed: %v", activeCacheID, err)
|
|
} else if ok && challengeID != "" {
|
|
vc, err := Verifications.Get(tenantID, challengeID)
|
|
if err == nil && vc != nil && vc.Scene == scene && vc.PhoneHash == phoneHash &&
|
|
vc.State == types.VerificationStatePending && now.Before(vc.ExpiresAt) {
|
|
return vc, nil
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, activeCacheID)
|
|
}
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vc, err := tenantAdp.TenantVerificationGetActive(tenantID, scene, phoneHash, now)
|
|
if err == nil && vc != nil {
|
|
if cacheErr := cacheVerificationCode(tenantID, vc); cacheErr != nil {
|
|
logs.Warn.Printf("cache verification active[%s]: write failed: %v", activeCacheID, cacheErr)
|
|
}
|
|
}
|
|
return vc, err
|
|
}
|
|
|
|
func (verificationMapper) Fail(tenantID types.TenantID, challengeID string, state types.VerificationState) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantVerificationFail(tenantID, challengeID, state); err != nil {
|
|
return err
|
|
}
|
|
if vc, getErr := tenantAdp.TenantVerificationGet(tenantID, challengeID); getErr == nil && vc != nil {
|
|
_ = cacheVerificationCode(tenantID, vc)
|
|
} else {
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (verificationMapper) MarkVerified(tenantID types.TenantID, challengeID, tokenHash string, verifiedAt time.Time) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantVerificationMarkVerified(tenantID, challengeID, tokenHash, verifiedAt); err != nil {
|
|
return err
|
|
}
|
|
if vc, getErr := tenantAdp.TenantVerificationGet(tenantID, challengeID); getErr == nil && vc != nil {
|
|
_ = cacheVerificationCode(tenantID, vc)
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash))
|
|
} else {
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (verificationMapper) Consume(tenantID types.TenantID, challengeID, tokenHash string, consumedAt time.Time) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tenantAdp.TenantVerificationConsume(tenantID, challengeID, tokenHash, consumedAt); err != nil {
|
|
return err
|
|
}
|
|
if vc, getErr := tenantAdp.TenantVerificationGet(tenantID, challengeID); getErr == nil && vc != nil {
|
|
_ = cacheVerificationCode(tenantID, vc)
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash))
|
|
} else {
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, challengeID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cacheVerificationCode(tenantID types.TenantID, vc *types.VerificationCode) error {
|
|
if vc == nil {
|
|
return nil
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, vc.ChallengeID)
|
|
if vc.State == types.VerificationStatePending {
|
|
return CacheObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash), vc.ChallengeID)
|
|
}
|
|
_ = DeleteCachedObject(tenantID, cacheNamespaceVerification, verificationActiveCacheID(vc.Scene, vc.PhoneHash))
|
|
return nil
|
|
}
|
|
|
|
func verificationActiveCacheID(scene, phoneHash string) string {
|
|
return "active:" + strings.TrimSpace(scene) + ":" + strings.TrimSpace(phoneHash)
|
|
}
|
|
|
|
func actionTokenCacheID(uid types.Uid, tokenID, action, target string) string {
|
|
return uid.String() + ":" + strings.TrimSpace(tokenID) + ":" + strings.TrimSpace(action) + ":" + strings.TrimSpace(target)
|
|
}
|
|
|
|
func tenantRegistrationInviteAdapter(tenantID types.TenantID) (adapter.TenantRegistrationInviteAdapter, error) {
|
|
if !tenantID.IsValid() {
|
|
return nil, types.ErrMalformed
|
|
}
|
|
if router, ok := adp.(adapter.TenantRoutedAdapter); ok {
|
|
business, _, err := router.TenantAdapterFor(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if inviteAdp, ok := business.(adapter.TenantRegistrationInviteAdapter); ok {
|
|
return inviteAdp, nil
|
|
}
|
|
}
|
|
if inviteAdp, ok := adp.(adapter.TenantRegistrationInviteAdapter); ok {
|
|
return inviteAdp, nil
|
|
}
|
|
return nil, types.ErrUnsupported
|
|
}
|
|
|
|
func (registrationInviteMapper) Create(tenantID types.TenantID, code *types.RegistrationInviteCode) error {
|
|
if code == nil {
|
|
return types.ErrMalformed
|
|
}
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteCreate(tenantID, code)
|
|
}
|
|
|
|
func (registrationInviteMapper) CreateBatch(tenantID types.TenantID, batch *types.RegistrationInviteBatch, codes []types.RegistrationInviteCode) error {
|
|
if batch == nil || len(codes) == 0 {
|
|
return types.ErrMalformed
|
|
}
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteCreateBatch(tenantID, batch, codes)
|
|
}
|
|
|
|
func (registrationInviteMapper) GetByCode(tenantID types.TenantID, code string) (*types.RegistrationInviteCode, error) {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteGetByCode(tenantID, code)
|
|
}
|
|
|
|
func (registrationInviteMapper) List(tenantID types.TenantID, opts types.RegistrationInviteListOptions) (*types.RegistrationInvitePage, error) {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteList(tenantID, opts)
|
|
}
|
|
|
|
func (registrationInviteMapper) GetBatch(tenantID types.TenantID, batchID int64) (*types.RegistrationInviteBatch, error) {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteBatchGet(tenantID, batchID)
|
|
}
|
|
|
|
func (registrationInviteMapper) GetBatchByIdempotency(tenantID types.TenantID, key string) (*types.RegistrationInviteBatch, error) {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteBatchGetByIdempotency(tenantID, key)
|
|
}
|
|
|
|
func (registrationInviteMapper) Revoke(tenantID types.TenantID, codeID int64, now time.Time) error {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteRevoke(tenantID, codeID, now)
|
|
}
|
|
|
|
func (registrationInviteMapper) RevokeBatch(tenantID types.TenantID, batchID int64, now time.Time) (int, error) {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteBatchRevoke(tenantID, batchID, now)
|
|
}
|
|
|
|
func (registrationInviteMapper) RecordUse(tenantID types.TenantID, code string, uid types.Uid, requestID string, now time.Time) error {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteRecordUse(tenantID, code, uid, requestID, now)
|
|
}
|
|
|
|
func (registrationInviteMapper) SyncUsage(tenantID types.TenantID, codeID int64) error {
|
|
inviteAdp, err := tenantRegistrationInviteAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return inviteAdp.TenantRegistrationInviteSyncUsage(tenantID, codeID)
|
|
}
|
|
|
|
// Get reads a persistent cache entry.
|
|
func (pcacheMapper) Get(tenantID types.TenantID, key string) (string, error) {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return tenantAdp.TenantPCacheGet(tenantID, key)
|
|
}
|
|
|
|
// Upsert creates or updates a persistent cache entry.
|
|
func (pcacheMapper) Upsert(tenantID types.TenantID, key string, value string, failOnDuplicate bool) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantPCacheUpsert(tenantID, key, value, failOnDuplicate)
|
|
}
|
|
|
|
// Delete deletes a single persistent cache entry.
|
|
func (pcacheMapper) Delete(tenantID types.TenantID, key string) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantPCacheDelete(tenantID, key)
|
|
}
|
|
|
|
// Expire expires older entries with the specified key prefix.
|
|
func (pcacheMapper) Expire(tenantID types.TenantID, keyPrefix string, olderThan time.Time) error {
|
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tenantAdp.TenantPCacheExpire(tenantID, keyPrefix, olderThan)
|
|
}
|
|
|
|
func SetTestUidGenerator(g types.UidGenerator) {
|
|
uGen = g
|
|
}
|
|
|
|
func init() {
|
|
Store = storeObj{}
|
|
Tenants = tenantsMapper{}
|
|
RegistrationConfig = registrationConfigMapper{}
|
|
TenantEntry = tenantEntryMapper{}
|
|
InternalAdminQueries = internalAdminQueryMapper{}
|
|
Users = usersMapper{}
|
|
Topics = topicsMapper{}
|
|
Subs = subsMapper{}
|
|
Messages = messagesMapper{}
|
|
Conversations = conversationsMapper{}
|
|
Calls = callsMapper{}
|
|
GroupBusiness = groupBusinessMapper{}
|
|
Workbench = workbenchMapper{}
|
|
Organization = organizationMapper{}
|
|
Meetings = meetingsMapper{}
|
|
DailyReports = dailyReportsMapper{}
|
|
MonthlyReports = monthlyReportsMapper{}
|
|
Mailbox = mailboxMapper{}
|
|
Profiles = profileMapper{}
|
|
AppReleases = appReleaseMapper{}
|
|
DevicePushTokens = devicePushTokenMapper{}
|
|
Safety = safetyMapper{}
|
|
Devices = deviceMapper{}
|
|
Files = fileMapper{}
|
|
PCache = pcacheMapper{}
|
|
Verifications = verificationMapper{}
|
|
RegistrationInvites = registrationInviteMapper{}
|
|
Security = userSecurityMapper{}
|
|
Relations = relationsMapper{}
|
|
Favorites = favoritesMapper{}
|
|
}
|