async def twitter_to_matrix(message: MessageData) -> TextMessageEventContent: content = TextMessageEventContent(msgtype=MessageType.TEXT, body=message.text, format=Format.HTML, formatted_body=message.text) for entity in reversed(message.entities.all) if message.entities else []: start, end = entity.indices if isinstance(entity, MessageEntityURL): content.body = content.body[: start] + entity.expanded_url + content.body[ end:] content.formatted_body = ( f'{content.formatted_body[:start]}' f'<a href="{entity.expanded_url}">{entity.display_url}</a>' f'{content.formatted_body[end:]}') elif isinstance(entity, MessageEntityUserMention): puppet = await pu.Puppet.get_by_twid(entity.id, create=False) if puppet: user_url = f"https://matrix.to/#/{puppet.mxid}" content.formatted_body = ( f'{content.formatted_body[:start]}' f'<a href="{user_url}">{puppet.name or entity.name}</a>' f'{content.formatted_body[end:]}') else: # Get the sigil (# or $) from the body text = content.formatted_body[start:end][0] + entity.text content.formatted_body = (f'{content.formatted_body[:start]}' f'<font color="#0000ff">{text}</font>' f'{content.formatted_body[end:]}') if content.formatted_body == content.body: content.formatted_body = None content.format = None content.body = html.unescape(content.body) return content
async def _add_forward_header(source: 'AbstractUser', content: TextMessageEventContent, fwd_from: MessageFwdHeader) -> None: if not content.formatted_body or content.format != Format.HTML: content.format = Format.HTML content.formatted_body = escape(content.body) fwd_from_html, fwd_from_text = None, None if isinstance(fwd_from.from_id, PeerUser): user = u.User.get_by_tgid(TelegramID(fwd_from.from_id.user_id)) if user: fwd_from_text = user.displayname or user.mxid fwd_from_html = (f"<a href='https://matrix.to/#/{user.mxid}'>" f"{escape(fwd_from_text)}</a>") if not fwd_from_text: puppet = pu.Puppet.get(TelegramID(fwd_from.from_id.user_id), create=False) if puppet and puppet.displayname: fwd_from_text = puppet.displayname or puppet.mxid fwd_from_html = (f"<a href='https://matrix.to/#/{puppet.mxid}'>" f"{escape(fwd_from_text)}</a>") if not fwd_from_text: try: user = await source.client.get_entity(fwd_from.from_id) if user: fwd_from_text, _ = pu.Puppet.get_displayname(user, False) fwd_from_html = f"<b>{escape(fwd_from_text)}</b>" except (ValueError, RPCError): fwd_from_text = fwd_from_html = "unknown user" elif isinstance(fwd_from.from_id, (PeerChannel, PeerChat)): from_id = (fwd_from.from_id.chat_id if isinstance(fwd_from.from_id, PeerChat) else fwd_from.from_id.channel_id) portal = po.Portal.get_by_tgid(TelegramID(from_id)) if portal and portal.title: fwd_from_text = portal.title if portal.alias: fwd_from_html = (f"<a href='https://matrix.to/#/{portal.alias}'>" f"{escape(fwd_from_text)}</a>") else: fwd_from_html = f"channel <b>{escape(fwd_from_text)}</b>" else: try: channel = await source.client.get_entity(fwd_from.from_id) if channel: fwd_from_text = f"channel {channel.title}" fwd_from_html = f"channel <b>{escape(channel.title)}</b>" except (ValueError, RPCError): fwd_from_text = fwd_from_html = "unknown channel" elif fwd_from.from_name: fwd_from_text = fwd_from.from_name fwd_from_html = f"<b>{escape(fwd_from.from_name)}</b>" else: fwd_from_text = "unknown source" fwd_from_html = f"unknown source" content.body = "\n".join([f"> {line}" for line in content.body.split("\n")]) content.body = f"Forwarded from {fwd_from_text}:\n{content.body}" content.formatted_body = ( f"Forwarded message from {fwd_from_html}<br/>" f"<tg-forward><blockquote>{content.formatted_body}</blockquote></tg-forward>")
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 matrix_to_signal( content: TextMessageEventContent) -> Tuple[str, List[Mention]]: if content.msgtype == MessageType.EMOTE: content.body = f"/me {content.body}" if content.formatted_body: content.formatted_body = f"/me {content.formatted_body}" mentions = [] if content.format == Format.HTML and content.formatted_body: parsed = MatrixParser.parse(add_surrogate(content.formatted_body)) text = del_surrogate(parsed.text) for mention in parsed.entities: mxid = mention.extra_info["user_id"] user = await u.User.get_by_mxid(mxid, create=False) if user and user.uuid: uuid = user.uuid else: puppet = await pu.Puppet.get_by_mxid(mxid, create=False) if puppet: uuid = puppet.uuid else: continue mentions.append( Mention(uuid=uuid, start=mention.offset, length=mention.length)) else: text = content.body return text, mentions
async def _handle_instagram_reel_share(self, source: 'u.User', intent: IntentAPI, item: ThreadItem) -> Optional[EventID]: media = item.reel_share.media prefix_html = None if item.reel_share.type == ReelShareType.REPLY: if item.reel_share.reel_owner_id == source.igpk: prefix = "Replied to your story" else: username = media.user.username prefix = f"Sent @{username}'s story" user_link = f'<a href="https://www.instagram.com/{username}/">@{username}</a>' prefix_html = f"Sent {user_link}'s story" elif item.reel_share.type == ReelShareType.REACTION: prefix = "Reacted to your story" elif item.reel_share.type == ReelShareType.MENTION: if item.reel_share.mentioned_user_id == source.igpk: prefix = "Mentioned you in their story" else: prefix = "You mentioned them in your story" else: self.log.debug(f"Unsupported reel share type {item.reel_share.type}") return None prefix_content = TextMessageEventContent(msgtype=MessageType.NOTICE, body=prefix) if prefix_html: prefix_content.format = Format.HTML prefix_content.formatted_body = prefix_html content = TextMessageEventContent(msgtype=MessageType.TEXT, body=item.reel_share.text) if not content.body and isinstance(media, MediaShareItem): content.body = media.caption.text if media.caption else "" if not content.body: content.body = "<no caption>" await self._send_message(intent, prefix_content, timestamp=item.timestamp // 1000) if isinstance(media, ExpiredMediaItem): # TODO send message about expired story pass else: fake_item_id = f"fi.mau.instagram.reel_share.{item.user_id}.{media.pk}" existing = await DBMessage.get_by_item_id(fake_item_id, self.receiver) if existing: # If the user already reacted or replied to the same reel share item, # use a Matrix reply instead of reposting the image. content.set_reply(existing.mxid) else: media_event_id = await self._handle_instagram_media(source, intent, item) await DBMessage(mxid=media_event_id, mx_room=self.mxid, item_id=fake_item_id, receiver=self.receiver, sender=media.user.pk).insert() return await self._send_message(intent, content, timestamp=item.timestamp // 1000)
async def telegram_to_matrix( evt: Message, source: "AbstractUser", main_intent: Optional[IntentAPI] = None, prefix_text: Optional[str] = None, prefix_html: Optional[str] = None, override_text: str = None, override_entities: List[TypeMessageEntity] = None, no_reply_fallback: bool = False) -> TextMessageEventContent: content = TextMessageEventContent( msgtype=MessageType.TEXT, body=add_surrogate(override_text or evt.message), ) entities = override_entities or evt.entities if entities: content.format = Format.HTML content.formatted_body = _telegram_entities_to_matrix_catch( content.body, entities) if prefix_html: if not content.formatted_body: content.format = Format.HTML content.formatted_body = escape(content.body) content.formatted_body = prefix_html + content.formatted_body if prefix_text: content.body = prefix_text + content.body if evt.fwd_from: await _add_forward_header(source, content, evt.fwd_from) if evt.reply_to_msg_id and not no_reply_fallback: await _add_reply_header(source, content, evt, main_intent) if isinstance(evt, Message) and evt.post and evt.post_author: if not content.formatted_body: content.formatted_body = escape(content.body) content.body += f"\n- {evt.post_author}" content.formatted_body += f"<br/><i>- <u>{evt.post_author}</u></i>" content.body = del_surrogate(content.body) if content.formatted_body: content.formatted_body = del_surrogate( content.formatted_body.replace("\n", "<br/>")) return content
def send_markdown(self, room_id: RoomID, markdown: str, msgtype: MessageType = MessageType.TEXT, relates_to: Optional[RelatesTo] = None, **kwargs) -> Awaitable[EventID]: content = TextMessageEventContent(msgtype=msgtype, format=Format.HTML) content.body, content.formatted_body = parse_markdown(markdown) if relates_to: content.relates_to = relates_to return self.send_message(room_id, content, **kwargs)
async def edit_handler(self, evt: MessageEvent, stmt: SedStatement, original_sed: HistoricalSed ) -> None: orig_evt = await self.client.get_event(evt.room_id, original_sed.seds_event) replaced = self._exec(stmt, orig_evt.content.body) content = TextMessageEventContent( msgtype=MessageType.NOTICE, body=replaced, format=Format.HTML, formatted_body=self.highlight_edits(replaced, orig_evt.content.body, stmt.highlight_edits), relates_to=RelatesTo(rel_type=RelationType.REPLACE, event_id=original_sed.output_event)) if orig_evt.content.msgtype == MessageType.EMOTE: displayname = await self._get_displayname(orig_evt.room_id, orig_evt.sender) content.body = f"* {displayname} {content.body}" content.formatted_body = f"* {escape(displayname)} {content.formatted_body}" await self.client.send_message(evt.room_id, content)
async def _try_replace_event(self, event_id: EventID, stmt: SedStatement, orig_evt: MessageEvent ) -> bool: replaced = self._exec(stmt, orig_evt.content.body) if replaced == orig_evt.content.body: return False content = TextMessageEventContent( msgtype=MessageType.NOTICE, body=replaced, format=Format.HTML, formatted_body=self.highlight_edits(replaced, orig_evt.content.body, stmt.highlight_edits)) if orig_evt.content.msgtype == MessageType.EMOTE: displayname = await self._get_displayname(orig_evt.room_id, orig_evt.sender) content.body = f"* {displayname} {content.body}" content.formatted_body = f"* {escape(displayname)} {content.formatted_body}" output_event = await orig_evt.reply(content) self.history[event_id] = HistoricalSed(output_event=output_event, seds_event=orig_evt.event_id) return True
def send_markdown(self, room_id: RoomID, markdown: str, msgtype: MessageType = MessageType.TEXT, edits: Optional[Union[EventID, MessageEvent]] = None, relates_to: Optional[RelatesTo] = None, **kwargs) -> Awaitable[EventID]: content = TextMessageEventContent(msgtype=msgtype, format=Format.HTML) content.body, content.formatted_body = parse_markdown(markdown) if relates_to: if edits: raise ValueError( "Can't use edits and relates_to at the same time.") content.relates_to = relates_to elif edits: content.set_edit(edits) return self.send_message(room_id, content, **kwargs)
async def telegram_to_matrix( evt: Message | SponsoredMessage, source: au.AbstractUser, main_intent: IntentAPI | None = None, prefix_text: str | None = None, prefix_html: str | None = None, override_text: str = None, override_entities: list[TypeMessageEntity] = None, no_reply_fallback: bool = False, require_html: bool = False, ) -> TextMessageEventContent: content = TextMessageEventContent( msgtype=MessageType.TEXT, body=add_surrogate(override_text or evt.message), ) entities = override_entities or evt.entities if entities: content.format = Format.HTML html = await _telegram_entities_to_matrix_catch( add_surrogate(content.body), entities) content.formatted_body = del_surrogate(html) if require_html: content.ensure_has_html() if prefix_html: content.ensure_has_html() content.formatted_body = prefix_html + content.formatted_body if prefix_text: content.body = prefix_text + content.body if getattr(evt, "fwd_from", None): await _add_forward_header(source, content, evt.fwd_from) if getattr(evt, "reply_to", None) and not no_reply_fallback: await _add_reply_header(source, content, evt, main_intent) if isinstance(evt, Message) and evt.post and evt.post_author: content.ensure_has_html() content.body += f"\n- {evt.post_author}" content.formatted_body += f"<br/><i>- <u>{evt.post_author}</u></i>" return content
async def send_markdown( self, room_id: RoomID, markdown: str, *, allow_html: bool = False, msgtype: MessageType = MessageType.TEXT, edits: EventID | MessageEvent | None = None, relates_to: RelatesTo | None = None, **kwargs, ) -> EventID: content = TextMessageEventContent(msgtype=msgtype, format=Format.HTML) content.body, content.formatted_body = await parse_formatted( markdown, allow_html=allow_html ) if relates_to: if edits: raise ValueError("Can't use edits and relates_to at the same time.") content.relates_to = relates_to elif edits: content.set_edit(edits) return await self.send_message(room_id, content, **kwargs)
async def _respond_formatted(self, e: MessageEvent, m: str) -> None: """Respond with formatted message in m.text matrix format, not m.notice. This is needed as mobile clients (Riot 0.9.10, RiotX) currently do not seem to render markdown / HTML in m.notice events which are conventionally send by bots. Desktop/web Riot.im does render MD/HTML in m.notice, however. """ # IRC people don't like notices. if '@appservice-irc:matrix.org' in await self.client.get_joined_members( e.room_id): t = MessageType.TEXT else: # But matrix people do. t = MessageType.NOTICE c = TextMessageEventContent(msgtype=t, formatted_body=m, format="org.matrix.custom.html") c.body, c.formatted_body = parse_formatted(m, allow_html=True) await e.respond(c, markdown=True, allow_html=True)
async def send_message(self, evt_type: EventType, evt: Event, room_id: RoomID, delivery_ids: Set[str], aggregation: Optional[Dict[str, Any]] = None ) -> None: try: tpl = self.messages[str(evt_type)] except TemplateNotFound: self.log.debug(f"Unhandled event of type {evt_type} -- {delivery_ids}") return aborted = False def abort() -> None: nonlocal aborted aborted = True args = { **attr.asdict(evt, recurse=False), **expand_enum(ACTION_CLASSES.get(evt_type)), **OTHER_ENUMS, "util": TemplateUtil, "abort": abort, "aggregation": aggregation, } args["templates"] = self.templates.proxy(args) content = TextMessageEventContent(msgtype=self.msgtype, format=Format.HTML, formatted_body=tpl.render(**args)) if not content.formatted_body or aborted: return content.formatted_body = spaces.sub(space, content.formatted_body.strip()) content.body = parse_html(content.formatted_body) content["xyz.maubot.github.webhook"] = { "delivery_ids": list(delivery_ids), "event_type": str(evt_type), **(evt.meta() if hasattr(evt, "meta") else {}), } await self.bot.client.send_message(room_id, content)