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
def facebook_to_matrix(message: fbchat.MessageData) -> TextMessageEventContent: text = message.text or "" content = TextMessageEventContent(msgtype=MessageType.TEXT, body=text) for m in reversed(message.mentions): original = text[m.offset:m.offset + m.length] if len(original) > 0 and original[0] == "@": original = original[1:] text = f"{text[:m.offset]}@{m.thread_id}\u2063{original}\u2063{text[m.offset + m.length:]}" html = escape(text) output = [] if html: codeblock = False blockquote = False line: str lines = html.split("\n") for i, line in enumerate(lines): blockquote, line = _handle_blockquote(output, blockquote, line) codeblock, line, post_args = _handle_codeblock_pre(output, codeblock, line) output.append(_convert_formatting(line)) if i != len(lines) - 1: output.append("<br/>") _handle_codeblock_post(output, *post_args) for attachment in message.attachments: if ((isinstance(attachment, fbchat.ShareAttachment) and attachment.original_url.rstrip("/") not in text)): output.append(f"<br/><a href='{attachment.original_url}'>{attachment.title}</a>") content.body += f"\n{attachment.title}: {attachment.original_url}" html = "".join(output) html = MENTION_REGEX.sub(_mention_replacer, html) if html != escape(content.body).replace("\n", "<br/>"): content.format = Format.HTML content.formatted_body = html return content
async def send_simple_html(self, room_id: str, html: str): ''' Publish a simple HTML message in the selected room. ''' from mautrix.types import Format content = TextMessageEventContent(msgtype=MessageType.TEXT) content.format = Format.HTML content.formatted_body = html await self._client.send_message(room_id=room_id, content=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
def facebook_to_matrix(message: Message) -> TextMessageEventContent: content = TextMessageEventContent(msgtype=MessageType.TEXT, body=message.text) text = message.text for m in reversed(message.mentions): original = text[m.offset:m.offset + m.length] if len(original) > 0 and original[0] == "@": original = original[1:] text = f"{text[:m.offset]}@{m.thread_id}\u2063{original}\u2063{text[m.offset + m.length:]}" html = escape(text) output = [] codeblock = False blockquote = False line: str lines = html.split("\n") for i, line in enumerate(lines): blockquote, line = _handle_blockquote(output, blockquote, line) codeblock, line, post_args = _handle_codeblock_pre( output, codeblock, line) output.append(_convert_formatting(line)) if i != len(lines) - 1: output.append("<br/>") _handle_codeblock_post(output, *post_args) html = "".join(output) html = MENTION_REGEX.sub(_mention_replacer, html) if html != escape(content.body).replace("\n", "<br/>"): content.format = Format.HTML content.formatted_body = html return content
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
async def facebook_to_matrix( msg: Union[graphql.MessageText, mqtt.Message]) -> TextMessageEventContent: if isinstance(msg, mqtt.Message): text = msg.text mentions = msg.mentions elif isinstance(msg, graphql.MessageText): text = msg.text mentions = msg.ranges else: raise ValueError( f"Unsupported Facebook message type {type(msg).__name__}") text = text or "" content = TextMessageEventContent(msgtype=MessageType.TEXT, body=text) mention_user_ids = [] for m in reversed(mentions): original = text[m.offset:m.offset + m.length] if len(original) > 0 and original[0] == "@": original = original[1:] mention_user_ids.append(int(m.user_id)) text = f"{text[:m.offset]}@{m.user_id}\u2063{original}\u2063{text[m.offset + m.length:]}" html = escape(text) output = [] if html: codeblock = False blockquote = False line: str lines = html.split("\n") for i, line in enumerate(lines): blockquote, line = _handle_blockquote(output, blockquote, line) codeblock, line, post_args = _handle_codeblock_pre( output, codeblock, line) output.append(_convert_formatting(line)) if i != len(lines) - 1: output.append("<br/>") _handle_codeblock_post(output, *post_args) html = "".join(output) mention_user_map = {} for fbid in mention_user_ids: user = await u.User.get_by_fbid(fbid) if user: mention_user_map[fbid] = user.mxid else: puppet = await pu.Puppet.get_by_fbid(fbid, create=False) mention_user_map[fbid] = puppet.mxid if puppet else None def _mention_replacer(match: Match) -> str: mxid = mention_user_map[int(match.group(1))] if not mxid: return match.group(2) return f"<a href=\"https://matrix.to/#/{mxid}\">{match.group(2)}</a>" html = MENTION_REGEX.sub(_mention_replacer, html) if html != escape(content.body).replace("\n", "<br/>"): content.format = Format.HTML content.formatted_body = html return content
async def _apply_emote_format(self, sender: 'u.User', content: TextMessageEventContent) -> None: if content.format != Format.HTML: content.format = Format.HTML content.formatted_body = escape_html(content.body).replace("\n", "<br/>") tpl = self.get_config("emote_format") puppet = p.Puppet.get(sender.tgid) content.formatted_body = Template(tpl).safe_substitute( dict(sender_mxid=sender.mxid, sender_username=sender.mxid_localpart, sender_displayname=escape_html(await self.get_displayname(sender)), mention=f"<a href='https://matrix.to/#/{puppet.mxid}'>{puppet.displayname}</a>", username=sender.username, displayname=puppet.displayname, body=content.body, formatted_body=content.formatted_body)) content.msgtype = MessageType.TEXT
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 _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" 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) 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 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
async def make_contact_event_content( source: au.AbstractUser, contact: MessageMediaContact ) -> TextMessageEventContent: name = " ".join(x for x in [contact.first_name, contact.last_name] if x) formatted_phone = f"+{contact.phone_number}" if phonenumbers is not None: try: parsed = phonenumbers.parse(formatted_phone) fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL formatted_phone = phonenumbers.format_number(parsed, fmt) except phonenumbers.NumberParseException: pass content = TextMessageEventContent( msgtype=MessageType.TEXT, body=f"Shared contact info for {name}: {formatted_phone}", ) content["net.maunium.telegram.contact"] = { "user_id": contact.user_id, "first_name": contact.first_name, "last_name": contact.last_name, "phone_number": contact.phone_number, "vcard": contact.vcard, } puppet = await pu.Puppet.get_by_tgid(TelegramID(contact.user_id)) if not puppet.displayname: try: entity = await source.client.get_entity(PeerUser(contact.user_id)) await puppet.update_info(source, entity) except Exception as e: source.log.warning(f"Failed to sync puppet info of received contact: {e}") else: content.format = Format.HTML content.formatted_body = ( f"Shared contact info for " f"<a href='https://matrix.to/#/{puppet.mxid}'>{html.escape(name)}</a>: " f"{html.escape(formatted_phone)}" ) return content
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)
async def _handle_facebook_attachment( self, intent: IntentAPI, attachment: AttachmentClass, reply_to: str, message_text: str) -> Optional[EventID]: if isinstance(attachment, AudioAttachment): mxc, mime, size = await self._reupload_fb_photo( attachment.url, intent, attachment.filename) event_id = await intent.send_file( self.mxid, mxc, file_type=MessageType.AUDIO, info=AudioInfo(size=size, mimetype=mime, duration=attachment.duration), file_name=attachment.filename, relates_to=self._get_facebook_reply(reply_to)) # elif isinstance(attachment, VideoAttachment): # TODO elif isinstance(attachment, FileAttachment): mxc, mime, size = await self._reupload_fb_photo( attachment.url, intent, attachment.name) event_id = await intent.send_file( self.mxid, mxc, info=FileInfo(size=size, mimetype=mime), file_name=attachment.name, relates_to=self._get_facebook_reply(reply_to)) elif isinstance(attachment, ImageAttachment): mxc, mime, size = await self._reupload_fb_photo( attachment.large_preview_url or attachment.preview_url, intent) info = ImageInfo(size=size, mimetype=mime, width=attachment.large_preview_width, height=attachment.large_preview_height) event_id = await intent.send_image( self.mxid, mxc, info=info, file_name=f"image.{attachment.original_extension}", relates_to=self._get_facebook_reply(reply_to)) elif isinstance(attachment, LocationAttachment): content = await self._convert_facebook_location(intent, attachment) content.relates_to = self._get_facebook_reply(reply_to) event_id = await intent.send_message(self.mxid, content) elif isinstance(attachment, ShareAttachment): # remove trailing slash for url searching url = attachment.original_url if url[-1] == "/": url = url[0:-1] title = attachment.title if title == "": title = url # Prevent sending urls that are already in the original message text if message_text is not None and url not in message_text: content = TextMessageEventContent( msgtype=MessageType.TEXT, body=f"{title}: {attachment.original_url}") content.format = Format.HTML content.formatted_body = f"<a href='{attachment.original_url}'>{title}</a>" event_id = await intent.send_message(self.mxid, content) elif message_text is None: content = TextMessageEventContent( msgtype=MessageType.TEXT, body=f"{title}: {attachment.original_url}") content.format = Format.HTML content.formatted_body = f"<a href='{attachment.original_url}'>{title}</a>" event_id = await intent.send_message(self.mxid, content) else: return None else: self.log.warning(f"Unsupported attachment type: {attachment}") return None self._last_bridged_mxid = event_id return event_id