Example #1
0
 def prepare_appservice(self) -> None:
     self.make_state_store()
     mb = 1024**2
     default_http_retry_count = self.config.get(
         "homeserver.http_retry_count", None)
     if self.name not in HTTPAPI.default_ua:
         HTTPAPI.default_ua = f"{self.name}/{self.version} {HTTPAPI.default_ua}"
     self.az = AppService(
         server=self.config["homeserver.address"],
         domain=self.config["homeserver.domain"],
         verify_ssl=self.config["homeserver.verify_ssl"],
         connection_limit=self.config["homeserver.connection_limit"],
         id=self.config["appservice.id"],
         as_token=self.config["appservice.as_token"],
         hs_token=self.config["appservice.hs_token"],
         tls_cert=self.config.get("appservice.tls_cert", None),
         tls_key=self.config.get("appservice.tls_key", None),
         bot_localpart=self.config["appservice.bot_username"],
         ephemeral_events=self.config["appservice.ephemeral_events"],
         default_ua=HTTPAPI.default_ua,
         default_http_retry_count=default_http_retry_count,
         log="mau.as",
         loop=self.loop,
         state_store=self.state_store,
         bridge_name=self.name,
         aiohttp_params={
             "client_max_size": self.config["appservice.max_body_size"] * mb
         },
     )
     self.az.app.router.add_post("/_matrix/app/com.beeper.bridge_state",
                                 self.get_bridge_state)
Example #2
0
    def prepare_appservice(self) -> None:
        self.state_store = self.state_store_class()
        mb = 1024 ** 2
        self.az = AppService(server=self.config["homeserver.address"],
                             domain=self.config["homeserver.domain"],
                             verify_ssl=self.config["homeserver.verify_ssl"],

                             as_token=self.config["appservice.as_token"],
                             hs_token=self.config["appservice.hs_token"],

                             tls_cert=self.config.get("appservice.tls_cert", None),
                             tls_key=self.config.get("appservice.tls_key", None),

                             bot_localpart=self.config["appservice.bot_username"],

                             log="mau.as",
                             loop=self.loop,

                             state_store=self.state_store,

                             real_user_content_key=self.real_user_content_key,

                             aiohttp_params={
                                 "client_max_size": self.config["appservice.max_body_size"] * mb
                             })
Example #3
0
 def prepare_appservice(self) -> None:
     self.make_state_store()
     mb = 1024**2
     default_http_retry_count = self.config.get(
         "homeserver.http_retry_count", None)
     self.az = AppService(
         server=self.config["homeserver.address"],
         domain=self.config["homeserver.domain"],
         verify_ssl=self.config["homeserver.verify_ssl"],
         id=self.config["appservice.id"],
         as_token=self.config["appservice.as_token"],
         hs_token=self.config["appservice.hs_token"],
         tls_cert=self.config.get("appservice.tls_cert", None),
         tls_key=self.config.get("appservice.tls_key", None),
         bot_localpart=self.config["appservice.bot_username"],
         ephemeral_events=self.config["appservice.ephemeral_events"],
         default_ua=f"{self.name}/{self.version} {HTTPAPI.default_ua}",
         default_http_retry_count=default_http_retry_count,
         log="mau.as",
         loop=self.loop,
         state_store=self.state_store,
         real_user_content_key=self.real_user_content_key,
         aiohttp_params={
             "client_max_size": self.config["appservice.max_body_size"] * mb
         })
Example #4
0
    def prepare_appservice(self) -> None:
        self.state_store = self.state_store_class()
        mb = 1024**2
        self.az = AppService(
            server=self.config["homeserver.address"],
            domain=self.config["homeserver.domain"],
            verify_ssl=self.config["homeserver.verify_ssl"],
            as_token=self.config["appservice.as_token"],
            hs_token=self.config["appservice.hs_token"],
            bot_localpart=self.config["appservice.bot_username"],
            log="mau.as",
            loop=self.loop,
            state_store=self.state_store,
            real_user_content_key=self.real_user_content_key,
            aiohttp_params={
                "client_max_size": self.config["appservice.max_body_size"] * mb
            })

        signal.signal(signal.SIGINT, signal.default_int_handler)
        signal.signal(signal.SIGTERM, signal.default_int_handler)
Example #5
0
    async def reset(self, config_file, homeserver_url):
        with open(config_file) as f:
            registration = yaml.load(f)

        api = HTTPAPI(base_url=homeserver_url, token=registration["as_token"])
        whoami = await api.request(Method.GET, Path.v3.account.whoami)
        self.user_id = whoami["user_id"]
        self.server_name = self.user_id.split(":", 1)[1]
        print("We are " + whoami["user_id"])

        self.az = MauService(
            id=registration["id"],
            domain=self.server_name,
            server=homeserver_url,
            as_token=registration["as_token"],
            hs_token=registration["hs_token"],
            bot_localpart=registration["sender_localpart"],
            state_store=MemoryBridgeStateStore(),
        )

        try:
            await self.az.start(host="127.0.0.1", port=None)
        except Exception:
            logging.exception("Failed to listen.")
            return

        joined_rooms = await self.az.intent.get_joined_rooms()
        print(f"Leaving from {len(joined_rooms)} rooms...")

        for room_id in joined_rooms:
            print(f"Leaving from {room_id}...")
            await self.leave_room(room_id, None)

        print("Resetting configuration...")
        self.config = {}
        await self.save()

        print("All done!")
Example #6
0
db_engine = sql.create_engine(config["appservice.database"]
                              or "sqlite:///mautrix-facebook.db")
Base.metadata.bind = db_engine
init_db(db_engine)

loop = asyncio.get_event_loop()

state_store = SQLStateStore()
mebibyte = 1024**2
appserv = AppService(config["homeserver.address"],
                     config["homeserver.domain"],
                     config["appservice.as_token"],
                     config["appservice.hs_token"],
                     config["appservice.bot_username"],
                     log="mau.as",
                     loop=loop,
                     verify_ssl=config["homeserver.verify_ssl"],
                     state_store=state_store,
                     real_user_content_key="net.maunium.facebook.puppet",
                     aiohttp_params={
                         "client_max_size":
                         config["appservice.max_body_size"] * mebibyte
                     })

context = Context(az=appserv, config=config, loop=loop)
context.mx = MatrixHandler(context)

init_user(context)
init_portal(context)
init_puppet(context)

signal.signal(signal.SIGINT, signal.default_int_handler)
Example #7
0
class Bridge(Program, ABC):
    db: Database
    az: AppService
    state_store_class: type[ASStateStore] = PgBridgeStateStore
    state_store: ASStateStore
    upgrade_table: UpgradeTable
    config_class: type[br.BaseBridgeConfig]
    config: br.BaseBridgeConfig
    matrix_class: type[br.BaseMatrixHandler]
    matrix: br.BaseMatrixHandler
    repo_url: str
    markdown_version: str
    manhole: br.manhole.ManholeState | None

    def __init__(
        self,
        module: str = None,
        name: str = None,
        description: str = None,
        command: str = None,
        version: str = None,
        config_class: type[br.BaseBridgeConfig] = None,
        matrix_class: type[br.BaseMatrixHandler] = None,
        state_store_class: type[ASStateStore] = None,
    ) -> None:
        super().__init__(module, name, description, command, version,
                         config_class)
        if matrix_class:
            self.matrix_class = matrix_class
        if state_store_class:
            self.state_store_class = state_store_class
        self.manhole = None

    def prepare_arg_parser(self) -> None:
        super().prepare_arg_parser()
        self.parser.add_argument(
            "-g",
            "--generate-registration",
            action="store_true",
            help="generate registration and quit",
        )
        self.parser.add_argument(
            "-r",
            "--registration",
            type=str,
            default="registration.yaml",
            metavar="<path>",
            help=("the path to save the generated registration to "
                  "(not needed for running the bridge)"),
        )
        self.parser.add_argument(
            "--ignore-unsupported-database",
            action="store_true",
            help="Run even if the database schema is too new",
        )
        self.parser.add_argument(
            "--ignore-foreign-tables",
            action="store_true",
            help=
            "Run even if the database contains tables from other programs (like Synapse)",
        )

    def preinit(self) -> None:
        super().preinit()
        if self.args.generate_registration:
            self.generate_registration()
            sys.exit(0)

    def prepare(self) -> None:
        super().prepare()
        self.prepare_db()
        self.prepare_appservice()
        self.prepare_bridge()

    def prepare_config(self) -> None:
        self.config = self.config_class(self.args.config,
                                        self.args.registration,
                                        self.args.base_config)
        if self.args.generate_registration:
            self.config._check_tokens = False
        self.load_and_update_config()

    def generate_registration(self) -> None:
        self.config.generate_registration()
        self.config.save()
        print(
            f"Registration generated and saved to {self.config.registration_path}"
        )

    def make_state_store(self) -> None:
        if self.state_store_class is None:
            raise RuntimeError("state_store_class is not set")
        elif issubclass(self.state_store_class, PgBridgeStateStore):
            self.state_store = self.state_store_class(self.db, self.get_puppet,
                                                      self.get_double_puppet)
        else:
            self.state_store = self.state_store_class()

    def prepare_appservice(self) -> None:
        self.make_state_store()
        mb = 1024**2
        default_http_retry_count = self.config.get(
            "homeserver.http_retry_count", None)
        if self.name not in HTTPAPI.default_ua:
            HTTPAPI.default_ua = f"{self.name}/{self.version} {HTTPAPI.default_ua}"
        self.az = AppService(
            server=self.config["homeserver.address"],
            domain=self.config["homeserver.domain"],
            verify_ssl=self.config["homeserver.verify_ssl"],
            connection_limit=self.config["homeserver.connection_limit"],
            id=self.config["appservice.id"],
            as_token=self.config["appservice.as_token"],
            hs_token=self.config["appservice.hs_token"],
            tls_cert=self.config.get("appservice.tls_cert", None),
            tls_key=self.config.get("appservice.tls_key", None),
            bot_localpart=self.config["appservice.bot_username"],
            ephemeral_events=self.config["appservice.ephemeral_events"],
            default_ua=HTTPAPI.default_ua,
            default_http_retry_count=default_http_retry_count,
            log="mau.as",
            loop=self.loop,
            state_store=self.state_store,
            bridge_name=self.name,
            aiohttp_params={
                "client_max_size": self.config["appservice.max_body_size"] * mb
            },
        )
        self.az.app.router.add_post("/_matrix/app/com.beeper.bridge_state",
                                    self.get_bridge_state)

    def prepare_db(self) -> None:
        if not hasattr(self, "upgrade_table") or not self.upgrade_table:
            raise RuntimeError("upgrade_table is not set")
        self.db = Database.create(
            self.config["appservice.database"],
            upgrade_table=self.upgrade_table,
            db_args=self.config["appservice.database_opts"],
            owner_name=self.name,
            ignore_foreign_tables=self.args.ignore_foreign_tables,
        )

    def prepare_bridge(self) -> None:
        self.matrix = self.matrix_class(bridge=self)

    def _log_db_error(self, e: Exception) -> None:
        self.log.critical("Failed to initialize database", exc_info=e)
        if isinstance(e, DatabaseException) and e.explanation:
            self.log.info(e.explanation)
        sys.exit(25)

    async def start_db(self) -> None:
        if hasattr(self, "db") and isinstance(self.db, Database):
            self.log.debug("Starting database...")
            ignore_unsupported = self.args.ignore_unsupported_database
            self.db.upgrade_table.allow_unsupported = ignore_unsupported
            try:
                await self.db.start()
                if isinstance(self.state_store, PgClientStateStore):
                    self.state_store.upgrade_table.allow_unsupported = ignore_unsupported
                    await self.state_store.upgrade_table.upgrade(self.db)
                if self.matrix.e2ee:
                    self.matrix.e2ee.crypto_db.allow_unsupported = ignore_unsupported
                    self.matrix.e2ee.crypto_db.override_pool(self.db)
            except Exception as e:
                self._log_db_error(e)

    async def stop_db(self) -> None:
        if hasattr(self, "db") and isinstance(self.db, Database):
            await self.db.stop()

    async def start(self) -> None:
        await self.start_db()

        self.log.debug("Starting appservice...")
        await self.az.start(self.config["appservice.hostname"],
                            self.config["appservice.port"])
        try:
            await self.matrix.wait_for_connection()
        except MUnknownToken:
            self.log.critical(
                "The as_token was not accepted. Is the registration file installed "
                "in your homeserver correctly?")
            sys.exit(16)
        except MExclusive:
            self.log.critical(
                "The as_token was accepted, but the /register request was not. "
                "Are the homeserver domain and username template in the config "
                "correct, and do they match the values in the registration?")
            sys.exit(16)

        await self.matrix.init_encryption()
        self.add_startup_actions(self.matrix.init_as_bot())
        await super().start()
        self.az.ready = True

        status_endpoint = self.config["homeserver.status_endpoint"]
        if status_endpoint and await self.count_logged_in_users() == 0:
            state = BridgeState(
                state_event=BridgeStateEvent.UNCONFIGURED).fill()
            await state.send(status_endpoint, self.az.as_token, self.log)

    async def system_exit(self) -> None:
        if hasattr(self, "db") and isinstance(self.db, Database):
            self.log.trace("Stopping database due to SystemExit")
            await self.db.stop()

    async def stop(self) -> None:
        if self.manhole:
            self.manhole.close()
            self.manhole = None
        await self.az.stop()
        await super().stop()
        if self.matrix.e2ee:
            await self.matrix.e2ee.stop()
        await self.stop_db()

    async def get_bridge_state(self, req: web.Request) -> web.Response:
        if not self.az._check_token(req):
            return web.json_response({"error": "Invalid auth token"},
                                     status=401)
        try:
            user = await self.get_user(UserID(req.url.query["user_id"]),
                                       create=False)
        except KeyError:
            user = None
        if user is None:
            return web.json_response({"error": "User not found"}, status=404)
        try:
            states = await user.get_bridge_states()
        except NotImplementedError:
            return web.json_response(
                {"error": "Bridge status not implemented"}, status=501)
        for state in states:
            await user.fill_bridge_state(state)
        global_state = BridgeState(state_event=BridgeStateEvent.RUNNING).fill()
        evt = GlobalBridgeState(
            remote_states={state.remote_id: state
                           for state in states},
            bridge_state=global_state)
        return web.json_response(evt.serialize())

    @abstractmethod
    async def get_user(self,
                       user_id: UserID,
                       create: bool = True) -> br.BaseUser | None:
        pass

    @abstractmethod
    async def get_portal(self, room_id: RoomID) -> br.BasePortal | None:
        pass

    @abstractmethod
    async def get_puppet(self,
                         user_id: UserID,
                         create: bool = False) -> br.BasePuppet | None:
        pass

    @abstractmethod
    async def get_double_puppet(self, user_id: UserID) -> br.BasePuppet | None:
        pass

    @abstractmethod
    def is_bridge_ghost(self, user_id: UserID) -> bool:
        pass

    @abstractmethod
    async def count_logged_in_users(self) -> int:
        return 0

    async def manhole_global_namespace(self,
                                       user_id: UserID) -> dict[str, Any]:
        own_user = await self.get_user(user_id, create=False)
        try:
            own_puppet = await own_user.get_puppet()
        except NotImplementedError:
            own_puppet = None
        return {
            "bridge": self,
            "manhole": self.manhole,
            "own_user": own_user,
            "own_puppet": own_puppet,
        }

    @property
    def manhole_banner_python_version(self) -> str:
        return f"Python {sys.version} on {sys.platform}"

    @property
    def manhole_banner_program_version(self) -> str:
        return f"{self.name} {self.version} with mautrix-python {__mautrix_version__}"

    def manhole_banner(self, user_id: UserID) -> str:
        return (f"{self.manhole_banner_python_version}\n"
                f"{self.manhole_banner_program_version}\n\n"
                f"Manhole opened by {user_id}\n")
Example #8
0
class BridgeAppService(AppService):
    az: MauService
    _api: HTTPAPI
    _rooms: Dict[str, Room]
    _users: Dict[str, str]

    def register_room(self, room: Room):
        self._rooms[room.id] = room

    def unregister_room(self, room_id):
        if room_id in self._rooms:
            del self._rooms[room_id]

    # this is mostly used by network rooms at init, it's a bit slow
    def find_rooms(self, rtype=None, user_id=None) -> List[Room]:
        ret = []

        if rtype is not None and type(rtype) != str:
            rtype = rtype.__name__

        for room in self._rooms.values():
            if (rtype is None or room.__class__.__name__
                    == rtype) and (user_id is None or room.user_id == user_id):
                ret.append(room)

        return ret

    def is_admin(self, user_id: str):
        if user_id == self.config["owner"]:
            return True

        for mask, value in self.config["allow"].items():
            if fnmatch(user_id, mask) and value == "admin":
                return True

        return False

    def is_user(self, user_id: str):
        if self.is_admin(user_id):
            return True

        for mask in self.config["allow"].keys():
            if fnmatch(user_id, mask):
                return True

        return False

    def is_local(self, mxid: str):
        return mxid.endswith(":" + self.server_name)

    def strip_nick(self, nick: str) -> Tuple[str, str]:
        m = re.match(r"^([~&@%\+]?)(.+)$", nick)
        if m:
            return (m.group(2), (m.group(1) if len(m.group(1)) > 0 else None))
        else:
            raise TypeError(f"Input nick is not valid: '{nick}'")

    def split_irc_user_id(self, user_id):
        (name, server) = user_id.split(":", 1)

        network = None
        nick = None

        if server != self.server_name:
            return None, None

        if not name.startswith("@" + self.puppet_prefix):
            return None, None

        network_nick = name[len(self.puppet_prefix) + 1:]

        m = re.match(r"([^" + self.puppet_separator + r"]+).(.+)$",
                     network_nick)

        if m:
            network = re.sub(
                r"=([0-9a-z]{2})",
                lambda m: bytes.fromhex(m.group(1)).decode("utf-8"),
                m.group(1)).lower()
            nick = re.sub(r"=([0-9a-z]{2})",
                          lambda m: bytes.fromhex(m.group(1)).decode("utf-8"),
                          m.group(2)).lower()

        return network, nick

    def nick_from_irc_user_id(self, network, user_id):
        (name, server) = user_id.split(":", 1)

        if server != self.server_name:
            return None

        prefix = "@" + re.sub(
            r"[^0-9a-z\-\.=\_/]",
            lambda m: "=" + m.group(0).encode("utf-8").hex(),
            f"{self.puppet_prefix}{network}{self.puppet_separator}".lower(),
        )

        if not name.startswith(prefix):
            return None

        nick = name[len(prefix):]
        nick = re.sub(r"=([0-9a-z]{2})",
                      lambda m: bytes.fromhex(m.group(1)).decode("utf-8"),
                      nick)

        return nick

    def irc_user_id(self, network, nick, at=True, server=True):
        nick, mode = self.strip_nick(nick)

        ret = re.sub(
            r"[^0-9a-z\-\.=\_/]",
            lambda m: "=" + m.group(0).encode("utf-8").hex(),
            f"{self.puppet_prefix}{network}{self.puppet_separator}{nick}".
            lower(),
        )

        if at:
            ret = "@" + ret

        if server:
            ret += ":" + self.server_name

        return ret

    async def cache_user(self, user_id, displayname):
        # start by caching that the user_id exists without a displayname
        if user_id not in self._users:
            self._users[user_id] = None

        # if the cached displayname is incorrect
        if displayname and self._users[user_id] != displayname:
            try:
                await self.az.intent.user(user_id).set_displayname(displayname)
                self._users[user_id] = displayname
            except MatrixRequestError as e:
                logging.warning(
                    f"Failed to set displayname '{displayname}' for user_id '{user_id}', got '{e}'"
                )

    def is_user_cached(self, user_id, displayname=None):
        return user_id in self._users and (displayname is None or
                                           self._users[user_id] == displayname)

    async def ensure_irc_user_id(self, network, nick, update_cache=True):
        user_id = self.irc_user_id(network, nick)

        # if we've seen this user before, we can skip registering
        if not self.is_user_cached(user_id):
            await self.az.intent.user(self.irc_user_id(network, nick)
                                      ).ensure_registered()

        # always ensure the displayname is up-to-date
        if update_cache:
            await self.cache_user(user_id, nick)

        return user_id

    async def _on_mx_event(self, event):

        if event.room_id and event.room_id in self._rooms:
            try:
                room = self._rooms[event.room_id]
                await room.on_mx_event(event)
            except RoomInvalidError:
                logging.info(
                    f"Event handler for {event.type} threw RoomInvalidError, leaving and cleaning up."
                )
                self.unregister_room(room.id)
                room.cleanup()

                await self.leave_room(room.id, room.members)
            except Exception:
                logging.exception(
                    "Ignoring exception from room handler. This should be fixed."
                )
        elif (str(event.type) == "m.room.member"
              and event.sender != self.user_id
              and event.content.membership == Membership.INVITE):
            # set owner if we have none and the user is from the same HS
            if self.config.get(
                    "owner",
                    None) is None and event.sender.endswith(":" +
                                                            self.server_name):
                logging.info(
                    f"We have an owner now, let us rejoice, {event.sender}!")
                self.config["owner"] = event.sender
                await self.save()

            if not self.is_user(event.sender):
                logging.info(
                    f"Non-whitelisted user {event.sender} tried to invite us, ignoring."
                )
                return
            else:
                logging.info(f"Got an invite from {event.sender}")

            if not event.content.is_direct:
                logging.debug("Got an invite to non-direct room, ignoring")
                return

            # only respond to invites unknown new rooms
            if event.room_id in self._rooms:
                logging.debug(
                    "Got an invite to room we're already in, ignoring")
                return

            # handle invites against puppets
            if event.state_key != self.user_id:
                logging.info(
                    f"Whitelisted user {event.sender} invited {event.state_key}, going to reject."
                )

                try:
                    await self.az.intent.user(event.state_key).kick_user(
                        event.room_id,
                        event.state_key,
                        "Will invite YOU instead",
                    )
                except Exception:
                    logging.exception("Failed to reject invitation.")

                (network, nick) = self.split_irc_user_id(event.state_key)

                if network is not None and nick is not None:
                    for room in self.find_rooms(NetworkRoom, event.sender):
                        if room.name.lower() == network.lower():
                            logging.debug(
                                "Found matching network room ({network}) for {event.sender}, emulating query command for {nick}"
                            )
                            await room.cmd_query(
                                argparse.Namespace(nick=nick, message=[]))
                            break

                return

            logging.info(
                f"Whitelisted user {event.sender} invited us, going to accept."
            )

            # accept invite sequence
            try:
                room = ControlRoom(id=event.room_id,
                                   user_id=event.sender,
                                   serv=self,
                                   members=[event.sender],
                                   bans=[])
                await room.save()
                self.register_room(room)

                await self.az.intent.join_room(room.id)

                # show help on open
                await room.show_help()
            except Exception:
                if event.room_id in self._rooms:
                    del self._rooms[event.room_id]
                logging.exception("Failed to create control room.")
        else:
            pass
            # print(json.dumps(event, indent=4, sort_keys=True))

    async def detect_public_endpoint(self):
        async with self.api.session as session:
            # first try https well-known
            try:
                resp = await session.request(
                    "GET",
                    "https://{}/.well-known/matrix/client".format(
                        self.server_name),
                )
                data = await resp.json(content_type=None)
                return data["m.homeserver"]["base_url"]
            except Exception:
                logging.debug("Did not find .well-known for HS")

            # try https directly
            try:
                resp = await session.request(
                    "GET", "https://{}/_matrix/client/versions".format(
                        self.server_name))
                await resp.json(content_type=None)
                return "https://{}".format(self.server_name)
            except Exception:
                logging.debug("Could not use direct connection to HS")

            # give up
            logging.warning(
                "Using internal URL for homeserver, media links are likely broken!"
            )
            return str(self.api.base_url)

    def mxc_to_url(self, mxc, filename=None):
        mxc = urllib.parse.urlparse(mxc)

        if filename is None:
            filename = ""
        else:
            filename = "/" + urllib.parse.quote(filename)

        return "{}/_matrix/media/r0/download/{}{}{}".format(
            self.endpoint, mxc.netloc, mxc.path, filename)

    async def reset(self, config_file, homeserver_url):
        with open(config_file) as f:
            registration = yaml.load(f)

        api = HTTPAPI(base_url=homeserver_url, token=registration["as_token"])
        whoami = await api.request(Method.GET, Path.v3.account.whoami)
        self.user_id = whoami["user_id"]
        self.server_name = self.user_id.split(":", 1)[1]
        print("We are " + whoami["user_id"])

        self.az = MauService(
            id=registration["id"],
            domain=self.server_name,
            server=homeserver_url,
            as_token=registration["as_token"],
            hs_token=registration["hs_token"],
            bot_localpart=registration["sender_localpart"],
            state_store=MemoryBridgeStateStore(),
        )

        try:
            await self.az.start(host="127.0.0.1", port=None)
        except Exception:
            logging.exception("Failed to listen.")
            return

        joined_rooms = await self.az.intent.get_joined_rooms()
        print(f"Leaving from {len(joined_rooms)} rooms...")

        for room_id in joined_rooms:
            print(f"Leaving from {room_id}...")
            await self.leave_room(room_id, None)

        print("Resetting configuration...")
        self.config = {}
        await self.save()

        print("All done!")

    def load_reg(self, config_file):
        with open(config_file) as f:
            self.registration = yaml.load(f)

    async def leave_room(self, room_id, members):
        members = members if members else []

        for member in members:
            (name, server) = member.split(":", 1)

            if name.startswith(
                    "@" + self.puppet_prefix) and server == self.server_name:
                try:
                    await self.az.intent.user(member).leave_room(room_id)
                except Exception:
                    logging.exception("Removing puppet on leave failed")

        try:
            await self.az.intent.leave_room(room_id)
        except MatrixRequestError:
            pass
        try:
            await self.az.intent.forget_room(room_id)
        except MatrixRequestError:
            pass

    def _keepalive(self):
        async def put_presence():
            try:
                await self.az.intent.set_presence(self.user_id)
            except Exception:
                pass

        asyncio.ensure_future(put_presence())
        asyncio.get_event_loop().call_later(60, self._keepalive)

    async def run(self, listen_address, listen_port, homeserver_url, owner,
                  safe_mode):

        if "sender_localpart" not in self.registration:
            print("Missing sender_localpart from registration file.")
            sys.exit(1)

        if "namespaces" not in self.registration or "users" not in self.registration[
                "namespaces"]:
            print("User namespaces missing from registration file.")
            sys.exit(1)

        # remove self namespace if exists
        self_ns = f"@{self.registration['sender_localpart']}:.*"
        ns_users = [
            x for x in self.registration["namespaces"]["users"]
            if x["regex"] != self_ns
        ]

        if len(ns_users) != 1:
            print(
                "A single user namespace is required for puppets in the registration file."
            )
            sys.exit(1)

        if "exclusive" not in ns_users[0] or not ns_users[0]["exclusive"]:
            print("User namespace must be exclusive.")
            sys.exit(1)

        m = re.match(r"^@(.+)([\_/])\.\*$", ns_users[0]["regex"])
        if not m:
            print(
                "User namespace regex must be an exact prefix like '@irc_.*' that includes the separator character (_ or /)."
            )
            sys.exit(1)

        self.puppet_separator = m.group(2)
        self.puppet_prefix = m.group(1) + self.puppet_separator

        print(f"Heisenbridge v{__version__}", flush=True)
        if safe_mode:
            print("Safe mode is enabled.", flush=True)

        self.api = HTTPAPI(base_url=homeserver_url,
                           token=self.registration["as_token"])

        # conduit requires that the appservice user is registered before whoami
        wait = 0
        while True:
            try:
                await self.api.request(
                    Method.POST,
                    Path.v3.register,
                    {
                        "type": "m.login.application_service",
                        "username": self.registration["sender_localpart"],
                    },
                )
                logging.debug("Appservice user registration succeeded.")
                break
            except MUserInUse:
                logging.debug("Appservice user is already registered.")
                break
            except MatrixConnectionError as e:
                if wait < 30:
                    wait += 5
                logging.warning(
                    f"Failed to connect to HS: {e}, retrying in {wait} seconds..."
                )
                await asyncio.sleep(wait)
            except Exception:
                logging.exception(
                    "Unexpected failure when registering appservice user.")
                sys.exit(1)

        # mautrix migration requires us to call whoami manually at this point
        whoami = await self.api.request(Method.GET, Path.v3.account.whoami)

        logging.info("We are " + whoami["user_id"])

        self.user_id = whoami["user_id"]
        self.server_name = self.user_id.split(":", 1)[1]

        self.az = MauService(
            id=self.registration["id"],
            domain=self.server_name,
            server=homeserver_url,
            as_token=self.registration["as_token"],
            hs_token=self.registration["hs_token"],
            bot_localpart=self.registration["sender_localpart"],
            state_store=MemoryBridgeStateStore(),
        )
        self.az.matrix_event_handler(self._on_mx_event)

        try:
            await self.az.start(host=listen_address, port=listen_port)
        except Exception:
            logging.exception("Failed to listen.")
            sys.exit(1)

        try:
            await self.az.intent.ensure_registered()
            logging.debug("Appservice user exists at least now.")
        except Exception:
            logging.exception(
                "Unexpected failure when registering appservice user.")
            sys.exit(1)

        self._rooms = {}
        self._users = {}
        self.config = {
            "networks": {},
            "owner": None,
            "allow": {},
            "idents": {},
            "member_sync": "half",
            "max_lines": 0,
            "use_pastebin": True,
            "media_url": None,
            "namespace": self.puppet_prefix,
        }
        logging.debug(f"Default config: {self.config}")
        self.synapse_admin = False

        try:
            is_admin = await self.api.request(
                Method.GET, SynapseAdminPath.v1.users[self.user_id].admin)
            self.synapse_admin = is_admin["admin"]
        except MForbidden:
            logging.info(
                f"We ({self.user_id}) are not a server admin, inviting puppets is required."
            )
        except Exception:
            logging.info(
                "Seems we are not connected to Synapse, inviting puppets is required."
            )

        # load config from HS
        await self.load()

        # use configured media_url for endpoint if we have it
        if self.config["media_url"]:
            self.endpoint = self.config["media_url"]
        else:
            self.endpoint = await self.detect_public_endpoint()

        print("Homeserver is publicly available at " + self.endpoint,
              flush=True)

        logging.info("Starting presence loop")
        self._keepalive()

        # do a little migration for servers, remove this later
        for network in self.config["networks"].values():
            new_servers = []

            for server in network["servers"]:
                if isinstance(server, str):
                    new_servers.append({
                        "address": server,
                        "port": 6667,
                        "tls": False
                    })

            if len(new_servers) > 0:
                logging.debug(
                    "Migrating servers from old to new config format")
                network["servers"] = new_servers

        logging.debug(f"Merged configuration from HS: {self.config}")

        # prevent starting bridge with changed namespace
        if self.config["namespace"] != self.puppet_prefix:
            logging.error(
                f"Previously used namespace '{self.config['namespace']}' does not match current '{self.puppet_prefix}'."
            )
            sys.exit(1)

        # honor command line owner
        if owner is not None and self.config["owner"] != owner:
            logging.info(f"Overriding loaded owner with '{owner}'")
            self.config["owner"] = owner

        # always ensure our merged and migrated configuration is up-to-date
        await self.save()

        print("Fetching joined rooms...", flush=True)

        joined_rooms = await self.az.intent.get_joined_rooms()
        logging.debug(f"Appservice rooms: {joined_rooms}")

        print(f"Bridge is in {len(joined_rooms)} rooms, initializing them...",
              flush=True)

        Room.init_class(self.az)

        # room types and their init order, network must be before chat and group
        room_types = [
            ControlRoom, NetworkRoom, PrivateRoom, ChannelRoom, PlumbedRoom,
            SpaceRoom
        ]

        room_type_map = {}
        for room_type in room_types:
            room_type_map[room_type.__name__] = room_type

        # import all rooms
        for room_id in joined_rooms:
            joined = {}

            try:
                config = await self.az.intent.get_account_data("irc", room_id)

                if "type" not in config or "user_id" not in config:
                    raise Exception("Invalid config")

                cls = room_type_map.get(config["type"])
                if not cls:
                    raise Exception("Unknown room type")

                # refresh room members state
                await self.az.intent.get_room_members(room_id)

                joined = await self.az.state_store.get_member_profiles(
                    room_id, (Membership.JOIN, ))
                banned = await self.az.state_store.get_members(
                    room_id, (Membership.BAN, ))

                room = cls(id=room_id,
                           user_id=config["user_id"],
                           serv=self,
                           members=joined.keys(),
                           bans=banned)
                room.from_config(config)

                # add to room displayname
                for user_id, member in joined.items():
                    if member.displayname is not None:
                        room.displaynames[user_id] = member.displayname
                    # add to global puppet cache if it's a puppet
                    if user_id.startswith("@" + self.puppet_prefix
                                          ) and self.is_local(user_id):
                        self._users[user_id] = member.displayname

                # only add valid rooms to event handler
                if room.is_valid():
                    self._rooms[room_id] = room
                else:
                    room.cleanup()
                    raise Exception("Room validation failed after init")
            except Exception:
                logging.exception(
                    f"Failed to reconfigure room {room_id} during init, leaving."
                )

                # regardless of same mode, we ignore this room
                self.unregister_room(room_id)

                if safe_mode:
                    print("Safe mode enabled, not leaving room.", flush=True)
                else:
                    await self.leave_room(room_id, joined.keys())

        print("All valid rooms initialized, connecting network rooms...",
              flush=True)

        wait = 1
        for room in list(self._rooms.values()):
            await room.post_init()

            # check again if we're still valid
            if not room.is_valid():
                logging.debug(
                    f"Room {room.id} failed validation after post init, leaving."
                )

                self.unregister_room(room.id)

                if not safe_mode:
                    await self.leave_room(room.id, room.members)

                continue

            # connect network rooms one by one, this may take a while
            if type(room) == NetworkRoom and room.connected:

                def sync_connect(room):
                    asyncio.ensure_future(room.connect())

                asyncio.get_event_loop().call_later(wait, sync_connect, room)
                wait += 1

        print(
            f"Init done with {wait-1} networks connecting, bridge is now running!",
            flush=True)

        await asyncio.Event().wait()
Example #9
0
    async def run(self, listen_address, listen_port, homeserver_url, owner,
                  safe_mode):

        if "sender_localpart" not in self.registration:
            print("Missing sender_localpart from registration file.")
            sys.exit(1)

        if "namespaces" not in self.registration or "users" not in self.registration[
                "namespaces"]:
            print("User namespaces missing from registration file.")
            sys.exit(1)

        # remove self namespace if exists
        self_ns = f"@{self.registration['sender_localpart']}:.*"
        ns_users = [
            x for x in self.registration["namespaces"]["users"]
            if x["regex"] != self_ns
        ]

        if len(ns_users) != 1:
            print(
                "A single user namespace is required for puppets in the registration file."
            )
            sys.exit(1)

        if "exclusive" not in ns_users[0] or not ns_users[0]["exclusive"]:
            print("User namespace must be exclusive.")
            sys.exit(1)

        m = re.match(r"^@(.+)([\_/])\.\*$", ns_users[0]["regex"])
        if not m:
            print(
                "User namespace regex must be an exact prefix like '@irc_.*' that includes the separator character (_ or /)."
            )
            sys.exit(1)

        self.puppet_separator = m.group(2)
        self.puppet_prefix = m.group(1) + self.puppet_separator

        print(f"Heisenbridge v{__version__}", flush=True)
        if safe_mode:
            print("Safe mode is enabled.", flush=True)

        self.api = HTTPAPI(base_url=homeserver_url,
                           token=self.registration["as_token"])

        # conduit requires that the appservice user is registered before whoami
        wait = 0
        while True:
            try:
                await self.api.request(
                    Method.POST,
                    Path.v3.register,
                    {
                        "type": "m.login.application_service",
                        "username": self.registration["sender_localpart"],
                    },
                )
                logging.debug("Appservice user registration succeeded.")
                break
            except MUserInUse:
                logging.debug("Appservice user is already registered.")
                break
            except MatrixConnectionError as e:
                if wait < 30:
                    wait += 5
                logging.warning(
                    f"Failed to connect to HS: {e}, retrying in {wait} seconds..."
                )
                await asyncio.sleep(wait)
            except Exception:
                logging.exception(
                    "Unexpected failure when registering appservice user.")
                sys.exit(1)

        # mautrix migration requires us to call whoami manually at this point
        whoami = await self.api.request(Method.GET, Path.v3.account.whoami)

        logging.info("We are " + whoami["user_id"])

        self.user_id = whoami["user_id"]
        self.server_name = self.user_id.split(":", 1)[1]

        self.az = MauService(
            id=self.registration["id"],
            domain=self.server_name,
            server=homeserver_url,
            as_token=self.registration["as_token"],
            hs_token=self.registration["hs_token"],
            bot_localpart=self.registration["sender_localpart"],
            state_store=MemoryBridgeStateStore(),
        )
        self.az.matrix_event_handler(self._on_mx_event)

        try:
            await self.az.start(host=listen_address, port=listen_port)
        except Exception:
            logging.exception("Failed to listen.")
            sys.exit(1)

        try:
            await self.az.intent.ensure_registered()
            logging.debug("Appservice user exists at least now.")
        except Exception:
            logging.exception(
                "Unexpected failure when registering appservice user.")
            sys.exit(1)

        self._rooms = {}
        self._users = {}
        self.config = {
            "networks": {},
            "owner": None,
            "allow": {},
            "idents": {},
            "member_sync": "half",
            "max_lines": 0,
            "use_pastebin": True,
            "media_url": None,
            "namespace": self.puppet_prefix,
        }
        logging.debug(f"Default config: {self.config}")
        self.synapse_admin = False

        try:
            is_admin = await self.api.request(
                Method.GET, SynapseAdminPath.v1.users[self.user_id].admin)
            self.synapse_admin = is_admin["admin"]
        except MForbidden:
            logging.info(
                f"We ({self.user_id}) are not a server admin, inviting puppets is required."
            )
        except Exception:
            logging.info(
                "Seems we are not connected to Synapse, inviting puppets is required."
            )

        # load config from HS
        await self.load()

        # use configured media_url for endpoint if we have it
        if self.config["media_url"]:
            self.endpoint = self.config["media_url"]
        else:
            self.endpoint = await self.detect_public_endpoint()

        print("Homeserver is publicly available at " + self.endpoint,
              flush=True)

        logging.info("Starting presence loop")
        self._keepalive()

        # do a little migration for servers, remove this later
        for network in self.config["networks"].values():
            new_servers = []

            for server in network["servers"]:
                if isinstance(server, str):
                    new_servers.append({
                        "address": server,
                        "port": 6667,
                        "tls": False
                    })

            if len(new_servers) > 0:
                logging.debug(
                    "Migrating servers from old to new config format")
                network["servers"] = new_servers

        logging.debug(f"Merged configuration from HS: {self.config}")

        # prevent starting bridge with changed namespace
        if self.config["namespace"] != self.puppet_prefix:
            logging.error(
                f"Previously used namespace '{self.config['namespace']}' does not match current '{self.puppet_prefix}'."
            )
            sys.exit(1)

        # honor command line owner
        if owner is not None and self.config["owner"] != owner:
            logging.info(f"Overriding loaded owner with '{owner}'")
            self.config["owner"] = owner

        # always ensure our merged and migrated configuration is up-to-date
        await self.save()

        print("Fetching joined rooms...", flush=True)

        joined_rooms = await self.az.intent.get_joined_rooms()
        logging.debug(f"Appservice rooms: {joined_rooms}")

        print(f"Bridge is in {len(joined_rooms)} rooms, initializing them...",
              flush=True)

        Room.init_class(self.az)

        # room types and their init order, network must be before chat and group
        room_types = [
            ControlRoom, NetworkRoom, PrivateRoom, ChannelRoom, PlumbedRoom,
            SpaceRoom
        ]

        room_type_map = {}
        for room_type in room_types:
            room_type_map[room_type.__name__] = room_type

        # import all rooms
        for room_id in joined_rooms:
            joined = {}

            try:
                config = await self.az.intent.get_account_data("irc", room_id)

                if "type" not in config or "user_id" not in config:
                    raise Exception("Invalid config")

                cls = room_type_map.get(config["type"])
                if not cls:
                    raise Exception("Unknown room type")

                # refresh room members state
                await self.az.intent.get_room_members(room_id)

                joined = await self.az.state_store.get_member_profiles(
                    room_id, (Membership.JOIN, ))
                banned = await self.az.state_store.get_members(
                    room_id, (Membership.BAN, ))

                room = cls(id=room_id,
                           user_id=config["user_id"],
                           serv=self,
                           members=joined.keys(),
                           bans=banned)
                room.from_config(config)

                # add to room displayname
                for user_id, member in joined.items():
                    if member.displayname is not None:
                        room.displaynames[user_id] = member.displayname
                    # add to global puppet cache if it's a puppet
                    if user_id.startswith("@" + self.puppet_prefix
                                          ) and self.is_local(user_id):
                        self._users[user_id] = member.displayname

                # only add valid rooms to event handler
                if room.is_valid():
                    self._rooms[room_id] = room
                else:
                    room.cleanup()
                    raise Exception("Room validation failed after init")
            except Exception:
                logging.exception(
                    f"Failed to reconfigure room {room_id} during init, leaving."
                )

                # regardless of same mode, we ignore this room
                self.unregister_room(room_id)

                if safe_mode:
                    print("Safe mode enabled, not leaving room.", flush=True)
                else:
                    await self.leave_room(room_id, joined.keys())

        print("All valid rooms initialized, connecting network rooms...",
              flush=True)

        wait = 1
        for room in list(self._rooms.values()):
            await room.post_init()

            # check again if we're still valid
            if not room.is_valid():
                logging.debug(
                    f"Room {room.id} failed validation after post init, leaving."
                )

                self.unregister_room(room.id)

                if not safe_mode:
                    await self.leave_room(room.id, room.members)

                continue

            # connect network rooms one by one, this may take a while
            if type(room) == NetworkRoom and room.connected:

                def sync_connect(room):
                    asyncio.ensure_future(room.connect())

                asyncio.get_event_loop().call_later(wait, sync_connect, room)
                wait += 1

        print(
            f"Init done with {wait-1} networks connecting, bridge is now running!",
            flush=True)

        await asyncio.Event().wait()
Example #10
0
async def sappservice(config_filename, loop):
    config = Config(config_filename, "", "")
    config.load()

    logging.config.dictConfig(copy.deepcopy(config["logging"]))

    log: logging.Logger = logging.getLogger("sappservice")

    log.info("Initializing matrix spring lobby appservice")
    log.info(f"Config file: {config_filename}")

    # def exception_hook(etype, value, trace):
    #     log.debug(traceback.format_exception(etype, value, trace))
    #
    # sys.excepthook = exception_hook

    ################
    #
    # Initialization
    #
    ################

    mebibyte = 1024**2

    server = config["homeserver.address"]
    domain = config["homeserver.domain"]
    verify_ssl = config["homeserver.verify_ssl"]

    as_token = config["appservice.as_token"]
    hs_token = config["appservice.hs_token"]

    bot_localpart = config["appservice.bot_username"]
    max_body_size = config["appservice.max_body_size"]

    hostname = config["appservice.hostname"]
    port = config["appservice.port"]
    client_name = config["spring.client_name"]
    rooms = config["bridge.rooms"]

    upgrade_table = UpgradeTable()

    db = PostgresDatabase(config["appservice.database"], upgrade_table)
    await db.start()

    state_store_db = PgASStateStore(db=db)
    await state_store_db.upgrade_table.upgrade(db.pool)

    appserv = AppService(
        server=server,
        domain=domain,
        verify_ssl=verify_ssl,
        as_token=as_token,
        hs_token=hs_token,
        bot_localpart=bot_localpart,
        loop=loop,
        id='appservice',
        state_store=state_store_db,
        aiohttp_params={"client_max_size": max_body_size * mebibyte})

    spring_lobby_client = SpringLobbyClient(appserv, config, loop=loop)

    await db.start()
    await appserv.start(hostname, port)
    await spring_lobby_client.start()

    ################
    #
    # Lobby events
    #
    ################

    @spring_lobby_client.bot.on("tasserver")
    async def on_lobby_tasserver(message):
        log.debug(f"on_lobby_tasserver {message}")
        if message.client.name == client_name:
            message.client._login()

    @spring_lobby_client.bot.on("clients")
    async def on_lobby_clients(message):
        log.debug(f"on_lobby_clients {message}")
        if message.client.name != client_name:
            channel = message.params[0]
            clients = message.params[1:]
            await spring_lobby_client.join_matrix_room(channel, clients)

    @spring_lobby_client.bot.on("joined")
    async def on_lobby_joined(message, user, channel):
        log.debug(f"LOBBY JOINED user: {user.username} room: {channel}")
        if user.username != "appservice":
            await spring_lobby_client.join_matrix_room(channel,
                                                       [user.username])

    @spring_lobby_client.bot.on("left")
    async def on_lobby_left(message, user, channel):
        log.debug(f"LOBBY LEFT user: {user.username} room: {channel}")

        if channel.startswith("__battle__"):
            return

        if user.username == "appservice":
            return

        await spring_lobby_client.leave_matrix_room(channel, [user.username])

    @spring_lobby_client.bot.on("said")
    async def on_lobby_said(message, user, target, text):
        if message.client.name == client_name:
            await spring_lobby_client.said(user, target, text)

    @spring_lobby_client.bot.on("saidex")
    async def on_lobby_saidex(message, user, target, text):
        if message.client.name == client_name:
            await spring_lobby_client.saidex(user, target, text)

    # @spring_lobby_client.bot.on("denied")
    # async def on_lobby_denied(message):
    #     return
    #     # if message.client.name != client_name:
    #     #    user = message.client.name
    #     #    await spring_appservice.register(user)

    # @spring_lobby_client.bot.on("adduser")
    # async def on_lobby_adduser(message):
    #     if message.client.name != client_name:
    #         username = message.params[0]
    #
    #         if username == "ChanServ":
    #             return
    #         if username == "appservice":
    #             return
    #
    #         await spring_lobby_client.login_matrix_account(username)

    # @spring_lobby_client.bot.on("removeuser")
    # async def on_lobby_removeuser(message):
    #     if message.client.name != client_name:
    #         username = message.params[0]
    #
    #         if username == "ChanServ":
    #             return
    #         if username == "appservice":
    #             return
    #
    #         await spring_lobby_client.logout_matrix_account(username)

    @spring_lobby_client.bot.on("accepted")
    async def on_lobby_accepted(message):
        log.debug(f"message Accepted {message}")
        await spring_lobby_client.config_rooms()
        await spring_lobby_client.sync_matrix_users()

    @spring_lobby_client.bot.on("failed")
    async def on_lobby_failed(message):
        log.debug(f"message FAILED {message}")

    matrix = Matrix(appserv, spring_lobby_client, config)

    appserv.matrix_event_handler(matrix.handle_event)

    await matrix.wait_for_connection()
    await matrix.init_as_bot()

    # appservice_account = await appserv.intent.whoami()
    # user = appserv.intent.user(appservice_account)

    await appserv.intent.set_presence(PresenceState.ONLINE)

    # location = config["homeserver"]["domain"].split(".")[0]
    # external_id = "MatrixAppService"
    # external_username = config["appservice"]["bot_username"].split("_")[1]

    # for room in rooms:
    #
    #     enabled = config["bridge.rooms"][room]["enabled"]
    #     room_id = config["bridge.rooms"][room]["room_id"]
    #     room_alias = f"{config['appservice.namespace']}_{room}"
    #
    #     if enabled is True:
    #         await user.ensure_joined(room_id=room_id)
    #         await appserv.intent.add_room_alias(room_id=RoomID(room_id), alias_localpart=room_alias, override=True)
    #     # else:
    #     #     # await appserv.intent.remove_room_alias(alias_localpart=room_alias)
    #     #     try:
    #     #         await user.leave_room(room_id=room_id)
    #     #     except Exception as e:
    #     #         log.debug(f"Failed to leave room, not previously joined: {e}")

    appserv.ready = True
    log.info("Initialization complete, running startup actions")

    for signame in ('SIGINT', 'SIGTERM'):
        loop.add_signal_handler(
            getattr(signal, signame),
            lambda: asyncio.ensure_future(spring_lobby_client.exit(signame)))