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