From a78a88c7412fbf1cbb55525169a9307dacf170e7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 28 Feb 2024 15:21:24 +0200 Subject: [PATCH 1/2] Prevent downgrading ghost user info by default (#464) Fixes #396 Closes #449 --- config/bridge.go | 5 +- config/upgrade.go | 1 + database/puppet.go | 28 +++++---- database/upgrades/00-latest.sql | 7 ++- .../upgrades/20-puppet-profile-fetch-ts.sql | 2 + example-config.yaml | 2 + go.mod | 4 +- go.sum | 8 +-- pkg/libsignalgo/profilekey.go | 12 +++- pkg/signalmeow/contact.go | 57 +++++++------------ pkg/signalmeow/profile.go | 43 +++++++------- pkg/signalmeow/store/contact_store.go | 44 ++++++++------ pkg/signalmeow/store/upgrades/00-latest.sql | 23 ++++---- .../store/upgrades/08-profile-fetch-time.sql | 11 ++++ .../store/upgrades/08-resync-schema-449.sql | 12 ++++ pkg/signalmeow/types/contact.go | 34 +++++++---- puppet.go | 21 +++++-- 17 files changed, 185 insertions(+), 129 deletions(-) create mode 100644 database/upgrades/20-puppet-profile-fetch-ts.sql create mode 100644 pkg/signalmeow/store/upgrades/08-profile-fetch-time.sql create mode 100644 pkg/signalmeow/store/upgrades/08-resync-schema-449.sql diff --git a/config/bridge.go b/config/bridge.go index 5adc6351..088d2869 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -35,6 +35,7 @@ type BridgeConfig struct { DisplaynameTemplate string `yaml:"displayname_template"` PrivateChatPortalMeta string `yaml:"private_chat_portal_meta"` UseContactAvatars bool `yaml:"use_contact_avatars"` + UseOutdatedProfiles bool `yaml:"use_outdated_profiles"` NumberInTopic bool `yaml:"number_in_topic"` NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"` @@ -169,12 +170,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/config/upgrade.go b/config/upgrade.go index eb924d2c..cb0ab06c 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -83,6 +83,7 @@ func DoUpgrade(helper *up.Helper) { } helper.Copy(up.Str, "bridge", "private_chat_portal_meta") helper.Copy(up.Bool, "bridge", "use_contact_avatars") + helper.Copy(up.Bool, "bridge", "use_outdated_profiles") helper.Copy(up.Bool, "bridge", "number_in_topic") helper.Copy(up.Str, "bridge", "note_to_self_avatar") helper.Copy(up.Int, "bridge", "portal_message_buffer") diff --git a/database/puppet.go b/database/puppet.go index 8c300dfa..99a65ffe 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -19,6 +19,7 @@ package database import ( "context" "database/sql" + "time" "github.com/google/uuid" "go.mau.fi/util/dbutil" @@ -28,7 +29,7 @@ import ( const ( puppetBaseSelect = ` SELECT uuid, number, name, name_quality, avatar_path, avatar_hash, avatar_url, name_set, avatar_set, - contact_info_set, is_registered, custom_mxid, access_token + contact_info_set, is_registered, profile_fetched_at, custom_mxid, access_token FROM puppet ` getPuppetBySignalIDQuery = puppetBaseSelect + `WHERE uuid=$1` @@ -38,18 +39,18 @@ const ( updatePuppetQuery = ` UPDATE puppet SET number=$2, name=$3, name_quality=$4, avatar_path=$5, avatar_hash=$6, avatar_url=$7, - name_set=$8, avatar_set=$9, contact_info_set=$10, is_registered=$11, - custom_mxid=$12, access_token=$13 + name_set=$8, avatar_set=$9, contact_info_set=$10, is_registered=$11, profile_fetched_at=$12, + custom_mxid=$13, access_token=$14 WHERE uuid=$1 ` insertPuppetQuery = ` INSERT INTO puppet ( uuid, number, name, name_quality, avatar_path, avatar_hash, avatar_url, - name_set, avatar_set, contact_info_set, is_registered, + name_set, avatar_set, contact_info_set, is_registered, profile_fetched_at, custom_mxid, access_token ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14 ) ` ) @@ -71,11 +72,12 @@ type Puppet struct { NameSet bool AvatarSet bool - IsRegistered bool + IsRegistered bool + ContactInfoSet bool + ProfileFetchedAt time.Time - CustomMXID id.UserID - AccessToken string - ContactInfoSet bool + CustomMXID id.UserID + AccessToken string } func newPuppet(qh *dbutil.QueryHelper[*Puppet]) *Puppet { @@ -100,6 +102,7 @@ func (pq *PuppetQuery) GetAllWithCustomMXID(ctx context.Context) ([]*Puppet, err func (p *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) { var number, customMXID sql.NullString + var profileFetchedAt sql.NullInt64 err := row.Scan( &p.SignalID, &number, @@ -112,14 +115,18 @@ func (p *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) { &p.AvatarSet, &p.ContactInfoSet, &p.IsRegistered, + &profileFetchedAt, &customMXID, &p.AccessToken, ) if err != nil { - return nil, nil + return nil, err } p.Number = number.String p.CustomMXID = id.UserID(customMXID.String) + if profileFetchedAt.Valid { + p.ProfileFetchedAt = time.UnixMilli(profileFetchedAt.Int64) + } return p, nil } @@ -136,6 +143,7 @@ func (p *Puppet) sqlVariables() []any { p.AvatarSet, p.ContactInfoSet, p.IsRegistered, + dbutil.UnixMilliPtr(p.ProfileFetchedAt), dbutil.StrPtr(p.CustomMXID), p.AccessToken, } diff --git a/database/upgrades/00-latest.sql b/database/upgrades/00-latest.sql index ae8e157e..4dec3ff8 100644 --- a/database/upgrades/00-latest.sql +++ b/database/upgrades/00-latest.sql @@ -1,4 +1,4 @@ --- v0 -> v19 (compatible with v17+): Latest revision +-- v0 -> v20 (compatible with v17+): Latest revision CREATE TABLE portal ( chat_id TEXT NOT NULL, @@ -33,8 +33,9 @@ CREATE TABLE puppet ( name_set BOOLEAN NOT NULL DEFAULT false, avatar_set BOOLEAN NOT NULL DEFAULT false, - is_registered BOOLEAN NOT NULL DEFAULT false, - contact_info_set BOOLEAN NOT NULL DEFAULT false, + is_registered BOOLEAN NOT NULL DEFAULT false, + contact_info_set BOOLEAN NOT NULL DEFAULT false, + profile_fetched_at BIGINT, custom_mxid TEXT, access_token TEXT NOT NULL, diff --git a/database/upgrades/20-puppet-profile-fetch-ts.sql b/database/upgrades/20-puppet-profile-fetch-ts.sql new file mode 100644 index 00000000..b398b2f6 --- /dev/null +++ b/database/upgrades/20-puppet-profile-fetch-ts.sql @@ -0,0 +1,2 @@ +-- v20 (compatible with v17+): Add profile fetch timestamp for puppets +ALTER TABLE puppet ADD profile_fetched_at BIGINT; diff --git a/example-config.yaml b/example-config.yaml index 086d6a52..a509a0c4 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -106,6 +106,8 @@ bridge: private_chat_portal_meta: default # Should avatars from the user's contact list be used? This is not safe on multi-user instances. use_contact_avatars: false + # Should the bridge sync ghost user info even if profile fetching fails? This is not safe on multi-user instances. + use_outdated_profiles: false # Should the Signal user's phone number be included in the room topic in private chat portal rooms? number_in_topic: true # Avatar image for the Note to Self room. diff --git a/go.mod b/go.mod index 59bd60f5..8e3abaa8 100644 --- a/go.mod +++ b/go.mod @@ -15,12 +15,12 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.17.1 - go.mau.fi/util v0.4.0 + go.mau.fi/util v0.4.1-0.20240222202553-953608f657a3 golang.org/x/crypto v0.19.0 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a golang.org/x/net v0.21.0 google.golang.org/protobuf v1.32.0 - maunium.net/go/mautrix v0.18.0-beta.1 + maunium.net/go/mautrix v0.18.0-beta.1.0.20240223191208-581aa8015501 nhooyr.io/websocket v1.8.10 ) diff --git a/go.sum b/go.sum index 8ff1bae5..db6ca004 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.mau.fi/util v0.4.0 h1:S2X3qU4pUcb/vxBRfAuZjbrR9xVMAXSjQojNBLPBbhs= -go.mau.fi/util v0.4.0/go.mod h1:leeiHtgVBuN+W9aDii3deAXnfC563iN3WK6BF8/AjNw= +go.mau.fi/util v0.4.1-0.20240222202553-953608f657a3 h1:NcRrdzORHKab5bP1Z8BpH0nxsxsvH0iPPZLpOUN+UIc= +go.mau.fi/util v0.4.1-0.20240222202553-953608f657a3/go.mod h1:leeiHtgVBuN+W9aDii3deAXnfC563iN3WK6BF8/AjNw= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= @@ -99,7 +99,7 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= -maunium.net/go/mautrix v0.18.0-beta.1 h1:YAr4PxmcrJzUHR56p/MLDQ0qS7PsvaqXERAtC4aSkYA= -maunium.net/go/mautrix v0.18.0-beta.1/go.mod h1:1Q8P5C/uNmSBmull6DSqcawpg/E7hcGLQCD+JoU+vUo= +maunium.net/go/mautrix v0.18.0-beta.1.0.20240223191208-581aa8015501 h1:3STixn49dd7VXL+p4hW0AEWy5/BeZlgA3i3BVsIgtqM= +maunium.net/go/mautrix v0.18.0-beta.1.0.20240223191208-581aa8015501/go.mod h1:1Q8P5C/uNmSBmull6DSqcawpg/E7hcGLQCD+JoU+vUo= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/pkg/libsignalgo/profilekey.go b/pkg/libsignalgo/profilekey.go index 2cd10005..ba43a60a 100644 --- a/pkg/libsignalgo/profilekey.go +++ b/pkg/libsignalgo/profilekey.go @@ -35,19 +35,25 @@ type ProfileKeyCommitment [C.SignalPROFILE_KEY_COMMITMENT_LEN]byte type ProfileKeyVersion [C.SignalPROFILE_KEY_VERSION_ENCODED_LEN]byte type AccessKey [C.SignalACCESS_KEY_LEN]byte +var blankProfileKey ProfileKey + +func (pk *ProfileKey) IsEmpty() bool { + return pk == nil || *pk == blankProfileKey +} + func (ak *AccessKey) String() string { - return string((*ak)[:]) + return string(ak[:]) } func (pv *ProfileKeyVersion) String() string { - return string((*pv)[:]) + return string(pv[:]) } func (pk *ProfileKey) Slice() []byte { if pk == nil { return nil } - return (*pk)[:] + return pk[:] } func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error) { diff --git a/pkg/signalmeow/contact.go b/pkg/signalmeow/contact.go index 7682c990..c4eacf2e 100644 --- a/pkg/signalmeow/contact.go +++ b/pkg/signalmeow/contact.go @@ -22,6 +22,7 @@ import ( "crypto/sha256" "encoding/binary" "encoding/hex" + "errors" "fmt" "net/http" "strings" @@ -46,27 +47,23 @@ func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDeta Logger() existingContact, err := cli.Store.ContactStore.LoadContact(ctx, parsedUUID) if err != nil { - log.Err(err).Msg("error loading contact") + log.Err(err).Msg("Failed to load contact from database") return nil, err } if existingContact == nil { - log.Debug().Msg("creating new contact") existingContact = &types.Contact{ UUID: parsedUUID, } - } else { - log.Debug().Msg("updating existing contact") } existingContact.E164 = contactDetails.GetNumber() 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") - //return *existingContact, nil, err + log.Err(err).Msg("Failed to store profile key from contact") } } @@ -86,10 +83,9 @@ func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDeta } } - log.Debug().Msg("storing contact") storeErr := cli.Store.ContactStore.StoreContact(ctx, *existingContact) if storeErr != nil { - log.Err(storeErr).Msg("error storing contact") + log.Err(storeErr).Msg("Failed to save contact") return existingContact, storeErr } return existingContact, nil @@ -104,7 +100,7 @@ func (cli *Client) fetchContactThenTryAndUpdateWithProfile(ctx context.Context, existingContact, err := cli.Store.ContactStore.LoadContact(ctx, profileUUID) if err != nil { - log.Err(err).Msg("error loading contact") + log.Err(err).Msg("Failed to load contact from database") return nil, err } if existingContact == nil { @@ -118,38 +114,26 @@ func (cli *Client) fetchContactThenTryAndUpdateWithProfile(ctx context.Context, } profile, err := cli.RetrieveProfileByID(ctx, profileUUID) if err != nil { - log.Err(err).Msg("error retrieving profile") - //return nil, nil, err - // Don't return here, we still want to return what we have + logLevel := zerolog.ErrorLevel + if errors.Is(err, errProfileKeyNotFound) { + logLevel = zerolog.DebugLevel + } + log.WithLevel(logLevel).Err(err).Msg("Failed to fetch profile") + // Continue to return contact without profile } if profile != nil { - if existingContact.ProfileName != profile.Name { - existingContact.ProfileName = profile.Name - contactChanged = true - } - if existingContact.ProfileAbout != profile.About { - existingContact.ProfileAbout = profile.About - contactChanged = true - } - if existingContact.ProfileAboutEmoji != profile.AboutEmoji { - existingContact.ProfileAboutEmoji = profile.AboutEmoji - contactChanged = true - } - if existingContact.ProfileAvatarPath != profile.AvatarPath { - existingContact.ProfileAvatarPath = profile.AvatarPath - contactChanged = true - } - if existingContact.ProfileKey == nil || *existingContact.ProfileKey != profile.Key { - existingContact.ProfileKey = &profile.Key + // Don't bother saving every fetched timestamp to the database, but save if anything else changed + if !existingContact.Profile.Equals(profile) || existingContact.Profile.FetchedAt.IsZero() { contactChanged = true } + existingContact.Profile = *profile } if contactChanged { - err := cli.Store.ContactStore.StoreContact(ctx, *existingContact) + err = cli.Store.ContactStore.StoreContact(ctx, *existingContact) if err != nil { - log.Err(err).Msg("error storing contact") + log.Err(err).Msg("Failed to save contact") return nil, err } } @@ -164,21 +148,18 @@ func (cli *Client) UpdateContactE164(ctx context.Context, uuid uuid.UUID, e164 s Logger() existingContact, err := cli.Store.ContactStore.LoadContact(ctx, uuid) if err != nil { - log.Err(err).Msg("error loading contact") + log.Err(err).Msg("Failed to load contact from database") return err } if existingContact == nil { - log.Debug().Msg("creating new contact") existingContact = &types.Contact{ UUID: uuid, } - } else { - log.Debug().Msg("found existing contact") } if existingContact.E164 == e164 { return nil } - log.Debug().Msg("e164 changed for contact") + log.Debug().Msg("Contact phone number changed") existingContact.E164 = e164 return cli.Store.ContactStore.StoreContact(ctx, *existingContact) } diff --git a/pkg/signalmeow/profile.go b/pkg/signalmeow/profile.go index 69cb240c..f27db40e 100644 --- a/pkg/signalmeow/profile.go +++ b/pkg/signalmeow/profile.go @@ -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" ) @@ -70,16 +71,8 @@ type ProfileResponse struct { //PaymentAddress []byte `json:"paymentAddress"` } -type Profile struct { - Name string - About string - AboutEmoji string - AvatarPath string - Key libsignalgo.ProfileKey -} - type ProfileCache struct { - profiles map[string]*Profile + profiles map[string]*types.Profile errors map[string]*error lastFetched map[string]time.Time } @@ -118,10 +111,10 @@ 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) (*types.Profile, error) { if cli.ProfileCache == nil { cli.ProfileCache = &ProfileCache{ - profiles: make(map[string]*Profile), + profiles: make(map[string]*types.Profile), errors: make(map[string]*error), lastFetched: make(map[string]time.Time), } @@ -144,6 +137,7 @@ func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID) // If we get here, we don't have a cached profile, so fetch it profile, err := cli.fetchProfileByID(ctx, signalID) if err != nil { + // TODO this check is wrong and most likely doesn't work, errors shouldn't use string comparisons // 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 @@ -151,9 +145,6 @@ func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID) } return nil, err } - if profile == nil { - return nil, errProfileKeyNotFound - } // If we get here, we have a valid profile, so cache it cli.ProfileCache.profiles[signalID.String()] = profile @@ -162,15 +153,13 @@ func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID) return profile, nil } -func (cli *Client) fetchProfileByID(ctx context.Context, signalID uuid.UUID) (*Profile, error) { +func (cli *Client) fetchProfileByID(ctx context.Context, signalID uuid.UUID) (*types.Profile, error) { log := zerolog.Ctx(ctx) profileKey, err := cli.ProfileKeyForSignalID(ctx, signalID) if err != nil { return nil, fmt.Errorf("error getting profile key: %w", err) - } - if profileKey == nil { - log.Warn().Msg("profileKey is nil") - return nil, nil + } else if profileKey == nil { + return nil, errProfileKeyNotFound } profileKeyVersion, err := profileKey.GetProfileKeyVersion(signalID) @@ -211,7 +200,17 @@ func (cli *Client) fetchProfileByID(ctx context.Context, signalID uuid.UUID) (*P if err != nil { return nil, fmt.Errorf("error sending request: %w", err) } - log.Trace().Msg("Got profile response") + var profile types.Profile + profile.FetchedAt = time.Now() + logEvt := log.Trace().Uint32("status_code", resp.GetStatus()) + if logEvt.Enabled() { + if json.Valid(resp.Body) { + logEvt.RawJSON("response_data", resp.Body) + } else { + logEvt.Str("invalid_response_data", base64.StdEncoding.EncodeToString(resp.Body)) + } + } + logEvt.Msg("Got profile response") if *resp.Status < 200 || *resp.Status >= 300 { return nil, fmt.Errorf("error getting profile (unsuccessful status code %d)", *resp.Status) } @@ -220,7 +219,6 @@ func (cli *Client) fetchProfileByID(ctx context.Context, signalID uuid.UUID) (*P if err != nil { return nil, fmt.Errorf("error unmarshalling profile response: %w", err) } - var profile Profile if len(profileResponse.Name) > 0 { profile.Name, err = decryptString(profileKey, profileResponse.Name) if err != nil { @@ -241,13 +239,14 @@ func (cli *Client) fetchProfileByID(ctx context.Context, signalID uuid.UUID) (*P return nil, fmt.Errorf("error decrypting profile aboutEmoji: %w", err) } } + // TODO store other metadata fields? profile.AvatarPath = profileResponse.Avatar profile.Key = *profileKey return &profile, nil } -func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, profileKey *libsignalgo.ProfileKey) ([]byte, error) { +func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, profileKey libsignalgo.ProfileKey) ([]byte, error) { username, password := cli.Store.BasicAuthCreds() opts := &web.HTTPReqOpt{ Host: web.CDN1Hostname, diff --git a/pkg/signalmeow/store/contact_store.go b/pkg/signalmeow/store/contact_store.go index 2d7247f3..f556124f 100644 --- a/pkg/signalmeow/store/contact_store.go +++ b/pkg/signalmeow/store/contact_store.go @@ -20,6 +20,7 @@ import ( "context" "database/sql" "errors" + "time" "github.com/google/uuid" "go.mau.fi/util/dbutil" @@ -50,7 +51,7 @@ const ( profile_about, profile_about_emoji, profile_avatar_path, - profile_avatar_hash + profile_fetched_at FROM signalmeow_contacts ` getAllContactsOfUserQuery = getAllContactsQuery + `WHERE our_aci_uuid = $1` @@ -68,7 +69,7 @@ const ( profile_about, profile_about_emoji, profile_avatar_path, - profile_avatar_hash + profile_fetched_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (our_aci_uuid, aci_uuid) DO UPDATE SET @@ -80,7 +81,7 @@ 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_fetched_at = excluded.profile_fetched_at ` upsertContactPhoneQuery = ` INSERT INTO signalmeow_contacts ( @@ -94,9 +95,9 @@ const ( profile_about, profile_about_emoji, profile_avatar_path, - profile_avatar_hash + profile_fetched_at ) - VALUES ($1, $2, $3, '', '', NULL, '', '', '', '', '') + VALUES ($1, $2, $3, '', '', NULL, '', '', '', '', NULL) ON CONFLICT (our_aci_uuid, aci_uuid) DO UPDATE SET e164_number = excluded.e164_number ` @@ -105,26 +106,29 @@ const ( func scanContact(row dbutil.Scannable) (*types.Contact, error) { var contact types.Contact var profileKey []byte + var profileFetchedAt sql.NullInt64 err := row.Scan( &contact.UUID, &contact.E164, &contact.ContactName, &contact.ContactAvatar.Hash, &profileKey, - &contact.ProfileName, - &contact.ProfileAbout, - &contact.ProfileAboutEmoji, - &contact.ProfileAvatarPath, - &contact.ProfileAvatarHash, + &contact.Profile.Name, + &contact.Profile.About, + &contact.Profile.AboutEmoji, + &contact.Profile.AvatarPath, + &profileFetchedAt, ) if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil { return nil, err } + if profileFetchedAt.Valid { + contact.Profile.FetchedAt = time.UnixMilli(profileFetchedAt.Int64) + } if len(profileKey) != 0 { - profileKeyConverted := libsignalgo.ProfileKey(profileKey) - contact.ProfileKey = &profileKeyConverted + contact.Profile.Key = libsignalgo.ProfileKey(profileKey) } return &contact, err } @@ -146,6 +150,10 @@ func (s *SQLStore) AllContacts(ctx context.Context) ([]*types.Contact, error) { } func (s *SQLStore) StoreContact(ctx context.Context, contact types.Contact) error { + var profileKey []byte + if contact.Profile.Key.IsEmpty() { + profileKey = contact.Profile.Key[:] + } _, err := s.db.Exec( ctx, upsertContactQuery, @@ -154,12 +162,12 @@ 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.ProfileAvatarHash, + profileKey, + contact.Profile.Name, + contact.Profile.About, + contact.Profile.AboutEmoji, + contact.Profile.AvatarPath, + dbutil.UnixMilliPtr(contact.Profile.FetchedAt), ) return err } diff --git a/pkg/signalmeow/store/upgrades/00-latest.sql b/pkg/signalmeow/store/upgrades/00-latest.sql index 73d02701..37e6e71b 100644 --- a/pkg/signalmeow/store/upgrades/00-latest.sql +++ b/pkg/signalmeow/store/upgrades/00-latest.sql @@ -1,4 +1,4 @@ --- v0 -> v6: Latest revision +-- v0 -> v7: Latest revision CREATE TABLE signalmeow_device ( aci_uuid TEXT PRIMARY KEY, @@ -76,18 +76,17 @@ CREATE TABLE signalmeow_groups ( ); CREATE TABLE signalmeow_contacts ( - our_aci_uuid TEXT NOT NULL, - aci_uuid TEXT NOT NULL, - -- TODO make all fields not null - e164_number TEXT, - contact_name TEXT, - contact_avatar_hash TEXT, + our_aci_uuid TEXT NOT NULL, + aci_uuid TEXT NOT NULL, + e164_number TEXT NOT NULL, + contact_name TEXT NOT NULL, + contact_avatar_hash TEXT NOT NULL, profile_key bytea, - profile_name TEXT, - profile_about TEXT, - profile_about_emoji TEXT, - profile_avatar_path TEXT NOT NULL DEFAULT '', - profile_avatar_hash TEXT, + profile_name TEXT NOT NULL, + profile_about TEXT NOT NULL, + profile_about_emoji TEXT NOT NULL, + profile_avatar_path TEXT NOT NULL, + profile_fetched_at BIGINT, PRIMARY KEY (our_aci_uuid, aci_uuid), FOREIGN KEY (our_aci_uuid) REFERENCES signalmeow_device (aci_uuid) ON DELETE CASCADE ON UPDATE CASCADE diff --git a/pkg/signalmeow/store/upgrades/08-profile-fetch-time.sql b/pkg/signalmeow/store/upgrades/08-profile-fetch-time.sql new file mode 100644 index 00000000..414245ca --- /dev/null +++ b/pkg/signalmeow/store/upgrades/08-profile-fetch-time.sql @@ -0,0 +1,11 @@ +-- v6 -> v8: Add profile_fetched_at and make other columns not null +ALTER TABLE signalmeow_contacts DROP COLUMN profile_avatar_hash; +ALTER TABLE signalmeow_contacts ADD COLUMN profile_fetched_at BIGINT; +-- only: postgres until "end only" +ALTER TABLE signalmeow_contacts ALTER COLUMN e164_number SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN contact_name SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN contact_avatar_hash SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_name SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about_emoji SET NOT NULL; +-- end only postgres diff --git a/pkg/signalmeow/store/upgrades/08-resync-schema-449.sql b/pkg/signalmeow/store/upgrades/08-resync-schema-449.sql new file mode 100644 index 00000000..95af7759 --- /dev/null +++ b/pkg/signalmeow/store/upgrades/08-resync-schema-449.sql @@ -0,0 +1,12 @@ +-- v7 -> v8: Migration from https://github.com/mautrix/signal/pull/449 to match the new v8 upgrade +ALTER TABLE signalmeow_contacts DROP COLUMN profile_avatar_hash; +ALTER TABLE signalmeow_contacts RENAME COLUMN profile_fetch_ts TO profile_fetched_at; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_fetched_at DROP DEFAULT; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_fetched_at DROP NOT NULL; +UPDATE signalmeow_contacts SET profile_fetched_at = NULL WHERE profile_fetched_at <= 0; +ALTER TABLE signalmeow_contacts ALTER COLUMN e164_number SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN contact_name SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN contact_avatar_hash SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_name SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about SET NOT NULL; +ALTER TABLE signalmeow_contacts ALTER COLUMN profile_about_emoji SET NOT NULL; diff --git a/pkg/signalmeow/types/contact.go b/pkg/signalmeow/types/contact.go index 11a73fb6..f00c89b8 100644 --- a/pkg/signalmeow/types/contact.go +++ b/pkg/signalmeow/types/contact.go @@ -17,27 +17,41 @@ package types import ( + "time" + "github.com/google/uuid" "go.mau.fi/mautrix-signal/pkg/libsignalgo" ) +type Profile struct { + Name string + About string + AboutEmoji string + AvatarPath string + Key libsignalgo.ProfileKey + FetchedAt time.Time +} + +func (p *Profile) Equals(other *Profile) bool { + return p.Name == other.Name && + p.About == other.About && + p.AboutEmoji == other.AboutEmoji && + p.AvatarPath == other.AvatarPath && + p.Key == other.Key +} + // The Contact struct combines information from two sources: // - A Signal "contact": contact info harvested from our user's phone's contact list // - A Signal "profile": contact info entered by the target user when registering for Signal // Users of this Contact struct should prioritize "contact" information, but fall back // to "profile" information if the contact information is not available. type Contact struct { - UUID uuid.UUID - E164 string - ContactName string - ContactAvatar ContactAvatar - ProfileKey *libsignalgo.ProfileKey - ProfileName string - ProfileAbout string - ProfileAboutEmoji string - ProfileAvatarPath string - ProfileAvatarHash string + UUID uuid.UUID + E164 string + ContactName string + ContactAvatar ContactAvatar + Profile Profile } type ContactAvatar struct { diff --git a/puppet.go b/puppet.go index 34f08ee7..1ecc2409 100644 --- a/puppet.go +++ b/puppet.go @@ -245,10 +245,21 @@ func (puppet *Puppet) UpdateInfo(ctx context.Context, source *User) { log.Err(err).Msg("Failed to fetch contact info") return } + if !puppet.bridge.Config.Bridge.UseOutdatedProfiles && puppet.ProfileFetchedAt.After(info.Profile.FetchedAt) { + log.Debug(). + Time("contact_profile_fetched_at", info.Profile.FetchedAt). + Time("puppet_profile_fetched_at", puppet.ProfileFetchedAt). + Msg("Ignoring outdated contact info") + return + } log.Trace().Msg("Updating puppet info") update := false + if puppet.ProfileFetchedAt.IsZero() && !info.Profile.FetchedAt.IsZero() { + update = true + } + puppet.ProfileFetchedAt = info.Profile.FetchedAt if info.E164 != "" && puppet.Number != info.E164 { puppet.Number = info.E164 update = true @@ -317,10 +328,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 = "" @@ -335,10 +346,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 } @@ -354,7 +365,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{} From 26e27fe9f2db56543d6a0b461b22aaa7a9bfb1f5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 28 Feb 2024 18:56:38 +0200 Subject: [PATCH 2/2] Fix initial schema for signalmeow databases --- pkg/signalmeow/store/upgrades/00-latest.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/signalmeow/store/upgrades/00-latest.sql b/pkg/signalmeow/store/upgrades/00-latest.sql index 37e6e71b..6c7dc4bb 100644 --- a/pkg/signalmeow/store/upgrades/00-latest.sql +++ b/pkg/signalmeow/store/upgrades/00-latest.sql @@ -1,4 +1,4 @@ --- v0 -> v7: Latest revision +-- v0 -> v8: Latest revision CREATE TABLE signalmeow_device ( aci_uuid TEXT PRIMARY KEY,