1
0
Fork 0
mirror of https://github.com/mautrix/signal.git synced 2026-05-15 05:36:53 -04:00
mautrix-signal/pkg/msgconv/from-signal-backup.go

256 lines
8.8 KiB
Go

// 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 msgconv
import (
"slices"
"github.com/google/uuid"
"go.mau.fi/util/exslices"
"go.mau.fi/util/ptr"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/backuppb"
)
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
type AttachmentMap map[uuid.UUID]*backuppb.FilePointer_LocatorInfo
func BackupToDataMessage(ci *backuppb.ChatItem, attMap AttachmentMap) (*signalpb.DataMessage, []*backuppb.Reaction) {
var dm signalpb.DataMessage
var reactions []*backuppb.Reaction
switch ti := ci.Item.(type) {
case *backuppb.ChatItem_StandardMessage:
reactions = ti.StandardMessage.Reactions
if text := ti.StandardMessage.Text; text != nil {
dm.Body = &text.Body
dm.BodyRanges = slices.DeleteFunc(exslices.CastFunc(text.BodyRanges, backupToSignalBodyRange), deleteNil)
}
dm.Attachments = make([]*signalpb.AttachmentPointer, 0, len(ti.StandardMessage.Attachments)+boolToInt(ti.StandardMessage.LongText != nil))
if ti.StandardMessage.LongText != nil {
randomUUID := uuid.New()
dm.Attachments = append(
dm.Attachments,
backupToSignalAttachment(ti.StandardMessage.LongText, 0, randomUUID, attMap),
)
}
for _, att := range ti.StandardMessage.Attachments {
var clientUUID uuid.UUID
if len(att.ClientUuid) == 16 {
clientUUID = uuid.UUID(att.ClientUuid)
} else {
clientUUID = uuid.New()
}
dm.Attachments = append(
dm.Attachments,
backupToSignalAttachment(att.Pointer, att.Flag, clientUUID, attMap),
)
}
dm.Preview = exslices.CastFunc(ti.StandardMessage.LinkPreview, func(from *backuppb.LinkPreview) *signalpb.Preview {
return backupToSignalLinkPreview(from, attMap)
})
case *backuppb.ChatItem_ContactMessage:
reactions = ti.ContactMessage.Reactions
dm.Contact = []*signalpb.DataMessage_Contact{backupToSignalContact(ti.ContactMessage.Contact, attMap)}
case *backuppb.ChatItem_StickerMessage:
reactions = ti.StickerMessage.Reactions
dm.Sticker = &signalpb.DataMessage_Sticker{
PackId: ti.StickerMessage.Sticker.PackId,
PackKey: ti.StickerMessage.Sticker.PackKey,
StickerId: &ti.StickerMessage.Sticker.StickerId,
Emoji: ti.StickerMessage.Sticker.Emoji,
Data: backupToSignalAttachment(ti.StickerMessage.Sticker.Data, 0, uuid.New(), attMap),
}
case *backuppb.ChatItem_Poll:
dm.PollCreate = &signalpb.DataMessage_PollCreate{
Question: &ti.Poll.Question,
AllowMultiple: &ti.Poll.AllowMultiple,
Options: exslices.CastFunc(ti.Poll.Options, func(from *backuppb.Poll_PollOption) string {
return from.Option
}),
}
// TODO handle votes
// TODO handle hasEnded somehow?
case *backuppb.ChatItem_RemoteDeletedMessage:
// TODO handle some other way? (also disappeared view-once messages)
return nil, nil
case *backuppb.ChatItem_PaymentNotification:
dm.Payment = &signalpb.DataMessage_Payment{
Item: &signalpb.DataMessage_Payment_Notification_{
Notification: &signalpb.DataMessage_Payment_Notification{
Transaction: nil,
Note: ti.PaymentNotification.Note,
},
},
}
case *backuppb.ChatItem_GiftBadge:
dm.GiftBadge = &signalpb.DataMessage_GiftBadge{
ReceiptCredentialPresentation: ti.GiftBadge.ReceiptCredentialPresentation,
}
case *backuppb.ChatItem_ViewOnceMessage:
reactions = ti.ViewOnceMessage.Reactions
if ti.ViewOnceMessage.Attachment == nil {
// TODO handle some other way?
return nil, reactions
}
dm.IsViewOnce = ptr.Ptr(true)
var clientUUID uuid.UUID
if len(ti.ViewOnceMessage.Attachment.ClientUuid) == 16 {
clientUUID = uuid.UUID(ti.ViewOnceMessage.Attachment.ClientUuid)
} else {
clientUUID = uuid.New()
}
dm.Attachments = []*signalpb.AttachmentPointer{backupToSignalAttachment(
ti.ViewOnceMessage.Attachment.Pointer,
ti.ViewOnceMessage.Attachment.Flag,
clientUUID,
attMap,
)}
}
return &dm, reactions
}
func backupToSignalContact(from *backuppb.ContactAttachment, attMap AttachmentMap) *signalpb.DataMessage_Contact {
var contact signalpb.DataMessage_Contact
if from.Name != nil {
contact.Name = &signalpb.DataMessage_Contact_Name{
GivenName: &from.Name.GivenName,
FamilyName: &from.Name.FamilyName,
Prefix: &from.Name.Prefix,
Suffix: &from.Name.Suffix,
MiddleName: &from.Name.MiddleName,
Nickname: &from.Name.Nickname,
}
}
contact.Number = exslices.CastFunc(from.Number, func(from *backuppb.ContactAttachment_Phone) *signalpb.DataMessage_Contact_Phone {
return &signalpb.DataMessage_Contact_Phone{
Value: &from.Value,
Type: ptr.NonZero(signalpb.DataMessage_Contact_Phone_Type(from.Type)),
Label: &from.Label,
}
})
contact.Email = exslices.CastFunc(from.Email, func(from *backuppb.ContactAttachment_Email) *signalpb.DataMessage_Contact_Email {
return &signalpb.DataMessage_Contact_Email{
Value: &from.Value,
Type: ptr.NonZero(signalpb.DataMessage_Contact_Email_Type(from.Type)),
Label: &from.Label,
}
})
contact.Address = exslices.CastFunc(from.Address, func(from *backuppb.ContactAttachment_PostalAddress) *signalpb.DataMessage_Contact_PostalAddress {
return &signalpb.DataMessage_Contact_PostalAddress{
Type: ptr.NonZero(signalpb.DataMessage_Contact_PostalAddress_Type(from.Type)),
Label: &from.Label,
Street: &from.Street,
Pobox: &from.Pobox,
Neighborhood: &from.Neighborhood,
City: &from.City,
Region: &from.Region,
Postcode: &from.Postcode,
Country: &from.Country,
}
})
if from.Avatar != nil {
contact.Avatar = &signalpb.DataMessage_Contact_Avatar{
Avatar: backupToSignalAttachment(from.Avatar, 0, uuid.New(), attMap),
}
}
contact.Organization = ptr.NonZero(from.Organization)
return &contact
}
func backupToSignalLinkPreview(from *backuppb.LinkPreview, attMap AttachmentMap) *signalpb.Preview {
var ap *signalpb.AttachmentPointer
if from.Image != nil {
ap = backupToSignalAttachment(from.Image, 0, uuid.New(), attMap)
}
return &signalpb.Preview{
Url: &from.Url,
Title: from.Title,
Image: ap,
Description: from.Description,
Date: from.Date,
}
}
func backupToSignalAttachment(
fp *backuppb.FilePointer,
flag backuppb.MessageAttachment_Flag,
clientUUID uuid.UUID,
atts AttachmentMap,
) *signalpb.AttachmentPointer {
sig := &signalpb.AttachmentPointer{
//IncrementalMacChunkSize: fp.IncrementalMacChunkSize,
ContentType: fp.ContentType,
IncrementalMac: fp.IncrementalMac,
FileName: fp.FileName,
Flags: ptr.NonZero(uint32(backupToSignalAttachmentFlag(flag))),
Width: fp.Width,
Height: fp.Height,
Caption: nil, // is this field deprecated or something?
BlurHash: fp.BlurHash,
ClientUuid: clientUUID[:],
}
if fp.LocatorInfo != nil {
if fp.LocatorInfo.TransitCdnKey != nil {
sig.AttachmentIdentifier = &signalpb.AttachmentPointer_CdnKey{CdnKey: *fp.LocatorInfo.TransitCdnKey}
sig.Size = &fp.LocatorInfo.Size
sig.Digest = fp.LocatorInfo.GetEncryptedDigest() // Note: may be nil if plaintextHash is set instead
sig.CdnNumber = fp.LocatorInfo.TransitCdnNumber
}
sig.Key = fp.LocatorInfo.Key
atts[clientUUID] = fp.LocatorInfo
}
return sig
}
func backupToSignalAttachmentFlag(flag backuppb.MessageAttachment_Flag) signalpb.AttachmentPointer_Flags {
switch flag {
case backuppb.MessageAttachment_VOICE_MESSAGE:
return signalpb.AttachmentPointer_VOICE_MESSAGE
case backuppb.MessageAttachment_BORDERLESS:
return signalpb.AttachmentPointer_BORDERLESS
case backuppb.MessageAttachment_GIF:
return signalpb.AttachmentPointer_GIF
case backuppb.MessageAttachment_NONE:
fallthrough
default:
return 0
}
}
func deleteNil(bodyRange *signalpb.BodyRange) bool {
return bodyRange == nil
}
func backupToSignalBodyRange(from *backuppb.BodyRange) *signalpb.BodyRange {
var out signalpb.BodyRange
out.Start = &from.Start
out.Length = &from.Length
switch av := from.AssociatedValue.(type) {
case *backuppb.BodyRange_MentionAci:
out.AssociatedValue = &signalpb.BodyRange_MentionAciBinary{MentionAciBinary: av.MentionAci}
case *backuppb.BodyRange_Style_:
out.AssociatedValue = &signalpb.BodyRange_Style_{Style: signalpb.BodyRange_Style(av.Style)}
}
return &out
}