diff --git a/mautrix_facebook/commands/__init__.py b/mautrix_facebook/commands/__init__.py index c7420e6ad883145fefe0b64e859ec02eacb2c802..6b11117d3feea72315da4ffe8a640f8712cf7a77 100644 --- a/mautrix_facebook/commands/__init__.py +++ b/mautrix_facebook/commands/__init__.py @@ -1,4 +1,4 @@ from .handler import (CommandProcessor, CommandHandler, CommandEvent, command_handler, - SECTION_AUTH, SECTION_MISC) -from .auth import login, enter_2fa_code -from .facebook import search + SECTION_AUTH, SECTION_MISC, SECTION_CONNECTION) +from .auth import enter_2fa_code +from . import facebook as _, conn as _ diff --git a/mautrix_facebook/commands/auth.py b/mautrix_facebook/commands/auth.py index 400c6939f414ef964bc7c485487b1b6addfc663d..bd9f0a4786702be0f6921a18de4d5951ed580419 100644 --- a/mautrix_facebook/commands/auth.py +++ b/mautrix_facebook/commands/auth.py @@ -176,10 +176,3 @@ async def logout_matrix(evt: CommandEvent) -> None: await puppet.switch_mxid(None, None) await evt.reply("Restored the original puppet for your Facebook Messenger account") - -@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH, - help_text="Mark this room as your bridge notice room") -async def set_notice_room(evt: CommandEvent) -> None: - evt.sender.notice_room = evt.room_id - evt.sender.save() - await evt.reply("This room has been marked as your bridge notice room") diff --git a/mautrix_facebook/commands/conn.py b/mautrix_facebook/commands/conn.py new file mode 100644 index 0000000000000000000000000000000000000000..1070eb727488dc6e634efcd974ce72b54d996e75 --- /dev/null +++ b/mautrix_facebook/commands/conn.py @@ -0,0 +1,79 @@ +# mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# 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 cast + +import fbchat + +from . import command_handler, CommandEvent, SECTION_CONNECTION + + +@command_handler(needs_auth=False, management_only=True, help_section=SECTION_CONNECTION, + help_text="Mark this room as your bridge notice room") +async def set_notice_room(evt: CommandEvent) -> None: + evt.sender.notice_room = evt.room_id + evt.sender.save() + await evt.reply("This room has been marked as your bridge notice room") + + +@command_handler(needs_auth=True, management_only=True, help_section=SECTION_CONNECTION, + help_text="Disconnect from Facebook Messenger") +async def disconnect(evt: CommandEvent) -> None: + if not evt.sender.listener: + await evt.reply("You don't have a Messenger MQTT connection") + return + evt.sender.listener.disconnect() + + +@command_handler(needs_auth=True, management_only=True, help_section=SECTION_CONNECTION, + help_text="Connect to Facebook Messenger") +async def connect(evt: CommandEvent) -> None: + if evt.sender.listen_task and not evt.sender.listen_task.done(): + await evt.reply("You already have a Messenger MQTT connection") + return + evt.sender.listen_task = evt.loop.create_task(evt.sender.try_listen()) + + +@command_handler(needs_auth=True, management_only=True, help_section=SECTION_CONNECTION, + help_text="Check if you're logged into Facebook Messenger") +async def ping(evt: CommandEvent) -> None: + if not evt.sender.is_logged_in(): + await evt.reply("You're not logged into Facebook Messenger") + return + own_info = cast(fbchat.User, + await evt.sender.client.fetch_thread_info([evt.sender.fbid]).__anext__()) + await evt.reply(f"You're logged in as {own_info.name} (user ID {own_info.id})") + + if not evt.sender.listen_task or evt.sender.listen_task.done(): + await evt.reply("You don't have a Messenger MQTT connection. Use `connect` to connect.") + elif not evt.sender._is_connected: + await evt.reply("The Messenger MQTT listener is **disconnected**.") + else: + await evt.reply("The Messenger MQTT listener is connected.") + + +@command_handler(needs_auth=True, management_only=True, help_section=SECTION_CONNECTION, + help_text="\"Refresh\" the Facebook Messenger page") +async def refresh(evt: CommandEvent) -> None: + if evt.sender.listener: + await evt.reply("Disconnecting Messenger MQTT connection") + evt.sender.listener.disconnect() + await evt.sender.listen_task + await evt.reply("Refreshing session") + ok = await evt.sender.load_session(_override=True) + if ok: + await evt.reply("Successfully refreshed session") + else: + await evt.reply("Failed to refresh session") diff --git a/mautrix_facebook/commands/handler.py b/mautrix_facebook/commands/handler.py index 773b4163c34a986f76fd606296b4ea4f94fc989f..12b7e3bfe6f2b2b75d6e86bfa02c2577d71a41e6 100644 --- a/mautrix_facebook/commands/handler.py +++ b/mautrix_facebook/commands/handler.py @@ -24,6 +24,7 @@ from .. import user as u, context as c HelpCacheKey = NamedTuple('FBHelpCacheKey', is_management=bool, is_admin=bool, is_logged_in=bool) SECTION_AUTH = HelpSection("Authentication", 10, "") +SECTION_CONNECTION = HelpSection("Connection management", 15, "") SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "") SECTION_PORTAL_MANAGEMENT = HelpSection("Portal management", 30, "") SECTION_MISC = HelpSection("Miscellaneous", 40, "") diff --git a/mautrix_facebook/user.py b/mautrix_facebook/user.py index 37fc149093eb83501babd6c90b77db1f87f4a91a..d8e8e55570a77d9f7d87c2843feadc71c00898bd 100644 --- a/mautrix_facebook/user.py +++ b/mautrix_facebook/user.py @@ -50,6 +50,7 @@ class User(BaseUser): is_admin: bool permission_level: str _is_logged_in: Optional[bool] + _is_connected: Optional[bool] _session_data: Optional[Dict[str, str]] _db_instance: Optional[DBUser] @@ -66,6 +67,7 @@ class User(BaseUser): self.command_status = None self.is_whitelisted, self.is_admin, self.permission_level = config.get_permissions(mxid) self._is_logged_in = None + self._is_connected = None self._session_data = session self._db_instance = db_instance self._community_id = None @@ -142,8 +144,8 @@ class User(BaseUser): return None - async def load_session(self) -> bool: - if self._is_logged_in: + async def load_session(self, _override: bool = False) -> bool: + if self._is_logged_in and not _override: return True elif not self._session_data: return False @@ -156,6 +158,7 @@ class User(BaseUser): self.log.info("Loaded session successfully") self.session = session self.client = fbchat.Client(session=self.session) + self._is_logged_in = True if self.listen_task: self.listen_task.cancel() self.listen_task = self.loop.create_task(self.try_listen()) @@ -183,8 +186,10 @@ class User(BaseUser): ok = False self._session_data = None self._is_logged_in = False + self._is_connected = None self.client = None self.session = None + self.listener = None self.save(_update_session_data=False) return ok @@ -327,6 +332,7 @@ class User(BaseUser): try: await self.listen() except Exception: + self._is_connected = False await self.send_bridge_notice("Fatal error in listener (see logs for more info)") self.log.exception("Fatal error in listener") try: @@ -335,7 +341,8 @@ class User(BaseUser): self.log.debug("Error disconnecting listener after error", exc_info=True) async def listen(self) -> None: - self.listener = fbchat.Listener(session=self.session, chat_on=True, foreground=False) + if not self.listener: + self.listener = fbchat.Listener(session=self.session, chat_on=True, foreground=False) handlers: Dict[Type[fbchat.Event], Callable[[Any], Awaitable[None]]] = { fbchat.MessageEvent: self.on_message, fbchat.MessageReplyEvent: self.on_message, @@ -362,11 +369,15 @@ class User(BaseUser): await handler(event) except Exception: self.log.exception("Failed to handle facebook event") + self._is_connected = False + await self.send_bridge_notice("Facebook Messenger connection closed without error") async def on_connect(self, evt: fbchat.Connect) -> None: + self._is_connected = True await self.send_bridge_notice("Connected to Facebook Messenger") async def on_disconnect(self, evt: fbchat.Disconnect) -> None: + self._is_connected = False await self.send_bridge_notice(f"Disconnected from Facebook Messenger: {evt.reason}") def stop_listening(self) -> None: