From cefeae9d680c0ac87bd60ab6e2824e778dabc640 Mon Sep 17 00:00:00 2001
From: Tulir Asokan <tulir@maunium.net>
Date: Tue, 30 Jun 2020 15:14:54 +0300
Subject: [PATCH] Re-add option to set user agent

---
 .../244273a86d81_re_add_user_agent_to_db.py   | 32 +++++++++++++++++++
 mautrix_facebook/commands/auth.py             | 17 ++++++++--
 mautrix_facebook/db/user.py                   |  3 +-
 mautrix_facebook/user.py                      | 15 +++++----
 mautrix_facebook/web/public.py                |  5 ++-
 requirements.txt                              |  2 +-
 6 files changed, 63 insertions(+), 11 deletions(-)
 create mode 100644 alembic/versions/244273a86d81_re_add_user_agent_to_db.py

diff --git a/alembic/versions/244273a86d81_re_add_user_agent_to_db.py b/alembic/versions/244273a86d81_re_add_user_agent_to_db.py
new file mode 100644
index 0000000..8eef779
--- /dev/null
+++ b/alembic/versions/244273a86d81_re_add_user_agent_to_db.py
@@ -0,0 +1,32 @@
+"""Re-add user agent to db
+
+Revision ID: 244273a86d81
+Revises: 54aecbd6187c
+Create Date: 2020-06-30 15:03:18.260742
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '244273a86d81'
+down_revision = '54aecbd6187c'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    with op.batch_alter_table('user', schema=None) as batch_op:
+        batch_op.add_column(sa.Column('user_agent', sa.String(length=255), nullable=True))
+
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    with op.batch_alter_table('user', schema=None) as batch_op:
+        batch_op.drop_column('user_agent')
+
+    # ### end Alembic commands ###
diff --git a/mautrix_facebook/commands/auth.py b/mautrix_facebook/commands/auth.py
index bd9f0a4..60ebc0b 100644
--- a/mautrix_facebook/commands/auth.py
+++ b/mautrix_facebook/commands/auth.py
@@ -44,7 +44,8 @@ async def login(evt: CommandEvent) -> None:
     await evt.reply("Logging in...")
     try:
         session = await fbchat.Session.login(evt.args[0], " ".join(evt.args[1:]),
-                                             on_2fa_callback=evt.sender.on_2fa_callback)
+                                             on_2fa_callback=evt.sender.on_2fa_callback,
+                                             user_agent=evt.sender.user_agent)
         await evt.sender.on_logged_in(session)
         evt.sender.command_status = None
         await evt.reply("Successfully logged in")
@@ -123,7 +124,7 @@ async def enter_login_cookies(evt: CommandEvent) -> None:
         session = await fbchat.Session.from_cookies({
             "c_user": evt.sender.command_status["c_user"],
             "xs": evt.args[0],
-        })
+        }, user_agent=evt.sender.user_agent)
     except fbchat.FacebookError as e:
         evt.sender.command_status = None
         await evt.reply(f"Failed to log in: {e}")
@@ -138,6 +139,18 @@ async def enter_login_cookies(evt: CommandEvent) -> None:
     evt.sender.command_status = None
 
 
+@command_handler(needs_auth=False, management_only=False, help_section=SECTION_AUTH,
+                 help_text="Change the user agent sent to Facebook", help_args="<_user agent_>")
+async def set_ua(evt: CommandEvent) -> None:
+    if len(evt.args) < 0:
+        await evt.reply("Usage: `$cmdprefix+sp login <user agent>`")
+        return
+    evt.sender.user_agent = " ".join(evt.args)
+    evt.sender.save()
+    await evt.reply(f"Set user agent to `{evt.sender.user_agent}`. The change will be applied when"
+                    " you log in, run `refresh`, or on the next bridge restart.")
+
+
 @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.fbid)
diff --git a/mautrix_facebook/db/user.py b/mautrix_facebook/db/user.py
index 90089aa..92aadc3 100644
--- a/mautrix_facebook/db/user.py
+++ b/mautrix_facebook/db/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
@@ -28,6 +28,7 @@ class User(Base):
     session: Dict[str, str] = Column(PickleType, nullable=True)
     fbid: str = Column(String(255), nullable=True)
     notice_room: RoomID = Column(String(255), nullable=True)
+    user_agent: str = Column(String(255), nullable=True)
 
     @classmethod
     def all(cls) -> Iterable['User']:
diff --git a/mautrix_facebook/user.py b/mautrix_facebook/user.py
index f0e8e6d..87ae1bf 100644
--- a/mautrix_facebook/user.py
+++ b/mautrix_facebook/user.py
@@ -51,6 +51,7 @@ class User(BaseUser):
     notice_room: RoomID
     _notice_room_lock: asyncio.Lock
     _notice_send_lock: asyncio.Lock
+    user_agent: Optional[str]
     is_admin: bool
     permission_level: str
     _is_logged_in: Optional[bool]
@@ -69,13 +70,14 @@ class User(BaseUser):
     _handlers: Dict[Type[fbchat.Event], Callable[[Any], Awaitable[None]]]
 
     def __init__(self, mxid: UserID, session: Optional[Dict[str, str]] = None,
-                 notice_room: Optional[RoomID] = None,
+                 notice_room: Optional[RoomID] = None, user_agent: Optional[str] = None,
                  db_instance: Optional[DBUser] = None) -> None:
         self.mxid = mxid
+        self.by_mxid[mxid] = self
         self.notice_room = notice_room
         self._notice_room_lock = asyncio.Lock()
         self._notice_send_lock = asyncio.Lock()
-        self.by_mxid[mxid] = self
+        self.user_agent = user_agent
         self.command_status = None
         self.is_whitelisted, self.is_admin, self.permission_level = config.get_permissions(mxid)
         self._is_logged_in = None
@@ -135,7 +137,7 @@ class User(BaseUser):
     def db_instance(self) -> DBUser:
         if not self._db_instance:
             self._db_instance = DBUser(mxid=self.mxid, session=self._session_data, fbid=self.fbid,
-                                       notice_room=self.notice_room)
+                                       notice_room=self.notice_room, user_agent=self.user_agent)
         return self._db_instance
 
     def save(self, _update_session_data: bool = True) -> None:
@@ -143,11 +145,11 @@ class User(BaseUser):
         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,
-                              notice_room=self.notice_room)
+                              notice_room=self.notice_room, user_agent=self.user_agent)
 
     @classmethod
     def from_db(cls, db_user: DBUser) -> 'User':
-        return User(mxid=db_user.mxid, session=db_user.session,
+        return User(mxid=db_user.mxid, session=db_user.session, user_agent=db_user.user_agent,
                     notice_room=db_user.notice_room, db_instance=db_user)
 
     @classmethod
@@ -194,7 +196,8 @@ class User(BaseUser):
         elif not self._session_data:
             return False
         try:
-            session = await fbchat.Session.from_cookies(self._session_data)
+            session = await fbchat.Session.from_cookies(self._session_data,
+                                                        user_agent=self.user_agent)
             logged_in = await session.is_logged_in()
         except Exception:
             self.log.exception("Failed to restore session")
diff --git a/mautrix_facebook/web/public.py b/mautrix_facebook/web/public.py
index 2724c92..926301e 100644
--- a/mautrix_facebook/web/public.py
+++ b/mautrix_facebook/web/public.py
@@ -102,6 +102,7 @@ class PublicBridgeWebsite:
         data = {
             "permissions": user.permission_level,
             "mxid": user.mxid,
+            "user_agent": user.user_agent,
             "facebook": None,
         }
         if await user.is_logged_in():
@@ -112,6 +113,8 @@ class PublicBridgeWebsite:
 
     async def login(self, request: web.Request) -> web.Response:
         user = self.check_token(request)
+        if not user.user_agent:
+            user.user_agent = request.headers.get("User-Agent", None)
 
         try:
             data = await request.json()
@@ -119,7 +122,7 @@ class PublicBridgeWebsite:
             raise web.HTTPBadRequest(body='{"error": "Malformed JSON"}', headers=self._headers)
 
         try:
-            session = await fbchat.Session.from_cookies(data)
+            session = await fbchat.Session.from_cookies(data, user_agent=user.user_agent)
         except fbchat.FacebookError:
             self.log.debug("Failed to log in", exc_info=True)
             raise web.HTTPUnauthorized(body='{"error": "Facebook authorization failed"}',
diff --git a/requirements.txt b/requirements.txt
index 195fc07..485161f 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.6.0.alpha2
-fbchat-asyncio>=0.6.4,<0.7
+fbchat-asyncio>=0.6.6,<0.7
-- 
GitLab