From 1e534f76f76010c88c350ae6be8c9b9d058f42a8 Mon Sep 17 00:00:00 2001
From: Tulir Asokan <tulir@maunium.net>
Date: Sat, 4 May 2019 00:26:34 +0300
Subject: [PATCH] Add support for creating private chats by inviting Messenger
 puppet to new room

---
 ROADMAP.md                 |  2 +-
 mautrix_facebook/matrix.py | 83 +++++++++++++++++++++++++++++++++++++-
 mautrix_facebook/portal.py | 49 +++++++++++++++++++++-
 3 files changed, 130 insertions(+), 4 deletions(-)

diff --git a/ROADMAP.md b/ROADMAP.md
index 8baa6e8..d2d5b64 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -65,5 +65,5 @@
     * [x] At startup
     * [ ] When added to chat
     * [x] When receiving message
-  * [ ] Private chat creation by inviting Matrix puppet of Messenger user to new room
+  * [x] Private chat creation by inviting Matrix puppet of Messenger user to new room
   * [x] Option to use own Matrix account for messages sent from other Messenger clients
diff --git a/mautrix_facebook/matrix.py b/mautrix_facebook/matrix.py
index 0c46e8e..17ba8c6 100644
--- a/mautrix_facebook/matrix.py
+++ b/mautrix_facebook/matrix.py
@@ -17,6 +17,7 @@ from typing import Tuple, TYPE_CHECKING
 import logging
 import asyncio
 
+from fbchat.models import ThreadType
 from mautrix.types import (EventID, RoomID, UserID, Event, EventType, MessageEvent, MessageType,
                            MessageEventContent, StateEvent, Membership, RedactionEvent,
                            PresenceEvent, TypingEvent, ReceiptEvent, PresenceState)
@@ -81,6 +82,57 @@ class MatrixHandler:
                      "<code>bridge.permissions</code> section in your config file.</p>")
             await self.az.intent.leave_room(room_id)
 
+    async def handle_puppet_invite(self, room_id: RoomID, puppet: 'pu.Puppet', inviter: 'u.User'
+                                   ) -> None:
+        intent = puppet.default_mxid_intent
+        self.log.debug(f"{inviter.mxid} invited puppet for {puppet.fbid} to {room_id}")
+        if not await inviter.is_logged_in():
+            await intent.error_and_leave(room_id, text="Please log in before inviting Facebook "
+                                                       "Messenger puppets to private chats.")
+            return
+
+        portal = po.Portal.get_by_mxid(room_id)
+        if portal:
+            if portal.is_direct:
+                await intent.error_and_leave(room_id, text="You can not invite additional users "
+                                                           "to private chats.")
+                return
+            # TODO add facebook inviting
+            # await portal.invite_facebook(inviter, puppet)
+            # await intent.join_room(room_id)
+            return
+        await intent.join_room(room_id)
+        try:
+            members = await intent.get_room_members(room_id)
+        except MatrixError:
+            self.log.exception(f"Failed to get member list after joining {room_id}")
+            await intent.leave_room(room_id)
+            members = []
+        if len(members) > 2:
+            # TODO add facebook group creating
+            await intent.send_notice(room_id, "You can not invite Facebook Messenger puppets to "
+                                              "multi-user rooms.")
+            await intent.leave_room(room_id)
+            return
+        portal = po.Portal.get_by_fbid(puppet.fbid, inviter.uid, ThreadType.USER)
+        if portal.mxid:
+            try:
+                await intent.invite_user(portal.mxid, inviter.mxid, check_cache=False)
+                await intent.send_notice(room_id,
+                                         text=("You already have a private chat with me "
+                                               f"in room {portal.mxid}"),
+                                         html=("You already have a private chat with me: "
+                                               f"<a href='https://matrix.to/#/{portal.mxid}'>"
+                                               "Link to room"
+                                               "</a>"))
+                await intent.leave_room(room_id)
+                return
+            except MatrixError:
+                pass
+        portal.mxid = room_id
+        portal.save()
+        await intent.send_notice(room_id, "Portal to private chat created.")
+
     async def handle_invite(self, room_id: RoomID, user_id: UserID, inviter_mxid: UserID) -> None:
         self.log.debug(f"{inviter_mxid} invited {user_id} to {room_id}")
         inviter = u.User.get_by_mxid(inviter_mxid)
@@ -91,11 +143,16 @@ class MatrixHandler:
         elif not inviter.is_whitelisted:
             return
 
+        puppet = pu.Puppet.get_by_mxid(user_id)
+        if puppet:
+            await self.handle_puppet_invite(room_id, puppet, inviter)
+            return
+
         # TODO handle puppet and user invites for group chats
 
         # The rest can probably be ignored
 
-    async def handle_join(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None:
+    async def handle_join(self, room_id: RoomID, user_id: UserID) -> None:
         user = u.User.get_by_mxid(user_id)
 
         portal = po.Portal.get_by_mxid(room_id)
@@ -115,6 +172,22 @@ class MatrixHandler:
         self.log.debug(f"{user} joined {room_id}")
         # await portal.join_matrix(user, event_id)
 
+    async def handle_leave(self, room_id: RoomID, user_id: UserID, sender_id: UserID) -> None:
+        portal = po.Portal.get_by_mxid(room_id)
+        if not portal:
+            return
+
+        user = u.User.get_by_mxid(user_id, create=False)
+        if not user:
+            return
+
+        if user_id != sender_id:
+            # sender = u.User.get_by_mxid(sender_id)
+            # await portal.handle_matrix_kick(user, sender)
+            pass
+        else:
+            await portal.handle_matrix_leave(user)
+
     @staticmethod
     async def handle_redaction(room_id: RoomID, user_id: UserID, event_id: EventID) -> None:
         user = u.User.get_by_mxid(user_id)
@@ -137,7 +210,6 @@ class MatrixHandler:
 
     async def handle_message(self, room: RoomID, sender_id: UserID, message: MessageEventContent,
                              event_id: EventID) -> None:
-        is_command, text = self.is_command(message)
         sender = u.User.get_by_mxid(sender_id)
         if not sender or not sender.is_whitelisted:
             self.log.debug(f"Ignoring message \"{message}\" from {sender} to {room}:"
@@ -145,6 +217,7 @@ class MatrixHandler:
             return
         self.log.debug(f"Received Matrix event \"{message}\" from {sender} in {room}")
 
+        is_command, text = self.is_command(message)
         portal = po.Portal.get_by_mxid(room)
         if not is_command and portal and await sender.is_logged_in():
             await portal.handle_matrix_message(sender, message, event_id)
@@ -216,8 +289,14 @@ class MatrixHandler:
 
         if evt.type == EventType.ROOM_MEMBER:
             evt: StateEvent
+            prev_membership = (evt.unsigned.prev_content.membership
+                               if evt.unsigned.prev_content else Membership.JOIN)
             if evt.content.membership == Membership.INVITE:
                 await self.handle_invite(evt.room_id, UserID(evt.state_key), evt.sender)
+            elif evt.content.membership == Membership.LEAVE:
+                await self.handle_leave(evt.room_id, UserID(evt.state_key), evt.sender)
+            elif evt.content.membership == Membership.JOIN and prev_membership != Membership.JOIN:
+                await self.handle_join(evt.room_id, UserID(evt.state_key))
         elif evt.type in (EventType.ROOM_MESSAGE, EventType.STICKER):
             evt: MessageEvent
             if evt.type != EventType.ROOM_MESSAGE:
diff --git a/mautrix_facebook/portal.py b/mautrix_facebook/portal.py
index f794aff..071b95c 100644
--- a/mautrix_facebook/portal.py
+++ b/mautrix_facebook/portal.py
@@ -31,7 +31,7 @@ from mautrix.types import (RoomID, EventType, ContentURI, MessageEventContent, E
                            ThumbnailInfo, FileInfo, AudioInfo, VideoInfo, Format,
                            TextMessageEventContent, MediaMessageEventContent, Membership)
 from mautrix.appservice import AppService, IntentAPI
-from mautrix.errors import MForbidden, IntentError
+from mautrix.errors import MForbidden, IntentError, MatrixError
 
 from .config import Config
 from .db import Portal as DBPortal, Message as DBMessage
@@ -126,6 +126,12 @@ class Portal:
     def save(self) -> None:
         self.db_instance.edit(mxid=self.mxid, name=self.name, photo_id=self.photo_id)
 
+    def delete(self) -> None:
+        self.by_fbid.pop(self.fbid_full, None)
+        self.by_mxid.pop(self.mxid, None)
+        if self._db_instance:
+            self._db_instance.delete()
+
     # endregion
     # region Properties
 
@@ -259,6 +265,39 @@ class Portal:
         if not self.is_direct:
             await self._update_participants(source, info)
 
+    # endregion
+    # region Matrix room cleanup
+
+    @staticmethod
+    async def cleanup_room(intent: IntentAPI, room_id: RoomID, message: str = "Portal deleted",
+                           puppets_only: bool = False) -> None:
+        try:
+            members = await intent.get_room_members(room_id)
+        except MatrixError:
+            members = []
+        for user_id in members:
+            puppet = p.Puppet.get_by_mxid(user_id, create=False)
+            if user_id != intent.mxid and (not puppets_only or puppet):
+                try:
+                    if puppet:
+                        await puppet.intent.leave_room(room_id)
+                    else:
+                        await intent.kick_user(room_id, user_id, message)
+                except MatrixError:
+                    pass
+        try:
+            await intent.leave_room(room_id)
+        except MatrixError:
+            pass
+
+    async def unbridge(self) -> None:
+        await self.cleanup_room(self.main_intent, self.mxid, "Room unbridged", puppets_only=True)
+        self.delete()
+
+    async def cleanup_and_delete(self) -> None:
+        await self.cleanup_room(self.main_intent, self.mxid)
+        self.delete()
+
     # endregion
     # region Matrix event handling
 
@@ -329,6 +368,14 @@ class Portal:
         except Exception:
             self.log.exception("Unsend failed")
 
+    async def handle_matrix_leave(self, user: 'u.User') -> None:
+        if self.is_direct:
+            self.log.info(f"{user.mxid} left private chat portal with {self.fbid},"
+                          " cleaning up and deleting...")
+            await self.cleanup_and_delete()
+        else:
+            self.log.debug(f"{user.mxid} left portal to {self.fbid}")
+
     # endregion
     # region Facebook event handling
 
-- 
GitLab