فهرست منبع

Add periodic ghost avatar resync

Tulir Asokan 3 سال پیش
والد
کامیت
9f0901f560
8فایلهای تغییر یافته به همراه233 افزوده شده و 64 حذف شده
  1. 7 2
      commands.go
  2. 19 7
      database/portal.go
  3. 40 11
      database/puppet.go
  4. 10 4
      database/upgrades/00-latest-revision.sql
  5. 13 0
      database/upgrades/50-puppet-background-sync.sql
  6. 29 18
      portal.go
  7. 28 18
      puppet.go
  8. 87 4
      user.go

+ 7 - 2
commands.go

@@ -1069,7 +1069,7 @@ var cmdSync = &commands.FullHandler{
 
 func fnSync(ce *WrappedCommandEvent) {
 	if len(ce.Args) == 0 {
-		ce.Reply("**Usage:** `sync <appstate/contacts/groups/space> [--create-portals]`")
+		ce.Reply("**Usage:** `sync <appstate/contacts/avatars/groups/space> [--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 {

+ 19 - 7
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 {

+ 40 - 11
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)
 	}

+ 10 - 4
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,

+ 13 - 0
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<>'';

+ 29 - 18
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

+ 28 - 18
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()
 	}
 }

+ 87 - 4
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)