def __init__(self, mxid: UserID = "", device_id: DeviceID = "", api: HTTPAPI = None, loop: Optional[asyncio.AbstractEventLoop] = None, *args, **kwargs) -> None: """ Initialize a ClientAPI. You must either provide the Args: mxid: The Matrix ID of the user. This is used for things like setting profile metadata. Additionally, the homeserver domain is extracted from this string and used for setting aliases and such. This can be changed later using `set_mxid`. api: The :class:`HTTPAPI` instance to use. You can also pass the ``args`` and ``kwargs`` to create a HTTPAPI instance rather than creating the instance yourself.`` """ if mxid: self.mxid = mxid else: self._mxid = None self.localpart = None self.domain = None self.device_id = device_id if loop: kwargs["loop"] = loop self.api = api or HTTPAPI(*args, **kwargs) self.loop = self.api.loop self.log = self.api.log
def __init__( self, mxid: UserID = "", device_id: DeviceID = "", api: HTTPAPI | None = None, **kwargs ) -> None: """ Initialize a ClientAPI. You must either provide the ``api`` parameter with an existing :class:`mautrix.api.HTTPAPI` instance, or provide the ``base_url`` and other arguments for creating it as kwargs. Args: mxid: The Matrix ID of the user. This is used for things like setting profile metadata. Additionally, the homeserver domain is extracted from this string and used for setting aliases and such. This can be changed later using `set_mxid`. device_id: The device ID corresponding to the access token used. api: The :class:`mautrix.api.HTTPAPI` instance to use. You can also pass the ``kwargs`` to create a HTTPAPI instance rather than creating the instance yourself. kwargs: If ``api`` is not specified, then the arguments to pass when creating a HTTPAPI. """ if mxid: self.mxid = mxid else: self._mxid = None self.localpart = None self.domain = None self.fill_member_event_callback = None self.versions_cache = None self.device_id = device_id self.api = api or HTTPAPI(**kwargs) self.log = self.api.log
async def read_client_auth_request(request: web.Request) -> Tuple[Optional[AuthRequestInfo], Optional[web.Response]]: server_name = request.match_info.get("server", None) server = registration_secrets().get(server_name, None) if not server: return None, resp.server_not_found try: body = await request.json() except JSONDecodeError: return None, resp.body_not_json try: username = body["username"] password = body["password"] except KeyError: return None, resp.username_or_password_missing try: base_url = server["url"] secret = server["secret"] except KeyError: return None, resp.invalid_server api = HTTPAPI(base_url, "", loop=get_loop()) return (api, secret, username, password), None
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!")
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()