signalmeow: store history transfer data in db

This commit is contained in:
Tulir Asokan 2025-01-18 16:29:20 +02:00
parent 030c83c197
commit dd3aab051f
8 changed files with 515 additions and 44 deletions

View file

@ -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(&params, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))

View file

@ -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) {

View file

@ -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)

View 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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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);

View 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);