diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..e2fdfb75 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,8 @@ +engines: + sonar-python: + enabled: true + checks: + python:S107: + enabled: false +exclude_patterns: +- "alembic/" diff --git a/.gitignore b/.gitignore index 2eca9ad6..e3545879 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ __pycache__ config.yaml registration.yaml +logs/ *.db diff --git a/example-config.yaml b/example-config.yaml index ea03b25f..27c64991 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -11,12 +11,11 @@ homeserver: # Application service host/registration related details # Changing these values requires regeneration of the registration. appservice: - # The protocol the homeserver should use when connecting to this appservice. - # Usually "http" or "https". - protocol: http + # The address that the homeserver can use to connect to this appservice. + address: http://localhost:8080 - # The hostname and port where the homeserver can find this appservice. - hostname: localhost + # The hostname and port where this appservice should listen. + hostname: 0.0.0.0 port: 8080 # The full URI to the database. @@ -34,9 +33,6 @@ appservice: # implicitly. external: https://example.com/public - # Whether or not to enable debug messages in the console. - debug: true - # The unique ID of this appservice. id: telegram # Username of the appservice bot. @@ -194,3 +190,44 @@ telegram: api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz # (Optional) Create your own bot at https://t.me/BotFather bot_token: disabled + # Telethon proxy configuration. + # You must install PySocks from pip for proxies to work. + proxy: + # Allowed types: disabled, socks4, socks5, http + type: disabled + # Proxy IP address and port. + address: 127.0.0.1 + port: 1080 + # Whether or not to perform DNS resolving remotely. + rdns: true + # Proxy authentication (optional). + username: "" + password: "" + +# Python logging configuration. +# +# See section 16.7.2 of the Python documentation for more info: +# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema +logging: + version: 1 + formatters: + precise: + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: precise + filename: ./mautrix-telegram.log + maxBytes: 10485760 + backupCount: 10 + console: + class: logging.StreamHandler + formatter: precise + loggers: + mau: + level: DEBUG + telethon: + level: DEBUG + root: + level: DEBUG + handlers: [file, console] diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index f50bfc92..0d06f101 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -17,6 +17,7 @@ import argparse import sys import logging +import logging.config import asyncio import sqlalchemy as sql @@ -29,6 +30,7 @@ from .base import Base from .config import Config from .matrix import MatrixHandler +from . import __version__ from .db import init as init_db from .abstract_user import init as init_abstract_user from .user import init as init_user, User @@ -40,12 +42,6 @@ from .public import PublicBridgeWebsite from .context import Context from .sqlstatestore import SQLStateStore -log = logging.getLogger("mau") -time_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s@%(name)s] %(message)s") -handler = logging.StreamHandler() -handler.setFormatter(time_formatter) -log.addHandler(handler) - parser = argparse.ArgumentParser( description="A Matrix-Telegram puppeting bridge.", prog="python -m mautrix-telegram") @@ -70,14 +66,11 @@ if args.generate_registration: print(f"Registration generated and saved to {config.registration_path}") sys.exit(0) -if config["appservice.debug"]: - telethon_log = logging.getLogger("telethon") - telethon_log.addHandler(handler) - telethon_log.setLevel(logging.DEBUG) - log.setLevel(logging.DEBUG) - log.debug("Debug messages enabled.") +logging.config.dictConfig(config["logging"]) +log = logging.getLogger("mau.init") +log.debug(f"Initializing mautrix-telegram {__version__}") -db_engine = sql.create_engine(config.get("appservice.database", "sqlite:///mautrix-telegram.db")) +db_engine = sql.create_engine(config["appservice.database"] or "sqlite:///mautrix-telegram.db") db_factory = orm.sessionmaker(bind=db_engine) db_session = orm.scoping.scoped_session(db_factory) Base.metadata.bind = db_engine @@ -114,9 +107,13 @@ with appserv.run(config["appservice.hostname"], config["appservice.port"]) as st startup_actions.append(context.bot.start()) try: + log.debug("Initialization complete, running startup actions") loop.run_until_complete(asyncio.gather(*startup_actions, loop=loop)) + log.debug("Startup actions complete, now running forever") loop.run_forever() except KeyboardInterrupt: - for user in User.by_tgid.values(): - user.stop() + log.debug("Keyboard interrupt received, stopping clients") + loop.run_until_complete( + asyncio.gather(*[user.stop() for user in User.by_tgid.values()], loop=loop)) + log.debug("Clients stopped, shutting down") sys.exit(0) diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index 0658f904..dd1fe03b 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -50,6 +50,23 @@ class AbstractUser: def connected(self): return self.client and self.client.is_connected() + @property + def _proxy_settings(self): + type = config["telegram.proxy.type"].lower() + if type == "disabled": + return None + elif type == "socks4": + type = 1 + elif type == "socks5": + type = 2 + elif type == "http": + type = 3 + + return (type, + config["telegram.proxy.address"], config["telegram.proxy.port"], + config["telegram.proxy.rdns"], + config["telegram.proxy.username"], config["telegram.proxy.password"]) + def _init_client(self): self.log.debug(f"Initializing client for {self.name}") device = f"{platform.system()} {platform.release()}" @@ -62,7 +79,8 @@ class AbstractUser: app_version=__version__, system_version=sysversion, device_model=device, - timeout=120) + timeout=120, + proxy=self._proxy_settings) self.client.add_event_handler(self._update_catch) async def update(self, update): @@ -95,7 +113,9 @@ class AbstractUser: return self.client and await self.client.is_user_authorized() async def has_full_access(self, allow_bot=False): - return self.puppet_whitelisted and (not self.is_bot or allow_bot) and await self.is_logged_in() + return (self.puppet_whitelisted + and (not self.is_bot or allow_bot) + and await self.is_logged_in()) async def start(self, delete_unless_authenticated=False): if not self.client: @@ -118,8 +138,8 @@ class AbstractUser: await self.start(delete_unless_authenticated=not even_if_no_session) return self - def stop(self): - self.client.disconnect() + async def stop(self): + await self.client.disconnect() self.client = None # region Telegram update handling diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py index 8caf21a5..4c6b135c 100644 --- a/mautrix_telegram/commands/auth.py +++ b/mautrix_telegram/commands/auth.py @@ -174,7 +174,7 @@ async def enter_phone_or_token(evt: CommandEvent): # phone numbers don't contain colons but telegram bot auth tokens do if evt.args[0].find(":") > 0: try: - await sign_in(bot_token=evt.args[0]) + await sign_in(evt, bot_token=evt.args[0]) except Exception: evt.log.exception("Error sending auth token") return await evt.reply("Unhandled exception while sending auth token. " @@ -194,7 +194,7 @@ async def enter_code(evt: CommandEvent): return await evt.reply("This bridge instance does not allow in-Matrix login. " "Please use `$cmdprefix+sp login` to get login instructions") try: - await sign_in(code=evt.args[0]) + await sign_in(evt, code=evt.args[0]) except Exception: evt.log.exception("Error sending phone code") return await evt.reply("Unhandled exception while sending code. " @@ -209,12 +209,17 @@ async def enter_password(evt: CommandEvent): return await evt.reply("This bridge instance does not allow in-Matrix login. " "Please use `$cmdprefix+sp login` to get login instructions") try: - await sign_in(password=" ".join(evt.args)) + await sign_in(evt, password=" ".join(evt.args)) + except AccessTokenInvalidError: + return await evt.reply("That bot token is not valid.") + except AccessTokenExpiredError: + return await evt.reply("That bot token has expired.") except Exception: evt.log.exception("Error sending password") return await evt.reply("Unhandled exception while sending password. " "Check console for more details.") + async def sign_in(evt: CommandEvent, **sign_in_info): try: await evt.sender.ensure_started(even_if_no_session=True) @@ -236,6 +241,7 @@ async def sign_in(evt: CommandEvent, **sign_in_info): return await evt.reply("Your account has two-factor authentication. " "Please send your password here.") + @command_handler(needs_auth=True, help_section=SECTION_AUTH, help_text="Log out from Telegram.") diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 09476659..fb2e14a2 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -144,7 +144,12 @@ class Config(DictWithRecursion): copy("homeserver.verify_ssl") copy("homeserver.domain") - copy("appservice.protocol") + if "appservice.protocol" in self and "appservice.address" not in self: + protocol, hostname, port = (self["appservice.protocol"], self["appservice.hostname"], + self["appservice.port"]) + base["appservice.address"] = f"{protocol}://{hostname}:{port}" + else: + copy("appservice.address") copy("appservice.hostname") copy("appservice.port") @@ -154,8 +159,6 @@ class Config(DictWithRecursion): copy("appservice.public.prefix") copy("appservice.public.external") - copy("appservice.debug") - copy("appservice.id") copy("appservice.bot_username") copy("appservice.bot_displayname") @@ -217,6 +220,20 @@ class Config(DictWithRecursion): copy("telegram.api_id") copy("telegram.api_hash") copy("telegram.bot_token") + copy("telegram.proxy.type") + copy("telegram.proxy.address") + copy("telegram.proxy.port") + copy("telegram.proxy.rdns") + copy("telegram.proxy.username") + copy("telegram.proxy.password") + + if "appservice.debug" in self and "logging" not in self: + level = "DEBUG" if self["appservice.debug"] else "INFO" + base["logging.root.level"] = level + base["logging.loggers.mau.level"] = level + base["logging.loggers.telethon.level"] = level + else: + copy("logging") self._data = base._data self.save() @@ -251,10 +268,8 @@ class Config(DictWithRecursion): self.set("appservice.as_token", self._new_token()) self.set("appservice.hs_token", self._new_token()) - url = (f"{self['appservice.protocol']}://" - f"{self['appservice.hostname']}:{self['appservice.port']}") self._registration = { - "id": self.get("appservice.id", "telegram"), + "id": self["appservice.id"] or "telegram", "as_token": self["appservice.as_token"], "hs_token": self["appservice.hs_token"], "namespaces": { @@ -267,7 +282,7 @@ class Config(DictWithRecursion): "regex": f"#{alias_format}:{homeserver}" }] }, - "url": url, + "url": self["appservice.address"], "sender_localpart": self["appservice.bot_username"], "rate_limited": False } diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 1a72fd0a..da62fad8 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -145,7 +145,7 @@ class User(AbstractUser): asyncio.ensure_future(self.post_login(), loop=self.loop) elif delete_unless_authenticated: self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...") - self.client.disconnect() + await self.client.disconnect() self.client.session.delete() return self