diff --git a/mautrix_telegram/commands/telegram.py b/mautrix_telegram/commands/telegram.py index eb2b84e6..86a4cc8f 100644 --- a/mautrix_telegram/commands/telegram.py +++ b/mautrix_telegram/commands/telegram.py @@ -14,17 +14,21 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Awaitable, Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple +import base64 import re -from telethon.errors import ( - InviteHashInvalidError, InviteHashExpiredError, UserAlreadyParticipantError) -from telethon.tl.types import User as TLUser -from telethon.tl.types import TypeUpdates -from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest +from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError, + UserAlreadyParticipantError) +from telethon.tl.types import User as TLUser, TypeUpdates, MessageMediaGame, PeerChat +from telethon.tl.types.messages import BotCallbackAnswer +from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatInviteRequest, + GetBotCallbackAnswerRequest) from telethon.tl.functions.channels import JoinChannelRequest from .. import puppet as pu, portal as po +from ..db import Message as DBMessage +from ..types import TelegramID from . import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS @@ -158,3 +162,54 @@ async def sync(evt: CommandEvent) -> Optional[Dict]: if not sync_only or sync_only == "me": await evt.sender.update_info() return await evt.reply("Synchronization complete.") + + +@command_handler(help_section=SECTION_MISC, + help_args="", + help_text="Play a Telegram game") +async def play(evt: CommandEvent) -> Optional[Dict]: + if len(evt.args) < 1: + return await evt.reply("**Usage:** `$cmdprefix+sp play `") + elif not await evt.sender.is_logged_in(): + return await evt.reply("You must be logged in with a real account to play games.") + elif evt.sender.is_bot: + return await evt.reply("Bots can't play games :(") + + try: + space = None + peer_type, play_id = base64.b64decode(evt.args[0]).decode("utf-8").split("-", 1) + if peer_type == "chan" or peer_type == "user": + tgid, msg_id = play_id.split("-") + elif peer_type == "chat": + tgid, space, msg_id = play_id.split("-") + space = TelegramID(int(space)) + else: + raise ValueError() + tgid = TelegramID(int(tgid)) + msg_id = TelegramID(int(msg_id)) + except ValueError: + return await evt.reply("Invalid play ID (format)") + + if peer_type == "chat": + orig_msg = DBMessage.get_by_tgid(msg_id, space) + if not orig_msg: + return await evt.reply("Invalid play ID (original message not found in db)") + new_msg = DBMessage.get_by_mxid(orig_msg.mxid, orig_msg.mx_room, evt.sender.tgid) + if not new_msg: + return await evt.reply("Invalid play ID (your copy of message not found in db)") + msg_id = new_msg.tgid + try: + peer = await evt.sender.client.get_input_entity(tgid) + except ValueError as e: + return await evt.reply("Invalid play ID (chat not found)") + + msg = await evt.sender.client.get_messages(entity=peer, ids=msg_id) + if not msg or not isinstance(msg.media, MessageMediaGame): + return await evt.reply("Invalid play ID (message doesn't look like a game)") + + game = await evt.sender.client(GetBotCallbackAnswerRequest(peer=peer, msg_id=msg_id, game=True)) + if not isinstance(game, BotCallbackAnswer): + return await evt.reply("Game request response invalid") + + await evt.reply(f"Click [here]({game.url}) to play {msg.media.game.title}:\n\n" + f"{msg.media.game.description}") diff --git a/mautrix_telegram/formatter/from_telegram.py b/mautrix_telegram/formatter/from_telegram.py index eb132b47..59554a07 100644 --- a/mautrix_telegram/formatter/from_telegram.py +++ b/mautrix_telegram/formatter/from_telegram.py @@ -179,9 +179,12 @@ async def _add_reply_header(source: "AbstractUser", text: str, html: str, evt: M async def telegram_to_matrix(evt: Message, source: "AbstractUser", main_intent: Optional[IntentAPI] = None, is_edit: bool = False, prefix_text: Optional[str] = None, - prefix_html: Optional[str] = None) -> Tuple[str, str, Dict]: - text = add_surrogates(evt.message) - html = _telegram_entities_to_matrix_catch(text, evt.entities) if evt.entities else None + prefix_html: Optional[str] = None, override_text: str = None, + override_entities: List[TypeMessageEntity] = None + ) -> Tuple[str, str, Dict]: + text = add_surrogates(override_text or evt.message) + entities = override_entities or evt.entities + html = _telegram_entities_to_matrix_catch(text, entities) if entities else None relates_to = {} # type: Dict if prefix_html: diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index 8e89644d..895ee727 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -23,6 +23,7 @@ import asyncio import random import mimetypes import unicodedata +import base64 import hashlib import logging import json @@ -55,12 +56,12 @@ from telethon.tl.types import ( MessageActionChatDeletePhoto, MessageActionChatDeleteUser, MessageActionChatEditPhoto, MessageActionChatEditTitle, MessageActionChatJoinedByLink, MessageActionChatMigrateTo, MessageActionPinMessage, MessageMediaContact, MessageMediaDocument, MessageMediaGeo, - MessageMediaPhoto, MessageMediaUnsupported, MessageService, PeerChannel, PeerChat, PeerUser, - Photo, PhotoCachedSize, SendMessageCancelAction, SendMessageTypingAction, + MessageMediaPhoto, MessageMediaUnsupported, MessageMediaGame, MessageService, PeerChannel, + PeerChat, PeerUser, Photo, PhotoCachedSize, SendMessageCancelAction, SendMessageTypingAction, TypeChannelParticipant, TypeChat, TypeChatParticipant, TypeDocumentAttribute, TypeInputPeer, TypeMessageAction, TypeMessageEntity, TypePeer, TypePhotoSize, TypeUpdates, TypeUser, TypeUserFull, UpdateChatUserTyping, UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping, - User, UserFull) + User, UserFull, MessageEntityCode) from mautrix_appservice import MatrixRequestError, IntentError, AppService, IntentAPI from .types import MatrixEventID, MatrixRoomID, MatrixUserID, TelegramID @@ -1414,13 +1415,11 @@ class Portal: async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, relates_to: dict = None) -> dict: - prev = evt.message - evt.message = ("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(evt, source, - self.main_intent) - evt.message = prev + 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( + evt, source, self.main_intent, override_text=override_text) await intent.set_typing(self.mxid, is_typing=False) return await intent.send_message(self.mxid, { "body": text, @@ -1431,6 +1430,35 @@ class Portal: "net.maunium.telegram.unsupported": True, }, timestamp=evt.date, external_url=self.get_external_url(evt)) + async def handle_telegram_game(self, source: 'AbstractUser', intent: IntentAPI, + evt: Message, relates_to: dict = None): + game = evt.media.game + if self.peer_type == "channel": + play_id = base64.b64encode(f"chan-{self.tgid}-{evt.id}".encode("utf-8")) + elif self.peer_type == "chat": + play_id = base64.b64encode(f"chat-{self.tgid}-{source.tgid}-{evt.id}".encode("utf-8")) + elif self.peer_type == "user": + play_id = base64.b64encode(f"user-{self.tgid}-{evt.id}".encode("utf-8")) + else: + raise ValueError("Portal has invalid peer type") + play_id = play_id.decode("utf-8") + command = f"!tg play {play_id}" + override_text = (f"Run {command} in your bridge management room to " + f"play {game.title}:\n\n{game.description}") + override_entities = [MessageEntityCode(offset=len("Run "), length=len(command))] + text, html, relates_to = 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)) + async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message) -> None: if not self.mxid: @@ -1514,7 +1542,7 @@ class Portal: entity = await source.client.get_entity(PeerUser(sender.tgid)) await sender.update_info(source, entity) - allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo, + allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo, MessageMediaGame, MessageMediaUnsupported) media = evt.media if hasattr(evt, "media") and isinstance(evt.media, allowed_media) else None @@ -1528,6 +1556,7 @@ class Portal: MessageMediaDocument: self.handle_telegram_document, MessageMediaGeo: self.handle_telegram_location, MessageMediaUnsupported: self.handle_telegram_unsupported, + MessageMediaGame: self.handle_telegram_game, }[type(media)](source, intent, evt, relates_to=formatter.telegram_reply_to_matrix(evt, source)) else: