mirror of
https://github.com/mautrix/signal.git
synced 2025-03-14 14:15:36 +00:00
signalmeow: store history transfer data in db
This commit is contained in:
parent
030c83c197
commit
dd3aab051f
8 changed files with 515 additions and 44 deletions
|
@ -23,6 +23,7 @@ package libsignalgo
|
|||
import "C"
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
|
@ -54,6 +55,18 @@ func GenerateGroupSecretParams() (GroupSecretParams, error) {
|
|||
return GenerateGroupSecretParamsWithRandomness(GenerateRandomness())
|
||||
}
|
||||
|
||||
func (gmk GroupMasterKey) GroupIdentifier() (*GroupIdentifier, error) {
|
||||
if groupSecretParams, err := DeriveGroupSecretParamsFromMasterKey(gmk); err != nil {
|
||||
return nil, fmt.Errorf("DeriveGroupSecretParamsFromMasterKey error: %w", err)
|
||||
} else if groupPublicParams, err := groupSecretParams.GetPublicParams(); err != nil {
|
||||
return nil, fmt.Errorf("GetPublicParams error: %w", err)
|
||||
} else if groupIdentifier, err := GetGroupIdentifier(*groupPublicParams); err != nil {
|
||||
return nil, fmt.Errorf("GetGroupIdentifier error: %w", err)
|
||||
} else {
|
||||
return groupIdentifier, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
|
||||
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_secret_params_generate_deterministic(¶ms, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))
|
||||
|
|
|
@ -41,12 +41,14 @@ import (
|
|||
const transferArchiveFetchTimeout = 1 * time.Hour
|
||||
|
||||
var (
|
||||
ErrTransferRelinkRequested = errors.New("relink requested")
|
||||
ErrTransferContinueWithoutUpload = errors.New("continue without upload")
|
||||
|
||||
ErrNoEphemeralBackupKey = errors.New("no ephemeral backup key")
|
||||
)
|
||||
|
||||
const (
|
||||
TransferErrorRelinkRequested = "RELINK_REQUESTED"
|
||||
TransferErrorContinueWithoutUpload = "CONTINUE_WITHOUT_UPLOAD"
|
||||
)
|
||||
|
||||
type TransferArchiveMetadata struct {
|
||||
CDN uint32 `json:"cdn"`
|
||||
Key string `json:"key"`
|
||||
|
@ -55,14 +57,7 @@ type TransferArchiveMetadata struct {
|
|||
|
||||
func (cli *Client) FetchAndProcessTransfer(ctx context.Context, meta *TransferArchiveMetadata) error {
|
||||
if meta.Error != "" {
|
||||
switch meta.Error {
|
||||
case "RELINK_REQUESTED":
|
||||
return ErrTransferRelinkRequested
|
||||
case "CONTINUE_WITHOUT_UPLOAD":
|
||||
return ErrTransferContinueWithoutUpload
|
||||
default:
|
||||
return fmt.Errorf("transfer archive error: %s", meta.Error)
|
||||
}
|
||||
return fmt.Errorf("transfer archive error: %s", meta.Error)
|
||||
}
|
||||
aesKey, hmacKey, err := cli.deriveTransferKeys()
|
||||
if err != nil {
|
||||
|
@ -98,15 +93,29 @@ func (cli *Client) FetchAndProcessTransfer(ctx context.Context, meta *TransferAr
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to seek to start of file: %w", err)
|
||||
}
|
||||
err = cli.processTransferArchive(ctx, aesKey, hmacKey, file, stat.Size())
|
||||
err = cli.Store.DoTxn(ctx, func(ctx context.Context) error {
|
||||
err = cli.Store.BackupStore.ClearBackup(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clear backup: %w", err)
|
||||
}
|
||||
err = cli.processTransferArchive(ctx, aesKey, hmacKey, file, stat.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cli.Store.BackupStore.RecalculateChatCounts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate message counts: %w", err)
|
||||
}
|
||||
cli.Store.EphemeralBackupKey = nil
|
||||
err = cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save device data after clearing ephemeral backup key: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.Store.EphemeralBackupKey = nil
|
||||
err = cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save device data after clearing ephemeral backup key: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -185,8 +194,27 @@ func (acp *archiveChunkProcessor) processChunk(buf []byte) error {
|
|||
}
|
||||
|
||||
func (acp *archiveChunkProcessor) processFrame(frame *backuppb.Frame) error {
|
||||
acp.cli.Log.Info().Any("backup_frame", frame).Msg("Received backup frame")
|
||||
return nil
|
||||
acp.cli.Log.Trace().Any("backup_frame", frame).Msg("Processing backup frame")
|
||||
switch item := frame.Item.(type) {
|
||||
case *backuppb.Frame_Recipient:
|
||||
return acp.cli.Store.BackupStore.AddBackupRecipient(acp.ctx, item.Recipient)
|
||||
case *backuppb.Frame_Chat:
|
||||
return acp.cli.Store.BackupStore.AddBackupChat(acp.ctx, item.Chat)
|
||||
case *backuppb.Frame_ChatItem:
|
||||
switch item.ChatItem.Item.(type) {
|
||||
case *backuppb.ChatItem_DirectStoryReplyMessage, *backuppb.ChatItem_UpdateMessage:
|
||||
zerolog.Ctx(acp.ctx).Debug().
|
||||
Uint64("chat_id", item.ChatItem.ChatId).
|
||||
Uint64("message_id", item.ChatItem.DateSent).
|
||||
Type("frame_type", item).
|
||||
Msg("Not saving unsupported chat item type")
|
||||
return nil
|
||||
}
|
||||
return acp.cli.Store.BackupStore.AddBackupChatItem(acp.ctx, item.ChatItem)
|
||||
default:
|
||||
zerolog.Ctx(acp.ctx).Debug().Type("frame_type", item).Msg("Ignoring backup frame")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *Client) deriveTransferKeys() (aesKey, hmacKey [32]byte, err error) {
|
||||
|
|
|
@ -469,18 +469,9 @@ func InviteLinkPasswordFromBytes(inviteLinkPassword []byte) types.SerializedInvi
|
|||
}
|
||||
|
||||
func groupIdentifierFromMasterKey(masterKey types.SerializedGroupMasterKey) (types.GroupIdentifier, error) {
|
||||
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyToBytes(masterKey))
|
||||
groupIdentifier, err := masterKeyToBytes(masterKey).GroupIdentifier()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("DeriveGroupSecretParamsFromMasterKey error: %w", err)
|
||||
}
|
||||
// Get the "group identifier" that isn't just the master key
|
||||
groupPublicParams, err := groupSecretParams.GetPublicParams()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GetPublicParams error: %w", err)
|
||||
}
|
||||
groupIdentifier, err := libsignalgo.GetGroupIdentifier(*groupPublicParams)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GetGroupIdentifier error: %w", err)
|
||||
return "", err
|
||||
}
|
||||
base64GroupIdentifier := base64.StdEncoding.EncodeToString(groupIdentifier[:])
|
||||
gid := types.GroupIdentifier(base64GroupIdentifier)
|
||||
|
|
331
pkg/signalmeow/store/backup_store.go
Normal file
331
pkg/signalmeow/store/backup_store.go
Normal file
|
@ -0,0 +1,331 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2025 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/util/ptr"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/backuppb"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
type BackupChat struct {
|
||||
*backuppb.Chat
|
||||
TotalMessages int
|
||||
LatestMessageID uint64
|
||||
}
|
||||
|
||||
type BackupStore interface {
|
||||
AddBackupRecipient(ctx context.Context, recipient *backuppb.Recipient) error
|
||||
AddBackupChat(ctx context.Context, chat *backuppb.Chat) error
|
||||
AddBackupChatItem(ctx context.Context, item *backuppb.ChatItem) error
|
||||
RecalculateChatCounts(ctx context.Context) error
|
||||
ClearBackup(ctx context.Context) error
|
||||
|
||||
GetBackupRecipient(ctx context.Context, recipientID uint64) (*backuppb.Recipient, error)
|
||||
GetBackupChatByUserID(ctx context.Context, userID libsignalgo.ServiceID) (*BackupChat, error)
|
||||
GetBackupChatByGroupID(ctx context.Context, groupID types.GroupIdentifier) (*BackupChat, error)
|
||||
GetBackupChats(ctx context.Context) ([]*BackupChat, error)
|
||||
GetBackupChatItems(ctx context.Context, chatID uint64, anchor time.Time, forward bool, limit int) ([]*backuppb.ChatItem, error)
|
||||
DeleteBackupChat(ctx context.Context, chatID uint64) error
|
||||
DeleteBackupChatItems(ctx context.Context, chatID uint64, minTime time.Time) error
|
||||
}
|
||||
|
||||
var _ BackupStore = (*sqlStore)(nil)
|
||||
|
||||
const (
|
||||
addBackupRecipientQuery = `
|
||||
INSERT INTO signalmeow_backup_recipient (account_id, recipient_id, aci_uuid, pni_uuid, group_master_key, data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`
|
||||
addBackupChatQuery = `
|
||||
INSERT INTO signalmeow_backup_chat (account_id, chat_id, recipient_id, data)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`
|
||||
addBackupChatItemQuery = `
|
||||
INSERT INTO signalmeow_backup_message (account_id, chat_id, sender_id, message_id, data)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
|
||||
getBackupRecipientQuery = `
|
||||
SELECT data FROM signalmeow_backup_recipient WHERE account_id=$1 AND recipient_id=$2
|
||||
`
|
||||
getBackupChatByACIQuery = `
|
||||
SELECT chat.data, chat.latest_message_id, chat.total_message_count FROM signalmeow_backup_recipient rcp
|
||||
INNER JOIN signalmeow_backup_chat chat ON rcp.account_id=chat.account_id AND rcp.recipient_id=chat.recipient_id
|
||||
WHERE rcp.account_id=$1 AND rcp.aci_uuid=$2
|
||||
`
|
||||
getBackupChatByPNIQuery = `
|
||||
SELECT chat.data, chat.latest_message_id, chat.total_message_count FROM signalmeow_backup_recipient rcp
|
||||
INNER JOIN signalmeow_backup_chat chat ON rcp.account_id=chat.account_id AND rcp.recipient_id=chat.recipient_id
|
||||
WHERE rcp.account_id=$1 AND rcp.pni_uuid=$2
|
||||
`
|
||||
getBackupChatByGroupIDQuery = `
|
||||
SELECT chat.data, chat.latest_message_id, chat.total_message_count FROM signalmeow_groups g
|
||||
INNER JOIN signalmeow_backup_recipient rcp ON g.account_id=rcp.account_id AND g.master_key=rcp.group_master_key
|
||||
INNER JOIN signalmeow_backup_chat chat ON rcp.account_id=chat.account_id AND rcp.recipient_id=chat.recipient_id
|
||||
WHERE g.account_id=$1 AND g.group_identifier=$2
|
||||
`
|
||||
getAllBackupChatsQuery = `
|
||||
SELECT data, latest_message_id, total_message_count
|
||||
FROM signalmeow_backup_chat
|
||||
WHERE account_id=$1
|
||||
`
|
||||
getBackupChatItemsQuery = `
|
||||
SELECT data
|
||||
FROM signalmeow_backup_message
|
||||
WHERE account_id=$1 AND chat_id=$2 AND message_id > $3 AND message_id < $4
|
||||
ORDER BY message_id DESC
|
||||
LIMIT $5
|
||||
`
|
||||
deleteBackupChatQuery = `
|
||||
DELETE FROM signalmeow_backup_chat WHERE account_id=$1 AND chat_id=$2
|
||||
`
|
||||
deleteBackupChatItemsQuery = `
|
||||
DELETE FROM signalmeow_backup_message WHERE account_id=$1 AND chat_id=$2 AND message_id >= $3
|
||||
`
|
||||
recalculateChatCountsQuery = `
|
||||
UPDATE signalmeow_backup_chat
|
||||
SET latest_message_id = (
|
||||
SELECT message_id
|
||||
FROM signalmeow_backup_message
|
||||
WHERE account_id=signalmeow_backup_chat.account_id AND chat_id=signalmeow_backup_chat.chat_id
|
||||
ORDER BY message_id DESC
|
||||
LIMIT 1
|
||||
),
|
||||
total_message_count = (
|
||||
SELECT COUNT(*)
|
||||
FROM signalmeow_backup_message
|
||||
WHERE account_id=signalmeow_backup_chat.account_id AND chat_id=signalmeow_backup_chat.chat_id
|
||||
)
|
||||
WHERE account_id=$1
|
||||
`
|
||||
)
|
||||
|
||||
func tryCastUUID(b []byte) uuid.UUID {
|
||||
if len(b) == 16 {
|
||||
return uuid.UUID(b)
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
func (s *sqlStore) AddBackupRecipient(ctx context.Context, recipient *backuppb.Recipient) error {
|
||||
recipientData, err := proto.Marshal(recipient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal recipient %d: %w", recipient.Id, err)
|
||||
}
|
||||
var aci, pni uuid.UUID
|
||||
var groupMasterKey types.SerializedGroupMasterKey
|
||||
switch dest := recipient.Destination.(type) {
|
||||
case *backuppb.Recipient_Contact:
|
||||
aci = tryCastUUID(dest.Contact.Aci)
|
||||
pni = tryCastUUID(dest.Contact.Pni)
|
||||
// TODO save identity key + trust level
|
||||
if aci != uuid.Nil || pni != uuid.Nil {
|
||||
_, err := s.LoadAndUpdateRecipient(ctx, aci, pni, func(recipient *types.Recipient) (changed bool, err error) {
|
||||
oldRecipient := ptr.Clone(recipient)
|
||||
if dest.Contact.E164 != nil {
|
||||
recipient.E164 = fmt.Sprintf("+%d", *dest.Contact.E164)
|
||||
}
|
||||
if len(dest.Contact.ProfileKey) == libsignalgo.ProfileKeyLength {
|
||||
recipient.Profile.Key = libsignalgo.ProfileKey(dest.Contact.ProfileKey)
|
||||
}
|
||||
if dest.Contact.ProfileGivenName != nil || dest.Contact.ProfileFamilyName != nil {
|
||||
recipient.Profile.Name = strings.TrimSpace(fmt.Sprintf("%s %s", dest.Contact.GetProfileGivenName(), dest.Contact.GetProfileFamilyName()))
|
||||
}
|
||||
changed = oldRecipient.E164 != recipient.E164 ||
|
||||
oldRecipient.Profile.Key != recipient.Profile.Key ||
|
||||
oldRecipient.Profile.Name != recipient.Profile.Name
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save info for recipient %d: %w", recipient.Id, err)
|
||||
}
|
||||
} else if dest.Contact.GetRegistered() != nil {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Uint64("recipient_id", recipient.Id).
|
||||
Any("entry", dest.Contact).
|
||||
Msg("Both ACI and PNI are invalid for registered contact recipient")
|
||||
}
|
||||
case *backuppb.Recipient_Group:
|
||||
groupMasterKey = types.SerializedGroupMasterKey(base64.StdEncoding.EncodeToString(dest.Group.MasterKey))
|
||||
if len(dest.Group.MasterKey) == libsignalgo.GroupMasterKeyLength {
|
||||
gid, err := libsignalgo.GroupMasterKey(dest.Group.MasterKey).GroupIdentifier()
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Uint64("recipient_id", recipient.Id).
|
||||
Msg("Failed to get group identifier from master key")
|
||||
} else if err = s.StoreMasterKey(ctx, types.GroupIdentifier(base64.StdEncoding.EncodeToString(gid[:])), groupMasterKey); err != nil {
|
||||
return fmt.Errorf("failed to save group master key for recipient %d: %w", recipient.Id, err)
|
||||
}
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Uint64("recipient_id", recipient.Id).
|
||||
Msg("Invalid group master key length")
|
||||
}
|
||||
case *backuppb.Recipient_Self:
|
||||
aci = s.AccountID
|
||||
default:
|
||||
}
|
||||
_, err = s.db.Exec(ctx, addBackupRecipientQuery, s.AccountID, recipient.Id, ptr.NonZero(aci), ptr.NonZero(pni), ptr.NonZero(groupMasterKey), recipientData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add backup recipient %d: %w", recipient.Id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sqlStore) AddBackupChat(ctx context.Context, chat *backuppb.Chat) error {
|
||||
chatData, err := proto.Marshal(chat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal chat %d: %w", chat.Id, err)
|
||||
}
|
||||
_, err = s.db.Exec(ctx, addBackupChatQuery, s.AccountID, chat.Id, chat.RecipientId, chatData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add backup chat %d: %w", chat.Id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sqlStore) AddBackupChatItem(ctx context.Context, item *backuppb.ChatItem) error {
|
||||
itemData, err := proto.Marshal(item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal chat item %d: %w", item.DateSent, err)
|
||||
}
|
||||
_, err = s.db.Exec(ctx, addBackupChatItemQuery, s.AccountID, item.ChatId, item.AuthorId, item.DateSent, itemData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add backup chat item %d: %w", item.DateSent, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sqlStore) ClearBackup(ctx context.Context) error {
|
||||
_, err := s.db.Exec(ctx, "DELETE FROM signalmeow_backup_message WHERE account_id=$1", s.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.Exec(ctx, "DELETE FROM signalmeow_backup_chat WHERE account_id=$1", s.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.Exec(ctx, "DELETE FROM signalmeow_backup_recipient WHERE account_id=$1", s.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
func scanProto[T proto.Message](row dbutil.Scannable) (val T, err error) {
|
||||
var data []byte
|
||||
err = row.Scan(&data)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
val = val.ProtoReflect().New().Interface().(T)
|
||||
err = proto.Unmarshal(data, val)
|
||||
return
|
||||
}
|
||||
|
||||
func scanChat(row dbutil.Scannable) (*BackupChat, error) {
|
||||
var data []byte
|
||||
var latestMessageID, totalMessageCount sql.NullInt64
|
||||
err := row.Scan(&data, &latestMessageID, &totalMessageCount)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var chat backuppb.Chat
|
||||
err = proto.Unmarshal(data, &chat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BackupChat{
|
||||
Chat: &chat,
|
||||
TotalMessages: int(totalMessageCount.Int64),
|
||||
LatestMessageID: uint64(totalMessageCount.Int64),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var chatScanner = dbutil.ConvertRowFn[*BackupChat](scanChat)
|
||||
var messageScanner = dbutil.ConvertRowFn[*backuppb.ChatItem](scanProto[*backuppb.ChatItem])
|
||||
|
||||
func (s *sqlStore) GetBackupRecipient(ctx context.Context, recipientID uint64) (*backuppb.Recipient, error) {
|
||||
return scanProto[*backuppb.Recipient](s.db.QueryRow(ctx, getBackupRecipientQuery, s.AccountID, recipientID))
|
||||
}
|
||||
|
||||
func (s *sqlStore) GetBackupChatByUserID(ctx context.Context, userID libsignalgo.ServiceID) (*BackupChat, error) {
|
||||
query := getBackupChatByACIQuery
|
||||
if userID.Type == libsignalgo.ServiceIDTypePNI {
|
||||
query = getBackupChatByPNIQuery
|
||||
}
|
||||
return scanChat(s.db.QueryRow(ctx, query, s.AccountID, userID.UUID))
|
||||
}
|
||||
|
||||
func (s *sqlStore) GetBackupChatByGroupID(ctx context.Context, groupID types.GroupIdentifier) (*BackupChat, error) {
|
||||
return scanChat(s.db.QueryRow(ctx, getBackupChatByGroupIDQuery, s.AccountID, groupID))
|
||||
}
|
||||
|
||||
func (s *sqlStore) GetBackupChats(ctx context.Context) ([]*BackupChat, error) {
|
||||
return chatScanner.NewRowIter(s.db.Query(ctx, getAllBackupChatsQuery, s.AccountID)).AsList()
|
||||
}
|
||||
|
||||
func (s *sqlStore) GetBackupChatItems(ctx context.Context, chatID uint64, anchor time.Time, forward bool, limit int) ([]*backuppb.ChatItem, error) {
|
||||
var minTS, maxTS int64
|
||||
if anchor.IsZero() {
|
||||
maxTS = time.Now().Add(24 * time.Hour).UnixMilli()
|
||||
} else if forward {
|
||||
minTS = anchor.UnixMilli()
|
||||
maxTS = time.Now().Add(24 * time.Hour).UnixMilli()
|
||||
} else {
|
||||
maxTS = anchor.UnixMilli()
|
||||
}
|
||||
return messageScanner.NewRowIter(s.db.Query(ctx, getBackupChatItemsQuery, s.AccountID, chatID, minTS, maxTS, limit)).AsList()
|
||||
}
|
||||
|
||||
func (s *sqlStore) DeleteBackupChatItems(ctx context.Context, chatID uint64, minTime time.Time) error {
|
||||
anchorTS := minTime.UnixMilli()
|
||||
if minTime.IsZero() {
|
||||
anchorTS = 0
|
||||
}
|
||||
_, err := s.db.Exec(ctx, deleteBackupChatItemsQuery, s.AccountID, chatID, anchorTS)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sqlStore) DeleteBackupChat(ctx context.Context, chatID uint64) error {
|
||||
_, err := s.db.Exec(ctx, deleteBackupChatQuery, s.AccountID, chatID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sqlStore) RecalculateChatCounts(ctx context.Context) error {
|
||||
_, err := s.db.Exec(ctx, recalculateChatCountsQuery, s.AccountID)
|
||||
return err
|
||||
}
|
|
@ -107,6 +107,10 @@ func (c *Container) scanDevice(row dbutil.Scannable) (*Device, error) {
|
|||
device.GroupStore = baseStore
|
||||
device.RecipientStore = baseStore
|
||||
device.DeviceStore = baseStore
|
||||
device.BackupStore = baseStore
|
||||
device.DoTxn = func(ctx context.Context, fn func(context.Context) error) error {
|
||||
return c.db.DoTxn(ctx, nil, fn)
|
||||
}
|
||||
|
||||
return &device, nil
|
||||
}
|
||||
|
|
|
@ -77,6 +77,9 @@ type Device struct {
|
|||
GroupStore GroupStore
|
||||
RecipientStore RecipientStore
|
||||
DeviceStore DeviceStore
|
||||
BackupStore BackupStore
|
||||
|
||||
DoTxn func(context.Context, func(context.Context) error) error
|
||||
}
|
||||
|
||||
func (d *Device) ClearDeviceKeys(ctx context.Context) error {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
-- v0 -> v18 (compatible with v13+): Latest revision
|
||||
-- v0 -> v19 (compatible with v13+): Latest revision
|
||||
CREATE TABLE signalmeow_device (
|
||||
aci_uuid TEXT PRIMARY KEY,
|
||||
|
||||
|
@ -43,10 +43,10 @@ CREATE TABLE signalmeow_kyber_pre_keys (
|
|||
);
|
||||
|
||||
CREATE TABLE signalmeow_identity_keys (
|
||||
account_id TEXT NOT NULL,
|
||||
their_service_id TEXT NOT NULL,
|
||||
key bytea NOT NULL,
|
||||
trust_level TEXT NOT NULL,
|
||||
account_id TEXT NOT NULL,
|
||||
their_service_id TEXT NOT NULL,
|
||||
key bytea NOT NULL,
|
||||
trust_level TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (account_id, their_service_id),
|
||||
FOREIGN KEY (account_id) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
|
@ -84,7 +84,7 @@ CREATE TABLE signalmeow_sender_keys (
|
|||
);
|
||||
|
||||
CREATE TABLE signalmeow_groups (
|
||||
account_id TEXT NOT NULL,
|
||||
account_id TEXT NOT NULL,
|
||||
group_identifier TEXT NOT NULL,
|
||||
master_key TEXT NOT NULL,
|
||||
|
||||
|
@ -92,17 +92,17 @@ CREATE TABLE signalmeow_groups (
|
|||
);
|
||||
|
||||
CREATE TABLE signalmeow_recipients (
|
||||
account_id TEXT NOT NULL,
|
||||
account_id TEXT NOT NULL,
|
||||
aci_uuid TEXT,
|
||||
pni_uuid TEXT,
|
||||
e164_number TEXT NOT NULL DEFAULT '',
|
||||
contact_name TEXT NOT NULL DEFAULT '',
|
||||
contact_avatar_hash TEXT NOT NULL DEFAULT '',
|
||||
e164_number TEXT NOT NULL DEFAULT '',
|
||||
contact_name TEXT NOT NULL DEFAULT '',
|
||||
contact_avatar_hash TEXT NOT NULL DEFAULT '',
|
||||
profile_key bytea,
|
||||
profile_name TEXT NOT NULL DEFAULT '',
|
||||
profile_about TEXT NOT NULL DEFAULT '',
|
||||
profile_about_emoji TEXT NOT NULL DEFAULT '',
|
||||
profile_avatar_path TEXT NOT NULL DEFAULT '',
|
||||
profile_name TEXT NOT NULL DEFAULT '',
|
||||
profile_about TEXT NOT NULL DEFAULT '',
|
||||
profile_about_emoji TEXT NOT NULL DEFAULT '',
|
||||
profile_avatar_path TEXT NOT NULL DEFAULT '',
|
||||
profile_fetched_at BIGINT,
|
||||
needs_pni_signature BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
|
@ -111,3 +111,52 @@ CREATE TABLE signalmeow_recipients (
|
|||
CONSTRAINT signalmeow_contacts_aci_unique UNIQUE (account_id, aci_uuid),
|
||||
CONSTRAINT signalmeow_contacts_pni_unique UNIQUE (account_id, pni_uuid)
|
||||
);
|
||||
|
||||
CREATE TABLE signalmeow_backup_recipient (
|
||||
account_id TEXT NOT NULL,
|
||||
recipient_id BIGINT NOT NULL,
|
||||
|
||||
aci_uuid TEXT,
|
||||
pni_uuid TEXT,
|
||||
|
||||
group_master_key TEXT,
|
||||
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (account_id, recipient_id),
|
||||
CONSTRAINT signalmeow_backup_recipient_device_fkey FOREIGN KEY (account_id)
|
||||
REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX signalmeow_backup_recipient_group_idx ON signalmeow_backup_recipient (account_id, group_master_key);
|
||||
CREATE INDEX signalmeow_backup_recipient_aci_idx ON signalmeow_backup_recipient (account_id, aci_uuid);
|
||||
|
||||
CREATE TABLE signalmeow_backup_chat (
|
||||
account_id TEXT NOT NULL,
|
||||
chat_id BIGINT NOT NULL,
|
||||
recipient_id BIGINT NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (account_id, chat_id),
|
||||
CONSTRAINT signalmeow_backup_chat_device_fkey FOREIGN KEY (account_id)
|
||||
REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT signalmeow_backup_chat_recipient_fkey FOREIGN KEY (account_id, recipient_id)
|
||||
REFERENCES signalmeow_backup_recipient (account_id, recipient_id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX signalmeow_backup_chat_recipient_id_idx ON signalmeow_backup_chat (account_id, recipient_id);
|
||||
|
||||
CREATE TABLE signalmeow_backup_message (
|
||||
account_id TEXT NOT NULL,
|
||||
chat_id BIGINT NOT NULL,
|
||||
sender_id BIGINT NOT NULL,
|
||||
message_id BIGINT NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (account_id, sender_id, message_id),
|
||||
CONSTRAINT signalmeow_backup_message_chat_fkey FOREIGN KEY (account_id, chat_id)
|
||||
REFERENCES signalmeow_backup_chat (account_id, chat_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT signalmeow_backup_message_sender_fkey FOREIGN KEY (account_id, sender_id)
|
||||
REFERENCES signalmeow_backup_recipient (account_id, recipient_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT signalmeow_backup_message_device_fkey FOREIGN KEY (account_id)
|
||||
REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX signalmeow_backup_chat_recipient_id_idx ON signalmeow_backup_chat (account_id, chat_id);
|
||||
|
|
52
pkg/signalmeow/store/upgrades/19-store-backup-data.sql
Normal file
52
pkg/signalmeow/store/upgrades/19-store-backup-data.sql
Normal file
|
@ -0,0 +1,52 @@
|
|||
-- v19 (compatible with v13+): Add tables for caching parsed backup data
|
||||
CREATE TABLE signalmeow_backup_recipient (
|
||||
account_id TEXT NOT NULL,
|
||||
recipient_id BIGINT NOT NULL,
|
||||
|
||||
aci_uuid TEXT,
|
||||
pni_uuid TEXT,
|
||||
|
||||
group_master_key TEXT,
|
||||
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (account_id, recipient_id),
|
||||
CONSTRAINT signalmeow_backup_recipient_device_fkey FOREIGN KEY (account_id)
|
||||
REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX signalmeow_backup_recipient_group_idx ON signalmeow_backup_recipient (account_id, group_master_key);
|
||||
CREATE INDEX signalmeow_backup_recipient_aci_idx ON signalmeow_backup_recipient (account_id, aci_uuid);
|
||||
|
||||
CREATE TABLE signalmeow_backup_chat (
|
||||
account_id TEXT NOT NULL,
|
||||
chat_id BIGINT NOT NULL,
|
||||
recipient_id BIGINT NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
latest_message_id BIGINT,
|
||||
total_message_count INTEGER,
|
||||
|
||||
PRIMARY KEY (account_id, chat_id),
|
||||
CONSTRAINT signalmeow_backup_chat_device_fkey FOREIGN KEY (account_id)
|
||||
REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT signalmeow_backup_chat_recipient_fkey FOREIGN KEY (account_id, recipient_id)
|
||||
REFERENCES signalmeow_backup_recipient (account_id, recipient_id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX signalmeow_backup_chat_recipient_id_idx ON signalmeow_backup_chat (account_id, recipient_id);
|
||||
|
||||
CREATE TABLE signalmeow_backup_message (
|
||||
account_id TEXT NOT NULL,
|
||||
chat_id BIGINT NOT NULL,
|
||||
sender_id BIGINT NOT NULL,
|
||||
message_id BIGINT NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (account_id, sender_id, message_id),
|
||||
CONSTRAINT signalmeow_backup_message_chat_fkey FOREIGN KEY (account_id, chat_id)
|
||||
REFERENCES signalmeow_backup_chat (account_id, chat_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT signalmeow_backup_message_sender_fkey FOREIGN KEY (account_id, sender_id)
|
||||
REFERENCES signalmeow_backup_recipient (account_id, recipient_id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT signalmeow_backup_message_device_fkey FOREIGN KEY (account_id)
|
||||
REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX signalmeow_backup_chat_recipient_id_idx ON signalmeow_backup_chat (account_id, chat_id);
|
Loading…
Add table
Reference in a new issue