From 1bc6d7b9e332b187721c64505af8c27dd89e9ed5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan <tulir@maunium.net> Date: Mon, 11 May 2020 02:49:07 +0300 Subject: [PATCH] Update fbchat Most event handlers and probably other things are broken --- mautrix_facebook/commands/auth.py | 35 ++- mautrix_facebook/commands/facebook.py | 18 +- mautrix_facebook/db/__init__.py | 2 +- mautrix_facebook/db/portal.py | 24 +- mautrix_facebook/formatter/from_facebook.py | 11 +- mautrix_facebook/formatter/from_matrix.py | 19 +- mautrix_facebook/matrix.py | 15 +- mautrix_facebook/portal.py | 152 +++++------ mautrix_facebook/puppet.py | 22 +- mautrix_facebook/user.py | 288 +++++++------------- mautrix_facebook/web/public.py | 14 +- requirements.txt | 2 +- 12 files changed, 287 insertions(+), 315 deletions(-) diff --git a/mautrix_facebook/commands/auth.py b/mautrix_facebook/commands/auth.py index cd1cd76..ef85c62 100644 --- a/mautrix_facebook/commands/auth.py +++ b/mautrix_facebook/commands/auth.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -20,7 +20,7 @@ import time from http.cookies import SimpleCookie from yarl import URL -from fbchat import FBchatException +import fbchat from mautrix.client import Client from mautrix.util.signed_token import sign_token @@ -52,6 +52,9 @@ async def login(evt: CommandEvent) -> None: if len(evt.args) < 2: await evt.reply("Usage: `$cmdprefix+sp login <email> <password>`") return + elif evt.sender.client: + await evt.reply("You're already logged in") + return evt.sender.command_status = { "action": "Login", "room_id": evt.room_id, @@ -60,10 +63,13 @@ async def login(evt: CommandEvent) -> None: evt.sender.user_agent = random.choice(USER_AGENTS) await evt.reply("Logging in...") try: - await evt.sender.login(evt.args[0], " ".join(evt.args[1:]), - user_agent=evt.sender.user_agent) + session = await fbchat.Session.login(evt.args[0], " ".join(evt.args[1:]), + on_2fa_callback=evt.sender.on_2fa_callback) + evt.sender.session = session + evt.sender.client = fbchat.Client(session=session) + # TODO call on_logged_in somehow evt.sender.command_status = None - except FBchatException as e: + except fbchat.FacebookError as e: evt.sender.command_status = None await evt.reply(f"Failed to log in: {e}") evt.log.exception("Failed to log in") @@ -92,6 +98,9 @@ async def enter_2fa_code(evt: CommandEvent) -> None: @command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH, help_text="Log in to Facebook with Cookie Monster") async def login_web(evt: CommandEvent) -> None: + if evt.sender.client: + await evt.reply("You're already logged in") + return external_url = URL(evt.config["appservice.public.external"]) token = sign_token(evt.processor.bridge.public_website.secret_key, { "mxid": evt.sender.mxid, @@ -111,6 +120,9 @@ async def login_web(evt: CommandEvent) -> None: @command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH, help_text="Log in to Facebook manually") async def login_cookie(evt: CommandEvent) -> None: + if evt.sender.client: + await evt.reply("You're already logged in") + return evt.sender.command_status = { "action": "Login", "room_id": evt.room_id, @@ -145,18 +157,19 @@ async def enter_login_cookies(evt: CommandEvent) -> None: cookie = SimpleCookie() cookie["c_user"] = evt.sender.command_status["c_user"] cookie["xs"] = evt.args[0] - ok = (await evt.sender.set_session(cookie, user_agent=evt.sender.user_agent) - and await evt.sender.is_logged_in(True)) - if not ok: + session = await fbchat.Session.from_cookies(cookie) + if not await session.is_logged_in(): await evt.reply("Failed to log in (see logs for more details)") else: + evt.sender.session = session + evt.sender.client = fbchat.Client(session=session) await evt.sender.on_logged_in(evt.sender.command_status["c_user"]) evt.sender.command_status = None @command_handler(needs_auth=True, help_section=SECTION_AUTH, help_text="Log out of Facebook") async def logout(evt: CommandEvent) -> None: - puppet = pu.Puppet.get_by_fbid(evt.sender.uid) + puppet = pu.Puppet.get_by_fbid(evt.sender.fbid) await evt.sender.logout() if puppet.is_real_user: await puppet.switch_mxid(None, None) @@ -166,7 +179,7 @@ async def logout(evt: CommandEvent) -> None: help_section=SECTION_AUTH, help_text="Replace your Facebook Messenger account's " "Matrix puppet with your Matrix account") async def login_matrix(evt: CommandEvent) -> None: - puppet = pu.Puppet.get_by_fbid(evt.sender.uid) + puppet = pu.Puppet.get_by_fbid(evt.sender.fbid) _, homeserver = Client.parse_mxid(evt.sender.mxid) if homeserver != pu.Puppet.hs_domain: await evt.reply("You can't log in with an account on a different homeserver") @@ -184,7 +197,7 @@ async def login_matrix(evt: CommandEvent) -> None: @command_handler(needs_auth=True, management_only=True, help_section=SECTION_AUTH, help_text="Revert your Facebook Messenger account's Matrix puppet to the original") async def logout_matrix(evt: CommandEvent) -> None: - puppet = pu.Puppet.get_by_fbid(evt.sender.uid) + puppet = pu.Puppet.get_by_fbid(evt.sender.fbid) if not puppet.is_real_user: await evt.reply("You're not logged in with your Matrix account") return diff --git a/mautrix_facebook/commands/facebook.py b/mautrix_facebook/commands/facebook.py index fe326d3..f1eebce 100644 --- a/mautrix_facebook/commands/facebook.py +++ b/mautrix_facebook/commands/facebook.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -16,7 +16,7 @@ from typing import Iterable, List import asyncio -from fbchat import User, ThreadLocation +import fbchat from .. import puppet as pu, portal as po, user as u from ..db import UserPortal as DBUserPortal @@ -27,18 +27,18 @@ from . import command_handler, CommandEvent, SECTION_MISC help_section=SECTION_MISC, help_text="Search for a Facebook user", help_args="<_search query_>") async def search(evt: CommandEvent) -> None: - res = await evt.sender.search_for_users(" ".join(evt.args)) + res = await evt.sender.client.search_for_users(" ".join(evt.args), limit=10) await evt.reply(await _handle_search_result(evt.sender, res)) @command_handler(needs_auth=True, management_only=False) async def search_by_id(evt: CommandEvent) -> None: - res = await evt.sender.fetch_user_info(*evt.args) - await evt.reply(await _handle_search_result(evt.sender, res.values())) + res = [item async for item in evt.sender.client.fetch_thread_info(*evt.args)] + await evt.reply(await _handle_search_result(evt.sender, res)) -async def _handle_search_result(sender: 'u.User', res: Iterable[User]) -> str: - puppets: List[pu.Puppet] = await asyncio.gather(*[pu.Puppet.get_by_fbid(user.uid, create=True) +async def _handle_search_result(sender: 'u.User', res: Iterable[fbchat.UserData]) -> str: + puppets: List[pu.Puppet] = await asyncio.gather(*[pu.Puppet.get_by_fbid(user.id, create=True) .update_info(sender, user) for user in res]) results = "".join( @@ -65,9 +65,9 @@ async def sync(evt: CommandEvent) -> None: else: limit = int(arg) - threads = await evt.sender.fetch_threads(limit=limit, thread_location=ThreadLocation.INBOX) ups = DBUserPortal.all(evt.sender.fbid) - for thread in threads: + async for thread in evt.sender.client.fetch_threads(limit, fbchat.ThreadLocation.INBOX): + # TODO check thread type? portal = po.Portal.get_by_thread(thread, evt.sender.fbid) if create_portals: await portal.create_matrix_room(evt.sender, thread) diff --git a/mautrix_facebook/db/__init__.py b/mautrix_facebook/db/__init__.py index 3d2a5d6..2107924 100644 --- a/mautrix_facebook/db/__init__.py +++ b/mautrix_facebook/db/__init__.py @@ -2,7 +2,7 @@ from mautrix.bridge.db import RoomState, UserProfile from .message import Message from .reaction import Reaction -from .portal import Portal +from .portal import Portal, ThreadType from .puppet import Puppet from .user import User from .user_portal import UserPortal diff --git a/mautrix_facebook/db/portal.py b/mautrix_facebook/db/portal.py index f27d726..4863f1d 100644 --- a/mautrix_facebook/db/portal.py +++ b/mautrix_facebook/db/portal.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -17,10 +17,30 @@ from typing import Optional, Iterator from sqlalchemy import Column, String, Enum, Boolean, false, and_ -from fbchat import ThreadType from mautrix.types import RoomID from mautrix.util.db import Base +from enum import Enum as EnumType +from fbchat import ThreadABC, User, Group, Page + + +class ThreadType(EnumType): + USER = 1 + GROUP = 2 + PAGE = 3 + UNKNOWN = 4 + + @classmethod + def from_thread(cls, thread: ThreadABC) -> 'ThreadType': + if isinstance(thread, User): + return cls.USER + elif isinstance(thread, Group): + return cls.GROUP + elif isinstance(thread, Page): + return cls.PAGE + else: + return cls.UNKNOWN + class Portal(Base): __tablename__ = "portal" diff --git a/mautrix_facebook/formatter/from_facebook.py b/mautrix_facebook/formatter/from_facebook.py index fd1fd88..ba5b4c0 100644 --- a/mautrix_facebook/formatter/from_facebook.py +++ b/mautrix_facebook/formatter/from_facebook.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -17,8 +17,7 @@ from typing import Tuple, List, Optional, Match from html import escape import re -from fbchat import Message, ShareAttachment - +import fbchat from mautrix.types import TextMessageEventContent, Format, MessageType from .. import puppet as pu, user as u @@ -138,7 +137,7 @@ def _handle_codeblock_post(output: List[str], cb_lang: OptStr, cb_content: OptSt output.append(_convert_formatting(post_cb_content)) -def facebook_to_matrix(message: Message) -> TextMessageEventContent: +def facebook_to_matrix(message: fbchat.MessageData) -> TextMessageEventContent: text = message.text or "" content = TextMessageEventContent(msgtype=MessageType.TEXT, body=text) for m in reversed(message.mentions): @@ -161,9 +160,9 @@ def facebook_to_matrix(message: Message) -> TextMessageEventContent: output.append("<br/>") _handle_codeblock_post(output, *post_args) links = [attachment for attachment in message.attachments - if isinstance(attachment, ShareAttachment)] + if isinstance(attachment, fbchat.ShareAttachment)] message.attachments = [attachment for attachment in message.attachments - if not isinstance(attachment, ShareAttachment)] + if not isinstance(attachment, fbchat.ShareAttachment)] for attachment in links: if attachment.original_url.rstrip("/") not in text: output.append(f"<br/><a href='{attachment.original_url}'>{attachment.title}</a>") diff --git a/mautrix_facebook/formatter/from_matrix.py b/mautrix_facebook/formatter/from_matrix.py index 442fb4c..8557a6f 100644 --- a/mautrix_facebook/formatter/from_matrix.py +++ b/mautrix_facebook/formatter/from_matrix.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -13,9 +13,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. -from typing import Optional, cast +from typing import Optional, Dict, Any, cast, TYPE_CHECKING -from fbchat import Message, Mention +from fbchat import Mention from mautrix.types import TextMessageEventContent, Format, UserID, RoomID, RelationType from mautrix.util.formatter import (MatrixParser as BaseMatrixParser, MarkdownString, EntityString, @@ -25,6 +25,15 @@ from .. import puppet as pu, user as u from ..db import Message as DBMessage +if TYPE_CHECKING: + from typing import TypedDict, List + + class SendParams(TypedDict): + text: str + mentions: List[Mention] + reply_to_id: str + + class FacebookFormatString(EntityString[SimpleEntity, EntityType], MarkdownString): def _mention_to_entity(self, mxid: UserID) -> Optional[SimpleEntity]: user = u.User.get_by_mxid(mxid, create=False) @@ -82,7 +91,7 @@ class MatrixParser(BaseMatrixParser[FacebookFormatString]): return cast(FacebookFormatString, super().parse(data)) -def matrix_to_facebook(content: TextMessageEventContent, room_id: RoomID) -> Message: +def matrix_to_facebook(content: TextMessageEventContent, room_id: RoomID) -> 'SendParams': mentions = [] reply_to_id = None if content.relates_to.rel_type == RelationType.REFERENCE: @@ -98,4 +107,4 @@ def matrix_to_facebook(content: TextMessageEventContent, room_id: RoomID) -> Mes for mention in parsed.entities] else: text = content.body - return Message(text=text, mentions=mentions, reply_to_id=reply_to_id) + return {"text": text, "mentions": mentions, "reply_to_id": reply_to_id} diff --git a/mautrix_facebook/matrix.py b/mautrix_facebook/matrix.py index b575f5b..686eb27 100644 --- a/mautrix_facebook/matrix.py +++ b/mautrix_facebook/matrix.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -14,8 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. from typing import List, Union, TYPE_CHECKING +from datetime import datetime +import time -from fbchat import ThreadType from mautrix.types import (EventID, RoomID, UserID, Event, EventType, MessageEvent, StateEvent, RedactionEvent, PresenceEventContent, ReceiptEvent, PresenceState, ReactionEvent, ReactionEventContent, RelationType, PresenceEvent, @@ -24,6 +25,7 @@ from mautrix.errors import MatrixError from mautrix.bridge import BaseMatrixHandler from . import user as u, portal as po, puppet as pu, commands as c +from .db import ThreadType if TYPE_CHECKING: from .context import Context @@ -79,7 +81,7 @@ class MatrixHandler(BaseMatrixHandler): "multi-user rooms.") await intent.leave_room(room_id) return - portal = po.Portal.get_by_fbid(puppet.fbid, invited_by.uid, ThreadType.USER) + portal = po.Portal.get_by_fbid(puppet.fbid, invited_by.fbid, ThreadType.USER) if portal.mxid: try: await intent.invite_user(portal.mxid, invited_by.mxid, check_cache=False) @@ -194,7 +196,9 @@ class MatrixHandler(BaseMatrixHandler): return user = u.User.get_by_mxid(user_id, create=False) if user: - user.set_active_status(info.presence == PresenceState.ONLINE) + # FIXME + # user.set_active_status(info.presence == PresenceState.ONLINE) + pass @staticmethod async def handle_typing(room_id: RoomID, typing: List[UserID]) -> None: @@ -221,7 +225,8 @@ class MatrixHandler(BaseMatrixHandler): if not portal: return - await user.mark_as_read(portal.fbid) + timestamp = datetime.utcfromtimestamp(data.get("ts", int(time.time() * 1000)) / 1000) + await user.client.mark_as_read([portal.thread_for(user)], at=timestamp) def filter_matrix_event(self, evt: Event) -> bool: if not isinstance(evt, (ReactionEvent, RedactionEvent, MessageEvent, StateEvent, diff --git a/mautrix_facebook/portal.py b/mautrix_facebook/portal.py index d8852ae..cde7a00 100644 --- a/mautrix_facebook/portal.py +++ b/mautrix_facebook/portal.py @@ -21,10 +21,7 @@ from yarl import URL import aiohttp import magic -from fbchat import (ThreadType, Thread, User as FBUser, Group as FBGroup, Page as FBPage, - Message as FBMessage, Sticker as FBSticker, AudioAttachment, VideoAttachment, - FileAttachment, ImageAttachment, LocationAttachment, ShareAttachment, - TypingStatus, MessageReaction) +import fbchat from mautrix.types import (RoomID, EventType, ContentURI, MessageEventContent, EventID, ImageInfo, MessageType, LocationMessageEventContent, LocationInfo, ThumbnailInfo, FileInfo, AudioInfo, Format, RelatesTo, RelationType, @@ -37,7 +34,7 @@ from mautrix.bridge import BasePortal from .formatter import facebook_to_matrix, matrix_to_facebook from .config import Config from .db import (Portal as DBPortal, Message as DBMessage, Reaction as DBReaction, - UserPortal as DBUserPortal) + UserPortal as DBUserPortal, ThreadType) from . import puppet as p, user as u if TYPE_CHECKING: @@ -51,9 +48,10 @@ except ImportError: config: Config -ThreadClass = Union[FBUser, FBGroup, FBPage] -AttachmentClass = Union[AudioAttachment, VideoAttachment, FileAttachment, ImageAttachment, - LocationAttachment, ShareAttachment] +ThreadClass = Union[fbchat.UserData, fbchat.GroupData, fbchat.PageData] +AttachmentClass = Union[fbchat.AudioAttachment, fbchat.VideoAttachment, fbchat.FileAttachment, + fbchat.ImageAttachment, fbchat.LocationAttachment, + fbchat.ShareAttachment] class FakeLock: @@ -158,6 +156,16 @@ class Portal(BasePortal): return f"{self.fbid}<->{self.fb_receiver}" return self.fbid + def thread_for(self, user: 'u.User') -> Union[fbchat.User, fbchat.Group, fbchat.Page]: + if self.fb_type == ThreadType.USER: + return fbchat.User(session=user.session, id=self.fbid) + elif self.fb_type == ThreadType.GROUP: + return fbchat.Group(session=user.session, id=self.fbid) + elif self.fb_type == ThreadType.PAGE: + return fbchat.Page(session=user.session, id=self.fbid) + else: + raise ValueError("Unsupported thread type") + @property def is_direct(self) -> bool: return self.fb_type == ThreadType.USER @@ -176,7 +184,8 @@ class Portal(BasePortal): async def update_info(self, source: Optional['u.User'] = None, info: Optional[ThreadClass] = None) -> ThreadClass: if not info: - info = (await source.fetch_thread_info(self.fbid))[self.fbid] + info = await source.client.fetch_thread_info(self.fbid).__anext__() + # TODO validate that we got some sane info? changed = any(await asyncio.gather(self._update_name(info.name), self._update_photo(info.photo), self._update_participants(source, info), @@ -186,10 +195,12 @@ class Portal(BasePortal): return info @staticmethod - def _get_photo_id(url: Optional[str]) -> Optional[str]: - if not url: + def _get_photo_id(photo: Optional[Union[fbchat.Image, str]]) -> Optional[str]: + if not photo: return None - path = URL(url).path + elif isinstance(photo, fbchat.Image): + photo = photo.url + path = URL(photo).path return path[path.rfind("/") + 1:] @staticmethod @@ -221,14 +232,14 @@ class Portal(BasePortal): return True return False - async def _update_photo(self, photo_url: str) -> bool: + async def _update_photo(self, photo: fbchat.Image) -> bool: if self.is_direct and not self.encrypted: return False - photo_id = self._get_photo_id(photo_url) + photo_id = self._get_photo_id(photo) if self.photo_id != photo_id: self.photo_id = photo_id - if photo_url: - self._avatar_uri, *_ = await self._reupload_fb_file(photo_url, self.main_intent) + if photo: + self._avatar_uri, *_ = await self._reupload_fb_file(photo.url, self.main_intent) else: self._avatar_uri = ContentURI("") if self.mxid: @@ -238,16 +249,18 @@ class Portal(BasePortal): async def _update_participants(self, source: 'u.User', info: ThreadClass) -> None: if self.is_direct: - await p.Puppet.get_by_fbid(info.uid).update_info(source=source, info=info) + await p.Puppet.get_by_fbid(info.id).update_info(source=source, info=info) return elif not self.mxid: return - users = await source.fetch_all_users_from_threads([info]) - puppets = [(user, p.Puppet.get_by_fbid(user.uid)) for user in users] - await asyncio.gather(*[puppet.update_info(source=source, info=user) - for user, puppet in puppets]) - await asyncio.gather(*[puppet.intent_for(self).ensure_joined(self.mxid) - for user, puppet in puppets]) + # TODO maybe change this back to happen simultaneously + async for user in source.client.fetch_thread_info([user.id for user in info.participants]): + if not isinstance(user, fbchat.UserData): + # TODO log + continue + puppet = p.Puppet.get_by_fbid(user.id) + await puppet.update_info(source, user) + await puppet.intent_for(self).ensure_joined(self.mxid) # endregion # region Matrix room creation @@ -421,7 +434,7 @@ class Portal(BasePortal): return # TODO this probably isn't nice for bridging images, it really only needs to lock the # actual message send call and dedup queue append. - async with self.require_send_lock(sender.uid): + async with self.require_send_lock(sender.fbid): if message.msgtype == MessageType.TEXT or message.msgtype == MessageType.NOTICE: fbid = await self._handle_matrix_text(sender, message) elif message.msgtype == MessageType.IMAGE: @@ -440,7 +453,7 @@ class Portal(BasePortal): self._last_bridged_mxid = event_id async def _handle_matrix_text(self, sender: 'u.User', message: TextMessageEventContent) -> str: - return await sender.send(matrix_to_facebook(message, self.mxid), self.fbid, self.fb_type) + return await self.thread_for(sender).send_text(**matrix_to_facebook(message, self.mxid)) async def _handle_matrix_image(self, sender: 'u.User', message: MediaMessageEventContent) -> Optional[str]: @@ -453,8 +466,8 @@ class Portal(BasePortal): else: return None mime = message.info.mimetype or magic.from_buffer(data, mime=True) - files = await sender._upload([(message.body, data, mime)]) - return await sender._send_files(files, thread_id=self.fbid, thread_type=self.fb_type) + files = await sender.client.upload([(message.body, data, mime)]) + return await self.thread_for(sender).send_files(files) async def _handle_matrix_location(self, sender: 'u.User', message: LocationMessageEventContent) -> str: @@ -468,7 +481,7 @@ class Portal(BasePortal): if message: try: message.delete() - await sender.unsend(message.fbid) + await fbchat.Message(thread=self.thread_for(sender), id=message.fbid).unsend() except Exception: self.log.exception("Unsend failed") return @@ -477,42 +490,27 @@ class Portal(BasePortal): if reaction: try: reaction.delete() - await sender.react_to_message(reaction.fb_msgid, reaction=None) + await fbchat.Message(thread=self.thread_for(sender), + id=reaction.fb_msgid).react(None) except Exception: self.log.exception("Removing reaction failed") - _matrix_to_facebook_reaction = { - "â¤": MessageReaction.HEART, - "â¤ï¸": MessageReaction.HEART, - "ðŸ˜": MessageReaction.LOVE, - "😆": MessageReaction.SMILE, - "😮": MessageReaction.WOW, - "😢": MessageReaction.SAD, - "😠": MessageReaction.ANGRY, - "ðŸ‘": MessageReaction.YES, - "👎": MessageReaction.NO - } - async def handle_matrix_reaction(self, sender: 'u.User', event_id: EventID, - reacting_to: EventID, raw_reaction: str) -> None: - async with self.require_send_lock(sender.uid): - try: - reaction = self._matrix_to_facebook_reaction[raw_reaction] - except KeyError: - return - + reacting_to: EventID, reaction: str) -> None: + async with self.require_send_lock(sender.fbid): message = DBMessage.get_by_mxid(reacting_to, self.mxid) if not message: self.log.debug(f"Ignoring reaction to unknown event {reacting_to}") return - existing = DBReaction.get_by_fbid(message.fbid, self.fb_receiver, sender.uid) - if existing and existing.reaction == reaction.value: + existing = DBReaction.get_by_fbid(message.fbid, self.fb_receiver, sender.fbid) + if existing and existing.reaction == reaction: return - await sender.react_to_message(message.fbid, reaction) + # TODO normalize reaction emoji bytes and maybe pre-reject invalid emojis + await fbchat.Message(thread=self.thread_for(sender), id=message.fbid).react(reaction) await self._upsert_reaction(existing, self.main_intent, event_id, message, sender, - reaction.value) + reaction) async def handle_matrix_leave(self, user: 'u.User') -> None: if self.is_direct: @@ -523,10 +521,8 @@ class Portal(BasePortal): self.log.debug(f"{user.mxid} left portal to {self.fbid}") async def handle_matrix_typing(self, users: Set['u.User']) -> None: - stopped_typing = [user.set_typing_status(TypingStatus.STOPPED, self.fbid, self.fb_type) - for user in self._typing - users] - started_typing = [user.set_typing_status(TypingStatus.TYPING, self.fbid, self.fb_type) - for user in users - self._typing] + stopped_typing = [self.thread_for(user).stop_typing() for user in self._typing - users] + started_typing = [self.thread_for(user).start_typing() for user in self._typing - users] self._typing = users await asyncio.gather(*stopped_typing, *started_typing, loop=self.loop) @@ -534,7 +530,7 @@ class Portal(BasePortal): # region Facebook event handling async def _bridge_own_message_pm(self, source: 'u.User', sender: 'p.Puppet', mid: str) -> bool: - if self.is_direct and sender.fbid == source.uid and not sender.is_real_user: + if self.is_direct and sender.fbid == source.fbid and not sender.is_real_user: if self.invite_own_puppet_to_pm: await self.main_intent.invite_user(self.mxid, sender.mxid) elif self.az.state_store.get_membership(self.mxid, sender.mxid) != Membership.JOIN: @@ -544,18 +540,18 @@ class Portal(BasePortal): return True async def handle_facebook_message(self, source: 'u.User', sender: 'p.Puppet', - message: FBMessage) -> None: + message: fbchat.MessageData) -> None: async with self.optional_send_lock(sender.fbid): - if message.uid in self._dedup: - await source.mark_as_delivered(self.fbid, message.uid) + if message.id in self._dedup: + await source.client.mark_as_delivered(message) return - self._dedup.appendleft(message.uid) + self._dedup.appendleft(message.id) if not self.mxid: mxid = await self.create_matrix_room(source) if not mxid: # Failed to create return - if not await self._bridge_own_message_pm(source, sender, f"message {message.uid}"): + if not await self._bridge_own_message_pm(source, sender, f"message {message.id}"): return intent = sender.intent_for(self) event_ids = [] @@ -569,13 +565,13 @@ class Portal(BasePortal): event_ids += [attach_id for attach_id in attach_ids if attach_id] if not event_ids: if message.text or any(x for x in message.attachments - if isinstance(x, ShareAttachment)): + if isinstance(x, fbchat.ShareAttachment)): event_ids = [await self._handle_facebook_text(intent, message)] else: self.log.warning(f"Unhandled Messenger message: {message}") - DBMessage.bulk_create(fbid=message.uid, fb_receiver=self.fb_receiver, mx_room=self.mxid, + DBMessage.bulk_create(fbid=message.id, fb_receiver=self.fb_receiver, mx_room=self.mxid, event_ids=[event_id for event_id in event_ids if event_id]) - await source.mark_as_delivered(self.fbid, message.uid) + await source.client.mark_as_delivered(message) async def _add_facebook_reply(self, content: TextMessageEventContent, reply: str) -> None: if reply: @@ -602,12 +598,12 @@ class Portal(BasePortal): event_type, content = await self.matrix.e2ee.encrypt(self.mxid, event_type, content) return await intent.send_message_event(self.mxid, event_type, content, **kwargs) - async def _handle_facebook_text(self, intent: IntentAPI, message: FBMessage) -> EventID: + async def _handle_facebook_text(self, intent: IntentAPI, message: fbchat.Message) -> EventID: content = facebook_to_matrix(message) await self._add_facebook_reply(content, message.reply_to_id) return await self._send_message(intent, content) - async def _handle_facebook_sticker(self, intent: IntentAPI, sticker: FBSticker, + async def _handle_facebook_sticker(self, intent: IntentAPI, sticker: fbchat.Sticker, reply_to: str) -> EventID: # TODO handle animated stickers? mxc, mime, size, decryption_info = await self._reupload_fb_file( @@ -622,23 +618,23 @@ class Portal(BasePortal): async def _handle_facebook_attachment(self, intent: IntentAPI, attachment: AttachmentClass, reply_to: str) -> Optional[EventID]: - if isinstance(attachment, AudioAttachment): + if isinstance(attachment, fbchat.AudioAttachment): mxc, mime, size, decryption_info = await self._reupload_fb_file( attachment.url, intent, attachment.filename, encrypt=self.encrypted) event_id = await self._send_message(intent, MediaMessageEventContent( url=mxc, file=decryption_info, msgtype=MessageType.AUDIO, body=attachment.filename, info=AudioInfo(size=size, mimetype=mime, duration=attachment.duration), relates_to=self._get_facebook_reply(reply_to))) - # elif isinstance(attachment, VideoAttachment): + # elif isinstance(attachment, fbchat.VideoAttachment): # TODO - elif isinstance(attachment, FileAttachment): + elif isinstance(attachment, fbchat.FileAttachment): mxc, mime, size, decryption_info = await self._reupload_fb_file( attachment.url, intent, attachment.name, encrypt=self.encrypted) event_id = await self._send_message(intent, MediaMessageEventContent( url=mxc, file=decryption_info, msgtype=MessageType.FILE, body=attachment.name, info=FileInfo(size=size, mimetype=mime), relates_to=self._get_facebook_reply(reply_to))) - elif isinstance(attachment, ImageAttachment): + elif isinstance(attachment, fbchat.ImageAttachment): mxc, mime, size, decryption_info = await self._reupload_fb_file( attachment.large_preview_url or attachment.preview_url, intent, encrypt=self.encrypted) @@ -648,11 +644,11 @@ class Portal(BasePortal): info=ImageInfo(size=size, mimetype=mime, width=attachment.large_preview_width, height=attachment.large_preview_height), relates_to=self._get_facebook_reply(reply_to))) - elif isinstance(attachment, LocationAttachment): + elif isinstance(attachment, fbchat.LocationAttachment): content = await self._convert_facebook_location(intent, attachment) content.relates_to = self._get_facebook_reply(reply_to) event_id = await self._send_message(intent, content) - elif isinstance(attachment, ShareAttachment): + elif isinstance(attachment, fbchat.ShareAttachment): # These are handled in the text formatter return None else: @@ -661,7 +657,8 @@ class Portal(BasePortal): self._last_bridged_mxid = event_id return event_id - async def _convert_facebook_location(self, intent: IntentAPI, location: LocationAttachment + async def _convert_facebook_location(self, intent: IntentAPI, + location: fbchat.LocationAttachment ) -> LocationMessageEventContent: long, lat = location.longitude, location.latitude long_char = "E" if long > 0 else "W" @@ -716,7 +713,7 @@ class Portal(BasePortal): # When we fetch thread info manually, we only get the URL instead of the ID, # so we can't use the actual ID here either. # self.photo_id = new_photo_id - photo_url = await source.fetch_image_url(new_photo_id) + photo_url = await source.client.fetch_image_url(new_photo_id) photo_id = self._get_photo_id(photo_url) if self.photo_id == photo_id: return @@ -849,8 +846,9 @@ class Portal(BasePortal): yield cls.from_db(db_portal) @classmethod - def get_by_thread(cls, thread: Thread, fb_receiver: Optional[str] = None) -> 'Portal': - return cls.get_by_fbid(thread.uid, fb_receiver, thread.type) + def get_by_thread(cls, thread: fbchat.ThreadABC, fb_receiver: Optional[str] = None + ) -> 'Portal': + return cls.get_by_fbid(thread.id, fb_receiver, ThreadType.from_thread(thread)) # endregion diff --git a/mautrix_facebook/puppet.py b/mautrix_facebook/puppet.py index 2dd772b..6488a84 100644 --- a/mautrix_facebook/puppet.py +++ b/mautrix_facebook/puppet.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -18,7 +18,7 @@ import logging import asyncio import attr -from fbchat import User as FBUser +import fbchat from mautrix.types import UserID, RoomID, SyncToken from mautrix.appservice import AppService, IntentAPI from mautrix.bridge.custom_puppet import CustomPuppetMixin @@ -136,10 +136,12 @@ class Puppet(CustomPuppetMixin): # region User info updating - async def update_info(self, source: Optional['u.User'] = None, info: Optional[FBUser] = None, + async def update_info(self, source: Optional['u.User'] = None, + info: Optional[fbchat.UserData] = None, update_avatar: bool = True) -> 'Puppet': if not info: - info = (await source.fetch_user_info(self.fbid))[self.fbid] + info = await source.client.fetch_thread_info([self.fbid]).__anext__() + # TODO validate that we got some sane info? try: changed = await self._update_name(info) if update_avatar: @@ -151,7 +153,7 @@ class Puppet(CustomPuppetMixin): return self @classmethod - def _get_displayname(cls, info: FBUser) -> str: + def _get_displayname(cls, info: fbchat.UserData) -> str: displayname = None for preference in config["bridge.displayname_preference"]: if getattr(info, preference, None): @@ -159,7 +161,7 @@ class Puppet(CustomPuppetMixin): return config["bridge.displayname_template"].format(displayname=displayname, **attr.asdict(info)) - async def _update_name(self, info: FBUser) -> bool: + async def _update_name(self, info: fbchat.UserData) -> bool: name = self._get_displayname(info) if name != self.name: self.name = name @@ -167,12 +169,12 @@ class Puppet(CustomPuppetMixin): return True return False - async def _update_photo(self, photo_url: str) -> bool: - photo_id = p.Portal._get_photo_id(photo_url) + async def _update_photo(self, photo: fbchat.Image) -> bool: + photo_id = p.Portal._get_photo_id(photo) if photo_id != self.photo_id: self.photo_id = photo_id - if photo_url: - avatar_uri, *_ = await p.Portal._reupload_fb_file(photo_url, + if photo: + avatar_uri, *_ = await p.Portal._reupload_fb_file(photo.url, self.default_mxid_intent) else: avatar_uri = "" diff --git a/mautrix_facebook/user.py b/mautrix_facebook/user.py index aeba8a4..815f333 100644 --- a/mautrix_facebook/user.py +++ b/mautrix_facebook/user.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -18,8 +18,7 @@ from http.cookies import SimpleCookie import asyncio import logging -from fbchat import Client, Message, ThreadType, User as FBUser, ActiveStatus, MessageReaction - +import fbchat from mautrix.types import UserID, PresenceState from mautrix.appservice import AppService from mautrix.client import Client as MxClient @@ -27,7 +26,7 @@ from mautrix.bridge._community import CommunityHelper, CommunityID from .config import Config from .commands import enter_2fa_code -from .db import User as DBUser, UserPortal as DBUserPortal, Contact as DBContact +from .db import User as DBUser, UserPortal as DBUserPortal, Contact as DBContact, ThreadType from . import portal as po, puppet as pu if TYPE_CHECKING: @@ -36,13 +35,17 @@ if TYPE_CHECKING: config: Config -class User(Client): +class User: az: AppService loop: asyncio.AbstractEventLoop log: logging.Logger = logging.getLogger("mau.user") by_mxid: Dict[UserID, 'User'] = {} by_fbid: Dict[str, 'User'] = {} + session: Optional[fbchat.Session] + client: Optional[fbchat.Client] + listener: Optional[fbchat.Listener] + listen_task: Optional[asyncio.Task] user_agent: str command_status: Optional[Dict[str, Any]] @@ -59,7 +62,6 @@ class User(Client): def __init__(self, mxid: UserID, session: Optional[SimpleCookie] = None, user_agent: Optional[str] = None, db_instance: Optional[DBUser] = None) -> None: - super().__init__(loop=self.loop) self.mxid = mxid self.by_mxid[mxid] = self self.user_agent = user_agent @@ -72,29 +74,32 @@ class User(Client): self._community_id = None self.log = self.log.getChild(self.mxid) - self._log = self._log.getChild(self.mxid) - self._req_log = self._req_log.getChild(self.mxid) - self._util_log = self._util_log.getChild(self.mxid) - self.set_active_status(False) + + self.client = None + self.session = None + self.listener = None + self.listen_task = None # region Sessions @property - def fbid(self) -> str: - return self.uid + def fbid(self) -> Optional[str]: + if not self.session: + return None + return self.session.user.id @property def db_instance(self) -> DBUser: if not self._db_instance: - self._db_instance = DBUser(mxid=self.mxid, session=self._session_data, fbid=self.uid, - user_agent=self.user_agent) + self._db_instance = DBUser(mxid=self.mxid, session=self._session_data, + fbid=self.fbid, user_agent=self.user_agent) return self._db_instance def save(self, _update_session_data: bool = True) -> None: self.log.debug("Saving session") - if _update_session_data: - self._session_data = self.get_session() - self.db_instance.edit(session=self._session_data, fbid=self.uid, + if _update_session_data and self.session: + self._session_data = self.session.get_cookies() + self.db_instance.edit(session=self._session_data, fbid=self.fbid, user_agent=self.user_agent) @classmethod @@ -145,27 +150,40 @@ class User(Client): return True elif not self._session_data: return False - ok = (await self.set_session(self._session_data, user_agent=self.user_agent) - and await self.is_logged_in(True)) - if ok: + session = await fbchat.Session.from_cookies(self._session_data) + if await session.is_logged_in(): self.log.info("Loaded session successfully") - self.listen(long_polling=False, mqtt=True) + self.session = session + self.client = fbchat.Client(session=self.session) + if self.listen_task: + self.listen_task.cancel() + self.listen_task = self.loop.create_task(self.try_listen()) asyncio.ensure_future(self.post_login(), loop=self.loop) - return ok + return True + return False async def is_logged_in(self, _override: bool = False) -> bool: + if not self.session: + return False if self._is_logged_in is None or _override: - self._is_logged_in = await super().is_logged_in() + self._is_logged_in = await self.session.is_logged_in() return self._is_logged_in # endregion - async def logout(self, safe: bool = False) -> bool: - self.stop_listening() - ok = await super().logout(safe) + async def logout(self) -> bool: + ok = True + if self.session: + try: + await self.session.logout() + except fbchat.FacebookError: + self.log.exception("Error while logging out") + ok = False self._session_data = None self._is_logged_in = False self._on_logged_in_done = False + self.client = None + self.session = None self.save(_update_session_data=False) return ok @@ -186,8 +204,9 @@ class User(Client): await self.sync_contacts() await self.sync_threads() self.log.debug("Updating own puppet info") - own_info = (await self.fetch_user_info(self.uid))[self.uid] - puppet = pu.Puppet.get_by_fbid(self.uid, create=True) + # TODO this might not be right (if it is, check that we got something sensible?) + own_info = await self.client.fetch_thread_info([self.fbid]).__anext__() + puppet = pu.Puppet.get_by_fbid(self.fbid, create=True) await puppet.update_info(source=self, info=own_info) async def _create_community(self) -> None: @@ -231,12 +250,12 @@ class User(Client): async def sync_contacts(self): try: self.log.debug("Fetching contacts...") - users = await self.fetch_all_users() + users = await self.client.fetch_users() self.log.debug(f"Fetched {len(users)} contacts") contacts = DBContact.all(self.fbid) update_avatars = config["bridge.update_avatar_initial_sync"] for user in users: - puppet = pu.Puppet.get_by_fbid(user.uid, create=True) + puppet = pu.Puppet.get_by_fbid(user.id, create=True) await puppet.update_info(self, user, update_avatar=update_avatars) await self._add_community_puppet(contacts.get(puppet.fbid, None), puppet) except Exception: @@ -248,17 +267,19 @@ class User(Client): if sync_count <= 0: return self.log.debug("Fetching threads...") - threads = await self.fetch_thread_list(limit=sync_count) ups = DBUserPortal.all(self.fbid) contacts = DBContact.all(self.fbid) - for thread in threads: - self.log.debug(f"Syncing thread {thread.uid} {thread.name}") - fb_receiver = self.uid if thread.type == ThreadType.USER else None + async for thread in self.client.fetch_threads(limit=sync_count): + if not isinstance(thread, (fbchat.UserData, fbchat.PageData, fbchat.GroupData)): + # TODO log? + continue + self.log.debug(f"Syncing thread {thread.id} {thread.name}") + fb_receiver = self.fbid if isinstance(thread, fbchat.User) else None portal = po.Portal.get_by_thread(thread, fb_receiver) puppet = None - if isinstance(thread, FBUser): - puppet = pu.Puppet.get_by_fbid(thread.uid, create=True) + if isinstance(thread, fbchat.UserData): + puppet = pu.Puppet.get_by_fbid(thread.id, create=True) await puppet.update_info(self, thread) await self._add_community(ups.get(portal.fbid, None), @@ -269,12 +290,7 @@ class User(Client): except Exception: self.log.exception("Failed to sync threads") - # region Facebook event handling - - async def on_logging_in(self, email: str = None) -> None: - self.log.info("Logging in {}...".format(email)) - - async def on_2fa_code(self) -> str: + async def on_2fa_callback(self) -> str: if self.command_status and self.command_status.get("action", "") == "Login": future = self.loop.create_future() self.command_status["future"] = future @@ -286,6 +302,36 @@ class User(Client): self.log.warning("Unexpected on2FACode call") # raise RuntimeError("No ongoing login command") + # region Facebook event handling + + async def try_listen(self) -> None: + try: + await self.listen() + except Exception: + self.log.exception("Fatal error in listener") + + async def listen(self) -> None: + self.listener = fbchat.Listener(session=self.session, chat_on=False, foreground=False) + handlers = { + fbchat.MessageEvent: self.on_message, + fbchat.TitleSet: self.on_title_change, + } + self.log.debug("Starting fbchat listener") + async for event in self.listener.listen(): + self.log.debug("Handling fbchat event %s", event) + try: + handler = handlers[type(event)] + except KeyError: + self.log.debug(f"Received unknown event type {type(event)}") + else: + await handler(event) + + def stop_listening(self) -> None: + if self.listener: + self.listener.disconnect() + if self.listen_task: + self.listen_task.cancel() + async def on_logged_in(self, email: str = None) -> None: """ Called when the client is successfully logged in @@ -300,152 +346,30 @@ class User(Client): await self.az.intent.send_notice(self.command_status["room_id"], f"Successfully logged in with {email}") self.save() - self.listen(long_polling=False, mqtt=True) + if self.listen_task: + self.listen_task.cancel() + self.listen_task = self.loop.create_task(self.try_listen()) asyncio.ensure_future(self.post_login(), loop=self.loop) - async def on_listening(self) -> None: - """Called when the client is listening.""" - self.log.info("Listening with long polling...") - - async def on_listening_mqtt(self) -> None: - """Called when the client is listening with MQTT.""" - self.log.info("Listening with MQTT...") - - async def on_listen_error(self, exception: Exception = None) -> bool: - """ - Called when an error was encountered while listening - - :param exception: The exception that was encountered - :return: Whether the loop should keep running - """ - self.log.exception("Got exception while listening, reconnecting in 10s") - await asyncio.sleep(10) - return True - - async def on_mqtt_fatal_error(self, exception: Exception = None) -> bool: - """Called when an error was encountered while listening. - - Args: - exception: The exception that was encountered - - Returns: - Whether the client should reconnect - """ - self.log.exception("MQTT connection failed, reconnecting in 10s") - await asyncio.sleep(10) - return True - - async def on_mqtt_parse_error(self, event_type=None, event_data=None, exception=None): - """Called when an error was encountered while parsing a MQTT message. + async def on_message(self, evt: fbchat.MessageEvent) -> None: + self.log.debug(f"onMessage({evt})") - Args: - event_type: The event type - event_data: The event data, either as a bytearray if JSON decoding failed or as a dict - if JSON decoding was successful. - exception: The exception that was encountered - """ - if isinstance(event_data, bytearray): - self.log.warning(f"MQTT JSON decoder error: {exception}") - else: - self.log.exception("Failed to parse MQTT message: %s", event_data) - - async def on_message(self, mid: str = None, author_id: str = None, message: str = None, - message_object: Message = None, thread_id: str = None, - thread_type: ThreadType = ThreadType.USER, at: int = None, - metadata: Any = None, msg: Any = None) -> None: - """ - Called when the client is listening, and somebody sends a message - - :param mid: The message ID - :param author_id: The ID of the author - :param message: (deprecated. Use `message_object.text` instead) - :param message_object: The message (As a `Message` object) - :param thread_id: Thread ID that the message was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the message was sent to. See :ref:`intro_threads` - :param at: The timestamp of the message - :param metadata: Extra metadata about the message - :param msg: A full set of the data recieved - :type message_object: models.Message - :type thread_type: models.ThreadType - """ - self.log.debug(f"onMessage({message_object}, {thread_id}, {thread_type})") - fb_receiver = self.uid if thread_type == ThreadType.USER else None - portal = po.Portal.get_by_fbid(thread_id, fb_receiver, thread_type) - puppet = pu.Puppet.get_by_fbid(author_id) + fb_receiver = self.fbid if isinstance(evt.thread, fbchat.User) else None + portal = po.Portal.get_by_thread(evt.thread, fb_receiver) + puppet = pu.Puppet.get_by_fbid(evt.author.id) if not puppet.name: await puppet.update_info(self) - message_object.uid = mid - await portal.handle_facebook_message(self, puppet, message_object) + await portal.handle_facebook_message(self, puppet, evt.message) - async def on_color_change(self, mid=None, author_id=None, new_color=None, thread_id=None, - thread_type=ThreadType.USER, at=None, metadata=None, msg=None - ) -> None: - """ - Called when the client is listening, and somebody changes a thread's color - - :param mid: The action ID - :param author_id: The ID of the person who changed the color - :param new_color: The new color - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param at: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type new_color: models.ThreadColor - :type thread_type: models.ThreadType - """ - self.log.info( - "Color change from {} in {} ({}): {}".format( - author_id, thread_id, thread_type.name, new_color - ) - ) - - async def on_emoji_change(self, mid=None, author_id=None, new_emoji=None, thread_id=None, - thread_type=ThreadType.USER, at=None, metadata=None, msg=None - ) -> None: - """ - Called when the client is listening, and somebody changes a thread's emoji - - :param mid: The action ID - :param author_id: The ID of the person who changed the emoji - :param new_emoji: The new emoji - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param at: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - self.log.info( - "Emoji change from {} in {} ({}): {}".format( - author_id, thread_id, thread_type.name, new_emoji - ) - ) - - async def on_title_change(self, mid=None, author_id=None, new_title=None, thread_id=None, - thread_type=ThreadType.USER, at=None, metadata=None, msg=None - ) -> None: - """ - Called when the client is listening, and somebody changes the title of a thread - - :param mid: The action ID - :param author_id: The ID of the person who changed the title - :param new_title: The new title - :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` - :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` - :param at: A timestamp of the action - :param metadata: Extra metadata about the action - :param msg: A full set of the data recieved - :type thread_type: models.ThreadType - """ - fb_receiver = self.uid if thread_type == ThreadType.USER else None - portal = po.Portal.get_by_fbid(thread_id, fb_receiver) + async def on_title_change(self, evt: fbchat.TitleSet) -> None: + portal = po.Portal.get_by_thread(evt.thread) if not portal: return - sender = pu.Puppet.get_by_fbid(author_id) + sender = pu.Puppet.get_by_fbid(evt.author.id) if not sender: return - await portal.handle_facebook_name(self, sender, new_title, mid) + # TODO find messageId for the event + await portal.handle_facebook_name(self, sender, evt.title, str(evt.at.timestamp())) async def on_image_change(self, mid: str = None, author_id: str = None, new_image: str = None, thread_id: str = None, thread_type: ThreadType = ThreadType.GROUP, @@ -462,7 +386,7 @@ class User(Client): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - fb_receiver = self.uid if thread_type == ThreadType.USER else None + fb_receiver = self.fbid if thread_type == ThreadType.USER else None portal = po.Portal.get_by_fbid(thread_id, fb_receiver) if not portal: return @@ -713,7 +637,7 @@ class User(Client): ) ) - async def on_reaction_added(self, mid: str = None, reaction: MessageReaction = None, + async def on_reaction_added(self, mid: str = None, reaction = None, author_id: str = None, thread_id: str = None, thread_type: ThreadType = None, at: int = None, msg: Any = None ) -> None: @@ -1048,7 +972,7 @@ class User(Client): """ pass - async def on_chat_timestamp(self, buddylist: Dict[str, ActiveStatus] = None, msg: Any = None + async def on_chat_timestamp(self, buddylist = None, msg: Any = None ) -> None: """ Called when the client receives chat online presence update @@ -1063,7 +987,7 @@ class User(Client): presence=PresenceState.ONLINE if status.active else PresenceState.OFFLINE, ignore_cache=True) - async def on_buddylist_overlay(self, statuses: Dict[str, ActiveStatus] = None, msg: Any = None + async def on_buddylist_overlay(self, statuses = None, msg: Any = None ) -> None: """ Called when the client is listening and client receives information about friend active status diff --git a/mautrix_facebook/web/public.py b/mautrix_facebook/web/public.py index 35cc74f..02594d3 100644 --- a/mautrix_facebook/web/public.py +++ b/mautrix_facebook/web/public.py @@ -1,5 +1,5 @@ # mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge -# Copyright (C) 2019 Tulir Asokan +# Copyright (C) 2020 Tulir Asokan # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -25,7 +25,7 @@ from aiohttp import web import pkg_resources import attr -from fbchat import User as FBUser +import fbchat from mautrix.types import UserID from mautrix.util.signed_token import verify_token @@ -106,7 +106,7 @@ class PublicBridgeWebsite: "facebook": None, } if await user.is_logged_in(): - info: FBUser = (await user.fetch_user_info(user.fbid))[user.fbid] + info = await user.client.fetch_thread_info([user.fbid]).__anext__() data["facebook"] = attr.asdict(info) return web.json_response(data, headers=self._acao_headers) @@ -128,10 +128,12 @@ class PublicBridgeWebsite: cookie["xs"] = data["xs"] user.user_agent = user_agent user.save() - ok = await user.set_session(cookie, user_agent) and await user.is_logged_in(True) - if not ok: + session = await fbchat.Session.from_cookies(cookie) + if not await session.is_logged_in(): raise web.HTTPUnauthorized(body='{"error": "Facebook authorization failed"}', headers=self._headers) + user.session = session + user.client = fbchat.Client(session=session) await user.on_logged_in(data["c_user"]) if user.command_status and user.command_status.get("action") == "Login": user.command_status = None @@ -140,7 +142,7 @@ class PublicBridgeWebsite: async def logout(self, request: web.Request) -> web.Response: user = self.check_token(request) - puppet = pu.Puppet.get_by_fbid(user.uid) + puppet = pu.Puppet.get_by_fbid(user.fbid) await user.logout() if puppet.is_real_user: await puppet.switch_mxid(None, None) diff --git a/requirements.txt b/requirements.txt index d3de26f..c6165bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ ruamel.yaml>=0.15.94,<0.17 commonmark>=0.8,<0.10 python-magic>=0.4,<0.5 mautrix==0.5.0.beta15 -fbchat-asyncio>=0.3.1b4 +https://github.com/tulir/fbchat-asyncio/tarball/rebase#egg=fbchat-asyncio -- GitLab