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 0000000000000000000000000000000000000000..8eef779377119c7524d8c3d8c3cbfbfd713f1f5c --- /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 bd9f0a4786702be0f6921a18de4d5951ed580419..60ebc0b16cb4bd0c717525caf46eebe4d12198d4 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 90089aac106dbea36bce922d98deff25a70a0321..92aadc3ffec07b07165997b177dc5a2a2c6e5137 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 f0e8e6db542c033119e21cbc128a06301d4d9907..87ae1bf0e64f2a13241dbf16da2e67311dfcc810 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 2724c92d2ee563ea3ce87292c3438cce3fab1c1e..926301e02e41e41068e936c7d4133cdae9739cea 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 195fc07bc603af23b122359211776c1932c12f7b..485161f018c639cfd642b19565dd0f49bcef3539 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