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