Files
im_server/server/store/store.go
T

5199 lines
189 KiB
Go

// Package store provides methods for registering and accessing database adapters.
package store
import (
"context"
"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
HealthCheck(context.Context) error
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)
LatestAppRelease(platform, channel string) (*types.AppRelease, error)
ModerationProvider() (*types.ModerationProviderConfig, error)
ModerationPolicy(tenantCode string) (*types.ModerationPolicy, error)
SensitiveWords(tenantCode string) (*types.SensitiveWordSet, error)
}
var adminInternalConfigProvider AdminInternalConfigProvider
func SetAdminInternalConfigProvider(provider AdminInternalConfigProvider) {
adminInternalConfigProvider = provider
}
type ModerationConfigPersistenceInterface interface {
Provider() (*types.ModerationProviderConfig, error)
Policy(tenantCode string) (*types.ModerationPolicy, error)
Words(tenantCode string) (*types.SensitiveWordSet, error)
}
type moderationConfigMapper struct{}
var ModerationConfig ModerationConfigPersistenceInterface = moderationConfigMapper{}
func (moderationConfigMapper) Provider() (*types.ModerationProviderConfig, error) {
if value, ok := moderationProviderCache.get("provider"); ok {
return value, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
value, err := adminInternalConfigProvider.ModerationProvider()
if err == nil && value != nil {
moderationProviderCache.set("provider", value)
}
return value, err
}
func (moderationConfigMapper) Policy(tenantCode string) (*types.ModerationPolicy, error) {
tenantCode = strings.TrimSpace(tenantCode)
if value, ok := moderationPolicyCache.get(tenantCode); ok {
return value, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
value, err := adminInternalConfigProvider.ModerationPolicy(tenantCode)
if err == nil && value != nil {
moderationPolicyCache.set(tenantCode, value)
}
return value, err
}
func (moderationConfigMapper) Words(tenantCode string) (*types.SensitiveWordSet, error) {
tenantCode = strings.TrimSpace(tenantCode)
if value, ok := moderationWordsCache.get(tenantCode); ok {
return value, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
value, err := adminInternalConfigProvider.SensitiveWords(tenantCode)
if err == nil && value != nil {
moderationWordsCache.set(tenantCode, value)
}
return value, err
}
// 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
}
tenantAdp, ok := adp.(adapter.TenantAdapter)
if ok {
if tenant, err := tenantAdp.TenantGetByCode(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 && !errors.Is(err, types.ErrNotFound) {
logs.Warn.Printf("local tenant by code lookup failed: code=%s err=%v", code, err)
}
}
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)
}
}
if !ok {
return nil, types.ErrUnsupported
}
return tenantAdp.TenantGetByCode(code)
}
// 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
}
tenantAdp, ok := adp.(adapter.TenantAdapter)
if ok {
if tenant, err := tenantAdp.TenantGetByID(id); err == nil && tenant != nil {
tenantByIDCache.set(idKey, cloneTenant(tenant))
tenantByCodeCache.set(tenant.Code, cloneTenant(tenant))
return tenant, nil
} else if err != nil && !errors.Is(err, types.ErrNotFound) {
logs.Warn.Printf("local tenant by id lookup failed: tenant=%d err=%v", id, err)
}
}
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)
}
}
if !ok {
return nil, types.ErrUnsupported
}
return tenantAdp.TenantGetByID(id)
}
func (tenantsMapper) ListActive(limit int) ([]types.Tenant, error) {
if limit <= 0 {
limit = 1000
}
tenantAdp, ok := adp.(adapter.TenantAdapter)
if ok {
if tenants, err := tenantAdp.TenantListActive(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("local active tenants lookup failed: err=%v", err)
}
}
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)
}
}
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
}
// HealthCheck verifies that the persistent storage adapter can execute a real operation.
func (storeObj) HealthCheck(ctx context.Context) error {
if adp == nil || !adp.IsOpen() {
return errors.New("store is not open")
}
if checker, ok := adp.(interface {
HealthCheck(context.Context) error
}); ok {
return checker.HealthCheck(ctx)
}
return errors.New("health check is not supported by " + adp.GetName())
}
// 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
BumpMutationVersion(tenantID types.TenantID, topic string, at time.Time) (int64, 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
}
func (topicsMapper) BumpMutationVersion(tenantID types.TenantID, topic string, at time.Time) (int64, error) {
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return 0, err
}
version, err := tenantAdp.TenantTopicBumpMutationVersion(tenantID, topic, at)
if err == nil {
_ = DeleteCachedObject(tenantID, cacheNamespaceTopic, topic)
}
return version, err
}
// 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)
if hard && types.GetTopicCat(topic) == types.TopicCatGrp {
_ = DeleteGroupMessageShardRoutes(tenantID, 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)
Mutations(tenantID types.TenantID, topic string, forUser types.Uid, sinceVersion int64, limit int) ([]types.MessageMutation, string, bool, int64, 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)
Dissolve(types.TenantID, string, types.Uid, string, time.Time) (*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)
GetAnnouncement(types.TenantID, string, 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)
GetMute(types.TenantID, string, types.Uid) (*types.GroupMute, 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)
}
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)
if delErr := DeleteCachedObject(tenantID, cacheNamespaceGroupSettings, topic); delErr != nil {
logs.Warn.Printf("cache group settings[%s]: delete corrupted cache failed: %v", topic, delErr)
}
} 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))
}
if patch.MuteAll != nil {
invalidateGroupMemberPages(tenantID, topic)
invalidateGroupMemberObjects(tenantID, topic)
invalidateTopicSubscriptions(tenantID, topic)
}
}
return settings, err
}
func (groupBusinessMapper) Dissolve(tenantID types.TenantID, topic string, actor types.Uid, reason string, now time.Time) (*types.GroupSettings, error) {
if strings.TrimSpace(topic) == "" || actor.IsZero() {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
settings, err := tenantAdp.TenantGroupDissolve(tenantID, strings.TrimSpace(topic), actor, reason, now)
if err == nil {
if settings != nil {
_ = CacheObject(tenantID, cacheNamespaceGroupSettings, strings.TrimSpace(topic), settings)
} else {
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupSettings, strings.TrimSpace(topic))
}
invalidateGroupMemberPages(tenantID, topic)
invalidateGroupMemberObjects(tenantID, topic)
invalidateTopicSubscriptions(tenantID, topic)
// The dissolve flips im_topics.state to StateDeleted. The cached topic
// object must be dropped too, otherwise a later Topics.Get (e.g. from
// initTopicGrp when a former member re-subscribes) would restore the
// dissolved group from the stale cache.
_ = DeleteCachedObject(tenantID, cacheNamespaceTopic, 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
}
topic = strings.TrimSpace(topic)
opts.Role = strings.TrimSpace(opts.Role)
opts.EligibleRole = strings.TrimSpace(opts.EligibleRole)
opts.Query = strings.TrimSpace(opts.Query)
opts.Cursor = strings.TrimSpace(opts.Cursor)
cacheKey := groupMemberPageCacheID(topic, opts)
if cached, ok := GetCachedGroupMemberPage(tenantID, cacheKey); ok {
return cached.Items, cached.NextCursor, nil
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
members, nextCursor, err := tenantAdp.TenantGroupMemberList(tenantID, topic, opts)
if err != nil {
return nil, "", err
}
CacheGroupMemberPage(tenantID, cacheKey, groupMemberPageCache{
Items: members,
NextCursor: nextCursor,
})
return members, nextCursor, nil
}
type groupMemberPageCache struct {
Items []types.GroupMember `json:"items"`
NextCursor string `json:"next_cursor"`
}
func groupMemberPageCacheID(topic string, opts types.GroupMemberListOptions) string {
limit := opts.Limit
if limit <= 0 || limit > 100 {
limit = 30
}
return strings.TrimSpace(topic) +
"|role=" + strconv.Quote(strings.TrimSpace(opts.Role)) +
"|eligible_role=" + strconv.Quote(strings.TrimSpace(opts.EligibleRole)) +
"|query=" + strconv.Quote(strings.TrimSpace(opts.Query)) +
"|cursor=" + strconv.Quote(strings.TrimSpace(opts.Cursor)) +
"|limit=" + strconv.Itoa(limit) +
"|mentionable=" + strconv.FormatBool(opts.Mentionable)
}
func CacheGroupMemberPage(tenantID types.TenantID, key string, payload groupMemberPageCache) {
if strings.TrimSpace(key) == "" {
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) (groupMemberPageCache, bool) {
if strings.TrimSpace(key) == "" {
return groupMemberPageCache{}, false
}
var payload groupMemberPageCache
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 groupMemberPageCache{}, false
}
func invalidateGroupMemberPages(tenantID types.TenantID, topic string) {
topic = strings.TrimSpace(topic)
if topic == "" {
return
}
if err := DeleteCachedObjectsByPrefix(tenantID, cacheNamespaceGroupPage, topic+"|"); err != nil {
logs.Warn.Printf("cache group member pages[%s]: delete failed: %v", topic, err)
}
}
func invalidateGroupSettings(tenantID types.TenantID, topic string) {
topic = strings.TrimSpace(topic)
if topic == "" {
return
}
if err := DeleteCachedObject(tenantID, cacheNamespaceGroupSettings, topic); err != nil {
logs.Warn.Printf("cache group settings[%s]: delete failed: %v", topic, err)
}
}
func invalidateGroupMemberObjects(tenantID types.TenantID, topic string) {
topic = strings.TrimSpace(topic)
if topic == "" {
return
}
if err := DeleteCachedObjectsByPrefix(tenantID, cacheNamespaceGroupMember, topic+":"); err != nil {
logs.Warn.Printf("cache group members[%s]: delete failed: %v", topic, err)
}
}
func invalidateTopicSubscriptions(tenantID types.TenantID, topic string) {
topic = strings.TrimSpace(topic)
if topic == "" {
return
}
if err := DeleteCachedObjectsByPrefix(tenantID, cacheNamespaceSub, topic+":"); err != nil {
logs.Warn.Printf("cache subscriptions[%s]: delete failed: %v", topic, err)
}
if channel := types.GrpToChn(topic); channel != topic {
if err := DeleteCachedObjectsByPrefix(tenantID, cacheNamespaceSub, channel+":"); err != nil {
logs.Warn.Printf("cache subscriptions[%s]: delete failed: %v", channel, err)
}
}
}
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))
}
invalidateGroupMemberPages(tenantID, topic)
}
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
}
invitation, err := tenantAdp.TenantGroupInvitationAccept(tenantID, strings.TrimSpace(invitationID), uid, memberPrivate, now)
if err == nil && invitation != nil {
invalidateGroupMemberPages(tenantID, invitation.Topic)
invalidateGroupSettings(tenantID, invitation.Topic)
}
return invitation, err
}
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
}
resolved, err := tenantAdp.TenantGroupJoinRequestResolve(tenantID, strings.TrimSpace(topic), strings.TrimSpace(requestID), actor, strings.TrimSpace(action), strings.TrimSpace(rejectReason), version, memberPrivate, now)
if err == nil && resolved != nil && resolved.Status == types.GroupJoinRequestStatusAccepted {
invalidateGroupMemberPages(tenantID, topic)
invalidateGroupSettings(tenantID, topic)
}
return resolved, err
}
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) GetAnnouncement(tenantID types.TenantID, topic, announcementID string) (*types.GroupAnnouncement, error) {
if strings.TrimSpace(topic) == "" || strings.TrimSpace(announcementID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantGroupAnnouncementGet(tenantID, strings.TrimSpace(topic), strings.TrimSpace(announcementID))
}
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) GetMute(tenantID types.TenantID, topic string, uid types.Uid) (*types.GroupMute, error) {
if strings.TrimSpace(topic) == "" || uid.IsZero() {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantGroupMuteGet(tenantID, strings.TrimSpace(topic), uid)
}
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
}
updated, err := tenantAdp.TenantGroupMuteUpsert(tenantID, mute)
if err == nil {
invalidateGroupMemberPages(tenantID, mute.Topic)
}
return updated, err
}
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
}
if err := tenantAdp.TenantGroupMuteDelete(tenantID, strings.TrimSpace(topic), uid, actor, now); err != nil {
return err
}
invalidateGroupMemberPages(tenantID, topic)
return nil
}
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
}
created, err := tenantAdp.TenantGroupInviteMember(tenantID, strings.TrimSpace(topic), inviter, target,
strings.TrimSpace(message), strings.TrimSpace(idempotencyKey), private, now)
if err == nil && created {
invalidateGroupMemberPages(tenantID, topic)
invalidateGroupSettings(tenantID, topic)
}
return created, err
}
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))
invalidateGroupMemberPages(tenantID, topic)
invalidateGroupSettings(tenantID, topic)
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))
invalidateGroupMemberPages(tenantID, topic)
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
}
topic = strings.TrimSpace(topic)
if err = tenantAdp.TenantGroupOwnerTransfer(tenantID, topic, previousOwner, newOwner, version, now); err != nil {
return err
}
for _, uid := range []types.Uid{previousOwner, newOwner} {
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupMember, groupMemberCacheID(topic, uid))
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(topic, uid, false))
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(topic, uid, true))
}
// im_topics.owner is updated by the transfer; drop the cached topic
// object so a later Topics.Get sees the new owner.
_ = DeleteCachedObject(tenantID, cacheNamespaceTopic, topic)
invalidateGroupMemberPages(tenantID, topic)
invalidateGroupSettings(tenantID, topic)
return nil
}
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 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 AttendancePersistenceInterface interface {
Config(tenantID types.TenantID, uid types.Uid, date time.Time) (*types.AttendanceConfig, error)
ListRecords(tenantID types.TenantID, uid types.Uid, opts types.AttendanceRecordListOptions) ([]types.AttendanceRecord, string, *types.AttendanceRecordStats, error)
Checkin(tenantID types.TenantID, uid types.Uid, checkinID string, req types.AttendanceCheckinRequest) (*types.AttendanceRecord, *types.AttendanceCheckin, error)
CreateAppeal(tenantID types.TenantID, uid types.Uid, appeal *types.AttendanceAppeal, idempotencyKey string) (*types.AttendanceAppeal, error)
ListAppeals(tenantID types.TenantID, uid types.Uid, opts types.AttendanceAppealListOptions) ([]types.AttendanceAppeal, string, error)
}
type attendanceMapper struct{}
var Attendance AttendancePersistenceInterface
func (attendanceMapper) Config(tenantID types.TenantID, uid types.Uid, date time.Time) (*types.AttendanceConfig, error) {
if uid.IsZero() || date.IsZero() {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantAttendanceConfig(tenantID, uid, date)
}
func (attendanceMapper) ListRecords(tenantID types.TenantID, uid types.Uid, opts types.AttendanceRecordListOptions) ([]types.AttendanceRecord, string, *types.AttendanceRecordStats, error) {
if uid.IsZero() {
return nil, "", nil, types.ErrMalformed
}
if opts.Limit <= 0 || opts.Limit > 100 {
opts.Limit = 50
}
opts.Scope = strings.TrimSpace(opts.Scope)
if opts.Scope == "" {
opts.Scope = "mine"
}
if opts.Scope != "mine" && opts.Scope != "managed" {
return nil, "", nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", nil, err
}
return tenantAdp.TenantAttendanceRecordList(tenantID, uid, opts)
}
func (attendanceMapper) Checkin(tenantID types.TenantID, uid types.Uid, checkinID string, req types.AttendanceCheckinRequest) (*types.AttendanceRecord, *types.AttendanceCheckin, error) {
if uid.IsZero() || strings.TrimSpace(checkinID) == "" || req.Date.IsZero() || req.ClientTime.IsZero() || !validAttendanceAction(req.Action) {
return nil, nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, nil, err
}
return tenantAdp.TenantAttendanceCheckin(tenantID, uid, strings.TrimSpace(checkinID), req)
}
func (attendanceMapper) CreateAppeal(tenantID types.TenantID, uid types.Uid, appeal *types.AttendanceAppeal, idempotencyKey string) (*types.AttendanceAppeal, error) {
if uid.IsZero() || appeal == nil || strings.TrimSpace(appeal.AppealID) == "" || strings.TrimSpace(appeal.RecordID) == "" || strings.TrimSpace(appeal.Reason) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantAttendanceAppealCreate(tenantID, uid, appeal, strings.TrimSpace(idempotencyKey))
}
func (attendanceMapper) ListAppeals(tenantID types.TenantID, uid types.Uid, opts types.AttendanceAppealListOptions) ([]types.AttendanceAppeal, string, error) {
if uid.IsZero() {
return nil, "", types.ErrMalformed
}
if opts.Limit <= 0 || opts.Limit > 100 {
opts.Limit = 50
}
opts.Scope = strings.TrimSpace(opts.Scope)
if opts.Scope == "" {
opts.Scope = "mine"
}
if opts.Scope != "mine" && opts.Scope != "managed" {
return nil, "", types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantAttendanceAppealList(tenantID, uid, opts)
}
func validAttendanceAction(action string) bool {
switch strings.TrimSpace(action) {
case types.AttendanceActionCheckIn, types.AttendanceActionCheckOut:
return true
default:
return false
}
}
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 MeetingNotesPersistenceInterface interface {
List(tenantID types.TenantID, uid types.Uid, query, cursor string, limit int) ([]types.Meeting, string, error)
ListFiles(tenantID types.TenantID, uid types.Uid, meetingID string) ([]types.MeetingNoteFile, error)
CreateFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string) (*types.MeetingNoteFile, error)
ReplaceFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error)
DeleteFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error)
}
type meetingsMapper struct{}
type meetingNotesMapper struct{}
var Meetings MeetingsPersistenceInterface
var MeetingNotes MeetingNotesPersistenceInterface
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 (meetingNotesMapper) List(tenantID types.TenantID, uid types.Uid, query, 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.TenantMeetingNoteList(tenantID, uid, strings.TrimSpace(query), strings.TrimSpace(cursor), limit)
}
func (meetingNotesMapper) ListFiles(tenantID types.TenantID, uid types.Uid, meetingID string) ([]types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFiles(tenantID, uid, strings.TrimSpace(meetingID))
}
func (meetingNotesMapper) CreateFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string) (*types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || strings.TrimSpace(noteFileID) == "" || strings.TrimSpace(fileID) == "" {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFileCreate(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(noteFileID), strings.TrimSpace(fileID), strings.TrimSpace(idempotencyKey))
}
func (meetingNotesMapper) ReplaceFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, fileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || strings.TrimSpace(noteFileID) == "" || strings.TrimSpace(fileID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFileReplace(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(noteFileID), strings.TrimSpace(fileID), strings.TrimSpace(idempotencyKey), version)
}
func (meetingNotesMapper) DeleteFile(tenantID types.TenantID, uid types.Uid, meetingID, noteFileID, idempotencyKey string, version int) (*types.MeetingNoteFile, error) {
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || strings.TrimSpace(noteFileID) == "" || version <= 0 {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMeetingNoteFileDelete(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(noteFileID), strings.TrimSpace(idempotencyKey), 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.RecorderID == 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(platform, channel string) (*types.AppRelease, error)
}
type appReleaseMapper struct{}
var AppReleases AppReleasePersistenceInterface
func (appReleaseMapper) Latest(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
}
cacheKey := platform + ":" + channel
if cached, ok := appReleaseConfigCache.get(cacheKey); ok {
copy := *cached
return &copy, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
release, err := adminInternalConfigProvider.LatestAppRelease(platform, channel)
if err != nil {
return nil, err
}
if release == nil {
return nil, types.ErrNotFound
}
copy := *release
appReleaseConfigCache.set(cacheKey, &copy)
return release, nil
}
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 DevicePushTokenProviderQueryInterface interface {
GetAllByProvider(tenantID types.TenantID, provider string, 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...)
}
func (devicePushTokenMapper) GetAllByProvider(tenantID types.TenantID, provider string, uids ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
provider = strings.ToLower(strings.TrimSpace(provider))
if provider == "" {
return map[types.Uid][]types.DeviceDef{}, 0, nil
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, 0, err
}
reader, ok := tenantAdp.(interface {
TenantDevicePushTokenGetAllByProvider(types.TenantID, string, ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error)
})
if !ok {
return map[types.Uid][]types.DeviceDef{}, 0, nil
}
return reader.TenantDevicePushTokenGetAllByProvider(tenantID, provider, 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
}
profile, err := tenantAdp.TenantUserProfileUpdate(tenantID, uid, patch)
if err != nil {
return nil, err
}
refreshUserProfileProjections(tenantID, uid, profile)
return profile, nil
}
func refreshUserProfileProjections(tenantID types.TenantID, uid types.Uid, profile *types.UserProfile) {
_ = DeleteCachedObject(tenantID, cacheNamespaceUser, uid.String())
if profile == nil {
return
}
public := userProfilePublicProjection(profile)
if user, err := Users.Get(tenantID, uid); err != nil {
logs.Warn.Printf("profile %s: load native user projection failed: %v", uid, err)
} else if user != nil {
merged := make(map[string]any)
if encoded, marshalErr := json.Marshal(user.Public); marshalErr == nil {
_ = json.Unmarshal(encoded, &merged)
}
for key, value := range public {
merged[key] = value
}
if err = Users.Update(tenantID, uid, map[string]any{"Public": merged}); err != nil {
logs.Warn.Printf("profile %s: update native user projection failed: %v", uid, err)
} else {
public = merged
}
}
cursor := ""
for {
items, nextCursor, err := Relations.ListFriends(tenantID, uid, "", "", cursor, 100)
if err != nil {
logs.Warn.Printf("profile %s: refresh friend subscription projections failed: %v", uid, err)
break
}
for _, item := range items {
if item.Friendship.Topic == "" || item.Friendship.PeerID.IsZero() {
continue
}
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(item.Friendship.Topic, item.Friendship.PeerID, false))
_ = DeleteCachedObject(tenantID, cacheNamespaceSub, subscriptionCacheID(item.Friendship.Topic, item.Friendship.PeerID, true))
}
if nextCursor == "" {
break
}
cursor = nextCursor
}
if topics, err := Users.GetTopics(tenantID, uid, &types.QueryOpt{Limit: 1000}); err != nil {
logs.Warn.Printf("profile %s: refresh group member projections failed: %v", uid, err)
} else {
for _, sub := range topics {
if types.GetTopicCat(sub.Topic) != types.TopicCatGrp {
continue
}
if err := Subs.Update(tenantID, sub.Topic, uid, map[string]any{"Public": public}); err != nil {
logs.Warn.Printf("profile %s: refresh group subscription %s public failed: %v", uid, sub.Topic, err)
}
invalidateGroupMemberPages(tenantID, sub.Topic)
}
}
}
func userProfilePublicProjection(profile *types.UserProfile) map[string]any {
nickname := strings.TrimSpace(profile.Nickname)
public := map[string]any{
"display_name": nickname,
"nickname": nickname,
"fn": nickname,
"name": nickname,
"avatar_file_id": strings.TrimSpace(profile.AvatarFileID),
"signature": strings.TrimSpace(profile.Signature),
"gender": strings.TrimSpace(profile.Gender),
"region": strings.TrimSpace(profile.Region),
"public_account": strings.TrimSpace(profile.PublicAccount),
}
return public
}
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 && patch.GroupInviteVerification == 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
}
if patch.Region != nil && len([]rune(strings.TrimSpace(*patch.Region))) > 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
}
if peer == nil {
return nil, types.ErrNotFound
}
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 {
if friendship, err := tenantAdp.TenantFriendshipGet(tenantID, userID, peerID); err != nil {
logs.Warn.Printf("friendship[%s:%s]: read failed after cache hit: %v", userID.UserId(), peerID.UserId(), err)
} else if friendship != nil && friendship.Status == types.RelationshipFriend {
ctx.Remark = friendship.Remark
if friendship.Topic != "" {
ctx.Topic = friendship.Topic
}
}
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
ctx.Remark = friendship.Remark
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
msg.InitTimes()
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, shardMonth, err := nextMessageSeqWithFloor(tenantID, msg.Topic, msg.SeqId-1, msg.CreatedAt.UTC().Format("200601"))
if err != nil {
if clientMsgID != "" {
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
}
return nil, err
}
msg.SeqId = seq
msg.ShardMonth = shardMonth
}
if isGroupMessage && msg.ShardMonth == "" {
msg.ShardMonth = msg.CreatedAt.UTC().Format("200601")
}
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.ShardMonth = duplicate.ShardMonth
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.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 != nil {
if err == types.ErrDuplicate {
if isGroupMessage && clientMsgID != "" {
duplicate, dupErr := tenantAdp.TenantGroupMessageGetByClientID(tenantID, msg.Topic, fromUid, clientMsgID)
if dupErr != nil {
return nil, dupErr
}
if duplicate != nil {
msg.SeqId = duplicate.SeqId
msg.ShardMonth = duplicate.ShardMonth
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 && clientMsgID != "" {
duplicate, dupErr := tenantAdp.TenantMessageGetByClientID(tenantID, fromUid, clientMsgID)
if dupErr != nil {
return nil, dupErr
}
if duplicate != nil {
copyPersistedMessage(msg, duplicate)
return &types.MessageSaveResult{SeqId: duplicate.SeqId, Duplicate: true}, nil
}
}
if retryErr := retryTenantMessageSaveAfterSeqConflict(tenantAdp, tenantID, msg, isGroupMessage); retryErr != nil {
if isGroupMessage && groupMsgRuntime != nil && clientMsgID != "" {
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
}
return nil, retryErr
}
err = nil
} else if isGroupMessage && groupMsgRuntime != nil && clientMsgID != "" {
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
}
if err != nil {
return nil, err
}
}
if err = tenantAdp.TenantTopicUpdateOnMessage(tenantID, msg.Topic, msg); err != nil {
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.PutShardRoute(tenantID, msg.Topic, GroupMessageShardRoute{
Month: msg.ShardMonth, MinSeq: msg.SeqId, MaxSeq: msg.SeqId, Count: 1,
}); err != nil {
logs.Warn.Printf("topic[%s]: failed to update group message shard route: %v", msg.Topic, err)
}
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 copyPersistedMessage(dst, src *types.Message) {
dst.ObjHeader = src.ObjHeader
dst.DeletedAt = src.DeletedAt
dst.DelId = src.DelId
dst.DeletedFor = src.DeletedFor
dst.SeqId = src.SeqId
dst.Topic = src.Topic
dst.ShardMonth = src.ShardMonth
dst.From = src.From
dst.ClientMsgID = src.ClientMsgID
dst.Head = src.Head
dst.Content = src.Content
}
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 NormalizeClientMsgID(head types.KVMap) (string, error) {
return normalizeClientMsgID(head)
}
func retryTenantMessageSaveAfterSeqConflict(tenantAdp adapter.TenantBusinessAdapter, tenantID types.TenantID, msg *types.Message, isGroupMessage bool) error {
var latest []types.Message
var err error
if isGroupMessage {
latest, err = tenantAdp.TenantGroupMessageGetAll(tenantID, msg.Topic, types.ZeroUid, &types.QueryOpt{Before: 1 << 62, Limit: 1})
} else {
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
}
prevSeq := msg.SeqId
msg.SeqId = latest[0].SeqId + 1
if err = tenantAdp.TenantTopicUpdateOnMessage(tenantID, msg.Topic, msg); err != nil {
return err
}
if isGroupMessage {
err = tenantAdp.TenantGroupMessageSave(tenantID, msg)
} else {
err = tenantAdp.TenantMessageSave(tenantID, msg)
}
if err != nil {
return err
}
// The re-allocated seq differs from the one handed out before the insert
// failed. Sync the Redis allocator and the idempotency record with the
// final seq, otherwise subsequent publishes keep colliding with the
// persisted row and resends of the same client mid return the abandoned
// seq forever.
BumpMessageSeqFloor(tenantID, msg.Topic, msg.SeqId)
if isGroupMessage {
// Drop the pending hot-cache entry the producer wrote for the
// abandoned seq so hot-range reads cannot surface a phantom message.
if err := InvalidateHotMessages(tenantID, msg.Topic, []types.Range{{Low: prevSeq, Hi: prevSeq + 1}}); err != nil {
logs.Warn.Printf("topic[%s]: failed to invalidate abandoned hot message seq=%d: %v", msg.Topic, prevSeq, err)
}
}
if msg.ClientMsgID != "" {
if fromUid := types.ParseUid(msg.From); !fromUid.IsZero() {
_ = MarkMessageUniqueDone(tenantID, msg.Topic, fromUid, msg.ClientMsgID, msg.SeqId, msg.Uid().String())
}
}
return nil
}
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_notice", "system_action", "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)
}
if delID == 0 {
if routeErr := groupMsgRuntime.DeleteShardRoutes(tenantID, topic); routeErr != nil {
logs.Warn.Printf("topic[%s]: failed to delete group message shard route: %v", topic, routeErr)
}
}
}
// 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)
}
func (messagesMapper) Mutations(tenantID types.TenantID, topic string, forUser types.Uid, sinceVersion int64, limit int) ([]types.MessageMutation, string, bool, int64, error) {
if topic == "" || forUser.IsZero() || sinceVersion < 0 {
return nil, "", false, 0, types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", false, 0, err
}
return tenantAdp.TenantMessageMutations(tenantID, topic, forUser, sinceVersion, 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 {
// Convert attachment URLs to file IDs.
var fids []string
for _, url := range attachments {
raw := strings.TrimSpace(url)
if raw == "" {
continue
}
if mediaHandler != nil {
if fid := mediaHandler.GetIdFromUrl(raw); !fid.IsZero() {
fids = append(fids, fid.String())
continue
}
}
if fid := media.GetIdFromUrl(raw, ""); !fid.IsZero() {
fids = append(fids, fid.String())
continue
}
if fid := types.ParseUid(raw); !fid.IsZero() {
fids = append(fids, fid.String())
}
}
if len(fids) > 0 {
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
}
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)
CountUnreadSystemNotificationsByEventPrefix(tenantID types.TenantID, uid types.Uid, eventPrefix string) (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) CountUnreadSystemNotificationsByEventPrefix(tenantID types.TenantID, uid types.Uid, eventPrefix string) (int, error) {
if uid.IsZero() || strings.TrimSpace(eventPrefix) == "" {
return 0, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return 0, err
}
return tenantAdp.TenantSystemNotificationUnreadCountByEventPrefix(tenantID, uid, eventPrefix)
}
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{}
SecurityAudit = securityAuditMapper{}
Users = usersMapper{}
Topics = topicsMapper{}
Subs = subsMapper{}
Messages = messagesMapper{}
Conversations = conversationsMapper{}
Calls = callsMapper{}
GroupBusiness = groupBusinessMapper{}
Workbench = workbenchMapper{}
Organization = organizationMapper{}
Attendance = attendanceMapper{}
Meetings = meetingsMapper{}
MeetingNotes = meetingNotesMapper{}
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{}
}