mautrix-signal/pkg/signalmeow/contact.go

168 lines
5.7 KiB
Go

// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2023 Scott Weber
//
// 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 signalmeow
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"net/http"
"strings"
"github.com/google/uuid"
"github.com/rs/zerolog"
"google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)
func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDetails *signalpb.ContactDetails, avatar *[]byte) (*types.Recipient, error) {
parsedUUID, err := uuid.Parse(contactDetails.GetAci())
if err != nil {
return nil, err
}
ctx = zerolog.Ctx(ctx).With().
Str("action", "store contact details as contact").
Stringer("uuid", parsedUUID).
Logger().WithContext(ctx)
return cli.Store.RecipientStore.LoadAndUpdateRecipient(ctx, parsedUUID, uuid.Nil, func(recipient *types.Recipient) (bool, error) {
if contactDetails.GetNumber() != "" {
recipient.E164 = contactDetails.GetNumber()
}
recipient.ContactName = contactDetails.GetName()
if profileKeyString := contactDetails.GetProfileKey(); profileKeyString != nil {
profileKey := libsignalgo.ProfileKey(profileKeyString)
recipient.Profile.Key = profileKey
}
if avatar != nil && *avatar != nil && len(*avatar) > 0 {
rawHash := sha256.Sum256(*avatar)
avatarHash := hex.EncodeToString(rawHash[:])
var contentType string
if avatarDetails := contactDetails.GetAvatar(); avatarDetails != nil && !strings.HasSuffix(avatarDetails.GetContentType(), "/*") {
contentType = *avatarDetails.ContentType
} else {
contentType = http.DetectContentType(*avatar)
}
recipient.ContactAvatar = types.ContactAvatar{
Image: *avatar,
ContentType: contentType,
Hash: avatarHash,
}
}
return true, nil
})
}
func (cli *Client) fetchContactThenTryAndUpdateWithProfile(ctx context.Context, aci uuid.UUID) (*types.Recipient, error) {
log := zerolog.Ctx(ctx).With().
Str("action", "fetch contact then try and update with profile").
Stringer("profile_aci", aci).
Logger()
ctx = log.WithContext(ctx)
profile, err := cli.RetrieveProfileByID(ctx, aci)
if err != nil {
log.Debug().Err(err).Msg("Failed to fetch profile")
// Continue to return contact without profile
}
return cli.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, uuid.Nil, func(recipient *types.Recipient) (changed bool, err error) {
if profile != nil {
// Don't bother saving every fetched timestamp to the database, but save if anything else changed
if !recipient.Profile.Equals(profile) || recipient.Profile.FetchedAt.IsZero() {
changed = true
}
recipient.Profile = *profile
}
return
})
}
func (cli *Client) ContactByACI(ctx context.Context, aci uuid.UUID) (*types.Recipient, error) {
return cli.fetchContactThenTryAndUpdateWithProfile(ctx, aci)
}
func (cli *Client) ContactByE164(ctx context.Context, e164 string) (*types.Recipient, error) {
contact, err := cli.Store.RecipientStore.LoadRecipientByE164(ctx, e164)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("ContactByE164 error loading contact")
return nil, err
}
if contact == nil {
return nil, nil
}
if contact.ACI != uuid.Nil {
contact, err = cli.fetchContactThenTryAndUpdateWithProfile(ctx, contact.ACI)
}
return contact, err
}
// UnmarshalContactDetailsMessages unmarshals a slice of ContactDetails messages from a byte buffer.
func unmarshalContactDetailsMessages(byteStream []byte) ([]*signalpb.ContactDetails, [][]byte, error) {
var contactDetailsList []*signalpb.ContactDetails
var avatarList [][]byte
buf := bytes.NewBuffer(byteStream)
for {
// If no more bytes are left to read, break the loop
if buf.Len() == 0 {
break
}
// Read the length prefix (varint) of the next Protobuf message
msgLen, err := binary.ReadUvarint(buf)
if err != nil {
return nil, nil, fmt.Errorf("Failed to read message length: %v", err)
}
// If no more bytes are left to read, break the loop
if buf.Len() == 0 {
break
}
// Read the Protobuf message using the length obtained
msgBytes := buf.Next(int(msgLen))
// Unmarshal the Protobuf message into a ContactDetails object
contactDetails := &signalpb.ContactDetails{}
if err := proto.Unmarshal(msgBytes, contactDetails); err != nil {
return nil, nil, fmt.Errorf("Failed to unmarshal ContactDetails: %v", err)
}
// Append the ContactDetails object to the result slice
contactDetailsList = append(contactDetailsList, contactDetails)
// If the ContactDetails object has an avatar, read it into a byte slice
if contactDetails.Avatar != nil && contactDetails.Avatar.Length != nil && *contactDetails.Avatar.Length > 0 {
avatarBytes := buf.Next(int(*contactDetails.Avatar.Length))
// TODO why is this making a copy?
avatarBytesCopy := make([]byte, len(avatarBytes))
copy(avatarBytesCopy, avatarBytes)
avatarList = append(avatarList, avatarBytesCopy)
} else {
// If there isn't, append nil so the indicies line up
avatarList = append(avatarList, nil)
}
}
return contactDetailsList, avatarList, nil
}