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_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 })
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 })
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)
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!")
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)
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")
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()
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()
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)))