diff --git a/commands.go b/commands.go index 2bc6f73..577bd22 100644 --- a/commands.go +++ b/commands.go @@ -1069,7 +1069,7 @@ var cmdSync = &commands.FullHandler{ func fnSync(ce *WrappedCommandEvent) { if len(ce.Args) == 0 { - ce.Reply("**Usage:** `sync [--create-portals]`") + ce.Reply("**Usage:** `sync [--contact-avatars] [--create-portals]`") return } args := strings.ToLower(strings.Join(ce.Args, " ")) @@ -1078,6 +1078,11 @@ func fnSync(ce *WrappedCommandEvent) { space := strings.Contains(args, "space") groups := strings.Contains(args, "groups") || space createPortals := strings.Contains(args, "--create-portals") + contactAvatars := strings.Contains(args, "--contact-avatars") + if contactAvatars && (!contacts || appState) { + ce.Reply("`--contact-avatars` can only be used with `sync contacts`") + return + } if appState { for _, name := range appstate.AllPatchNames { @@ -1094,7 +1099,7 @@ func fnSync(ce *WrappedCommandEvent) { } } } else if contacts { - err := ce.User.ResyncContacts() + err := ce.User.ResyncContacts(contactAvatars) if err != nil { ce.Reply("Error resyncing contacts: %v", err) } else { diff --git a/database/portal.go b/database/portal.go index 62802d0..a0fe982 100644 --- a/database/portal.go +++ b/database/portal.go @@ -64,7 +64,7 @@ func (pq *PortalQuery) New() *Portal { } } -const portalColumns = "jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id, relay_user_id, expiration_time" +const portalColumns = "jid, receiver, mxid, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, encrypted, first_event_id, next_batch_id, relay_user_id, expiration_time" func (pq *PortalQuery) GetAll() []*Portal { return pq.getAll(fmt.Sprintf("SELECT %s FROM portal", portalColumns)) @@ -135,9 +135,12 @@ type Portal struct { MXID id.RoomID Name string + NameSet bool Topic string + TopicSet bool Avatar string AvatarURL id.ContentURI + AvatarSet bool Encrypted bool FirstEventID id.EventID @@ -150,7 +153,7 @@ type Portal struct { func (portal *Portal) Scan(row dbutil.Scannable) *Portal { var mxid, avatarURL, firstEventID, nextBatchID, relayUserID sql.NullString - err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted, &firstEventID, &nextBatchID, &relayUserID, &portal.ExpirationTime) + err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.NameSet, &portal.Topic, &portal.TopicSet, &portal.Avatar, &avatarURL, &portal.AvatarSet, &portal.Encrypted, &firstEventID, &nextBatchID, &relayUserID, &portal.ExpirationTime) if err != nil { if err != sql.ErrNoRows { portal.log.Errorln("Database scan failed:", err) @@ -180,8 +183,14 @@ func (portal *Portal) relayUserPtr() *id.UserID { } func (portal *Portal) Insert() { - _, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id, relay_user_id, expiration_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", - portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime) + _, err := portal.db.Exec(` + INSERT INTO portal (jid, receiver, mxid, name, name_set, topic, topic_set, avatar, avatar_url, avatar_set, + encrypted, first_event_id, next_batch_id, relay_user_id, expiration_time) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + `, + portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.NameSet, portal.Topic, portal.TopicSet, + portal.Avatar, portal.AvatarURL.String(), portal.AvatarSet, portal.Encrypted, portal.FirstEventID.String(), + portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime) if err != nil { portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err) } @@ -190,11 +199,14 @@ func (portal *Portal) Insert() { func (portal *Portal) Update(txn *sql.Tx) { query := ` UPDATE portal - SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6, first_event_id=$7, next_batch_id=$8, relay_user_id=$9, expiration_time=$10 - WHERE jid=$11 AND receiver=$12 + SET mxid=$1, name=$2, name_set=$3, topic=$4, topic_set=$5, avatar=$6, avatar_url=$7, avatar_set=$8, + encrypted=$9, first_event_id=$10, next_batch_id=$11, relay_user_id=$12, expiration_time=$13 + WHERE jid=$14 AND receiver=$15 ` args := []interface{}{ - portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime, portal.Key.JID, portal.Key.Receiver, + portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, + portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime, + portal.Key.JID, portal.Key.Receiver, } var err error if txn != nil { diff --git a/database/puppet.go b/database/puppet.go index f7261ed..806df8d 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -18,6 +18,7 @@ package database import ( "database/sql" + "time" log "maunium.net/go/maulogger/v2" @@ -43,7 +44,7 @@ func (pq *PuppetQuery) New() *Puppet { } func (pq *PuppetQuery) GetAll() (puppets []*Puppet) { - rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet") + rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, name_set, avatar_set, last_sync, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet") if err != nil || rows == nil { return nil } @@ -55,7 +56,7 @@ func (pq *PuppetQuery) GetAll() (puppets []*Puppet) { } func (pq *PuppetQuery) Get(jid types.JID) *Puppet { - row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE username=$1", jid.User) + row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, name_set, avatar_set, last_sync, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE username=$1", jid.User) if row == nil { return nil } @@ -63,7 +64,7 @@ func (pq *PuppetQuery) Get(jid types.JID) *Puppet { } func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet { - row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid=$1", mxid) + row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, name_set, avatar_set, last_sync, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid=$1", mxid) if row == nil { return nil } @@ -71,7 +72,7 @@ func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet { } func (pq *PuppetQuery) GetAllWithCustomMXID() (puppets []*Puppet) { - rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid<>''") + rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, name_set, avatar_set, last_sync, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid<>''") if err != nil || rows == nil { return nil } @@ -89,8 +90,11 @@ type Puppet struct { JID types.JID Avatar string AvatarURL id.ContentURI + AvatarSet bool Displayname string NameQuality int8 + NameSet bool + LastSync time.Time CustomMXID id.UserID AccessToken string @@ -101,10 +105,10 @@ type Puppet struct { func (puppet *Puppet) Scan(row dbutil.Scannable) *Puppet { var displayname, avatar, avatarURL, customMXID, accessToken, nextBatch sql.NullString - var quality sql.NullInt64 - var enablePresence, enableReceipts sql.NullBool + var quality, lastSync sql.NullInt64 + var enablePresence, enableReceipts, nameSet, avatarSet sql.NullBool var username string - err := row.Scan(&username, &avatar, &avatarURL, &displayname, &quality, &customMXID, &accessToken, &nextBatch, &enablePresence, &enableReceipts) + err := row.Scan(&username, &avatar, &avatarURL, &displayname, &quality, &nameSet, &avatarSet, &lastSync, &customMXID, &accessToken, &nextBatch, &enablePresence, &enableReceipts) if err != nil { if err != sql.ErrNoRows { puppet.log.Errorln("Database scan failed:", err) @@ -116,6 +120,11 @@ func (puppet *Puppet) Scan(row dbutil.Scannable) *Puppet { puppet.Avatar = avatar.String puppet.AvatarURL, _ = id.ParseContentURI(avatarURL.String) puppet.NameQuality = int8(quality.Int64) + puppet.NameSet = nameSet.Bool + puppet.AvatarSet = avatarSet.Bool + if lastSync.Int64 > 0 { + puppet.LastSync = time.Unix(lastSync.Int64, 0) + } puppet.CustomMXID = id.UserID(customMXID.String) puppet.AccessToken = accessToken.String puppet.NextBatch = nextBatch.String @@ -129,16 +138,36 @@ func (puppet *Puppet) Insert() { puppet.log.Warnfln("Not inserting %s: not a user", puppet.JID) return } - _, err := puppet.db.Exec("INSERT INTO puppet (username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", - puppet.JID.User, puppet.Avatar, puppet.AvatarURL.String(), puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts) + var lastSyncTs int64 + if !puppet.LastSync.IsZero() { + lastSyncTs = puppet.LastSync.Unix() + } + _, err := puppet.db.Exec(` + INSERT INTO puppet (username, avatar, avatar_url, avatar_set, displayname, name_quality, name_set, last_sync, + custom_mxid, access_token, next_batch, enable_presence, enable_receipts) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + `, puppet.JID.User, puppet.Avatar, puppet.AvatarURL.String(), puppet.AvatarSet, puppet.Displayname, + puppet.NameQuality, puppet.NameSet, lastSyncTs, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, + puppet.EnablePresence, puppet.EnableReceipts, + ) if err != nil { puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err) } } func (puppet *Puppet) Update() { - _, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7, enable_presence=$8, enable_receipts=$9 WHERE username=$10", - puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL.String(), puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts, puppet.JID.User) + var lastSyncTs int64 + if !puppet.LastSync.IsZero() { + lastSyncTs = puppet.LastSync.Unix() + } + _, err := puppet.db.Exec(` + UPDATE puppet + SET displayname=$1, name_quality=$2, name_set=$3, avatar=$4, avatar_url=$5, avatar_set=$6, last_sync=$7, + custom_mxid=$8, access_token=$9, next_batch=$10, enable_presence=$11, enable_receipts=$12 + WHERE username=$13 + `, puppet.Displayname, puppet.NameQuality, puppet.NameSet, puppet.Avatar, puppet.AvatarURL.String(), puppet.AvatarSet, + lastSyncTs, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts, + puppet.JID.User) if err != nil { puppet.log.Warnfln("Failed to update %s: %v", puppet.JID, err) } diff --git a/database/upgrades/00-latest-revision.sql b/database/upgrades/00-latest-revision.sql index b4b3830..36860f5 100644 --- a/database/upgrades/00-latest-revision.sql +++ b/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v49: Latest revision +-- v0 -> v50: Latest revision CREATE TABLE "user" ( mxid TEXT PRIMARY KEY, @@ -19,10 +19,13 @@ CREATE TABLE portal ( jid TEXT, receiver TEXT, mxid TEXT UNIQUE, - name TEXT NOT NULL, - topic TEXT NOT NULL, - avatar TEXT NOT NULL, + name TEXT NOT NULL, + name_set BOOLEAN NOT NULL DEFAULT false, + topic TEXT NOT NULL, + topic_set BOOLEAN NOT NULL DEFAULT false, + avatar TEXT NOT NULL, avatar_url TEXT, + avatar_set BOOLEAN NOT NULL DEFAULT false, encrypted BOOLEAN NOT NULL DEFAULT false, first_event_id TEXT, @@ -39,6 +42,9 @@ CREATE TABLE puppet ( name_quality SMALLINT, avatar TEXT, avatar_url TEXT, + name_set BOOLEAN NOT NULL DEFAULT false, + avatar_set BOOLEAN NOT NULL DEFAULT false, + last_sync BIGINT NOT NULL DEFAULT 0, custom_mxid TEXT, access_token TEXT, diff --git a/database/upgrades/50-puppet-background-sync.sql b/database/upgrades/50-puppet-background-sync.sql new file mode 100644 index 0000000..86f9be0 --- /dev/null +++ b/database/upgrades/50-puppet-background-sync.sql @@ -0,0 +1,13 @@ +-- v50: Add last sync timestamp for puppets + +ALTER TABLE puppet ADD COLUMN last_sync BIGINT NOT NULL DEFAULT 0; +ALTER TABLE puppet ADD COLUMN name_set BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE puppet ADD COLUMN avatar_set BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE portal ADD COLUMN name_set BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE portal ADD COLUMN avatar_set BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE portal ADD COLUMN topic_set BOOLEAN NOT NULL DEFAULT false; +UPDATE puppet SET name_set=true WHERE displayname<>''; +UPDATE puppet SET avatar_set=true WHERE avatar<>''; +UPDATE portal SET name_set=true WHERE name<>''; +UPDATE portal SET avatar_set=true WHERE avatar<>''; +UPDATE portal SET topic_set=true WHERE topic<>''; diff --git a/portal.go b/portal.go index b1b2dcc..62f5de2 100644 --- a/portal.go +++ b/portal.go @@ -811,20 +811,20 @@ func (portal *Portal) markHandled(txn *sql.Tx, msg *database.Message, info *type return msg } -func (portal *Portal) getMessagePuppet(user *User, info *types.MessageInfo) *Puppet { +func (portal *Portal) getMessagePuppet(user *User, info *types.MessageInfo) (puppet *Puppet) { if info.IsFromMe { return portal.bridge.GetPuppetByJID(user.JID) } else if portal.IsPrivateChat() { - return portal.bridge.GetPuppetByJID(portal.Key.JID) + puppet = portal.bridge.GetPuppetByJID(portal.Key.JID) } else { - puppet := portal.bridge.GetPuppetByJID(info.Sender) - if puppet == nil { - portal.log.Warnfln("Message %+v doesn't seem to have a valid sender (%s): puppet is nil", *info, info.Sender) - return nil - } - puppet.SyncContact(user, true, true, "handling message") - return puppet + puppet = portal.bridge.GetPuppetByJID(info.Sender) } + if puppet == nil { + portal.log.Warnfln("Message %+v doesn't seem to have a valid sender (%s): puppet is nil", *info, info.Sender) + return nil + } + puppet.SyncContact(user, true, true, "handling message") + return puppet } func (portal *Portal) getMessageIntent(user *User, info *types.MessageInfo) *appservice.IntentAPI { @@ -939,12 +939,12 @@ func (portal *Portal) UpdateAvatar(user *User, setBy types.JID, updateInfo bool) } return false } else if avatar == nil { - if portal.Avatar == "remove" { + if portal.Avatar == "remove" && portal.AvatarSet { return false } portal.AvatarURL = id.ContentURI{} avatar = &types.ProfilePictureInfo{ID: "remove"} - } else if avatar.ID == portal.Avatar { + } else if avatar.ID == portal.Avatar && portal.AvatarSet { return false } else if len(avatar.URL) == 0 { portal.log.Warnln("Didn't get URL in response to avatar query") @@ -957,6 +957,8 @@ func (portal *Portal) UpdateAvatar(user *User, setBy types.JID, updateInfo bool) } portal.AvatarURL = url } + portal.Avatar = avatar.ID + portal.AvatarSet = false if len(portal.MXID) > 0 { intent := portal.MainIntent() @@ -970,11 +972,13 @@ func (portal *Portal) UpdateAvatar(user *User, setBy types.JID, updateInfo bool) if err != nil { portal.log.Warnln("Failed to set room avatar:", err) return false + } else { + portal.AvatarSet = true } } - portal.Avatar = avatar.ID if updateInfo { portal.UpdateBridgeInfo() + portal.Update(nil) } return true } @@ -983,9 +987,10 @@ func (portal *Portal) UpdateName(name string, setBy types.JID, updateInfo bool) if name == "" && portal.IsBroadcastList() { name = UnnamedBroadcastName } - if portal.Name != name { - portal.log.Debugfln("Updating name %s -> %s", portal.Name, name) + if portal.Name != name || !portal.NameSet { + portal.log.Debugfln("Updating name %q -> %q", portal.Name, name) portal.Name = name + portal.NameSet = false intent := portal.MainIntent() if !setBy.IsEmpty() { @@ -996,12 +1001,13 @@ func (portal *Portal) UpdateName(name string, setBy types.JID, updateInfo bool) _, err = portal.MainIntent().SetRoomName(portal.MXID, name) } if err == nil { + portal.NameSet = true if updateInfo { portal.UpdateBridgeInfo() + portal.Update(nil) } return true } else { - portal.Name = "" portal.log.Warnln("Failed to set room name:", err) } } @@ -1009,9 +1015,10 @@ func (portal *Portal) UpdateName(name string, setBy types.JID, updateInfo bool) } func (portal *Portal) UpdateTopic(topic string, setBy types.JID, updateInfo bool) bool { - if portal.Topic != topic { - portal.log.Debugfln("Updating topic %s -> %s", portal.Topic, topic) + if portal.Topic != topic || !portal.TopicSet { + portal.log.Debugfln("Updating topic %q -> %q", portal.Topic, topic) portal.Topic = topic + portal.TopicSet = false intent := portal.MainIntent() if !setBy.IsEmpty() { @@ -1022,12 +1029,13 @@ func (portal *Portal) UpdateTopic(topic string, setBy types.JID, updateInfo bool _, err = portal.MainIntent().SetRoomTopic(portal.MXID, topic) } if err == nil { + portal.TopicSet = true if updateInfo { portal.UpdateBridgeInfo() + portal.Update(nil) } return true } else { - portal.Topic = "" portal.log.Warnln("Failed to set room topic:", err) } } @@ -1376,6 +1384,7 @@ func (portal *Portal) CreateMatrixRoom(user *User, groupInfo *types.GroupInfo, i Parsed: event.RoomAvatarEventContent{URL: portal.AvatarURL}, }, }) + portal.AvatarSet = true } var invite []id.UserID @@ -1410,6 +1419,8 @@ func (portal *Portal) CreateMatrixRoom(user *User, groupInfo *types.GroupInfo, i if err != nil { return err } + portal.NameSet = len(portal.Name) > 0 + portal.TopicSet = len(portal.Topic) > 0 portal.MXID = resp.RoomID portal.bridge.portalsLock.Lock() portal.bridge.portalsByMXID[portal.MXID] = portal diff --git a/puppet.go b/puppet.go index 386a41f..df73a47 100644 --- a/puppet.go +++ b/puppet.go @@ -244,21 +244,22 @@ func (puppet *Puppet) UpdateAvatar(source *User) bool { puppet.log.Warnln("Failed to get avatar URL:", err) } else if puppet.Avatar == "" { puppet.Avatar = "unauthorized" + puppet.AvatarSet = false return true } return false } else if avatar == nil { - if puppet.Avatar == "remove" { + if puppet.Avatar == "remove" && puppet.AvatarSet { return false } puppet.AvatarURL = id.ContentURI{} avatar = &types.ProfilePictureInfo{ID: "remove"} - } else if avatar.ID == puppet.Avatar { + } else if avatar.ID == puppet.Avatar && puppet.AvatarSet { return false } else if len(avatar.URL) == 0 { puppet.log.Warnln("Didn't get URL in response to avatar query") return false - } else { + } else if avatar.ID != puppet.Avatar || puppet.AvatarURL.IsEmpty() { url, err := reuploadAvatar(puppet.DefaultIntent(), avatar.URL) if err != nil { puppet.log.Warnln("Failed to reupload avatar:", err) @@ -267,27 +268,31 @@ func (puppet *Puppet) UpdateAvatar(source *User) bool { puppet.AvatarURL = url } + puppet.Avatar = avatar.ID + puppet.AvatarSet = false err = puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL) if err != nil { puppet.log.Warnln("Failed to set avatar:", err) + } else { + puppet.log.Debugln("Updated avatar", puppet.Avatar, "->", avatar.ID) + puppet.AvatarSet = true } - puppet.log.Debugln("Updated avatar", puppet.Avatar, "->", avatar.ID) - puppet.Avatar = avatar.ID go puppet.updatePortalAvatar() return true } -func (puppet *Puppet) UpdateName(source *User, contact types.ContactInfo) bool { +func (puppet *Puppet) UpdateName(contact types.ContactInfo) bool { newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(puppet.JID, contact) - if puppet.Displayname != newName && quality >= puppet.NameQuality { + if (puppet.Displayname != newName || !puppet.NameSet) && quality >= puppet.NameQuality { + puppet.Displayname = newName + puppet.NameQuality = quality + puppet.NameSet = false err := puppet.DefaultIntent().SetDisplayName(newName) if err == nil { puppet.log.Debugln("Updated name", puppet.Displayname, "->", newName) - puppet.Displayname = newName - puppet.NameQuality = quality + puppet.NameSet = true go puppet.updatePortalName() - puppet.Update() } else { puppet.log.Warnln("Failed to set display name:", err) } @@ -336,6 +341,7 @@ func (puppet *Puppet) updatePortalName() { func (puppet *Puppet) SyncContact(source *User, onlyIfNoName, shouldHavePushName bool, reason string) { if onlyIfNoName && len(puppet.Displayname) > 0 && (!shouldHavePushName || puppet.NameQuality > config.NameQualityPhone) { + source.EnqueuePuppetResync(puppet) return } @@ -345,10 +351,10 @@ func (puppet *Puppet) SyncContact(source *User, onlyIfNoName, shouldHavePushName } else if !contact.Found { puppet.log.Warnfln("No contact info found through %s in SyncContact (sync reason: %s)", source.MXID, reason) } - puppet.Sync(source, contact) + puppet.Sync(source, &contact, false) } -func (puppet *Puppet) Sync(source *User, contact types.ContactInfo) { +func (puppet *Puppet) Sync(source *User, contact *types.ContactInfo, forceAvatarSync bool) { puppet.syncLock.Lock() defer puppet.syncLock.Unlock() err := puppet.DefaultIntent().EnsureRegistered() @@ -356,16 +362,20 @@ func (puppet *Puppet) Sync(source *User, contact types.ContactInfo) { puppet.log.Errorln("Failed to ensure registered:", err) } - if puppet.JID.User == source.JID.User { - contact.PushName = source.Client.Store.PushName - } + puppet.log.Debugfln("Syncing info through %s", source.JID) update := false - update = puppet.UpdateName(source, contact) || update - if len(puppet.Avatar) == 0 || puppet.bridge.Config.Bridge.UserAvatarSync { + if contact != nil { + if puppet.JID.User == source.JID.User { + contact.PushName = source.Client.Store.PushName + } + update = puppet.UpdateName(*contact) || update + } + if len(puppet.Avatar) == 0 || forceAvatarSync || puppet.bridge.Config.Bridge.UserAvatarSync { update = puppet.UpdateAvatar(source) || update } - if update { + if update || puppet.LastSync.Add(24*time.Hour).Before(time.Now()) { + puppet.LastSync = time.Now() puppet.Update() } } diff --git a/user.go b/user.go index 63bd3f2..d49565c 100644 --- a/user.go +++ b/user.go @@ -83,6 +83,11 @@ type User struct { BackfillQueue *BackfillQueue BridgeState *bridge.BridgeStateQueue + + puppetResyncQueue []*Puppet + puppetResyncQueueDedup map[types.JID]struct{} + puppetResyncQueueLock sync.Mutex + nextPuppetResync time.Time } func (br *WABridge) getUserByMXID(userID id.UserID, onlyIfExists bool) *User { @@ -216,6 +221,8 @@ func (br *WABridge) NewUser(dbUser *database.User) *User { historySyncs: make(chan *events.HistorySync, 32), lastPresence: types.PresenceUnavailable, + + puppetResyncQueueDedup: make(map[types.JID]struct{}), } user.PermissionLevel = user.bridge.Config.Bridge.Permissions.Get(user.MXID) @@ -223,9 +230,85 @@ func (br *WABridge) NewUser(dbUser *database.User) *User { user.Whitelisted = user.PermissionLevel >= bridgeconfig.PermissionLevelUser user.Admin = user.PermissionLevel >= bridgeconfig.PermissionLevelAdmin user.BridgeState = br.NewBridgeStateQueue(user, user.log) + go user.puppetResyncLoop() return user } +const puppetSyncMinInterval = 7 * 24 * time.Hour +const puppetSyncLoopInterval = 4 * time.Hour + +func (user *User) puppetResyncLoop() { + user.nextPuppetResync = time.Now().Add(puppetSyncLoopInterval) + for { + time.Sleep(user.nextPuppetResync.Sub(time.Now())) + user.nextPuppetResync = time.Now().Add(puppetSyncLoopInterval) + user.doPuppetResync() + } +} + +func (user *User) EnqueuePuppetResync(puppet *Puppet) { + if puppet.LastSync.Add(puppetSyncMinInterval).After(time.Now()) { + return + } + user.puppetResyncQueueLock.Lock() + if _, exists := user.puppetResyncQueueDedup[puppet.JID]; !exists { + user.puppetResyncQueueDedup[puppet.JID] = struct{}{} + user.puppetResyncQueue = append(user.puppetResyncQueue, puppet) + user.log.Infofln("Enqueued resync for %s (next sync in %s)", puppet.JID, user.nextPuppetResync.Sub(time.Now())) + } + user.puppetResyncQueueLock.Unlock() +} + +func (user *User) doPuppetResync() { + if !user.IsLoggedIn() { + return + } + user.puppetResyncQueueLock.Lock() + if len(user.puppetResyncQueue) == 0 { + user.puppetResyncQueueLock.Unlock() + return + } + queue := user.puppetResyncQueue + user.puppetResyncQueue = nil + user.puppetResyncQueueDedup = make(map[types.JID]struct{}) + user.puppetResyncQueueLock.Unlock() + var jids []types.JID + var filteredPuppets []*Puppet + for _, puppet := range queue { + if puppet.LastSync.Add(puppetSyncMinInterval).After(time.Now()) { + user.log.Debugfln("Not resyncing %s, last sync was %s ago", puppet.JID, time.Now().Sub(puppet.LastSync)) + continue + } + jids = append(jids, puppet.JID) + filteredPuppets = append(filteredPuppets, puppet) + } + if len(jids) == 0 { + user.log.Debugfln("Skipping background sync, all puppets in queue have been synced in the past 3 days") + return + } + user.log.Debugfln("Doing background sync for %+v", jids) + infos, err := user.Client.GetUserInfo(jids) + if err != nil { + user.log.Errorfln("Error getting user info for background sync: %v", err) + return + } + for _, puppet := range filteredPuppets { + info, ok := infos[puppet.JID] + if !ok { + user.log.Warnfln("Didn't get info for %s in background sync", puppet.JID) + continue + } + var contactPtr *types.ContactInfo + contact, err := user.Session.Contacts.GetContact(puppet.JID) + if err != nil { + user.log.Warnfln("Failed to get contact info for %s in background sync: %v", puppet.JID, err) + } else if contact.Found { + contactPtr = &contact + } + puppet.Sync(user, contactPtr, info.PictureID != puppet.Avatar) + } +} + func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) (ok bool) { inviteContent := event.Content{ Parsed: &event.MemberEventContent{ @@ -670,7 +753,7 @@ func (user *User) HandleEvent(event interface{}) { } } else if v.Name == appstate.WAPatchCriticalUnblockLow { go func() { - err := user.ResyncContacts() + err := user.ResyncContacts(false) if err != nil { user.log.Errorln("Failed to resync puppets: %v", err) } @@ -1020,7 +1103,7 @@ func (user *User) syncPuppet(jid types.JID, reason string) { user.bridge.GetPuppetByJID(jid).SyncContact(user, false, false, reason) } -func (user *User) ResyncContacts() error { +func (user *User) ResyncContacts(forceAvatarSync bool) error { contacts, err := user.Client.Store.Contacts.GetAllContacts() if err != nil { return fmt.Errorf("failed to get cached contacts: %w", err) @@ -1029,7 +1112,7 @@ func (user *User) ResyncContacts() error { for jid, contact := range contacts { puppet := user.bridge.GetPuppetByJID(jid) if puppet != nil { - puppet.Sync(user, contact) + puppet.Sync(user, &contact, forceAvatarSync) } else { user.log.Warnfln("Got a nil puppet for %s while syncing contacts", jid) } @@ -1196,7 +1279,7 @@ func (user *User) handlePictureUpdate(evt *events.Picture) { puppet := user.bridge.GetPuppetByJID(evt.JID) user.log.Debugfln("Received picture update for puppet %s (current: %s, new: %s)", evt.JID, puppet.Avatar, evt.PictureID) if puppet.Avatar != evt.PictureID { - puppet.UpdateAvatar(user) + puppet.Sync(user, nil, true) } } else if portal := user.GetPortalByJID(evt.JID); portal != nil { user.log.Debugfln("Received picture update for portal %s (current: %s, new: %s)", evt.JID, portal.Avatar, evt.PictureID)