Skip to content

Commit

Permalink
Allow creating private chat portal by inviting WhatsApp puppet. Fixes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Jul 5, 2020
1 parent b7cd2c7 commit ffb8529
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 26 deletions.
2 changes: 1 addition & 1 deletion ROADMAP.md
Expand Up @@ -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

Expand Down
161 changes: 136 additions & 25 deletions matrix.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand All @@ -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.")
}
}

Expand All @@ -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
}

Expand Down
15 changes: 15 additions & 0 deletions portal.go
Expand Up @@ -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,
Expand Down

0 comments on commit ffb8529

Please sign in to comment.