示例#1
0
    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
示例#2
0
    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
示例#3
0
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
示例#4
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!")
示例#5
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()