Просмотр исходного кода

Add option to not re-sync chat info and user avatars to avoid rate limits

Tulir Asokan 4 лет назад
Родитель
Сommit
2188dc7701
9 измененных файлов с 233 добавлено и 102 удалено
  1. 4 0
      config/bridge.go
  2. 5 0
      database/message.go
  3. 7 0
      example-config.yaml
  4. 2 2
      go.mod
  5. 6 0
      go.sum
  6. 5 16
      matrix.go
  7. 165 68
      portal.go
  8. 14 7
      puppet.go
  9. 25 9
      user.go

+ 4 - 0
config/bridge.go

@@ -58,6 +58,8 @@ type BridgeConfig struct {
 	HistoryDisableNotifs bool   `yaml:"initial_history_disable_notifications"`
 	RecoverChatSync      int    `yaml:"recovery_chat_sync_count"`
 	RecoverHistory       bool   `yaml:"recovery_history_backfill"`
+	ChatMetaSync         bool   `yaml:"chat_meta_sync"`
+	UserAvatarSync       bool   `yaml:"user_avatar_sync"`
 	SyncChatMaxAge       uint64 `yaml:"sync_max_chat_age"`
 
 	SyncWithCustomPuppets bool   `yaml:"sync_with_custom_puppets"`
@@ -116,6 +118,8 @@ func (bc *BridgeConfig) setDefaults() {
 	bc.InitialHistoryFill = 20
 	bc.RecoverChatSync = -1
 	bc.RecoverHistory = true
+	bc.ChatMetaSync = true
+	bc.UserAvatarSync = true
 	bc.SyncChatMaxAge = 259200
 
 	bc.SyncWithCustomPuppets = true

+ 5 - 0
database/message.go

@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"database/sql"
 	"encoding/json"
+	"strings"
 
 	waProto "github.com/Rhymen/go-whatsapp/binary/proto"
 
@@ -93,6 +94,10 @@ type Message struct {
 	Content   *waProto.Message
 }
 
+func (msg *Message) IsFakeMXID() bool {
+	return strings.HasPrefix(msg.MXID.String(), "net.maunium.whatsapp.fake::")
+}
+
 func (msg *Message) Scan(row Scannable) *Message {
 	var content []byte
 	err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &msg.Timestamp, &content)

+ 7 - 0
example-config.yaml

@@ -134,6 +134,13 @@ bridge:
     recovery_chat_sync_limit: -1
     # Whether or not to sync history when recovering from downtime.
     recovery_history_backfill: true
+    # Whether or not portal info should be fetched from the server when syncing,
+    # instead of relying on finding any changes in the message history.
+    # If you get 599 errors often, you should try disabling this.
+    chat_meta_sync: true
+    # Whether or not puppet avatars should be fetched from the server even if an avatar is already set.
+    # If you get 599 errors often, you should try disabling this.
+    user_avatar_sync: true
     # Maximum number of seconds since last message in chat to skip
     # syncing the chat in any case. This setting will take priority
     # over both recovery_chat_sync_limit and initial_chat_sync_count.

+ 2 - 2
go.mod

@@ -13,7 +13,7 @@ require (
 	gopkg.in/yaml.v2 v2.3.0
 	maunium.net/go/mauflag v1.0.0
 	maunium.net/go/maulogger/v2 v2.1.1
-	maunium.net/go/mautrix v0.8.0
+	maunium.net/go/mautrix v0.8.2
 )
 
-replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.19
+replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.20

+ 6 - 0
go.sum

@@ -141,6 +141,8 @@ github.com/tulir/go-whatsapp v0.3.18 h1:45pkdjEnAp6yV4RTSWCZn2Eenep+MN7Kndf/rRXQ
 github.com/tulir/go-whatsapp v0.3.18/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o=
 github.com/tulir/go-whatsapp v0.3.19 h1:76VtmcjKGX8MbfJN9NNi1f0IVmigTLUcxqE1VRcovcQ=
 github.com/tulir/go-whatsapp v0.3.19/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o=
+github.com/tulir/go-whatsapp v0.3.20 h1:nK92MgruqXwk+QlaAS39xhzHNbFvJIEgUIOUrN3i8Yc=
+github.com/tulir/go-whatsapp v0.3.20/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o=
 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -234,3 +236,7 @@ maunium.net/go/mautrix v0.8.0-rc.4 h1:3JXoL2JJPE5nh/YSw9sv9dQA9ulma9yHTMOBMBY1xd
 maunium.net/go/mautrix v0.8.0-rc.4/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY=
 maunium.net/go/mautrix v0.8.0 h1:G1jlVslNUTWEqaxuatHAMmzTWnGyoCIc4tAF5GpQJd8=
 maunium.net/go/mautrix v0.8.0/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY=
+maunium.net/go/mautrix v0.8.1 h1:YvJGy7euB+x6Mz74jJ+G4NiyrLiX9pzmXpnQB9vFONg=
+maunium.net/go/mautrix v0.8.1/go.mod h1:KiViCshKBUZwrVRvTOXsJBFfstvR/btxckHUbOPdu54=
+maunium.net/go/mautrix v0.8.2 h1:E3NudQ/QolmE/yhHau8iCkbmcq6gCLvoEvukdqPFJu4=
+maunium.net/go/mautrix v0.8.2/go.mod h1:KiViCshKBUZwrVRvTOXsJBFfstvR/btxckHUbOPdu54=

+ 5 - 16
matrix.go

@@ -289,6 +289,10 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
 
 func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
 	defer mx.bridge.Metrics.TrackEvent(evt.Type)()
+	if mx.shouldIgnoreEvent(evt) {
+		return
+	}
+
 	user := mx.bridge.GetUserByMXID(evt.Sender)
 	if user == nil || !user.Whitelisted || !user.IsConnected() {
 		return
@@ -299,22 +303,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
 		return
 	}
 
-	var resp <-chan string
-	var err error
-	switch content := evt.Content.Parsed.(type) {
-	case *event.RoomNameEventContent:
-		resp, err = user.Conn.UpdateGroupSubject(content.Name, portal.Key.JID)
-	case *event.TopicEventContent:
-		resp, err = user.Conn.UpdateGroupDescription(portal.Key.JID, content.Topic)
-	case *event.RoomAvatarEventContent:
-		return
-	}
-	if err != nil {
-		mx.log.Errorln(err)
-	} else {
-		out := <-resp
-		mx.log.Infoln(out)
-	}
+	portal.HandleMatrixMeta(user, evt)
 }
 
 func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool {

+ 165 - 68
portal.go

@@ -204,12 +204,12 @@ func (portal *Portal) handleMessageLoop() {
 			}
 		}
 		portal.backfillLock.Lock()
-		portal.handleMessage(msg)
+		portal.handleMessage(msg, false)
 		portal.backfillLock.Unlock()
 	}
 }
 
-func (portal *Portal) handleMessage(msg PortalMessage) {
+func (portal *Portal) handleMessage(msg PortalMessage, isBackfill bool) {
 	if len(portal.MXID) == 0 {
 		portal.log.Warnln("handleMessage called even though portal.MXID is empty")
 		return
@@ -254,6 +254,8 @@ func (portal *Portal) handleMessage(msg PortalMessage) {
 		portal.HandleContactMessage(msg.source, data)
 	case whatsapp.LocationMessage:
 		portal.HandleLocationMessage(msg.source, data)
+	case whatsapp.StubMessage:
+		portal.HandleStubMessage(msg.source, data, isBackfill)
 	case whatsappExt.MessageRevocation:
 		portal.HandleMessageRevoke(msg.source, data)
 	case FakeMessage:
@@ -421,30 +423,35 @@ func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInf
 		}
 	}
 
-	if avatar.Status != 0 {
-		return false
+	if avatar.Status == 404 {
+		avatar.Tag = "remove"
+		avatar.Status = 0
 	}
-
-	if portal.Avatar == avatar.Tag {
+	if avatar.Status != 0 || portal.Avatar == avatar.Tag {
 		return false
 	}
 
-	data, err := avatar.DownloadBytes()
-	if err != nil {
-		portal.log.Warnln("Failed to download avatar:", err)
-		return false
-	}
+	if avatar.Tag == "remove" {
+		portal.AvatarURL = id.ContentURI{}
+	} else {
+		data, err := avatar.DownloadBytes()
+		if err != nil {
+			portal.log.Warnln("Failed to download avatar:", err)
+			return false
+		}
 
-	mimeType := http.DetectContentType(data)
-	resp, err := portal.MainIntent().UploadBytes(data, mimeType)
-	if err != nil {
-		portal.log.Warnln("Failed to upload avatar:", err)
-		return false
+		mimeType := http.DetectContentType(data)
+		resp, err := portal.MainIntent().UploadBytes(data, mimeType)
+		if err != nil {
+			portal.log.Warnln("Failed to upload avatar:", err)
+			return false
+		}
+
+		portal.AvatarURL = resp.ContentURI
 	}
 
-	portal.AvatarURL = resp.ContentURI
 	if len(portal.MXID) > 0 {
-		_, err = portal.MainIntent().SetRoomAvatar(portal.MXID, resp.ContentURI)
+		_, err := portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
 		if err != nil {
 			portal.log.Warnln("Failed to set room topic:", err)
 			return false
@@ -457,40 +464,50 @@ func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInf
 	return true
 }
 
-func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID, updateInfo bool) bool {
+func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID, intent *appservice.IntentAPI, updateInfo bool) bool {
 	if portal.Name != name {
-		intent := portal.MainIntent()
-		if len(setBy) > 0 {
-			intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
+		portal.log.Debugfln("Updating name %s -> %s", portal.Name, name)
+		portal.Name = name
+		if intent == nil {
+			intent = portal.MainIntent()
+			if len(setBy) > 0 {
+				intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
+			}
 		}
 		_, err := intent.SetRoomName(portal.MXID, name)
 		if err == nil {
-			portal.Name = name
 			if updateInfo {
 				portal.UpdateBridgeInfo()
 			}
 			return true
+		} else {
+			portal.Name = ""
+			portal.log.Warnln("Failed to set room name:", err)
 		}
-		portal.log.Warnln("Failed to set room name:", err)
 	}
 	return false
 }
 
-func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID, updateInfo bool) bool {
+func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID, intent *appservice.IntentAPI, updateInfo bool) bool {
 	if portal.Topic != topic {
-		intent := portal.MainIntent()
-		if len(setBy) > 0 {
-			intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
+		portal.log.Debugfln("Updating topic %s -> %s", portal.Topic, topic)
+		portal.Topic = topic
+		if intent == nil {
+			intent = portal.MainIntent()
+			if len(setBy) > 0 {
+				intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
+			}
 		}
 		_, err := intent.SetRoomTopic(portal.MXID, topic)
 		if err == nil {
-			portal.Topic = topic
 			if updateInfo {
 				portal.UpdateBridgeInfo()
 			}
 			return true
+		} else {
+			portal.Topic = ""
+			portal.log.Warnln("Failed to set room topic:", err)
 		}
-		portal.log.Warnln("Failed to set room topic:", err)
 	}
 	return false
 }
@@ -500,8 +517,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
 		return false
 	} else if portal.IsStatusBroadcastRoom() {
 		update := false
-		update = portal.UpdateName("WhatsApp Status Broadcast", "", false) || update
-		update = portal.UpdateTopic("WhatsApp status updates from your contacts", "", false) || update
+		update = portal.UpdateName("WhatsApp Status Broadcast", "", nil, false) || update
+		update = portal.UpdateTopic("WhatsApp status updates from your contacts", "", nil, false) || update
 		return update
 	}
 	metadata, err := user.Conn.GetGroupMetaData(portal.Key.JID)
@@ -521,8 +538,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
 
 	portal.SyncParticipants(metadata)
 	update := false
-	update = portal.UpdateName(metadata.Name, metadata.NameSetBy, false) || update
-	update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, false) || update
+	update = portal.UpdateName(metadata.Name, metadata.NameSetBy, nil, false) || update
+	update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, nil, false) || update
 
 	portal.RestrictMessageSending(metadata.Announce)
 
@@ -586,7 +603,7 @@ func (portal *Portal) Sync(user *User, contact whatsapp.Contact) {
 
 	update := false
 	update = portal.UpdateMetadata(user) || update
-	if !portal.IsStatusBroadcastRoom() {
+	if !portal.IsStatusBroadcastRoom() && portal.Avatar == "" {
 		update = portal.UpdateAvatar(user, nil, false) || update
 	}
 	if update {
@@ -620,7 +637,7 @@ func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent {
 	}
 }
 
-func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) {
+func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) id.EventID {
 	levels, err := portal.MainIntent().PowerLevels(portal.MXID)
 	if err != nil {
 		levels = portal.GetBasePowerLevels()
@@ -640,14 +657,17 @@ func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) {
 		}
 	}
 	if changed {
-		_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
+		resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels)
 		if err != nil {
 			portal.log.Errorln("Failed to change power levels:", err)
+		} else {
+			return resp.EventID
 		}
 	}
+	return ""
 }
 
-func (portal *Portal) RestrictMessageSending(restrict bool) {
+func (portal *Portal) RestrictMessageSending(restrict bool) id.EventID {
 	levels, err := portal.MainIntent().PowerLevels(portal.MXID)
 	if err != nil {
 		levels = portal.GetBasePowerLevels()
@@ -659,17 +679,20 @@ func (portal *Portal) RestrictMessageSending(restrict bool) {
 	}
 
 	if levels.EventsDefault == newLevel {
-		return
+		return ""
 	}
 
 	levels.EventsDefault = newLevel
-	_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
+	resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels)
 	if err != nil {
 		portal.log.Errorln("Failed to change power levels:", err)
+		return ""
+	} else {
+		return resp.EventID
 	}
 }
 
-func (portal *Portal) RestrictMetadataChanges(restrict bool) {
+func (portal *Portal) RestrictMetadataChanges(restrict bool) id.EventID {
 	levels, err := portal.MainIntent().PowerLevels(portal.MXID)
 	if err != nil {
 		levels = portal.GetBasePowerLevels()
@@ -683,11 +706,14 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) {
 	changed = levels.EnsureEventLevel(event.StateRoomAvatar, newLevel) || changed
 	changed = levels.EnsureEventLevel(event.StateTopic, newLevel) || changed
 	if changed {
-		_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
+		resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels)
 		if err != nil {
 			portal.log.Errorln("Failed to change power levels:", err)
+		} else {
+			return resp.EventID
 		}
 	}
+	return ""
 }
 
 func (portal *Portal) BackfillHistory(user *User, lastMessageTime uint64) error {
@@ -873,7 +899,7 @@ func (portal *Portal) handleHistory(user *User, messages []interface{}) {
 		if portal.privateChatBackfillInvitePuppet != nil && message.GetKey().GetFromMe() && portal.IsPrivateChat() {
 			portal.privateChatBackfillInvitePuppet()
 		}
-		portal.handleMessage(PortalMessage{portal.Key.JID, user, data, message.GetMessageTimestamp()})
+		portal.handleMessage(PortalMessage{portal.Key.JID, user, data, message.GetMessageTimestamp()}, true)
 	}
 }
 
@@ -1105,7 +1131,7 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp
 		return
 	}
 	message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID)
-	if message != nil {
+	if message != nil && !message.IsFakeMXID() {
 		evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
 		if err != nil {
 			portal.log.Warnln("Failed to get reply target:", err)
@@ -1128,7 +1154,7 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp
 
 func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.MessageRevocation) {
 	msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.Id)
-	if msg == nil {
+	if msg == nil || msg.IsFakeMXID() {
 		return
 	}
 	var intent *appservice.IntentAPI
@@ -1198,7 +1224,7 @@ func isGatewayError(err error) bool {
 }
 
 func (portal *Portal) sendMessageWithRetry(intent *appservice.IntentAPI, eventType event.Type, content interface{}, timestamp int64, retries int) (*mautrix.RespSendEvent, error) {
-	for ;;retries-- {
+	for ; ; retries-- {
 		resp, err := portal.sendMessageDirect(intent, eventType, content, timestamp)
 		if retries > 0 && isGatewayError(err) {
 			portal.log.Warnfln("Got gateway error trying to send message, retrying in %d seconds", int(BadGatewaySleep.Seconds()))
@@ -1254,6 +1280,55 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa
 	portal.finishHandling(source, message.Info.Source, resp.EventID)
 }
 
+func (portal *Portal) HandleStubMessage(source *User, message whatsapp.StubMessage, isBackfill bool) {
+	if portal.bridge.Config.Bridge.ChatMetaSync {
+		// Chat meta sync is enabled, so we use chat update commands and full-syncs instead of message history
+		return
+	}
+	intent := portal.startHandling(source, message.Info)
+	if intent == nil {
+		return
+	}
+	var senderJID string
+	if message.Info.FromMe {
+		senderJID = source.JID
+	} else {
+		senderJID = message.Info.SenderJid
+	}
+	var eventID id.EventID
+	// TODO find more real event IDs
+	// TODO timestamp massaging
+	switch message.Type {
+	case waProto.WebMessageInfo_GROUP_CHANGE_SUBJECT:
+		portal.UpdateName(message.FirstParam, "", intent, true)
+	case waProto.WebMessageInfo_GROUP_CHANGE_ICON:
+		portal.UpdateAvatar(source, nil, true)
+	case waProto.WebMessageInfo_GROUP_CHANGE_DESCRIPTION:
+		if isBackfill {
+			// TODO fetch topic from server
+		}
+		//portal.UpdateTopic(message.FirstParam, "", intent, true)
+	case waProto.WebMessageInfo_GROUP_CHANGE_ANNOUNCE:
+		eventID = portal.RestrictMessageSending(message.FirstParam == "on")
+	case waProto.WebMessageInfo_GROUP_CHANGE_RESTRICT:
+		eventID = portal.RestrictMetadataChanges(message.FirstParam == "on")
+	case waProto.WebMessageInfo_GROUP_PARTICIPANT_ADD, waProto.WebMessageInfo_GROUP_PARTICIPANT_INVITE:
+		portal.HandleWhatsAppInvite(senderJID, intent, message.Params)
+	case waProto.WebMessageInfo_GROUP_PARTICIPANT_REMOVE, waProto.WebMessageInfo_GROUP_PARTICIPANT_LEAVE:
+		portal.HandleWhatsAppKick(senderJID, message.Params)
+	case waProto.WebMessageInfo_GROUP_PARTICIPANT_PROMOTE:
+		eventID = portal.ChangeAdminStatus(message.Params, true)
+	case waProto.WebMessageInfo_GROUP_PARTICIPANT_DEMOTE:
+		eventID = portal.ChangeAdminStatus(message.Params, false)
+	default:
+		return
+	}
+	if len(eventID) == 0 {
+		eventID = id.EventID(fmt.Sprintf("net.maunium.whatsapp.fake::%s", message.Info.Id))
+	}
+	portal.markHandled(source, message.Info.Source, eventID)
+}
+
 func (portal *Portal) HandleLocationMessage(source *User, message whatsapp.LocationMessage) {
 	intent := portal.startHandling(source, message.Info)
 	if intent == nil {
@@ -1428,17 +1503,19 @@ func (portal *Portal) HandleWhatsAppKick(senderJID string, jids []string) {
 	}
 }
 
-func (portal *Portal) HandleWhatsAppInvite(senderJID string, jids []string) {
-	senderIntent := portal.MainIntent()
-	if senderJID != "unknown" {
-		sender := portal.bridge.GetPuppetByJID(senderJID)
-		senderIntent = sender.IntentFor(portal)
+func (portal *Portal) HandleWhatsAppInvite(senderJID string, intent *appservice.IntentAPI, jids []string) {
+	if intent == nil {
+		intent = portal.MainIntent()
+		if senderJID != "unknown" {
+			sender := portal.bridge.GetPuppetByJID(senderJID)
+			intent = sender.IntentFor(portal)
+		}
 	}
 	for _, jid := range jids {
 		puppet := portal.bridge.GetPuppetByJID(jid)
-		_, err := senderIntent.InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: puppet.MXID})
+		_, err := intent.InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: puppet.MXID})
 		if err != nil {
-			portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, senderIntent.UserID, err)
+			portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err)
 		}
 		err = puppet.DefaultIntent().EnsureJoined(portal.MXID)
 		if err != nil {
@@ -1465,7 +1542,7 @@ type mediaMessage struct {
 }
 
 func (portal *Portal) uploadWithRetry(intent *appservice.IntentAPI, data []byte, mimeType string, retries int) (*mautrix.RespMediaUpload, error) {
-	for ;;retries-- {
+	for ; ; retries-- {
 		uploaded, err := intent.UploadBytes(data, mimeType)
 		if isGatewayError(err) {
 			portal.log.Warnfln("Got gateway error trying to upload media, retrying in %d seconds", int(BadGatewaySleep.Seconds()))
@@ -1998,8 +2075,6 @@ func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) {
 	}
 }
 
-var timeout = errors.New("message sending timed out")
-
 func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
 	if !portal.HasRelaybot() && (
 		(portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) ||
@@ -2013,10 +2088,10 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
 	}
 	portal.markHandled(sender, info, evt.ID)
 	portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.Key.GetId())
-	portal.sendRaw(sender, evt, info, false)
+	portal.sendRaw(sender, evt, info)
 }
 
-func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebMessageInfo, isRetry bool) {
+func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebMessageInfo) {
 	errChan := make(chan error, 1)
 	go sender.Conn.SendRaw(info, errChan)
 
@@ -2024,20 +2099,13 @@ func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebM
 	var errorEventID id.EventID
 	select {
 	case err = <-errChan:
-		var statusResp whatsapp.StatusResponse
-		if !isRetry && errors.As(err, &statusResp) && statusResp.Status == 599 {
-			portal.log.Debugfln("599 status response sending %s to WhatsApp (%+v), retrying...", evt.ID, statusResp)
-			errorEventID = portal.sendErrorMessage(fmt.Sprintf("%v. The bridge will retry in 5 seconds.", err))
-			time.Sleep(5 * time.Second)
-			portal.sendRaw(sender, evt, info, true)
-		}
 	case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second):
 		if portal.bridge.Config.Bridge.FetchMessageOnTimeout && portal.wasMessageSent(sender, info.Key.GetId()) {
 			portal.log.Debugln("Matrix event %s was bridged, but response didn't arrive within timeout")
 			portal.sendDeliveryReceipt(evt.ID)
 		} else {
 			portal.log.Warnfln("Response when bridging Matrix event %s is taking long to arrive", evt.ID)
-			errorEventID = portal.sendErrorMessage(timeout.Error())
+			errorEventID = portal.sendErrorMessage("message sending timed out")
 		}
 		err = <-errChan
 	}
@@ -2045,9 +2113,11 @@ func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebM
 		portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
 		var statusResp whatsapp.StatusResponse
 		if errors.As(err, &statusResp) && statusResp.Status == 599 {
-			portal.log.Debugfln("599 status response data: %+v", statusResp)
+			portal.log.Debugfln("599 status response extra data: %+v", statusResp.Extra)
+			portal.sendErrorMessage(fmt.Sprintf("%v. Please try again after a few minutes", err))
+		} else {
+			portal.sendErrorMessage(err.Error())
 		}
-		portal.sendErrorMessage(err.Error())
 	} else {
 		portal.log.Debugfln("Handled Matrix event %s", evt.ID)
 		portal.sendDeliveryReceipt(evt.ID)
@@ -2231,3 +2301,30 @@ func (portal *Portal) HandleMatrixInvite(sender *User, evt *event.Event) {
 		portal.log.Infoln("Add %s response: %s", puppet.JID, <-resp)
 	}
 }
+
+func (portal *Portal) HandleMatrixMeta(sender *User, evt *event.Event) {
+	var resp <-chan string
+	var err error
+	switch content := evt.Content.Parsed.(type) {
+	case *event.RoomNameEventContent:
+		if content.Name == portal.Name {
+			return
+		}
+		portal.Name = content.Name
+		resp, err = sender.Conn.UpdateGroupSubject(content.Name, portal.Key.JID)
+	case *event.TopicEventContent:
+		if content.Topic == portal.Topic {
+			return
+		}
+		portal.Topic = content.Topic
+		resp, err = sender.Conn.UpdateGroupDescription(portal.Key.JID, content.Topic)
+	case *event.RoomAvatarEventContent:
+		return
+	}
+	if err != nil {
+		portal.log.Errorln("Failed to update metadata:", err)
+	} else {
+		out := <-resp
+		portal.log.Debugln("Successfully updated metadata:", out)
+	}
+}

+ 14 - 7
puppet.go

@@ -22,9 +22,10 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/Rhymen/go-whatsapp"
 	log "maunium.net/go/maulogger/v2"
 
+	"github.com/Rhymen/go-whatsapp"
+
 	"maunium.net/go/mautrix/appservice"
 	"maunium.net/go/mautrix/id"
 
@@ -190,15 +191,18 @@ func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicI
 		}
 	}
 
-	if avatar.Status != 0 {
-		return false
+	if avatar.Status == 404 {
+		avatar.Tag = "remove"
+		avatar.Status = 0
+	} else if avatar.Status == 401 && puppet.Avatar != "unauthorized" {
+		puppet.Avatar = "unauthorized"
+		return true
 	}
-
-	if avatar.Tag == puppet.Avatar {
+	if avatar.Status != 0 || avatar.Tag == puppet.Avatar {
 		return false
 	}
 
-	if len(avatar.URL) == 0 {
+	if avatar.Tag == "remove" || len(avatar.URL) == 0 {
 		err := puppet.DefaultIntent().SetAvatarURL(id.ContentURI{})
 		if err != nil {
 			puppet.log.Warnln("Failed to remove avatar:", err)
@@ -296,7 +300,10 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
 
 	update := false
 	update = puppet.UpdateName(source, contact) || update
-	update = puppet.UpdateAvatar(source, nil) || update
+	// TODO figure out how to update avatars after being offline
+	if len(puppet.Avatar) == 0 || puppet.bridge.Config.Bridge.UserAvatarSync {
+		update = puppet.UpdateAvatar(source, nil) || update
+	}
 	if update {
 		puppet.Update()
 	}

+ 25 - 9
user.go

@@ -641,8 +641,11 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool)
 		}
 		create := (chat.LastMessageTime >= user.LastConnection && user.LastConnection > 0) || i < limit
 		if len(chat.Portal.MXID) > 0 || create || createAll {
-			chat.Portal.Sync(user, chat.Contact)
-			err := chat.Portal.BackfillHistory(user, chat.LastMessageTime)
+			// Don't sync unless chat meta sync is enabled or portal doesn't exist
+			if user.bridge.Config.Bridge.ChatMetaSync || len(chat.Portal.MXID) == 0 {
+				chat.Portal.Sync(user, chat.Contact)
+			}
+			err = chat.Portal.BackfillHistory(user, chat.LastMessageTime)
 			if err != nil {
 				chat.Portal.log.Errorln("Error backfilling history:", err)
 			}
@@ -927,6 +930,10 @@ func (user *User) HandleContactMessage(message whatsapp.ContactMessage) {
 	user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
 }
 
+func (user *User) HandleStubMessage(message whatsapp.StubMessage) {
+	user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
+}
+
 func (user *User) HandleLocationMessage(message whatsapp.LocationMessage) {
 	user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
 }
@@ -1015,7 +1022,7 @@ func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) {
 			intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal)
 			for _, msgID := range info.IDs {
 				msg := user.bridge.DB.Message.GetByJID(portal.Key, msgID)
-				if msg == nil {
+				if msg == nil || msg.IsFakeMXID() {
 					continue
 				}
 
@@ -1066,7 +1073,7 @@ func (user *User) markSelfRead(jid, messageID string) {
 		user.log.Debugfln("User read chat %s/%s in WhatsApp mobile (last known event: %s/%s)", portal.Key.JID, portal.MXID, message.JID, message.MXID)
 	} else {
 		message = user.bridge.DB.Message.GetByJID(portal.Key, messageID)
-		if message == nil {
+		if message == nil || message.IsFakeMXID() {
 			return
 		}
 		user.log.Debugfln("User read message %s/%s in %s/%s in WhatsApp mobile", message.JID, message.MXID, portal.Key.JID, portal.MXID)
@@ -1118,13 +1125,22 @@ func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) {
 		return
 	}
 
+	// These don't come down the message history :(
 	switch cmd.Data.Action {
-	case whatsappExt.ChatActionNameChange:
-		go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, true)
 	case whatsappExt.ChatActionAddTopic:
-		go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, true)
+		go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil,true)
 	case whatsappExt.ChatActionRemoveTopic:
-		go portal.UpdateTopic("", cmd.Data.SenderJID, true)
+		go portal.UpdateTopic("", cmd.Data.SenderJID, nil,true)
+	}
+
+	if !user.bridge.Config.Bridge.ChatMetaSync {
+		// Ignore chat update commands, we're relying on the message history.
+		return
+	}
+
+	switch cmd.Data.Action {
+	case whatsappExt.ChatActionNameChange:
+		go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, nil, true)
 	case whatsappExt.ChatActionPromote:
 		go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true)
 	case whatsappExt.ChatActionDemote:
@@ -1136,7 +1152,7 @@ func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) {
 	case whatsappExt.ChatActionRemove:
 		go portal.HandleWhatsAppKick(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
 	case whatsappExt.ChatActionAdd:
-		go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
+		go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs)
 	case whatsappExt.ChatActionIntroduce:
 		if cmd.Data.SenderJID != "unknown" {
 			go portal.Sync(user, whatsapp.Contact{Jid: portal.Key.JID})