浏览代码

Allow creating private chat portal by inviting WhatsApp puppet. Fixes #110

Tulir Asokan 5 年之前
父节点
当前提交
ffb8529b73
共有 3 个文件被更改,包括 152 次插入26 次删除
  1. 1 1
      ROADMAP.md
  2. 136 25
      matrix.go
  3. 15 0
      portal.go

+ 1 - 1
ROADMAP.md

@@ -59,7 +59,7 @@
     * [x] At startup
     * [x] When receiving invite
     * [x] When receiving message
-  * [ ] Private chat creation by inviting Matrix puppet of WhatsApp user to new room
+  * [x] Private chat creation by inviting Matrix puppet of WhatsApp user to new room
   * [x] Option to use own Matrix account for messages sent from WhatsApp mobile/other web clients
   * [x] Shared group chat portals
 

+ 136 - 25
matrix.go

@@ -21,11 +21,14 @@ import (
 	"strings"
 
 	"maunium.net/go/maulogger/v2"
+	"maunium.net/go/mautrix"
 
 	"maunium.net/go/mautrix/appservice"
 	"maunium.net/go/mautrix/event"
 	"maunium.net/go/mautrix/format"
 	"maunium.net/go/mautrix/id"
+
+	"maunium.net/go/mautrix-whatsapp/database"
 )
 
 type MatrixHandler struct {
@@ -67,37 +70,45 @@ func (mx *MatrixHandler) HandleEncryption(evt *event.Event) {
 	}
 }
 
-func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
-	intent := mx.as.BotIntent()
-
-	user := mx.bridge.GetUserByMXID(evt.Sender)
-	if user == nil {
-		return
-	}
-
+func (mx *MatrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
 	resp, err := intent.JoinRoomByID(evt.RoomID)
 	if err != nil {
-		mx.log.Debugfln("Failed to join room %s with invite from %s: %v", evt.RoomID, evt.Sender, err)
-		return
+		mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err)
+		return nil
 	}
 
 	members, err := intent.JoinedMembers(resp.RoomID)
 	if err != nil {
-		mx.log.Debugfln("Failed to get members in room %s after accepting invite from %s: %v", resp.RoomID, evt.Sender, err)
+		mx.log.Debugfln("Failed to get members in room %s after accepting invite from %s as %s: %v", resp.RoomID, evt.Sender, intent.UserID, err)
 		_, _ = intent.LeaveRoom(resp.RoomID)
-		return
+		return nil
 	}
 
 	if len(members.Joined) < 2 {
-		mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender)
+		mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender, "as", intent.UserID)
 		_, _ = intent.LeaveRoom(resp.RoomID)
+		return nil
+	}
+	return members
+}
+
+func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
+	intent := mx.as.BotIntent()
+
+	user := mx.bridge.GetUserByMXID(evt.Sender)
+	if user == nil {
+		return
+	}
+
+	members := mx.joinAndCheckMembers(evt, intent)
+	if members == nil {
 		return
 	}
 
 	if !user.Whitelisted {
-		_, _ = intent.SendNotice(resp.RoomID, "You are not whitelisted to use this bridge.\n"+
+		_, _ = intent.SendNotice(evt.RoomID, "You are not whitelisted to use this bridge.\n"+
 			"If you're the owner of this bridge, see the bridge.permissions section in your config file.")
-		_, _ = intent.LeaveRoom(resp.RoomID)
+		_, _ = intent.LeaveRoom(evt.RoomID)
 		return
 	}
 
@@ -115,17 +126,113 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
 			hasPuppets = true
 			continue
 		}
-		mx.log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender)
-		intent.SendNotice(resp.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
-		intent.LeaveRoom(resp.RoomID)
+		mx.log.Debugln("Leaving multi-user room", evt.RoomID, "after accepting invite from", evt.Sender)
+		_, _ = intent.SendNotice(evt.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
+		_, _ = intent.LeaveRoom(evt.RoomID)
 		return
 	}
 
 	if !hasPuppets {
 		user := mx.bridge.GetUserByMXID(evt.Sender)
-		user.SetManagementRoom(resp.RoomID)
-		intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.")
-		mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender)
+		user.SetManagementRoom(evt.RoomID)
+		_, _ = intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.")
+		mx.log.Debugln(evt.RoomID, "registered as a management room with", evt.Sender)
+	}
+}
+
+func (mx *MatrixHandler) handleExistingPrivatePortal(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) {
+	err := portal.MainIntent().EnsureInvited(portal.MXID, inviter.MXID)
+	if err != nil {
+		mx.log.Warnfln("Failed to invite %s to existing private chat portal %s with %s: %v. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.JID, err)
+		mx.createPrivatePortalFromInvite(portal.Key, roomID, inviter, puppet, portal)
+		return
+	}
+	intent := puppet.DefaultIntent()
+	_, _ = intent.SendNotice(roomID, "You already have a private chat portal with me at %s")
+	mx.log.Debugln("Leaving private chat room", roomID, "as", puppet.MXID, "after accepting invite from", inviter.MXID, "as we already have chat with the user")
+	_, _ = intent.LeaveRoom(roomID)
+}
+
+func (mx *MatrixHandler) createPrivatePortalFromInvite(key database.PortalKey, roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) {
+	if portal == nil {
+		portal = mx.bridge.NewManualPortal(key)
+	}
+	portal.MXID = roomID
+	portal.Topic = "WhatsApp private chat"
+	_, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic)
+	if portal.bridge.Config.Bridge.PrivateChatPortalMeta {
+		portal.Name = puppet.Displayname
+		portal.AvatarURL = puppet.AvatarURL
+		portal.Avatar = puppet.Avatar
+		_, _ = portal.MainIntent().SetRoomName(portal.MXID, portal.Name)
+		_, _ = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL)
+	} else {
+		portal.Name = ""
+	}
+	portal.log.Infoln("Created private chat portal in %s after invite from", roomID, inviter.MXID)
+	intent := puppet.DefaultIntent()
+
+	if mx.bridge.Config.Bridge.Encryption.Default {
+		_, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{UserID: mx.bridge.Bot.UserID})
+		if err != nil {
+			portal.log.Warnln("Failed to invite bridge bot to enable e2be:", err)
+		}
+		err = mx.bridge.Bot.EnsureJoined(roomID)
+		if err != nil {
+			portal.log.Warnln("Failed to join as bridge bot to enable e2be:", err)
+		}
+		_, err = intent.SendStateEvent(roomID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
+		if err != nil {
+			portal.log.Warnln("Failed to enable e2be:", err)
+		}
+		mx.as.StateStore.SetMembership(roomID, inviter.MXID, event.MembershipJoin)
+		mx.as.StateStore.SetMembership(roomID, puppet.MXID, event.MembershipJoin)
+		mx.as.StateStore.SetMembership(roomID, mx.bridge.Bot.UserID, event.MembershipJoin)
+		portal.Encrypted = true
+	}
+	portal.Update()
+	portal.UpdateBridgeInfo()
+	_, _ = intent.SendNotice(roomID, "Private chat portal created")
+
+	err := portal.FillInitialHistory(inviter)
+	if err != nil {
+		portal.log.Errorln("Failed to fill history:", err)
+	}
+
+	inviter.addPortalToCommunity(portal)
+	inviter.addPuppetToCommunity(puppet)
+}
+
+func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
+	intent := puppet.DefaultIntent()
+	members := mx.joinAndCheckMembers(evt, intent)
+	if members == nil {
+		return
+	}
+	var hasBridgeBot, hasOtherUsers bool
+	for mxid, _ := range members.Joined {
+		if mxid == intent.UserID || mxid == inviter.MXID {
+			continue
+		} else if mxid == mx.bridge.Bot.UserID {
+			hasBridgeBot = true
+		} else {
+			hasOtherUsers = true
+		}
+	}
+	if !hasBridgeBot && !hasOtherUsers {
+		key := database.NewPortalKey(puppet.JID, inviter.JID)
+		existingPortal := mx.bridge.GetPortalByJID(key)
+		if existingPortal != nil && len(existingPortal.MXID) > 0 {
+			mx.handleExistingPrivatePortal(evt.RoomID, inviter, puppet, existingPortal)
+		} else {
+			mx.createPrivatePortalFromInvite(key, evt.RoomID, inviter, puppet, existingPortal)
+		}
+	} else if !hasBridgeBot {
+		mx.log.Debugln("Leaving multi-user room", evt.RoomID, "as", puppet.MXID, "after accepting invite from", evt.Sender)
+		_, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a WhatsApp group.")
+		_, _ = intent.LeaveRoom(evt.RoomID)
+	} else {
+		_, _ = intent.SendNotice(evt.RoomID, "This puppet will remain inactive until this room is bridged to a WhatsApp group.")
 	}
 }
 
@@ -145,13 +252,17 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
 		return
 	}
 
-	portal := mx.bridge.GetPortalByMXID(evt.RoomID)
-	if portal == nil {
+	user := mx.bridge.GetUserByMXID(evt.Sender)
+	if user == nil || !user.Whitelisted || !user.IsConnected() {
 		return
 	}
 
-	user := mx.bridge.GetUserByMXID(evt.Sender)
-	if user == nil || !user.Whitelisted || !user.IsConnected() {
+	portal := mx.bridge.GetPortalByMXID(evt.RoomID)
+	if portal == nil {
+		puppet := mx.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
+		if content.Membership == event.MembershipInvite && puppet != nil {
+			mx.HandlePuppetInvite(evt, user, puppet)
+		}
 		return
 	}
 

+ 15 - 0
portal.go

@@ -125,6 +125,21 @@ func (portal *Portal) GetUsers() []*User {
 	return nil
 }
 
+func (bridge *Bridge) NewManualPortal(key database.PortalKey) *Portal {
+	portal := &Portal{
+		Portal: bridge.DB.Portal.New(),
+		bridge: bridge,
+		log:    bridge.Log.Sub(fmt.Sprintf("Portal/%s", key)),
+
+		recentlyHandled: [recentlyHandledLength]types.WhatsAppMessageID{},
+
+		messages: make(chan PortalMessage, 128),
+	}
+	portal.Key = key
+	go portal.handleMessageLoop()
+	return portal
+}
+
 func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal {
 	portal := &Portal{
 		Portal: dbPortal,