Module trappedbot.callbacks

Callbacks to give to the nio.AsyncClient.

nio uses callbacks to do work on Matrix events. botloop() sets callback funtions defined here on the nio.AsyncClient object it uses.

Expand source code
"""Callbacks to give to the `nio.AsyncClient`.

`nio` uses callbacks to do work on Matrix events.
`trappedbot.botclient.botloop` sets callback funtions defined here on the
`nio.AsyncClient` object it uses.
"""

import traceback

from nio import (
    JoinError,
    KeyVerificationStart,
    KeyVerificationCancel,
    KeyVerificationKey,
    KeyVerificationMac,
    ToDeviceError,
    LocalProtocolError,
)
from nio.client.async_client import AsyncClient
from nio.events.room_events import RoomMessageText
from nio.rooms import MatrixRoom

from trappedbot import appconfig
from trappedbot.applogger import LOGGER
from trappedbot.chat_functions import send_text_to_room
from trappedbot.commands.command import process_command
from trappedbot.storage import Storage
from trappedbot.tasks.builtin import BUILTIN_TASKS


def msglog(logline: str, room: MatrixRoom, event: RoomMessageText):
    """Log a line with room name/user/message metadata

    Convenience function.

    TODO: Consider NOT logging message contents unless passing a special flag?
        Logging messages by default might not be what people expect.
    """
    LOGGER.debug(
        f"{logline} % {room.display_name} | {room.user_name(event.sender)}: {event.body}"
    )


def in_dms(room: MatrixRoom) -> bool:
    """Are we in DMs, or in a larger room?

    Assume a room with only two members (including our account) is a DM,
    and a room with more than two members is not.

    TODO: is there a better way to determine this?
    """
    # room.is_group is often a DM, but not always.
    # room.is_group does not allow room aliases
    # room.member_count > 2 ... we assume a public room
    # room.member_count <= 2 ... we assume a DM
    return room.member_count <= 2


class Callbacks(object):
    """Collection of all callbacks.

    Conveniently keeps the `client` and `store` properties so that callbacks
    can reference them.
    """

    def __init__(self, client: AsyncClient, store: Storage):
        """Initialize.

        Arguments:
        ---------
            client (nio.AsyncClient): nio client used to interact with matrix
            store (Storage): Bot storage
        """
        self.client = client
        self.store = store

    async def message(self, room: MatrixRoom, event: RoomMessageText):
        """Handle an incoming message event.

        room:   The room the event came from
        event:  The event defining the message
        """
        config = appconfig.get()

        LOGGER.debug(f"Responding to a message from {event.sender}...")

        if event.sender == self.client.user:
            msglog("Ignoring message from myself", room, event)
            return
        elif event.body.startswith(config.command_prefix):
            msg = event.body[len(config.command_prefix) :]
            await process_command(self.client, msg, room, event)
            return
        else:
            for response in config.responses:
                if response.regex.search(event.body):
                    await send_text_to_room(
                        self.client,
                        room.room_id,
                        response.message,
                        replyto=event,
                        replyto_room=room,
                    )
            return

    async def invite(self, room, event):
        """Handle an incoming invite event.

        If an invite is received, then join the room specified in the invite.
        """
        LOGGER.debug(f"Got invite to {room.room_id} from {event.sender}.")

        # Attempt to join 3 times before giving up
        for attempt in range(3):
            result = await self.client.join(room.room_id)
            if type(result) == JoinError:
                LOGGER.error(
                    f"Error joining room {room.room_id} (attempt %d): %s",
                    attempt,
                    result.message,
                )
            else:
                break
        else:
            LOGGER.error("Unable to join room: %s", room.room_id)

        # Successfully joined room
        LOGGER.info(f"Joined {room.room_id}")

    async def to_device_cb(self, event):  # noqa
        """Handle events sent to device.

        Specifically this will perform Emoji verification.
        It will accept an incoming Emoji verification requests
        and follow the verification protocol.
        """
        try:
            client = self.client
            LOGGER.debug(
                f"Device Event of type {type(event)} received in " "to_device_cb()."
            )

            if isinstance(event, KeyVerificationStart):  # first step
                """first step: receive KeyVerificationStart
                KeyVerificationStart(
                    source={'content':
                            {'method': 'm.sas.v1',
                             'from_device': 'DEVICEIDXY',
                             'key_agreement_protocols':
                                ['curve25519-hkdf-sha256', 'curve25519'],
                             'hashes': ['sha256'],
                             'message_authentication_codes':
                                ['hkdf-hmac-sha256', 'hmac-sha256'],
                             'short_authentication_string':
                                ['decimal', 'emoji'],
                             'transaction_id': 'SomeTxId'
                             },
                            'type': 'm.key.verification.start',
                            'sender': '@user2:example.org'
                            },
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    from_device='DEVICEIDXY',
                    method='m.sas.v1',
                    key_agreement_protocols=[
                        'curve25519-hkdf-sha256', 'curve25519'],
                    hashes=['sha256'],
                    message_authentication_codes=[
                        'hkdf-hmac-sha256', 'hmac-sha256'],
                    short_authentication_string=['decimal', 'emoji'])
                """

                if "emoji" not in event.short_authentication_string:
                    estr = (
                        "Other device does not support emoji verification "
                        f"{event.short_authentication_string}. Aborting."
                    )
                    print(estr)
                    LOGGER.info(estr)
                    return
                resp = await client.accept_key_verification(event.transaction_id)
                if isinstance(resp, ToDeviceError):
                    estr = f"accept_key_verification() failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)

                sas = client.key_verifications[event.transaction_id]

                todevice_msg = sas.share_key()
                resp = await client.to_device(todevice_msg)
                if isinstance(resp, ToDeviceError):
                    estr = f"to_device() failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)

            elif isinstance(event, KeyVerificationCancel):  # anytime
                """at any time: receive KeyVerificationCancel
                KeyVerificationCancel(source={
                    'content': {'code': 'm.mismatched_sas',
                                'reason': 'Mismatched authentication string',
                                'transaction_id': 'SomeTxId'},
                    'type': 'm.key.verification.cancel',
                    'sender': '@user2:example.org'},
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    code='m.mismatched_sas',
                    reason='Mismatched short authentication string')
                """

                # There is no need to issue a
                # client.cancel_key_verification(tx_id, reject=False)
                # here. The SAS flow is already cancelled.
                # We only need to inform the user.
                estr = (
                    f"Verification has been cancelled by {event.sender} "
                    f'for reason "{event.reason}".'
                )
                print(estr)
                LOGGER.info(estr)

            elif isinstance(event, KeyVerificationKey):  # second step
                """Second step is to receive KeyVerificationKey
                KeyVerificationKey(
                    source={'content': {
                            'key': 'SomeCryptoKey',
                            'transaction_id': 'SomeTxId'},
                        'type': 'm.key.verification.key',
                        'sender': '@user2:example.org'
                    },
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    key='SomeCryptoKey')
                """
                sas = client.key_verifications[event.transaction_id]

                print(f"{sas.get_emoji()}")
                # don't log the emojis

                # The bot process must run in forground with a screen and
                # keyboard so that user can accept/reject via keyboard.
                # For emoji verification bot must not run as service or
                # in background.
                yn = input("Do the emojis match? (Y/N) (C for Cancel) ")
                if yn.lower() == "y":
                    estr = (
                        "Match! The verification for this " "device will be accepted."
                    )
                    print(estr)
                    LOGGER.info(estr)
                    resp = await client.confirm_short_auth_string(event.transaction_id)
                    if isinstance(resp, ToDeviceError):
                        estr = "confirm_short_auth_string() " f"failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)
                elif yn.lower() == "n":  # no, don't match, reject
                    estr = (
                        "No match! Device will NOT be verified "
                        "by rejecting verification."
                    )
                    print(estr)
                    LOGGER.info(estr)
                    resp = await client.cancel_key_verification(
                        event.transaction_id, reject=True
                    )
                    if isinstance(resp, ToDeviceError):
                        estr = f"cancel_key_verification failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)
                else:  # C or anything for cancel
                    estr = "Cancelled by user! Verification will be " "cancelled."
                    print(estr)
                    LOGGER.info(estr)
                    resp = await client.cancel_key_verification(
                        event.transaction_id, reject=False
                    )
                    if isinstance(resp, ToDeviceError):
                        estr = f"cancel_key_verification failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)

            elif isinstance(event, KeyVerificationMac):  # third step
                """Third step is to receive KeyVerificationMac
                KeyVerificationMac(
                    source={'content': {
                        'mac': {'ed25519:DEVICEIDXY': 'SomeKey1',
                                'ed25519:SomeKey2': 'SomeKey3'},
                        'keys': 'SomeCryptoKey4',
                        'transaction_id': 'SomeTxId'},
                        'type': 'm.key.verification.mac',
                        'sender': '@user2:example.org'},
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    mac={'ed25519:DEVICEIDXY': 'SomeKey1',
                         'ed25519:SomeKey2': 'SomeKey3'},
                    keys='SomeCryptoKey4')
                """
                sas = client.key_verifications[event.transaction_id]
                try:
                    todevice_msg = sas.get_mac()
                except LocalProtocolError as e:
                    # e.g. it might have been cancelled by ourselves
                    estr = (
                        f"Cancelled or protocol error: Reason: {e}.\n"
                        f"Verification with {event.sender} not concluded. "
                        "Try again?"
                    )
                    print(estr)
                    LOGGER.info(estr)
                else:
                    resp = await client.to_device(todevice_msg)
                    if isinstance(resp, ToDeviceError):
                        estr = f"to_device failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)
                    estr = (
                        f"sas.we_started_it = {sas.we_started_it}\n"
                        f"sas.sas_accepted = {sas.sas_accepted}\n"
                        f"sas.canceled = {sas.canceled}\n"
                        f"sas.timed_out = {sas.timed_out}\n"
                        f"sas.verified = {sas.verified}\n"
                        f"sas.verified_devices = {sas.verified_devices}\n"
                    )
                    print(estr)
                    LOGGER.info(estr)
                    estr = (
                        "Emoji verification was successful!\n"
                        "Initiate another Emoji verification from "
                        "another device or room if desired. "
                        "Or if done verifying, hit Control-C to stop the "
                        "bot in order to restart it as a service or to "
                        "run it in the background."
                    )
                    print(estr)
                    LOGGER.info(estr)
            else:
                estr = (
                    f"Received unexpected event type {type(event)}. "
                    f"Event is {event}. Event will be ignored."
                )
                print(estr)
                LOGGER.info(estr)
        except BaseException:
            estr = traceback.format_exc()
            print(estr)
            LOGGER.info(estr)

Functions

def in_dms(room: nio.rooms.MatrixRoom) ‑> bool

Are we in DMs, or in a larger room?

Assume a room with only two members (including our account) is a DM, and a room with more than two members is not.

TODO: is there a better way to determine this?

Expand source code
def in_dms(room: MatrixRoom) -> bool:
    """Are we in DMs, or in a larger room?

    Assume a room with only two members (including our account) is a DM,
    and a room with more than two members is not.

    TODO: is there a better way to determine this?
    """
    # room.is_group is often a DM, but not always.
    # room.is_group does not allow room aliases
    # room.member_count > 2 ... we assume a public room
    # room.member_count <= 2 ... we assume a DM
    return room.member_count <= 2
def msglog(logline: str, room: nio.rooms.MatrixRoom, event: nio.events.room_events.RoomMessageText)

Log a line with room name/user/message metadata

Convenience function.

TODO: Consider NOT logging message contents unless passing a special flag? Logging messages by default might not be what people expect.

Expand source code
def msglog(logline: str, room: MatrixRoom, event: RoomMessageText):
    """Log a line with room name/user/message metadata

    Convenience function.

    TODO: Consider NOT logging message contents unless passing a special flag?
        Logging messages by default might not be what people expect.
    """
    LOGGER.debug(
        f"{logline} % {room.display_name} | {room.user_name(event.sender)}: {event.body}"
    )

Classes

class Callbacks (client: nio.client.async_client.AsyncClient, store: Storage)

Collection of all callbacks.

Conveniently keeps the client and store properties so that callbacks can reference them.

Initialize.

Arguments:

client (nio.AsyncClient): nio client used to interact with matrix
store (Storage): Bot storage
Expand source code
class Callbacks(object):
    """Collection of all callbacks.

    Conveniently keeps the `client` and `store` properties so that callbacks
    can reference them.
    """

    def __init__(self, client: AsyncClient, store: Storage):
        """Initialize.

        Arguments:
        ---------
            client (nio.AsyncClient): nio client used to interact with matrix
            store (Storage): Bot storage
        """
        self.client = client
        self.store = store

    async def message(self, room: MatrixRoom, event: RoomMessageText):
        """Handle an incoming message event.

        room:   The room the event came from
        event:  The event defining the message
        """
        config = appconfig.get()

        LOGGER.debug(f"Responding to a message from {event.sender}...")

        if event.sender == self.client.user:
            msglog("Ignoring message from myself", room, event)
            return
        elif event.body.startswith(config.command_prefix):
            msg = event.body[len(config.command_prefix) :]
            await process_command(self.client, msg, room, event)
            return
        else:
            for response in config.responses:
                if response.regex.search(event.body):
                    await send_text_to_room(
                        self.client,
                        room.room_id,
                        response.message,
                        replyto=event,
                        replyto_room=room,
                    )
            return

    async def invite(self, room, event):
        """Handle an incoming invite event.

        If an invite is received, then join the room specified in the invite.
        """
        LOGGER.debug(f"Got invite to {room.room_id} from {event.sender}.")

        # Attempt to join 3 times before giving up
        for attempt in range(3):
            result = await self.client.join(room.room_id)
            if type(result) == JoinError:
                LOGGER.error(
                    f"Error joining room {room.room_id} (attempt %d): %s",
                    attempt,
                    result.message,
                )
            else:
                break
        else:
            LOGGER.error("Unable to join room: %s", room.room_id)

        # Successfully joined room
        LOGGER.info(f"Joined {room.room_id}")

    async def to_device_cb(self, event):  # noqa
        """Handle events sent to device.

        Specifically this will perform Emoji verification.
        It will accept an incoming Emoji verification requests
        and follow the verification protocol.
        """
        try:
            client = self.client
            LOGGER.debug(
                f"Device Event of type {type(event)} received in " "to_device_cb()."
            )

            if isinstance(event, KeyVerificationStart):  # first step
                """first step: receive KeyVerificationStart
                KeyVerificationStart(
                    source={'content':
                            {'method': 'm.sas.v1',
                             'from_device': 'DEVICEIDXY',
                             'key_agreement_protocols':
                                ['curve25519-hkdf-sha256', 'curve25519'],
                             'hashes': ['sha256'],
                             'message_authentication_codes':
                                ['hkdf-hmac-sha256', 'hmac-sha256'],
                             'short_authentication_string':
                                ['decimal', 'emoji'],
                             'transaction_id': 'SomeTxId'
                             },
                            'type': 'm.key.verification.start',
                            'sender': '@user2:example.org'
                            },
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    from_device='DEVICEIDXY',
                    method='m.sas.v1',
                    key_agreement_protocols=[
                        'curve25519-hkdf-sha256', 'curve25519'],
                    hashes=['sha256'],
                    message_authentication_codes=[
                        'hkdf-hmac-sha256', 'hmac-sha256'],
                    short_authentication_string=['decimal', 'emoji'])
                """

                if "emoji" not in event.short_authentication_string:
                    estr = (
                        "Other device does not support emoji verification "
                        f"{event.short_authentication_string}. Aborting."
                    )
                    print(estr)
                    LOGGER.info(estr)
                    return
                resp = await client.accept_key_verification(event.transaction_id)
                if isinstance(resp, ToDeviceError):
                    estr = f"accept_key_verification() failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)

                sas = client.key_verifications[event.transaction_id]

                todevice_msg = sas.share_key()
                resp = await client.to_device(todevice_msg)
                if isinstance(resp, ToDeviceError):
                    estr = f"to_device() failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)

            elif isinstance(event, KeyVerificationCancel):  # anytime
                """at any time: receive KeyVerificationCancel
                KeyVerificationCancel(source={
                    'content': {'code': 'm.mismatched_sas',
                                'reason': 'Mismatched authentication string',
                                'transaction_id': 'SomeTxId'},
                    'type': 'm.key.verification.cancel',
                    'sender': '@user2:example.org'},
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    code='m.mismatched_sas',
                    reason='Mismatched short authentication string')
                """

                # There is no need to issue a
                # client.cancel_key_verification(tx_id, reject=False)
                # here. The SAS flow is already cancelled.
                # We only need to inform the user.
                estr = (
                    f"Verification has been cancelled by {event.sender} "
                    f'for reason "{event.reason}".'
                )
                print(estr)
                LOGGER.info(estr)

            elif isinstance(event, KeyVerificationKey):  # second step
                """Second step is to receive KeyVerificationKey
                KeyVerificationKey(
                    source={'content': {
                            'key': 'SomeCryptoKey',
                            'transaction_id': 'SomeTxId'},
                        'type': 'm.key.verification.key',
                        'sender': '@user2:example.org'
                    },
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    key='SomeCryptoKey')
                """
                sas = client.key_verifications[event.transaction_id]

                print(f"{sas.get_emoji()}")
                # don't log the emojis

                # The bot process must run in forground with a screen and
                # keyboard so that user can accept/reject via keyboard.
                # For emoji verification bot must not run as service or
                # in background.
                yn = input("Do the emojis match? (Y/N) (C for Cancel) ")
                if yn.lower() == "y":
                    estr = (
                        "Match! The verification for this " "device will be accepted."
                    )
                    print(estr)
                    LOGGER.info(estr)
                    resp = await client.confirm_short_auth_string(event.transaction_id)
                    if isinstance(resp, ToDeviceError):
                        estr = "confirm_short_auth_string() " f"failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)
                elif yn.lower() == "n":  # no, don't match, reject
                    estr = (
                        "No match! Device will NOT be verified "
                        "by rejecting verification."
                    )
                    print(estr)
                    LOGGER.info(estr)
                    resp = await client.cancel_key_verification(
                        event.transaction_id, reject=True
                    )
                    if isinstance(resp, ToDeviceError):
                        estr = f"cancel_key_verification failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)
                else:  # C or anything for cancel
                    estr = "Cancelled by user! Verification will be " "cancelled."
                    print(estr)
                    LOGGER.info(estr)
                    resp = await client.cancel_key_verification(
                        event.transaction_id, reject=False
                    )
                    if isinstance(resp, ToDeviceError):
                        estr = f"cancel_key_verification failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)

            elif isinstance(event, KeyVerificationMac):  # third step
                """Third step is to receive KeyVerificationMac
                KeyVerificationMac(
                    source={'content': {
                        'mac': {'ed25519:DEVICEIDXY': 'SomeKey1',
                                'ed25519:SomeKey2': 'SomeKey3'},
                        'keys': 'SomeCryptoKey4',
                        'transaction_id': 'SomeTxId'},
                        'type': 'm.key.verification.mac',
                        'sender': '@user2:example.org'},
                    sender='@user2:example.org',
                    transaction_id='SomeTxId',
                    mac={'ed25519:DEVICEIDXY': 'SomeKey1',
                         'ed25519:SomeKey2': 'SomeKey3'},
                    keys='SomeCryptoKey4')
                """
                sas = client.key_verifications[event.transaction_id]
                try:
                    todevice_msg = sas.get_mac()
                except LocalProtocolError as e:
                    # e.g. it might have been cancelled by ourselves
                    estr = (
                        f"Cancelled or protocol error: Reason: {e}.\n"
                        f"Verification with {event.sender} not concluded. "
                        "Try again?"
                    )
                    print(estr)
                    LOGGER.info(estr)
                else:
                    resp = await client.to_device(todevice_msg)
                    if isinstance(resp, ToDeviceError):
                        estr = f"to_device failed with {resp}"
                        print(estr)
                        LOGGER.info(estr)
                    estr = (
                        f"sas.we_started_it = {sas.we_started_it}\n"
                        f"sas.sas_accepted = {sas.sas_accepted}\n"
                        f"sas.canceled = {sas.canceled}\n"
                        f"sas.timed_out = {sas.timed_out}\n"
                        f"sas.verified = {sas.verified}\n"
                        f"sas.verified_devices = {sas.verified_devices}\n"
                    )
                    print(estr)
                    LOGGER.info(estr)
                    estr = (
                        "Emoji verification was successful!\n"
                        "Initiate another Emoji verification from "
                        "another device or room if desired. "
                        "Or if done verifying, hit Control-C to stop the "
                        "bot in order to restart it as a service or to "
                        "run it in the background."
                    )
                    print(estr)
                    LOGGER.info(estr)
            else:
                estr = (
                    f"Received unexpected event type {type(event)}. "
                    f"Event is {event}. Event will be ignored."
                )
                print(estr)
                LOGGER.info(estr)
        except BaseException:
            estr = traceback.format_exc()
            print(estr)
            LOGGER.info(estr)

Methods

async def invite(self, room, event)

Handle an incoming invite event.

If an invite is received, then join the room specified in the invite.

Expand source code
async def invite(self, room, event):
    """Handle an incoming invite event.

    If an invite is received, then join the room specified in the invite.
    """
    LOGGER.debug(f"Got invite to {room.room_id} from {event.sender}.")

    # Attempt to join 3 times before giving up
    for attempt in range(3):
        result = await self.client.join(room.room_id)
        if type(result) == JoinError:
            LOGGER.error(
                f"Error joining room {room.room_id} (attempt %d): %s",
                attempt,
                result.message,
            )
        else:
            break
    else:
        LOGGER.error("Unable to join room: %s", room.room_id)

    # Successfully joined room
    LOGGER.info(f"Joined {room.room_id}")
async def message(self, room: nio.rooms.MatrixRoom, event: nio.events.room_events.RoomMessageText)

Handle an incoming message event.

room: The room the event came from event: The event defining the message

Expand source code
async def message(self, room: MatrixRoom, event: RoomMessageText):
    """Handle an incoming message event.

    room:   The room the event came from
    event:  The event defining the message
    """
    config = appconfig.get()

    LOGGER.debug(f"Responding to a message from {event.sender}...")

    if event.sender == self.client.user:
        msglog("Ignoring message from myself", room, event)
        return
    elif event.body.startswith(config.command_prefix):
        msg = event.body[len(config.command_prefix) :]
        await process_command(self.client, msg, room, event)
        return
    else:
        for response in config.responses:
            if response.regex.search(event.body):
                await send_text_to_room(
                    self.client,
                    room.room_id,
                    response.message,
                    replyto=event,
                    replyto_room=room,
                )
        return
async def to_device_cb(self, event)

Handle events sent to device.

Specifically this will perform Emoji verification. It will accept an incoming Emoji verification requests and follow the verification protocol.

Expand source code
async def to_device_cb(self, event):  # noqa
    """Handle events sent to device.

    Specifically this will perform Emoji verification.
    It will accept an incoming Emoji verification requests
    and follow the verification protocol.
    """
    try:
        client = self.client
        LOGGER.debug(
            f"Device Event of type {type(event)} received in " "to_device_cb()."
        )

        if isinstance(event, KeyVerificationStart):  # first step
            """first step: receive KeyVerificationStart
            KeyVerificationStart(
                source={'content':
                        {'method': 'm.sas.v1',
                         'from_device': 'DEVICEIDXY',
                         'key_agreement_protocols':
                            ['curve25519-hkdf-sha256', 'curve25519'],
                         'hashes': ['sha256'],
                         'message_authentication_codes':
                            ['hkdf-hmac-sha256', 'hmac-sha256'],
                         'short_authentication_string':
                            ['decimal', 'emoji'],
                         'transaction_id': 'SomeTxId'
                         },
                        'type': 'm.key.verification.start',
                        'sender': '@user2:example.org'
                        },
                sender='@user2:example.org',
                transaction_id='SomeTxId',
                from_device='DEVICEIDXY',
                method='m.sas.v1',
                key_agreement_protocols=[
                    'curve25519-hkdf-sha256', 'curve25519'],
                hashes=['sha256'],
                message_authentication_codes=[
                    'hkdf-hmac-sha256', 'hmac-sha256'],
                short_authentication_string=['decimal', 'emoji'])
            """

            if "emoji" not in event.short_authentication_string:
                estr = (
                    "Other device does not support emoji verification "
                    f"{event.short_authentication_string}. Aborting."
                )
                print(estr)
                LOGGER.info(estr)
                return
            resp = await client.accept_key_verification(event.transaction_id)
            if isinstance(resp, ToDeviceError):
                estr = f"accept_key_verification() failed with {resp}"
                print(estr)
                LOGGER.info(estr)

            sas = client.key_verifications[event.transaction_id]

            todevice_msg = sas.share_key()
            resp = await client.to_device(todevice_msg)
            if isinstance(resp, ToDeviceError):
                estr = f"to_device() failed with {resp}"
                print(estr)
                LOGGER.info(estr)

        elif isinstance(event, KeyVerificationCancel):  # anytime
            """at any time: receive KeyVerificationCancel
            KeyVerificationCancel(source={
                'content': {'code': 'm.mismatched_sas',
                            'reason': 'Mismatched authentication string',
                            'transaction_id': 'SomeTxId'},
                'type': 'm.key.verification.cancel',
                'sender': '@user2:example.org'},
                sender='@user2:example.org',
                transaction_id='SomeTxId',
                code='m.mismatched_sas',
                reason='Mismatched short authentication string')
            """

            # There is no need to issue a
            # client.cancel_key_verification(tx_id, reject=False)
            # here. The SAS flow is already cancelled.
            # We only need to inform the user.
            estr = (
                f"Verification has been cancelled by {event.sender} "
                f'for reason "{event.reason}".'
            )
            print(estr)
            LOGGER.info(estr)

        elif isinstance(event, KeyVerificationKey):  # second step
            """Second step is to receive KeyVerificationKey
            KeyVerificationKey(
                source={'content': {
                        'key': 'SomeCryptoKey',
                        'transaction_id': 'SomeTxId'},
                    'type': 'm.key.verification.key',
                    'sender': '@user2:example.org'
                },
                sender='@user2:example.org',
                transaction_id='SomeTxId',
                key='SomeCryptoKey')
            """
            sas = client.key_verifications[event.transaction_id]

            print(f"{sas.get_emoji()}")
            # don't log the emojis

            # The bot process must run in forground with a screen and
            # keyboard so that user can accept/reject via keyboard.
            # For emoji verification bot must not run as service or
            # in background.
            yn = input("Do the emojis match? (Y/N) (C for Cancel) ")
            if yn.lower() == "y":
                estr = (
                    "Match! The verification for this " "device will be accepted."
                )
                print(estr)
                LOGGER.info(estr)
                resp = await client.confirm_short_auth_string(event.transaction_id)
                if isinstance(resp, ToDeviceError):
                    estr = "confirm_short_auth_string() " f"failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)
            elif yn.lower() == "n":  # no, don't match, reject
                estr = (
                    "No match! Device will NOT be verified "
                    "by rejecting verification."
                )
                print(estr)
                LOGGER.info(estr)
                resp = await client.cancel_key_verification(
                    event.transaction_id, reject=True
                )
                if isinstance(resp, ToDeviceError):
                    estr = f"cancel_key_verification failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)
            else:  # C or anything for cancel
                estr = "Cancelled by user! Verification will be " "cancelled."
                print(estr)
                LOGGER.info(estr)
                resp = await client.cancel_key_verification(
                    event.transaction_id, reject=False
                )
                if isinstance(resp, ToDeviceError):
                    estr = f"cancel_key_verification failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)

        elif isinstance(event, KeyVerificationMac):  # third step
            """Third step is to receive KeyVerificationMac
            KeyVerificationMac(
                source={'content': {
                    'mac': {'ed25519:DEVICEIDXY': 'SomeKey1',
                            'ed25519:SomeKey2': 'SomeKey3'},
                    'keys': 'SomeCryptoKey4',
                    'transaction_id': 'SomeTxId'},
                    'type': 'm.key.verification.mac',
                    'sender': '@user2:example.org'},
                sender='@user2:example.org',
                transaction_id='SomeTxId',
                mac={'ed25519:DEVICEIDXY': 'SomeKey1',
                     'ed25519:SomeKey2': 'SomeKey3'},
                keys='SomeCryptoKey4')
            """
            sas = client.key_verifications[event.transaction_id]
            try:
                todevice_msg = sas.get_mac()
            except LocalProtocolError as e:
                # e.g. it might have been cancelled by ourselves
                estr = (
                    f"Cancelled or protocol error: Reason: {e}.\n"
                    f"Verification with {event.sender} not concluded. "
                    "Try again?"
                )
                print(estr)
                LOGGER.info(estr)
            else:
                resp = await client.to_device(todevice_msg)
                if isinstance(resp, ToDeviceError):
                    estr = f"to_device failed with {resp}"
                    print(estr)
                    LOGGER.info(estr)
                estr = (
                    f"sas.we_started_it = {sas.we_started_it}\n"
                    f"sas.sas_accepted = {sas.sas_accepted}\n"
                    f"sas.canceled = {sas.canceled}\n"
                    f"sas.timed_out = {sas.timed_out}\n"
                    f"sas.verified = {sas.verified}\n"
                    f"sas.verified_devices = {sas.verified_devices}\n"
                )
                print(estr)
                LOGGER.info(estr)
                estr = (
                    "Emoji verification was successful!\n"
                    "Initiate another Emoji verification from "
                    "another device or room if desired. "
                    "Or if done verifying, hit Control-C to stop the "
                    "bot in order to restart it as a service or to "
                    "run it in the background."
                )
                print(estr)
                LOGGER.info(estr)
        else:
            estr = (
                f"Received unexpected event type {type(event)}. "
                f"Event is {event}. Event will be ignored."
            )
            print(estr)
            LOGGER.info(estr)
    except BaseException:
        estr = traceback.format_exc()
        print(estr)
        LOGGER.info(estr)