feat: complete IM P0 and P1 reliability work

This commit is contained in:
richjohnrambo
2026-08-17 13:43:27 +08:00
parent ddf0f7a55e
commit f2c229135b
43 changed files with 4512 additions and 1181 deletions
+241 -10
View File
@@ -129,6 +129,7 @@ type storeObj struct{}
// TenantsPersistenceInterface defines tenant registry lookups used before authentication.
type TenantsPersistenceInterface interface {
GetByCode(code string) (*types.Tenant, error)
GetByID(id types.TenantID) (*types.Tenant, error)
}
type tenantsMapper struct{}
@@ -145,6 +146,18 @@ func (tenantsMapper) GetByCode(code string) (*types.Tenant, error) {
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
}
tenantAdp, ok := adp.(adapter.TenantAdapter)
if !ok {
return nil, types.ErrUnsupported
}
return tenantAdp.TenantGetByID(id)
}
func tenantAuthAdapter(tenantID types.TenantID) (adapter.TenantAuthAdapter, error) {
if !tenantID.IsValid() {
return nil, types.ErrMalformed
@@ -1080,6 +1093,7 @@ func (callsMapper) List(tenantID types.TenantID, uid types.Uid, topic, cursor st
}
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)
@@ -1090,19 +1104,41 @@ type GroupBusinessPersistenceInterface interface {
CreateInvitation(types.TenantID, *types.GroupInvitation) (*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, time.Time) (*types.GroupJoinRequest, error)
ResolveJoinRequest(types.TenantID, string, string, types.Uid, string, string, int, any, time.Time) (*types.GroupJoinRequest, error)
LatestAnnouncement(types.TenantID, string) (*types.GroupAnnouncement, error)
PutAnnouncement(types.TenantID, *types.GroupAnnouncement, int) (*types.GroupAnnouncement, error)
ReadAnnouncement(types.TenantID, string, string, types.Uid, time.Time) error
ListMutes(types.TenantID, string, string, int) ([]types.GroupMute, string, error)
UpsertMute(types.TenantID, *types.GroupMute) (*types.GroupMute, error)
DeleteMute(types.TenantID, string, types.Uid, types.Uid, time.Time) error
InviteMember(types.TenantID, string, types.Uid, types.Uid, string, string, any, time.Time) (bool, error)
Leave(types.TenantID, string, types.Uid, time.Time) error
UpdateRole(types.TenantID, string, types.Uid, types.Uid, string, types.AccessMode, time.Time) error
TransferOwner(types.TenantID, string, types.Uid, types.Uid, int, time.Time) error
CanPost(types.TenantID, string, types.Uid, bool, time.Time) (bool, error)
ListPendingEvents(types.TenantID, int, time.Time) ([]types.GroupEvent, error)
MarkEventDelivered(types.TenantID, string, time.Time) error
}
type groupBusinessMapper struct{}
var GroupBusiness GroupBusinessPersistenceInterface
func (groupBusinessMapper) CreateGroup(tenantID types.TenantID, topic *types.Topic, owner types.Uid, ownerPrivate any,
settings *types.GroupSettings, members []types.Uid, memberPrivate any, now time.Time) error {
if topic == nil || topic.TenantID != tenantID || owner.IsZero() || settings == nil || settings.Topic != topic.Id {
return types.ErrMalformed
}
topic.InitTimes()
topic.TouchedAt = topic.CreatedAt
topic.Owner = owner.String()
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
}
return tenantAdp.TenantGroupCreate(tenantID, topic, owner, ownerPrivate, settings, members, memberPrivate, now)
}
func (groupBusinessMapper) CreateSettings(tenantID types.TenantID, settings *types.GroupSettings) (*types.GroupSettings, error) {
if settings == nil || settings.Topic == "" || settings.OwnerUserID.IsZero() || settings.CreatedBy.IsZero() {
return nil, types.ErrMalformed
@@ -1219,7 +1255,7 @@ func (groupBusinessMapper) ListJoinRequests(tenantID types.TenantID, topic, stat
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, now time.Time) (*types.GroupJoinRequest, error) {
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
}
@@ -1227,7 +1263,7 @@ func (groupBusinessMapper) ResolveJoinRequest(tenantID types.TenantID, topic, re
if err != nil {
return nil, err
}
return tenantAdp.TenantGroupJoinRequestResolve(tenantID, strings.TrimSpace(topic), strings.TrimSpace(requestID), actor, strings.TrimSpace(action), strings.TrimSpace(rejectReason), version, now)
return tenantAdp.TenantGroupJoinRequestResolve(tenantID, strings.TrimSpace(topic), strings.TrimSpace(requestID), actor, strings.TrimSpace(action), strings.TrimSpace(rejectReason), version, memberPrivate, now)
}
func (groupBusinessMapper) LatestAnnouncement(tenantID types.TenantID, topic string) (*types.GroupAnnouncement, error) {
@@ -1299,6 +1335,87 @@ func (groupBusinessMapper) DeleteMute(tenantID types.TenantID, topic string, uid
return tenantAdp.TenantGroupMuteDelete(tenantID, strings.TrimSpace(topic), uid, actor, now)
}
func (groupBusinessMapper) InviteMember(tenantID types.TenantID, topic string, inviter, target types.Uid,
message, idempotencyKey string, private any, now time.Time) (bool, error) {
if strings.TrimSpace(topic) == "" || inviter.IsZero() || target.IsZero() || now.IsZero() {
return false, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return false, err
}
return tenantAdp.TenantGroupInviteMember(tenantID, strings.TrimSpace(topic), inviter, target,
strings.TrimSpace(message), strings.TrimSpace(idempotencyKey), private, now)
}
func (groupBusinessMapper) Leave(tenantID types.TenantID, topic string, uid types.Uid, now time.Time) error {
if strings.TrimSpace(topic) == "" || uid.IsZero() || now.IsZero() {
return types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
}
return tenantAdp.TenantGroupLeave(tenantID, strings.TrimSpace(topic), uid, now)
}
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
}
return tenantAdp.TenantGroupRoleUpdate(tenantID, strings.TrimSpace(topic), actor, target, strings.TrimSpace(role), modeGiven, now)
}
func (groupBusinessMapper) TransferOwner(tenantID types.TenantID, topic string, previousOwner, newOwner types.Uid,
version int, now time.Time) error {
if strings.TrimSpace(topic) == "" || previousOwner.IsZero() || newOwner.IsZero() || now.IsZero() {
return types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
}
return tenantAdp.TenantGroupOwnerTransfer(tenantID, strings.TrimSpace(topic), previousOwner, newOwner, version, now)
}
func (groupBusinessMapper) CanPost(tenantID types.TenantID, topic string, uid types.Uid, admin bool, now time.Time) (bool, error) {
if strings.TrimSpace(topic) == "" || uid.IsZero() || now.IsZero() {
return false, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return false, err
}
return tenantAdp.TenantGroupCanPost(tenantID, strings.TrimSpace(topic), uid, admin, now)
}
func (groupBusinessMapper) ListPendingEvents(tenantID types.TenantID, limit int, now time.Time) ([]types.GroupEvent, error) {
if limit <= 0 || limit > 100 {
limit = 50
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantGroupEventListPending(tenantID, limit, now)
}
func (groupBusinessMapper) MarkEventDelivered(tenantID types.TenantID, eventID string, now time.Time) error {
if strings.TrimSpace(eventID) == "" || now.IsZero() {
return types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return err
}
return tenantAdp.TenantGroupEventMarkDelivered(tenantID, strings.TrimSpace(eventID), now)
}
type WorkbenchPersistenceInterface interface {
Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error)
}
@@ -1724,6 +1841,10 @@ type DevicePushTokenPersistenceInterface interface {
Upsert(tenantID types.TenantID, token *types.DevicePushToken) (*types.DevicePushToken, error)
}
type DevicePushTokenQueryInterface interface {
GetAll(tenantID types.TenantID, uid ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error)
}
type devicePushTokenMapper struct{}
var DevicePushTokens DevicePushTokenPersistenceInterface
@@ -1740,6 +1861,20 @@ func (devicePushTokenMapper) Upsert(tenantID types.TenantID, token *types.Device
return tenantAdp.TenantDevicePushTokenUpsert(tenantID, token)
}
func (devicePushTokenMapper) GetAll(tenantID types.TenantID, uids ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, 0, err
}
reader, ok := tenantAdp.(interface {
TenantDevicePushTokenGetAll(types.TenantID, ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error)
})
if !ok {
return map[types.Uid][]types.DeviceDef{}, 0, nil
}
return reader.TenantDevicePushTokenGetAll(tenantID, uids...)
}
type SafetyPersistenceInterface interface {
CreateRiskChallenge(tenantID types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error)
ConfirmRiskChallenge(tenantID types.TenantID, uid types.Uid, challengeID, decision, actionTokenID string, now time.Time) (*types.RiskChallenge, error)
@@ -1863,7 +1998,7 @@ func (profileMapper) GetPrivacy(tenantID types.TenantID, uid types.Uid) (*types.
}
func (profileMapper) UpdatePrivacy(tenantID types.TenantID, uid types.Uid, patch types.UserPrivacyPatch) (*types.UserPrivacySettings, error) {
if uid.IsZero() || patch.Version <= 0 {
if uid.IsZero() || patch.Version <= 0 || !validUserPrivacyPatch(patch) {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
@@ -1873,6 +2008,31 @@ func (profileMapper) UpdatePrivacy(tenantID types.TenantID, uid types.Uid, patch
return tenantAdp.TenantUserPrivacyUpdate(tenantID, uid, patch)
}
func validUserPrivacyPatch(patch types.UserPrivacyPatch) bool {
if patch.AddMeMethods == nil && patch.ProfileVisibility == nil && patch.LastSeenVisibility == nil && patch.ReadReceipts == nil {
return false
}
for method := range patch.AddMeMethods {
switch method {
case "account", "phone", "qr", "group":
default:
return false
}
}
validVisibility := func(value *string) bool {
if value == nil {
return true
}
switch strings.TrimSpace(*value) {
case "everyone", "friends", "nobody":
return true
default:
return false
}
}
return validVisibility(patch.ProfileVisibility) && validVisibility(patch.LastSeenVisibility)
}
func validUserProfilePatch(patch types.UserProfilePatch) bool {
if patch.AvatarFileID == nil && patch.Nickname == nil && patch.BirthDate == nil && patch.Signature == nil && patch.Gender == nil && patch.Region == nil {
return false
@@ -2321,20 +2481,77 @@ func normalizeClientMsgID(head types.KVMap) (string, error) {
func validateMessageHeaders(head types.KVMap) error {
if schema, ok := head["x-im-schema"]; ok {
value, ok := schema.(string)
if !ok || !validIMMessageHeaderToken(value, 32) {
if !validIMMessageSchema(schema) {
return types.ErrMalformed
}
}
if msgType, ok := head["type"]; ok {
value, ok := msgType.(string)
if !ok || !validIMMessageType(value) {
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 {
@@ -2351,13 +2568,27 @@ func validIMMessageHeaderToken(value string, max int) bool {
func validIMMessageType(value string) bool {
switch strings.TrimSpace(value) {
case "text", "image", "file", "audio", "video", "location", "contact", "card", "call", "system":
case "text", "image", "file", "audio", "voice", "video", "location", "contact", "contact_card",
"card", "link", "forward_bundle", "sticker", "call", "system", "system_event":
return true
default:
return false
}
}
func canonicalIMMessageType(value string) string {
switch strings.TrimSpace(value) {
case "audio":
return "voice"
case "contact":
return "contact_card"
case "system":
return "system_event"
default:
return strings.TrimSpace(value)
}
}
// 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)