From f2af17d35950295ae5be2ffe72fe737cec8fb311 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 20 Dec 2021 23:43:23 +0200 Subject: [PATCH] Add support for contact messages --- mautrix_telegram/portal.py | 81 ++++----------- mautrix_telegram/util/__init__.py | 1 + mautrix_telegram/util/media_fallback.py | 129 ++++++++++++++++++++++++ optional-requirements.txt | 3 + 4 files changed, 152 insertions(+), 62 deletions(-) create mode 100644 mautrix_telegram/util/media_fallback.py diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index ba51f587..8115c548 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -63,6 +63,7 @@ from telethon.tl.types import ( DocumentAttributeImageSize, GeoPoint, InputChatUploadedPhoto, MessageActionChatEditPhoto, MessageMediaGeo, SendMessageCancelAction, SendMessageTypingAction, TypeInputPeer, UpdateNewMessage, InputMediaUploadedDocument, InputMediaUploadedPhoto, TypeMessage, + MessageMediaContact, ) from mautrix.errors import MatrixRequestError, IntentError, MForbidden @@ -1607,7 +1608,7 @@ class Portal(DBPortal, BasePortal): async def handle_matrix_power_levels( self, sender: u.User, new_users: dict[UserID, int], old_users: dict[UserID, int], event_id: EventID | None - ) -> None: + ) -> None: # TODO handle all power level changes and bridge exact admin rights to supergroups/channels for user, level in new_users.items(): if not user or user == self.main_intent.mxid or user == sender.mxid: @@ -2025,68 +2026,12 @@ class Portal(DBPortal, BasePortal): await intent.set_typing(self.mxid, is_typing=False) return await self._send_message(intent, content, timestamp=evt.date) - @staticmethod - def _format_dice(roll: MessageMediaDice) -> str: - if roll.emoticon == "\U0001F3B0": - emojis = { - 0: "\U0001F36B", # "🍫", - 1: "\U0001F352", # "🍒", - 2: "\U0001F34B", # "🍋", - 3: "7\ufe0f\u20e3" # "7️⃣", - } - res = roll.value - 1 - slot1, slot2, slot3 = emojis[res % 4], emojis[res // 4 % 4], emojis[res // 16] - return f"{slot1} {slot2} {slot3} ({roll.value})" - elif roll.emoticon == "\u26BD": - results = { - 1: "miss", - 2: "hit the woodwork", - 3: "goal", # seems to go in through the center - 4: "goal", - 5: "goal 🎉", # seems to go in through the top right corner, includes confetti - } - elif roll.emoticon == "\U0001F3B3": - results = { - 1: "miss", - 2: "1 pin down", - 3: "3 pins down, split", - 4: "4 pins down, split", - 5: "5 pins down", - 6: "strike 🎉", - } - # elif roll.emoticon == "\U0001F3C0": - # results = { - # 2: "rolled off", - # 3: "stuck", - # } - # elif roll.emoticon == "\U0001F3AF": - # results = { - # 1: "bounced off", - # 2: "outer rim", - # - # 6: "bullseye", - # } - else: - return str(roll.value) - return f"{results[roll.value]} ({roll.value})" - async def _handle_telegram_dice( self, _: au.AbstractUser, intent: IntentAPI, evt: Message, relates_to: RelatesTo ) -> EventID: - emoji_text = { - "\U0001F3AF": " Dart throw", - "\U0001F3B2": " Dice roll", - "\U0001F3C0": " Basketball throw", - "\U0001F3B0": " Slot machine", - "\U0001F3B3": " Bowling", - "\u26BD": " Football kick" - } - roll: MessageMediaDice = evt.media - text = f"{roll.emoticon}{emoji_text.get(roll.emoticon, '')} result: {self._format_dice(roll)}" - content = TextMessageEventContent(msgtype=MessageType.TEXT, format=Format.HTML, body=text, - formatted_body=f"

{text}

", relates_to=relates_to, - external_url=self._get_external_url(evt)) - content["net.maunium.telegram.dice"] = {"emoticon": roll.emoticon, "value": roll.value} + content = util.make_dice_event_content(evt.media) + content.relates_to = relates_to + content.external_url = self._get_external_url(evt) await intent.set_typing(self.mxid, is_typing=False) return await self._send_message(intent, content, timestamp=evt.date) @@ -2128,14 +2073,25 @@ class Portal(DBPortal, BasePortal): override_text=override_text, override_entities=override_entities) content.msgtype = MessageType.NOTICE content.external_url = self._get_external_url(evt) + content.relates_to = relates_to content["net.maunium.telegram.game"] = play_id await intent.set_typing(self.mxid, is_typing=False) return await self._send_message(intent, content, timestamp=evt.date) + async def _handle_telegram_contact( + self, source: au.AbstractUser, intent: IntentAPI, evt: Message, relates_to: RelatesTo + ) -> EventID: + content = await util.make_contact_event_content(source, evt.media) + content.relates_to = relates_to + content.external_url = self._get_external_url(evt) + + await intent.set_typing(self.mxid, is_typing=False) + return await self._send_message(intent, content, timestamp=evt.date) + async def handle_telegram_edit( self, source: au.AbstractUser, sender: p.Puppet, evt: Message - ) -> None: + ) -> None: if not self.mxid: self.log.trace("Ignoring edit to %d as chat has no Matrix room", evt.id) return @@ -2352,7 +2308,7 @@ class Portal(DBPortal, BasePortal): f" updating with data {entity!s}") allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo, - MessageMediaGame, MessageMediaDice, MessageMediaPoll, + MessageMediaGame, MessageMediaDice, MessageMediaPoll, MessageMediaContact, MessageMediaUnsupported) if sender: intent = sender.intent_for(self) @@ -2371,6 +2327,7 @@ class Portal(DBPortal, BasePortal): MessageMediaDice: self._handle_telegram_dice, MessageMediaUnsupported: self._handle_telegram_unsupported, MessageMediaGame: self._handle_telegram_game, + MessageMediaContact: self._handle_telegram_contact, }[type(evt.media)] relates_to = await formatter.telegram_reply_to_matrix(evt, source) event_id = await handler(source, intent, evt, relates_to) diff --git a/mautrix_telegram/util/__init__.py b/mautrix_telegram/util/__init__.py index 1deae593..ad3f8eb9 100644 --- a/mautrix_telegram/util/__init__.py +++ b/mautrix_telegram/util/__init__.py @@ -4,3 +4,4 @@ from .recursive_dict import recursive_del, recursive_set, recursive_get from .color_log import ColorFormatter from .send_lock import PortalSendLock from .deduplication import PortalDedup +from .media_fallback import make_dice_event_content, make_contact_event_content diff --git a/mautrix_telegram/util/media_fallback.py b/mautrix_telegram/util/media_fallback.py new file mode 100644 index 00000000..91c48553 --- /dev/null +++ b/mautrix_telegram/util/media_fallback.py @@ -0,0 +1,129 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2021 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +import html + +from telethon.tl.types import MessageMediaDice, MessageMediaContact, PeerUser + +from mautrix.types import TextMessageEventContent, MessageType, Format + +from .. import puppet as pu, abstract_user as au +from ..types import TelegramID + +try: + import phonenumbers +except ImportError: + phonenumbers = None + + +def _format_dice(roll: MessageMediaDice) -> str: + if roll.emoticon == "\U0001F3B0": + emojis = { + 0: "\U0001F36B", # "🍫", + 1: "\U0001F352", # "🍒", + 2: "\U0001F34B", # "🍋", + 3: "7\ufe0f\u20e3" # "7️⃣", + } + res = roll.value - 1 + slot1, slot2, slot3 = emojis[res % 4], emojis[res // 4 % 4], emojis[res // 16] + return f"{slot1} {slot2} {slot3} ({roll.value})" + elif roll.emoticon == "\u26BD": + results = { + 1: "miss", + 2: "hit the woodwork", + 3: "goal", # seems to go in through the center + 4: "goal", + 5: "goal 🎉", # seems to go in through the top right corner, includes confetti + } + elif roll.emoticon == "\U0001F3B3": + results = { + 1: "miss", + 2: "1 pin down", + 3: "3 pins down, split", + 4: "4 pins down, split", + 5: "5 pins down", + 6: "strike 🎉", + } + # elif roll.emoticon == "\U0001F3C0": + # results = { + # 2: "rolled off", + # 3: "stuck", + # } + # elif roll.emoticon == "\U0001F3AF": + # results = { + # 1: "bounced off", + # 2: "outer rim", + # + # 6: "bullseye", + # } + else: + return str(roll.value) + return f"{results[roll.value]} ({roll.value})" + + +def make_dice_event_content(roll: MessageMediaDice) -> TextMessageEventContent: + emoji_text = { + "\U0001F3AF": " Dart throw", + "\U0001F3B2": " Dice roll", + "\U0001F3C0": " Basketball throw", + "\U0001F3B0": " Slot machine", + "\U0001F3B3": " Bowling", + "\u26BD": " Football kick" + } + text = f"{roll.emoticon}{emoji_text.get(roll.emoticon, '')} result: {_format_dice(roll)}" + content = TextMessageEventContent(msgtype=MessageType.TEXT, format=Format.HTML, body=text, + formatted_body=f"

{text}

") + content["net.maunium.telegram.dice"] = {"emoticon": roll.emoticon, "value": roll.value} + return content + + +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: + parsed = phonenumbers.parse(formatted_phone) + fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL + formatted_phone = phonenumbers.format_number(parsed, fmt) + 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"{html.escape(name)}: " + f"{html.escape(formatted_phone)}" + ) + return content diff --git a/optional-requirements.txt b/optional-requirements.txt index cd07d1d3..e8f8462d 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -14,6 +14,9 @@ qrcode>=6,<7 #/hq_thumbnails moviepy>=1,<2 +#/formatted_phonenumbers +phonenumbers>=8,<9 + #/metrics prometheus_client>=0.6,<0.13