Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use latest profile info #449

Closed
4 changes: 2 additions & 2 deletions config/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,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()
}
Expand Down
57 changes: 31 additions & 26 deletions pkg/signalmeow/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -116,44 +116,49 @@ func (cli *Client) fetchContactThenTryAndUpdateWithProfile(ctx context.Context,
} else {
log.Debug().Msg("updating existing contact")
}
profile, 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
}

if profile != nil {
if existingContact.ProfileName != profile.Name {
existingContact.ProfileName = profile.Name
} else if profile != nil {
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 {
err := cli.Store.ContactStore.StoreContact(ctx, *existingContact)
existingContact.ProfileFetchTs = lastFetched.UnixMilli()
err = cli.Store.ContactStore.StoreContact(ctx, *existingContact)
if err != nil {
log.Err(err).Msg("error storing contact")
return nil, err
return
}
}
return existingContact, nil

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
}

func (cli *Client) UpdateContactE164(ctx context.Context, uuid uuid.UUID, e164 string) error {
Expand Down Expand Up @@ -183,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)
}

Expand All @@ -196,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
}

Expand Down
28 changes: 15 additions & 13 deletions pkg/signalmeow/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/rs/zerolog"

"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -118,7 +116,7 @@ func (cli *Client) ProfileKeyForSignalID(ctx context.Context, signalACI uuid.UUI

var errProfileKeyNotFound = errors.New("profile key not found")

func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID) (*Profile, error) {
func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID) (*Profile, time.Time, error) {
if cli.ProfileCache == nil {
cli.ProfileCache = &ProfileCache{
profiles: make(map[string]*Profile),
Expand All @@ -133,33 +131,34 @@ func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID)
if ok && time.Since(lastFetched) < 1*time.Hour {
profile, ok := cli.ProfileCache.profiles[signalID.String()]
if ok {
return profile, nil
return profile, lastFetched, nil
}
err, ok := cli.ProfileCache.errors[signalID.String()]
if ok {
return nil, *err
return nil, lastFetched, *err
}
}

// If we get here, we don't have a cached profile, so fetch it
profile, err := cli.fetchProfileByID(ctx, signalID)
lastFetched = time.Now()
if err != nil {
// If we get a 401 or 5xx error, we should not retry until the cache expires
if strings.HasPrefix(err.Error(), "401") || strings.HasPrefix(err.Error(), "5") {
cli.ProfileCache.errors[signalID.String()] = &err
cli.ProfileCache.lastFetched[signalID.String()] = time.Now()
cli.ProfileCache.lastFetched[signalID.String()] = lastFetched
}
return nil, err
return nil, lastFetched, err
}
if profile == nil {
return nil, errProfileKeyNotFound
return nil, lastFetched, errProfileKeyNotFound
}

// If we get here, we have a valid profile, so cache it
cli.ProfileCache.profiles[signalID.String()] = profile
cli.ProfileCache.lastFetched[signalID.String()] = time.Now()
cli.ProfileCache.lastFetched[signalID.String()] = lastFetched

return profile, nil
return profile, lastFetched, nil
}

func (cli *Client) fetchProfileByID(ctx context.Context, signalID uuid.UUID) (*Profile, error) {
Expand Down Expand Up @@ -248,6 +247,9 @@ func (cli *Client) fetchProfileByID(ctx context.Context, signalID uuid.UUID) (*P
}

func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, profileKey *libsignalgo.ProfileKey) ([]byte, error) {
if profileKey == nil {
return nil, fmt.Errorf("failed to prepare request: profileKey is nil")
}
username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Expand Down
75 changes: 59 additions & 16 deletions pkg/signalmeow/store/contact_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +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)
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
Expand All @@ -50,7 +51,8 @@ const (
profile_about,
profile_about_emoji,
profile_avatar_path,
profile_avatar_hash
profile_avatar_hash,
profile_fetch_ts
FROM signalmeow_contacts
`
getAllContactsOfUserQuery = getAllContactsQuery + `WHERE our_aci_uuid = $1`
Expand All @@ -68,9 +70,10 @@ const (
profile_about,
profile_about_emoji,
profile_avatar_path,
profile_avatar_hash
profile_avatar_hash,
profile_fetch_ts
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (our_aci_uuid, aci_uuid) DO UPDATE SET
e164_number = excluded.e164_number,
contact_name = excluded.contact_name,
Expand All @@ -80,7 +83,8 @@ const (
profile_about = excluded.profile_about,
profile_about_emoji = excluded.profile_about_emoji,
profile_avatar_path = excluded.profile_avatar_path,
profile_avatar_hash = excluded.profile_avatar_hash
profile_avatar_hash = excluded.profile_avatar_hash,
profile_fetch_ts = excluded.profile_fetch_ts
`
upsertContactPhoneQuery = `
INSERT INTO signalmeow_contacts (
Expand All @@ -94,9 +98,10 @@ const (
profile_about,
profile_about_emoji,
profile_avatar_path,
profile_avatar_hash
profile_avatar_hash,
profile_fetch_ts
)
VALUES ($1, $2, $3, '', '', NULL, '', '', '', '', '')
VALUES ($1, $2, $3, '', '', NULL, '', '', '', '', '', 0)
ON CONFLICT (our_aci_uuid, aci_uuid) DO UPDATE
SET e164_number = excluded.e164_number
`
Expand All @@ -111,11 +116,12 @@ 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,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
Expand All @@ -124,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
}
Expand All @@ -137,6 +143,42 @@ func (s *SQLStore) LoadContactByE164(ctx context.Context, e164 string) (*types.C
return scanContact(s.db.QueryRow(ctx, getContactByPhoneQuery, s.ACI, e164))
}

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) {
rows, err := s.db.Query(ctx, getAllContactsOfUserQuery, s.ACI)
if err != nil {
Expand All @@ -154,12 +196,13 @@ 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,
)
return err
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/signalmeow/store/upgrades/00-latest.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- v0 -> v6: Latest revision
-- v0 -> v7: Latest revision
CREATE TABLE signalmeow_device (
aci_uuid TEXT PRIMARY KEY,

Expand Down Expand Up @@ -88,6 +88,7 @@ CREATE TABLE signalmeow_contacts (
profile_about_emoji TEXT,
profile_avatar_path TEXT NOT NULL DEFAULT '',
profile_avatar_hash TEXT,
profile_fetch_ts BIGINT NOT NULL DEFAULT 0,

PRIMARY KEY (our_aci_uuid, aci_uuid),
FOREIGN KEY (our_aci_uuid) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE
Expand Down
2 changes: 2 additions & 0 deletions pkg/signalmeow/store/upgrades/07-profile-fetch-timestamps.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- v7 (compatible with v5+): Save profile fetch timestamp
ALTER TABLE signalmeow_contacts ADD COLUMN profile_fetch_ts BIGINT NOT NULL DEFAULT 0;
12 changes: 7 additions & 5 deletions pkg/signalmeow/types/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ 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 {
Expand Down
Loading
Loading