class Bot:
    def __init__(self):
        self.client = AsyncClient(settings.MATRIX_SERVER,
                                  settings.MATRIX_USERNAME)
        self.command_handlers = {
            "cbstart": self.start,
            "cbstats": self.cbstats,
        }
        self.client.add_event_callback(self.message_handler, RoomMessageText)
        self.client.add_event_callback(self.invite_handler, InviteEvent)

    async def run(self):
        await self.client.login(settings.MATRIX_PASSWORD)
        await self.client.sync_forever(timeout=30000)

    async def invite_handler(self, room: MatrixRoom, event: InviteEvent):
        await self.client.join(room.room_id)

    async def message_handler(self, room, event: RoomMessageText):
        # Set up command handlers
        args = event.body.split(" ")
        if len(args) > 0 and args[0].startswith("!"):
            command = args[0][1:]
            if command in self.command_handlers:
                await self.command_handlers[command](room, event)

    async def send_message(self, room, message):
        await self.client.room_send(
            room_id=room.room_id,
            message_type="m.room.message",
            content={
                "msgtype": "m.text",
                "format": "org.matrix.custom.html",
                "body": message,
                "formatted_body": message,
            },
        )

    async def start(self, room, _):
        await self.send_message(room, "Running")

    async def cbstats(self, room, event):
        args = event.body.split(" ")[1:]
        message = ""
        country_info = None
        try:
            if len(args) == 0:
                stats, updated = data.get_global_cases()
            else:
                country = " ".join(args)
                stats, country_info, updated = data.get_country_cases(country)
            last_updated = datetime.fromtimestamp(int(updated) / 1000)
            message = formatting.format_stats(stats, country_info,
                                              last_updated)
        except JSONDecodeError:
            if country is not None:
                message = f"{country} doesn't exist lmao"
            else:
                message = "Error: Could not look up stats"
        await self.send_message(room, message)
Exemple #2
0
async def main():
    # Read config file
    config = Config("config.yaml")

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

    # Initialize the matrix client
    nio_client = AsyncClient(
        config.homeserver_url,
        config.user_id,
        device_id=config.device_id,
        config=nio_client_config,
        store_path="/home/brendan/Documents/matrix-monzo-next/nio_store",
    )

    # Initialise the monzo client
    monzo_client = Monzo(config.monzo_access_token)

    await nio_client.login(config.password, "monzo_bot")

    # Set up event callbacks
    callbacks = Callbacks(config, nio_client, monzo_client)
    nio_client.add_event_callback(callbacks.message, (RoomMessageText, ))
    nio_client.add_event_callback(callbacks.invite, (InviteEvent, ))

    # First do a sync with full_state = true to retrieve the state of the room.
    await nio_client.sync(full_state=True)

    await nio_client.sync_forever(30000)
Exemple #3
0
class MatrixClient:
    def __init__(self, bot_funcs):
        self.bot_funcs = bot_funcs
        self.client = AsyncClient(URI, USERNAME)
        self.last_sent = 0
        print("init finished")

    async def message_callback(self, room: MatrixRoom, event: RoomMessageText):
        for f in self.bot_funcs:
            if event.body.startswith(f["trigger"]):
                ts = time.time()
                if ts - self.last_sent < 2:
                    continue
                self.last_sent = time.time()
                res = f["ret_func"](event.body, event.sender)
                if res is None:
                    continue
                await self.client.room_send(room_id=ROOM,
                                            message_type="m.room.message",
                                            content={
                                                "msgtype": "m.text",
                                                "body": res
                                            })

    async def exec_client(self):
        self.client.add_event_callback(self.message_callback, RoomMessageText)
        print(await self.client.login(PW))
        await self.client.sync_forever(timeout=30000)  # milliseconds
Exemple #4
0
async def init(homeserver: str, username: str, password: str,
               keyfile: Optional[str],
               keyphrase: Optional[str]) -> AsyncClient:
    config = AsyncClientConfig(store=SqliteMemoryStore, store_sync_tokens=True)
    client = AsyncClient(homeserver, username, config=config)
    response = await client.login(password)
    client.add_event_callback(event_cb, RoomMessage)
    await client.import_keys(keyfile, keyphrase)
    return client
Exemple #5
0
async def main():
    urbitClient.connect()

    matrixClient = AsyncClient(matrixHomeServer, matrixBotUser)
    print(await matrixClient.login(matrixBotPass))
    matrixClient.add_event_callback(matrixTextListener, RoomMessageText)
    matrixClient.add_event_callback(matrixMediaListener, RoomMessageMedia)

    await matrixClient.sync_forever(timeout=30000)
Exemple #6
0
async def main() -> None:
    client = AsyncClient("https://matrix.ether.ai", os.environ['BOT_USER'])
    client.add_event_callback(message_callback, RoomMessageText)

    print(await client.login(os.environ['BOT_PASSWORD']))
    # "Logged in as @alice:example.org device id: RANDOMDID"
    # If you made a new room and haven't joined as that user, you can use
    # await client.join("your-room-id")
    file_path = 'logs/myLinks.log'

    # Referenced from stack overflow https://stackoverflow.com/a/24818607
    last_line = None
    with open(file_path, 'r') as f:
        while True:
            line = f.readline()
            if not line:
                break
            last_line = line

    while True:
        with open(file_path, 'r') as f:
            lines = f.readlines()
        if lines[-1] != last_line:
            last_line = lines[-1]
            await client.room_send(
                # Watch out! If you join an old room you'll see lots of old messages
                room_id="!nXDYFQhwioFnAwUHiB:ether.ai",
                message_type="m.room.message",
                content={
                    "msgtype": "m.text",
                    "body": lines[-1]
                }
            )
        time.sleep(0.00000000000001)

    # while True:
    #     line = file1.readline()
    #
    #     if not line:
    #         await client.sync_forever(timeout=1000)  # milliseconds
    #
    #     else:
    #         await client.room_send(
    #             # Watch out! If you join an old room you'll see lots of old messages
    #             room_id="!nXDYFQhwioFnAwUHiB:ether.ai",
    #             message_type="m.room.message",
    #             content={
    #                 "msgtype": "m.text",
    #                 "body": line
    #             }
    #         )
    await client.sync_forever(timeout=30000)  # milliseconds
Exemple #7
0
async def main():
    # Read config file
    config = Config("config.yaml")

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

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

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

    logger.debug("Connected to Matrix!")

    # Assign an access token to the bot instead of logging in and creating a new device
    client.access_token = config.access_token

    # Set up event callbacks
    callbacks = Callbacks(client, store, config)
    client.add_event_callback(callbacks.message, (RoomMessageText, ))
    client.add_event_callback(callbacks.invite, (InviteEvent, ))

    # Retrieve the last sync token if it exists
    token = store.get_sync_token()

    # Sync loop
    while True:
        # Sync with the server
        sync_response = await client.sync(timeout=30000,
                                          full_state=True,
                                          since=token)

        # Check if the sync had an error
        if type(sync_response) == SyncError:
            logger.warning("Error in client sync: %s", sync_response.message)
            continue

        # Save the latest sync token
        token = sync_response.next_batch
        if token:
            store.save_sync_token(token)
Exemple #8
0
async def main() -> None:
    bot_info = load_bot_info()

    mongo_client = MongoClient(
        "mongodb+srv://{}:{}@{}/weekling?retryWrites=true&w=majority".format(
            bot_info["db_username"], bot_info["db_password"],
            bot_info["db_hostname"]))
    db = mongo_client.weekling

    # NOTE Hemppa-hack
    jointime = datetime.datetime.now()  # HACKHACKHACK to avoid running old
    # commands after join
    join_hack_time = 5  # Seconds
    """
    Create the client-object with correct info, set callbacks for reacting 
    to events and login. If an access token is not found in config-file,
    ask for password.
    """
    client = AsyncClient(bot_info["homeserver"])

    client.add_event_callback(pass_to_invite_callback(client),
                              InviteMemberEvent)

    client.add_event_callback(
        pass_to_message_callback(client, db, jointime, join_hack_time),
        RoomMessageText)

    # Ask password from command line, press enter to use stored access token
    access_token = bot_info["access_token"]
    user_id = bot_info["user_id"]
    if len(access_token) != 0 and len(user_id) != 0:
        client.access_token = access_token
        # Manually set user id because not calling client.login()
        client.user_id = user_id
    else:
        password = getpass.getpass()
        response = await client.login(password)
        # Save info to file for future use
        bot_info["access_token"] = response.access_token
        try:
            with io.open(LOGIN_FILE, "w", encoding="utf-8") as fp:
                fp.write(json.dumps(bot_info))
        except OSError as e:
            print(f"Writing login-info failed: {e}")

    print(f"Logged in as {client}")

    await client.sync_forever(timeout=30000, full_state=False)  # milliseconds
Exemple #9
0
async def main() -> None:
    client = AsyncClient("https://matrix.example.org", "@alice:example.org")
    client.add_event_callback(message_callback, RoomMessageText)

    print(await client.login("my-secret-password"))
    # "Logged in as @alice:example.org device id: RANDOMDID"

    # If you made a new room and haven't joined as that user, you can use
    # await client.join("your-room-id")

    await client.room_send(
        # Watch out! If you join an old room you'll see lots of old messages
        room_id="!my-fave-room:example.org",
        message_type="m.room.message",
        content={
            "msgtype": "m.text",
            "body": "Hello world!"
        })
    await client.sync_forever(timeout=30000)  # milliseconds
Exemple #10
0
async def main(sugaroid: Sugaroid) -> None:
    config = Config.from_environment()
    client = AsyncClient(config['homeserver'])
    client.access_token = config['access_token']
    client.user_id = config['user_id']
    client.device_id = config['device_id']

    print("Status: sleeping for 30000")
    await client.sync(30000)
    print("Resuming:")
    cb = Callbacks(client, sugaroid=sugaroid)
    client.add_event_callback(cb.message, RoomMessageText)
    while True:
        try:
            await client.sync_forever(timeout=30000, full_state=True)
        except KeyboardInterrupt:
            break
        except (ClientConnectionError, ServerDisconnectedError):
            print("Unable to connect to homeserver, retrying in 15s")
            time.sleep(15)
        finally:
            await client.close()
Exemple #11
0
async def main():
    global g_client

    print("Connecting to server: {}".format(botconfig.client_url))
    g_client = AsyncClient(botconfig.client_url,
                           f"@{botconfig.username}:cclub.cs.wmich.edu")

    # log in
    password = get_password()
    print(await g_client.login(password))

    # figure out what time it is
    await room_send_text(botconfig.ROOM_ID_BOTTOY,
                         tk.get_session_startup_string())

    # register event callbacks
    g_client.add_event_callback(on_message, RoomMessageText)
    g_client.add_event_callback(on_image, RoomMessageImage)
    g_client.add_event_callback(on_video, RoomMessageVideo)
    g_client.add_event_callback(on_membership, RoomMemberEvent)

    # loop forever
    await g_client.sync_forever(timeout=120000)  # two minutes
Exemple #12
0
async def main():
    global client
    access_token = os.getenv('MATRIX_ACCESS_TOKEN')
    join_on_invite = os.getenv('JOIN_ON_INVITE')

    client = AsyncClient(os.environ['MATRIX_SERVER'],
                         os.environ['MATRIX_USER'])
    if access_token:
        client.access_token = access_token
    else:
        await client.login(os.environ['MATRIX_PASSWORD'])
        print("Access token:", client.access_token)
    await client.sync()
    if client.logged_in:
        client.add_event_callback(message_cb, RoomMessageText)
        client.add_event_callback(unknown_cb, RoomMessageUnknown)
        if join_on_invite:
            print('Note: Bot will join rooms if invited')
            client.add_event_callback(invite_cb, (InviteEvent, ))
        print('Bot running')
        await client.sync_forever(timeout=30000)
    else:
        print('Client was not able to log in, check env variables!')
Exemple #13
0
class Bot:

    def __init__(self):
        self.appid = 'org.vranki.hemppa'
        self.version = '1.2'
        self.client = None
        self.join_on_invite = False
        self.modules = dict()
        self.pollcount = 0
        self.poll_task = None
        self.owners = []
        self.debug = os.getenv("DEBUG")

        self.initializeLogger()
        self.logger = logging.getLogger("hemppa")
        self.logger.debug("Initialized")

    def initializeLogger(self):

        if os.path.exists('config/logging.config'):
            logging.config.fileConfig('config/logging.config')
        else:
            log_format = '%(levelname)s - %(name)s - %(message)s'
            logging.basicConfig(format=log_format)

        if self.debug:
            logging.root.setLevel(logging.DEBUG)
        else:
            logging.root.setLevel(logging.INFO)

    async def send_text(self, room, body):
        msg = {
            "body": body,
            "msgtype": "m.text"
        }
        await self.client.room_send(room.room_id, 'm.room.message', msg)

    async def send_html(self, room, html, plaintext):
        msg = {
            "msgtype": "m.text",
            "format": "org.matrix.custom.html",
            "formatted_body": html,
            "body": plaintext
        }
        await self.client.room_send(room.room_id, 'm.room.message', msg)

    def remove_callback(self, callback):
        for cb_object in bot.client.event_callbacks:
            if cb_object.func == callback:
                self.logger.info("remove callback")
                bot.client.event_callbacks.remove(cb_object)

    def get_room_by_id(self, room_id):
        return self.client.rooms[room_id]

    # Throws exception if event sender is not a room admin
    def must_be_admin(self, room, event):
        if not self.is_admin(room, event):
            raise CommandRequiresAdmin

    # Throws exception if event sender is not a bot owner
    def must_be_owner(self, event):
        if not self.is_owner(event):
            raise CommandRequiresOwner

    # Returns true if event's sender is admin in the room event was sent in,
    # or is bot owner
    def is_admin(self, room, event):
        if self.is_owner(event):
            return True
        if event.sender not in room.power_levels.users:
            return False
        return room.power_levels.users[event.sender] >= 50

    # Returns true if event's sender is owner of the bot
    def is_owner(self, event):
        return event.sender in self.owners

    def save_settings(self):
        module_settings = dict()
        for modulename, moduleobject in self.modules.items():
            try:
                module_settings[modulename] = moduleobject.get_settings()
            except Exception:
                traceback.print_exc(file=sys.stderr)
        data = {self.appid: self.version, 'module_settings': module_settings}
        self.set_account_data(data)

    def load_settings(self, data):
        if not data:
            return
        if not data.get('module_settings'):
            return
        for modulename, moduleobject in self.modules.items():
            if data['module_settings'].get(modulename):
                try:
                    moduleobject.set_settings(
                        data['module_settings'][modulename])
                except Exception:
                    traceback.print_exc(file=sys.stderr)

    async def message_cb(self, room, event):
        # Figure out the command
        body = event.body
        if not self.starts_with_command(body):
            return

        command = body.split().pop(0)

        # Strip away non-alphanumeric characters, including leading ! for security
        command = re.sub(r'\W+', '', command)

        moduleobject = self.modules.get(command)

        if moduleobject is not None:
            if moduleobject.enabled:
                try:
                    await moduleobject.matrix_message(bot, room, event)
                except CommandRequiresAdmin:
                    await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.')
                except CommandRequiresOwner:
                    await self.send_text(room, f'Sorry, only bot owner can run that command.')
                except Exception:
                    await self.send_text(room,
                                         f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
                    traceback.print_exc(file=sys.stderr)
        else:
            self.logger.error(f"Unknown command: {command}")

            # TODO Make this configurable
            # await self.send_text(room,
            #                     f"Sorry. I don't know what to do. Execute !help to get a list of available commands.")

    @staticmethod
    def starts_with_command(body):
        """Checks if body starts with ! and has one or more letters after it"""
        return re.match(r"^!\w.*", body) is not None

    async def invite_cb(self, room, event):
        room: MatrixRoom
        event: InviteEvent

        if self.join_on_invite or self.is_owner(event):
            for attempt in range(3):
                result = await self.client.join(room.room_id)
                if type(result) == JoinError:
                    self.logger.error(f"Error joining room %s (attempt %d): %s", room.room_id, attempt, result.message)
                else:
                    self.logger.info(f"joining room '{room.display_name}'({room.room_id}) invited by '{event.sender}'")
                    break
        else:
            self.logger.warning(f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}')

    def load_module(self, modulename):
        try:
            self.logger.info(f'load module: {modulename}')
            module = importlib.import_module('modules.' + modulename)
            module = reload(module)
            cls = getattr(module, 'MatrixModule')
            return cls(modulename)
        except ModuleNotFoundError:
            self.logger.error(f'Module {modulename} failed to load!')
            traceback.print_exc(file=sys.stderr)
            return None

    def reload_modules(self):
        for modulename in bot.modules:
            self.logger.info(f'Reloading {modulename} ..')
            self.modules[modulename] = self.load_module(modulename)

        self.load_settings(self.get_account_data())

    def get_modules(self):
        modulefiles = glob.glob('./modules/*.py')

        for modulefile in modulefiles:
            modulename = os.path.splitext(os.path.basename(modulefile))[0]
            moduleobject = self.load_module(modulename)
            if moduleobject:
                self.modules[modulename] = moduleobject

    def clear_modules(self):
        self.modules = dict()

    async def poll_timer(self):
        while True:
            self.pollcount = self.pollcount + 1
            for modulename, moduleobject in self.modules.items():
                if moduleobject.enabled:
                    try:
                        await moduleobject.matrix_poll(bot, self.pollcount)
                    except Exception:
                        traceback.print_exc(file=sys.stderr)
            await asyncio.sleep(10)

    def set_account_data(self, data):
        userid = urllib.parse.quote(self.matrix_user)

        ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"

        response = requests.put(ad_url, json.dumps(data))
        self.__handle_error_response(response)

        if response.status_code != 200:
            self.logger.error('Setting account data failed. response: %s json: %s', response, response.json())

    def get_account_data(self):
        userid = urllib.parse.quote(self.matrix_user)

        ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"

        response = requests.get(ad_url)
        self.__handle_error_response(response)

        if response.status_code == 200:
            return response.json()
        self.logger.error(f'Getting account data failed: {response} {response.json()} - this is normal if you have not saved any settings yet.')
        return None

    def __handle_error_response(self, response):
        if response.status_code == 401:
            self.logger.error("access token is invalid or missing")
            self.logger.info("NOTE: check MATRIX_ACCESS_TOKEN or set MATRIX_PASSWORD")
            sys.exit(2)

    def init(self):

        self.matrix_user = os.getenv('MATRIX_USER')
        self.matrix_pass = os.getenv('MATRIX_PASSWORD')
        matrix_server = os.getenv('MATRIX_SERVER')
        bot_owners = os.getenv('BOT_OWNERS')
        access_token = os.getenv('MATRIX_ACCESS_TOKEN')
        join_on_invite = os.getenv('JOIN_ON_INVITE')

        if matrix_server and self.matrix_user and bot_owners:
            self.client = AsyncClient(matrix_server, self.matrix_user)
            self.client.access_token = access_token

            if self.client.access_token is None:
                if self.matrix_pass is None:
                    self.logger.error("Either MATRIX_ACCESS_TOKEN or MATRIX_PASSWORD need to be set")
                    sys.exit(1)

            self.join_on_invite = join_on_invite is not None
            self.owners = bot_owners.split(',')
            self.get_modules()

        else:
            self.logger.error("The environment variables MATRIX_SERVER, MATRIX_USER and BOT_OWNERS are mandatory")
            sys.exit(1)


    def start(self):
        self.load_settings(self.get_account_data())
        enabled_modules = [module for module_name, module in self.modules.items() if module.enabled]
        self.logger.info(f'Starting {len(enabled_modules)} modules..')
        for modulename, moduleobject in self.modules.items():
            if moduleobject.enabled:
                try:
                    moduleobject.matrix_start(bot)
                except Exception:
                    traceback.print_exc(file=sys.stderr)

    def stop(self):
        self.logger.info(f'Stopping {len(self.modules)} modules..')
        for modulename, moduleobject in self.modules.items():
            try:
                moduleobject.matrix_stop(bot)
            except Exception:
                traceback.print_exc(file=sys.stderr)

    async def run(self):
        if not self.client.access_token:
            login_response = await self.client.login(self.matrix_pass)

            if isinstance(login_response, LoginError):
                self.logger.error(f"Failed to login: {login_response.message}")
                return

            last_16 = self.client.access_token[-16:]
            self.logger.info(f"Logged in with password, access token: ...{last_16}")

        await self.client.sync()
        for roomid, room in self.client.rooms.items():
            self.logger.info(f"Bot is on '{room.display_name}'({roomid}) with {len(room.users)} users")
            if len(room.users) == 1:
                self.logger.info(f'Room {roomid} has no other users - leaving it.')
                self.logger.info(await self.client.room_leave(roomid))

        self.start()

        self.poll_task = asyncio.get_event_loop().create_task(self.poll_timer())

        if self.client.logged_in:
            self.load_settings(self.get_account_data())
            self.client.add_event_callback(self.message_cb, RoomMessageText)
            self.client.add_event_callback(self.invite_cb, (InviteEvent,))

            if self.join_on_invite:
                self.logger.info('Note: Bot will join rooms if invited')
            self.logger.info('Bot running as %s, owners %s', self.client.user, self.owners)
            self.bot_task = asyncio.create_task(self.client.sync_forever(timeout=30000))
            await self.bot_task
        else:
            self.logger.error('Client was not able to log in, check env variables!')

    async def shutdown(self):

        if self.client.logged_in:
            logout = await self.client.logout()

            if isinstance(logout, LogoutResponse):
                self.logger.info("Logout successful")
                try:
                    await self.client.close()
                    self.logger.info("Connection closed")
                except Exception as e:
                    self.logger.error("error while closing client: %s", e)

            else:
                logout: LogoutError
                self.logger.error(f"Logout unsuccessful. msg: {logout.message}")
        else:
            await self.client.client_session.close()
async def main():
    # Read config file
    # A different config file path can be specified as the first command line arg
    if len(sys.argv) > 1:
        config_filepath = sys.argv[1]
    else:
        config_filepath = "config.yaml"
    CONFIG.read_config(config_filepath)

    # Configure the python job scheduler
    SCHEDULER.configure({"apscheduler.timezone": CONFIG.timezone})

    # 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_path,
        config=client_config,
    )

    # Configure the database
    store = Storage(client)

    # Set up event callbacks
    callbacks = Callbacks(client, store)
    client.add_event_callback(callbacks.message, (RoomMessageText,))
    client.add_event_callback(callbacks.invite, (InviteMemberEvent,))
    client.add_event_callback(callbacks.decryption_failure, (MegolmEvent,))

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        try:
            # Try to login with the configured username/password
            try:
                login_response = await client.login(
                    password=CONFIG.user_password, device_name=CONFIG.device_name,
                )

                # Check if login failed. Usually incorrect password
                if type(login_response) == LoginError:
                    logger.error("Failed to login: %s", login_response.message)
                    logger.warning("Trying again in 15s...")

                    # Sleep so we don't bombard the server with login requests
                    sleep(15)
                    continue
            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? "
                    "https://github.com/poljar/matrix-nio#installation "
                    "Error: %s",
                    e,
                )
                return False

            # Login succeeded!

            logger.info(f"Logged in as {CONFIG.user_id}")
            logger.info("Startup complete")

            # Allow jobs to fire
            try:
                SCHEDULER.start()
            except SchedulerAlreadyRunningError:
                pass

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

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

            # Sleep so we don't bombard the server with login requests
            sleep(15)
        except Exception:
            logger.exception("Unknown exception occurred:")
            logger.warning("Restarting 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()
Exemple #15
0
class MatrixBot:
    def __init__(self, config):
        self.config = config
        self.modules = []
        if self.config['main'].get('debug', '').lower() == 'true':
            self.debug = True
        else:
            self.debug = False

        for key, value in modules.__dict__.items():
            if inspect.isclass(value) and issubclass(value, MatrixBotModule):
                print("loading {}...".format(value))
                m = value.create(config)
                if m:
                    self.modules.append(m)

    async def send_room_text(self, room, content):
        await self.client.room_send(room_id=room.room_id,
                                    message_type="m.room.message",
                                    content={
                                        "body": content,
                                        "msgtype": "m.text"
                                    })

    async def send_room_html(self, room, content):
        await self.client.room_send(room_id=room.room_id,
                                    message_type="m.room.message",
                                    content={
                                        "body": re.sub('<[^<]+?>', '',
                                                       content),
                                        "msgtype": "m.text",
                                        "format": "org.matrix.custom.html",
                                        "formatted_body": content,
                                    })

    async def send_room_content(self, room, msgtype, url, name, extra):
        await self.client.room_send(room_id=room.room_id,
                                    message_type="m.room.message",
                                    content={
                                        "body": name,
                                        "msgtype": msgtype,
                                        "url": url,
                                        "info": extra,
                                    })

    async def send_room_image(self, room, url, name, extra):
        await self.send_room_content(room=room,
                                     msgtype="m.image",
                                     url=url,
                                     name=name,
                                     extra=extra)

    async def send_room_file(self, room, url, name, extra):
        await self.send_room_content(room=room,
                                     msgtype="m.file",
                                     url=url,
                                     name=name,
                                     extra=extra)

    async def on_room_message(self, room, event):
        if event.sender == self.client.user:
            return

        for module in self.modules:
            try:
                await module.handle_room_message(self, room, event)
            except Exception as e:
                if self.debug:
                    msg = E.PRE(traceback.format_exc())
                    html_data = lxml.html.tostring(msg).decode('utf-8')
                    await self.send_room_html(room=room, content=html_data)
                else:
                    await self.send_room_text(room=room,
                                              content="There was an error.")

    async def on_invite(self, room, event):
        self.client.join(room.room_id)

    async def run(self):
        client_config = ClientConfig(store_sync_tokens=True)
        self.client = AsyncClient(homeserver=self.config["main"]["base_url"],
                                  user=self.config["main"]["user_id"],
                                  device_id=self.config["main"]["device_id"],
                                  store_path=self.config["main"]["store_path"],
                                  config=client_config)
        self.client.add_event_callback(self.on_invite, InviteEvent)
        self.client.add_event_callback(self.on_room_message, RoomMessageText)

        print("Logging in...")
        status = await self.client.login(
            self.config["main"]["password"],
            device_name=self.config["main"]["device_name"])
        if not self.client.logged_in:
            print("Error logging in.")
            return
        print(f"Logged in as user {self.client.user_id}")

        await self.client.sync_forever(timeout=30000, full_state=True)
Exemple #16
0
async def main():
    """The first function that is run when starting the bot"""

    # Read user-configured options from a config file.
    # A different config file path can be specified as the first command line argument
    if len(sys.argv) > 1:
        config_path = sys.argv[1]
    else:
        config_path = "config.yaml"

    # Read the parsed config file and create a Config object
    config = Config(config_path)

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

    # 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_path,
        config=client_config,
    )

    if config.user_token:
        client.access_token = config.user_token
        client.user_id = config.user_id

    # Set up event callbacks
    callbacks = Callbacks(client, store, config)
    client.add_event_callback(callbacks.invite, (InviteMemberEvent, ))
    client.add_event_callback(callbacks.decryption_failure, (MegolmEvent, ))
    client.add_response_callback(callbacks.sync, (SyncResponse, ))
    client.add_event_callback(callbacks.unknown, (UnknownEvent, ))

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        try:
            if config.user_token:
                # Use token to log in
                client.load_store()

                # Sync encryption keys with the server
                if client.should_upload_keys:
                    await client.keys_upload()
            else:
                # Try to login with the configured username/password
                try:
                    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: %s",
                                     login_response.message)
                        return False
                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? "
                        "https://github.com/poljar/matrix-nio#installation "
                        "Error: %s",
                        e,
                    )
                    return False

                # Login succeeded!

            logger.info(f"Logged in as {config.user_id}")
            # join the pins room we'll be writing to
            result = client.join(config.pins_room)
            if type(result) == JoinError:
                raise Exception(
                    f"Error joining pins room {config.pins_room}", )

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

        except Exception as e:
            logger.error("%s", e)
            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
            try:
                await client.close()
            except Exception as e2:
                logger.error("Also got exception while closing, %s", e2)
Exemple #17
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()
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)
Exemple #19
0
async def main():
    """Entry point."""
    # Read config file
    config = Config("config.yaml")

    # 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=config.enable_encryption,
    )

    # 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,
    )

    # Assign an access token to the bot instead of logging in and creating a new device
    client.access_token = config.access_token

    # 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_event_callback(callbacks.joined, (RoomMemberEvent,))

    # Create a new sync token, attempting to load one from the database if it has one already
    sync_token = SyncToken(store)

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        # try:
        # Try to login with the configured username/password
        # try:
        #     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(f"Failed to login: %s", login_response.message)
        #         return False
        # except LocalProtocolError as e:
        #     # There's an edge case here where the user enables encryption
        #     # but hasn't installed the correct C dependencies. In that case,
        #     # a LocalProtocolError is raised on login.
        #     # Warn the user if these conditions are met.
        #     if config.enable_encryption:
        #         logger.fatal(
        #             "Failed to login and encryption is enabled. "
        #             "Have you installed the correct dependencies? "
        #             "https://github.com/poljar/matrix-nio#installation"
        #         )
        #         return False
        #     else:
        #         # We don't know why this was raised. Throw it at the user
        #         logger.fatal("Error logging in: %s", e)
        #         return False

        # Login succeeded!

        # ===============================
        # Sync encryption keys with the server
        # Required for participating in encrypted rooms
        # if client.should_upload_keys:
        #     await client.keys_upload()

        # logger.info(f"Logged in as {config.user_id}")
        # await client.sync_forever(timeout=30000, full_state=True)
        # ===============================

        # ===============================
        logger.debug("Syncing: %s", sync_token.token)
        sync_response = await client.sync(timeout=30000, since=sync_token.token)

        # Check if the sync had an errors
        if type(sync_response) == SyncError:
            logger.warning("Error in client sync: %s", sync_response.message)
            continue

        # Save the latest sync token to the database
        token = sync_response.next_batch
        if token:
            sync_token.update(token)
Exemple #20
0
class Session:
    client: AsyncClient = None
    config: SessionConfig = None
    plugins: Dict[str, BasePlugin] = None
    messenger: Messenger = None
    loggers: List[Logger] = []

    def __init__(self, config: SessionConfig):
        self.config = config
        self.client = AsyncClient(config.homeserver, config.matrix_id)
        try:
            with open(config.next_batch_file, "r") as next_batch_token:
                self.client.next_batch = next_batch_token.read()
        except FileNotFoundError:
            # No existing next_batch file; no worries.
            self.client.next_batch = 0

        # Update next_batch every sync
        self.client.add_response_callback(self.__sync_cb, SyncResponse)
        # Handle text messages
        self.client.add_event_callback(self.__message_cb, RoomMessageText)
        # Handle invites
        self.client.add_event_callback(self.__autojoin_room_cb, InviteEvent)

        self.client.add_ephemeral_callback(self.sample, ReceiptEvent)

        self.load_plugins()
        self.messenger = Messenger(self.client)

    async def sample(self, room: MatrixRoom, event: ReceiptEvent) -> None:
        CORE_LOG.info(room.read_receipts)

    async def start(self) -> None:
        """Start the session.

        Logs in as the user provided in the config and begins listening for
        events to respond to. For security, it will logout any other
        sessions.
        """

        login_status = await self.client.login(
            password=self.config.password, device_name="remote-bot"
        )
        if isinstance(login_status, LoginError):
            print(f"Failed to login: {login_status}", file=sys.stderr)
            await self.stop()
        else:
            # Remove previously registered devices; ignore this device
            maybe_devices = await self.client.devices()
            if isinstance(maybe_devices, DevicesResponse):
                await self.client.delete_devices(
                    list(
                        map(
                            lambda x: x.id,
                            filter(
                                lambda x: x.id != self.client.device_id,
                                maybe_devices.devices,
                            ),
                        )
                    ),
                    auth={
                        "type": "m.login.password",
                        "user": self.config.matrix_id,
                        "password": self.config.password,
                    },
                )

            CORE_LOG.info(login_status)

        # Force a full state sync to load Room info
        await self.client.sync(full_state=True)
        await self.client.sync_forever(timeout=30000)

    async def stop(self) -> None:
        """Politely closes the session and ends the process.
        
        If logged in, logs out.
        """
        print("Shutting down...")
        if self.client.logged_in:
            await self.client.logout()
        await self.client.close()
        sys.exit(0)

    def load_plugins(self) -> None:
        """Dynamically loads all plugins from the plugins directory.

        New plugins can be added by creating new classes in the `plugins` module.
        """
        self.plugins = {}
        importlib.import_module("plugins")
        modules = []
        plugin_files = os.listdir(os.path.join(os.path.dirname(__file__), "plugins"))
        if len(plugin_files) == 0:
            print("NOTE: No plugin files found.")

        for plugin in plugin_files:
            if plugin.startswith("__") or not plugin.endswith(".py"):
                # Skip files like __init__.py and .gitignore
                continue

            module_name = "plugins." + plugin.rsplit(".")[0]
            modules.append(importlib.import_module(module_name, package="plugins"))

        for module in modules:
            if module.__name__ in sys.modules:
                importlib.reload(module)

            clsmembers = inspect.getmembers(
                module,
                lambda member: inspect.isclass(member)
                and member.__module__ == module.__name__,
            )

            for name, cls in clsmembers:
                if not issubclass(cls, BasePlugin):
                    # We only want plugins that derive from BasePlugin
                    CORE_LOG.warn(
                        f"Skipping {name} as it doesn't derive from the BasePlugin"
                    )
                    continue
                CORE_LOG.info(f"Loading plugin {name} ...")

                # Create logger for each plugin
                plugin_logger = Logger(f"olive.plugin.{name}")
                plugin_logger.info(f"{name}'s logger is working hard!")
                logger_group.add_logger(plugin_logger)

                # Generate standard config
                config = PluginConfig(plugin_logger)

                # Instantiate the plugin!
                self.plugins[name] = cls(config)

        CORE_LOG.info("Loaded plugins")

    async def __send(
        self, room: MatrixRoom, body: str = None, content: dict = None
    ) -> bool:
        # You must either include a body message or build your own content dict.
        assert body or content
        if not content:
            content = {"msgtype": "m.text", "body": body}
        try:
            send_status = await self.client.room_send(
                room.room_id, message_type="m.room.message", content=content
            )
        except SendRetryError as err:
            print(f"Failed to send message '{body}' to room '{room}'. Error:\n{err}")
            return False

        if isinstance(send_status, RoomSendError):
            print(send_status)
            return False

        return True

    async def __autojoin_room_cb(
        self, room: MatrixInvitedRoom, event: InviteEvent
    ) -> None:
        if room.room_id not in self.client.rooms:
            await self.client.join(room.room_id)
            await self.__send(room, f"Hello, {room.display_name}!")

            # TODO: Replace forced client sync. I'd like to avoid handling the
            # Invite event three times, but dont' want to force syncs. Probably
            # not common enought to matter?
            await self.client.sync(300)

    async def __message_cb(self, room: MatrixRoom, event: RoomMessageText):
        """Executes any time a MatrixRoom the bot is in receives a RoomMessageText.

        On each message, it tests each plugin to see if it is triggered by the
        event; if so, the method will run that plugins `process_event` method.
        """

        await self.client.room_read_markers(
            room.room_id, event.event_id, event.event_id
        )

        if event.sender == self.client.user_id:
            # Message is from us; we can ignore.
            return

        for name, plugin in self.plugins.items():
            try:
                await plugin.process_event(room, event, self.messenger)
            except Exception as err:
                print(
                    f"Plugin {name} encountered an error while "
                    + f"processing the event {event} in room {room.display_name}."
                    + f"\n{err}",
                    file=sys.stderr,
                )
                _, _, tb = sys.exc_info()
                traceback.print_tb(tb)

    async def __sync_cb(self, response: SyncResponse) -> None:
        with open(self.config.next_batch_file, "w") as next_batch_token:
            next_batch_token.write(response.next_batch)
Exemple #21
0
async def main():
    # Read config file

    # A different config file path can be specified as the first command line argument
    if len(sys.argv) > 1:
        config_path = sys.argv[1]
    else:
        config_path = "config.yaml"
    config = Config(config_path)

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

    # 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_path,
        config=client_config,
    )

    if config.user_token:
        client.access_token = config.user_token
        client.user_id = config.user_id

    # Set up event callbacks
    callbacks = Callbacks(client, store, config)
    # noinspection PyTypeChecker
    client.add_event_callback(callbacks.message, (RoomMessageText, ))
    # noinspection PyTypeChecker
    client.add_event_callback(callbacks.invite, (InviteMemberEvent, ))

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        try:
            if config.user_token:
                # Use token to log in
                client.load_store()

                # Sync encryption keys with the server
                if client.should_upload_keys:
                    await client.keys_upload()
            else:
                # Try to login with the configured username/password
                try:
                    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: %s",
                                     login_response.message)
                        break
                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? "
                        "https://github.com/poljar/matrix-nio#installation "
                        "Error: %s",
                        e,
                    )
                    break

                # Login succeeded!

            # Join the management room or fail
            response = await with_ratelimit(client, "join",
                                            config.management_room)
            if type(response) == JoinError:
                logger.fatal("Could not join the management room, aborting.")
                break
            else:
                logger.info(f"Management room membership is good")

            # Resolve management room ID if not known
            if config.management_room.startswith('#'):
                # Resolve the room ID
                response = await with_ratelimit(client, "room_resolve_alias",
                                                config.management_room)
                if type(response) == RoomResolveAliasResponse:
                    config.management_room_id = response.room_id
                else:
                    logger.fatal(
                        "Could not resolve the management room ID from alias, aborting"
                    )
                    break

            logger.info(f"Logged in as {config.user_id}")
            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()
Exemple #22
0
class Bot:
    def __init__(self):
        self.appid = 'org.vranki.hemppa'
        self.version = '1.4'
        self.client = None
        self.join_on_invite = False
        self.modules = dict()
        self.pollcount = 0
        self.poll_task = None
        self.owners = []
        self.debug = os.getenv("DEBUG", "false").lower() == "true"
        self.logger = None

        self.jointime = None  # HACKHACKHACK to avoid running old commands after join
        self.join_hack_time = 5  # Seconds

        self.initialize_logger()

    def initialize_logger(self):

        if os.path.exists('config/logging.yml'):
            with open('config/logging.yml') as f:
                config = yaml.load(f, Loader=yaml.Loader)
                logging.config.dictConfig(config)
        else:
            log_format = '%(levelname)s - %(name)s - %(message)s'
            logging.basicConfig(format=log_format)

        self.logger = logging.getLogger("hemppa")

        if self.debug:
            logging.root.setLevel(logging.DEBUG)
            self.logger.info("enabled debugging")

        self.logger.debug("Logger initialized")

    async def send_text(self,
                        room,
                        body,
                        msgtype="m.notice",
                        bot_ignore=False):
        msg = {
            "body": body,
            "msgtype": msgtype,
        }
        if bot_ignore:
            msg["org.vranki.hemppa.ignore"] = "true"

        await self.client.room_send(room.room_id, 'm.room.message', msg)

    async def send_html(self,
                        room,
                        html,
                        plaintext,
                        msgtype="m.notice",
                        bot_ignore=False):
        msg = {
            "msgtype": msgtype,
            "format": "org.matrix.custom.html",
            "formatted_body": html,
            "body": plaintext
        }
        if bot_ignore:
            msg["org.vranki.hemppa.ignore"] = "true"
        await self.client.room_send(room.room_id, 'm.room.message', msg)

    async def send_image(self, room, url, body):
        """

        :param room: A MatrixRoom the image should be send to
        :param url: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri
        :param body: A textual representation of the image
        :return:
        """
        msg = {"url": url, "body": body, "msgtype": "m.image"}
        await self.client.room_send(room.room_id, 'm.room.message', msg)

    async def send_msg(self, mxid, roomname, message):
        # Sends private message to user. Returns true on success.

        # Find if we already have a common room with user:
        msg_room = None
        for croomid in self.client.rooms:
            roomobj = self.client.rooms[croomid]
            if len(roomobj.users) == 2:
                for user in roomobj.users:
                    if user == mxid:
                        msg_room = roomobj

        # Nope, let's create one
        if not msg_room:
            msg_room = await self.client.room_create(
                visibility=RoomVisibility.private,
                name=roomname,
                is_direct=True,
                preset=RoomPreset.private_chat,
                invite={mxid},
            )

        if not msg_room or (type(msg_room) is RoomCreateError):
            self.logger.error(
                f'Unable to create room when trying to message {mxid}')
            return False

        # Send message to the room
        await self.send_text(msg_room, message)
        return True

    def remove_callback(self, callback):
        for cb_object in self.client.event_callbacks:
            if cb_object.func == callback:
                self.logger.info("remove callback")
                self.client.event_callbacks.remove(cb_object)

    def get_room_by_id(self, room_id):
        return self.client.rooms[room_id]

    # Throws exception if event sender is not a room admin
    def must_be_admin(self, room, event):
        if not self.is_admin(room, event):
            raise CommandRequiresAdmin

    # Throws exception if event sender is not a bot owner
    def must_be_owner(self, event):
        if not self.is_owner(event):
            raise CommandRequiresOwner

    # Returns true if event's sender is admin in the room event was sent in,
    # or is bot owner
    def is_admin(self, room, event):
        if self.is_owner(event):
            return True
        if event.sender not in room.power_levels.users:
            return False
        return room.power_levels.users[event.sender] >= 50

    # Returns true if event's sender is owner of the bot
    def is_owner(self, event):
        return event.sender in self.owners

    # Checks if this event should be ignored by bot, including custom property
    def should_ignore_event(self, event):
        return "org.vranki.hemppa.ignore" in event.source['content']

    def save_settings(self):
        module_settings = dict()
        for modulename, moduleobject in self.modules.items():
            try:
                module_settings[modulename] = moduleobject.get_settings()
            except Exception:
                traceback.print_exc(file=sys.stderr)
        data = {self.appid: self.version, 'module_settings': module_settings}
        self.set_account_data(data)

    def load_settings(self, data):
        if not data:
            return
        if not data.get('module_settings'):
            return
        for modulename, moduleobject in self.modules.items():
            if data['module_settings'].get(modulename):
                try:
                    moduleobject.set_settings(
                        data['module_settings'][modulename])
                except Exception:
                    traceback.print_exc(file=sys.stderr)

    async def message_cb(self, room, event):
        # Ignore if asked to ignore
        if self.should_ignore_event(event):
            print('Ignoring this!')
            return

        body = event.body
        # Figure out the command
        if not self.starts_with_command(body):
            return

        if self.owners_only and not self.is_owner(event):
            self.logger.info(
                f"Ignoring {event.sender}, because they're not an owner")
            await self.send_text(room,
                                 "Sorry, only bot owner can run commands.")
            return

        # HACK to ignore messages for some time after joining.
        if self.jointime:
            if (datetime.datetime.now() -
                    self.jointime).seconds < self.join_hack_time:
                self.logger.info(
                    f"Waiting for join delay, ignoring message: {body}")
                return
            self.jointime = None

        command = body.split().pop(0)

        # Strip away non-alphanumeric characters, including leading ! for security
        command = re.sub(r'\W+', '', command)

        moduleobject = self.modules.get(command)

        if moduleobject is not None:
            if moduleobject.enabled:
                try:
                    await moduleobject.matrix_message(self, room, event)
                except CommandRequiresAdmin:
                    await self.send_text(
                        room,
                        f'Sorry, you need admin power level in this room to run that command.'
                    )
                except CommandRequiresOwner:
                    await self.send_text(
                        room, f'Sorry, only bot owner can run that command.')
                except Exception:
                    await self.send_text(
                        room,
                        f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details'
                    )
                    traceback.print_exc(file=sys.stderr)
        else:
            self.logger.error(f"Unknown command: {command}")
            # TODO Make this configurable
            # await self.send_text(room,
            #                     f"Sorry. I don't know what to do. Execute !help to get a list of available commands.")

    @staticmethod
    def starts_with_command(body):
        """Checks if body starts with ! and has one or more letters after it"""
        return re.match(r"^!\w.*", body) is not None

    async def invite_cb(self, room, event):
        room: MatrixRoom
        event: InviteEvent

        if self.join_on_invite or self.is_owner(event):
            for attempt in range(3):
                self.jointime = datetime.datetime.now()
                result = await self.client.join(room.room_id)
                if type(result) == JoinError:
                    self.logger.error(
                        f"Error joining room %s (attempt %d): %s",
                        room.room_id, attempt, result.message)
                else:
                    self.logger.info(
                        f"joining room '{room.display_name}'({room.room_id}) invited by '{event.sender}'"
                    )
                    return
        else:
            self.logger.warning(
                f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}'
            )

    async def memberevent_cb(self, room, event):
        # Automatically leaves rooms where bot is alone.
        if room.member_count == 1 and event.membership == 'leave':
            self.logger.info(
                f"membership event in {room.display_name} ({room.room_id}) with {room.member_count} members by '{event.sender}' - leaving room as i don't want to be left alone!"
            )
            await self.client.room_leave(room.room_id)

    def load_module(self, modulename):
        try:
            self.logger.info(f'load module: {modulename}')
            module = importlib.import_module('modules.' + modulename)
            module = reload(module)
            cls = getattr(module, 'MatrixModule')
            return cls(modulename)
        except ModuleNotFoundError:
            self.logger.error(f'Module {modulename} failed to load!')
            traceback.print_exc(file=sys.stderr)
            return None

    def reload_modules(self):
        for modulename in self.modules:
            self.logger.info(f'Reloading {modulename} ..')
            self.modules[modulename] = self.load_module(modulename)

        self.load_settings(self.get_account_data())

    def get_modules(self):
        modulefiles = glob.glob('./modules/*.py')

        for modulefile in modulefiles:
            modulename = os.path.splitext(os.path.basename(modulefile))[0]
            moduleobject = self.load_module(modulename)
            if moduleobject:
                self.modules[modulename] = moduleobject

    def clear_modules(self):
        self.modules = dict()

    async def poll_timer(self):
        while True:
            self.pollcount = self.pollcount + 1
            for modulename, moduleobject in self.modules.items():
                if moduleobject.enabled:
                    try:
                        await moduleobject.matrix_poll(self, self.pollcount)
                    except Exception:
                        traceback.print_exc(file=sys.stderr)
            await asyncio.sleep(10)

    def set_account_data(self, data):
        userid = urllib.parse.quote(self.matrix_user)

        ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"

        response = requests.put(ad_url, json.dumps(data))
        self.__handle_error_response(response)

        if response.status_code != 200:
            self.logger.error(
                'Setting account data failed. response: %s json: %s', response,
                response.json())

    def get_account_data(self):
        userid = urllib.parse.quote(self.matrix_user)

        ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"

        response = requests.get(ad_url)
        self.__handle_error_response(response)

        if response.status_code == 200:
            return response.json()
        self.logger.error(
            f'Getting account data failed: {response} {response.json()} - this is normal if you have not saved any settings yet.'
        )
        return None

    def __handle_error_response(self, response):
        if response.status_code == 401:
            self.logger.error("access token is invalid or missing")
            self.logger.info("NOTE: check MATRIX_ACCESS_TOKEN")
            sys.exit(2)

    def init(self):

        self.matrix_user = os.getenv('MATRIX_USER')
        matrix_server = os.getenv('MATRIX_SERVER')
        bot_owners = os.getenv('BOT_OWNERS')
        access_token = os.getenv('MATRIX_ACCESS_TOKEN')
        join_on_invite = os.getenv('JOIN_ON_INVITE')
        owners_only = os.getenv('OWNERS_ONLY') is not None

        if matrix_server and self.matrix_user and bot_owners and access_token:
            self.client = AsyncClient(matrix_server, self.matrix_user)
            self.client.access_token = access_token
            self.join_on_invite = join_on_invite is not None
            self.owners = bot_owners.split(',')
            self.owners_only = owners_only
            self.get_modules()

        else:
            self.logger.error(
                "The environment variables MATRIX_SERVER, MATRIX_USER, MATRIX_ACCESS_TOKEN and BOT_OWNERS are mandatory"
            )
            sys.exit(1)

    def start(self):
        self.load_settings(self.get_account_data())
        enabled_modules = [
            module for module_name, module in self.modules.items()
            if module.enabled
        ]
        self.logger.info(f'Starting {len(enabled_modules)} modules..')
        for modulename, moduleobject in self.modules.items():
            if moduleobject.enabled:
                try:
                    moduleobject.matrix_start(self)
                except Exception:
                    traceback.print_exc(file=sys.stderr)

    def stop(self):
        self.logger.info(f'Stopping {len(self.modules)} modules..')
        for modulename, moduleobject in self.modules.items():
            try:
                moduleobject.matrix_stop(self)
            except Exception:
                traceback.print_exc(file=sys.stderr)

    async def run(self):
        await self.client.sync()
        for roomid, room in self.client.rooms.items():
            self.logger.info(
                f"Bot is on '{room.display_name}'({roomid}) with {len(room.users)} users"
            )
            if len(room.users) == 1:
                self.logger.info(
                    f'Room {roomid} has no other users - leaving it.')
                self.logger.info(await self.client.room_leave(roomid))

        self.start()

        self.poll_task = asyncio.get_event_loop().create_task(
            self.poll_timer())

        if self.client.logged_in:
            self.load_settings(self.get_account_data())
            self.client.add_event_callback(self.message_cb, RoomMessageText)
            self.client.add_event_callback(self.invite_cb, (InviteEvent, ))
            self.client.add_event_callback(self.memberevent_cb,
                                           (RoomMemberEvent, ))

            if self.join_on_invite:
                self.logger.info('Note: Bot will join rooms if invited')
            self.logger.info('Bot running as %s, owners %s', self.client.user,
                             self.owners)
            self.bot_task = asyncio.create_task(
                self.client.sync_forever(timeout=30000))
            await self.bot_task
        else:
            self.logger.error(
                'Client was not able to log in, check env variables!')

    async def shutdown(self):
        await self.close()

    async def close(self):
        try:
            await self.client.close()
            self.logger.info("Connection closed")
        except Exception as ex:
            self.logger.error("error while closing client: %s", ex)

    def handle_exit(self, signame, loop):
        self.logger.info(f"Received signal {signame}")
        if self.poll_task:
            self.poll_task.cancel()
        self.bot_task.cancel()
        self.stop()
Exemple #23
0
class Bot():
    def __init__(self, controller, creds):
        self.name = controller.MATRIXBZ_BOT_NAME
        self.CHANNEL_GREETING = controller.CHANNEL_GREETING
        self.user = creds['user']
        self.password = creds['password']
        self.client = AsyncClient(creds['homeserver'], creds['user'])
        self.client.add_event_callback(self.invite_cb, InviteMemberEvent)
        self.client.add_event_callback(self.message_cb, RoomMessageText)
        self._setup_handlers(controller)
        if hasattr(controller, 'AUTH'):
            self.AUTH = controller.AUTH(controller)
        else:
            self.AUTH = BlockAll(controller)
        if hasattr(controller, 'CACHE'):
            self.CACHE = controller.CACHE
        else:
            self.CACHE = NoCache()

    def _setup_handlers(self, controller):
        self.commands = {}
        self.msg_handler = None
        self.startup_method = None
        members = inspect.getmembers(controller, predicate=inspect.ismethod)
        for member in members:
            if hasattr(member[1], 'matrixbz_method'):
                # add member[1]
                command_str = f'!{self.name} {member[0]}'
                self.commands[command_str] = member[1]
            elif hasattr(member[1], 'matrixbz_msg_handler'):
                if self.msg_handler:
                    raise Exception('Can only mark one matrixbz_msg_handler!')
                self.msg_handler = member[1]
            elif hasattr(member[1], 'matrixbz_startup_method'):
                if self.startup_method:
                    raise Exception(
                        'Can only mark one matrixbz_startup_method!')
                self.startup_method = member[1]
        command_prefixes = '|'.join(list(self.commands.keys()))
        self.command_regex = re.compile(f'^({command_prefixes})( .+)?$')

    async def message_cb(self, room, event):
        if not self.AUTH.authenticate_message(room, event):
            return
        txt = event.body.strip()
        context = {'room': room, 'event': event, 'client': self.client}
        match = self.command_regex.match(txt)
        if match:
            await self._handle_command(match, context)
        elif self.msg_handler:
            try:
                await self.msg_handler(context)
            except:
                return

    async def _handle_command(self, match, context):
        room = context.get('room')
        event = context.get('event')
        full_request = match.group(0)
        command_str = match.group(1)
        command = self.commands[command_str]
        use_cache = hasattr(command, 'cache_result')
        content = None
        if use_cache:
            content = self.CACHE.get_result(room, event)
        if not content:
            args = []
            args_str = match.group(2)
            if args_str:
                args = shlex.split(args_str)
            content = await self._execute_command(command, args, context)
        if content:
            if use_cache:
                self.CACHE.set_result(content, room, event)
            await self.client.room_send(room_id=room.room_id,
                                        message_type='m.room.message',
                                        content=content)

    async def _execute_command(self, command, args, context):
        try:
            res = await command(*args, context=context)
            if res:
                content = await res.get_content(client=self.client)
                return content
        except:
            return None

    async def invite_cb(self, room, event):
        if not self.AUTH.authenticate_invite(room, event):
            return
        if event.membership == 'invite' and event.state_key == self.user:
            await self.client.join(room.room_id)
            greeting = self._get_greeting()
            await self.client.room_send(room_id=room.room_id,
                                        message_type='m.room.message',
                                        content={
                                            'msgtype': 'm.text',
                                            "format": "org.matrix.custom.html",
                                            'body': greeting,
                                            'formatted_body': greeting
                                        })

    def _get_greeting(self):
        if self.CHANNEL_GREETING:
            return self.CHANNEL_GREETING
        msg = f"<h1>Hello!, I'm {self.name}.</h1><h3>Try some commands:</h3><ul>"
        command_strings = list(self.commands.keys())
        for cmd in command_strings:
            msg = msg + f'<li><code>{cmd}</code></li>'
        msg = msg + '</ul>'
        return msg

    async def loginandsync(self):
        await self.client.login(self.password)
        if self.startup_method:
            await self.startup_method(self.client)
        await self.client.sync_forever(timeout=30000)

    def run(self):
        asyncio.get_event_loop().run_until_complete(self.loginandsync())
Exemple #24
0
class Bot:
    commands = {}
    allowed_users = {}
    cfg = cfg

    sync_delay = 1000
    user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:60.9) '
    'Gecko/20100101 Goanna/4.4 Firefox/60.9 PaleMoon/28.7.2'

    def __init__(self, loglevel=None):
        config = ClientConfig(encryption_enabled=True,
                              pickle_key=cfg.pickle_key,
                              store_name=cfg.store_name,
                              store_sync_tokens=True)

        if not os.path.exists(cfg.store_path):
            os.makedirs(cfg.store_path)

        self.http_session = aiohttp.ClientSession(
            headers={'User-Agent': self.user_agent})

        self.client = AsyncClient(
            cfg.server,
            cfg.user,
            cfg.device_id,
            config=config,
            store_path=cfg.store_path
        )

        logger_group.level = getattr(
            logbook, loglevel) if loglevel else logbook.CRITICAL
        logbook.StreamHandler(sys.stdout).push_application()

        self.logger = logbook.Logger('bot')
        logger_group.add_logger(self.logger)

        self.mli = MessageLinksInfo(self.http_session)

        self._register_commands()
        self.client.add_response_callback(self._sync_cb, SyncResponse)
        self.client.add_response_callback(
            self._key_query_cb, KeysQueryResponse)
        self.client.add_event_callback(self._invite_cb, InviteMemberEvent)

    def _preserve_name(self, path):
        return path.split('/')[-1].split('.py')[0].strip().replace(' ', '_').replace('-', '')

    def _validate_module(self, module):
        return hasattr(module, 'handler') and callable(module.handler)

    def _process_module(self, module):
        name = self._preserve_name(module.name) if hasattr(module, 'name') and isinstance(
            module.name, str) else module.__name__

        handler = module.handler

        raw_aliases = module.aliases if hasattr(module, 'aliases') and \
            (isinstance(module.aliases, tuple) or isinstance(module.aliases, str)) else ()
        raw_aliases = raw_aliases if isinstance(
            raw_aliases, tuple) else [raw_aliases]
        aliases = ()

        for alias in raw_aliases:
            alias = self._preserve_name(alias.replace('%', ''))
            aliases = (*aliases, alias)

        help = module.help if hasattr(module, 'help') and \
            isinstance(module.help, str) else ''

        return (name, handler, aliases, help)

    def _register_commands(self):
        files_to_import = [fn for fn in glob.glob(
            "./commands/*.py") if not fn.count('__')]
        for file_path in files_to_import:
            try:
                name = self._preserve_name(file_path)
                spec = importlib.util.spec_from_file_location(
                    name, file_path)
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)

                name = module.name if hasattr(module, 'name') else name

                if not self._validate_module(module):
                    raise ImportError(f'Unable to register command \'{name}\' '
                                      f'(\'{module.__file__}\'). '
                                      'Command module must contain a callable '
                                      'object with name \'handler\'.')

                command = Command(
                    *self._process_module(module), self)

                if not self.commands.get(command.name):
                    self.commands[command.name] = command
                else:
                    raise ImportError(
                        f'Unable to register command \'{command.name}\' '
                        f'(\'{module.__file__}\'). '
                        'A command with this name already exists.')

                for alias in command.aliases:
                    if not self.commands.get(alias):
                        self.commands[alias] = command
                    else:
                        self.logger.warn(f'Unable to register alias \'{alias}\'! '
                                         'An alias with this name already exists '
                                         f'({self.commands[alias]}). Ignoring.')
            except Exception as e:
                self.logger.critical(e)

        if self.commands:
            self.logger.info(
                f'Registered commands: {list(set(self.commands.values()))}')
        else:
            self.logger.warn('No commands added!')

    def _parse_command(self, message):
        match = re.findall(r'^%([\w\d_]*)\s?(.*)$', message)
        if match:
            return (match[0][0], (match[0][1].split()))
        else:
            return (None, None)

    async def _serve_forever(self):
        response = await self.client.login(cfg.password)
        self.logger.info(response)

        await self.client.sync_forever(1000, full_state=True)

    async def _key_query_cb(self, response):
        for device in self.client.device_store:
            if device.trust_state.value == 0:
                if device.user_id in cfg.manager_accounts:
                    self.client.verify_device(device)
                    self.logger.info(
                        f'Verified manager\'s device {device.device_id} for user {device.user_id}')
                else:
                    self.client.blacklist_device(device)

    async def _invite_cb(self, room, event):
        if room.room_id not in self.client.rooms and \
           event.sender in cfg.manager_accounts:
            await self.client.join(room.room_id)
            self.logger.info(
                f'Accepted invite to room {room.room_id} from {event.sender}')

    def _is_sender_verified(self, sender):
        devices = [
            d for d in self.client.device_store.active_user_devices(sender)]
        return all(map(lambda d: d.trust_state.value == 1, devices))

    async def _process_links(self, message, room_id):
        info = await self.mli._get_info(message)
        if info:
            nl = '\n'
            content = {
                'body': f'{nl.join(info)}',
                'formatted_body': f'{nl.join(map(lambda i: i.join(["<strong>", "</strong>"]),info))}',
                'format': 'org.matrix.custom.html',
                'msgtype': 'm.text'
            }
            await self.client.room_send(room_id, 'm.room.message', content)

    async def _sync_cb(self, response):
        if len(response.rooms.join) > 0:
            joins = response.rooms.join
            for room_id in joins:
                for event in joins[room_id].timeline.events:
                    if self._is_sender_verified(event.sender) and hasattr(event, 'body'):
                        command, args = self._parse_command(event.body)
                        if command and command in self.commands:
                            await self.commands[command].run(
                                args, event, room_id)
                            self.logger.debug(
                                f'serving command \'{command}\' with arguments {args} in room {room_id}')
                        if event.sender != self.cfg.user:
                            await self._process_links(event.body, room_id)

    def serve(self):
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._serve_forever())
Exemple #25
0
async def main():
    """The first function that is run when starting the bot"""

    # Read user-configured options from a config file.
    # A different config file path can be specified as the first command line argument
    if len(sys.argv) > 1:
        config_path = sys.argv[1]
    else:
        config_path = "config.yaml"

    # Read the parsed config file and create a Config object
    config = Config(config_path)

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

    # 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_path,
        config=client_config,
    )

    if config.user_token:
        client.access_token = config.user_token
        client.user_id = config.user_id

    # 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_event_callback(callbacks.decryption_failure, (MegolmEvent, ))
    client.add_event_callback(callbacks.unknown, (UnknownEvent, ))

    # Set up a scheduler
    scheduler = AsyncIOScheduler()

    # Set up MISPAlert
    misp_alert = MISPAlert(client, config, store)

    # Add a job that checks for new taged events every minute
    trigger = IntervalTrigger(
        seconds=60,
        start_date=datetime.now() + timedelta(seconds=2),
    )

    # Add the query job
    scheduler.add_job(misp_alert.alerter, trigger=trigger)

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        try:
            if config.user_token:
                # Use token to log in
                client.load_store()

                # Sync encryption keys with the server
                if client.should_upload_keys:
                    await client.keys_upload()
            else:
                # Try to login with the configured username/password
                try:
                    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: %s",
                                     login_response.message)
                        return False
                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? "
                        "https://github.com/poljar/matrix-nio#installation "
                        "Error: %s",
                        e,
                    )
                    return False

                # Login succeeded!

            logger.info(f"Logged in as {config.user_id}")

            # Allow jobs to fire
            try:
                scheduler.start()
            except SchedulerAlreadyRunningError:
                pass

            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()
Exemple #26
0
async def main():

    # TODO: this really needs to be replaced
    # probably using https://docs.python.org/3.8/library/functools.html#functools.partial
    global client
    global plugin_loader

    # Read config file
    config = Config("config.yaml")

    # 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=config.enable_encryption,
    )

    # 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,
    )

    # instantiate the pluginLoader
    plugin_loader = PluginLoader()

    # Set up event callbacks
    callbacks = Callbacks(client, store, config, plugin_loader)
    client.add_event_callback(callbacks.message, (RoomMessageText, ))
    client.add_event_callback(callbacks.invite, (InviteEvent, ))
    client.add_event_callback(callbacks.event_unknown, (UnknownEvent, ))
    client.add_response_callback(run_plugins)

    # Keep trying to reconnect on failure (with some time in-between)
    error_retries: int = 0
    while True:
        try:
            # Try to login with the configured username/password
            try:
                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(
                        f"Failed to login: {login_response.message}, retrying in 15s... ({error_retries})"
                    )
                    # try logging in a few times to work around temporary login errors during homeserver restarts
                    if error_retries < 3:
                        error_retries += 1
                        await sleep(15)
                        continue
                    else:
                        return False
                else:
                    error_retries = 0

            except LocalProtocolError as e:
                # There's an edge case here where the user enables encryption but hasn't installed
                # the correct C dependencies. In that case, a LocalProtocolError is raised on login.
                # Warn the user if these conditions are met.
                if config.enable_encryption:
                    logger.fatal(
                        "Failed to login and encryption is enabled. Have you installed the correct dependencies? "
                        "https://github.com/poljar/matrix-nio#installation")
                    return False
                else:
                    # We don't know why this was raised. Throw it at the user
                    logger.fatal(f"Error logging in: {e}")

            # Login succeeded!

            # Sync encryption keys with the server
            # Required for participating in encrypted rooms
            if client.should_upload_keys:
                await client.keys_upload()

            logger.info(f"Logged in as {config.user_id}")
            await client.sync_forever(timeout=30000, full_state=True)

        except (ClientConnectionError, ServerDisconnectedError, AttributeError,
                asyncio.TimeoutError) as err:
            logger.debug(err)
            logger.warning(
                f"Unable to connect to homeserver, retrying in 15s...")

            # Sleep so we don't bombard the server with login requests
            await sleep(15)
        finally:
            # Make sure to close the client connection on disconnect
            await client.close()
Exemple #27
0
async def main():
    # 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, ))

    # Keep trying to reconnect on failure (with some time in-between)
    while True:
        try:
            # Try to login with the configured username/password
            try:
                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(f"Failed to login: %s",
                                 login_response.message)
                    return False
            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? "
                    "https://github.com/poljar/matrix-nio#installation "
                    "Error: %s", e)
                return False

            # Login succeeded!

            # Sync encryption keys with the server
            # Required for participating in encrypted rooms
            if client.should_upload_keys:
                await client.keys_upload()

            logger.info(f"Logged in as {config.user_id}")
            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()
Exemple #28
0
class Bot:

    def __init__(self):
        self.appid = 'org.vranki.hemppa'
        self.version = '1.5'
        self.client = None
        self.join_on_invite = False
        self.modules = dict()
        self.module_aliases = dict()
        self.leave_empty_rooms = True
        self.uri_cache = dict()
        self.pollcount = 0
        self.poll_task = None
        self.owners = []
        self.debug = os.getenv("DEBUG", "false").lower() == "true"
        self.logger = None

        self.jointime = None  # HACKHACKHACK to avoid running old commands after join
        self.join_hack_time = 5  # Seconds

        self.initialize_logger()

    def initialize_logger(self):

        if os.path.exists('config/logging.yml'):
            with open('config/logging.yml') as f:
                config = yaml.load(f, Loader=yaml.Loader)
                logging.config.dictConfig(config)
        else:
            log_format = '%(levelname)s - %(name)s - %(message)s'
            logging.basicConfig(format=log_format)

        self.logger = logging.getLogger("hemppa")

        if self.debug:
            logging.root.setLevel(logging.DEBUG)
            self.logger.info("enabled debugging")

        self.logger.debug("Logger initialized")

    async def upload_and_send_image(self, room, url, text=None, blob=False, blob_content_type="image/png"):
        """

        :param room: A MatrixRoom the image should be send to after uploading
        :param url: Url of binary content of the image to upload
        :param text: A textual representation of the image
        :param blob: Flag to indicate if the second param is an url or a binary content
        :param blob_content_type: Content type of the image in case of binary content
        :return:
        """
        cache_key = url
        if blob:  ## url is bytes, cannot be used a key for cache
            cache_key = hashlib.md5(url).hexdigest()

        try:
            matrix_uri, mimetype, w, h, size = self.uri_cache[cache_key]
        except KeyError:
            try:
                res = await self.upload_image(url, blob, blob_content_type)
                matrix_uri, mimetype, w, h, size = res
                self.uri_cache[cache_key] = list(res)
                self.save_settings()
            except UploadFailed:
                return await self.send_text(room, f"Sorry. Something went wrong fetching {url} and uploading it to the image to matrix server :(")

        if not text and not blob:
            text = f"{url}"
        return await self.send_image(room, matrix_uri, text, mimetype, w, h, size)

    # Helper function to upload a image from URL to homeserver. Use send_image() to actually send it to room.
    # Throws exception if upload fails
    async def upload_image(self, url_or_bytes, blob=False, blob_content_type="image/png"):
        """

        :param url_or_bytes: Url or binary content of the image to upload
        :param blob: Flag to indicate if the first param is an url or a binary content
        :param blob_content_type: Content type of the image in case of binary content
        :return: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri, Content type, Width, Height, Image size in bytes
        """

        self.client: AsyncClient
        response: UploadResponse
        if blob:
            (response, alist) = await self.client.upload(lambda a, b: url_or_bytes, blob_content_type)
            i = Image.open(BytesIO(url_or_bytes))
            image_length = len(url_or_bytes)
            content_type = blob_content_type
        else:
            self.logger.debug(f"start downloading image from url {url_or_bytes}")
            headers = {'User-Agent': 'Mozilla/5.0'}
            url_response = requests.get(url_or_bytes, headers=headers)
            self.logger.debug(f"response [status_code={url_response.status_code}, headers={url_response.headers}")

            if url_response.status_code == 200:
                content_type = url_response.headers.get("content-type")
                self.logger.info(f"uploading content to matrix server [size={len(url_response.content)}, content-type: {content_type}]")
                (response, alist) = await self.client.upload(lambda a, b: url_response.content, content_type)
                self.logger.debug("response: %s", response)
                i = Image.open(BytesIO(url_response.content))
                image_length = len(url_response.content)
            else:
                self.logger.error("unable to request url: %s", url_response)

                raise UploadFailed

        if isinstance(response, UploadResponse):
            self.logger.info("uploaded file to %s", response.content_uri)
            return response.content_uri, content_type, i.size[0], i.size[1], image_length
        else:
            response: UploadError
            self.logger.error("unable to upload file. msg: %s", response.message)

        raise UploadFailed

    async def send_text(self, room, body, msgtype="m.notice", bot_ignore=False):
        """

        :param room: A MatrixRoom the text should be send to
        :param body: Textual content of the message
        :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
        :param bot_ignore: Flag to mark the message to be ignored by the bot
        :return: the NIO Response from room_send()
        """

        msg = {
            "body": body,
            "msgtype": msgtype,
        }
        if bot_ignore:
            msg["org.vranki.hemppa.ignore"] = "true"

        return await self.client.room_send(room.room_id, 'm.room.message', msg)

    async def send_html(self, room, html, plaintext, msgtype="m.notice", bot_ignore=False):
        """

        :param room: A MatrixRoom the html should be send to
        :param html: Html content of the message
        :param plaintext: Plaintext content of the message
        :param msgtype: The message type for the room https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
        :param bot_ignore: Flag to mark the message to be ignored by the bot
        :return:
        """

        msg = {
            "msgtype": msgtype,
            "format": "org.matrix.custom.html",
            "formatted_body": html,
            "body": plaintext
        }
        if bot_ignore:
            msg["org.vranki.hemppa.ignore"] = "true"
        await self.client.room_send(room.room_id, 'm.room.message', msg)

    async def send_location(self, room, body, latitude, longitude, bot_ignore=False):
        """

        :param room: A MatrixRoom the html should be send to
        :param html: Html content of the message
        :param body: Plaintext content of the message
        :param latitude: Latitude in WGS84 coordinates (float)
        :param longitude: Longitude in WGS84 coordinates (float)
        :param bot_ignore: Flag to mark the message to be ignored by the bot
        :return:
        """
        locationmsg = {
            "body": str(body),
            "geo_uri": 'geo:' + str(latitude) + ',' + str(longitude),
            "msgtype": "m.location",
            }
        await self.client.room_send(room.room_id, 'm.room.message', locationmsg)

    async def send_image(self, room, url, body, mimetype=None, width=None, height=None, size=None):
        """

        :param room: A MatrixRoom the image should be send to
        :param url: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri
        :param body: A textual representation of the image
        :param mimetype: The mimetype of the image
        :param width: Width in pixel of the image
        :param height: Height in pixel of the image
        :param size: Size in bytes of the image
        :return:
        """
        msg = {
            "url": url,
            "body": body,
            "msgtype": "m.image",
            "info": {
                "thumbnail_info": None,
                "thumbnail_url": None,
            },
        }

        if mimetype:
            msg["info"]["mimetype"] = mimetype
        if width:
            msg["info"]["w"] = width
        if height:
            msg["info"]["h"] = height
        if size:
            msg["info"]["size"] = size

        return await self.client.room_send(room.room_id, 'm.room.message', msg)

    async def send_msg(self, mxid, roomname, message):
        """

        :param mxid: A Matrix user id to send the message to
        :param roomname: A Matrix room id to send the message to
        :param message: Text to be sent as message
        :return bool: Success upon sending the message
        """
        # Sends private message to user. Returns true on success.
        msg_room = await self.find_or_create_private_msg(mxid, roomname)
        if not msg_room or (type(msg_room) is RoomCreateError):
            self.logger.error(f'Unable to create room when trying to message {mxid}')
            return False

        # Send message to the room
        await self.send_text(msg_room, message)
        return True

    async def find_or_create_private_msg(self, mxid, roomname):
        # Find if we already have a common room with user:
        msg_room = None
        for croomid in self.client.rooms:
            roomobj = self.client.rooms[croomid]
            if len(roomobj.users) == 2:
                for user in roomobj.users:
                    if user == mxid:
                        msg_room = roomobj

        # Nope, let's create one
        if not msg_room:
            msg_room = await self.client.room_create(visibility=RoomVisibility.private,
                name=roomname,
                is_direct=True,
                preset=RoomPreset.private_chat,
                invite={mxid},
            )
        return msg_room


    def remove_callback(self, callback):
        for cb_object in self.client.event_callbacks:
            if cb_object.func == callback:
                self.logger.info("remove callback")
                self.client.event_callbacks.remove(cb_object)

    def get_room_by_id(self, room_id):
        try:
            return self.client.rooms[room_id]
        except KeyError:
            return None

    async def get_room_by_alias(self, alias):
        rar = await self.client.room_resolve_alias(alias)
        if type(rar) is RoomResolveAliasResponse:
            return rar.room_id
        return None

    # Throws exception if event sender is not a room admin
    def must_be_admin(self, room, event, power_level=50):
        if not self.is_admin(room, event, power_level=power_level):
            raise CommandRequiresAdmin

    # Throws exception if event sender is not a bot owner
    def must_be_owner(self, event):
        if not self.is_owner(event):
            raise CommandRequiresOwner

    # Returns true if event's sender has PL50 or more in the room event was sent in,
    # or is bot owner
    def is_admin(self, room, event, power_level=50):
        if self.is_owner(event):
            return True
        if event.sender not in room.power_levels.users:
            return False
        return room.power_levels.users[event.sender] >= power_level

    # Returns true if event's sender is owner of the bot
    def is_owner(self, event):
        return event.sender in self.owners

    # Checks if this event should be ignored by bot, including custom property
    def should_ignore_event(self, event):
        return "org.vranki.hemppa.ignore" in event.source['content']

    def save_settings(self):
        module_settings = dict()
        for modulename, moduleobject in self.modules.items():
            try:
                module_settings[modulename] = moduleobject.get_settings()
            except Exception:
                self.logger.exception(f'unhandled exception {modulename}.get_settings')
        data = {self.appid: self.version, 'module_settings': module_settings, 'uri_cache': self.uri_cache}
        self.set_account_data(data)

    def load_settings(self, data):
        if not data:
            return
        if not data.get('module_settings'):
            return
        if data.get('uri_cache'):
            self.uri_cache = data['uri_cache']
        for modulename, moduleobject in self.modules.items():
            if data['module_settings'].get(modulename):
                try:
                    moduleobject.set_settings(
                        data['module_settings'][modulename])
                except Exception:
                    self.logger.exception(f'unhandled exception {modulename}.set_settings')

    async def message_cb(self, room, event):
        # Ignore if asked to ignore
        if self.should_ignore_event(event):
            if self.debug:
                self.logger.debug('Ignoring event!')
            return

        body = event.body
        # Figure out the command
        if not self.starts_with_command(body):
            return

        if self.owners_only and not self.is_owner(event):
            self.logger.info(f"Ignoring {event.sender}, because they're not an owner")
            await self.send_text(room, "Sorry, only bot owner can run commands.")
            return

        # HACK to ignore messages for some time after joining.
        if self.jointime:
            if (datetime.datetime.now() - self.jointime).seconds < self.join_hack_time:
                self.logger.info(f"Waiting for join delay, ignoring message: {body}")
                return
            self.jointime = None

        command = body.split().pop(0)

        # Strip away non-alphanumeric characters, including leading ! for security
        command = re.sub(r'\W+', '', command)

        # Fallback to any declared aliases
        moduleobject = self.modules.get(command) or self.modules.get(self.module_aliases.get(command))

        if moduleobject is not None:
            if moduleobject.enabled:
                try:
                    await moduleobject.matrix_message(self, room, event)
                except CommandRequiresAdmin:
                    await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.')
                except CommandRequiresOwner:
                    await self.send_text(room, f'Sorry, only bot owner can run that command.')
                except Exception:
                    await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
                    self.logger.exception(f'unhandled exception in !{command}')
        else:
            self.logger.error(f"Unknown command: {command}")
            # TODO Make this configurable
            # await self.send_text(room,
            #                     f"Sorry. I don't know what to do. Execute !help to get a list of available commands.")

    @staticmethod
    def starts_with_command(body):
        """Checks if body starts with ! and has one or more letters after it"""
        return re.match(r"^!\w.*", body) is not None

    async def invite_cb(self, room, event):
        room: MatrixRoom
        event: InviteEvent

        if self.join_on_invite or self.is_owner(event):
            for attempt in range(3):
                self.jointime = datetime.datetime.now()
                result = await self.client.join(room.room_id)
                if type(result) == JoinError:
                    self.logger.error(f"Error joining room %s (attempt %d): %s", room.room_id, attempt, result.message)
                else:
                    self.logger.info(f"joining room '{room.display_name}'({room.room_id}) invited by '{event.sender}'")
                    return
        else:
            self.logger.warning(f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}')

    async def memberevent_cb(self, room, event):
        # Automatically leaves rooms where bot is alone.
        if room.member_count == 1 and event.membership=='leave' and event.sender != self.matrix_user:
            self.logger.info(f"Membership event in {room.display_name} ({room.room_id}) with {room.member_count} members by '{event.sender}' (I am {self.matrix_user})- leaving room as i don't want to be left alone!")
            await self.client.room_leave(room.room_id)

    def load_module(self, modulename):
        try:
            self.logger.info(f'load module: {modulename}')
            module = importlib.import_module('modules.' + modulename)
            module = reload(module)
            cls = getattr(module, 'MatrixModule')
            return cls(modulename)
        except Exception:
            self.logger.exception(f'Module {modulename} failed to load')
            return None

    def reload_modules(self):
        for modulename in self.modules:
            self.logger.info(f'Reloading {modulename} ..')
            self.modules[modulename] = self.load_module(modulename)

        self.load_settings(self.get_account_data())

    def get_modules(self):
        modulefiles = glob.glob('./modules/*.py')

        for modulefile in modulefiles:
            modulename = os.path.splitext(os.path.basename(modulefile))[0]
            moduleobject = self.load_module(modulename)
            if moduleobject:
                self.modules[modulename] = moduleobject

    def clear_modules(self):
        self.modules = dict()

    async def poll_timer(self):
        while True:
            self.pollcount = self.pollcount + 1
            for modulename, moduleobject in self.modules.items():
                if moduleobject.enabled:
                    try:
                        await moduleobject.matrix_poll(self, self.pollcount)
                    except Exception:
                        self.logger.exception(f'unhandled exception from {modulename}.matrix_poll')
            await asyncio.sleep(10)

    def set_account_data(self, data):
        userid = urllib.parse.quote(self.matrix_user)

        ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"

        response = requests.put(ad_url, json.dumps(data))
        self.__handle_error_response(response)

        if response.status_code != 200:
            self.logger.error('Setting account data failed. response: %s json: %s', response, response.json())

    def get_account_data(self):
        userid = urllib.parse.quote(self.matrix_user)

        ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"

        response = requests.get(ad_url)
        self.__handle_error_response(response)

        if response.status_code == 200:
            return response.json()
        self.logger.error(f'Getting account data failed: {response} {response.json()} - this is normal if you have not saved any settings yet.')
        return None

    def __handle_error_response(self, response):
        if response.status_code == 401:
            self.logger.error("access token is invalid or missing")
            self.logger.info("NOTE: check MATRIX_ACCESS_TOKEN")
            sys.exit(2)

    def init(self):

        self.matrix_user = os.getenv('MATRIX_USER')
        matrix_server = os.getenv('MATRIX_SERVER')
        bot_owners = os.getenv('BOT_OWNERS')
        access_token = os.getenv('MATRIX_ACCESS_TOKEN')
        join_on_invite = os.getenv('JOIN_ON_INVITE')
        owners_only = os.getenv('OWNERS_ONLY') is not None
        leave_empty_rooms = os.getenv('LEAVE_EMPTY_ROOMS')

        if matrix_server and self.matrix_user and bot_owners and access_token:
            self.client = AsyncClient(matrix_server, self.matrix_user, ssl = matrix_server.startswith("https://"))
            self.client.access_token = access_token
            self.join_on_invite = (join_on_invite or '').lower() == 'true'
            self.leave_empty_rooms = (leave_empty_rooms or 'true').lower() == 'true'
            self.owners = bot_owners.split(',')
            self.owners_only = owners_only
            self.get_modules()

        else:
            self.logger.error("The environment variables MATRIX_SERVER, MATRIX_USER, MATRIX_ACCESS_TOKEN and BOT_OWNERS are mandatory")
            sys.exit(1)

    def start(self):
        self.load_settings(self.get_account_data())
        enabled_modules = [module for module_name, module in self.modules.items() if module.enabled]
        self.logger.info(f'Starting {len(enabled_modules)} modules..')
        for modulename, moduleobject in self.modules.items():
            if moduleobject.enabled:
                try:
                    moduleobject.matrix_start(self)
                except Exception:
                    self.logger.exception(f'unhandled exception from {modulename}.matrix_start')
        self.logger.info(f'All modules started.')

    def stop(self):
        self.logger.info(f'Stopping {len(self.modules)} modules..')
        for modulename, moduleobject in self.modules.items():
            try:
                moduleobject.matrix_stop(self)
            except Exception:
                self.logger.exception(f'unhandled exception from {modulename}.matrix_stop')
        self.logger.info(f'All modules stopped.')

    async def run(self):
        await self.client.sync()
        for roomid, room in self.client.rooms.items():
            self.logger.info(f"Bot is on '{room.display_name}'({roomid}) with {len(room.users)} users")
            if len(room.users) == 1 and self.leave_empty_rooms:
                self.logger.info(f'Room {roomid} has no other users - leaving it.')
                self.logger.info(await self.client.room_leave(roomid))

        self.start()

        self.poll_task = asyncio.get_event_loop().create_task(self.poll_timer())

        if self.client.logged_in:
            self.load_settings(self.get_account_data())
            self.client.add_event_callback(self.message_cb, RoomMessageText)
            self.client.add_event_callback(self.invite_cb, (InviteEvent,))
            self.client.add_event_callback(self.memberevent_cb, (RoomMemberEvent,))

            if self.join_on_invite:
                self.logger.info('Note: Bot will join rooms if invited')
            self.logger.info('Bot running as %s, owners %s', self.client.user, self.owners)
            self.bot_task = asyncio.create_task(self.client.sync_forever(timeout=30000))
            await self.bot_task
        else:
            self.logger.error('Client was not able to log in, check env variables!')

    async def shutdown(self):
        await self.close()

    async def close(self):
        try:
            await self.client.close()
            self.logger.info("Connection closed")
        except Exception as ex:
            self.logger.error("error while closing client: %s", ex)

    def handle_exit(self, signame, loop):
        self.logger.info(f"Received signal {signame}")
        if self.poll_task:
            self.poll_task.cancel()
        self.bot_task.cancel()
        self.stop()
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)])
Exemple #30
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()