feat: add im home and message action flows
This commit is contained in:
@@ -1098,6 +1098,21 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) DeleteList(tenantID, top
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteList", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).DeleteList), tenantID, topic, delID, forUser, msgDelAge, ranges)
|
||||
}
|
||||
|
||||
// Edit mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) 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) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Edit", tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
||||
ret0, _ := ret[0].(*types.Message)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Edit indicates an expected call of Edit.
|
||||
func (mr *MockMessagesPersistenceInterfaceMockRecorder) Edit(tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Edit", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).Edit), tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
|
||||
}
|
||||
|
||||
// GetAll mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) GetAll(tenantID types.TenantID, topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Message, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1129,12 +1144,42 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) GetDeleted(tenantID, top
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeleted", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).GetDeleted), tenantID, topic, forUser, opt)
|
||||
}
|
||||
|
||||
// GetOne mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) GetOne(tenantID types.TenantID, topic string, seq int) (*types.Message, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOne", tenantID, topic, seq)
|
||||
ret0, _ := ret[0].(*types.Message)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOne indicates an expected call of GetOne.
|
||||
func (mr *MockMessagesPersistenceInterfaceMockRecorder) GetOne(tenantID, topic, seq interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOne", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).GetOne), tenantID, topic, seq)
|
||||
}
|
||||
|
||||
// Recall mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) Recall(tenantID types.TenantID, topic string, seq int, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Recall", tenantID, topic, seq, actor, expectedVersion, recalledAt)
|
||||
ret0, _ := ret[0].(*types.Message)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Recall indicates an expected call of Recall.
|
||||
func (mr *MockMessagesPersistenceInterfaceMockRecorder) Recall(tenantID, topic, seq, actor, expectedVersion, recalledAt interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recall", reflect.TypeOf((*MockMessagesPersistenceInterface)(nil).Recall), tenantID, topic, seq, actor, expectedVersion, recalledAt)
|
||||
}
|
||||
|
||||
// Save mocks base method.
|
||||
func (m *MockMessagesPersistenceInterface) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (error, bool) {
|
||||
func (m *MockMessagesPersistenceInterface) Save(tenantID types.TenantID, msg *types.Message, attachmentURLs []string, readBySender bool) (*types.MessageSaveResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Save", tenantID, msg, attachmentURLs, readBySender)
|
||||
ret0, _ := ret[0].(error)
|
||||
ret1, _ := ret[1].(bool)
|
||||
ret0, _ := ret[0].(*types.MessageSaveResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
|
||||
+230
-11
@@ -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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// SystemNotification is a product-level notification shown in the system inbox.
|
||||
type SystemNotification struct {
|
||||
ID int64 `db:"id" json:"-"`
|
||||
TenantID TenantID `db:"tenant_id" json:"-"`
|
||||
UserID Uid `db:"user_id" json:"-"`
|
||||
EventID string `db:"event_id" json:"id"`
|
||||
EventType string `db:"event_type" json:"event_type"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Summary string `db:"summary" json:"summary"`
|
||||
Action KVMap `db:"action" json:"action,omitempty"`
|
||||
ReadAt *time.Time `db:"read_at" json:"read_at,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// RealNameVerification is the latest real-name verification submission for a user.
|
||||
type RealNameVerification struct {
|
||||
ID int64 `db:"id" json:"-"`
|
||||
TenantID TenantID `db:"tenant_id" json:"-"`
|
||||
UserID Uid `db:"user_id" json:"-"`
|
||||
SubmissionID string `db:"submission_id" json:"submission_id"`
|
||||
Status string `db:"status" json:"status"`
|
||||
NameCipher string `db:"name_cipher" json:"-"`
|
||||
IDType string `db:"id_type" json:"id_type,omitempty"`
|
||||
IDNumberCipher string `db:"id_number_cipher" json:"-"`
|
||||
IDNumberHash string `db:"id_number_hash" json:"-"`
|
||||
MaskedName string `db:"masked_name" json:"masked_name,omitempty"`
|
||||
MaskedIDNumber string `db:"masked_id_number" json:"masked_id_number,omitempty"`
|
||||
ReviewReason *string `db:"review_reason" json:"review_reason,omitempty"`
|
||||
VerifiedAt *time.Time `db:"verified_at" json:"verified_at,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"-"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"-"`
|
||||
}
|
||||
|
||||
// ActionToken is a short-lived one-time token for sensitive actions.
|
||||
type ActionToken struct {
|
||||
ID int64 `db:"id" json:"-"`
|
||||
TenantID TenantID `db:"tenant_id" json:"-"`
|
||||
UserID Uid `db:"user_id" json:"-"`
|
||||
TokenID string `db:"token_id" json:"-"`
|
||||
Action string `db:"action" json:"action"`
|
||||
Target string `db:"target" json:"target,omitempty"`
|
||||
TokenHash string `db:"token_hash" json:"-"`
|
||||
State int16 `db:"state" json:"-"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
ConsumedAt *time.Time `db:"consumed_at" json:"-"`
|
||||
CreatedAt time.Time `db:"created_at" json:"-"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"-"`
|
||||
}
|
||||
|
||||
// IndependentPassword stores the hashed PIN for a product feature such as youth mode.
|
||||
type IndependentPassword struct {
|
||||
TenantID TenantID `db:"tenant_id" json:"-"`
|
||||
UserID Uid `db:"user_id" json:"-"`
|
||||
Purpose string `db:"purpose" json:"purpose"`
|
||||
PasswordHash []byte `db:"password_hash" json:"-"`
|
||||
FailedAttempts int `db:"failed_attempts" json:"-"`
|
||||
LockedUntil *time.Time `db:"locked_until" json:"-"`
|
||||
CreatedAt time.Time `db:"created_at" json:"-"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"-"`
|
||||
}
|
||||
@@ -1228,9 +1228,33 @@ type Message struct {
|
||||
SeqId int
|
||||
Topic string
|
||||
// Sender's user ID as string (without 'usr' prefix), could be empty.
|
||||
From string
|
||||
Head KVMap `json:"Head,omitempty" bson:",omitempty"`
|
||||
Content any
|
||||
From string
|
||||
ClientMsgID string `db:"client_msg_id" json:"ClientMsgId,omitempty" bson:"client_msg_id,omitempty"`
|
||||
Version int `json:"Version,omitempty" bson:",omitempty"`
|
||||
RecalledAt *time.Time `db:"recalled_at" json:"RecalledAt,omitempty" bson:"recalled_at,omitempty"`
|
||||
RecalledBy string `json:"RecalledBy,omitempty" bson:"recalled_by,omitempty"`
|
||||
Head KVMap `json:"Head,omitempty" bson:",omitempty"`
|
||||
Content any
|
||||
}
|
||||
|
||||
// MessageRevision stores an immutable snapshot of a message before editing.
|
||||
type MessageRevision struct {
|
||||
ObjHeader `bson:",inline"`
|
||||
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||
Topic string
|
||||
SeqId int
|
||||
Version int
|
||||
Actor string
|
||||
Head KVMap `json:"Head,omitempty" bson:",omitempty"`
|
||||
Content any
|
||||
Reason string
|
||||
}
|
||||
|
||||
// MessageSaveResult describes the outcome of a message persistence attempt.
|
||||
type MessageSaveResult struct {
|
||||
SeqId int
|
||||
MarkedReadBySender bool
|
||||
Duplicate bool
|
||||
}
|
||||
|
||||
// Range is a range of message SeqIDs. Low end is inclusive (closed), high end is exclusive (open): [Low, Hi).
|
||||
|
||||
@@ -19,6 +19,7 @@ type VerificationCode struct {
|
||||
TenantID TenantID `db:"tenant_id" json:"-"`
|
||||
ChallengeID string `db:"challenge_id" json:"challenge_id"`
|
||||
Scene string `db:"scene" json:"scene"`
|
||||
Action string `db:"action" json:"action,omitempty"`
|
||||
PhoneHash string `db:"phone_hash" json:"-"`
|
||||
PhoneMask string `db:"phone_mask" json:"phone_mask,omitempty"`
|
||||
TargetUser Uid `db:"target_user" json:"-"`
|
||||
|
||||
Reference in New Issue
Block a user