diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index e7659b41..6d8a74cc 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -73,7 +73,7 @@ if args.generate_registration: sys.exit(0) logging.config.dictConfig(copy.deepcopy(config["logging"])) -log = logging.getLogger("mau.init") # type: logging.Logger +log: logging.Logger = logging.getLogger("mau.init") log.debug(f"Initializing mautrix-telegram {__version__}") db_engine = sql.create_engine(config["appservice.database"] or "sqlite:///mautrix-telegram.db") @@ -91,7 +91,7 @@ try: except ImportError: pass -loop = asyncio.get_event_loop() # type: asyncio.AbstractEventLoop +loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() state_store = SQLStateStore() mebibyte = 1024 ** 2 @@ -123,7 +123,7 @@ if config["metrics.enabled"]: if prometheus: prometheus.start_http_server(config["metrics.listen_port"]) else: - log.warn("Metrics are enabled in the config, but prometheus-async is not installed.") + log.warn("Metrics are enabled in the config, but prometheus_client is not installed.") with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start: start_ts = time() @@ -131,9 +131,9 @@ with appserv.run(config["appservice.hostname"], config["appservice.port"]) as st init_abstract_user(context) init_formatter(context) init_portal(context) - startup_actions = (init_puppet(context) + - init_user(context) + - [start, context.mx.init_as_bot()]) # type: List[Awaitable[Any]] + startup_actions: List[Awaitable[Any]] = (init_puppet(context) + + init_user(context) + + [start, context.mx.init_as_bot()]) if context.bot: startup_actions.append(context.bot.start()) diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index 961dd657..cb60c07a 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -42,9 +42,9 @@ if TYPE_CHECKING: from .config import Config from .bot import Bot -config = None # type: Config +config: Optional['Config'] = None # Value updated from config in init() -MAX_DELETIONS = 10 # type: int +MAX_DELETIONS: int = 10 UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage, UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage] @@ -59,26 +59,41 @@ except ImportError: Histogram = None UPDATE_TIME = None + class AbstractUser(ABC): - session_container = None # type: AlchemySessionContainer - loop = None # type: asyncio.AbstractEventLoop - log = None # type: logging.Logger - az = None # type: AppService - bot = None # type: Bot - ignore_incoming_bot_events = True # type: bool + session_container: AlchemySessionContainer = None + loop: asyncio.AbstractEventLoop = None + log: logging.Logger + az: AppService + bot: 'Bot' + ignore_incoming_bot_events: bool = True + + client: Optional[MautrixTelegramClient] + mxid: Optional[MatrixUserID] + + tgid: Optional[TelegramID] + username: Optional['str'] + is_bot: bool + + is_relaybot: bool + relaybot: Optional['Bot'] + + puppet_whitelisted: bool + whitelisted: bool + relaybot_whitelisted: bool + matrix_puppet_whitelisted: bool + is_admin: bool def __init__(self) -> None: - self.is_admin = False # type: bool - self.matrix_puppet_whitelisted = False # type: bool - self.puppet_whitelisted = False # type: bool - self.whitelisted = False # type: bool - self.relaybot_whitelisted = False # type: bool - self.client = None # type: MautrixTelegramClient - self.tgid = None # type: TelegramID - self.mxid = None # type: MatrixUserID - self.is_relaybot = False # type: bool - self.is_bot = False # type: bool - self.relaybot = None # type: Optional[Bot] + self.is_admin = False + self.matrix_puppet_whitelisted = False + self.puppet_whitelisted = False + self.whitelisted = False + self.relaybot_whitelisted = False + self.client = None + self.is_relaybot = False + self.is_bot = False + self.relaybot = None @property def connected(self) -> bool: @@ -367,7 +382,6 @@ class AbstractUser(ABC): message.delete() number_left = DBMessage.count_spaces_by_mxid(message.mxid, message.mx_room) if number_left == 0: - portal = po.Portal.get_by_mxid(message.mx_room) await self._try_redact(message) async def delete_channel_message(self, update: UpdateDeleteChannelMessages) -> None: @@ -414,7 +428,7 @@ class AbstractUser(ABC): # endregion -def init(context: "Context") -> None: +def init(context: 'Context') -> None: global config, MAX_DELETIONS AbstractUser.az, config, AbstractUser.loop, AbstractUser.relaybot = context.core AbstractUser.ignore_incoming_bot_events = config["bridge.relaybot.ignore_own_incoming_events"] diff --git a/mautrix_telegram/bot.py b/mautrix_telegram/bot.py index ecb54e95..51078388 100644 --- a/mautrix_telegram/bot.py +++ b/mautrix_telegram/bot.py @@ -35,32 +35,40 @@ from . import puppet as pu, portal as po, user as u if TYPE_CHECKING: from .config import Config - from .context import Context -config = None # type: Config +config: Optional['Config'] = None ReplyFunc = Callable[[str], Awaitable[Message]] class Bot(AbstractUser): - log = logging.getLogger("mau.bot") # type: logging.Logger - mxid_regex = re.compile("@.+:.+") # type: Pattern + log: logging.Logger = logging.getLogger("mau.bot") + mxid_regex: Pattern = re.compile("@.+:.+") + + token: str + chats: Dict[int, str] + tg_whitelist: List[int] + whitelist_group_admins: bool + _me_info: Optional[User] + _me_mxid: Optional[MatrixUserID] def __init__(self, token: str) -> None: super().__init__() - self.token = token # type: str - self.puppet_whitelisted = True # type: bool - self.whitelisted = True # type: bool - self.relaybot_whitelisted = True # type: bool - self.username = None # type: str - self.is_relaybot = True # type: bool - self.is_bot = True # type: bool - self.chats = {} # type: Dict[int, str] - self.tg_whitelist = [] # type: List[int] + self.token = token + self.tgid = None + self.mxid = None + self.puppet_whitelisted = True + self.whitelisted = True + self.relaybot_whitelisted = True + self.username = None + self.is_relaybot = True + self.is_bot = True + self.chats = {} + self.tg_whitelist = [] self.whitelist_group_admins = (config["bridge.relaybot.whitelist_group_admins"] - or False) # type: bool - self._me_info = None # type: Optional[User] - self._me_mxid = None # type: Optional[MatrixUserID] + or False) + self._me_info = None + self._me_mxid = None async def get_me(self, use_cache: bool = True) -> Tuple[User, MatrixUserID]: if not use_cache or not self._me_mxid: @@ -91,7 +99,7 @@ class Bot(AbstractUser): async def post_login(self) -> None: await self.init_permissions() info = await self.client.get_me() - self.tgid = info.id + self.tgid = TelegramID(info.id) self.username = info.username self.mxid = pu.Puppet.get_mxid_from_id(self.tgid) diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index eba0d443..f712fffa 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -19,13 +19,15 @@ from ruamel.yaml.comments import CommentedMap import random import string -yaml = YAML() # type: YAML +yaml: YAML = YAML() yaml.indent(4) class DictWithRecursion: + _data: CommentedMap + def __init__(self, data: Optional[CommentedMap] = None) -> None: - self._data = data or CommentedMap() # type: CommentedMap + self._data = data or CommentedMap() @staticmethod def _parse_key(key: str) -> Tuple[str, Optional[str]]: @@ -102,14 +104,20 @@ class DictWithRecursion: class Config(DictWithRecursion): + path: str + registration_path: str + base_path: str + _registration: Optional[Dict[str, Any]] + _overrides: Dict[str, Any] + def __init__(self, path: str, registration_path: str, base_path: str, overrides: Dict[str, Any] = None) -> None: super().__init__() - self.path = path # type: str - self.registration_path = registration_path # type: str - self.base_path = base_path # type: str - self._registration = None # type: Optional[Dict] - self._overrides = overrides or {} # type: Dict[str, Any] + self.path = path + self.registration_path = registration_path + self.base_path = base_path + self._registration = None + self._overrides = overrides or {} def __getitem__(self, key: str) -> Any: try: @@ -327,10 +335,10 @@ class Config(DictWithRecursion): def generate_registration(self) -> None: homeserver = self["homeserver.domain"] - username_format = self.get("bridge.username_template", "telegram_{userid}") \ - .format(userid=".+") - alias_format = self.get("bridge.alias_template", "telegram_{groupname}") \ - .format(groupname=".+") + username_format = self.get("bridge.username_template", + "telegram_{userid}").format(userid=".+") + alias_format = self.get("bridge.alias_template", + "telegram_{groupname}").format(groupname=".+") self.set("appservice.as_token", self._new_token()) self.set("appservice.hs_token", self._new_token()) @@ -354,5 +362,5 @@ class Config(DictWithRecursion): "rate_limited": False } if self["appservice.community_id"]: - self._registration["namespaces"]["users"][0]["group_id"] \ - = self["appservice.community_id"] + self._registration["namespaces"]["users"][0]["group_id"] = self[ + "appservice.community_id"] diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py index d2edcf27..1735d322 100644 --- a/mautrix_telegram/context.py +++ b/mautrix_telegram/context.py @@ -28,16 +28,25 @@ if TYPE_CHECKING: class Context: + az: 'AppService' + config: 'Config' + loop: 'asyncio.AbstractEventLoop' + bot: Optional['Bot'] + mx: Optional['MatrixHandler'] + session_container: 'AlchemySessionContainer' + public_website: Optional['PublicBridgeWebsite'] + provisioning_api: Optional['ProvisioningAPI'] + def __init__(self, az: 'AppService', config: 'Config', loop: 'asyncio.AbstractEventLoop', session_container: 'AlchemySessionContainer', bot: Optional['Bot']) -> None: - self.az = az # type: AppService - self.config = config # type: Config - self.loop = loop # type: asyncio.AbstractEventLoop - self.bot = bot # type: Optional[Bot] - self.mx = None # type: Optional[MatrixHandler] - self.session_container = session_container # type: AlchemySessionContainer - self.public_website = None # type: Optional[PublicBridgeWebsite] - self.provisioning_api = None # type: Optional[ProvisioningAPI] + self.az = az + self.config = config + self.loop = loop + self.bot = bot + self.mx = None + self.session_container = session_container + self.public_website = None + self.provisioning_api = None @property def core(self) -> Tuple['AppService', 'Config', 'asyncio.AbstractEventLoop', Optional['Bot']]: diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 4796036c..661da6cc 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -26,6 +26,9 @@ from . import user as u, portal as po, puppet as pu, commands as com if TYPE_CHECKING: from .context import Context + from .config import Config + from .bot import Bot + from mautrix_appservice import AppService try: from prometheus_client import Histogram @@ -38,12 +41,17 @@ except ImportError: class MatrixHandler: - log = logging.getLogger("mau.mx") # type: logging.Logger + log: logging.Logger = logging.getLogger("mau.mx") + az: 'AppService' + config: 'Config' + bot: 'Bot' + commands: 'com.CommandProcessor' + previously_typing: Dict[MatrixRoomID, Set[MatrixUserID]] def __init__(self, context: 'Context') -> None: self.az, self.config, _, self.tgbot = context.core - self.commands = com.CommandProcessor(context) # type: com.CommandProcessor - self.previously_typing = [] # type: List[MatrixUserID] + self.commands = com.CommandProcessor(context) + self.previously_typing = {} self.az.matrix_event_handler(self.handle_event) @@ -372,14 +380,16 @@ class MatrixHandler: return await user.set_presence(presence == "online") - async def handle_typing(self, room_id: MatrixRoomID, now_typing: List[MatrixUserID]) -> None: + async def handle_typing(self, room_id: MatrixRoomID, now_typing: Set[MatrixUserID]) -> None: portal = po.Portal.get_by_mxid(room_id) if not portal: return - for user_id in set(self.previously_typing + now_typing): + previously_typing = self.previously_typing.get(room_id, set()) + + for user_id in set(previously_typing | now_typing): is_typing = user_id in now_typing - was_typing = user_id in self.previously_typing + was_typing = user_id in previously_typing if is_typing and was_typing: continue @@ -389,7 +399,7 @@ class MatrixHandler: await portal.set_typing(user, is_typing) - self.previously_typing = now_typing + self.previously_typing[room_id] = now_typing def filter_matrix_event(self, event: MatrixEvent) -> bool: sender = event.get("sender", None) @@ -405,38 +415,38 @@ class MatrixHandler: self.log.exception("Error handling manually received Matrix event") async def handle_ephemeral_event(self, evt: MatrixEvent) -> None: - evt_type = evt.get("type", "m.unknown") # type: str - room_id = evt.get("room_id", None) # type: Optional[MatrixRoomID] - sender = evt.get("sender", None) # type: Optional[MatrixUserID] - content = evt.get("content", {}) # type: Dict + evt_type: str = evt.get("type", "m.unknown") + room_id: Optional[MatrixRoomID] = evt.get("room_id", None) + sender: Optional[MatrixUserID] = evt.get("sender", None) + content: Dict = evt.get("content", {}) if evt_type == "m.receipt": await self.handle_read_receipts(room_id, self.parse_read_receipts(content)) elif evt_type == "m.presence": await self.handle_presence(sender, content.get("presence", "offline")) elif evt_type == "m.typing": - await self.handle_typing(room_id, content.get("user_ids", [])) + await self.handle_typing(room_id, set(content.get("user_ids", []))) async def handle_event(self, evt: MatrixEvent) -> None: if self.filter_matrix_event(evt): return start_time = time.time() self.log.debug("Received event: %s", evt) - evt_type = evt.get("type", "m.unknown") # type: str - room_id = evt.get("room_id", None) # type: Optional[MatrixRoomID] - event_id = evt.get("event_id", None) # type: Optional[MatrixEventID] - sender = evt.get("sender", None) # type: Optional[MatrixUserID] + evt_type: str = evt.get("type", "m.unknown") + room_id: Optional[MatrixRoomID] = evt.get("room_id", None) + event_id: Optional[MatrixEventID] = evt.get("event_id", None) + sender: Optional[MatrixUserID] = evt.get("sender", None) state_key = evt.get("state_key", None) - content = evt.get("content", {}) # type: Dict + content: Dict = evt.get("content", {}) if state_key is not None: if evt_type == "m.room.member": - prev_content = evt.get("unsigned", {}).get("prev_content", {}) # type: Dict - membership = content.get("membership", "") # type: str - prev_membership = prev_content.get("membership", "leave") # type: str + prev_content: Dict = evt.get("unsigned", {}).get("prev_content", {}) + membership: str = content.get("membership", "") + prev_membership: str = prev_content.get("membership", "leave") if membership == prev_membership: - match = re.compile("@(.+):(.+)").match(state_key) # type: Match - mxid = match.group(0) # type: str - displayname = content.get("displayname", None) or mxid # type: str - prev_displayname = prev_content.get("displayname", None) or mxid # type: str + match: Match = re.compile("@(.+):(.+)").match(state_key) + mxid: str = match.group(0) + displayname: str = content.get("displayname", None) or mxid + prev_displayname: str = prev_content.get("displayname", None) or mxid if displayname != prev_displayname: await self.handle_name_change(room_id, state_key, displayname, prev_displayname, event_id) diff --git a/mautrix_telegram/sqlstatestore.py b/mautrix_telegram/sqlstatestore.py index cc8d4f51..cd7e4ab0 100644 --- a/mautrix_telegram/sqlstatestore.py +++ b/mautrix_telegram/sqlstatestore.py @@ -23,10 +23,13 @@ from .db import RoomState, UserProfile class SQLStateStore(StateStore): + profile_cache: Dict[Tuple[str, str], UserProfile] + room_state_cache: Dict[str, RoomState] + def __init__(self) -> None: super().__init__() - self.profile_cache = {} # type: Dict[Tuple[str, str], UserProfile] - self.room_state_cache = {} # type: Dict[str, RoomState] + self.profile_cache = {} + self.room_state_cache = {} @staticmethod def is_registered(user: MatrixUserID) -> bool: diff --git a/mautrix_telegram/tgclient.py b/mautrix_telegram/tgclient.py index 71737d76..2f49bcd6 100644 --- a/mautrix_telegram/tgclient.py +++ b/mautrix_telegram/tgclient.py @@ -21,9 +21,12 @@ from telethon.tl.types import ( InputMediaUploadedDocument, InputMediaUploadedPhoto, TypeDocumentAttribute, TypeInputMedia, TypeInputPeer, TypeMessageEntity, TypeMessageMedia, TypePeer) from telethon.tl.patched import Message +from telethon.sessions.abstract import Session class MautrixTelegramClient(TelegramClient): + session: Session + async def upload_file_direct(self, file: bytes, mime_type: str = None, attributes: List[TypeDocumentAttribute] = None, file_name: str = None, max_image_size: float = 10 * 1000 ** 2, diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 7d196209..cfab854e 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -13,7 +13,8 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Awaitable, Dict, List, Iterable, Match, NewType, Optional, Tuple, TYPE_CHECKING +from typing import (Awaitable, Dict, List, Iterable, Match, NewType, Optional, Tuple, Any, + TYPE_CHECKING) import logging import asyncio import re @@ -35,15 +36,23 @@ if TYPE_CHECKING: from .config import Config from .context import Context -config = None # type: Config +config: Optional['Config'] = None SearchResult = NewType('SearchResult', Tuple['pu.Puppet', int]) class User(AbstractUser): - log = logging.getLogger("mau.user") # type: logging.Logger - by_mxid = {} # type: Dict[str, User] - by_tgid = {} # type: Dict[int, User] + log: logging.Logger = logging.getLogger("mau.user") + by_mxid: Dict[str, 'User'] = {} + by_tgid: Dict[int, 'User'] = {} + + phone: Optional[str] + contacts: List['pu.Puppet'] + saved_contacts: int + portals: Dict[Tuple[TelegramID, TelegramID], 'po.Portal'] + command_status: Optional[Dict[str, Any]] + + _db_instance: Optional[DBUser] def __init__(self, mxid: MatrixUserID, tgid: Optional[TelegramID] = None, username: Optional[str] = None, phone: Optional[str] = None, @@ -52,19 +61,19 @@ class User(AbstractUser): db_portals: Optional[Iterable[Tuple[TelegramID, TelegramID]]] = None, db_instance: Optional[DBUser] = None) -> None: super().__init__() - self.mxid = mxid # type: MatrixUserID - self.tgid = tgid # type: TelegramID - self.is_bot = is_bot # type: bool - self.username = username # type: str - self.phone = phone # type: str - self.contacts = [] # type: List[pu.Puppet] - self.saved_contacts = saved_contacts # type: int + self.mxid = mxid + self.tgid = tgid + self.is_bot = is_bot + self.username = username + self.phone = phone + self.contacts = [] + self.saved_contacts = saved_contacts self.db_contacts = db_contacts - self.portals = {} # type: Dict[Tuple[TelegramID, TelegramID], po.Portal] + self.portals = {} self.db_portals = db_portals or [] - self._db_instance = db_instance # type: Optional[DBUser] + self._db_instance = db_instance - self.command_status = None # type: Optional[Dict] + self.command_status = None (self.relaybot_whitelisted, self.whitelisted, @@ -83,7 +92,7 @@ class User(AbstractUser): @property def mxid_localpart(self) -> str: - match = re.compile("@(.+):(.+)").match(self.mxid) # type: Match + match: Match = re.compile("@(.+):(.+)").match(self.mxid) return match.group(1) @property @@ -228,7 +237,7 @@ class User(AbstractUser): self.phone = info.phone changed = True if self.tgid != info.id: - self.tgid = info.id + self.tgid = TelegramID(info.id) self.by_tgid[self.tgid] = self if changed: self.save()