Эх сурвалжийг харах

Implement WhatsApp->Matrix group info updates

Tulir Asokan 3 жил өмнө
parent
commit
149e9bc8af
6 өөрчлөгдсөн 280 нэмэгдсэн , 324 устгасан
  1. 8 20
      config/bridge.go
  2. 13 31
      example-config.yaml
  3. 1 1
      go.mod
  4. 2 2
      go.sum
  5. 163 150
      portal.go
  6. 93 120
      user.go

+ 8 - 20
config/bridge.go

@@ -42,15 +42,13 @@ type BridgeConfig struct {
 		End   bool `yaml:"end"`
 	} `yaml:"call_notices"`
 
-	InitialChatSync      int   `yaml:"initial_chat_sync_count"`
-	InitialHistoryFill   int   `yaml:"initial_history_fill_count"`
-	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"`
-	BridgeMatrixLeave    bool  `yaml:"bridge_matrix_leave"`
-	SyncChatMaxAge       int64 `yaml:"sync_max_chat_age"`
+	HistorySync struct {
+		CreatePortals        bool `yaml:"create_portals"`
+		Backfill             bool `yaml:"backfill"`
+		DoublePuppetBackfill bool `yaml:"double_puppet_backfill"`
+	}
+	UserAvatarSync    bool `yaml:"user_avatar_sync"`
+	BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
 
 	SyncWithCustomPuppets bool   `yaml:"sync_with_custom_puppets"`
 	SyncDirectChatList    bool   `yaml:"sync_direct_chat_list"`
@@ -58,7 +56,6 @@ type BridgeConfig struct {
 	DefaultBridgePresence bool   `yaml:"default_bridge_presence"`
 	LoginSharedSecret     string `yaml:"login_shared_secret"`
 
-	DoublePuppetBackfill  bool   `yaml:"double_puppet_backfill"`
 	PrivateChatPortalMeta bool   `yaml:"private_chat_portal_meta"`
 	BridgeNotices         bool   `yaml:"bridge_notices"`
 	ResendBridgeInfo      bool   `yaml:"resend_bridge_info"`
@@ -95,7 +92,6 @@ type BridgeConfig struct {
 }
 
 func (bc *BridgeConfig) setDefaults() {
-	bc.DeliveryReceipts = false
 	bc.MaxConnectionAttempts = 3
 	bc.ConnectionRetryDelay = -1
 	bc.ReportConnectionRetry = true
@@ -104,22 +100,14 @@ func (bc *BridgeConfig) setDefaults() {
 	bc.CallNotices.Start = true
 	bc.CallNotices.End = true
 
-	bc.InitialChatSync = 10
-	bc.InitialHistoryFill = 20
-	bc.RecoverChatSync = -1
-	bc.RecoverHistory = true
-	bc.ChatMetaSync = true
+	bc.HistorySync.CreatePortals = true
 	bc.UserAvatarSync = true
 	bc.BridgeMatrixLeave = true
-	bc.SyncChatMaxAge = 259200
 
 	bc.SyncWithCustomPuppets = true
 	bc.DefaultBridgePresence = true
 	bc.DefaultBridgeReceipts = true
-	bc.LoginSharedSecret = ""
 
-	bc.DoublePuppetBackfill = false
-	bc.PrivateChatPortalMeta = false
 	bc.BridgeNotices = true
 	bc.EnableStatusBroadcast = true
 }

+ 13 - 31
example-config.yaml

@@ -101,32 +101,20 @@ bridge:
         start: true
         end: true
 
-    # Number of chats to sync for new users.
-    initial_chat_sync_count: 10
-    # Number of old messages to fill when creating new portal rooms.
-    initial_history_fill_count: 20
-    # Whether or not notifications should be turned off while filling initial history.
-    # Only applicable when using double puppeting.
-    initial_history_disable_notifications: false
-    # Maximum number of chats to sync when recovering from downtime.
-    # Set to -1 to sync all new chats during downtime.
-    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
+    history_sync:
+        # Whether to create portals from history sync payloads from WhatsApp.
+        create_portals: true
+        # Whether to enable backfilling history sync payloads from WhatsApp using batch sending
+        # This requires a server with MSC2716 support, which is currently an experimental feature in synapse.
+        # It can be enabled by setting experimental_features -> enable_msc2716 to true in homeserver.yaml.
+        backfill: false
+        # Whether to use custom puppet for backfilling.
+        # In order to use this, the custom puppets must be in the appservice's user ID namespace.
+        double_puppet_backfill: false
     # 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
     # Whether or not Matrix users leaving groups should be bridged to WhatsApp
     bridge_matrix_leave: 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.
-    # Default is 3 days = 259200 seconds
-    sync_max_chat_age: 259200
 
     # Whether or not to sync with custom puppets to receive EDUs that
     # are not normally sent to appservices.
@@ -147,18 +135,12 @@ bridge:
     # manually.
     login_shared_secret: null
 
-    # Whether to use custom puppet for backfilling.
-    # In order to use this, the custom puppets must be in the appservice's user ID namespace.
-    double_puppet_backfill: false
-    # Whether or not to explicitly set the avatar and room name for private
-    # chat portal rooms. This can be useful if the previous field works fine,
-    # but causes room avatar/name bugs.
+    # Whether to explicitly set the avatar and room name for private chat portal rooms.
     private_chat_portal_meta: false
-    # Whether or not Matrix m.notice-type messages should be bridged.
+    # Whether Matrix m.notice-type messages should be bridged.
     bridge_notices: true
     # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
-    # This field will automatically be changed back to false after it,
-    # except if the config file is not writable.
+    # This field will automatically be changed back to false after it, except if the config file is not writable.
     resend_bridge_info: false
     # When using double puppeting, should muted chats be muted in Matrix?
     mute_bridging: false

+ 1 - 1
go.mod

@@ -8,7 +8,7 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.9
 	github.com/prometheus/client_golang v1.11.0
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48
+	go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600
 	golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
 	google.golang.org/protobuf v1.27.1
 	gopkg.in/yaml.v2 v2.4.0

+ 2 - 2
go.sum

@@ -139,8 +139,8 @@ github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
 github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
 go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 h1:xpQTMgJGGaF+c8jV/LA/FVXAPJxZbSAGeflOc+Ly6uQ=
 go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
-go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48 h1:e4cAP66APziJd8YFAJbYtPtkMJLi4wullnqs87lWZWo=
-go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
+go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600 h1:3huw0OOUNmU1c9vJHifEdTJJnFn6UchoHFaazdHkd34=
+go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
 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=

+ 163 - 150
portal.go

@@ -193,14 +193,6 @@ type Portal struct {
 	hasRelaybot *bool
 }
 
-func (portal *Portal) syncDoublePuppetDetailsAfterCreate(source *User) {
-	doublePuppet := portal.bridge.GetPuppetByCustomMXID(source.MXID)
-	if doublePuppet == nil {
-		return
-	}
-	source.syncChatDoublePuppetDetails(doublePuppet, portal, true)
-}
-
 func (portal *Portal) handleMessageLoop() {
 	for msg := range portal.messages {
 		if len(portal.MXID) == 0 {
@@ -214,7 +206,6 @@ func (portal *Portal) handleMessageLoop() {
 				portal.log.Errorln("Failed to create portal room:", err)
 				continue
 			}
-			portal.syncDoublePuppetDetailsAfterCreate(msg.source)
 		}
 		if msg.evt != nil {
 			portal.handleMessage(msg.source, msg.evt)
@@ -310,6 +301,7 @@ func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User,
 
 const UndecryptableMessageNotice = "Decrypting message from WhatsApp failed, waiting for sender to re-send... " +
 	"([learn more](https://faq.whatsapp.com/general/security-and-privacy/seeing-waiting-for-this-message-this-may-take-a-while))"
+
 var undecryptableMessageContent event.MessageEventContent
 
 func init() {
@@ -391,7 +383,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
 			_, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
 				Reason: "The undecryptable message was actually the deletion of another message",
 			})
-			existingMsg.UpdateMXID("net.maunium.whatsapp.fake::" + existingMsg.MXID, false)
+			existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false)
 		}
 	} else {
 		portal.log.Warnln("Unhandled message:", evt.Info, evt.Message)
@@ -399,7 +391,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
 			_, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
 				Reason: "The undecryptable message contained an unsupported message type",
 			})
-			existingMsg.UpdateMXID("net.maunium.whatsapp.fake::" + existingMsg.MXID, false)
+			existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false)
 		}
 		return
 	}
@@ -557,7 +549,7 @@ func (portal *Portal) SyncParticipants(source *User, metadata *types.GroupInfo)
 	portal.kickExtraUsers(participantMap)
 }
 
-func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool {
+func (portal *Portal) UpdateAvatar(user *User, setBy types.JID, updateInfo bool) bool {
 	avatar, err := user.Client.GetProfilePictureInfo(portal.Key.JID, false)
 	if err != nil {
 		if !errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) {
@@ -585,7 +577,14 @@ func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool {
 	}
 
 	if len(portal.MXID) > 0 {
-		_, err := portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
+		intent := portal.MainIntent()
+		if !setBy.IsEmpty() {
+			intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
+		}
+		_, err = intent.SetRoomAvatar(portal.MXID, portal.AvatarURL)
+		if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
+			_, err = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
+		}
 		if err != nil {
 			portal.log.Warnln("Failed to set room topic:", err)
 			return false
@@ -598,20 +597,22 @@ func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool {
 	return true
 }
 
-func (portal *Portal) UpdateName(name string, setBy types.JID, intent *appservice.IntentAPI, updateInfo bool) bool {
+func (portal *Portal) UpdateName(name string, setBy types.JID, updateInfo bool) bool {
 	if name == "" && portal.IsBroadcastList() {
 		name = UnnamedBroadcastName
 	}
 	if portal.Name != name {
 		portal.log.Debugfln("Updating name %s -> %s", portal.Name, name)
 		portal.Name = name
-		if intent == nil {
-			intent = portal.MainIntent()
-			if !setBy.IsEmpty() {
-				intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
-			}
+
+		intent := portal.MainIntent()
+		if !setBy.IsEmpty() {
+			intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
 		}
 		_, err := intent.SetRoomName(portal.MXID, name)
+		if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
+			_, err = portal.MainIntent().SetRoomName(portal.MXID, name)
+		}
 		if err == nil {
 			if updateInfo {
 				portal.UpdateBridgeInfo()
@@ -625,17 +626,19 @@ func (portal *Portal) UpdateName(name string, setBy types.JID, intent *appservic
 	return false
 }
 
-func (portal *Portal) UpdateTopic(topic string, setBy types.JID, intent *appservice.IntentAPI, updateInfo bool) 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)
 		portal.Topic = topic
-		if intent == nil {
-			intent = portal.MainIntent()
-			if !setBy.IsEmpty() {
-				intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
-			}
+
+		intent := portal.MainIntent()
+		if !setBy.IsEmpty() {
+			intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal)
 		}
 		_, err := intent.SetRoomTopic(portal.MXID, topic)
+		if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
+			_, err = portal.MainIntent().SetRoomTopic(portal.MXID, topic)
+		}
 		if err == nil {
 			if updateInfo {
 				portal.UpdateBridgeInfo()
@@ -654,8 +657,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
 		return false
 	} else if portal.IsStatusBroadcastList() {
 		update := false
-		update = portal.UpdateName(StatusBroadcastName, types.EmptyJID, nil, false) || update
-		update = portal.UpdateTopic(StatusBroadcastTopic, types.EmptyJID, nil, false) || update
+		update = portal.UpdateName(StatusBroadcastName, types.EmptyJID, false) || update
+		update = portal.UpdateTopic(StatusBroadcastTopic, types.EmptyJID, false) || update
 		return update
 	} else if portal.IsBroadcastList() {
 		update := false
@@ -680,8 +683,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
 
 	portal.SyncParticipants(user, metadata)
 	update := false
-	update = portal.UpdateName(metadata.Name, metadata.NameSetBy, nil, false) || update
-	update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, nil, false) || update
+	update = portal.UpdateName(metadata.Name, metadata.NameSetBy, false) || update
+	update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, false) || update
 
 	portal.RestrictMessageSending(metadata.IsAnnounce)
 	portal.RestrictMetadataChanges(metadata.IsLocked)
@@ -764,7 +767,7 @@ func (portal *Portal) Sync(user *User) bool {
 	update := false
 	update = portal.UpdateMetadata(user) || update
 	if !portal.IsPrivateChat() && !portal.IsBroadcastList() && portal.Avatar == "" {
-		update = portal.UpdateAvatar(user, false) || update
+		update = portal.UpdateAvatar(user, types.EmptyJID, false) || update
 	}
 	if update {
 		portal.Update()
@@ -798,35 +801,35 @@ func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent {
 	}
 }
 
-//func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) id.EventID {
-//	levels, err := portal.MainIntent().PowerLevels(portal.MXID)
-//	if err != nil {
-//		levels = portal.GetBasePowerLevels()
-//	}
-//	newLevel := 0
-//	if setAdmin {
-//		newLevel = 50
-//	}
-//	changed := false
-//	for _, jid := range jids {
-//		puppet := portal.bridge.GetPuppetByJID(jid)
-//		changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed
-//
-//		user := portal.bridge.GetUserByJID(jid)
-//		if user != nil {
-//			changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed
-//		}
-//	}
-//	if changed {
-//		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) ChangeAdminStatus(jids []types.JID, setAdmin bool) id.EventID {
+	levels, err := portal.MainIntent().PowerLevels(portal.MXID)
+	if err != nil {
+		levels = portal.GetBasePowerLevels()
+	}
+	newLevel := 0
+	if setAdmin {
+		newLevel = 50
+	}
+	changed := false
+	for _, jid := range jids {
+		puppet := portal.bridge.GetPuppetByJID(jid)
+		changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed
+
+		user := portal.bridge.GetUserByJID(jid)
+		if user != nil {
+			changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed
+		}
+	}
+	if changed {
+		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) id.EventID {
 	levels, err := portal.MainIntent().PowerLevels(portal.MXID)
@@ -1079,7 +1082,7 @@ func (portal *Portal) backfill(source *User, messages []*waProto.HistorySyncMsg)
 			intent = puppet.DefaultIntent()
 		} else {
 			intent = puppet.IntentFor(portal)
-			if intent.IsCustomPuppet && !portal.bridge.Config.Bridge.DoublePuppetBackfill {
+			if intent.IsCustomPuppet && !portal.bridge.Config.Bridge.HistorySync.DoublePuppetBackfill {
 				intent = puppet.DefaultIntent()
 				addMember(puppet)
 			}
@@ -1266,7 +1269,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
 			portal.Name = metadata.Name
 			portal.Topic = metadata.Topic
 		}
-		portal.UpdateAvatar(user, false)
+		portal.UpdateAvatar(user, types.EmptyJID, false)
 	}
 
 	bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo()
@@ -1337,6 +1340,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
 	}
 
 	portal.ensureUserInvited(user)
+	user.syncChatDoublePuppetDetails(portal, true)
 
 	if metadata != nil {
 		portal.SyncParticipants(user, metadata)
@@ -1678,95 +1682,104 @@ func (portal *Portal) convertContactMessage(intent *appservice.IntentAPI, msg *w
 	return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: content}
 }
 
-// FIXME
-//func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error {
-//	_, err := intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
-//	if err != nil {
-//		httpErr, ok := err.(mautrix.HTTPError)
-//		if ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_FORBIDDEN" {
-//			_, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
-//		}
-//	}
-//	return err
-//}
-//
-//func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI, target id.UserID, targetIntent *appservice.IntentAPI) {
-//	if !isSameUser || targetIntent == nil {
-//		err := portal.tryKickUser(target, kicker)
-//		if err != nil {
-//			portal.log.Warnfln("Failed to kick %s from %s: %v", target, portal.MXID, err)
-//			if targetIntent != nil {
-//				_, _ = targetIntent.LeaveRoom(portal.MXID)
-//			}
-//		}
-//	} else {
-//		_, err := targetIntent.LeaveRoom(portal.MXID)
-//		if err != nil {
-//			portal.log.Warnfln("Failed to leave portal as %s: %v", target, err)
-//			_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: target})
-//		}
-//	}
-//}
-//
-//func (portal *Portal) HandleWhatsAppKick(source *User, senderJID string, jids []string) {
-//	sender := portal.bridge.GetPuppetByJID(senderJID)
-//	senderIntent := sender.IntentFor(portal)
-//	for _, jid := range jids {
-//		if source != nil && source.JID == jid {
-//			portal.log.Debugln("Ignoring self-kick by", source.MXID)
-//			continue
-//		}
-//		puppet := portal.bridge.GetPuppetByJID(jid)
-//		portal.removeUser(puppet.JID == sender.JID, senderIntent, puppet.MXID, puppet.DefaultIntent())
-//
-//		if !portal.IsBroadcastList() {
-//			user := portal.bridge.GetUserByJID(jid)
-//			if user != nil {
-//				var customIntent *appservice.IntentAPI
-//				if puppet.CustomMXID == user.MXID {
-//					customIntent = puppet.CustomIntent()
-//				}
-//				portal.removeUser(puppet.JID == sender.JID, senderIntent, user.MXID, customIntent)
-//			}
-//		}
-//	}
-//}
-//
-//func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID string, intent *appservice.IntentAPI, jids []string) (evtID id.EventID) {
-//	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)
-//		puppet.SyncContact(source, true)
-//		content := event.Content{
-//			Parsed: event.MemberEventContent{
-//				Membership:  "invite",
-//				Displayname: puppet.Displayname,
-//				AvatarURL:   puppet.AvatarURL.CUString(),
-//			},
-//			Raw: map[string]interface{}{
-//				"net.maunium.whatsapp.puppet": true,
-//			},
-//		}
-//		resp, err := intent.SendStateEvent(portal.MXID, event.StateMember, puppet.MXID.String(), &content)
-//		if err != nil {
-//			portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err)
-//			_ = portal.MainIntent().EnsureInvited(portal.MXID, puppet.MXID)
-//		} else {
-//			evtID = resp.EventID
-//		}
-//		err = puppet.DefaultIntent().EnsureJoined(portal.MXID)
-//		if err != nil {
-//			portal.log.Errorfln("Failed to ensure %s is joined: %v", puppet.MXID, err)
-//		}
-//	}
-//	return
-//}
+func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error {
+	_, err := intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
+	if err != nil {
+		httpErr, ok := err.(mautrix.HTTPError)
+		if ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_FORBIDDEN" {
+			_, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID})
+		}
+	}
+	return err
+}
+
+func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI, target id.UserID, targetIntent *appservice.IntentAPI) {
+	if !isSameUser || targetIntent == nil {
+		err := portal.tryKickUser(target, kicker)
+		if err != nil {
+			portal.log.Warnfln("Failed to kick %s from %s: %v", target, portal.MXID, err)
+			if targetIntent != nil {
+				_, _ = portal.leaveWithPuppetMeta(targetIntent)
+			}
+		}
+	} else {
+		_, err := portal.leaveWithPuppetMeta(targetIntent)
+		if err != nil {
+			portal.log.Warnfln("Failed to leave portal as %s: %v", target, err)
+			_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: target})
+		}
+	}
+}
+
+func (portal *Portal) HandleWhatsAppKick(source *User, senderJID types.JID, jids []types.JID) {
+	sender := portal.bridge.GetPuppetByJID(senderJID)
+	senderIntent := sender.IntentFor(portal)
+	for _, jid := range jids {
+		if source != nil && source.JID.User == jid.User {
+			portal.log.Debugln("Ignoring self-kick by", source.MXID)
+			continue
+		}
+		puppet := portal.bridge.GetPuppetByJID(jid)
+		portal.removeUser(puppet.JID == sender.JID, senderIntent, puppet.MXID, puppet.DefaultIntent())
+
+		if !portal.IsBroadcastList() {
+			user := portal.bridge.GetUserByJID(jid)
+			if user != nil {
+				var customIntent *appservice.IntentAPI
+				if puppet.CustomMXID == user.MXID {
+					customIntent = puppet.CustomIntent()
+				}
+				portal.removeUser(puppet.JID == sender.JID, senderIntent, user.MXID, customIntent)
+			}
+		}
+	}
+}
+
+func (portal *Portal) leaveWithPuppetMeta(intent *appservice.IntentAPI) (*mautrix.RespSendEvent, error) {
+	content := event.Content{
+		Parsed: event.MemberEventContent{
+			Membership: event.MembershipLeave,
+		},
+		Raw: map[string]interface{}{
+			"net.maunium.whatsapp.puppet": true,
+		},
+	}
+	return intent.SendStateEvent(portal.MXID, event.StateMember, intent.UserID.String(), &content)
+}
+
+func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID *types.JID, jids []types.JID) (evtID id.EventID) {
+	intent := portal.MainIntent()
+	if senderJID != nil && !senderJID.IsEmpty() {
+		sender := portal.bridge.GetPuppetByJID(*senderJID)
+		intent = sender.IntentFor(portal)
+	}
+	for _, jid := range jids {
+		puppet := portal.bridge.GetPuppetByJID(jid)
+		puppet.SyncContact(source, true)
+		content := event.Content{
+			Parsed: event.MemberEventContent{
+				Membership:  "invite",
+				Displayname: puppet.Displayname,
+				AvatarURL:   puppet.AvatarURL.CUString(),
+			},
+			Raw: map[string]interface{}{
+				"net.maunium.whatsapp.puppet": true,
+			},
+		}
+		resp, err := intent.SendStateEvent(portal.MXID, event.StateMember, puppet.MXID.String(), &content)
+		if err != nil {
+			portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err)
+			_ = portal.MainIntent().EnsureInvited(portal.MXID, puppet.MXID)
+		} else {
+			evtID = resp.EventID
+		}
+		err = puppet.DefaultIntent().EnsureJoined(portal.MXID)
+		if err != nil {
+			portal.log.Errorfln("Failed to ensure %s is joined: %v", puppet.MXID, err)
+		}
+	}
+	return
+}
 
 func (portal *Portal) makeMediaBridgeFailureMessage(intent *appservice.IntentAPI, info *types.MessageInfo, bridgeErr error, captionContent *event.MessageEventContent) *ConvertedMessage {
 	portal.log.Errorfln("Failed to bridge media for %s: %v", info.ID, bridgeErr)

+ 93 - 120
user.go

@@ -376,13 +376,31 @@ func (user *User) handleHistorySync(evt *waProto.HistorySync) {
 			continue
 		}
 
+		muteEnd := time.Unix(int64(conv.GetMuteEndTime()), 0)
+		if muteEnd.After(time.Now()) {
+			_ = user.Client.Store.ChatSettings.PutMutedUntil(jid, muteEnd)
+		}
+		if conv.GetArchived() {
+			_ = user.Client.Store.ChatSettings.PutArchived(jid, true)
+		}
+		if conv.GetPinned() > 0 {
+			_ = user.Client.Store.ChatSettings.PutPinned(jid, true)
+		}
+
 		portal := user.GetPortalByJID(jid)
-		err = portal.CreateMatrixRoom(user)
-		if err != nil {
-			user.log.Warnfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err)
-			continue
+		if user.bridge.Config.Bridge.HistorySync.CreatePortals {
+			err = portal.CreateMatrixRoom(user)
+			if err != nil {
+				user.log.Warnfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err)
+				continue
+			}
+		}
+		if len(portal.MXID) > 0 && user.bridge.Config.Bridge.HistorySync.Backfill {
+			portal.backfill(user, conv.GetMessages())
+			if !conv.GetMarkedAsUnread() && conv.GetUnreadCount() == 0 {
+				user.markSelfReadFull(portal)
+			}
 		}
-		portal.backfill(user, conv.GetMessages())
 	}
 }
 
@@ -432,6 +450,10 @@ func (user *User) HandleEvent(event interface{}) {
 		go user.syncPuppet(v.JID)
 	case *events.PushName:
 		go user.syncPuppet(v.JID)
+	case *events.GroupInfo:
+		go user.handleGroupUpdate(v)
+	case *events.Picture:
+		go user.handlePictureUpdate(v)
 	case *events.Receipt:
 		go user.handleReceipt(v)
 	case *events.ChatPresence:
@@ -540,29 +562,21 @@ type CustomReadReceipt struct {
 	DoublePuppet bool  `json:"net.maunium.whatsapp.puppet,omitempty"`
 }
 
-func (user *User) syncChatDoublePuppetDetails(doublePuppet *Puppet, portal *Portal, justCreated bool) {
+func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) {
+	doublePuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID)
+	if doublePuppet == nil {
+		return
+	}
 	if doublePuppet == nil || doublePuppet.CustomIntent() == nil || len(portal.MXID) == 0 {
 		return
 	}
-	intent := doublePuppet.CustomIntent()
-	// FIXME this might not be possible to do anymore
-	//if chat.UnreadCount == 0 && (justCreated || !user.bridge.Config.Bridge.MarkReadOnlyOnCreate) {
-	//	lastMessage := user.bridge.DB.Message.GetLastInChatBefore(chat.Portal.Key, chat.ReceivedAt.Unix())
-	//	if lastMessage != nil {
-	//		err := intent.MarkReadWithContent(chat.Portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true})
-	//		if err != nil {
-	//			user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, chat.Portal.MXID, err)
-	//		}
-	//	}
-	//} else if chat.UnreadCount == -1 {
-	//	user.log.Debugfln("Invalid unread count (missing field?) in chat info %+v", chat.Source)
-	//}
 	if justCreated || !user.bridge.Config.Bridge.TagOnlyOnCreate {
 		chat, err := user.Client.Store.ChatSettings.GetChatSettings(portal.Key.JID)
 		if err != nil {
 			user.log.Warnfln("Failed to get settings of %s: %v", portal.Key.JID, err)
 			return
 		}
+		intent := doublePuppet.CustomIntent()
 		user.updateChatMute(intent, portal, chat.MutedUntil)
 		user.updateChatTag(intent, portal, user.bridge.Config.Bridge.ArchiveTag, chat.Archived)
 		user.updateChatTag(intent, portal, user.bridge.Config.Bridge.PinnedTag, chat.Pinned)
@@ -709,12 +723,8 @@ func (user *User) markOtherRead(portal *Portal, intent *appservice.IntentAPI, me
 }
 
 func (user *User) markSelfRead(portal *Portal, messageID types.MessageID) {
-	puppet := user.bridge.GetPuppetByJID(user.JID)
-	if puppet == nil {
-		return
-	}
-	intent := puppet.CustomIntent()
-	if intent == nil {
+	puppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
+	if puppet == nil || puppet.CustomIntent() == nil {
 		return
 	}
 	var message *database.Message
@@ -731,106 +741,69 @@ func (user *User) markSelfRead(portal *Portal, messageID types.MessageID) {
 		}
 		user.log.Debugfln("User read message %s/%s in %s/%s in WhatsApp mobile", message.JID, message.MXID, portal.Key.JID, portal.MXID)
 	}
-	err := intent.MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true})
+	err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true})
 	if err != nil {
 		user.log.Warnfln("Failed to bridge own read receipt in %s: %v", portal.Key.JID, err)
 	}
 }
 
-//func (user *User) HandleCommand(cmd whatsapp.JSONCommand) {
-//	switch cmd.Type {
-//	case whatsapp.CommandPicture:
-//		if strings.HasSuffix(cmd.JID, whatsapp.NewUserSuffix) {
-//			puppet := user.bridge.GetPuppetByJID(cmd.JID)
-//			go puppet.UpdateAvatar(user, cmd.ProfilePicInfo)
-//		} else if user.bridge.Config.Bridge.ChatMetaSync {
-//			portal := user.GetPortalByJID(cmd.JID)
-//			go portal.UpdateAvatar(user, cmd.ProfilePicInfo, true)
-//		}
-//	case whatsapp.CommandDisconnect:
-//		if cmd.Kind == "replaced" {
-//			user.cleanDisconnection = true
-//			go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" +
-//				"Use the `reconnect` command to disconnect the other client and resume bridging.")
-//		} else {
-//			user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw))
-//			go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server (reason code: %s).\n\n"+
-//				"Use the `reconnect` command to reconnect.", cmd.Kind)
-//		}
-//	}
-//}
-
-//func (user *User) HandleChatUpdate(cmd whatsapp.ChatUpdate) {
-//	if cmd.Command != whatsapp.ChatUpdateCommandAction {
-//		return
-//	}
-//
-//	portal := user.GetPortalByJID(cmd.JID)
-//	if len(portal.MXID) == 0 {
-//		if cmd.Data.Action == whatsapp.ChatActionIntroduce || cmd.Data.Action == whatsapp.ChatActionCreate {
-//			go func() {
-//				err := portal.CreateMatrixRoom(user)
-//				if err != nil {
-//					user.log.Errorln("Failed to create portal room after receiving join event:", err)
-//				}
-//			}()
-//		}
-//		return
-//	}
-//
-//	// These don't come down the message history :(
-//	switch cmd.Data.Action {
-//	case whatsapp.ChatActionAddTopic:
-//		go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil, true)
-//	case whatsapp.ChatActionRemoveTopic:
-//		go portal.UpdateTopic("", cmd.Data.SenderJID, nil, true)
-//	case whatsapp.ChatActionRemove:
-//		// We ignore leaving groups in the message history to avoid accidentally leaving rejoined groups,
-//		// but if we get a real-time command that says we left, it should be safe to bridge it.
-//		if !user.bridge.Config.Bridge.ChatMetaSync {
-//			for _, jid := range cmd.Data.UserChange.JIDs {
-//				if jid == user.JID {
-//					go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
-//					break
-//				}
-//			}
-//		}
-//	}
-//
-//	if !user.bridge.Config.Bridge.ChatMetaSync {
-//		// Ignore chat update commands, we're relying on the message history.
-//		return
-//	}
-//
-//	switch cmd.Data.Action {
-//	case whatsapp.ChatActionNameChange:
-//		go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, nil, true)
-//	case whatsapp.ChatActionPromote:
-//		go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true)
-//	case whatsapp.ChatActionDemote:
-//		go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, false)
-//	case whatsapp.ChatActionAnnounce:
-//		go portal.RestrictMessageSending(cmd.Data.Announce)
-//	case whatsapp.ChatActionRestrict:
-//		go portal.RestrictMetadataChanges(cmd.Data.Restrict)
-//	case whatsapp.ChatActionRemove:
-//		go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
-//	case whatsapp.ChatActionAdd:
-//		go portal.HandleWhatsAppInvite(user, cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs)
-//	case whatsapp.ChatActionIntroduce:
-//		if cmd.Data.SenderJID != "unknown" {
-//			go portal.Sync(user, whatsapp.Contact{JID: portal.Key.JID})
-//		}
-//	}
-//}
-//
-//func (user *User) HandleJSONMessage(evt whatsapp.RawJSONMessage) {
-//	if !json.Valid(evt.RawMessage) {
-//		return
-//	}
-//	user.log.Debugfln("JSON message with tag %s: %s", evt.Tag, evt.RawMessage)
-//	user.updateLastConnectionIfNecessary()
-//}
+func (user *User) markSelfReadFull(portal *Portal) {
+	puppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
+	if puppet == nil || puppet.CustomIntent() == nil {
+		return
+	}
+	lastMessage := user.bridge.DB.Message.GetLastInChat(portal.Key)
+	if lastMessage == nil {
+		return
+	}
+	err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true})
+	if err != nil {
+		user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, portal.MXID, err)
+	}
+}
+
+func (user *User) handleGroupUpdate(evt *events.GroupInfo) {
+	portal := user.GetPortalByJID(evt.JID)
+	if portal == nil || len(portal.MXID) == 0 {
+		// TODO create portal when added to group
+		user.log.Debugfln("Ignoring group info update in chat with no portal: %+v", evt)
+		return
+	}
+	switch {
+	case evt.Announce != nil:
+		portal.RestrictMessageSending(evt.Announce.IsAnnounce)
+	case evt.Locked != nil:
+		portal.RestrictMetadataChanges(evt.Locked.IsLocked)
+	case evt.Name != nil:
+		portal.UpdateName(evt.Name.Name, evt.Name.NameSetBy, true)
+	case evt.Topic != nil:
+		portal.UpdateTopic(evt.Topic.Topic, evt.Topic.TopicSetBy, true)
+	case evt.Leave != nil:
+		if evt.Sender != nil && !evt.Sender.IsEmpty() {
+			portal.HandleWhatsAppKick(user, *evt.Sender, evt.Leave)
+		}
+	case evt.Join != nil:
+		portal.HandleWhatsAppInvite(user, evt.Sender, evt.Join)
+	case evt.Promote != nil:
+		portal.ChangeAdminStatus(evt.Promote, true)
+	case evt.Demote != nil:
+		portal.ChangeAdminStatus(evt.Demote, false)
+	}
+}
+
+func (user *User) handlePictureUpdate(evt *events.Picture) {
+	if evt.JID.Server == types.DefaultUserServer {
+		puppet := user.bridge.GetPuppetByJID(evt.JID)
+		if puppet.Avatar != evt.PictureID {
+			puppet.UpdateAvatar(user)
+		}
+	} else {
+		portal := user.GetPortalByJID(evt.JID)
+		if portal != nil && portal.Avatar != evt.PictureID {
+			portal.UpdateAvatar(user, evt.Author, true)
+		}
+	}
+}
 
 func (user *User) NeedsRelaybot(portal *Portal) bool {
 	return !user.HasSession() // || !user.IsInPortal(portal.Key)