Exemple #1
0
 def get_id_from_mxid(cls, mxid: UserID) -> Optional[Address]:
     identifier = cls.mxid_template.parse(mxid)
     if not identifier:
         return None
     if identifier.startswith("phone_"):
         return Address(number="+" + identifier[len("phone_"):])
     else:
         try:
             return Address(uuid=UUID(identifier.upper()))
         except ValueError:
             return None
Exemple #2
0
 async def status(self, request: web.Request) -> web.Response:
     user = await self.check_token(request)
     data = {
         "permissions": user.permission_level,
         "mxid": user.mxid,
         "signal": None,
     }
     if await user.is_logged_in():
         try:
             profile = await self.bridge.signal.get_profile(
                 username=user.username,
                 address=Address(number=user.username))
         except Exception as e:
             self.log.exception(
                 f"Failed to get {user.username}'s profile for whoami")
             data["signal"] = {
                 "number": user.username,
                 "ok": False,
                 "error": str(e),
             }
         else:
             addr = profile.address if profile else None
             number = addr.number if addr else None
             uuid = addr.uuid if addr else None
             data["signal"] = {
                 "number": number or user.username,
                 "uuid": str(uuid or user.uuid or ""),
                 "name": profile.name if profile else None,
                 "ok": True,
             }
     return web.json_response(data, headers=self._acao_headers)
Exemple #3
0
async def signal_to_matrix(message: MessageData) -> TextMessageEventContent:
    content = TextMessageEventContent(msgtype=MessageType.TEXT,
                                      body=message.body)
    surrogated_text = add_surrogate(message.body)
    if message.mentions:
        text_chunks = []
        html_chunks = []
        last_offset = 0
        for mention in message.mentions:
            before = surrogated_text[last_offset:mention.start]
            last_offset = mention.start + mention.length

            text_chunks.append(before)
            html_chunks.append(escape(before))
            puppet = await pu.Puppet.get_by_address(Address(uuid=mention.uuid))
            name = add_surrogate(puppet.name or puppet.mxid)
            text_chunks.append(name)
            html_chunks.append(
                f'<a href="https://matrix.to/#/{puppet.mxid}">{name}</a>')
        end = surrogated_text[last_offset:]
        text_chunks.append(end)
        html_chunks.append(escape(end))
        content.body = del_surrogate("".join(text_chunks))
        content.format = Format.HTML
        content.formatted_body = del_surrogate("".join(html_chunks))
    return content
Exemple #4
0
    async def handle_matrix_reaction(self, sender: 'u.User', event_id: EventID,
                                     reacting_to: EventID, emoji: str) -> None:
        # Signal doesn't seem to use variation selectors at all
        emoji = emoji.rstrip("\ufe0f")

        message = await DBMessage.get_by_mxid(reacting_to, self.mxid)
        if not message:
            self.log.debug(f"Ignoring reaction to unknown event {reacting_to}")
            return

        existing = await DBReaction.get_by_signal_id(self.chat_id,
                                                     self.receiver,
                                                     message.sender,
                                                     message.timestamp,
                                                     sender.uuid)
        if existing and existing.emoji == emoji:
            return

        dedup_id = (message.sender, message.timestamp, emoji)
        self._reaction_dedup.appendleft(dedup_id)
        async with self._reaction_lock:
            reaction = Reaction(emoji=emoji,
                                remove=False,
                                target_author=Address(uuid=message.sender),
                                target_sent_timestamp=message.timestamp)
            await self.signal.react(username=sender.username,
                                    recipient=self.recipient,
                                    reaction=reaction)
            await self._upsert_reaction(existing, self.main_intent, event_id,
                                        sender, message, emoji)
            self.log.trace(
                f"{sender.mxid} reacted to {message.timestamp} with {emoji}")
        await self._send_delivery_receipt(event_id)
Exemple #5
0
    async def handle_receipt(self, evt: ReceiptEvent) -> None:
        # These events come from custom puppet syncing, so there's always only one user.
        event_id, receipts = evt.content.popitem()
        receipt_type, users = receipts.popitem()
        user_id, data = users.popitem()

        user = await u.User.get_by_mxid(user_id, create=False)
        if not user or not user.username:
            return

        portal = await po.Portal.get_by_mxid(evt.room_id)
        if not portal:
            return

        message = await DBMessage.get_by_mxid(event_id, portal.mxid)
        if not message:
            return

        user.log.trace(
            f"Sending read receipt for {message.timestamp} to {message.sender}"
        )
        await self.signal.mark_read(user.username,
                                    Address(uuid=message.sender),
                                    timestamps=[message.timestamp],
                                    when=data.ts)
Exemple #6
0
 async def _handle_uuid_receive(self, uuid: UUID) -> None:
     self.log.debug(f"Found UUID for user: {uuid}")
     user = await u.User.get_by_username(self.number)
     if user and not user.uuid:
         user.uuid = self.uuid
         user.by_uuid[user.uuid] = user
         await user.update()
     self.uuid = uuid
     self.by_uuid[self.uuid] = self
     await self._set_uuid(uuid)
     async for portal in p.Portal.find_private_chats_with(Address(number=self.number)):
         self.log.trace(f"Updating chat_id of private chat portal {portal.receiver}")
         portal.handle_uuid_receive(self.uuid)
     prev_intent = self.default_mxid_intent
     self.default_mxid = self.get_mxid_from_id(self.address)
     self.default_mxid_intent = self.az.intent.user(self.default_mxid)
     self.intent = self._fresh_intent()
     await self.default_mxid_intent.ensure_registered()
     if self.name:
         await self.default_mxid_intent.set_displayname(self.name)
     self.log = Puppet.log.getChild(str(uuid))
     self.log.debug(f"Migrating memberships {prev_intent.mxid}"
                    f" -> {self.default_mxid_intent.mxid}")
     try:
         joined_rooms = await prev_intent.get_joined_rooms()
     except MForbidden as e:
         self.log.debug(f"Got MForbidden ({e.message}) when getting joined rooms of old mxid, "
                        "assuming there are no rooms to rejoin")
         return
     for room_id in joined_rooms:
         await prev_intent.invite_user(room_id, self.default_mxid)
         await prev_intent.leave_room(room_id)
         await self.default_mxid_intent.join_room_by_id(room_id)
Exemple #7
0
 def handle_uuid_receive(self, uuid: UUID) -> None:
     if not self.is_direct or self.chat_id.uuid:
         raise ValueError(
             "handle_uuid_receive can only be used for private chat portals with "
             "a phone number chat_id")
     del self.by_chat_id[(self.chat_id_str, self.receiver)]
     self.chat_id = Address(uuid=uuid)
     self.by_chat_id[(self.chat_id_str, self.receiver)] = self
Exemple #8
0
async def mark_trusted(evt: CommandEvent) -> None:
    number = evt.args[0]
    safety_num = "".join(evt.args[1:]).replace("\n", "")
    if len(safety_num) != 60 or not safety_num.isdecimal():
        await evt.reply("That doesn't look like a valid safety number")
        return
    msg = await evt.bridge.signal.trust(evt.sender.username, Address(number=number),
                                        fingerprint=safety_num, trust_level="TRUSTED_VERIFIED")
    await evt.reply(msg)
Exemple #9
0
 async def _postinit(self) -> None:
     self.by_chat_id[(self.chat_id, self.receiver)] = self
     if self.mxid:
         self.by_mxid[self.mxid] = self
     if self.is_direct:
         puppet = await p.Puppet.get_by_address(Address(uuid=self.chat_id))
         self._main_intent = puppet.default_mxid_intent
     elif not self.is_direct:
         self._main_intent = self.az.intent
Exemple #10
0
async def _get_puppet_from_cmd(evt: CommandEvent) -> Optional['pu.Puppet']:
    if len(evt.args) == 0 or not evt.args[0].startswith("+"):
        await evt.reply(f"**Usage:** `$cmdprefix+sp {evt.command} <phone>` "
                        "(enter phone number in international format)")
        return None
    phone = "".join(evt.args).translate(remove_extra_chars)
    if not phone[1:].isdecimal():
        await evt.reply(f"**Usage:** `$cmdprefix+sp {evt.command} <phone>` "
                        "(enter phone number in international format)")
        return None
    return await pu.Puppet.get_by_address(Address(number=phone))
Exemple #11
0
    async def handle_matrix_message(self, sender: 'u.User',
                                    message: MessageEventContent,
                                    event_id: EventID) -> None:
        if ((message.get(self.bridge.real_user_content_key, False)
             and await p.Puppet.get_by_custom_mxid(sender.mxid))):
            self.log.debug(
                f"Ignoring puppet-sent message by confirmed puppet user {sender.mxid}"
            )
            return
        request_id = int(time.time() * 1000)
        self._msgts_dedup.appendleft((sender.uuid, request_id))

        quote = None
        if message.get_reply_to():
            reply = await DBMessage.get_by_mxid(message.get_reply_to(),
                                                self.mxid)
            # TODO include actual text? either store in db or fetch event from homeserver
            quote = Quote(id=reply.timestamp,
                          author=Address(uuid=reply.sender),
                          text="")

        text = message.body
        attachments: Optional[List[Attachment]] = None
        attachment_path: Optional[str] = None
        if message.msgtype == MessageType.EMOTE:
            text = f"/me {text}"
        elif message.msgtype.is_media:
            attachment_path = await self._download_matrix_media(message)
            attachment = self._make_attachment(message, attachment_path)
            attachments = [attachment]
            text = None
            self.log.trace("Formed outgoing attachment %s", attachment)
        await self.signal.send(username=sender.username,
                               recipient=self.recipient,
                               body=text,
                               quote=quote,
                               attachments=attachments,
                               timestamp=request_id)
        msg = DBMessage(mxid=event_id,
                        mx_room=self.mxid,
                        sender=sender.uuid,
                        timestamp=request_id,
                        signal_chat_id=self.chat_id,
                        signal_receiver=self.receiver)
        await msg.insert()
        await self._send_delivery_receipt(event_id)
        self.log.debug(f"Handled Matrix message {event_id} -> {request_id}")
        if attachment_path and self.config["signal.remove_file_after_handling"]:
            try:
                os.remove(attachment_path)
            except FileNotFoundError:
                pass
Exemple #12
0
async def mark_trusted(evt: CommandEvent) -> EventID:
    if len(evt.args) < 2:
        return await evt.reply("**Usage:** `$cmdprefix+sp mark-trusted <recipient phone> "
                               "<safety number>`")
    number = evt.args[0].translate(remove_extra_chars)
    safety_num = "".join(evt.args[1:]).replace("\n", "")
    if len(safety_num) != 60 or not safety_num.isdecimal():
        return await evt.reply("That doesn't look like a valid safety number")
    try:
        await evt.bridge.signal.trust(evt.sender.username, Address(number=number),
                                      safety_number=safety_num, trust_level="TRUSTED_VERIFIED")
    except UnknownIdentityKey as e:
        return await evt.reply(f"Failed to mark {number} as trusted: {e}")
    return await evt.reply(f"Successfully marked {number} as trusted")
Exemple #13
0
    async def handle_matrix_redaction(self, sender: 'u.User', event_id: EventID,
                                      redaction_event_id: EventID) -> None:
        if not self.mxid:
            return

        reaction = await DBReaction.get_by_mxid(event_id, self.mxid)
        if reaction:
            try:
                await reaction.delete()
                remove_reaction = Reaction(emoji=reaction.emoji, remove=True,
                                           target_author=Address(uuid=reaction.msg_author),
                                           target_sent_timestamp=reaction.msg_timestamp)
                await self.signal.react(username=sender.username, recipient=self.recipient,
                                        reaction=remove_reaction)
                await self._send_delivery_receipt(redaction_event_id)
                self.log.trace(f"Removed {reaction} after Matrix redaction")
            except Exception:
                self.log.exception("Removing reaction failed")
Exemple #14
0
    async def update_info(self, info: ChatInfo) -> None:
        if self.is_direct:
            if not isinstance(info, (Contact, Profile, Address)):
                raise ValueError(f"Unexpected type for direct chat update_info: {type(info)}")
            if not self.name:
                puppet = await p.Puppet.get_by_address(Address(uuid=self.chat_id))
                if not puppet.name:
                    await puppet.update_info(info)
                self.name = puppet.name
            return

        if not isinstance(info, Group):
            raise ValueError(f"Unexpected type for group update_info: {type(info)}")
        changed = await self._update_name(info.name)
        changed = await self._update_avatar()
        await self._update_participants(info.members)
        if changed:
            await self.update_bridge_info()
            await self.update()
Exemple #15
0
 async def status(self, request: web.Request) -> web.Response:
     user = await self.check_token(request)
     data = {
         "permissions": user.permission_level,
         "mxid": user.mxid,
         "signal": None,
     }
     if await user.is_logged_in():
         profile = await self.bridge.signal.get_profile(
             username=user.username, address=Address(number=user.username))
         addr = profile.address if profile else None
         number = addr.number if addr else None
         uuid = addr.uuid if addr else None
         data["signal"] = {
             "number": number or user.username,
             "uuid": str(uuid or user.uuid or ""),
             "name": profile.name if profile else None,
         }
     return web.json_response(data, headers=self._acao_headers)
Exemple #16
0
 def recipient(self) -> Union[str, Address]:
     if self.is_direct:
         return Address(uuid=self.chat_id)
     else:
         return self.chat_id
Exemple #17
0
 def address(self) -> Optional[Address]:
     if not self.username:
         return None
     return Address(uuid=self.uuid, number=self.username)
Exemple #18
0
 def address(self) -> Address:
     return Address(uuid=self.uuid, number=self.number)