Even^4 more migrations to mautrix-python

This commit is contained in:
Tulir Asokan
2019-08-06 00:23:32 +03:00
parent 30563f3648
commit 3af306abe0
5 changed files with 188 additions and 256 deletions
@@ -13,7 +13,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, List, Tuple, Callable, Pattern, Match, TYPE_CHECKING, Dict, Any
from typing import Optional, List, Tuple, Callable, Pattern, Match, TYPE_CHECKING
import re
import logging
@@ -21,7 +21,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, M
TypeMessageEntity)
from telethon.helpers import add_surrogate, del_surrogate
from mautrix.types import RoomID
from mautrix.types import RoomID, MessageEventContent
from ... import puppet as pu
from ...types import TelegramID
@@ -90,26 +90,12 @@ def matrix_to_telegram(html: str) -> ParsedMessage:
raise FormatError(f"Failed to convert Matrix format: {html}") from e
def matrix_reply_to_telegram(content: Dict[str, Any], tg_space: TelegramID,
def matrix_reply_to_telegram(content: MessageEventContent, tg_space: TelegramID,
room_id: Optional[RoomID] = None) -> Optional[TelegramID]:
relates_to = content.get("m.relates_to", None) or {}
if not relates_to:
return None
reply = (relates_to if relates_to.get("rel_type", None) == "m.reference"
else relates_to.get("m.in_reply_to", None) or {})
if not reply:
return None
room_id = room_id or reply.get("room_id", None)
event_id = reply.get("event_id", None)
event_id = content.get_reply_to()
if not event_id:
return
try:
if content["format"] == "org.matrix.custom.html":
content["formatted_body"] = trim_reply_fallback_html(content["formatted_body"])
except KeyError:
pass
content["body"] = trim_reply_fallback_text(content["body"])
content.trim_reply_fallback()
message = DBMessage.get_by_mxid(event_id, room_id, tg_space)
if message:
+2 -2
View File
@@ -456,8 +456,8 @@ class BasePortal(ABC):
pass
@abstractmethod
def handle_matrix_power_levels(self, sender: 'u.User', new_levels: PowerLevelStateEventContent,
old_levels: PowerLevelStateEventContent) -> Awaitable[None]:
def handle_matrix_power_levels(self, sender: 'u.User', new_levels: Dict[UserID, int],
old_levels: Dict[UserID, int]) -> Awaitable[None]:
pass
# endregion
+61 -75
View File
@@ -37,8 +37,9 @@ from telethon.tl.types import (
SendMessageCancelAction, SendMessageTypingAction, TypeInputPeer, TypeMessageEntity,
UpdateNewMessage, InputMediaUploadedDocument)
from mautrix.types import (EventID, RoomID, UserID, ContentURI, MessageType,
TextMessageEventContent, Format)
from mautrix.types import (EventID, RoomID, UserID, ContentURI, MessageType, MessageEventContent,
TextMessageEventContent, MediaMessageEventContent, Format,
LocationMessageEventContent)
from mautrix.bridge import BasePortal as MautrixBasePortal
from ..types import TelegramID
@@ -181,38 +182,31 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
# We'll just assume the user is already in the chat.
pass
async def _apply_msg_format(self, sender: 'u.User', msgtype: str, message: Dict[str, Any]
async def _apply_msg_format(self, sender: 'u.User', content: MessageEventContent
) -> None:
if "formatted_body" not in message:
message["format"] = "org.matrix.custom.html"
message["formatted_body"] = escape_html(message.get("body", "")).replace("\n", "<br/>")
body = message["formatted_body"]
if not content.formatted_body or content.format != Format.HTML:
content.format = Format.HTML
content.formatted_body = escape_html(content.body).replace("\n", "<br/>")
tpl = (self.get_config(f"message_formats.[{msgtype}]")
tpl = (self.get_config(f"message_formats.[{content.msgtype.value}]")
or "<b>$sender_displayname</b>: $message")
displayname = await self.get_displayname(sender)
tpl_args = dict(sender_mxid=sender.mxid,
sender_username=sender.mxid_localpart,
sender_displayname=escape_html(displayname),
message=body)
message["formatted_body"] = Template(tpl).safe_substitute(tpl_args)
message=content.formatted_body)
content.formatted_body = Template(tpl).safe_substitute(tpl_args)
async def _pre_process_matrix_message(self, sender: 'u.User', use_relaybot: bool,
message: Dict[str, Any]) -> None:
msgtype = message.get("msgtype", "m.text")
if msgtype == "m.emote":
await self._apply_msg_format(sender, msgtype, message)
if "m.new_content" in message:
await self._apply_msg_format(sender, msgtype, message["m.new_content"])
message["m.new_content"]["msgtype"] = "m.text"
message["msgtype"] = "m.text"
content: MessageEventContent) -> None:
if content.msgtype == MessageType.EMOTE:
await self._apply_msg_format(sender, content)
content.msgtype = MessageType.TEXT
elif use_relaybot:
await self._apply_msg_format(sender, msgtype, message)
if "m.new_content" in message:
await self._apply_msg_format(sender, msgtype, message["m.new_content"])
await self._apply_msg_format(sender, content)
@staticmethod
def _matrix_event_to_entities(event: Union[str, TextMessageEventContent]
def _matrix_event_to_entities(event: Union[str, MessageEventContent]
) -> Tuple[str, Optional[List[TypeMessageEntity]]]:
try:
if isinstance(event, str):
@@ -227,57 +221,51 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
async def _handle_matrix_text(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient',
message: Dict, reply_to: TelegramID) -> None:
content: TextMessageEventContent, reply_to: TelegramID) -> None:
async with self.send_lock(sender_id):
lp = self.get_config("telegram_link_preview")
relates_to = message.get("m.relates_to", None) or {}
if relates_to.get("rel_type", None) == "m.replace":
orig_msg = DBMessage.get_by_mxid(relates_to.get("event_id", ""), self.mxid, space)
if orig_msg and "m.new_content" in message:
message = message["m.new_content"]
response = await client.edit_message(self.peer, orig_msg.tgid, message,
if content.get_edit():
orig_msg = DBMessage.get_by_mxid(content.get_edit(), self.mxid, space)
if orig_msg:
response = await client.edit_message(self.peer, orig_msg.tgid, content,
parse_mode=self._matrix_event_to_entities,
link_preview=lp)
self._add_telegram_message_to_db(event_id, space, -1, response)
return
response = await client.send_message(self.peer, message, reply_to=reply_to,
response = await client.send_message(self.peer, content, reply_to=reply_to,
parse_mode=self._matrix_event_to_entities,
link_preview=lp)
self._add_telegram_message_to_db(event_id, space, 0, response)
async def _handle_matrix_file(self, msgtype: MessageType, sender_id: TelegramID,
event_id: EventID, space: TelegramID,
client: 'MautrixTelegramClient', message: dict,
reply_to: TelegramID) -> None:
file = await self.main_intent.download_media(message["url"])
async def _handle_matrix_file(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient',
content: MediaMessageEventContent, reply_to: TelegramID) -> None:
file = await self.main_intent.download_media(content.url)
info = message.get("info", {})
mime = info.get("mimetype", None)
mime = content.info.mimetype
w, h = None, None
w, h = content.info.width, content.info.height
if msgtype == MessageType.STICKER:
if content.msgtype == MessageType.STICKER:
if mime != "image/gif":
mime, file, w, h = util.convert_image(file, source_mime=mime, target_type="webp")
else:
# Remove sticker description
message["mxtg_filename"] = "sticker.gif"
message["body"] = ""
elif "w" in info and "h" in info:
w, h = info["w"], info["h"]
content["net.maunium.telegram.internal.filename"] = "sticker.gif"
content.body = ""
file_name = self._get_file_meta(message["mxtg_filename"], mime)
file_name = self._get_file_meta(content["net.maunium.telegram.internal.filename"], mime)
attributes = [DocumentAttributeFilename(file_name=file_name)]
if w and h:
attributes.append(DocumentAttributeImageSize(w, h))
caption = message["body"] if message["body"].lower() != file_name.lower() else None
caption = content.body if content.body.lower() != file_name.lower() else None
media = await client.upload_file_direct(
file, mime, attributes, file_name,
max_image_size=config["bridge.image_as_file_size"] * 1000 ** 2)
async with self.send_lock(sender_id):
if await self._matrix_document_edit(client, message, space, caption, media, event_id):
if await self._matrix_document_edit(client, content, space, caption, media, event_id):
return
try:
response = await client.send_media(self.peer, media, reply_to=reply_to,
@@ -289,12 +277,11 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
caption=caption)
self._add_telegram_message_to_db(event_id, space, 0, response)
async def _matrix_document_edit(self, client: 'MautrixTelegramClient', message: dict,
space: TelegramID, caption: str, media: Any, event_id: EventID
) -> bool:
relates_to = message.get("m.relates_to", None) or {}
if relates_to.get("rel_type", None) == "m.replace":
orig_msg = DBMessage.get_by_mxid(relates_to.get("event_id", ""), self.mxid, space)
async def _matrix_document_edit(self, client: 'MautrixTelegramClient',
content: MessageEventContent, space: TelegramID,
caption: str, media: Any, event_id: EventID) -> bool:
if content.get_edit():
orig_msg = DBMessage.get_by_mxid(content.get_edit(), self.mxid, space)
if orig_msg:
response = await client.edit_message(self.peer, orig_msg.tgid,
caption, file=media)
@@ -304,18 +291,19 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
async def _handle_matrix_location(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient',
message: Dict[str, Any], reply_to: TelegramID) -> None:
content: LocationMessageEventContent, reply_to: TelegramID
) -> None:
try:
lat, long = message["geo_uri"][len("geo:"):].split(",")
lat, long = content.geo_uri[len("geo:"):].split(",")
lat, long = float(lat), float(long)
except (KeyError, ValueError):
self.log.exception("Failed to parse location")
return None
caption, entities = self._matrix_event_to_entities(message)
caption, entities = self._matrix_event_to_entities(content)
media = MessageMediaGeo(geo=GeoPoint(lat, long, access_hash=0))
async with self.send_lock(sender_id):
if await self._matrix_document_edit(client, message, space, caption, media, event_id):
if await self._matrix_document_edit(client, content, space, caption, media, event_id):
return
response = await client.send_media(self.peer, media, reply_to=reply_to,
caption=caption, entities=entities)
@@ -335,14 +323,14 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
mxid=event_id,
edit_index=edit_index).insert()
async def handle_matrix_message(self, sender: 'u.User', message: Dict[str, Any],
async def handle_matrix_message(self, sender: 'u.User', content: MessageEventContent,
event_id: EventID) -> None:
if "body" not in message or "msgtype" not in message:
if not content.body or not content.msgtype:
self.log.debug(f"Ignoring message {event_id} in {self.mxid} without body or msgtype")
return
puppet = p.Puppet.get_by_custom_mxid(sender.mxid)
if puppet and message.get("net.maunium.telegram.puppet", False):
if puppet and content.get("net.maunium.telegram.puppet", False):
self.log.debug("Ignoring puppet-sent message by confirmed puppet user %s", sender.mxid)
return
@@ -351,28 +339,27 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
sender_id = sender.tgid if logged_in else self.bot.tgid
space = (self.tgid if self.peer_type == "channel" # Channels have their own ID space
else (sender.tgid if logged_in else self.bot.tgid))
reply_to = formatter.matrix_reply_to_telegram(message, space, room_id=self.mxid)
reply_to = formatter.matrix_reply_to_telegram(content, space, room_id=self.mxid)
message["mxtg_filename"] = message["body"]
await self._pre_process_matrix_message(sender, not logged_in, message)
msgtype = message["msgtype"]
content["net.maunium.telegram.internal.filename"] = content.body
await self._pre_process_matrix_message(sender, not logged_in, content)
if msgtype == "m.notice":
if content.msgtype == MessageType.NOTICE:
bridge_notices = self.get_config("bridge_notices.default")
excepted = sender.mxid in self.get_config("bridge_notices.exceptions")
if not bridge_notices and not excepted:
return
if msgtype == "m.text" or msgtype == "m.notice":
await self._handle_matrix_text(sender_id, event_id, space, client, message, reply_to)
elif msgtype == "m.location":
await self._handle_matrix_location(sender_id, event_id, space, client, message,
if content.msgtype in (MessageType.TEXT, MessageType.NOTICE):
await self._handle_matrix_text(sender_id, event_id, space, client, content, reply_to)
elif content.msgtype == MessageType.LOCATION:
await self._handle_matrix_location(sender_id, event_id, space, client, content,
reply_to)
elif msgtype in ("m.sticker", "m.image", "m.file", "m.audio", "m.video"):
await self._handle_matrix_file(msgtype, sender_id, event_id, space, client, message,
reply_to)
elif content.msgtype in (MessageType.STICKER, MessageType.IMAGE, MessageType.FILE,
MessageType.AUDIO, MessageType.VIDEO):
await self._handle_matrix_file(sender_id, event_id, space, client, content, reply_to)
else:
self.log.debug(f"Unhandled Matrix event: {message}")
self.log.debug(f"Unhandled Matrix event: {content}")
async def handle_matrix_pin(self, sender: 'u.User',
pinned_message: Optional[EventID]) -> None:
@@ -418,9 +405,8 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
EditAdminRequest(channel=await self.get_input_entity(sender),
user_id=user_id, admin_rights=rights))
async def handle_matrix_power_levels(self, sender: 'u.User',
new_users: Dict[UserID, int],
old_users: Dict[str, int]) -> None:
async def handle_matrix_power_levels(self, sender: 'u.User', new_users: Dict[UserID, int],
old_users: Dict[UserID, int]) -> 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:
+1 -1
View File
@@ -155,7 +155,7 @@ class PortalMetadata(BasePortal, ABC):
if levels.get_user_level(self.main_intent.mxid) == 100:
levels = self._get_base_power_levels(levels, entity)
await self.main_intent.set_power_levels(self.mxid, levels)
await self.handle_matrix_power_levels(source, levels, PowerLevelStateEventContent())
await self.handle_matrix_power_levels(source, levels.users, {})
async def invite_telegram(self, source: 'u.User',
puppet: Union[p.Puppet, 'AbstractUser']) -> None:
+119 -159
View File
@@ -13,7 +13,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Awaitable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
from typing import Awaitable, Dict, List, Optional, Tuple, Union, NamedTuple, TYPE_CHECKING
from html import escape as escape_html
from abc import ABC
import random
@@ -37,7 +37,9 @@ from telethon.tl.types import (
UpdateUserTyping, MessageEntityPre, ChatPhotoEmpty)
from mautrix.appservice import IntentAPI
from mautrix.types import EventID, UserID, ImageInfo, ThumbnailInfo
from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType,
EventType, MediaMessageEventContent, TextMessageEventContent,
LocationMessageEventContent, Format)
from ..types import TelegramID
from ..db import Message as DBMessage, TelegramFile as DBTelegramFile
@@ -52,6 +54,8 @@ if TYPE_CHECKING:
InviteList = Union[UserID, List[UserID]]
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
DocAttrs = NamedTuple("DocAttrs", name=Optional[str], mime_type=Optional[str], is_sticker=bool,
sticker_alt=Optional[str], width=int, height=int)
config: Optional['Config'] = None
@@ -71,7 +75,7 @@ class PortalTelegram(BasePortal, ABC):
_: Union[UpdateUserTyping, UpdateChatUserTyping]) -> None:
await user.intent.set_typing(self.mxid, is_typing=True)
def get_external_url(self, evt: Message) -> Optional[str]:
def _get_external_url(self, evt: Message) -> Optional[str]:
if self.peer_type == "channel" and self.username is not None:
return f"https://t.me/{self.username}/{evt.id}"
elif self.peer_type != "user":
@@ -90,7 +94,7 @@ class PortalTelegram(BasePortal, ABC):
evt, source, self.main_intent,
prefix_html=f"<img src='{file.mxc}' alt='Inline Telegram photo'/><br/>",
prefix_text="Inline image: ")
content.external_url = self.get_external_url(evt)
content.external_url = self._get_external_url(evt)
await intent.set_typing(self.mxid, is_typing=False)
return await intent.send_message(self.mxid, content, timestamp=evt.date)
info = ImageInfo(
@@ -101,42 +105,36 @@ class PortalTelegram(BasePortal, ABC):
await intent.set_typing(self.mxid, is_typing=False)
result = await intent.send_image(self.mxid, file.mxc, info=info, text=name,
relates_to=relates_to, timestamp=evt.date,
external_url=self.get_external_url(evt))
external_url=self._get_external_url(evt))
if evt.message:
text, html, _ = await formatter.telegram_to_matrix(evt, source, self.main_intent,
no_reply_fallback=True)
result = await intent.send_text(self.mxid, text, html=html, timestamp=evt.date,
external_url=self.get_external_url(evt))
external_url=self._get_external_url(evt))
return result
@staticmethod
def _parse_telegram_document_attributes(attributes: List[TypeDocumentAttribute]) -> Dict:
attrs = {
"name": None,
"mime_type": None,
"is_sticker": False,
"sticker_alt": None,
"width": None,
"height": None,
} # type: Dict
def _parse_telegram_document_attributes(attributes: List[TypeDocumentAttribute]) -> DocAttrs:
attrs = DocAttrs(name=None, mime_type=None, is_sticker=False, sticker_alt=None,
width=0, height=0)
for attr in attributes:
if isinstance(attr, DocumentAttributeFilename):
attrs["name"] = attrs["name"] or attr.file_name
attrs["mime_type"], _ = mimetypes.guess_type(attr.file_name)
attrs.name = attrs.name or attr.file_name
attrs.mime_type, _ = mimetypes.guess_type(attr.file_name)
elif isinstance(attr, DocumentAttributeSticker):
attrs["is_sticker"] = True
attrs["sticker_alt"] = attr.alt
attrs.is_sticker = True
attrs.sticker_alt = attr.alt
elif isinstance(attr, DocumentAttributeVideo):
attrs["width"], attrs["height"] = attr.w, attr.h
attrs.width, attrs.height = attr.w, attr.h
return attrs
@staticmethod
def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: Dict,
def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: DocAttrs,
thumb_size: TypePhotoSize) -> Tuple[ImageInfo, str]:
document = evt.media.document
name = evt.message or attrs["name"]
if attrs["is_sticker"]:
alt = attrs["sticker_alt"]
name = evt.message or attrs.name
if attrs.is_sticker:
alt = attrs.sticker_alt
if len(alt) > 0:
try:
name = f"{alt} ({unicodedata.name(alt[0]).lower()})"
@@ -150,12 +148,12 @@ class PortalTelegram(BasePortal, ABC):
mime_type = file.mime_type or document.mime_type
info = ImageInfo(size=file.size, mimetype=mime_type)
if attrs["mime_type"] and not file.was_converted:
file.mime_type = attrs["mime_type"] or file.mime_type
if attrs.mime_type and not file.was_converted:
file.mime_type = attrs.mime_type or file.mime_type
if file.width and file.height:
info.width, info.height = file.width, file.height
elif attrs["width"] and attrs["height"]:
info.width, info.height = attrs["width"], attrs["height"]
elif attrs.width and attrs.height:
info.width, info.height = attrs.width, attrs.height
if file.thumbnail:
info.thumbnail_url = file.thumbnail.mxc
@@ -167,13 +165,14 @@ class PortalTelegram(BasePortal, ABC):
return info, name
async def handle_telegram_document(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None) -> Optional[EventID]:
evt: Message, relates_to: RelatesTo = None
) -> Optional[EventID]:
document = evt.media.document
attrs = self._parse_telegram_document_attributes(document.attributes)
if document.size > config["bridge.max_document_size"] * 1000 ** 2:
name = attrs["name"] or ""
name = attrs.name or ""
caption = f"\n{evt.message}" if evt.message else ""
return await intent.send_notice(self.mxid, f"Too large file {name}{caption}")
@@ -183,7 +182,7 @@ class PortalTelegram(BasePortal, ABC):
thumb_loc = None
thumb_size = None
file = await util.transfer_file_to_matrix(source.client, intent, document, thumb_loc,
is_sticker=attrs["is_sticker"])
is_sticker=attrs.is_sticker)
if not file:
return None
@@ -191,88 +190,62 @@ class PortalTelegram(BasePortal, ABC):
await intent.set_typing(self.mxid, is_typing=False)
kwargs = {
"room_id": self.mxid,
"url": file.mxc,
"info": info,
"text": name,
"relates_to": relates_to,
"timestamp": evt.date,
"external_url": self.get_external_url(evt)
}
if attrs["is_sticker"]:
return await intent.send_sticker(**kwargs)
mime_type = info["mimetype"]
if mime_type.startswith("video/"):
kwargs["file_type"] = "m.video"
elif mime_type.startswith("audio/"):
kwargs["file_type"] = "m.audio"
elif mime_type.startswith("image/"):
kwargs["file_type"] = "m.image"
else:
kwargs["file_type"] = "m.file"
return await intent.send_file(**kwargs)
event_type = EventType.STICKER if attrs.is_sticker else EventType.ROOM_MESSAGE
content = MediaMessageEventContent(
body=name, info=info, url=file.mxc, relates_to=relates_to,
external_url=self._get_external_url(evt),
msgtype={
"video/": MessageType.VIDEO,
"audio/": MessageType.AUDIO,
"image/": MessageType.IMAGE,
}.get(info.mimetype[:6], default=MessageType.FILE))
return await intent.send_message_event(self.mxid, event_type, content, timestamp=evt.date)
def handle_telegram_location(self, _: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: dict = None) -> Awaitable[EventID]:
location = evt.media.geo
long = location.long
lat = location.lat
long = evt.media.geo.long
lat = evt.media.geo.lat
long_char = "E" if long > 0 else "W"
lat_char = "N" if lat > 0 else "S"
rounded_long = round(long, 5)
rounded_lat = round(lat, 5)
body = f"{rounded_lat}° {lat_char}, {rounded_long}° {long_char}"
body = f"{round(lat, 5)}° {lat_char}, {round(long, 5)}° {long_char}"
url = f"https://maps.google.com/?q={lat},{long}"
formatted_body = f"Location: <a href='{url}'>{body}</a>"
# At least riot-web ignores formatting in m.location messages,
# so we'll add a plaintext link.
body = f"Location: {body}\n{url}"
content = LocationMessageEventContent(
msgtype=MessageType.LOCATION, geo_uri=f"geo:{lat},{long}",
body=f"Location: {body}\n{url}",
relates_to=relates_to, external_url=self._get_external_url(evt))
content["format"] = Format.HTML
content["formatted_body"] = f"Location: <a href='{url}'>{body}</a>"
return intent.send_message(self.mxid, {
"msgtype": "m.location",
"geo_uri": f"geo:{lat},{long}",
"body": body,
"format": "org.matrix.custom.html",
"formatted_body": formatted_body,
"m.relates_to": relates_to or None,
}, timestamp=evt.date, external_url=self.get_external_url(evt))
return intent.send_message(self.mxid, content, timestamp=evt.date)
async def handle_telegram_text(self, source: 'AbstractUser', intent: IntentAPI, is_bot: bool,
evt: Message) -> EventID:
self.log.debug(f"Sending {evt.message} to {self.mxid} by {intent.mxid}")
text, html, relates_to = await formatter.telegram_to_matrix(evt, source, self.main_intent)
content = await formatter.telegram_to_matrix(evt, source, self.main_intent)
content.external_url = self._get_external_url(evt)
if is_bot and self.get_config("bot_messages_as_notices"):
content.msgtype = MessageType.NOTICE
await intent.set_typing(self.mxid, is_typing=False)
msgtype = "m.notice" if is_bot and self.get_config("bot_messages_as_notices") else "m.text"
return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to,
msgtype=msgtype, timestamp=evt.date,
external_url=self.get_external_url(evt))
return await intent.send_message(self.mxid, content, timestamp=evt.date)
async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None) -> EventID:
override_text = ("This message is not supported on your version of Mautrix-Telegram. "
"Please check https://github.com/tulir/mautrix-telegram or ask your "
"bridge administrator about possible updates.")
text, html, relates_to = await formatter.telegram_to_matrix(
content = await formatter.telegram_to_matrix(
evt, source, self.main_intent, override_text=override_text)
content.msgtype = MessageType.NOTICE
content.external_url = self._get_external_url(evt)
content["net.maunium.telegram.unsupported"] = True
await intent.set_typing(self.mxid, is_typing=False)
return await intent.send_message(self.mxid, {
"body": text,
"msgtype": "m.notice",
"format": "org.matrix.custom.html",
"formatted_body": html,
"m.relates_to": relates_to,
"net.maunium.telegram.unsupported": True,
}, timestamp=evt.date, external_url=self.get_external_url(evt))
return await intent.send_message(self.mxid, content, timestamp=evt.date)
async def handle_telegram_poll(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: dict) -> EventID:
poll = evt.media.poll # type: Poll
relates_to: RelatesTo) -> EventID:
poll: Poll = evt.media.poll
poll_id = self._encode_msgid(source, evt)
_n = 0
@@ -282,21 +255,19 @@ class PortalTelegram(BasePortal, ABC):
_n += 1
return _n
text = (f"Poll: {poll.question}\n"
+ "\n".join(f"{n()}. {answer.text}" for answer in poll.answers) +
"\n"
f"Vote with !tg vote {poll_id} <choice number>")
text_answers = "\n".join(f"{n()}. {answer.text}" for answer in poll.answers)
html_answers = "\n".join(f"<li>{answer.text}</li>" for answer in poll.answers)
content = TextMessageEventContent(
msgtype=MessageType.TEXT, format=Format.HTML,
body=f"Poll: {poll.question}\n{text_answers}\n"
f"Vote with !tg vote {poll_id} <choice number>",
formatted_body=f"<strong>Poll</strong>: {poll.question}<br/>\n"
f"<ol>{html_answers}</ol>\n"
f"Vote with <code>!tg vote {poll_id} &lt;choice number&gt;</code>",
relates_to=relates_to, external_url=self._get_external_url(evt))
html = (f"<strong>Poll</strong>: {poll.question}<br/>\n"
f"<ol>"
+ "\n".join(f"<li>{answer.text}</li>"
for answer in poll.answers) +
"</ol>\n"
f"Vote with <code>!tg vote {poll_id} &lt;choice number&gt;</code>")
await intent.set_typing(self.mxid, is_typing=False)
return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to,
msgtype="m.text", timestamp=evt.date,
external_url=self.get_external_url(evt))
return await intent.send_message(self.mxid, content, timestamp=evt.date)
@staticmethod
def _int_to_bytes(i: int) -> bytes:
@@ -322,31 +293,29 @@ class PortalTelegram(BasePortal, ABC):
return base64.b64encode(play_id).decode("utf-8").rstrip("=")
async def handle_telegram_game(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None) -> EventID:
evt: Message, relates_to: RelatesTo = None) -> EventID:
game = evt.media.game
play_id = self._encode_msgid(source, evt)
command = f"!tg play {play_id}"
override_text = f"Run {command} in your bridge management room to play {game.title}"
override_entities = [
MessageEntityPre(offset=len("Run "), length=len(command), language="")]
text, html, relates_to = await formatter.telegram_to_matrix(
content = await formatter.telegram_to_matrix(
evt, source, self.main_intent,
override_text=override_text, override_entities=override_entities)
await intent.set_typing(self.mxid, is_typing=False)
return await intent.send_message(self.mxid, {
"body": text,
"msgtype": "m.notice",
"format": "org.matrix.custom.html",
"formatted_body": html,
"m.relates_to": relates_to,
"net.maunium.telegram.game": play_id,
}, timestamp=evt.date, external_url=self.get_external_url(evt))
content.msgtype = MessageType.NOTICE
content.external_url = self._get_external_url(evt)
content["net.maunium.telegram.game"] = play_id
async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet,
evt: Message) -> None:
await intent.set_typing(self.mxid, is_typing=False)
return await intent.send_message(self.mxid, content, timestamp=evt.date)
async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message
) -> None:
if not self.mxid:
return
elif hasattr(evt, "media") and isinstance(evt.media, (MessageMediaGame,)):
elif hasattr(evt, "media") and isinstance(evt.media, MessageMediaGame):
self.log.debug("Ignoring game message edit event")
return
@@ -368,45 +337,38 @@ class PortalTelegram(BasePortal, ABC):
).insert()
return
text, html, _ = await formatter.telegram_to_matrix(evt, source, self.main_intent)
content = await formatter.telegram_to_matrix(evt, source, self.main_intent,
no_reply_fallback=True)
editing_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space)
if not editing_msg:
self.log.info(f"Didn't find edited message {evt.id}@{tg_space} (src {source.tgid}) "
"in database.")
return
msgtype = ("m.notice"
if sender and sender.is_bot and self.get_config("bot_messages_as_notices")
else "m.text")
content = {
"body": f"Edit: {text}",
"msgtype": msgtype,
"format": "org.matrix.custom.html",
"formatted_body": (f"<a href='https://matrix.to/#/{editing_msg.mx_room}/"
f"{editing_msg.mxid}'>Edit</a>: "
f"{html or escape_html(text)}"),
"external_url": self.get_external_url(evt),
"m.new_content": {
"body": text,
"msgtype": "m.text",
**({"format": "org.matrix.custom.html",
"formatted_body": html} if html else {}),
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": editing_msg.mxid,
},
}
content.msgtype = (MessageType.NOTICE if (sender and sender.is_bot
and self.get_config("bot_messages_as_notices"))
else MessageType.TEXT)
content.external_url = self._get_external_url(evt)
content.set_edit(editing_msg.mxid)
# TODO remove this stuff once mautrix-python generates m.new_content
new_content = content.serialize()
del new_content["m.relates_to"]
content["m.new_content"] = new_content
content.body = f"Edit: {content.body}"
content.format = Format.HTML
content.formatted_body = (f"<a href=\"https://matrix.to/#/{editing_msg.mx_room}/"
f"{editing_msg.mxid}\">Edit</a>: "
f"{content.formatted_body or escape_html(content.body)}")
intent = sender.intent if sender else self.main_intent
await intent.set_typing(self.mxid, is_typing=False)
response = await intent.send_message(self.mxid, content)
mxid = response["event_id"]
event_id = await intent.send_message(self.mxid, content)
prev_edit_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space, -1) or editing_msg
DBMessage(mxid=mxid, mx_room=self.mxid, tg_space=tg_space, tgid=evt.id,
DBMessage(mxid=event_id, mx_room=self.mxid, tg_space=tg_space, tgid=TelegramID(evt.id),
edit_index=prev_edit_msg.edit_index + 1).insert()
DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=mxid)
DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id)
async def handle_telegram_message(self, source: 'AbstractUser', sender: p.Puppet,
evt: Message) -> None:
@@ -450,9 +412,9 @@ class PortalTelegram(BasePortal, ABC):
intent = sender.intent if sender else self.main_intent
if not media and evt.message:
is_bot = sender.is_bot if sender else False
response = await self.handle_telegram_text(source, intent, is_bot, evt)
event_id = await self.handle_telegram_text(source, intent, is_bot, evt)
elif media:
response = await {
event_id = await {
MessageMediaPhoto: self.handle_telegram_photo,
MessageMediaDocument: self.handle_telegram_document,
MessageMediaGeo: self.handle_telegram_location,
@@ -465,33 +427,31 @@ class PortalTelegram(BasePortal, ABC):
self.log.debug("Unhandled Telegram message: %s", evt)
return
if not response:
if not event_id:
return
mxid = response["event_id"]
prev_id = self.dedup.update(evt, (mxid, tg_space), (temporary_identifier, tg_space))
prev_id = self.dedup.update(evt, (event_id, tg_space), (temporary_identifier, tg_space))
if prev_id:
self.log.debug(f"Sent message {evt.id}@{tg_space} to Matrix as {mxid}. "
self.log.debug(f"Sent message {evt.id}@{tg_space} to Matrix as {event_id}. "
f"Temporary dedup identifier was {temporary_identifier}, "
f"but dedup map contained {prev_id[1]} instead! -- "
"This was probably a race condition caused by Telegram sending updates"
"to other clients before responding to the sender. I'll just redact "
"the likely duplicate message now.")
await intent.redact(self.mxid, mxid)
await intent.redact(self.mxid, event_id)
return
self.log.debug("Handled Telegram message: %s", evt)
try:
DBMessage(tgid=TelegramID(evt.id), mx_room=self.mxid, mxid=mxid,
DBMessage(tgid=TelegramID(evt.id), mx_room=self.mxid, mxid=event_id,
tg_space=tg_space, edit_index=0).insert()
DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=mxid)
DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id)
except IntegrityError as e:
self.log.exception(f"{e.__class__.__name__} while saving message mapping. "
"This might mean that an update was handled after it left the "
"dedup cache queue. You can try enabling bridge.deduplication."
"pre_db_check in the config.")
await intent.redact(self.mxid, mxid)
await intent.redact(self.mxid, event_id)
async def _create_room_on_action(self, source: 'AbstractUser',
action: TypeMessageAction) -> bool:
@@ -544,9 +504,9 @@ class PortalTelegram(BasePortal, ABC):
levels = await self.main_intent.get_power_levels(self.mxid)
if user:
levels["users"][user.mxid] = 50
levels.users[user.mxid] = 50
if puppet:
levels["users"][puppet.mxid] = 50
levels.users[puppet.mxid] = 50
await self.main_intent.set_power_levels(self.mxid, levels)
async def receive_telegram_pin_sender(self, sender: p.Puppet) -> None:
@@ -578,9 +538,9 @@ class PortalTelegram(BasePortal, ABC):
async def set_telegram_admins_enabled(self, enabled: bool) -> None:
level = 50 if enabled else 10
levels = await self.main_intent.get_power_levels(self.mxid)
levels["invite"] = level
levels["events"]["m.room.name"] = level
levels["events"]["m.room.avatar"] = level
levels.invite = level
levels.events[EventType.ROOM_NAME] = level
levels.events[EventType.ROOM_AVATAR] = level
await self.main_intent.set_power_levels(self.mxid, levels)