feat: add im home and message action flows

This commit is contained in:
richjohnrambo
2026-08-12 10:36:41 +08:00
parent d239e37f56
commit 5bd35f97a6
21 changed files with 3087 additions and 61 deletions
+230 -11
View File
@@ -953,7 +953,10 @@ func (subsMapper) Delete(tenantID types.TenantID, topic string, user types.Uid)
// 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) (error, bool)
Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error)
GetOne(tenantID types.TenantID, topic string, seq int) (*types.Message, error)
Recall(tenantID types.TenantID, topic string, seq int, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error)
Edit(tenantID types.TenantID, topic string, seq int, 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)
@@ -965,33 +968,84 @@ type messagesMapper struct{}
// Messages is a singleton ancor object for exporting MessagesPersistenceInterface.
var Messages MessagesPersistenceInterface
// Save message
func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (error, bool) {
if msg == nil || msg.TenantID != tenantID {
return types.ErrMalformed, false
func (messagesMapper) GetOne(tenantID types.TenantID, topic string, seq int) (*types.Message, error) {
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMessageGetOne(tenantID, topic, seq)
}
func (messagesMapper) Recall(tenantID types.TenantID, topic string, seq int, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error) {
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
return tenantAdp.TenantMessageRecall(tenantID, topic, seq, actor, expectedVersion, recalledAt)
}
func (messagesMapper) Edit(tenantID types.TenantID, topic string, seq int, 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 err, false
return nil, err
}
return tenantAdp.TenantMessageEdit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
}
// Save message
func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
if msg == nil || msg.TenantID != tenantID {
return nil, types.ErrMalformed
}
tenantAdp, err := tenantBusinessAdapter(tenantID)
if err != nil {
return nil, err
}
clientMsgID, err := normalizeClientMsgID(msg.Head)
if err != nil {
return nil, err
}
msg.ClientMsgID = clientMsgID
fromUid := types.ParseUid(msg.From)
if clientMsgID != "" {
duplicate, err := tenantAdp.TenantMessageGetByClientID(tenantID, fromUid, clientMsgID)
if err != nil {
return nil, err
}
if duplicate != nil {
msg.ObjHeader = duplicate.ObjHeader
msg.DeletedAt = duplicate.DeletedAt
msg.DelId = duplicate.DelId
msg.DeletedFor = duplicate.DeletedFor
msg.SeqId = duplicate.SeqId
msg.Topic = duplicate.Topic
msg.From = duplicate.From
msg.ClientMsgID = duplicate.ClientMsgID
msg.Head = duplicate.Head
msg.Content = duplicate.Content
return &types.MessageSaveResult{SeqId: duplicate.SeqId, Duplicate: true}, nil
}
}
msg.InitTimes()
msg.SetUid(Store.GetUid())
// Increment topic's or user's SeqId
err = tenantAdp.TenantTopicUpdateOnMessage(tenantID, msg.Topic, msg)
if err != nil {
return err, false
return nil, err
}
err = tenantAdp.TenantMessageSave(tenantID, msg)
if err != nil {
return err, false
return nil, 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.
fromUid := types.ParseUid(msg.From)
if !fromUid.IsZero() {
// Ignore the error here. It's not a big deal if it fails.
if subErr := tenantAdp.TenantSubsUpdate(tenantID, msg.Topic, fromUid,
@@ -1014,11 +1068,74 @@ func (messagesMapper) Save(tenantID types.TenantID, msg *types.Message, attachme
}
}
if len(attachments) > 0 {
return tenantAdp.TenantFileLinkAttachments(tenantID, "", types.ZeroUid, msg.Uid(), attachments), markedReadBySender
if err := tenantAdp.TenantFileLinkAttachments(tenantID, "", types.ZeroUid, msg.Uid(), attachments); err != nil {
return nil, err
}
}
}
return nil, markedReadBySender
return &types.MessageSaveResult{SeqId: msg.SeqId, MarkedReadBySender: markedReadBySender}, 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 {
value, ok := schema.(string)
if !ok || !validIMMessageHeaderToken(value, 32) {
return types.ErrMalformed
}
}
if msgType, ok := head["type"]; ok {
value, ok := msgType.(string)
if !ok || !validIMMessageType(value) {
return types.ErrMalformed
}
}
return nil
}
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", "video", "location", "contact", "card", "call", "system":
return true
default:
return false
}
}
// DeleteList deletes multiple messages defined by a list of ranges.
@@ -1469,6 +1586,16 @@ var Verifications VerificationPersistenceInterface
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
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
CreateSystemNotification(tenantID types.TenantID, notification *types.SystemNotification) error
ListSystemNotifications(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.SystemNotification, string, error)
MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) error
}
type userSecurityMapper struct{}
@@ -1492,6 +1619,98 @@ func (userSecurityMapper) BumpAuthVersion(tenantID types.TenantID, uid types.Uid
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) 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
}
return tenantAdp.TenantActionTokenCreate(tenantID, token)
}
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
}
return tenantAdp.TenantActionTokenConsume(tenantID, uid, token, action, target, now)
}
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) 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) 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