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
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)
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
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)
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)
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)
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
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)
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
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))
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
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")
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")
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()
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)
def recipient(self) -> Union[str, Address]: if self.is_direct: return Address(uuid=self.chat_id) else: return self.chat_id
def address(self) -> Optional[Address]: if not self.username: return None return Address(uuid=self.uuid, number=self.username)
def address(self) -> Address: return Address(uuid=self.uuid, number=self.number)