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: