diff --git a/config/bridge.go b/config/bridge.go index 149d636f..c6c96fa9 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -179,12 +179,12 @@ type DisplaynameParams struct { func (bc BridgeConfig) FormatDisplayname(contact *types.Contact) string { var buffer strings.Builder _ = bc.displaynameTemplate.Execute(&buffer, DisplaynameParams{ - ProfileName: contact.ProfileName, + ProfileName: contact.Profile.Name, ContactName: contact.ContactName, //Username: contact.Username, PhoneNumber: contact.E164, UUID: contact.UUID.String(), - AboutEmoji: contact.ProfileAboutEmoji, + AboutEmoji: contact.Profile.AboutEmoji, }) return buffer.String() } diff --git a/pkg/signalmeow/contact.go b/pkg/signalmeow/contact.go index 84f48ab7..9bad34b3 100644 --- a/pkg/signalmeow/contact.go +++ b/pkg/signalmeow/contact.go @@ -62,7 +62,7 @@ func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDeta existingContact.ContactName = contactDetails.GetName() if profileKeyString := contactDetails.GetProfileKey(); profileKeyString != nil { profileKey := libsignalgo.ProfileKey(profileKeyString) - existingContact.ProfileKey = &profileKey + existingContact.Profile.Key = &profileKey err = cli.Store.ProfileKeyStore.StoreProfileKey(ctx, existingContact.UUID, profileKey) if err != nil { log.Err(err).Msg("storing profile key") @@ -95,17 +95,17 @@ func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDeta return existingContact, nil } -func (cli *Client) fetchContactThenTryAndUpdateWithProfile(ctx context.Context, profileUUID uuid.UUID) (*types.Contact, error) { +func (cli *Client) fetchContactThenTryAndUpdateWithProfile(ctx context.Context, profileUUID uuid.UUID) (existingContact *types.Contact, otherSourceUUID uuid.UUID, err error) { log := zerolog.Ctx(ctx).With(). Str("action", "fetch contact then try and update with profile"). Stringer("profile_uuid", profileUUID). Logger() contactChanged := false - existingContact, err := cli.Store.ContactStore.LoadContact(ctx, profileUUID) + existingContact, err = cli.Store.ContactStore.LoadContact(ctx, profileUUID) if err != nil { log.Err(err).Msg("error loading contact") - return nil, err + return } if existingContact == nil { log.Debug().Msg("creating new contact") @@ -116,56 +116,49 @@ func (cli *Client) fetchContactThenTryAndUpdateWithProfile(ctx context.Context, } else { log.Debug().Msg("updating existing contact") } - profile, lastFetched, err := cli.RetrieveProfileByID(ctx, profileUUID) - if err != nil { - log.Err(err).Msg("error retrieving profile") - //return nil, nil, err + profile, lastFetched, fetchErr := cli.RetrieveProfileByID(ctx, profileUUID) + if fetchErr != nil { + log.Err(fetchErr).Msg("error retrieving profile") // Don't return here, we still want to return what we have } else if profile != nil { - if existingContact.ProfileName != profile.Name { - existingContact.ProfileName = profile.Name + if existingContact.Profile.Name != profile.Name { + existingContact.Profile.Name = profile.Name contactChanged = true } - if existingContact.ProfileAbout != profile.About { - existingContact.ProfileAbout = profile.About + if existingContact.Profile.About != profile.About { + existingContact.Profile.About = profile.About contactChanged = true } - if existingContact.ProfileAboutEmoji != profile.AboutEmoji { - existingContact.ProfileAboutEmoji = profile.AboutEmoji + if existingContact.Profile.AboutEmoji != profile.AboutEmoji { + existingContact.Profile.AboutEmoji = profile.AboutEmoji contactChanged = true } - if existingContact.ProfileAvatarPath != profile.AvatarPath { - existingContact.ProfileAvatarPath = profile.AvatarPath + if existingContact.Profile.AvatarPath != profile.AvatarPath { + existingContact.Profile.AvatarPath = profile.AvatarPath contactChanged = true } - if existingContact.ProfileKey == nil || *existingContact.ProfileKey != profile.Key { - existingContact.ProfileKey = &profile.Key + if existingContact.Profile.Key == nil || *existingContact.Profile.Key != profile.Key { + existingContact.Profile.Key = &profile.Key contactChanged = true } } if contactChanged { existingContact.ProfileFetchTs = lastFetched.UnixMilli() - err := cli.Store.ContactStore.StoreContact(ctx, *existingContact) + err = cli.Store.ContactStore.StoreContact(ctx, *existingContact) if err != nil { log.Err(err).Msg("error storing contact") - return nil, err + return } } - if err != nil { - var otherContact *types.Contact - otherContact, err = cli.Store.ContactStore.LoadContactWithLatestOtherProfile(ctx, existingContact) - if err != nil { - log.Err(err).Msg("error retrieving contact with a newer profile from other users") - } else if otherContact != nil { - existingContact.ProfileName = otherContact.ProfileName - existingContact.ProfileAbout = otherContact.ProfileAbout - existingContact.ProfileAboutEmoji = otherContact.ProfileAboutEmoji - existingContact.ProfileAvatarPath = otherContact.ProfileAvatarPath + if fetchErr != nil { + otherSourceUUID, fetchErr = cli.Store.ContactStore.UpdateContactWithLatestProfile(ctx, existingContact) + if fetchErr != nil { + log.Err(fetchErr).Msg("error retrieving latest profile for contact from other users") } } - return existingContact, nil + return } func (cli *Client) UpdateContactE164(ctx context.Context, uuid uuid.UUID, e164 string) error { @@ -195,7 +188,7 @@ func (cli *Client) UpdateContactE164(ctx context.Context, uuid uuid.UUID, e164 s return cli.Store.ContactStore.StoreContact(ctx, *existingContact) } -func (cli *Client) ContactByID(ctx context.Context, uuid uuid.UUID) (*types.Contact, error) { +func (cli *Client) ContactByID(ctx context.Context, uuid uuid.UUID) (contact *types.Contact, otherSourceUUID uuid.UUID, err error) { return cli.fetchContactThenTryAndUpdateWithProfile(ctx, uuid) } @@ -208,7 +201,7 @@ func (cli *Client) ContactByE164(ctx context.Context, e164 string) (*types.Conta if contact == nil { return nil, nil } - contact, err = cli.fetchContactThenTryAndUpdateWithProfile(ctx, contact.UUID) + contact, _, err = cli.fetchContactThenTryAndUpdateWithProfile(ctx, contact.UUID) return contact, err } diff --git a/pkg/signalmeow/profile.go b/pkg/signalmeow/profile.go index 86b28691..33a19938 100644 --- a/pkg/signalmeow/profile.go +++ b/pkg/signalmeow/profile.go @@ -35,6 +35,7 @@ import ( "github.com/rs/zerolog" "github.com/element-hq/mautrix-signal/pkg/libsignalgo" + "github.com/element-hq/mautrix-signal/pkg/signalmeow/types" "github.com/element-hq/mautrix-signal/pkg/signalmeow/web" ) @@ -71,11 +72,8 @@ type ProfileResponse struct { } type Profile struct { - Name string - About string - AboutEmoji string - AvatarPath string - Key libsignalgo.ProfileKey + types.ProfileFields + Key libsignalgo.ProfileKey } type ProfileCache struct { diff --git a/pkg/signalmeow/store/contact_store.go b/pkg/signalmeow/store/contact_store.go index 0a225a27..1f509fb9 100644 --- a/pkg/signalmeow/store/contact_store.go +++ b/pkg/signalmeow/store/contact_store.go @@ -31,7 +31,7 @@ import ( type ContactStore interface { LoadContact(ctx context.Context, theirUUID uuid.UUID) (*types.Contact, error) LoadContactByE164(ctx context.Context, e164 string) (*types.Contact, error) - LoadContactWithLatestOtherProfile(ctx context.Context, contact *types.Contact) (*types.Contact, error) + UpdateContactWithLatestProfile(ctx context.Context, contact *types.Contact) (sourceUUID uuid.UUID, err error) StoreContact(ctx context.Context, contact types.Contact) error AllContacts(ctx context.Context) ([]*types.Contact, error) UpdatePhone(ctx context.Context, theirUUID uuid.UUID, newE164 string) error @@ -58,13 +58,7 @@ const ( getAllContactsOfUserQuery = getAllContactsQuery + `WHERE our_aci_uuid = $1` getContactByUUIDQuery = getAllContactsQuery + `WHERE our_aci_uuid = $1 AND aci_uuid = $2` getContactByPhoneQuery = getAllContactsQuery + `WHERE our_aci_uuid = $1 AND e164_number = $2` - - getContactWithLatestOtherProfileQuery = getAllContactsQuery + ` - WHERE our_aci_uuid <> $1 AND aci_uuid = $2 AND LENGTH(COALESCE(profile_key, '')) > 0 - ORDER BY profile_fetch_ts DESC LIMIT 1 - ` - - upsertContactQuery = ` + upsertContactQuery = ` INSERT INTO signalmeow_contacts ( our_aci_uuid, aci_uuid, @@ -122,10 +116,10 @@ func scanContact(row dbutil.Scannable) (*types.Contact, error) { &contact.ContactName, &contact.ContactAvatar.Hash, &profileKey, - &contact.ProfileName, - &contact.ProfileAbout, - &contact.ProfileAboutEmoji, - &contact.ProfileAvatarPath, + &contact.Profile.Name, + &contact.Profile.About, + &contact.Profile.AboutEmoji, + &contact.Profile.AvatarPath, &contact.ProfileAvatarHash, &contact.ProfileFetchTs, ) @@ -136,7 +130,7 @@ func scanContact(row dbutil.Scannable) (*types.Contact, error) { } if len(profileKey) != 0 { profileKeyConverted := libsignalgo.ProfileKey(profileKey) - contact.ProfileKey = &profileKeyConverted + contact.Profile.Key = &profileKeyConverted } return &contact, err } @@ -149,8 +143,40 @@ func (s *SQLStore) LoadContactByE164(ctx context.Context, e164 string) (*types.C return scanContact(s.db.QueryRow(ctx, getContactByPhoneQuery, s.ACI, e164)) } -func (s *SQLStore) LoadContactWithLatestOtherProfile(ctx context.Context, contact *types.Contact) (*types.Contact, error) { - return scanContact(s.db.QueryRow(ctx, getContactWithLatestOtherProfileQuery, s.ACI, contact.UUID)) +func (s *SQLStore) UpdateContactWithLatestProfile(ctx context.Context, contact *types.Contact) (sourceUUID uuid.UUID, err error) { + var profileKey []byte + err = s.db.QueryRow( + ctx, + `SELECT + profile_key, + profile_name, + profile_about, + profile_about_emoji, + profile_avatar_path, + our_aci_uuid + FROM signalmeow_contacts + WHERE + our_aci_uuid <> $1 AND + aci_uuid = $2 AND + LENGTH(COALESCE(profile_key, '')) > 0 + ORDER BY profile_fetch_ts DESC LIMIT 1`, + s.ACI, + contact.UUID, + ).Scan( + &profileKey, + &contact.Profile.Name, + &contact.Profile.About, + &contact.Profile.AboutEmoji, + &contact.Profile.AvatarPath, + &sourceUUID, + ) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } else if err == nil { + profileKeyConverted := libsignalgo.ProfileKey(profileKey) + contact.Profile.Key = &profileKeyConverted + } + return } func (s *SQLStore) AllContacts(ctx context.Context) ([]*types.Contact, error) { @@ -170,11 +196,11 @@ func (s *SQLStore) StoreContact(ctx context.Context, contact types.Contact) erro contact.E164, contact.ContactName, contact.ContactAvatar.Hash, - contact.ProfileKey.Slice(), - contact.ProfileName, - contact.ProfileAbout, - contact.ProfileAboutEmoji, - contact.ProfileAvatarPath, + contact.Profile.Key.Slice(), + contact.Profile.Name, + contact.Profile.About, + contact.Profile.AboutEmoji, + contact.Profile.AvatarPath, contact.ProfileAvatarHash, contact.ProfileFetchTs, ) diff --git a/pkg/signalmeow/types/contact.go b/pkg/signalmeow/types/contact.go index c8368182..2534e5a9 100644 --- a/pkg/signalmeow/types/contact.go +++ b/pkg/signalmeow/types/contact.go @@ -32,15 +32,16 @@ type Contact struct { E164 string ContactName string ContactAvatar ContactAvatar - ProfileKey *libsignalgo.ProfileKey - ProfileName string - ProfileAbout string - ProfileAboutEmoji string - ProfileAvatarPath string + Profile ContactProfile ProfileAvatarHash string ProfileFetchTs int64 } +type ContactProfile struct { + ProfileFields + Key *libsignalgo.ProfileKey +} + type ContactAvatar struct { Image []byte ContentType string diff --git a/pkg/signalmeow/types/profile.go b/pkg/signalmeow/types/profile.go new file mode 100644 index 00000000..22d2d0e6 --- /dev/null +++ b/pkg/signalmeow/types/profile.go @@ -0,0 +1,24 @@ +// mautrix-signal - A Matrix-signal puppeting bridge. +// Copyright (C) 2023 Scott Weber, 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 . + +package types + +type ProfileFields struct { + Name string + About string + AboutEmoji string + AvatarPath string +} diff --git a/puppet.go b/puppet.go index 87cc9f52..966dc992 100644 --- a/puppet.go +++ b/puppet.go @@ -246,11 +246,23 @@ func (puppet *Puppet) UpdateInfo(ctx context.Context, source *User) { ctx = log.WithContext(ctx) var err error log.Debug().Msg("Fetching contact info to update puppet") - info, err := source.Client.ContactByID(ctx, puppet.SignalID) + info, sourceUUID, err := source.Client.ContactByID(ctx, puppet.SignalID) if err != nil { log.Err(err).Msg("Failed to fetch contact info") return } + if sourceUUID != uuid.Nil { + source = puppet.bridge.GetUserBySignalID(sourceUUID) + if source == nil || source.Client == nil { + log.Warn(). + Stringer("source_uuid", sourceUUID). + Msg("No fallback user for profile info update") + return + } + log.Debug(). + Stringer("source_mxid", source.MXID). + Msg("Using fallback user for profile info update") + } log.Trace().Msg("Updating puppet info") @@ -323,10 +335,10 @@ func (puppet *Puppet) updateAvatar(ctx context.Context, source *User, info *type puppet.AvatarSet = false puppet.AvatarPath = "" } else { - if puppet.AvatarPath == info.ProfileAvatarPath && puppet.AvatarSet { + if puppet.AvatarPath == info.Profile.AvatarPath && puppet.AvatarSet { return false } - if info.ProfileAvatarPath == "" { + if info.Profile.AvatarPath == "" { puppet.AvatarURL = id.ContentURI{} puppet.AvatarPath = "" puppet.AvatarHash = "" @@ -341,10 +353,10 @@ func (puppet *Puppet) updateAvatar(ctx context.Context, source *User, info *type return true } var err error - avatarData, err = source.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, info.ProfileKey) + avatarData, err = source.Client.DownloadUserAvatar(ctx, info.Profile.AvatarPath, info.Profile.Key) if err != nil { log.Err(err). - Str("profile_avatar_path", info.ProfileAvatarPath). + Str("profile_avatar_path", info.Profile.AvatarPath). Msg("Failed to download new user avatar") return true } @@ -360,7 +372,7 @@ func (puppet *Puppet) updateAvatar(ctx context.Context, source *User, info *type // Path changed, but actual avatar didn't return true } - puppet.AvatarPath = info.ProfileAvatarPath + puppet.AvatarPath = info.Profile.AvatarPath puppet.AvatarHash = newHash puppet.AvatarSet = false puppet.AvatarURL = id.ContentURI{}