Ejemplo n.º 1
0
class MatrixInterface(MessengerInterface):
    display_name: str
    avatar_path: str

    identifier: str
    username: str

    matrix: AsyncClient
    bot: Bot

    log = logging.getLogger(__name__)

    public_url: str
    web_dir: str
    debug: bool

    def __init__(self,
                 bot: Bot,
                 home_server: str,
                 username: str,
                 access_token: str,
                 device_id: str,
                 store_filepath: str,
                 web_dir: str,
                 public_url: str,
                 display_name: str,
                 avatar_path: str,
                 debug: bool = False):

        if not os.path.exists(store_filepath):
            os.makedirs(store_filepath)

        self.debug = debug
        self.public_url = public_url
        self.web_dir = web_dir

        self.display_name = display_name
        self.avatar_path = avatar_path

        self.username = username
        self.identifier = f"@{username}:{home_server[home_server.find('//') + 2:]}"

        self.matrix = AsyncClient(home_server,
                                  self.identifier,
                                  device_id,
                                  store_path=store_filepath,
                                  config=AsyncClientConfig(
                                      encryption_enabled=True,
                                      store=SqliteStore,
                                      store_name="matrix.db",
                                      store_sync_tokens=True))
        self.matrix.access_token = access_token
        self.matrix.user_id = self.identifier

        self.matrix.add_event_callback(self.handle_message, RoomMessageText)
        self.matrix.add_event_callback(self.crypto_event, MegolmEvent)
        self.matrix.add_event_callback(self.invite_event, InviteMemberEvent)
        self.matrix.add_event_callback(self.room_event, RoomMemberEvent)

        self.matrix.restore_login(self.identifier, device_id, access_token)
        self.bot = bot

        self.log.level = logging.DEBUG
        self.log.debug(f"Initialized Matrix Bot: {self.identifier}")

    async def crypto_event(self, room: MatrixRoom, event: MegolmEvent):
        self.log.error(f"Can't decrypt for {room.name}")
        if event.session_id not in self.matrix.outgoing_key_requests:
            self.log.warning(f"Fetching keys for {room.name}")
            resp = await self.matrix.request_room_key(event)
            if isinstance(resp, RoomKeyRequestResponse):
                self.log.info(
                    f"Got Response for {resp.room_id}, start syncing")
                await self.matrix.sync(full_state=True)
                self.log.info("Finished sync")
            elif isinstance(resp, RoomKeyRequestError):
                self.log.error(f"Got Error for requesting room key: {resp}")

    async def invite_event(self, room: MatrixRoom, event: InviteMemberEvent):
        if not event.membership == "invite" or event.state_key != self.matrix.user_id:
            return

        self.log.debug(f"Invite Event for {room.name}")

        resp = await self.matrix.join(room.room_id)
        if isinstance(resp, JoinError):
            self.log.error(
                f"Can't Join {room.room_id} ({room.encrypted}): {JoinError.message}"
            )
            return

        await self.matrix.sync()
        self.log.debug(f"Joined room {room.name}")

        await self.send_response(room.room_id,
                                 self.bot.handle_input('Start', room.room_id))

        if room.member_count > 2:
            await self.send_response(room.room_id, [
                BotResponse(
                    "Noch ein Hinweis: Da wir hier nicht zu zweit sind reagiere ich nur auf mentions!"
                )
            ])

    async def room_event(self, room: MatrixRoom, event: RoomMemberEvent):
        self.log.debug(f"Got RoomEvent: {event}")
        if event.membership == "leave" and event.state_key != self.matrix.user_id:
            if room.member_count == 1:
                resp = await self.matrix.room_leave(room.room_id)
                self.log.debug(f"Left room: {resp}")
                if isinstance(resp, RoomLeaveResponse):
                    self.bot.delete_user(room.room_id)
        elif event.membership == "leave" and event.state_key == self.matrix.user_id:
            self.log.info(
                f"Got kicked from {room.name}: {event.content['reason']}")

    @prometheus_async.aio.time(BOT_RESPONSE_TIME)
    async def handle_message(self, room: MatrixRoom, event: RoomMessageText):
        if self.identifier == event.sender:
            self.log.debug("Skipped message from myself")
            return

        # We need a mention in group rooms to handle messages
        if event.body.startswith(self.display_name):
            event.body = event.body[len(self.display_name) + 1:].strip()
        else:
            if room.member_count > 2:
                self.log.debug(
                    f"Skipped message in a group without mention: {event.body}"
                )
                return

        RECV_MESSAGE_COUNT.inc()
        self.log.debug(f"Received from {room.room_id}: {event}")
        await self.send_response(
            room.room_id, self.bot.handle_input(event.body, room.room_id))

    async def send_response(self, room_id: str, responses: List[BotResponse]):
        # Check if device is verified
        # if self.matrix.room_contains_unverified(room.room_id):
        #    devices = self.matrix.room_devices(room.room_id)
        #    for user in devices:
        #        for device in devices[user]:
        #            self.matrix.verify_device(devices[user][device])
        #            self.log.debug(f"Verified {device} of {user}")

        if self.debug:
            return

        for message in responses:
            if message.images:
                for image in message.images:
                    # Calculate metadata
                    mime_type = "image/jpeg"
                    file_stat = os.stat(image)

                    im = Image.open(image)
                    (width, height) = im.size

                    url = await self.upload_file(image, mime_type)

                    image = {
                        "body": os.path.basename(image),
                        "msgtype": "m.image",
                        "url": url,
                        "info": {
                            "size": file_stat.st_size,
                            "mimetype": mime_type,
                            "w": width,  # width in pixel
                            "h": height,  # height in pixel
                        },
                    }

                    resp = await self.matrix.room_send(
                        room_id=room_id,
                        message_type="m.room.message",
                        content=image,
                        ignore_unverified_devices=True)
                    if isinstance(resp, ErrorResponse):
                        self.log.error(f"Could not send image: {resp}")
                    else:
                        SENT_IMAGES_COUNT.inc()

            resp = await self.matrix.room_send(room_id=room_id,
                                               message_type="m.room.message",
                                               content={
                                                   "msgtype":
                                                   "m.text",
                                                   "body":
                                                   adapt_text(str(message),
                                                              just_strip=True),
                                                   "format":
                                                   "org.matrix.custom.html",
                                                   "formatted_body":
                                                   str(message).replace(
                                                       "\n", "<br />")
                                               },
                                               ignore_unverified_devices=True)
            if isinstance(resp, ErrorResponse):
                self.log.error(f"Could not send message: {resp}")
                FAILED_MESSAGE_COUNT.inc()
            else:
                SENT_MESSAGE_COUNT.inc()

    async def upload_file(self, path: str, mime_type: str) -> Optional[str]:
        file_stat = os.stat(path)

        async with aiofiles.open(path, "r+b") as f:
            resp, maybe_keys = await self.matrix.upload(
                f,
                content_type=mime_type,
                filename=os.path.basename(path),
                filesize=file_stat.st_size)

        if not isinstance(resp, UploadResponse):
            self.log.error(f"Failed to upload file. Failure response: {resp}")
            return None

        return resp.content_uri

    async def async_run(self) -> None:
        # Needed to update all room members etc.
        self.log.debug("Start first full sync")
        await self.matrix.sync(full_state=True)
        self.log.debug("Finished first sync")

        self.log.debug("Check profile for completeness")
        profile = await self.matrix.get_profile(self.identifier)
        if profile.displayname != self.display_name:
            resp = await self.matrix.set_displayname(self.display_name)
            if isinstance(resp, ProfileSetDisplayNameError):
                self.log.error(f"Cant set display name: {resp}")
            else:
                self.log.debug(f"Set display name to {self.display_name}")

        if profile.avatar_url is None:
            url = await self.upload_file(self.avatar_path, "image/png")
            if url is not None:
                resp = await self.matrix.set_avatar(url)
                if isinstance(resp, ProfileSetAvatarError):
                    self.log.error(f"Can't set avatar: {resp}")
                else:
                    self.log.debug(f"Set avatar to {url}")
        await self.matrix.sync_forever(timeout=300)

    def run(self):
        asyncio.get_event_loop().run_until_complete(self.async_run())

    async def send_unconfirmed_reports(self) -> None:
        unconfirmed_reports = self.bot.get_available_user_messages()

        if unconfirmed_reports:
            await self.matrix.sync(full_state=True)

        for report, userid, message in unconfirmed_reports:
            if not userid in self.matrix.rooms:
                self.log.error(f"Room {userid} does not exist")
                self.bot.disable_user(userid)
                continue

            try:
                await self.send_response(userid, message)
            except LocalProtocolError as e:
                self.log.warning(
                    f"Error while sending report to {userid}: {e}")
            else:
                self.bot.confirm_message_send(report, userid)
                self.log.warning(f"Sent report to {userid}")

        await self.matrix.close()

    async def send_message_to_users(self, message: str,
                                    users: List[Union[str, int]]):
        for user in users:
            await self.send_response(user, [BotResponse(message)])
Ejemplo n.º 2
0
async def login() -> AsyncClient:
    """Handle login with or without stored credentials."""
    # Configuration options for the AsyncClient
    client_config = AsyncClientConfig(
        max_limit_exceeded=0,
        max_timeouts=0,
        store_sync_tokens=True,
        encryption_enabled=True,
    )

    # If there are no previously-saved credentials, we'll use the password
    if not os.path.exists(CONFIG_FILE):
        print("First time use. Did not find credential file. Asking for "
              "homeserver, user, and password to create credential file.")
        homeserver = "https://matrix.example.org"
        homeserver = input(f"Enter your homeserver URL: [{homeserver}] ")

        if not (homeserver.startswith("https://")
                or homeserver.startswith("http://")):
            homeserver = "https://" + homeserver

        user_id = "@user:example.org"
        user_id = input(f"Enter your full user ID: [{user_id}] ")

        device_name = "matrix-nio"
        device_name = input(f"Choose a name for this device: [{device_name}] ")

        if not os.path.exists(STORE_PATH):
            os.makedirs(STORE_PATH)

        # Initialize the matrix client
        client = AsyncClient(
            homeserver,
            user_id,
            store_path=STORE_PATH,
            config=client_config,
        )
        pw = getpass.getpass()

        resp = await client.login(password=pw, device_name=device_name)

        # check that we logged in succesfully
        if (isinstance(resp, LoginResponse)):
            write_details_to_disk(resp, homeserver)
        else:
            print(f"homeserver = \"{homeserver}\"; user = \"{user_id}\"")
            print(f"Failed to log in: {resp}")
            sys.exit(1)

        print("Logged in using a password. Credentials were stored. "
              "On next execution the stored login credentials will be used.")

    # Otherwise the config file exists, so we'll use the stored credentials
    else:
        # open the file in read-only mode
        with open(CONFIG_FILE, "r") as f:
            config = json.load(f)
            # Initialize the matrix client based on credentials from file
            client = AsyncClient(
                config['homeserver'],
                config['user_id'],
                device_id=config['device_id'],
                store_path=STORE_PATH,
                config=client_config,
            )

            client.restore_login(user_id=config['user_id'],
                                 device_id=config['device_id'],
                                 access_token=config['access_token'])
        print("Logged in using stored credentials.")

    return client
Ejemplo n.º 3
0
async def main():  # noqa
    """Create bot as Matrix client and enter event loop."""
    # Read config file
    # A different config file path can be specified
    # as the first command line argument
    if len(sys.argv) > 1:
        config_filepath = sys.argv[1]
    else:
        config_filepath = "config.yaml"
    config = Config(config_filepath)

    # Configure the database
    store = Storage(config.database_filepath)

    # Configuration options for the AsyncClient
    client_config = AsyncClientConfig(
        max_limit_exceeded=0,
        max_timeouts=0,
        store_sync_tokens=True,
        encryption_enabled=True,
    )

    # Initialize the matrix client
    client = AsyncClient(
        config.homeserver_url,
        config.user_id,
        device_id=config.device_id,
        store_path=config.store_filepath,
        config=client_config,
    )

    # Set up event callbacks
    callbacks = Callbacks(client, store, config)
    client.add_event_callback(callbacks.message, (RoomMessageText, ))
    client.add_event_callback(callbacks.invite, (InviteMemberEvent, ))
    client.add_to_device_callback(callbacks.accept_all_verify,
                                  (KeyVerificationEvent, ))

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        try:
            # Try to login with the configured username/password
            try:
                if config.access_token:
                    logger.debug("Using access token from config file to log "
                                 f"in. access_token={config.access_token}")

                    client.restore_login(user_id=config.user_id,
                                         device_id=config.device_id,
                                         access_token=config.access_token)
                else:
                    logger.debug("Using password from config file to log in.")
                    login_response = await client.login(
                        password=config.user_password,
                        device_name=config.device_name,
                    )

                    # Check if login failed
                    if type(login_response) == LoginError:
                        logger.error("Failed to login: "******"{login_response.message}")
                        return False
                    logger.info((f"access_token of device {config.device_name}"
                                 f" is: \"{login_response.access_token}\""))
            except LocalProtocolError as e:
                # There's an edge case here where the user hasn't installed
                # the correct C dependencies. In that case, a
                # LocalProtocolError is raised on login.
                logger.fatal(
                    "Failed to login. "
                    "Have you installed the correct dependencies? "
                    "Error: %s", e)
                return False

            # Login succeeded!
            logger.debug(f"Logged in successfully as user {config.user_id} "
                         f"with device {config.device_id}.")
            # Sync encryption keys with the server
            # Required for participating in encrypted rooms
            if client.should_upload_keys:
                await client.keys_upload()

            if config.change_device_name:
                content = {"display_name": config.device_name}
                resp = await client.update_device(config.device_id, content)
                if isinstance(resp, UpdateDeviceError):
                    logger.debug(f"update_device failed with {resp}")
                else:
                    logger.debug(f"update_device successful with {resp}")

            await client.sync(timeout=30000, full_state=True)
            for device_id, olm_device in client.device_store[
                    config.user_id].items():
                logger.info("Setting up trust for my own "
                            f"device {device_id} and session key "
                            f"{olm_device.keys}.")
                client.verify_device(olm_device)

            await client.sync_forever(timeout=30000, full_state=True)

        except (ClientConnectionError, ServerDisconnectedError):
            logger.warning(
                "Unable to connect to homeserver, retrying in 15s...")

            # Sleep so we don't bombard the server with login requests
            sleep(15)
        finally:
            # Make sure to close the client connection on disconnect
            await client.close()
class E2EEClient:
    def __init__(self, join_rooms: set):
        self.STORE_PATH = os.environ['LOGIN_STORE_PATH']
        self.CONFIG_FILE = f"{self.STORE_PATH}/credentials.json"

        self.join_rooms = join_rooms
        self.client: AsyncClient = None
        self.client_config = AsyncClientConfig(
            max_limit_exceeded=0,
            max_timeouts=0,
            store_sync_tokens=True,
            encryption_enabled=True,
        )

        self.greeting_sent = False

    def _write_details_to_disk(self, resp: LoginResponse, homeserver) -> None:
        with open(self.CONFIG_FILE, "w") as f:
            json.dump(
                {
                    'homeserver': homeserver,  # e.g. "https://matrix.example.org"
                    'user_id': resp.user_id,  # e.g. "@user:example.org"
                    'device_id': resp.device_id,  # device ID, 10 uppercase letters
                    'access_token': resp.access_token  # cryptogr. access token
                },
                f
            )

    async def _login_first_time(self) -> None:
        homeserver = os.environ['MATRIX_SERVER']
        user_id = os.environ['MATRIX_USERID']
        pw = os.environ['MATRIX_PASSWORD']
        device_name = os.environ['MATRIX_DEVICE']

        if not os.path.exists(self.STORE_PATH):
            os.makedirs(self.STORE_PATH)

        self.client = AsyncClient(
            homeserver,
            user_id,
            store_path=self.STORE_PATH,
            config=self.client_config,
            ssl=(os.environ['MATRIX_SSLVERIFY'] == 'True'),
        )

        resp = await self.client.login(password=pw, device_name=device_name)

        if (isinstance(resp, LoginResponse)):
            self._write_details_to_disk(resp, homeserver)
        else:
            logging.info(
                f"homeserver = \"{homeserver}\"; user = \"{user_id}\"")
            logging.critical(f"Failed to log in: {resp}")
            sys.exit(1)

    async def _login_with_stored_config(self) -> None:
        if self.client:
            return

        with open(self.CONFIG_FILE, "r") as f:
            config = json.load(f)

            self.client = AsyncClient(
                config['homeserver'],
                config['user_id'],
                device_id=config['device_id'],
                store_path=self.STORE_PATH,
                config=self.client_config,
                ssl=bool(os.environ['MATRIX_SSLVERIFY']),
            )

            self.client.restore_login(
                user_id=config['user_id'],
                device_id=config['device_id'],
                access_token=config['access_token']
            )

    async def login(self) -> None:
        if os.path.exists(self.CONFIG_FILE):
            logging.info('Logging in using stored credentials.')
        else:
            logging.info('First time use, did not find credential file.')
            await self._login_first_time()
            logging.info(
                f"Logged in, credentials are stored under '{self.STORE_PATH}'.")

        await self._login_with_stored_config()

    async def _message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None:
        logging.info(colored(
            f"@{room.user_name(event.sender)} in {room.display_name} | {event.body}",
            'green'
        ))

    async def _sync_callback(self, response: SyncResponse) -> None:
        logging.info(f"We synced, token: {response.next_batch}")

        if not self.greeting_sent:
            self.greeting_sent = True

            greeting = f"Hi, I'm up and runnig from **{os.environ['MATRIX_DEVICE']}**, waiting for webhooks!"
            await self.send_message(greeting, os.environ['MATRIX_ADMIN_ROOM'], 'Webhook server')

    async def send_message(
        self,
        message: str,
        room: str,
        sender: str,
        sync: Optional[bool] = False
    ) -> None:
        if sync:
            await self.client.sync(timeout=3000, full_state=True)

        msg_prefix = ""
        if os.environ['DISPLAY_APP_NAME'] == 'True':
            msg_prefix = f"**{sender}** says:  \n"

        content = {
            'msgtype': 'm.text',
            'body': f"{msg_prefix}{message}",
        }
        if os.environ['USE_MARKDOWN'] == 'True':
            # Markdown formatting removes YAML newlines if not padded with spaces,
            # and can also mess up posted data like system logs
            logging.debug('Markdown formatting is turned on.')

            content['format'] = 'org.matrix.custom.html'
            content['formatted_body'] = markdown(
                f"{msg_prefix}{message}", extensions=['extra'])

        await self.client.room_send(
            room_id=room,
            message_type="m.room.message",
            content=content,
            ignore_unverified_devices=True
        )

    async def run(self) -> None:
        await self.login()

        self.client.add_event_callback(self._message_callback, RoomMessageText)
        self.client.add_response_callback(self._sync_callback, SyncResponse)

        if self.client.should_upload_keys:
            await self.client.keys_upload()

        for room in self.join_rooms:
            await self.client.join(room)
        await self.client.joined_rooms()

        logging.info('The Matrix client is waiting for events.')

        await self.client.sync_forever(timeout=300000, full_state=True)
Ejemplo n.º 5
0
class MatrixBot:
    def __init__(self):
        self.client = None

    async def on_message(self, room: MatrixRoom,
                         event: RoomMessageText) -> None:
        if event.sender == self.client.user_id:
            return
        print(
            f"{strftime('%Y-%m-%d %H:%M:%S', localtime(event.server_timestamp/1000))}: {room.room_id}({room.display_name})|<{event.sender}({room.user_name(event.sender)})> {event.body}"
        )
        if time() - event.server_timestamp / 1000 > config.max_timediff:
            print(" ` skip old message")
            return
        msg = event.body
        msgtr = common.process_message(
            msg, config.default_tabs, config.min_levenshtein_ratio,
            "[TEST MODE] " if config.test_mode else False)
        if msgtr:
            content = {
                "msgtype": "m.text",
                "body": msgtr,
            }
            m_relates_to = event.source["content"].get("m.relates_to", None)
            if m_relates_to and m_relates_to.get("rel_type",
                                                 None) == "io.element.thread":
                content["m.relates_to"] = {
                    "rel_type": "io.element.thread",
                    "event_id": m_relates_to.get("event_id", None),
                }
            await self.client.room_send(
                room_id=room.room_id,
                message_type="m.room.message",
                content=content,
                ignore_unverified_devices=True,
            )

    async def on_invite(self, room: MatrixRoom,
                        event: InviteMemberEvent) -> None:
        print(
            f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}: <<< Invited to {room.room_id} by {event.sender} >>>"
        )
        await self.client.join(room.room_id)

    async def run(self) -> None:
        client_config = AsyncClientConfig(store_sync_tokens=True,
                                          encryption_enabled=True)
        self.client = AsyncClient(config.matrix_homeserver,
                                  store_path="./store",
                                  config=client_config)

        self.client.restore_login(
            user_id=config.matrix_user_id,
            device_id=config.matrix_device_id,
            access_token=config.matrix_access_token,
        )

        if self.client.should_upload_keys:
            await self.client.keys_upload()
        if self.client.should_query_keys:
            await self.client.keys_query()
        if self.client.should_claim_keys:
            await self.client.keys_claim()
        await self.client.sync(full_state=True)

        self.client.add_event_callback(self.on_message, RoomMessageText)
        self.client.add_event_callback(self.on_invite, InviteMemberEvent)

        await self.client.sync_forever(timeout=30000)
        await self.client.close()