feat: add im tenant db routing and chapter seven flows

This commit is contained in:
richjohnrambo
2026-08-13 11:30:48 +08:00
parent 774a448c12
commit 31f82d9c41
19 changed files with 5261 additions and 47 deletions
+31
View File
@@ -1144,6 +1144,22 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) GetDeleted(tenantID, top
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeleted", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).GetDeleted), tenantID, topic, forUser, opt)
}
// Search mocks base method.
func (m *MockMessagesPersistenceInterface) Search(tenantID types.TenantID, topic string, forUser types.Uid, query, messageType, cursor string, limit int) ([]types.Message, string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", tenantID, topic, forUser, query, messageType, cursor, limit)
ret0, _ := ret[0].([]types.Message)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// Search indicates an expected call of Search.
func (mr *MockMessagesPersistenceInterfaceMockRecorder) Search(tenantID, topic, forUser, query, messageType, cursor, limit interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).Search), tenantID, topic, forUser, query, messageType, cursor, limit)
}
// GetOne mocks base method.
func (m *MockMessagesPersistenceInterface) GetOne(tenantID types.TenantID, topic string, seq int) (*types.Message, error) {
m.ctrl.T.Helper()
@@ -1342,6 +1358,21 @@ func (mr *MockFilePersistenceInterfaceMockRecorder) LinkAttachments(tenantID, to
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkAttachments", reflect.TypeOf((*MockFilePersistenceInterface)(nil).LinkAttachments), tenantID, topic, msgId, attachments)
}
// UserHasAccess mocks base method.
func (m *MockFilePersistenceInterface) UserHasAccess(tenantID types.TenantID, fid string, uid types.Uid) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserHasAccess", tenantID, fid, uid)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UserHasAccess indicates an expected call of UserHasAccess.
func (mr *MockFilePersistenceInterfaceMockRecorder) UserHasAccess(tenantID, fid, uid interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserHasAccess", reflect.TypeOf((*MockFilePersistenceInterface)(nil).UserHasAccess), tenantID, fid, uid)
}
// StartUpload mocks base method.
func (m *MockFilePersistenceInterface) StartUpload(tenantID types.TenantID, fd *types.FileDef) error {
m.ctrl.T.Helper()
+184
View File
@@ -149,6 +149,13 @@ func tenantAuthAdapter(tenantID types.TenantID) (adapter.TenantAuthAdapter, erro
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
@@ -160,6 +167,17 @@ func tenantBusinessAdapter(tenantID types.TenantID) (adapter.TenantBusinessAdapt
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
@@ -960,6 +978,7 @@ type MessagesPersistenceInterface interface {
DeleteList(tenantID types.TenantID, topic string, delID int, forUser types.Uid, msgDelAge time.Duration, ranges []types.Range) error
GetAll(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Message, error)
GetDeleted(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Range, int, error)
Search(tenantID types.TenantID, topic string, forUser types.Uid, query, messageType, cursor string, limit int) ([]types.Message, string, error)
}
// messagesMapper is a concrete type implementing MessagesPersistenceInterface.
@@ -1003,14 +1022,75 @@ type RelationsPersistenceInterface interface {
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
@@ -1111,6 +1191,20 @@ func (relationsMapper) BlockUser(tenantID types.TenantID, blockerID, blockedID t
return tenantAdp.TenantUserBlockUpsert(tenantID, blockerID, blockedID, now)
}
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
}
return tenantAdp.TenantUserBlockList(tenantID, blockerID, cursor, limit)
}
func (relationsMapper) UnblockUser(tenantID types.TenantID, blockerID, blockedID types.Uid) error {
if blockerID.IsZero() || blockedID.IsZero() || blockerID == blockedID {
return types.ErrMalformed
@@ -1133,6 +1227,68 @@ func (relationsMapper) DeleteFriend(tenantID types.TenantID, userID, peerID type
return tenantAdp.TenantFriendshipDeletePair(tenantID, userID, peerID, now)
}
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,
@@ -1381,6 +1537,20 @@ func (messagesMapper) GetAll(tenantID types.TenantID, topic string, forUser type
return tenantAdp.TenantMessageGetAll(tenantID, topic, forUser, opt)
}
func (messagesMapper) Search(tenantID types.TenantID, topic string, forUser types.Uid, query, messageType, cursor string, limit int) ([]types.Message, string, error) {
if topic == "" || forUser.IsZero() {
return nil, "", types.ErrMalformed
}
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, "", err
}
return tenantAdp.TenantMessageSearch(tenantID, topic, forUser, query, messageType, cursor, limit)
}
// GetDeleted returns the ranges of deleted messages and the largest DelId reported in the list.
func (messagesMapper) GetDeleted(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Range, int, error) {
tenantAdp, err := tenantBusinessAdapter(tenantID)
@@ -1649,6 +1819,8 @@ type FilePersistenceInterface interface {
// 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.
@@ -1738,6 +1910,17 @@ func (fileMapper) LinkAttachments(tenantID types.TenantID, topic string, msgId t
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.
@@ -2004,4 +2187,5 @@ func init() {
Verifications = verificationMapper{}
Security = userSecurityMapper{}
Relations = relationsMapper{}
Favorites = favoritesMapper{}
}
+20
View File
@@ -0,0 +1,20 @@
package types
import "time"
type FavoriteItem struct {
ID int64
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
FavoriteID string `db:"favorite_id"`
UserID Uid `db:"user_id"`
ItemType string `db:"item_type"`
SourceTopic string `db:"source_topic"`
SourceSeq int `db:"source_seq"`
Snapshot any
AttachmentRefs any `db:"attachment_refs"`
Availability string `db:"availability"`
IdempotencyKey string `db:"idempotency_key"`
CollectedAt time.Time `db:"collected_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
+13
View File
@@ -74,3 +74,16 @@ type FriendRequestListItem struct {
Peer *User
Direction string
}
type UserSearchItem struct {
User *User
Relationship string
RequestID string
CanAdd bool
Action string
}
type BlockListItem struct {
Block UserBlock
User *User
}
+2
View File
@@ -35,6 +35,8 @@ type Tenant struct {
Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"`
TenantDesc *string `db:"tenant_desc" json:"tenant_desc,omitempty"`
DBName string `db:"db_name" json:"-"`
DBDSN string `db:"db_dsn" json:"-"`
State TenantState `db:"state" json:"-"`
CreatedAt time.Time `db:"created_at" json:"-"`
CreatedBy int64 `db:"created_by" json:"-"`