Example #1
0
 def __init__(self, bridge: 'Bridge', homeserver_address: str, user_id_prefix: str,
              user_id_suffix: str, db_url: str, key_sharing_config: Dict[str, bool] = None
              ) -> None:
     self.loop = bridge.loop or asyncio.get_event_loop()
     self.bridge = bridge
     self.az = bridge.az
     self.device_name = bridge.name
     self._id_prefix = user_id_prefix
     self._id_suffix = user_id_suffix
     self._share_session_events = {}
     self.key_sharing_config = key_sharing_config or {}
     pickle_key = "mautrix.bridge.e2ee"
     if db_url.startswith("postgres://"):
         if not PgCryptoStore or not PgCryptoStateStore:
             raise RuntimeError("Database URL is set to postgres, but asyncpg is not installed")
         self.crypto_db = Database(url=db_url, upgrade_table=PgCryptoStore.upgrade_table,
                                   log=logging.getLogger("mau.crypto.db"), loop=self.loop)
         self.crypto_store = PgCryptoStore("", pickle_key, self.crypto_db)
         self.state_store = PgCryptoStateStore(self.crypto_db, bridge.get_portal)
     elif db_url.startswith("pickle:///"):
         self.crypto_db = None
         self.crypto_store = PickleCryptoStore("", pickle_key, db_url[len("pickle:///"):])
         self.state_store = SQLCryptoStateStore(bridge.get_portal)
     else:
         raise RuntimeError("Unsupported database scheme")
     self.client = Client(base_url=homeserver_address, mxid=self.az.bot_mxid, loop=self.loop,
                          sync_store=self.crypto_store)
     self.crypto = OlmMachine(self.client, self.crypto_store, self.state_store)
     self.crypto.allow_key_share = self.allow_key_share
Example #2
0
async def login(evt: CommandEvent) -> EventID:
    override_sender = False
    if len(evt.args) > 0 and evt.sender.is_admin and evt.args[0]:
        override_user_id = UserID(evt.args[0])
        try:
            Client.parse_user_id(override_user_id)
        except ValueError:
            return await evt.reply(
                f"**Usage:** `$cmdprefix+sp login [override user ID]`\n\n"
                f"{override_user_id!r} is not a valid Matrix user ID"
            )
        orig_user_id = evt.sender.mxid
        evt.sender = await u.User.get_and_start_by_mxid(override_user_id)
        override_sender = True
        if orig_user_id != evt.sender:
            await evt.reply(
                f"Admin override: logging in as {evt.sender.mxid} instead of {orig_user_id}"
            )

    if await evt.sender.is_logged_in():
        return await evt.reply(f"You are already logged in as {evt.sender.human_tg_id}.")

    allow_matrix_login = evt.config["bridge.allow_matrix_login"]
    if allow_matrix_login and not override_sender:
        evt.sender.command_status = {
            "next": enter_phone_or_token,
            "action": "Login",
        }

    nb = "**N.B. Logging in grants the bridge full access to your Telegram account.**"
    if evt.config["appservice.public.enabled"]:
        prefix = evt.config["appservice.public.external"]
        url = f"{prefix}/login?token={evt.public_website.make_token(evt.sender.mxid, '/login')}"
        if override_sender:
            return await evt.reply(
                f"[Click here to log in]({url}) as "
                f"[{evt.sender.mxid}](https://matrix.to/#/{evt.sender.mxid})."
            )
        elif allow_matrix_login:
            return await evt.reply(
                f"[Click here to log in]({url}). Alternatively, send your phone"
                f" number (or bot auth token) here to log in.\n\n{nb}"
            )
        return await evt.reply(f"[Click here to log in]({url}).\n\n{nb}")
    elif allow_matrix_login:
        if override_sender:
            return await evt.reply(
                "This bridge instance does not allow you to log in outside of Matrix. "
                "Logging in as another user inside Matrix is not currently possible."
            )
        return await evt.reply(
            "Please send your phone number (or bot auth token) here to start "
            f"the login process.\n\n{nb}"
        )
    return await evt.reply("This bridge instance has been configured to not allow logging in.")
Example #3
0
async def exchange_token(request: web.Request) -> web.Response:
    try:
        data: 'OpenIDPayload' = await request.json()
    except json.JSONDecodeError:
        raise Error.request_not_json
    if "access_token" not in data or "matrix_server_name" not in data:
        raise Error.invalid_openid_payload
    try:
        user_id = await check_openid_token(data["access_token"])
        _, homeserver = Client.parse_user_id(user_id)
    except (ClientError, json.JSONDecodeError, KeyError, ValueError):
        raise Error.invalid_openid_token
    if homeserver != data["matrix_server_name"]:
        raise Error.homeserver_mismatch
    permissions = config.get_permissions(user_id)
    if not permissions.user:
        raise Error.no_access
    token = Token.random(user_id)
    await token.insert()
    return web.json_response(
        {
            "user_id": user_id,
            "token": token.secret,
            "level": permissions.level,
            "permissions": {
                "docker": permissions.admin,
            },
        },
        headers=account_cors_headers)
Example #4
0
    def get_permissions(self, mxid: UserID) -> Permissions:
        permissions = self["bridge.permissions"]
        if mxid in permissions:
            return self._get_permissions(mxid)

        _, homeserver = Client.parse_user_id(mxid)
        if homeserver in permissions:
            return self._get_permissions(homeserver)

        return self._get_permissions("*")
Example #5
0
 def __init__(
     self,
     bridge: br.Bridge,
     homeserver_address: str,
     user_id_prefix: str,
     user_id_suffix: str,
     db_url: str,
     key_sharing_config: dict[str, bool] = None,
 ) -> None:
     self.loop = bridge.loop or asyncio.get_event_loop()
     self.bridge = bridge
     self.az = bridge.az
     self.device_name = bridge.name
     self._id_prefix = user_id_prefix
     self._id_suffix = user_id_suffix
     self._share_session_events = {}
     self.key_sharing_config = key_sharing_config or {}
     pickle_key = "mautrix.bridge.e2ee"
     self.crypto_db = Database.create(
         url=db_url,
         upgrade_table=PgCryptoStore.upgrade_table,
         log=logging.getLogger("mau.crypto.db"),
     )
     self.crypto_store = PgCryptoStore("", pickle_key, self.crypto_db)
     self.state_store = PgCryptoStateStore(self.crypto_db,
                                           bridge.get_portal)
     default_http_retry_count = bridge.config.get(
         "homeserver.http_retry_count", None)
     self.client = Client(
         base_url=homeserver_address,
         mxid=self.az.bot_mxid,
         loop=self.loop,
         sync_store=self.crypto_store,
         log=self.log.getChild("client"),
         default_retry_count=default_http_retry_count,
     )
     self.crypto = OlmMachine(self.client, self.crypto_store,
                              self.state_store)
     self.client.add_event_handler(InternalEventType.SYNC_STOPPED,
                                   self._exit_on_sync_fail)
     self.crypto.allow_key_share = self.allow_key_share
Example #6
0
 async def _create_community(self) -> None:
     template = self.config["bridge.community_template"]
     if not template:
         return
     localpart, server = MxClient.parse_user_id(self.mxid)
     community_localpart = template.format(localpart=localpart, server=server)
     self.log.debug(f"Creating personal filtering community {community_localpart}...")
     self._community_id, created = await self._community_helper.create(community_localpart)
     if created:
         await self._community_helper.update(self._community_id, name="Facebook Messenger",
                                             avatar_url=self.config["appservice.bot_avatar"],
                                             short_desc="Your Facebook bridged chats")
         await self._community_helper.invite(self._community_id, self.mxid)
Example #7
0
    def __init__(self, mconf: Optional[MirkConfig] = None):
        self._mconf = mconf
        if not self._mconf:
            self._mconf = MirkConfig()
            self._mconf.load()

        self._joined_rooms: list[str] = []
        self._client = MatrixClient(mxid=self._mconf.username,
                                    base_url=self._mconf.host)
        self._client.add_event_handler(EventType.ROOM_MEMBER,
                                       self._handle_invite)
        self._client.add_event_handler(EventType.ROOM_MESSAGE,
                                       self._handle_message)
Example #8
0
async def login_matrix(evt: CommandEvent) -> None:
    puppet = await pu.Puppet.get_by_address(evt.sender.address)
    _, homeserver = Client.parse_mxid(evt.sender.mxid)
    if homeserver != pu.Puppet.hs_domain:
        await evt.reply("You can't log in with an account on a different homeserver")
        return
    try:
        await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
        await evt.reply("Successfully replaced your Signal account's "
                        "Matrix puppet with your Matrix account.")
    except cpu.OnlyLoginSelf:
        await evt.reply("You may only log in with your own Matrix account")
    except cpu.InvalidAccessToken:
        await evt.reply("Invalid access token")
Example #9
0
class MirkMatrixClient:
    ''' Handle interactions with Matrix for Laniakea's "Mirk" Matrix bot. '''
    def __init__(self, mconf: Optional[MirkConfig] = None):
        self._mconf = mconf
        if not self._mconf:
            self._mconf = MirkConfig()
            self._mconf.load()

        self._joined_rooms: list[str] = []
        self._client = MatrixClient(mxid=self._mconf.username,
                                    base_url=self._mconf.host)
        self._client.add_event_handler(EventType.ROOM_MEMBER,
                                       self._handle_invite)
        self._client.add_event_handler(EventType.ROOM_MESSAGE,
                                       self._handle_message)

    async def _handle_invite(self, event) -> None:
        if event.content.membership == Membership.INVITE:
            await self._client.join_room(event.room_id)

    async def _handle_message(self, event: MessageEvent) -> None:
        if event.sender != self._client.mxid:
            pass  # we don't reply to anything for now

    async def login(self):
        ''' Log into Matrix as the currently selected bot user, using password authentication '''
        log.debug('Logging into Matrix')
        await self._client.login(password=self._mconf.password)
        log.info('Logged into Matrix as %s', await self._client.whoami())

        self._joined_rooms = await self._client.get_joined_rooms()
        log.info('Publishing in rooms: %s', ' '.join(self._joined_rooms))

    def stop(self):
        self._client.stop()

    async def send_simple_text(self, room_id: RoomID, text: str):
        ''' Publish a simple text message in the selected room. '''
        # pylint: disable=unexpected-keyword-arg
        await self._client.send_message(room_id=room_id,
                                        content=TextMessageEventContent(
                                            msgtype=MessageType.TEXT,
                                            body=text))

    async def send_simple_html(self, room_id: RoomID, html: str):
        ''' Publish a simple HTML message in the selected room. '''
        from mautrix.types import Format
        # pylint: disable=unexpected-keyword-arg
        content = TextMessageEventContent(msgtype=MessageType.TEXT)
        content.format = Format.HTML
        content.formatted_body = html
        await self._client.send_message(room_id=room_id, content=content)
Example #10
0
class EncryptionManager:
    loop: asyncio.AbstractEventLoop
    log: TraceLogger = logging.getLogger("mau.bridge.e2ee")

    client: Client
    crypto: OlmMachine
    crypto_store: CryptoStore | SyncStore
    crypto_db: Database | None
    state_store: StateStore

    bridge: br.Bridge
    az: AppService
    _id_prefix: str
    _id_suffix: str

    _share_session_events: dict[RoomID, asyncio.Event]

    def __init__(
        self,
        bridge: br.Bridge,
        homeserver_address: str,
        user_id_prefix: str,
        user_id_suffix: str,
        db_url: str,
        key_sharing_config: dict[str, bool] = None,
    ) -> None:
        self.loop = bridge.loop or asyncio.get_event_loop()
        self.bridge = bridge
        self.az = bridge.az
        self.device_name = bridge.name
        self._id_prefix = user_id_prefix
        self._id_suffix = user_id_suffix
        self._share_session_events = {}
        self.key_sharing_config = key_sharing_config or {}
        pickle_key = "mautrix.bridge.e2ee"
        self.crypto_db = Database.create(
            url=db_url,
            upgrade_table=PgCryptoStore.upgrade_table,
            log=logging.getLogger("mau.crypto.db"),
        )
        self.crypto_store = PgCryptoStore("", pickle_key, self.crypto_db)
        self.state_store = PgCryptoStateStore(self.crypto_db,
                                              bridge.get_portal)
        default_http_retry_count = bridge.config.get(
            "homeserver.http_retry_count", None)
        self.client = Client(
            base_url=homeserver_address,
            mxid=self.az.bot_mxid,
            loop=self.loop,
            sync_store=self.crypto_store,
            log=self.log.getChild("client"),
            default_retry_count=default_http_retry_count,
        )
        self.crypto = OlmMachine(self.client, self.crypto_store,
                                 self.state_store)
        self.client.add_event_handler(InternalEventType.SYNC_STOPPED,
                                      self._exit_on_sync_fail)
        self.crypto.allow_key_share = self.allow_key_share

    async def _exit_on_sync_fail(self, data) -> None:
        if data["error"]:
            self.log.critical("Exiting due to crypto sync error")
            sys.exit(32)

    async def allow_key_share(self, device: DeviceIdentity,
                              request: RequestedKeyInfo) -> bool:
        require_verification = self.key_sharing_config.get(
            "require_verification", True)
        allow = self.key_sharing_config.get("allow", False)
        if not allow:
            self.log.debug(
                f"Key sharing not enabled, ignoring key request from "
                f"{device.user_id}/{device.device_id}")
            return False
        elif device.trust == TrustState.BLACKLISTED:
            raise RejectKeyShare(
                f"Rejecting key request from blacklisted device "
                f"{device.user_id}/{device.device_id}",
                code=RoomKeyWithheldCode.BLACKLISTED,
                reason="You have been blacklisted by this device",
            )
        elif device.trust == TrustState.VERIFIED or not require_verification:
            portal = await self.bridge.get_portal(request.room_id)
            if portal is None:
                raise RejectKeyShare(
                    f"Rejecting key request for {request.session_id} from "
                    f"{device.user_id}/{device.device_id}: room is not a portal",
                    code=RoomKeyWithheldCode.UNAVAILABLE,
                    reason="Requested room is not a portal",
                )
            user = await self.bridge.get_user(device.user_id)
            if not await user.is_in_portal(portal):
                raise RejectKeyShare(
                    f"Rejecting key request for {request.session_id} from "
                    f"{device.user_id}/{device.device_id}: user is not in portal",
                    code=RoomKeyWithheldCode.UNAUTHORIZED,
                    reason="You're not in that portal",
                )
            self.log.debug(
                f"Accepting key request for {request.session_id} from "
                f"{device.user_id}/{device.device_id}")
            return True
        else:
            raise RejectKeyShare(
                f"Rejecting key request from unverified device "
                f"{device.user_id}/{device.device_id}",
                code=RoomKeyWithheldCode.UNVERIFIED,
                reason="You have not been verified by this device",
            )

    def _ignore_user(self, user_id: str) -> bool:
        return (user_id.startswith(self._id_prefix)
                and user_id.endswith(self._id_suffix)
                and user_id != self.az.bot_mxid)

    async def handle_member_event(self, evt: StateEvent) -> None:
        if self._ignore_user(evt.state_key):
            # We don't want to invalidate group sessions because a ghost left or joined
            return
        await self.crypto.handle_member_event(evt)

    async def _share_session_lock(self, room_id: RoomID) -> bool:
        try:
            event = self._share_session_events[room_id]
        except KeyError:
            self._share_session_events[room_id] = asyncio.Event()
            return True
        else:
            await event.wait()
            return False

    async def encrypt(
        self, room_id: RoomID, event_type: EventType,
        content: Serializable | JSON
    ) -> tuple[EventType, EncryptedMegolmEventContent]:
        try:
            encrypted = await self.crypto.encrypt_megolm_event(
                room_id, event_type, content)
        except EncryptionError:
            self.log.debug(
                "Got EncryptionError, sharing group session and trying again")
            if await self._share_session_lock(room_id):
                try:
                    users = await self.az.state_store.get_members_filtered(
                        room_id, self._id_prefix, self._id_suffix,
                        self.az.bot_mxid)
                    await self.crypto.share_group_session(room_id, users)
                finally:
                    self._share_session_events.pop(room_id).set()
            encrypted = await self.crypto.encrypt_megolm_event(
                room_id, event_type, content)
        return EventType.ROOM_ENCRYPTED, encrypted

    async def decrypt(self,
                      evt: EncryptedEvent,
                      wait_session_timeout: int = 5) -> MessageEvent:
        try:
            decrypted = await self.crypto.decrypt_megolm_event(evt)
        except SessionNotFound as e:
            if not wait_session_timeout:
                raise
            self.log.debug(
                f"Couldn't find session {e.session_id} trying to decrypt {evt.event_id},"
                f" waiting {wait_session_timeout} seconds...")
            got_keys = await self.crypto.wait_for_session(
                evt.room_id,
                e.sender_key,
                e.session_id,
                timeout=wait_session_timeout)
            if got_keys:
                self.log.debug(f"Got session {e.session_id} after waiting, "
                               f"trying to decrypt {evt.event_id} again")
                decrypted = await self.crypto.decrypt_megolm_event(evt)
            else:
                raise
        self.log.trace("Decrypted event %s: %s", evt.event_id, decrypted)
        return decrypted

    async def start(self) -> None:
        flows = await self.client.get_login_flows()
        if not flows.supports_type(LoginType.APPSERVICE):
            self.log.critical(
                "Encryption enabled in config, but homeserver does not support appservice login"
            )
            sys.exit(30)
        self.log.debug("Logging in with bridge bot user")
        if self.crypto_db:
            try:
                await self.crypto_db.start()
            except Exception as e:
                self.bridge._log_db_error(e)
        await self.crypto_store.open()
        device_id = await self.crypto_store.get_device_id()
        if device_id:
            self.log.debug(f"Found device ID in database: {device_id}")
        # We set the API token to the AS token here to authenticate the appservice login
        # It'll get overridden after the login
        self.client.api.token = self.az.as_token
        await self.client.login(
            login_type=LoginType.APPSERVICE,
            device_name=self.device_name,
            device_id=device_id,
            store_access_token=True,
            update_hs_url=False,
        )
        await self.crypto.load()
        if not device_id:
            await self.crypto_store.put_device_id(self.client.device_id)
            self.log.debug(
                f"Logged in with new device ID {self.client.device_id}")
        _ = self.client.start(self._filter)
        self.log.info("End-to-bridge encryption support is enabled")

    async def stop(self) -> None:
        self.client.stop()
        await self.crypto_store.close()
        if self.crypto_db:
            await self.crypto_db.stop()

    @property
    def _filter(self) -> Filter:
        all_events = EventType.find("*")
        return Filter(
            account_data=EventFilter(types=[all_events]),
            presence=EventFilter(not_types=[all_events]),
            room=RoomFilter(
                include_leave=False,
                state=StateFilter(not_types=[all_events]),
                timeline=RoomEventFilter(not_types=[all_events]),
                account_data=RoomEventFilter(not_types=[all_events]),
                ephemeral=RoomEventFilter(not_types=[all_events]),
            ),
        )
Example #11
0
 def mxid_localpart(self) -> str:
     localpart, server = Client.parse_user_id(self.mxid)
     return localpart
Example #12
0
class EncryptionManager:
    loop: asyncio.AbstractEventLoop
    log: TraceLogger = logging.getLogger("mau.bridge.e2ee")

    client: Client
    crypto: OlmMachine
    crypto_store: Union[CryptoStore, SyncStore]
    crypto_db: Optional[Database]
    state_store: StateStore

    bridge: 'Bridge'
    az: AppService
    login_shared_secret: bytes
    _id_prefix: str
    _id_suffix: str

    sync_task: asyncio.Future
    _share_session_events: Dict[RoomID, asyncio.Event]

    def __init__(self,
                 bridge: 'Bridge',
                 login_shared_secret: str,
                 homeserver_address: str,
                 user_id_prefix: str,
                 user_id_suffix: str,
                 db_url: str,
                 key_sharing_config: Dict[str, bool] = None) -> None:
        self.loop = bridge.loop or asyncio.get_event_loop()
        self.bridge = bridge
        self.az = bridge.az
        self.device_name = bridge.name
        self._id_prefix = user_id_prefix
        self._id_suffix = user_id_suffix
        self.login_shared_secret = login_shared_secret.encode("utf-8")
        self._share_session_events = {}
        self.key_sharing_config = key_sharing_config or {}
        pickle_key = "mautrix.bridge.e2ee"
        if db_url.startswith("postgres://"):
            if not PgCryptoStore or not PgCryptoStateStore:
                raise RuntimeError(
                    "Database URL is set to postgres, but asyncpg is not installed"
                )
            self.crypto_db = Database(
                url=db_url,
                upgrade_table=PgCryptoStore.upgrade_table,
                log=logging.getLogger("mau.crypto.db"),
                loop=self.loop)
            self.crypto_store = PgCryptoStore("", pickle_key, self.crypto_db)
            self.state_store = PgCryptoStateStore(self.crypto_db,
                                                  bridge.get_portal)
        elif db_url.startswith("pickle:///"):
            self.crypto_db = None
            self.crypto_store = PickleCryptoStore("", pickle_key,
                                                  db_url[len("pickle:///"):])
            self.state_store = SQLCryptoStateStore(bridge.get_portal)
        else:
            raise RuntimeError("Unsupported database scheme")
        self.client = Client(base_url=homeserver_address,
                             mxid=self.az.bot_mxid,
                             loop=self.loop,
                             sync_store=self.crypto_store)
        self.crypto = OlmMachine(self.client, self.crypto_store,
                                 self.state_store)
        self.crypto.allow_key_share = self.allow_key_share

    async def allow_key_share(self, device: DeviceIdentity,
                              request: RequestedKeyInfo) -> bool:
        require_verification = self.key_sharing_config.get(
            "require_verification", True)
        allow = self.key_sharing_config.get("allow", False)
        if not allow:
            self.log.debug(
                f"Key sharing not enabled, ignoring key request from "
                f"{device.user_id}/{device.device_id}")
            return False
        elif device.trust == TrustState.BLACKLISTED:
            raise RejectKeyShare(
                f"Rejecting key request from blacklisted device "
                f"{device.user_id}/{device.device_id}",
                code=RoomKeyWithheldCode.BLACKLISTED,
                reason="You have been blacklisted by this device")
        elif device.trust == TrustState.VERIFIED or not require_verification:
            portal = await self.bridge.get_portal(request.room_id)
            if portal is None:
                raise RejectKeyShare(
                    f"Rejecting key request for {request.session_id} from "
                    f"{device.user_id}/{device.device_id}: room is not a portal",
                    code=RoomKeyWithheldCode.UNAVAILABLE,
                    reason="Requested room is not a portal")
            user = await self.bridge.get_user(device.user_id)
            if not await user.is_in_portal(portal):
                raise RejectKeyShare(
                    f"Rejecting key request for {request.session_id} from "
                    f"{device.user_id}/{device.device_id}: user is not in portal",
                    code=RoomKeyWithheldCode.UNAUTHORIZED,
                    reason="You're not in that portal")
            self.log.debug(
                f"Accepting key request for {request.session_id} from "
                f"{device.user_id}/{device.device_id}")
            return True
        else:
            raise RejectKeyShare(
                f"Rejecting key request from unverified device "
                f"{device.user_id}/{device.device_id}",
                code=RoomKeyWithheldCode.UNVERIFIED,
                reason="You have not been verified by this device")

    def _ignore_user(self, user_id: str) -> bool:
        return (user_id.startswith(self._id_prefix)
                and user_id.endswith(self._id_suffix)
                and user_id != self.az.bot_mxid)

    async def handle_member_event(self, evt: StateEvent) -> None:
        if self._ignore_user(evt.state_key):
            # We don't want to invalidate group sessions because a ghost left or joined
            return
        await self.crypto.handle_member_event(evt)

    async def _share_session_lock(self, room_id: RoomID) -> bool:
        try:
            event = self._share_session_events[room_id]
        except KeyError:
            self._share_session_events[room_id] = asyncio.Event()
            return True
        else:
            await event.wait()
            return False

    async def encrypt(
        self, room_id: RoomID, event_type: EventType,
        content: Union[Serializable, JSON]
    ) -> Tuple[EventType, EncryptedMegolmEventContent]:
        try:
            encrypted = await self.crypto.encrypt_megolm_event(
                room_id, event_type, content)
        except EncryptionError:
            self.log.debug(
                "Got EncryptionError, sharing group session and trying again")
            if await self._share_session_lock(room_id):
                try:
                    users = await self.az.state_store.get_members_filtered(
                        room_id, self._id_prefix, self._id_suffix,
                        self.az.bot_mxid)
                    await self.crypto.share_group_session(room_id, users)
                finally:
                    self._share_session_events.pop(room_id).set()
            encrypted = await self.crypto.encrypt_megolm_event(
                room_id, event_type, content)
        return EventType.ROOM_ENCRYPTED, encrypted

    async def decrypt(self, evt: EncryptedEvent) -> MessageEvent:
        decrypted = await self.crypto.decrypt_megolm_event(evt)
        self.log.trace("Decrypted event %s: %s", evt.event_id, decrypted)
        return decrypted

    async def start(self) -> None:
        self.log.debug("Logging in with bridge bot user")
        password = hmac.new(self.login_shared_secret,
                            self.az.bot_mxid.encode("utf-8"),
                            hashlib.sha512).hexdigest()
        if self.crypto_db:
            await self.crypto_db.start()
        await self.crypto_store.open()
        device_id = await self.crypto_store.get_device_id()
        if device_id:
            self.log.debug(f"Found device ID in database: {device_id}")
        await self.client.login(password=password,
                                device_name=self.device_name,
                                device_id=device_id,
                                store_access_token=True,
                                update_hs_url=False)
        await self.crypto.load()
        if not device_id:
            await self.crypto_store.put_device_id(self.client.device_id)
            self.log.debug(
                f"Logged in with new device ID {self.client.device_id}")
        self.sync_task = self.client.start(self._filter)
        self.log.info("End-to-bridge encryption support is enabled")

    async def stop(self) -> None:
        self.sync_task.cancel()
        await self.crypto_store.close()
        if self.crypto_db:
            await self.crypto_db.stop()

    @property
    def _filter(self) -> Filter:
        all_events = EventType.find("*")
        return Filter(
            account_data=EventFilter(types=[all_events]),
            presence=EventFilter(not_types=[all_events]),
            room=RoomFilter(
                include_leave=False,
                state=StateFilter(not_types=[all_events]),
                timeline=RoomEventFilter(not_types=[all_events]),
                account_data=RoomEventFilter(not_types=[all_events]),
                ephemeral=RoomEventFilter(not_types=[all_events]),
            ),
        )
Example #13
0
File: bot.py Project: maubot/karma
class KarmaBot(Plugin):
    karma_t: Type[Karma]
    version: Type[Version]

    async def start(self) -> None:
        await super().start()
        self.config.load_and_update()
        self.karma_t, self.version = make_tables(self.database)

    @command.new("karma", help="View users' karma or karma top lists")
    async def karma(self) -> None:
        pass

    @karma.subcommand("up", help="Upvote an event")
    @command.argument("event_id", "Event ID", required=True)
    def upvote(self, evt: MessageEvent, event_id: EventID) -> Awaitable[None]:
        return self._vote(evt, event_id, +1)

    @karma.subcommand("down", help="Downvote a message")
    @command.argument("event_id", "Event ID", required=True)
    def downvote(self, evt: MessageEvent,
                 event_id: EventID) -> Awaitable[None]:
        return self._vote(evt, event_id, -1)

    @command.passive(UPVOTE)
    def upvote(self, evt: MessageEvent, _: Tuple[str]) -> Awaitable[None]:
        return self._vote(evt, evt.content.get_reply_to(), +1)

    @command.passive(DOWNVOTE)
    def downvote(self, evt: MessageEvent, _: Tuple[str]) -> Awaitable[None]:
        return self._vote(evt, evt.content.get_reply_to(), -1)

    @command.passive(regex=UPVOTE_EMOJI,
                     field=lambda evt: evt.content.relates_to.key,
                     event_type=EventType.REACTION,
                     msgtypes=None)
    def upvote_react(self, evt: ReactionEvent,
                     key: Tuple[str]) -> Awaitable[None]:
        try:
            return self._vote(evt, evt.content.relates_to.event_id, 1)
        except KeyError:
            pass

    @command.passive(regex=DOWNVOTE_EMOJI,
                     field=lambda evt: evt.content.relates_to.key,
                     event_type=EventType.REACTION,
                     msgtypes=None)
    def downvote_react(self, evt: ReactionEvent,
                       key: Tuple[str]) -> Awaitable[None]:
        try:
            return self._vote(evt, evt.content.relates_to.event_id, -1)
        except KeyError:
            pass

    @event.on(EventType.ROOM_REDACTION)
    async def redact(self, evt: RedactionEvent) -> None:
        karma = self.karma_t.get_by_given_from(evt.redacts)
        if karma:
            self.log.debug(
                f"Deleting {karma} due to redaction by {evt.sender}.")
            karma.delete()

    @karma.subcommand("stats", help="View global karma statistics")
    async def karma_stats(self, evt: MessageEvent) -> None:
        await evt.reply("Not yet implemented :(")

    @karma.subcommand("view", help="View your or another users karma")
    @command.argument("user",
                      "user ID",
                      required=False,
                      parser=lambda val: Client.parse_user_id(val)
                      if val else None)
    async def view_karma(self, evt: MessageEvent,
                         user: Optional[Tuple[str, str]]) -> None:
        if user is not None:
            mxid = UserID(f"@{user[0]}:{user[1]}")
            name = f"[{user[0]}](https://matrix.to/#/{mxid})"
            word_have = "has"
            word_to_be = "is"
        else:
            mxid = evt.sender
            name = "You"
            word_have = "have"
            word_to_be = "are"
        karma = self.karma_t.get_karma(mxid)
        if karma is None or karma.total is None:
            await evt.reply(f"{name} {word_have} no karma :(")
            return
        index = self.karma_t.find_index_from_top(mxid)
        await evt.reply(
            f"{name} {word_have} {karma.total} karma "
            f"(+{karma.positive}/-{karma.negative}) "
            f"and {word_to_be} #{index + 1 or '∞'} on the top list.")

    @karma.subcommand("export", help="Export the data of your karma")
    async def export_own_karma(self, evt: MessageEvent) -> None:
        karma_list = [
            karma.to_dict() for karma in self.karma_t.export(evt.sender)
        ]
        data = json.dumps(karma_list).encode("utf-8")
        url = await self.client.upload_media(data,
                                             mime_type="application/json")
        await evt.reply(
            MediaMessageEventContent(msgtype=MessageType.FILE,
                                     body=f"karma-{evt.sender}.json",
                                     url=url,
                                     info=FileInfo(
                                         mimetype="application/json",
                                         size=len(data),
                                     )))

    @karma.subcommand("breakdown", help="View your karma breakdown")
    async def own_karma_breakdown(self, evt: MessageEvent) -> None:
        await evt.reply("Not yet implemented :(")

    @karma.subcommand("top", help="View the highest rated users")
    async def karma_top(self, evt: MessageEvent) -> None:
        await evt.reply(self._karma_user_list("top"))

    @karma.subcommand("bottom", help="View the lowest rated users")
    async def karma_bottom(self, evt: MessageEvent) -> None:
        await evt.reply(self._karma_user_list("bottom"))

    @karma.subcommand("best", help="View the highest rated messages")
    async def karma_best(self, evt: MessageEvent) -> None:
        await evt.reply(self._karma_message_list("best"))

    @karma.subcommand("worst", help="View the lowest rated messages")
    async def karma_worst(self, evt: MessageEvent) -> None:
        await evt.reply(self._karma_message_list("worst"))

    def _parse_content(self, evt: Event) -> str:
        if not self.config["store_content"]:
            return ""
        if isinstance(evt, MessageEvent):
            if evt.content.msgtype in (MessageType.NOTICE, MessageType.TEXT,
                                       MessageType.EMOTE):
                body = evt.content.body
                if evt.content.msgtype == MessageType.EMOTE:
                    body = "/me " + body
                if self.config["store_content"] == "partial":
                    body = body.split("\n")[0]
                    if len(body) > 60:
                        body = body[:50] + " \u2026"
                return body
            name = media_reply_fallback_body_map[evt.content.msgtype]
            return f"[{name}]({self.client.api.get_download_url(evt.content.url)})"
        elif isinstance(evt, StateEvent):
            return "a state event"
        return "an unknown event"

    @staticmethod
    def _sign(value: int) -> str:
        if value > 0:
            return f"+{value}"
        elif value < 0:
            return str(value)
        else:
            return "±0"

    async def _vote(self, evt: MessageEvent, target: EventID,
                    value: int) -> None:
        if not target:
            return
        in_filter = evt.sender in self.config["filter"]
        if self.config["democracy"] == in_filter or sha1(
                evt.sender) in self.config["opt_out"]:
            if self.config["errors.filtered_users"] and isinstance(
                    evt, MessageEvent):
                await evt.reply("Sorry, you're not allowed to vote.")
            return
        if self.karma_t.is_vote_event(target):
            if self.config["errors.vote_on_vote"] and isinstance(
                    evt, MessageEvent):
                await evt.reply("Sorry, you can't vote on votes.")
            return
        karma_target = await self.client.get_event(evt.room_id, target)
        if not karma_target:
            return
        if karma_target.sender == evt.sender and value > 0:
            if self.config["errors.upvote_self"] and isinstance(
                    evt, MessageEvent):
                await evt.reply("Hey! You can't upvote yourself!")
            return
        karma_id = dict(given_to=karma_target.sender,
                        given_by=evt.sender,
                        given_in=evt.room_id,
                        given_for=karma_target.event_id)
        anonymize = sha1(karma_target.sender) in self.config["opt_out"]
        if anonymize:
            karma_id["given_to"] = ""
        existing = self.karma_t.get(**karma_id)
        if existing is not None:
            if existing.value == value:
                if self.config["errors.already_voted"] and isinstance(
                        evt, MessageEvent):
                    await evt.reply(
                        f"You already {self._sign(value)}'d that message.")
                return
            existing.update(new_value=value)
        else:
            karma = self.karma_t(**karma_id,
                                 given_from=evt.event_id,
                                 value=value,
                                 content=self._parse_content(karma_target)
                                 if not anonymize else "")
            karma.insert()
        if isinstance(evt, MessageEvent):
            await evt.mark_read()

    def _denotify(self, mxid: UserID) -> str:
        localpart, _ = self.client.parse_user_id(mxid)
        return "\u2063".join(localpart)

    def _user_link(self, user_id: UserID) -> str:
        if not user_id:
            return "Anonymous"
        return f"[{self._denotify(user_id)}](https://matrix.to/#/{user_id})"

    def _karma_user_list(self, list_type: str) -> Optional[str]:
        if list_type == "top":
            karma_list = self.karma_t.get_top_users()
            message = "#### Highest karma\n\n"
        elif list_type in ("bot", "bottom"):
            karma_list = self.karma_t.get_bottom_users()
            message = "#### Lowest karma\n\n"
        else:
            return None
        message += "\n".join(
            f"{index + 1}. {self._user_link(karma.user_id)}: "
            f"{self._sign(karma.total)} (+{karma.positive}/-{karma.negative})"
            for index, karma in enumerate(karma_list) if karma.user_id)
        return message

    def _message_text(self, index, event) -> str:
        text = (
            f"{index + 1}. [Event](https://matrix.to/#/{event.room_id}/{event.event_id})"
            f" by {self._user_link(event.sender)} with"
            f" {self._sign(event.total)} karma (+{event.positive}/-{event.negative})\n"
        )
        if event.content and self.config["show_content"]:
            text += f"    \n    > {html.escape(event.content)}\n"
        return text

    def _karma_message_list(self, list_type: str) -> Optional[str]:
        if list_type == "best":
            karma_list = self.karma_t.get_best_events()
            message = "#### Best messages\n\n"
        elif list_type == "worst":
            karma_list = self.karma_t.get_worst_events()
            message = "#### Worst messages\n\n"
        else:
            return None
        message += "\n".join(
            self._message_text(index, event)
            for index, event in enumerate(karma_list))
        return message

    @classmethod
    def get_config_class(cls) -> Type[BaseProxyConfig]:
        return Config
Example #14
0
File: sfcb.py Project: maubot/sfcb
 def _validate_user(user: Any) -> bool:
     try:
         Client.parse_user_id(user)
         return True
     except (ValueError, IndexError):
         return False