More migrations to mautrix-python

This commit is contained in:
Tulir Asokan
2019-07-19 00:17:57 +03:00
parent 8d4a9dc231
commit eef498d47a
6 changed files with 172 additions and 138 deletions
+14 -9
View File
@@ -24,17 +24,19 @@ from telethon.tl.patched import MessageService, Message
from telethon.tl.types import ( from telethon.tl.types import (
Channel, ChannelForbidden, Chat, ChatForbidden, MessageActionChannelMigrateFrom, PeerUser, Channel, ChannelForbidden, Chat, ChatForbidden, MessageActionChannelMigrateFrom, PeerUser,
TypeUpdate, UpdateChannelPinnedMessage, UpdateChatPinnedMessage, UpdateChatParticipantAdmin, TypeUpdate, UpdateChannelPinnedMessage, UpdateChatPinnedMessage, UpdateChatParticipantAdmin,
UpdateChatParticipants, UpdateChatUserTyping, UpdateDeleteChannelMessages, UpdateDeleteMessages, UpdateChatParticipants, UpdateChatUserTyping, UpdateDeleteChannelMessages, UpdateNewMessage,
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateNewMessage, UpdateDeleteMessages, UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage,
UpdateReadHistoryOutbox, UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateReadHistoryOutbox, UpdateShortChatMessage, UpdateShortMessage, UpdateUserName,
UpdateUserPhoto, UpdateUserStatus, UpdateUserTyping, User, UserStatusOffline, UserStatusOnline) UpdateUserPhoto, UpdateUserStatus, UpdateUserTyping, User, UserStatusOffline, UserStatusOnline)
from mautrix_appservice import MatrixRequestError, AppService from mautrix.types import UserID
from mautrix.errors import MatrixError
from mautrix.appservice import AppService
from alchemysession import AlchemySessionContainer from alchemysession import AlchemySessionContainer
from . import portal as po, puppet as pu, __version__ from . import portal as po, puppet as pu, __version__
from .db import Message as DBMessage from .db import Message as DBMessage
from .types import TelegramID, MatrixUserID from .types import TelegramID
from .tgclient import MautrixTelegramClient from .tgclient import MautrixTelegramClient
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -69,7 +71,7 @@ class AbstractUser(ABC):
ignore_incoming_bot_events: bool = True ignore_incoming_bot_events: bool = True
client: Optional[MautrixTelegramClient] client: Optional[MautrixTelegramClient]
mxid: Optional[MatrixUserID] mxid: Optional[UserID]
tgid: Optional[TelegramID] tgid: Optional[TelegramID]
username: Optional['str'] username: Optional['str']
@@ -140,8 +142,10 @@ class AbstractUser(ABC):
api_hash=config["telegram.api_hash"], api_hash=config["telegram.api_hash"],
app_version=__version__ if appversion == "auto" else appversion, app_version=__version__ if appversion == "auto" else appversion,
system_version=MautrixTelegramClient.__version__ if sysversion == "auto" else sysversion, system_version=(MautrixTelegramClient.__version__
device_model=f"{platform.system()} {platform.release()}" if device == "auto" else device, if sysversion == "auto" else sysversion),
device_model=(f"{platform.system()} {platform.release()}"
if device == "auto" else device),
timeout=config["telegram.connection.timeout"], timeout=config["telegram.connection.timeout"],
connection_retries=config["telegram.connection.retries"], connection_retries=config["telegram.connection.retries"],
@@ -197,7 +201,8 @@ class AbstractUser(ABC):
raise NotImplementedError() raise NotImplementedError()
async def is_logged_in(self) -> bool: async def is_logged_in(self) -> bool:
return self.client and self.client.is_connected() and await self.client.is_user_authorized() return (self.client and self.client.is_connected()
and await self.client.is_user_authorized())
async def has_full_access(self, allow_bot: bool = False) -> bool: async def has_full_access(self, allow_bot: bool = False) -> bool:
return (self.puppet_whitelisted return (self.puppet_whitelisted
@@ -368,7 +373,7 @@ class AbstractUser(ABC):
return return
try: try:
await portal.main_intent.redact(message.mx_room, message.mxid) await portal.main_intent.redact(message.mx_room, message.mxid)
except MatrixRequestError: except MatrixError:
pass pass
async def delete_message(self, update: UpdateDeleteMessages) -> None: async def delete_message(self, update: UpdateDeleteMessages) -> None:
+6 -5
View File
@@ -27,7 +27,8 @@ from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest
from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest
from telethon.errors import ChannelInvalidError, ChannelPrivateError from telethon.errors import ChannelInvalidError, ChannelPrivateError
from .types import MatrixUserID from mautrix.types import UserID
from .abstract_user import AbstractUser from .abstract_user import AbstractUser
from .db import BotChat from .db import BotChat
from .types import TelegramID from .types import TelegramID
@@ -50,7 +51,7 @@ class Bot(AbstractUser):
tg_whitelist: List[int] tg_whitelist: List[int]
whitelist_group_admins: bool whitelist_group_admins: bool
_me_info: Optional[User] _me_info: Optional[User]
_me_mxid: Optional[MatrixUserID] _me_mxid: Optional[UserID]
def __init__(self, token: str) -> None: def __init__(self, token: str) -> None:
super().__init__() super().__init__()
@@ -70,7 +71,7 @@ class Bot(AbstractUser):
self._me_info = None self._me_info = None
self._me_mxid = None self._me_mxid = None
async def get_me(self, use_cache: bool = True) -> Tuple[User, MatrixUserID]: async def get_me(self, use_cache: bool = True) -> Tuple[User, UserID]:
if not use_cache or not self._me_mxid: if not use_cache or not self._me_mxid:
self._me_info = await self.client.get_me() self._me_info = await self.client.get_me()
self._me_mxid = pu.Puppet.get_mxid_from_id(TelegramID(self._me_info.id)) self._me_mxid = pu.Puppet.get_mxid_from_id(TelegramID(self._me_info.id))
@@ -186,7 +187,7 @@ class Bot(AbstractUser):
"Portal is not public. Use `/invite <mxid>` to get an invite.") "Portal is not public. Use `/invite <mxid>` to get an invite.")
async def handle_command_invite(self, portal: po.Portal, reply: ReplyFunc, async def handle_command_invite(self, portal: po.Portal, reply: ReplyFunc,
mxid_input: MatrixUserID) -> Message: mxid_input: UserID) -> Message:
if len(mxid_input) == 0: if len(mxid_input) == 0:
return await reply("Usage: `/invite <mxid>`") return await reply("Usage: `/invite <mxid>`")
elif not portal.mxid: elif not portal.mxid:
@@ -194,7 +195,7 @@ class Bot(AbstractUser):
"Create one with /portal first.") "Create one with /portal first.")
if not self.mxid_regex.match(mxid_input): if not self.mxid_regex.match(mxid_input):
return await reply("That doesn't look like a Matrix ID.") return await reply("That doesn't look like a Matrix ID.")
user = await u.User.get_by_mxid(MatrixUserID(mxid_input)).ensure_started() user = await u.User.get_by_mxid(mxid_input).ensure_started()
if not user.relaybot_whitelisted: if not user.relaybot_whitelisted:
return await reply("That user is not whitelisted to use the bridge.") return await reply("That user is not whitelisted to use the bridge.")
elif await user.is_logged_in(): elif await user.is_logged_in():
-4
View File
@@ -34,10 +34,6 @@ class Config(BaseBridgeConfig):
except KeyError: except KeyError:
return super().__getitem__(key) return super().__getitem__(key)
@staticmethod
def _new_token() -> str:
return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(64))
def do_update(self, helper: ConfigUpdateHelper) -> None: def do_update(self, helper: ConfigUpdateHelper) -> None:
copy, copy_dict, base = helper copy, copy_dict, base = helper
+3 -1
View File
@@ -16,6 +16,8 @@
from sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean from sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean
from typing import Optional from typing import Optional
from mautrix.types import ContentURI
from .base import Base from .base import Base
@@ -23,7 +25,7 @@ class TelegramFile(Base):
__tablename__ = "telegram_file" __tablename__ = "telegram_file"
id = Column(String, primary_key=True) id = Column(String, primary_key=True)
mxc = Column(String) mxc: ContentURI = Column(String)
mime_type = Column(String) mime_type = Column(String)
was_converted = Column(Boolean) was_converted = Column(Boolean)
timestamp = Column(BigInteger) timestamp = Column(BigInteger)
+148 -118
View File
@@ -13,11 +13,11 @@
# #
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Awaitable, Dict, List, Optional, Pattern, Tuple, Union, cast, TYPE_CHECKING, Any from typing import (Awaitable, Dict, List, Optional, Pattern, Tuple, Union, Any, Deque, cast,
from collections import deque TYPE_CHECKING)
from datetime import datetime
from string import Template
from html import escape as escape_html from html import escape as escape_html
from collections import deque
from string import Template
import asyncio import asyncio
import random import random
import mimetypes import mimetypes
@@ -37,7 +37,7 @@ 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, EditTitleRequest, CreateChannelRequest, EditAdminRequest, EditPhotoRequest, EditTitleRequest,
GetParticipantsRequest, InviteToChannelRequest, JoinChannelRequest, LeaveChannelRequest, GetParticipantsRequest, InviteToChannelRequest, JoinChannelRequest, LeaveChannelRequest,
UpdateUsernameRequest) UpdateUsernameRequest)
from telethon.tl.functions.messages import ReadHistoryRequest as ReadMessageHistoryRequest from telethon.tl.functions.messages import ReadHistoryRequest as ReadMessageHistoryRequest
@@ -63,9 +63,12 @@ from telethon.tl.types import (
TypePhotoSize, TypeUpdates, TypeUser, PhotoSize, TypeUserFull, UpdateChatUserTyping, TypePhotoSize, TypeUpdates, TypeUser, PhotoSize, TypeUserFull, UpdateChatUserTyping,
UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping, User, UserFull, MessageEntityPre, UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping, User, UserFull, MessageEntityPre,
InputMediaUploadedDocument, InputPeerPhotoFileLocation) InputMediaUploadedDocument, InputPeerPhotoFileLocation)
from mautrix_appservice import MatrixRequestError, IntentError, AppService, IntentAPI
from .types import MatrixEventID, MatrixRoomID, MatrixUserID, TelegramID from mautrix.errors import MatrixRequestError, IntentError
from mautrix.appservice import AppService, IntentAPI
from mautrix.types import EventID, RoomID, UserID, RoomCreatePreset, ContentURI, MessageType
from .types import TelegramID
from .context import Context from .context import Context
from .db import Portal as DBPortal, Message as DBMessage, TelegramFile as DBTelegramFile from .db import Portal as DBPortal, Message as DBMessage, TelegramFile as DBTelegramFile
from .util import ignore_coro, sane_mimetypes from .util import ignore_coro, sane_mimetypes
@@ -77,70 +80,96 @@ if TYPE_CHECKING:
from .config import Config from .config import Config
from .tgclient import MautrixTelegramClient from .tgclient import MautrixTelegramClient
config = None # type: Config config: Optional[Config] = None
TypeMessage = Union[Message, MessageService] TypeMessage = Union[Message, MessageService]
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant] TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
DedupMXID = Tuple[MatrixEventID, TelegramID] DedupMXID = Tuple[EventID, TelegramID]
InviteList = Union[MatrixUserID, List[MatrixUserID]] InviteList = Union[UserID, List[UserID]]
class Portal: class Portal:
base_log = logging.getLogger("mau.portal") # type: logging.Logger base_log: logging.Logger = logging.getLogger("mau.portal")
az = None # type: AppService az: AppService = None
bot = None # type: Bot bot: Bot = None
loop = None # type: asyncio.AbstractEventLoop loop: asyncio.AbstractEventLoop = None
# Config cache # Config cache
filter_mode = None # type: str filter_mode: str = None
filter_list = None # type: List[str] filter_list: List[str] = None
public_portals = False # type: bool public_portals: bool = False
max_initial_member_sync = -1 # type: int max_initial_member_sync: int = -1
sync_channel_members = True # type: bool sync_channel_members: bool = True
sync_matrix_state = True # type: bool sync_matrix_state: bool = True
dedup_pre_db_check = False # type: bool dedup_pre_db_check: bool = False
dedup_cache_queue_length = 20 # type: int dedup_cache_queue_length: int = 20
alias_template = None # type: str alias_template: str = None
mx_alias_regex = None # type: Pattern mx_alias_regex: Pattern = None
hs_domain = None # type: str hs_domain: str = None
# Instance cache # Instance cache
by_mxid = {} # type: Dict[MatrixRoomID, Portal] by_mxid: Dict[RoomID, 'Portal'] = {}
by_tgid = {} # type: Dict[Tuple[TelegramID, TelegramID], Portal] by_tgid: Dict[Tuple[TelegramID, TelegramID], 'Portal'] = {}
mxid: Optional[RoomID]
tgid: TelegramID
tg_receiver: TelegramID
peer_type: str
username: str
megagroup: bool
title: Optional[str]
about: Optional[str]
photo_id: Optional[str]
local_config: Dict[str, Any]
deleted: bool
log: logging.Logger
_db_instance: DBPortal
_main_intent: Optional[IntentAPI]
_room_create_lock: asyncio.Lock
_temp_pinned_message_id: Optional[TelegramID]
_temp_pinned_message_id_space: Optional[TelegramID]
_temp_pinned_message_sender: Optional['p.Puppet']
_dedup: Deque[str]
_dedup_mxid: Dict[str, DedupMXID]
_dedup_action: Deque[str]
_send_locks: Dict[int, asyncio.Lock]
def __init__(self, tgid: TelegramID, peer_type: str, tg_receiver: Optional[TelegramID] = None, def __init__(self, tgid: TelegramID, peer_type: str, tg_receiver: Optional[TelegramID] = None,
mxid: Optional[MatrixRoomID] = None, username: Optional[str] = None, mxid: Optional[RoomID] = None, username: Optional[str] = None,
megagroup: Optional[bool] = False, title: Optional[str] = None, megagroup: Optional[bool] = False, title: Optional[str] = None,
about: Optional[str] = None, photo_id: Optional[str] = None, about: Optional[str] = None, photo_id: Optional[str] = None,
local_config: Optional[str] = None, db_instance: DBPortal = None) -> None: local_config: Optional[str] = None, db_instance: DBPortal = None) -> None:
self.mxid = mxid # type: Optional[MatrixRoomID] self.mxid = mxid
self.tgid = tgid # type: TelegramID self.tgid = tgid
self.tg_receiver = tg_receiver or tgid # type: TelegramID self.tg_receiver = tg_receiver or tgid
self.peer_type = peer_type # type: str self.peer_type = peer_type
self.username = username # type: str self.username = username
self.megagroup = megagroup # type: bool self.megagroup = megagroup
self.title = title # type: Optional[str] self.title = title
self.about = about # type: str self.about = about
self.photo_id = photo_id # type: str self.photo_id = photo_id
self.local_config = json.loads(local_config or "{}") # type: Dict[str, Any] self.local_config = json.loads(local_config or "{}")
self._db_instance = db_instance # type: DBPortal self._db_instance = db_instance
self.deleted = False # type: bool self.deleted = False
self.log = self.base_log.getChild(self.tgid_log) if self.tgid else self.base_log self.log = self.base_log.getChild(self.tgid_log) if self.tgid else self.base_log
self._main_intent = None # type: IntentAPI self._main_intent = None
self._room_create_lock = asyncio.Lock() # type: asyncio.Lock self._room_create_lock = asyncio.Lock()
self._temp_pinned_message_id = None # type: Optional[int] self._temp_pinned_message_id = None
self._temp_pinned_message_id_space = None # type: Optional[TelegramID] self._temp_pinned_message_id_space = None
self._temp_pinned_message_sender = None # type: Optional[p.Puppet] self._temp_pinned_message_sender = None
self._dedup = deque() # type: deque self._dedup = deque()
self._dedup_mxid = {} # type: Dict[str, DedupMXID] self._dedup_mxid = {}
self._dedup_action = deque() # type: deque self._dedup_action = deque()
self._send_locks = {} # type: Dict[int, asyncio.Lock] self._send_locks = {}
if tgid: if tgid:
self.by_tgid[self.tgid_full] = self self.by_tgid[self.tgid_full] = self
@@ -260,7 +289,7 @@ class Portal:
try: try:
found_mxid = self._dedup_mxid[evt_hash] found_mxid = self._dedup_mxid[evt_hash]
except KeyError: except KeyError:
return MatrixEventID("None"), TelegramID(0) return EventID("None"), TelegramID(0)
if found_mxid != expected_mxid: if found_mxid != expected_mxid:
return found_mxid return found_mxid
@@ -304,13 +333,11 @@ class Portal:
# region Matrix room info updating # region Matrix room info updating
async def invite_to_matrix(self, users: InviteList) -> None: async def invite_to_matrix(self, users: InviteList) -> None:
if isinstance(users, str): if isinstance(users, list):
await self.main_intent.invite(self.mxid, users, check_cache=True)
elif isinstance(users, list):
for user in users: for user in users:
await self.main_intent.invite(self.mxid, user, check_cache=True) await self.main_intent.invite_user(self.mxid, user, check_cache=True)
else: else:
raise ValueError("Invalid invite identifier given to invite_matrix()") await self.main_intent.invite_user(self.mxid, users, check_cache=True)
async def update_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User], async def update_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User],
direct: bool, puppet: p.Puppet = None, levels: Dict = None, direct: bool, puppet: p.Puppet = None, levels: Dict = None,
@@ -348,7 +375,7 @@ class Portal:
return await self._create_matrix_room(user, entity, invites) return await self._create_matrix_room(user, entity, invites)
async def _create_matrix_room(self, user: 'AbstractUser', entity: TypeChat, invites: InviteList async def _create_matrix_room(self, user: 'AbstractUser', entity: TypeChat, invites: InviteList
) -> Optional[MatrixRoomID]: ) -> Optional[RoomID]:
direct = self.peer_type == "user" direct = self.peer_type == "user"
if self.mxid: if self.mxid:
@@ -375,11 +402,11 @@ class Portal:
self.megagroup = entity.megagroup self.megagroup = entity.megagroup
if self.peer_type == "channel" and entity.username: if self.peer_type == "channel" and entity.username:
public = Portal.public_portals preset = RoomCreatePreset.PUBLIC
alias = self._get_alias_localpart(entity.username) alias = self._get_alias_localpart(entity.username)
self.username = entity.username self.username = entity.username
else: else:
public = False preset = RoomCreatePreset.PRIVATE
# TODO invite link alias? # TODO invite link alias?
alias = None alias = None
@@ -402,13 +429,13 @@ class Portal:
"content": {"groups": [config["appservice.community_id"]]}, "content": {"groups": [config["appservice.community_id"]]},
}) })
room_id = await self.main_intent.create_room(alias=alias, is_public=public, room_id = await self.main_intent.create_room(alias_localpart=alias, preset=preset,
is_direct=direct, invitees=invites or [], is_direct=direct, invitees=invites or [],
name=self.title, initial_state=initial_state) name=self.title, initial_state=initial_state)
if not room_id: if not room_id:
raise Exception(f"Failed to create room") raise Exception(f"Failed to create room")
self.mxid = MatrixRoomID(room_id) self.mxid = RoomID(room_id)
self.by_mxid[self.mxid] = self self.by_mxid[self.mxid] = self
self.save() self.save()
self.az.state_store.set_power_levels(self.mxid, power_levels) self.az.state_store.set_power_levels(self.mxid, power_levels)
@@ -510,7 +537,7 @@ class Portal:
and Portal.max_initial_member_sync == -1 and Portal.max_initial_member_sync == -1
and (self.megagroup or self.peer_type != "channel")) and (self.megagroup or self.peer_type != "channel"))
if trust_member_list: if trust_member_list:
joined_mxids = cast(List[MatrixUserID], joined_mxids = cast(List[UserID],
await self.main_intent.get_room_members(self.mxid)) await self.main_intent.get_room_members(self.mxid))
for user_mxid in joined_mxids: for user_mxid in joined_mxids:
if user_mxid == self.az.bot_mxid: if user_mxid == self.az.bot_mxid:
@@ -519,7 +546,7 @@ class Portal:
if puppet_id and puppet_id not in allowed_tgids: if puppet_id and puppet_id not in allowed_tgids:
if self.bot and puppet_id == self.bot.tgid: if self.bot and puppet_id == self.bot.tgid:
self.bot.remove_chat(self.tgid) self.bot.remove_chat(self.tgid)
await self.main_intent.kick(self.mxid, user_mxid, await self.main_intent.kick_user(self.mxid, user_mxid,
"User had left this Telegram chat.") "User had left this Telegram chat.")
continue continue
mx_user = u.User.get_by_mxid(user_mxid, create=False) mx_user = u.User.get_by_mxid(user_mxid, create=False)
@@ -527,7 +554,7 @@ class Portal:
mx_user.unregister_portal(self) mx_user.unregister_portal(self)
if mx_user and not self.has_bot and mx_user.tgid not in allowed_tgids: if mx_user and not self.has_bot and mx_user.tgid not in allowed_tgids:
await self.main_intent.kick(self.mxid, mx_user.mxid, await self.main_intent.kick_user(self.mxid, mx_user.mxid,
"You had left this Telegram chat.") "You had left this Telegram chat.")
continue continue
@@ -551,12 +578,12 @@ class Portal:
if sender and sender.tgid != puppet.tgid if sender and sender.tgid != puppet.tgid
else "Left Telegram chat") else "Left Telegram chat")
if sender and sender.tgid != puppet.tgid: if sender and sender.tgid != puppet.tgid:
await self.main_intent.kick(self.mxid, puppet.mxid, kick_message) await self.main_intent.kick_user(self.mxid, puppet.mxid, kick_message)
else: else:
await puppet.intent.leave_room(self.mxid) await puppet.intent.leave_room(self.mxid)
if user: if user:
user.unregister_portal(self) user.unregister_portal(self)
await self.main_intent.kick(self.mxid, user.mxid, kick_message) await self.main_intent.kick_user(self.mxid, user.mxid, kick_message)
async def update_info(self, user: 'AbstractUser', entity: TypeChat = None) -> None: async def update_info(self, user: 'AbstractUser', entity: TypeChat = None) -> None:
if self.peer_type == "user": if self.peer_type == "user":
@@ -625,7 +652,8 @@ class Portal:
return None, None return None, None
if isinstance(photo, Document) and not photo.thumbs: if isinstance(photo, Document) and not photo.thumbs:
return None, None return None, None
largest = max(photo.sizes if isinstance(photo, Photo) else photo.thumbs,
largest = max(photo.thumbs if isinstance(photo, Document) else photo.sizes,
key=(lambda photo2: (len(photo2.bytes) key=(lambda photo2: (len(photo2.bytes)
if not isinstance(photo2, PhotoSize) if not isinstance(photo2, PhotoSize)
else photo2.size))) else photo2.size)))
@@ -663,7 +691,7 @@ class Portal:
raise ValueError(f"Unknown photo type {type(photo)}") raise ValueError(f"Unknown photo type {type(photo)}")
if self.photo_id != photo_id: if self.photo_id != photo_id:
if not photo_id: if not photo_id:
await self.main_intent.set_room_avatar(self.mxid, "") await self.main_intent.set_room_avatar(self.mxid, ContentURI(""))
self.photo_id = "" self.photo_id = ""
if save: if save:
self.save() self.save()
@@ -739,7 +767,7 @@ class Portal:
authenticated = [] # type: List[u.User] authenticated = [] # type: List[u.User]
has_bot = self.has_bot has_bot = self.has_bot
for member_str in members: for member_str in members:
member = MatrixUserID(member_str) member = UserID(member_str)
if p.Puppet.get_id_from_mxid(member) or member == self.main_intent.mxid: if p.Puppet.get_id_from_mxid(member) or member == self.main_intent.mxid:
continue continue
user = await u.User.get_by_mxid(member).ensure_started() # type: u.User user = await u.User.get_by_mxid(member).ensure_started() # type: u.User
@@ -749,20 +777,20 @@ class Portal:
return authenticated return authenticated
@staticmethod @staticmethod
async def cleanup_room(intent: IntentAPI, room_id: str, message: str = "Portal deleted", async def cleanup_room(intent: IntentAPI, room_id: RoomID, message: str = "Portal deleted",
puppets_only: bool = False) -> None: puppets_only: bool = False) -> None:
try: try:
members = await intent.get_room_members(room_id) members = await intent.get_room_members(room_id)
except MatrixRequestError: except MatrixRequestError:
members = [] members = []
for user in members: for user in members:
puppet = p.Puppet.get_by_mxid(MatrixUserID(user), create=False) puppet = p.Puppet.get_by_mxid(UserID(user), create=False)
if user != intent.mxid and (not puppets_only or puppet): if user != intent.mxid and (not puppets_only or puppet):
try: try:
if puppet: if puppet:
await puppet.intent.leave_room(room_id) await puppet.intent.leave_room(room_id)
else: else:
await intent.kick(room_id, user, message) await intent.kick_user(room_id, user, message)
except (MatrixRequestError, IntentError): except (MatrixRequestError, IntentError):
pass pass
await intent.leave_room(room_id) await intent.leave_room(room_id)
@@ -816,7 +844,7 @@ class Portal:
} }
async def name_change_matrix(self, user: 'u.User', displayname: str, prev_displayname: str, async def name_change_matrix(self, user: 'u.User', displayname: str, prev_displayname: str,
event_id: MatrixEventID) -> None: event_id: EventID) -> None:
async with self.require_send_lock(self.bot.tgid): async with self.require_send_lock(self.bot.tgid):
message = await self._get_state_change_message( message = await self._get_state_change_message(
"name_change", user, "name_change", user,
@@ -830,6 +858,7 @@ class Portal:
self.is_duplicate(response, (event_id, space)) self.is_duplicate(response, (event_id, space))
async def get_displayname(self, user: 'u.User') -> str: async def get_displayname(self, user: 'u.User') -> str:
# FIXME mautrix4
return (await self.main_intent.get_displayname(self.mxid, user.mxid) return (await self.main_intent.get_displayname(self.mxid, user.mxid)
or user.mxid) or user.mxid)
@@ -851,7 +880,7 @@ class Portal:
return user.client(SetTypingRequest( return user.client(SetTypingRequest(
self.peer, action() if typing else SendMessageCancelAction())) self.peer, action() if typing else SendMessageCancelAction()))
async def mark_read(self, user: 'u.User', event_id: MatrixEventID) -> None: async def mark_read(self, user: 'u.User', event_id: EventID) -> None:
if user.is_bot: if user.is_bot:
return return
space = self.tgid if self.peer_type == "channel" else user.tgid space = self.tgid if self.peer_type == "channel" else user.tgid
@@ -886,7 +915,7 @@ class Portal:
if not ban: if not ban:
await source.client.edit_permissions(channel, target, view_messages=True) await source.client.edit_permissions(channel, target, view_messages=True)
async def leave_matrix(self, user: 'u.User', event_id: MatrixEventID) -> None: async def leave_matrix(self, user: 'u.User', event_id: EventID) -> None:
if await user.needs_relaybot(self): if await user.needs_relaybot(self):
if not self.has_bot: if not self.has_bot:
return return
@@ -915,7 +944,7 @@ class Portal:
channel = await self.get_input_entity(user) channel = await self.get_input_entity(user)
await user.client(LeaveChannelRequest(channel=channel)) await user.client(LeaveChannelRequest(channel=channel))
async def join_matrix(self, user: 'u.User', event_id: MatrixEventID) -> None: async def join_matrix(self, user: 'u.User', event_id: EventID) -> None:
if await user.needs_relaybot(self): if await user.needs_relaybot(self):
async with self.require_send_lock(self.bot.tgid): async with self.require_send_lock(self.bot.tgid):
message = await self._get_state_change_message("join", user) message = await self._get_state_change_message("join", user)
@@ -993,7 +1022,7 @@ class Portal:
except KeyError: except KeyError:
return None return None
async def _handle_matrix_text(self, sender_id: TelegramID, event_id: MatrixEventID, async def _handle_matrix_text(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient', space: TelegramID, client: 'MautrixTelegramClient',
message: Dict, reply_to: TelegramID) -> None: message: Dict, reply_to: TelegramID) -> None:
lock = self.require_send_lock(sender_id) lock = self.require_send_lock(sender_id)
@@ -1015,18 +1044,18 @@ class Portal:
link_preview=lp) link_preview=lp)
self._add_telegram_message_to_db(event_id, space, 0, response) self._add_telegram_message_to_db(event_id, space, 0, response)
async def _handle_matrix_file(self, msgtype: str, sender_id: TelegramID, async def _handle_matrix_file(self, msgtype: MessageType, sender_id: TelegramID,
event_id: MatrixEventID, space: TelegramID, event_id: EventID, space: TelegramID,
client: 'MautrixTelegramClient', message: dict, client: 'MautrixTelegramClient', message: dict,
reply_to: TelegramID) -> None: reply_to: TelegramID) -> None:
file = await self.main_intent.download_file(message["url"]) file = await self.main_intent.download_media(message["url"])
info = message.get("info", {}) info = message.get("info", {})
mime = info.get("mimetype", None) mime = info.get("mimetype", None)
w, h = None, None w, h = None, None
if msgtype == "m.sticker": if msgtype == MessageType.STICKER:
if mime != "image/gif": if mime != "image/gif":
mime, file, w, h = util.convert_image(file, source_mime=mime, target_type="webp") mime, file, w, h = util.convert_image(file, source_mime=mime, target_type="webp")
else: else:
@@ -1048,14 +1077,8 @@ class Portal:
max_image_size=config["bridge.image_as_file_size"] * 1000 ** 2) 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:
relates_to = message.get("m.relates_to", None) or {} if await self._matrix_document_edit(client, message, space, caption, media, event_id):
if relates_to.get("rel_type", None) == "m.replace": return
orig_msg = DBMessage.get_by_mxid(relates_to.get("event_id", ""), self.mxid, space)
if orig_msg:
response = await client.edit_message(self.peer, orig_msg.tgid,
caption, file=media)
self._add_telegram_message_to_db(event_id, space, -1, response)
return
try: try:
response = await client.send_media(self.peer, media, reply_to=reply_to, response = await client.send_media(self.peer, media, reply_to=reply_to,
caption=caption) caption=caption)
@@ -1066,7 +1089,20 @@ class Portal:
caption=caption) caption=caption)
self._add_telegram_message_to_db(event_id, space, 0, response) self._add_telegram_message_to_db(event_id, space, 0, response)
async def _handle_matrix_location(self, sender_id: TelegramID, event_id: MatrixEventID, 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)
if orig_msg:
response = await client.edit_message(self.peer, orig_msg.tgid,
caption, file=media)
self._add_telegram_message_to_db(event_id, space, -1, response)
return True
return False
async def _handle_matrix_location(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient', space: TelegramID, client: 'MautrixTelegramClient',
message: Dict[str, Any], reply_to: TelegramID) -> None: message: Dict[str, Any], reply_to: TelegramID) -> None:
try: try:
@@ -1080,19 +1116,13 @@ class Portal:
lock = self.require_send_lock(sender_id) lock = self.require_send_lock(sender_id)
async with lock: async with lock:
relates_to = message.get("m.relates_to", None) or {} if await self._matrix_document_edit(client, message, space, caption, media, event_id):
if relates_to.get("rel_type", None) == "m.replace": return
orig_msg = DBMessage.get_by_mxid(relates_to.get("event_id", ""), self.mxid, space)
if orig_msg:
response = await client.edit_message(self.peer, orig_msg.tgid,
caption, file=media)
self._add_telegram_message_to_db(event_id, space, -1, response)
return
response = await client.send_media(self.peer, media, reply_to=reply_to, response = await client.send_media(self.peer, media, reply_to=reply_to,
caption=caption, entities=entities) caption=caption, entities=entities)
self._add_telegram_message_to_db(event_id, space, 0, response) self._add_telegram_message_to_db(event_id, space, 0, response)
def _add_telegram_message_to_db(self, event_id: MatrixEventID, space: TelegramID, def _add_telegram_message_to_db(self, event_id: EventID, space: TelegramID,
edit_index: int, response: TypeMessage) -> None: edit_index: int, response: TypeMessage) -> None:
self.log.debug("Handled Matrix message: %s", response) self.log.debug("Handled Matrix message: %s", response)
self.is_duplicate(response, (event_id, space), force_hash=edit_index != 0) self.is_duplicate(response, (event_id, space), force_hash=edit_index != 0)
@@ -1107,7 +1137,7 @@ class Portal:
edit_index=edit_index).insert() 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', message: Dict[str, Any],
event_id: MatrixEventID) -> None: event_id: EventID) -> None:
if "body" not in message or "msgtype" not in message: if "body" not in message or "msgtype" not in message:
self.log.debug(f"Ignoring message {event_id} in {self.mxid} without body or msgtype") self.log.debug(f"Ignoring message {event_id} in {self.mxid} without body or msgtype")
return return
@@ -1146,7 +1176,7 @@ class Portal:
self.log.debug(f"Unhandled Matrix event: {message}") self.log.debug(f"Unhandled Matrix event: {message}")
async def handle_matrix_pin(self, sender: 'u.User', async def handle_matrix_pin(self, sender: 'u.User',
pinned_message: Optional[MatrixEventID]) -> None: pinned_message: Optional[EventID]) -> None:
if self.peer_type != "chat" and self.peer_type != "channel": if self.peer_type != "chat" and self.peer_type != "channel":
return return
try: try:
@@ -1162,7 +1192,7 @@ class Portal:
except ChatNotModifiedError: except ChatNotModifiedError:
pass pass
async def handle_matrix_deletion(self, deleter: 'u.User', event_id: MatrixEventID) -> None: async def handle_matrix_deletion(self, deleter: 'u.User', event_id: EventID) -> None:
real_deleter = deleter if not await deleter.needs_relaybot(self) else self.bot real_deleter = deleter if not await deleter.needs_relaybot(self) else self.bot
space = self.tgid if self.peer_type == "channel" else real_deleter.tgid space = self.tgid if self.peer_type == "channel" else real_deleter.tgid
message = DBMessage.get_by_mxid(event_id, self.mxid, space) message = DBMessage.get_by_mxid(event_id, self.mxid, space)
@@ -1190,7 +1220,7 @@ class Portal:
user_id=user_id, admin_rights=rights)) user_id=user_id, admin_rights=rights))
async def handle_matrix_power_levels(self, sender: 'u.User', async def handle_matrix_power_levels(self, sender: 'u.User',
new_users: Dict[MatrixUserID, int], new_users: Dict[UserID, int],
old_users: Dict[str, int]) -> None: old_users: Dict[str, int]) -> None:
# TODO handle all power level changes and bridge exact admin rights to supergroups/channels # TODO handle all power level changes and bridge exact admin rights to supergroups/channels
for user, level in new_users.items(): for user, level in new_users.items():
@@ -1228,12 +1258,12 @@ class Portal:
self.title = title self.title = title
self.save() self.save()
async def handle_matrix_avatar(self, sender: 'u.User', url: str) -> None: async def handle_matrix_avatar(self, sender: 'u.User', url: ContentURI) -> None:
if self.peer_type not in ("chat", "channel"): if self.peer_type not in ("chat", "channel"):
# Invalid peer type # Invalid peer type
return return
file = await self.main_intent.download_file(url) file = await self.main_intent.download_media(url)
mime = magic.from_buffer(file, mime=True) mime = magic.from_buffer(file, mime=True)
ext = sane_mimetypes.guess_extension(mime) ext = sane_mimetypes.guess_extension(mime)
uploaded = await sender.client.upload_file(file, file_name=f"avatar{ext}", use_cache=False) uploaded = await sender.client.upload_file(file, file_name=f"avatar{ext}", use_cache=False)
@@ -1255,7 +1285,7 @@ class Portal:
self.save() self.save()
break break
async def handle_matrix_upgrade(self, new_room: MatrixRoomID) -> None: async def handle_matrix_upgrade(self, new_room: RoomID) -> None:
old_room = self.mxid old_room = self.mxid
self.migrate_and_save_matrix(new_room) self.migrate_and_save_matrix(new_room)
await self.main_intent.join_room(new_room) await self.main_intent.join_room(new_room)
@@ -1267,7 +1297,7 @@ class Portal:
if not entity: if not entity:
user_mxids = await self.main_intent.get_room_members(self.mxid) user_mxids = await self.main_intent.get_room_members(self.mxid)
for user_str in user_mxids: for user_str in user_mxids:
user_id = MatrixUserID(user_str) user_id = UserID(user_str)
if user_id == self.az.bot_mxid: if user_id == self.az.bot_mxid:
continue continue
user = u.User.get_by_mxid(user_id, create=False) user = u.User.get_by_mxid(user_id, create=False)
@@ -1299,7 +1329,7 @@ class Portal:
user_tgids = set() user_tgids = set()
user_mxids = await self.main_intent.get_room_members(self.mxid, ("join", "invite")) user_mxids = await self.main_intent.get_room_members(self.mxid, ("join", "invite"))
for user_str in user_mxids: for user_str in user_mxids:
user = MatrixUserID(user_str) user = UserID(user_str)
if user == self.az.bot_mxid: if user == self.az.bot_mxid:
continue continue
mx_user = u.User.get_by_mxid(user, create=False) mx_user = u.User.get_by_mxid(user, create=False)
@@ -1405,7 +1435,7 @@ class Portal:
return None return None
async def handle_telegram_photo(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, async def handle_telegram_photo(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: Dict = None) -> Optional[Dict]: relates_to: Dict = None) -> Optional[EventID]:
loc, largest_size = self._get_largest_photo_size(evt.media.photo) loc, largest_size = self._get_largest_photo_size(evt.media.photo)
file = await util.transfer_file_to_matrix(source.client, intent, loc) file = await util.transfer_file_to_matrix(source.client, intent, loc)
if not file: if not file:
@@ -1503,7 +1533,7 @@ class Portal:
return info, name return info, name
async def handle_telegram_document(self, source: 'AbstractUser', intent: IntentAPI, async def handle_telegram_document(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None) -> Optional[Dict]: evt: Message, relates_to: dict = None) -> Optional[EventID]:
document = evt.media.document document = evt.media.document
attrs = self._parse_telegram_document_attributes(document.attributes) attrs = self._parse_telegram_document_attributes(document.attributes)
@@ -1552,7 +1582,7 @@ class Portal:
return await intent.send_file(**kwargs) return await intent.send_file(**kwargs)
def handle_telegram_location(self, _: 'AbstractUser', intent: IntentAPI, evt: Message, def handle_telegram_location(self, _: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: dict = None) -> Awaitable[dict]: relates_to: dict = None) -> Awaitable[EventID]:
location = evt.media.geo location = evt.media.geo
long = location.long long = location.long
lat = location.lat lat = location.lat
@@ -1580,7 +1610,7 @@ class Portal:
}, timestamp=evt.date, external_url=self.get_external_url(evt)) }, timestamp=evt.date, external_url=self.get_external_url(evt))
async def handle_telegram_text(self, source: 'AbstractUser', intent: IntentAPI, is_bot: bool, async def handle_telegram_text(self, source: 'AbstractUser', intent: IntentAPI, is_bot: bool,
evt: Message) -> dict: evt: Message) -> EventID:
self.log.debug(f"Sending {evt.message} to {self.mxid} by {intent.mxid}") 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) text, html, relates_to = await formatter.telegram_to_matrix(evt, source, self.main_intent)
await intent.set_typing(self.mxid, is_typing=False) await intent.set_typing(self.mxid, is_typing=False)
@@ -1590,7 +1620,7 @@ class Portal:
external_url=self.get_external_url(evt)) external_url=self.get_external_url(evt))
async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI, async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None) -> dict: evt: Message, relates_to: dict = None) -> EventID:
override_text = ("This message is not supported on your version of Mautrix-Telegram. " override_text = ("This message is not supported on your version of Mautrix-Telegram. "
"Please check https://github.com/tulir/mautrix-telegram or ask your " "Please check https://github.com/tulir/mautrix-telegram or ask your "
"bridge administrator about possible updates.") "bridge administrator about possible updates.")
@@ -1607,7 +1637,7 @@ class Portal:
}, 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, async def handle_telegram_poll(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: dict) -> dict: relates_to: dict) -> EventID:
poll = evt.media.poll # type: Poll poll = evt.media.poll # type: Poll
poll_id = self._encode_msgid(source, evt) poll_id = self._encode_msgid(source, evt)
@@ -1658,7 +1688,7 @@ class Portal:
return base64.b64encode(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, async def handle_telegram_game(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None): evt: Message, relates_to: dict = None) -> EventID:
game = evt.media.game game = evt.media.game
play_id = self._encode_msgid(source, evt) play_id = self._encode_msgid(source, evt)
command = f"!tg play {play_id}" command = f"!tg play {play_id}"
@@ -1693,7 +1723,7 @@ class Portal:
tg_space = self.tgid if self.peer_type == "channel" else source.tgid tg_space = self.tgid if self.peer_type == "channel" else source.tgid
temporary_identifier = MatrixEventID( temporary_identifier = EventID(
f"${random.randint(1000000000000, 9999999999999)}TGBRIDGEDITEMP") f"${random.randint(1000000000000, 9999999999999)}TGBRIDGEDITEMP")
duplicate_found = self.is_duplicate(evt, (temporary_identifier, tg_space), force_hash=True) duplicate_found = self.is_duplicate(evt, (temporary_identifier, tg_space), force_hash=True)
if duplicate_found: if duplicate_found:
@@ -1758,7 +1788,7 @@ class Portal:
tg_space = self.tgid if self.peer_type == "channel" else source.tgid tg_space = self.tgid if self.peer_type == "channel" else source.tgid
temporary_identifier = MatrixEventID( temporary_identifier = EventID(
f"${random.randint(1000000000000, 9999999999999)}TGBRIDGETEMP") f"${random.randint(1000000000000, 9999999999999)}TGBRIDGETEMP")
duplicate_found = self.is_duplicate(evt, (temporary_identifier, tg_space)) duplicate_found = self.is_duplicate(evt, (temporary_identifier, tg_space))
if duplicate_found: if duplicate_found:
@@ -2034,7 +2064,7 @@ class Portal:
self.log = self.base_log.getChild(str(self.tgid)) self.log = self.base_log.getChild(str(self.tgid))
self.log.info(f"Telegram chat upgraded from {old_id}") self.log.info(f"Telegram chat upgraded from {old_id}")
def migrate_and_save_matrix(self, new_id: MatrixRoomID) -> None: def migrate_and_save_matrix(self, new_id: RoomID) -> None:
try: try:
del self.by_mxid[self.mxid] del self.by_mxid[self.mxid]
except KeyError: except KeyError:
@@ -2073,7 +2103,7 @@ class Portal:
# region Class instance lookup # region Class instance lookup
@classmethod @classmethod
def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['Portal']: def get_by_mxid(cls, mxid: RoomID) -> Optional['Portal']:
try: try:
return cls.by_mxid[mxid] return cls.by_mxid[mxid]
except KeyError: except KeyError:
+1 -1
View File
@@ -1,4 +1,4 @@
cryptg cryptg
Pillow Pillow
moviepy moviepy
prometheus-client prometheus_client