feat: complete IM P0 and P1 reliability work
This commit is contained in:
+241
-10
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user