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