Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cead705c21 | |||
| e5a2afee37 | |||
| f2efb235eb | |||
| ffc1a5ad8f | |||
| 1c3764b099 | |||
| 5af045844e | |||
| be255ec7af | |||
| 7f7dec4e80 | |||
| 8a6687d00c |
@@ -144,6 +144,8 @@ bridge:
|
|||||||
# Use inline images instead of a separate message for the caption.
|
# Use inline images instead of a separate message for the caption.
|
||||||
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
|
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
|
||||||
inline_images: false
|
inline_images: false
|
||||||
|
# Maximum size of image in megabytes before sending to Telegram as a document.
|
||||||
|
image_as_file_size: 10
|
||||||
|
|
||||||
# Whether to bridge Telegram bot messages as m.notices or m.texts.
|
# Whether to bridge Telegram bot messages as m.notices or m.texts.
|
||||||
bot_messages_as_notices: true
|
bot_messages_as_notices: true
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "0.5.0rc2"
|
__version__ = "0.5.0rc3"
|
||||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ session_container = AlchemySessionContainer(engine=db_engine, session=db_session
|
|||||||
manage_tables=False)
|
manage_tables=False)
|
||||||
session_container.core_mode = True
|
session_container.core_mode = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uvloop
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
log.debug("Using uvloop for asyncio")
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
loop = asyncio.get_event_loop() # type: asyncio.AbstractEventLoop
|
loop = asyncio.get_event_loop() # type: asyncio.AbstractEventLoop
|
||||||
|
|
||||||
state_store = SQLStateStore()
|
state_store = SQLStateStore()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import commonmark
|
|||||||
|
|
||||||
from telethon.errors import FloodWaitError
|
from telethon.errors import FloodWaitError
|
||||||
|
|
||||||
from ..types import MatrixRoomID
|
from ..types import MatrixRoomID, MatrixEventID
|
||||||
from ..util import format_duration
|
from ..util import format_duration
|
||||||
from .. import user as u, context as c
|
from .. import user as u, context as c
|
||||||
|
|
||||||
@@ -60,8 +60,9 @@ md_renderer = HtmlEscapingRenderer()
|
|||||||
|
|
||||||
|
|
||||||
class CommandEvent:
|
class CommandEvent:
|
||||||
def __init__(self, processor: 'CommandProcessor', room: MatrixRoomID, sender: u.User,
|
def __init__(self, processor: 'CommandProcessor', room: MatrixRoomID, event: MatrixEventID,
|
||||||
command: str, args: List[str], is_management: bool, is_portal: bool) -> None:
|
sender: u.User, command: str, args: List[str], is_management: bool,
|
||||||
|
is_portal: bool) -> None:
|
||||||
self.az = processor.az
|
self.az = processor.az
|
||||||
self.log = processor.log
|
self.log = processor.log
|
||||||
self.loop = processor.loop
|
self.loop = processor.loop
|
||||||
@@ -70,6 +71,7 @@ class CommandEvent:
|
|||||||
self.public_website = processor.public_website
|
self.public_website = processor.public_website
|
||||||
self.command_prefix = processor.command_prefix
|
self.command_prefix = processor.command_prefix
|
||||||
self.room_id = room
|
self.room_id = room
|
||||||
|
self.event_id = event
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
self.command = command
|
self.command = command
|
||||||
self.args = args
|
self.args = args
|
||||||
@@ -89,6 +91,9 @@ class CommandEvent:
|
|||||||
html = message
|
html = message
|
||||||
return self.az.intent.send_notice(self.room_id, message, html=html)
|
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:
|
class CommandHandler:
|
||||||
def __init__(self, handler: Callable[[CommandEvent], Awaitable[Dict]], needs_auth: bool,
|
def __init__(self, handler: Callable[[CommandEvent], Awaitable[Dict]], needs_auth: bool,
|
||||||
@@ -175,9 +180,10 @@ class CommandProcessor:
|
|||||||
self.public_website = context.public_website
|
self.public_website = context.public_website
|
||||||
self.command_prefix = self.config["bridge.command_prefix"]
|
self.command_prefix = self.config["bridge.command_prefix"]
|
||||||
|
|
||||||
async def handle(self, room: MatrixRoomID, sender: u.User, command: str, args: List[str],
|
async def handle(self, room: MatrixRoomID, event_id: MatrixEventID, sender: u.User,
|
||||||
is_management: bool, is_portal: bool) -> Optional[Dict]:
|
command: str, args: List[str], is_management: bool, is_portal: bool
|
||||||
evt = CommandEvent(self, room, sender, command, args, is_management, is_portal)
|
) -> Optional[Dict]:
|
||||||
|
evt = CommandEvent(self, room, event_id, sender, command, args, is_management, is_portal)
|
||||||
orig_command = command
|
orig_command = command
|
||||||
command = command.lower()
|
command = command.lower()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from typing import Dict, Callable, Optional
|
|||||||
from ...types import MatrixRoomID
|
from ...types import MatrixRoomID
|
||||||
from ... import portal as po
|
from ... import portal as po
|
||||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
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,
|
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,
|
from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest,
|
||||||
ResetAuthorizationRequest)
|
ResetAuthorizationRequest)
|
||||||
|
|
||||||
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
|
from .. import command_handler, CommandEvent, SECTION_AUTH
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=True,
|
@command_handler(needs_auth=True,
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ from telethon.errors import (
|
|||||||
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
|
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
|
||||||
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError)
|
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError)
|
||||||
|
|
||||||
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
|
from ... import puppet as pu, user as u
|
||||||
from mautrix_telegram import puppet as pu, user as u
|
from ...commands import command_handler, CommandEvent, SECTION_AUTH
|
||||||
from mautrix_telegram.util import format_duration, ignore_coro
|
from ...util import format_duration, ignore_coro
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False,
|
@command_handler(needs_auth=False,
|
||||||
|
|||||||
@@ -19,18 +19,21 @@ import codecs
|
|||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError,
|
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError, OptionsTooMuchError,
|
||||||
UserAlreadyParticipantError)
|
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.types.messages import BotCallbackAnswer
|
||||||
from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatInviteRequest,
|
from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatInviteRequest,
|
||||||
GetBotCallbackAnswerRequest)
|
GetBotCallbackAnswerRequest, SendVoteRequest)
|
||||||
from telethon.tl.functions.channels import JoinChannelRequest
|
from telethon.tl.functions.channels import JoinChannelRequest
|
||||||
|
|
||||||
from mautrix_telegram import puppet as pu, portal as po
|
from ... import puppet as pu, portal as po
|
||||||
from mautrix_telegram.db import Message as DBMessage
|
from ...abstract_user import AbstractUser
|
||||||
from mautrix_telegram.types import TelegramID
|
from ...db import Message as DBMessage
|
||||||
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
|
from ...types import TelegramID
|
||||||
|
from ...commands import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
|
||||||
|
|
||||||
|
|
||||||
@command_handler(help_section=SECTION_MISC,
|
@command_handler(help_section=SECTION_MISC,
|
||||||
@@ -167,6 +170,45 @@ async def sync(evt: CommandEvent) -> Optional[Dict]:
|
|||||||
PEER_TYPE_CHAT = b"g"
|
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,
|
@command_handler(help_section=SECTION_MISC,
|
||||||
help_args="<_play ID_>",
|
help_args="<_play ID_>",
|
||||||
help_text="Play a Telegram game.")
|
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 :(")
|
return await evt.reply("Bots can't play games :(")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
play_id = evt.args[0]
|
peer, msg = await _parse_encoded_msgid(evt.sender, evt.args[0], type_name="play")
|
||||||
play_id += (4 - len(play_id) % 4) * "="
|
except MessageIDError as e:
|
||||||
play_id = base64.b64decode(play_id)
|
return await evt.reply(e.message)
|
||||||
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)")
|
|
||||||
|
|
||||||
if peer_type == PEER_TYPE_CHAT:
|
if not isinstance(msg.media, MessageMediaGame):
|
||||||
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):
|
|
||||||
return await evt.reply("Invalid play ID (message doesn't look like a game)")
|
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):
|
if not isinstance(game, BotCallbackAnswer):
|
||||||
return await evt.reply("Game request response invalid")
|
return await evt.reply("Game request response invalid")
|
||||||
|
|
||||||
await evt.reply(f"Click [here]({game.url}) to play {msg.media.game.title}:\n\n"
|
await evt.reply(f"Click [here]({game.url}) to play {msg.media.game.title}:\n\n"
|
||||||
f"{msg.media.game.description}")
|
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()
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ class Config(DictWithRecursion):
|
|||||||
copy("bridge.sync_with_custom_puppets")
|
copy("bridge.sync_with_custom_puppets")
|
||||||
copy("bridge.telegram_link_preview")
|
copy("bridge.telegram_link_preview")
|
||||||
copy("bridge.inline_images")
|
copy("bridge.inline_images")
|
||||||
|
copy("bridge.image_as_file_size")
|
||||||
|
|
||||||
copy("bridge.bot_messages_as_notices")
|
copy("bridge.bot_messages_as_notices")
|
||||||
if isinstance(self["bridge.bridge_notices"], bool):
|
if isinstance(self["bridge.bridge_notices"], bool):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String
|
|||||||
from sqlalchemy.engine.result import RowProxy
|
from sqlalchemy.engine.result import RowProxy
|
||||||
from typing import Optional, Iterable, Tuple
|
from typing import Optional, Iterable, Tuple
|
||||||
|
|
||||||
from ..types import MatrixUserID, MatrixRoomID, TelegramID
|
from ..types import MatrixUserID, TelegramID
|
||||||
from .base import Base
|
from .base import Base
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ class MatrixHandler:
|
|||||||
# Not enough values to unpack, i.e. no arguments
|
# Not enough values to unpack, i.e. no arguments
|
||||||
command = text
|
command = text
|
||||||
args = []
|
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)
|
is_portal=portal is not None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
+62
-29
@@ -38,17 +38,17 @@ from telethon.tl.functions.messages import (
|
|||||||
EditChatPhotoRequest, EditChatTitleRequest, ExportChatInviteRequest, GetFullChatRequest,
|
EditChatPhotoRequest, EditChatTitleRequest, ExportChatInviteRequest, GetFullChatRequest,
|
||||||
UpdatePinnedMessageRequest, MigrateChatRequest, SetTypingRequest, EditChatAboutRequest)
|
UpdatePinnedMessageRequest, MigrateChatRequest, SetTypingRequest, EditChatAboutRequest)
|
||||||
from telethon.tl.functions.channels import (
|
from telethon.tl.functions.channels import (
|
||||||
CreateChannelRequest, EditAdminRequest, EditBannedRequest, EditPhotoRequest,
|
CreateChannelRequest, EditAdminRequest, EditBannedRequest, EditPhotoRequest, EditTitleRequest,
|
||||||
EditTitleRequest, GetParticipantsRequest, InviteToChannelRequest,
|
GetParticipantsRequest, InviteToChannelRequest, JoinChannelRequest, LeaveChannelRequest,
|
||||||
JoinChannelRequest, LeaveChannelRequest, UpdateUsernameRequest)
|
UpdateUsernameRequest)
|
||||||
from telethon.tl.functions.messages import ReadHistoryRequest as ReadMessageHistoryRequest
|
from telethon.tl.functions.messages import ReadHistoryRequest as ReadMessageHistoryRequest
|
||||||
from telethon.tl.functions.channels import ReadHistoryRequest as ReadChannelHistoryRequest
|
from telethon.tl.functions.channels import ReadHistoryRequest as ReadChannelHistoryRequest
|
||||||
from telethon.errors import ChatAdminRequiredError, ChatNotModifiedError
|
from telethon.errors import ChatAdminRequiredError, ChatNotModifiedError
|
||||||
from telethon.tl.patched import Message, MessageService
|
from telethon.tl.patched import Message, MessageService
|
||||||
from telethon.tl.types import (
|
from telethon.tl.types import (
|
||||||
Channel, ChatAdminRights, ChatBannedRights, ChannelFull, ChannelParticipantAdmin,
|
Channel, ChatAdminRights, ChatBannedRights, ChannelFull, ChannelParticipantAdmin,
|
||||||
ChannelParticipantCreator, ChannelParticipantsRecent, ChannelParticipantsSearch, Chat,
|
ChannelParticipantCreator, ChannelParticipantsRecent, ChannelParticipantsSearch, Chat, ChatFull,
|
||||||
ChatFull, ChatInviteEmpty, ChatParticipantAdmin, ChatParticipantCreator, ChatPhoto,
|
ChatInviteEmpty, ChatParticipantAdmin, ChatParticipantCreator, ChatPhoto, Poll, PollAnswer,
|
||||||
DocumentAttributeFilename, DocumentAttributeImageSize, DocumentAttributeSticker,
|
DocumentAttributeFilename, DocumentAttributeImageSize, DocumentAttributeSticker,
|
||||||
DocumentAttributeVideo, FileLocation, GeoPoint, InputChannel, InputChatUploadedPhoto,
|
DocumentAttributeVideo, FileLocation, GeoPoint, InputChannel, InputChatUploadedPhoto,
|
||||||
InputPeerChannel, InputPeerChat, InputPeerUser, InputUser, InputUserSelf,
|
InputPeerChannel, InputPeerChat, InputPeerUser, InputUser, InputUserSelf,
|
||||||
@@ -56,12 +56,12 @@ from telethon.tl.types import (
|
|||||||
MessageActionChatDeletePhoto, MessageActionChatDeleteUser, MessageActionChatEditPhoto,
|
MessageActionChatDeletePhoto, MessageActionChatDeleteUser, MessageActionChatEditPhoto,
|
||||||
MessageActionChatEditTitle, MessageActionChatJoinedByLink, MessageActionChatMigrateTo,
|
MessageActionChatEditTitle, MessageActionChatJoinedByLink, MessageActionChatMigrateTo,
|
||||||
MessageActionPinMessage, MessageActionGameScore, MessageMediaContact, MessageMediaDocument,
|
MessageActionPinMessage, MessageActionGameScore, MessageMediaContact, MessageMediaDocument,
|
||||||
MessageMediaGeo, MessageMediaPhoto, MessageMediaUnsupported, MessageMediaGame, PeerChannel,
|
MessageMediaGeo, MessageMediaPhoto, MessageMediaUnsupported, MessageMediaGame, MessageMediaPoll,
|
||||||
PeerChat, PeerUser, Photo, PhotoCachedSize, SendMessageCancelAction, SendMessageTypingAction,
|
PeerChannel, PeerChat, PeerUser, Photo, PhotoCachedSize, SendMessageCancelAction,
|
||||||
TypeChannelParticipant, TypeChat, TypeChatParticipant, TypeDocumentAttribute, TypeInputPeer,
|
SendMessageTypingAction, TypeChannelParticipant, TypeChat, TypeChatParticipant,
|
||||||
TypeMessageAction, TypeMessageEntity, TypePeer, TypePhotoSize, TypeUpdates, TypeUser, PhotoSize,
|
TypeDocumentAttribute, TypeInputPeer, TypeMessageAction, TypeMessageEntity, TypePeer,
|
||||||
TypeUserFull, UpdateChatUserTyping, UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping,
|
TypePhotoSize, TypeUpdates, TypeUser, PhotoSize, TypeUserFull, UpdateChatUserTyping,
|
||||||
User, UserFull, MessageEntityPre)
|
UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping, User, UserFull, MessageEntityPre)
|
||||||
from mautrix_appservice import MatrixRequestError, IntentError, AppService, IntentAPI
|
from mautrix_appservice import MatrixRequestError, IntentError, AppService, IntentAPI
|
||||||
|
|
||||||
from .types import MatrixEventID, MatrixRoomID, MatrixUserID, TelegramID
|
from .types import MatrixEventID, MatrixRoomID, MatrixUserID, TelegramID
|
||||||
@@ -610,7 +610,10 @@ class Portal:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_largest_photo_size(photo: Union[Photo, List[TypePhotoSize]]) -> TypePhotoSize:
|
def _get_largest_photo_size(photo: Union[Photo, List[TypePhotoSize]]
|
||||||
|
) -> Optional[TypePhotoSize]:
|
||||||
|
if not photo:
|
||||||
|
return None
|
||||||
return max(photo.sizes if isinstance(photo, Photo) else photo, key=(lambda photo2: (
|
return max(photo.sizes if isinstance(photo, Photo) else photo, key=(lambda photo2: (
|
||||||
len(photo2.bytes) if not isinstance(photo2, PhotoSize) else photo2.size)))
|
len(photo2.bytes) if not isinstance(photo2, PhotoSize) else photo2.size)))
|
||||||
|
|
||||||
@@ -981,7 +984,9 @@ class Portal:
|
|||||||
|
|
||||||
caption = message["body"] if message["body"].lower() != file_name.lower() else None
|
caption = message["body"] if message["body"].lower() != file_name.lower() else None
|
||||||
|
|
||||||
media = await client.upload_file_direct(file, mime, attributes, file_name)
|
media = await client.upload_file_direct(
|
||||||
|
file, mime, attributes, file_name,
|
||||||
|
max_image_size=config["bridge.image_as_file_size"] * 1000 ** 2)
|
||||||
lock = self.require_send_lock(sender_id)
|
lock = self.require_send_lock(sender_id)
|
||||||
async with lock:
|
async with lock:
|
||||||
response = await client.send_media(self.peer, media, reply_to=reply_to,
|
response = await client.send_media(self.peer, media, reply_to=reply_to,
|
||||||
@@ -1406,7 +1411,7 @@ class Portal:
|
|||||||
attrs = self._parse_telegram_document_attributes(document.attributes)
|
attrs = self._parse_telegram_document_attributes(document.attributes)
|
||||||
|
|
||||||
thumb = self._get_largest_photo_size(document.thumbs)
|
thumb = self._get_largest_photo_size(document.thumbs)
|
||||||
if not isinstance(thumb, (PhotoSize, PhotoCachedSize)):
|
if thumb and not isinstance(thumb, (PhotoSize, PhotoCachedSize)):
|
||||||
self.log.debug(f"Unsupported thumbnail type {type(thumb)}")
|
self.log.debug(f"Unsupported thumbnail type {type(thumb)}")
|
||||||
thumb = None
|
thumb = None
|
||||||
file = await util.transfer_file_to_matrix(source.client, intent, document, thumb,
|
file = await util.transfer_file_to_matrix(source.client, intent, document, thumb,
|
||||||
@@ -1497,30 +1502,57 @@ class Portal:
|
|||||||
"net.maunium.telegram.unsupported": True,
|
"net.maunium.telegram.unsupported": True,
|
||||||
}, timestamp=evt.date, external_url=self.get_external_url(evt))
|
}, timestamp=evt.date, external_url=self.get_external_url(evt))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def enc(answer: PollAnswer) -> str:
|
||||||
|
return base64.b64encode(answer.option).decode("utf-8").rstrip("=")
|
||||||
|
|
||||||
|
text = (f"Poll ID {poll_id}: {poll.question}\n"
|
||||||
|
+ "\n".join(f"* {enc(answer)}: {answer.text}" for answer in poll.answers) +
|
||||||
|
"\n"
|
||||||
|
f"Vote with !tg vote <poll ID> <choice ID>")
|
||||||
|
|
||||||
|
html = (f"<strong>Poll</strong> ID <code>{poll_id}</code>: {poll.question}<br/>\n"
|
||||||
|
f"<ul>"
|
||||||
|
+ "\n".join(f"<li><code>{enc(answer)}</code>: {answer.text}</li>"
|
||||||
|
for answer in poll.answers) +
|
||||||
|
"</ul>\n"
|
||||||
|
f"Vote with <code>!tg vote <poll ID> <choice 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,
|
||||||
|
external_url=self.get_external_url(evt))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _int_to_bytes(i: int) -> bytes:
|
def _int_to_bytes(i: int) -> bytes:
|
||||||
hex_value = "{0:010x}".format(i)
|
hex_value = "{0:010x}".format(i)
|
||||||
return codecs.decode(hex_value, "hex_codec")
|
return codecs.decode(hex_value, "hex_codec")
|
||||||
|
|
||||||
async def handle_telegram_game(self, source: 'AbstractUser', intent: IntentAPI,
|
def _encode_msgid(self, source: 'AbstractUser', evt: Message) -> str:
|
||||||
evt: Message, _: dict = None):
|
|
||||||
game = evt.media.game
|
|
||||||
if self.peer_type == "channel":
|
if self.peer_type == "channel":
|
||||||
play_id = base64.b64encode(b"c"
|
play_id = (b"c"
|
||||||
+ self._int_to_bytes(self.tgid)
|
+ self._int_to_bytes(self.tgid)
|
||||||
+ self._int_to_bytes(evt.id))
|
+ self._int_to_bytes(evt.id))
|
||||||
elif self.peer_type == "chat":
|
elif self.peer_type == "chat":
|
||||||
play_id = base64.b64encode(b"g"
|
play_id = (b"g"
|
||||||
+ self._int_to_bytes(self.tgid)
|
+ self._int_to_bytes(self.tgid)
|
||||||
+ self._int_to_bytes(evt.id)
|
+ self._int_to_bytes(evt.id)
|
||||||
+ self._int_to_bytes(source.tgid))
|
+ self._int_to_bytes(source.tgid))
|
||||||
elif self.peer_type == "user":
|
elif self.peer_type == "user":
|
||||||
play_id = base64.b64encode(b"u"
|
play_id = (b"u"
|
||||||
+ self._int_to_bytes(self.tgid)
|
+ self._int_to_bytes(self.tgid)
|
||||||
+ self._int_to_bytes(evt.id))
|
+ self._int_to_bytes(evt.id))
|
||||||
else:
|
else:
|
||||||
raise ValueError("Portal has invalid peer type")
|
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}"
|
command = f"!tg play {play_id}"
|
||||||
override_text = f"Run {command} in your bridge management room to play {game.title}"
|
override_text = f"Run {command} in your bridge management room to play {game.title}"
|
||||||
override_entities = [MessageEntityPre(offset=len("Run "), length=len(command), language="")]
|
override_entities = [MessageEntityPre(offset=len("Run "), length=len(command), language="")]
|
||||||
@@ -1625,7 +1657,7 @@ class Portal:
|
|||||||
await sender.update_info(source, entity)
|
await sender.update_info(source, entity)
|
||||||
|
|
||||||
allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo, MessageMediaGame,
|
allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo, MessageMediaGame,
|
||||||
MessageMediaUnsupported)
|
MessageMediaPoll, MessageMediaUnsupported)
|
||||||
media = evt.media if hasattr(evt, "media") and isinstance(evt.media,
|
media = evt.media if hasattr(evt, "media") and isinstance(evt.media,
|
||||||
allowed_media) else None
|
allowed_media) else None
|
||||||
intent = sender.intent if sender else self.main_intent
|
intent = sender.intent if sender else self.main_intent
|
||||||
@@ -1637,6 +1669,7 @@ class Portal:
|
|||||||
MessageMediaPhoto: self.handle_telegram_photo,
|
MessageMediaPhoto: self.handle_telegram_photo,
|
||||||
MessageMediaDocument: self.handle_telegram_document,
|
MessageMediaDocument: self.handle_telegram_document,
|
||||||
MessageMediaGeo: self.handle_telegram_location,
|
MessageMediaGeo: self.handle_telegram_location,
|
||||||
|
MessageMediaPoll: self.handle_telegram_poll,
|
||||||
MessageMediaUnsupported: self.handle_telegram_unsupported,
|
MessageMediaUnsupported: self.handle_telegram_unsupported,
|
||||||
MessageMediaGame: self.handle_telegram_game,
|
MessageMediaGame: self.handle_telegram_game,
|
||||||
}[type(media)](source, intent, evt,
|
}[type(media)](source, intent, evt,
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ from telethon.tl.patched import Message
|
|||||||
class MautrixTelegramClient(TelegramClient):
|
class MautrixTelegramClient(TelegramClient):
|
||||||
async def upload_file_direct(self, file: bytes, mime_type: str = None,
|
async def upload_file_direct(self, file: bytes, mime_type: str = None,
|
||||||
attributes: List[TypeDocumentAttribute] = None,
|
attributes: List[TypeDocumentAttribute] = None,
|
||||||
file_name: str = None
|
file_name: str = None, max_image_size: float = 10 * 1000 ** 2,
|
||||||
) -> Union[InputMediaUploadedDocument, InputMediaUploadedPhoto]:
|
) -> Union[InputMediaUploadedDocument, InputMediaUploadedPhoto]:
|
||||||
file_handle = await super().upload_file(file, file_name=file_name, use_cache=False)
|
file_handle = await super().upload_file(file, file_name=file_name, use_cache=False)
|
||||||
|
|
||||||
if mime_type == "image/png" or mime_type == "image/jpeg":
|
if (mime_type == "image/png" or mime_type == "image/jpeg") and len(file) < max_image_size:
|
||||||
return InputMediaUploadedPhoto(file_handle)
|
return InputMediaUploadedPhoto(file_handle)
|
||||||
else:
|
else:
|
||||||
attributes = attributes or []
|
attributes = attributes or []
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ class User(AbstractUser):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def db_contacts(self) -> Iterable[TelegramID]:
|
def db_contacts(self) -> Iterable[TelegramID]:
|
||||||
return (puppet.id for puppet in self.contacts)
|
return (puppet.id
|
||||||
|
for puppet in self.contacts
|
||||||
|
if puppet)
|
||||||
|
|
||||||
@db_contacts.setter
|
@db_contacts.setter
|
||||||
def db_contacts(self, contacts: Iterable[TelegramID]) -> None:
|
def db_contacts(self, contacts: Iterable[TelegramID]) -> None:
|
||||||
@@ -110,7 +112,9 @@ class User(AbstractUser):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def db_portals(self) -> Iterable[Tuple[TelegramID, TelegramID]]:
|
def db_portals(self) -> Iterable[Tuple[TelegramID, TelegramID]]:
|
||||||
return (portal.tgid_full for portal in self.portals.values() if not portal.deleted)
|
return (portal.tgid_full
|
||||||
|
for portal in self.portals.values()
|
||||||
|
if portal and not portal.deleted)
|
||||||
|
|
||||||
@db_portals.setter
|
@db_portals.setter
|
||||||
def db_portals(self, portals: Iterable[Tuple[TelegramID, TelegramID]]) -> None:
|
def db_portals(self, portals: Iterable[Tuple[TelegramID, TelegramID]]) -> None:
|
||||||
@@ -132,9 +136,13 @@ class User(AbstractUser):
|
|||||||
contacts=self.db_contacts, saved_contacts=self.saved_contacts,
|
contacts=self.db_contacts, saved_contacts=self.saved_contacts,
|
||||||
portals=self.db_portals)
|
portals=self.db_portals)
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self, contacts: bool = False, portals: bool = False) -> None:
|
||||||
self.db_instance.update(tgid=self.tgid, tg_username=self.username, tg_phone=self.phone,
|
self.db_instance.update(tgid=self.tgid, tg_username=self.username, tg_phone=self.phone,
|
||||||
saved_contacts=self.saved_contacts)
|
saved_contacts=self.saved_contacts)
|
||||||
|
if contacts:
|
||||||
|
self.db_instance.contacts = self.db_contacts
|
||||||
|
if portals:
|
||||||
|
self.db_instance.portals = self.db_portals
|
||||||
|
|
||||||
def delete(self, delete_db: bool = True) -> None:
|
def delete(self, delete_db: bool = True) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -240,7 +248,7 @@ class User(AbstractUser):
|
|||||||
pass
|
pass
|
||||||
self.portals = {}
|
self.portals = {}
|
||||||
self.contacts = []
|
self.contacts = []
|
||||||
self.save()
|
self.save(portals=True, contacts=True)
|
||||||
if self.tgid:
|
if self.tgid:
|
||||||
try:
|
try:
|
||||||
del self.by_tgid[self.tgid]
|
del self.by_tgid[self.tgid]
|
||||||
@@ -295,7 +303,7 @@ class User(AbstractUser):
|
|||||||
creators.append(
|
creators.append(
|
||||||
portal.create_matrix_room(self, entity, invites=[self.mxid],
|
portal.create_matrix_room(self, entity, invites=[self.mxid],
|
||||||
synchronous=synchronous_create))
|
synchronous=synchronous_create))
|
||||||
self.save()
|
self.save(portals=True)
|
||||||
await asyncio.gather(*creators, loop=self.loop)
|
await asyncio.gather(*creators, loop=self.loop)
|
||||||
|
|
||||||
def register_portal(self, portal: po.Portal) -> None:
|
def register_portal(self, portal: po.Portal) -> None:
|
||||||
@@ -305,12 +313,12 @@ class User(AbstractUser):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
self.portals[portal.tgid_full] = portal
|
self.portals[portal.tgid_full] = portal
|
||||||
self.save()
|
self.save(portals=True)
|
||||||
|
|
||||||
def unregister_portal(self, portal: po.Portal) -> None:
|
def unregister_portal(self, portal: po.Portal) -> None:
|
||||||
try:
|
try:
|
||||||
del self.portals[portal.tgid_full]
|
del self.portals[portal.tgid_full]
|
||||||
self.save()
|
self.save_portals()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -335,7 +343,7 @@ class User(AbstractUser):
|
|||||||
puppet = pu.Puppet.get(user.id)
|
puppet = pu.Puppet.get(user.id)
|
||||||
await puppet.update_info(self, user)
|
await puppet.update_info(self, user)
|
||||||
self.contacts.append(puppet)
|
self.contacts.append(puppet)
|
||||||
self.save()
|
self.save(contacts=True)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region Class instance lookup
|
# region Class instance lookup
|
||||||
|
|||||||
Reference in New Issue
Block a user