Add command to vote in polls. Fixes #257
This commit is contained in:
@@ -22,7 +22,7 @@ import commonmark
|
||||
|
||||
from telethon.errors import FloodWaitError
|
||||
|
||||
from ..types import MatrixRoomID
|
||||
from ..types import MatrixRoomID, MatrixEventID
|
||||
from ..util import format_duration
|
||||
from .. import user as u, context as c
|
||||
|
||||
@@ -60,8 +60,9 @@ md_renderer = HtmlEscapingRenderer()
|
||||
|
||||
|
||||
class CommandEvent:
|
||||
def __init__(self, processor: 'CommandProcessor', room: MatrixRoomID, sender: u.User,
|
||||
command: str, args: List[str], is_management: bool, is_portal: bool) -> None:
|
||||
def __init__(self, processor: 'CommandProcessor', room: MatrixRoomID, event: MatrixEventID,
|
||||
sender: u.User, command: str, args: List[str], is_management: bool,
|
||||
is_portal: bool) -> None:
|
||||
self.az = processor.az
|
||||
self.log = processor.log
|
||||
self.loop = processor.loop
|
||||
@@ -70,6 +71,7 @@ class CommandEvent:
|
||||
self.public_website = processor.public_website
|
||||
self.command_prefix = processor.command_prefix
|
||||
self.room_id = room
|
||||
self.event_id = event
|
||||
self.sender = sender
|
||||
self.command = command
|
||||
self.args = args
|
||||
@@ -89,6 +91,9 @@ class CommandEvent:
|
||||
html = message
|
||||
return self.az.intent.send_notice(self.room_id, message, html=html)
|
||||
|
||||
def mark_read(self) -> Awaitable[Dict]:
|
||||
return self.az.intent.mark_read(self.room_id, self.event_id)
|
||||
|
||||
|
||||
class CommandHandler:
|
||||
def __init__(self, handler: Callable[[CommandEvent], Awaitable[Dict]], needs_auth: bool,
|
||||
@@ -175,9 +180,10 @@ class CommandProcessor:
|
||||
self.public_website = context.public_website
|
||||
self.command_prefix = self.config["bridge.command_prefix"]
|
||||
|
||||
async def handle(self, room: MatrixRoomID, sender: u.User, command: str, args: List[str],
|
||||
is_management: bool, is_portal: bool) -> Optional[Dict]:
|
||||
evt = CommandEvent(self, room, sender, command, args, is_management, is_portal)
|
||||
async def handle(self, room: MatrixRoomID, event_id: MatrixEventID, sender: u.User,
|
||||
command: str, args: List[str], is_management: bool, is_portal: bool
|
||||
) -> Optional[Dict]:
|
||||
evt = CommandEvent(self, room, event_id, sender, command, args, is_management, is_portal)
|
||||
orig_command = command
|
||||
command = command.lower()
|
||||
try:
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import Dict, Callable, Optional
|
||||
from ...types import MatrixRoomID
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||
from .util import user_has_power_level, get_initial_state
|
||||
from .util import user_has_power_level
|
||||
|
||||
|
||||
async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
|
||||
|
||||
@@ -22,7 +22,7 @@ from telethon.tl.types import Authorization
|
||||
from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest,
|
||||
ResetAuthorizationRequest)
|
||||
|
||||
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
|
||||
from .. import command_handler, CommandEvent, SECTION_AUTH
|
||||
|
||||
|
||||
@command_handler(needs_auth=True,
|
||||
|
||||
@@ -23,9 +23,9 @@ from telethon.errors import (
|
||||
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
|
||||
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError)
|
||||
|
||||
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
|
||||
from mautrix_telegram import puppet as pu, user as u
|
||||
from mautrix_telegram.util import format_duration, ignore_coro
|
||||
from ... import puppet as pu, user as u
|
||||
from ...commands import command_handler, CommandEvent, SECTION_AUTH
|
||||
from ...util import format_duration, ignore_coro
|
||||
|
||||
|
||||
@command_handler(needs_auth=False,
|
||||
|
||||
@@ -19,18 +19,21 @@ import codecs
|
||||
import base64
|
||||
import re
|
||||
|
||||
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError,
|
||||
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError, OptionsTooMuchError,
|
||||
UserAlreadyParticipantError)
|
||||
from telethon.tl.types import User as TLUser, TypeUpdates, MessageMediaGame
|
||||
from telethon.tl.patched import Message
|
||||
from telethon.tl.types import (User as TLUser, TypeUpdates, MessageMediaGame, MessageMediaPoll,
|
||||
TypePeer)
|
||||
from telethon.tl.types.messages import BotCallbackAnswer
|
||||
from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatInviteRequest,
|
||||
GetBotCallbackAnswerRequest)
|
||||
GetBotCallbackAnswerRequest, SendVoteRequest)
|
||||
from telethon.tl.functions.channels import JoinChannelRequest
|
||||
|
||||
from mautrix_telegram import puppet as pu, portal as po
|
||||
from mautrix_telegram.db import Message as DBMessage
|
||||
from mautrix_telegram.types import TelegramID
|
||||
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
|
||||
from ... import puppet as pu, portal as po
|
||||
from ...abstract_user import AbstractUser
|
||||
from ...db import Message as DBMessage
|
||||
from ...types import TelegramID
|
||||
from ...commands import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC,
|
||||
@@ -167,6 +170,45 @@ async def sync(evt: CommandEvent) -> Optional[Dict]:
|
||||
PEER_TYPE_CHAT = b"g"
|
||||
|
||||
|
||||
class MessageIDError(ValueError):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
async def _parse_encoded_msgid(user: AbstractUser, enc_id: str, type_name: str
|
||||
) -> Tuple[TypePeer, Message]:
|
||||
try:
|
||||
enc_id += (4 - len(enc_id) % 4) * "="
|
||||
enc_id = base64.b64decode(enc_id)
|
||||
peer_type, enc_id = bytes([enc_id[0]]), enc_id[1:]
|
||||
tgid = TelegramID(int(codecs.encode(enc_id[0:5], "hex_codec"), 16))
|
||||
msg_id = TelegramID(int(codecs.encode(enc_id[5:10], "hex_codec"), 16))
|
||||
space = None
|
||||
if peer_type == PEER_TYPE_CHAT:
|
||||
space = TelegramID(int(codecs.encode(enc_id[10:15], "hex_codec"), 16))
|
||||
except ValueError as e:
|
||||
raise MessageIDError(f"Invalid {type_name} ID (format)") from e
|
||||
|
||||
if peer_type == PEER_TYPE_CHAT:
|
||||
orig_msg = DBMessage.get_by_tgid(msg_id, space)
|
||||
if not orig_msg:
|
||||
raise MessageIDError(f"Invalid {type_name} ID (original message not found in db)")
|
||||
new_msg = DBMessage.get_by_mxid(orig_msg.mxid, orig_msg.mx_room, user.tgid)
|
||||
if not new_msg:
|
||||
raise MessageIDError(f"Invalid {type_name} ID (your copy of message not found in db)")
|
||||
msg_id = new_msg.tgid
|
||||
try:
|
||||
peer = await user.client.get_input_entity(tgid)
|
||||
except ValueError as e:
|
||||
raise MessageIDError(f"Invalid {type_name} ID (chat not found)") from e
|
||||
|
||||
msg = await user.client.get_messages(entity=peer, ids=msg_id)
|
||||
if not msg:
|
||||
raise MessageIDError(f"Invalid {type_name} ID (message not found)")
|
||||
return peer, msg
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC,
|
||||
help_args="<_play ID_>",
|
||||
help_text="Play a Telegram game.")
|
||||
@@ -179,38 +221,45 @@ async def play(evt: CommandEvent) -> Optional[Dict]:
|
||||
return await evt.reply("Bots can't play games :(")
|
||||
|
||||
try:
|
||||
play_id = evt.args[0]
|
||||
play_id += (4 - len(play_id) % 4) * "="
|
||||
play_id = base64.b64decode(play_id)
|
||||
peer_type, play_id = bytes([play_id[0]]), play_id[1:]
|
||||
tgid = TelegramID(int(codecs.encode(play_id[0:5], "hex_codec"), 16))
|
||||
msg_id = TelegramID(int(codecs.encode(play_id[5:10], "hex_codec"), 16))
|
||||
space = None
|
||||
if peer_type == PEER_TYPE_CHAT:
|
||||
space = TelegramID(int(codecs.encode(play_id[10:15], "hex_codec"), 16))
|
||||
except ValueError:
|
||||
return await evt.reply("Invalid play ID (format)")
|
||||
peer, msg = await _parse_encoded_msgid(evt.sender, evt.args[0], type_name="play")
|
||||
except MessageIDError as e:
|
||||
return await evt.reply(e.message)
|
||||
|
||||
if peer_type == 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:
|
||||
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):
|
||||
if 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))
|
||||
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}")
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_MISC,
|
||||
help_args="<_poll ID_> <_choice ID_>",
|
||||
help_text="Vote in a Telegram poll.")
|
||||
async def vote(evt: CommandEvent) -> Optional[Dict]:
|
||||
if len(evt.args) < 2:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp vote <poll ID> <choice ID>`")
|
||||
elif not await evt.sender.is_logged_in():
|
||||
return await evt.reply("You must be logged in with a real account to vote in polls.")
|
||||
elif evt.sender.is_bot:
|
||||
return await evt.reply("Bots can't vote in polls :(")
|
||||
|
||||
try:
|
||||
peer, msg = await _parse_encoded_msgid(evt.sender, evt.args[0], type_name="poll")
|
||||
except MessageIDError as e:
|
||||
return await evt.reply(e.message)
|
||||
|
||||
if not isinstance(msg.media, MessageMediaPoll):
|
||||
return await evt.reply("Invalid poll ID (message doesn't look like a poll)")
|
||||
|
||||
options = [base64.b64decode(option + (4 - len(option) % 4) * "=")
|
||||
for option in evt.args[1:]]
|
||||
try:
|
||||
resp = await evt.sender.client(SendVoteRequest(peer=peer, msg_id=msg.id, options=options))
|
||||
except OptionsTooMuchError:
|
||||
return await evt.reply("You passed too many options.")
|
||||
# TODO use response
|
||||
return await evt.mark_read()
|
||||
|
||||
@@ -248,7 +248,7 @@ class MatrixHandler:
|
||||
# Not enough values to unpack, i.e. no arguments
|
||||
command = text
|
||||
args = []
|
||||
await self.commands.handle(room, sender, command, args, is_management,
|
||||
await self.commands.handle(room, event_id, sender, command, args, is_management,
|
||||
is_portal=portal is not None)
|
||||
|
||||
@staticmethod
|
||||
|
||||
+32
-20
@@ -48,7 +48,7 @@ from telethon.tl.patched import Message, MessageService
|
||||
from telethon.tl.types import (
|
||||
Channel, ChatAdminRights, ChatBannedRights, ChannelFull, ChannelParticipantAdmin,
|
||||
ChannelParticipantCreator, ChannelParticipantsRecent, ChannelParticipantsSearch, Chat, ChatFull,
|
||||
ChatInviteEmpty, ChatParticipantAdmin, ChatParticipantCreator, ChatPhoto, Poll,
|
||||
ChatInviteEmpty, ChatParticipantAdmin, ChatParticipantCreator, ChatPhoto, Poll, PollAnswer,
|
||||
DocumentAttributeFilename, DocumentAttributeImageSize, DocumentAttributeSticker,
|
||||
DocumentAttributeVideo, FileLocation, GeoPoint, InputChannel, InputChatUploadedPhoto,
|
||||
InputPeerChannel, InputPeerChat, InputPeerUser, InputUser, InputUserSelf,
|
||||
@@ -1502,14 +1502,23 @@ class Portal:
|
||||
"net.maunium.telegram.unsupported": True,
|
||||
}, timestamp=evt.date, external_url=self.get_external_url(evt))
|
||||
|
||||
async def handle_telegram_poll(self, _: 'AbstractUser', intent: IntentAPI, evt: Message,
|
||||
async def handle_telegram_poll(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
|
||||
relates_to: dict) -> dict:
|
||||
poll = evt.media.poll # type: Poll
|
||||
poll_id = self._encode_msgid(source, evt)
|
||||
text = (f"Poll: {poll.question}\n\n"
|
||||
+ "\n".join(f"* {answer.text}" for answer in poll.answers))
|
||||
html = (f"<strong>Poll:</strong> {poll.question}<br/>\n<ol>"
|
||||
+ "\n".join(f"<li>{answer.text}</li>" for answer in poll.answers)
|
||||
+ "</ol>")
|
||||
+ "\n".join(f"* {answer.text}" for answer in poll.answers)
|
||||
+ "\n"
|
||||
+ f"Poll ID: {poll_id}")
|
||||
|
||||
def enc(answer: PollAnswer) -> str:
|
||||
return base64.b64encode(answer.option).decode("utf-8").rstrip("=")
|
||||
|
||||
html = (f"<strong>Poll:</strong> {poll.question}<br/>\n<ul>"
|
||||
+ "\n".join(f"<li><code>{enc(answer)}</code>: {answer.text}</li>"
|
||||
for answer in poll.answers)
|
||||
+ "</ul>\n"
|
||||
+ f"Poll ID: <code>{poll_id}</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,
|
||||
@@ -1520,25 +1529,28 @@ class Portal:
|
||||
hex_value = "{0:010x}".format(i)
|
||||
return codecs.decode(hex_value, "hex_codec")
|
||||
|
||||
async def handle_telegram_game(self, source: 'AbstractUser', intent: IntentAPI,
|
||||
evt: Message, _: dict = None):
|
||||
game = evt.media.game
|
||||
def _encode_msgid(self, source: 'AbstractUser', evt: Message) -> str:
|
||||
if self.peer_type == "channel":
|
||||
play_id = base64.b64encode(b"c"
|
||||
+ self._int_to_bytes(self.tgid)
|
||||
+ self._int_to_bytes(evt.id))
|
||||
play_id = (b"c"
|
||||
+ self._int_to_bytes(self.tgid)
|
||||
+ self._int_to_bytes(evt.id))
|
||||
elif self.peer_type == "chat":
|
||||
play_id = base64.b64encode(b"g"
|
||||
+ self._int_to_bytes(self.tgid)
|
||||
+ self._int_to_bytes(evt.id)
|
||||
+ self._int_to_bytes(source.tgid))
|
||||
play_id = (b"g"
|
||||
+ self._int_to_bytes(self.tgid)
|
||||
+ self._int_to_bytes(evt.id)
|
||||
+ self._int_to_bytes(source.tgid))
|
||||
elif self.peer_type == "user":
|
||||
play_id = base64.b64encode(b"u"
|
||||
+ self._int_to_bytes(self.tgid)
|
||||
+ self._int_to_bytes(evt.id))
|
||||
play_id = (b"u"
|
||||
+ self._int_to_bytes(self.tgid)
|
||||
+ self._int_to_bytes(evt.id))
|
||||
else:
|
||||
raise ValueError("Portal has invalid peer type")
|
||||
play_id = play_id.decode("utf-8").rstrip("=")
|
||||
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):
|
||||
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="")]
|
||||
|
||||
Reference in New Issue
Block a user