From ad67996d917ab059e9deac9050568aaa8a91181e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Mar 2018 00:35:37 +0200 Subject: [PATCH 1/2] Update ROADMAP.md --- ROADMAP.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e6a0a5d7..cbd8ce27 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,20 +1,20 @@ # Features & roadmap * Matrix → Telegram - * [ ] Message content + * [x] Message content * [x] Plaintext messages * [x] Formatted messages * [x] Bot commands (!command -> /command) * [x] Mentions * [x] Rich quotes - * [ ] Locations (not implemented in Riot) + * [x] Locations (not implemented in Riot) * [x] Images * [x] Files * [x] Message redactions * [ ] † Presence * [ ] † Typing notifications * [ ] † Read receipts - * [ ] Pinning messages + * [x] Pinning messages * [x] Power level * [x] Normal chats * [ ] Non-hardcoded PL requirements @@ -52,7 +52,7 @@ * [x] Read receipts (private chat only) * [x] Pinning messages * [x] Admin/chat creator status - * [ ] Supergroup/channel permissions (precise per-user not supported in Matrix) + * [ ] Supergroup/channel permissions (precise per-user permissions not supported in Matrix) * [x] Membership actions * [x] Inviting * [x] Kicking From 715b658a3dddbd9cfd8bd1ba8a21da607be13369 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Mar 2018 11:25:29 +0200 Subject: [PATCH 2/2] Switch to a simpler non-versioned automatic config update --- example-config.yaml | 13 ++- mautrix_telegram/__main__.py | 9 +- mautrix_telegram/bot.py | 2 +- mautrix_telegram/config.py | 216 +++++++++++++---------------------- 4 files changed, 96 insertions(+), 144 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 08688e6b..ec113d9f 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -1,7 +1,11 @@ # Homeserver details homeserver: + # The address that this appservice can use to connect to the homeserver. address: https://matrix.org + # The domain of the homeserver (for MXIDs, etc). domain: matrix.org + # Whether or not to verify the SSL certificate of the homeserver. + # Only applies if address starts with https:// verify_ssl: true # Application service host/registration related details @@ -15,6 +19,9 @@ appservice: hostname: localhost port: 8080 + # The full URI to the database. + database: sqlite:///mautrix-telegram.db + # Public part of web server for out-of-Matrix interaction with the bridge. # Used for things like login if the user wants to make sure the 2FA password isn't stored in # the HS database. @@ -127,8 +134,4 @@ telegram: api_id: 12345 api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz # (Optional) Create your own bot at https://t.me/BotFather - #bot_token: 123456789:ABCD-QBPd3VrWRhg623xYh07WUWErYA9eMI - -# The version of the config. The bridge will read this and automatically update the config if -# the schema has changed. For the latest version, check the example config. -version: 2 + bot_token: disabled diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index dc9355b1..f23e18b9 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -26,7 +26,7 @@ from telethon_aio.sessions import AlchemySessionContainer from mautrix_appservice import AppService from .base import Base -from .config import Config +from .config import Config, DictWithRecursion from .matrix import MatrixHandler from .db import init as init_db @@ -50,15 +50,18 @@ parser = argparse.ArgumentParser( prog="python -m mautrix-telegram") parser.add_argument("-c", "--config", type=str, default="config.yaml", metavar="", help="the path to your config file") +parser.add_argument("-b", "--base-config", type=str, default="example-config.yaml", + metavar="", help="the path to the example config " + "(for automatic config updates)") parser.add_argument("-g", "--generate-registration", action="store_true", help="generate registration and quit") parser.add_argument("-r", "--registration", type=str, default="registration.yaml", metavar="", help="the path to save the generated registration to") args = parser.parse_args() -config = Config(args.config, args.registration) +config = Config(args.config, args.registration, args.base_config) config.load() -config.check_updates() +config.update() if args.generate_registration: config.generate_registration() diff --git a/mautrix_telegram/bot.py b/mautrix_telegram/bot.py index 3e0b7cb9..5f868f0b 100644 --- a/mautrix_telegram/bot.py +++ b/mautrix_telegram/bot.py @@ -253,6 +253,6 @@ def init(context): global config config = context.config token = config["telegram.bot_token"] - if token: + if token and not token.lower().startswith("disable"): return Bot(token) return None diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index cfc00357..018727e7 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -16,8 +16,6 @@ # along with this program. If not, see . from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap -from ruamel.yaml.tokens import CommentToken -from ruamel.yaml.error import CommentMark import random import string @@ -93,38 +91,27 @@ class DictWithRecursion: def __delitem__(self, key): self.delete(key) - def comment(self, key, message): - indent = key.count(".") * 4 - try: - path, key = key.rsplit(".", 1) - except ValueError: - path = None - entry = self[path] if path else self._data - c = entry.ca.items.setdefault(key, [None, [], None, None]) - c[1] = [] - entry.yaml_set_comment_before_after_key(key=key, before=message, indent=indent) - - def comment_newline(self, key): - try: - path, key = key.rsplit(".", 1) - except ValueError: - path = None - entry = self[path] if path else self._data - c = entry.ca.items.setdefault(key, [None, [], None, None]) - c[2] = CommentToken("\n\n", CommentMark(0), None) - class Config(DictWithRecursion): - def __init__(self, path, registration_path): + def __init__(self, path, registration_path, base_path): super().__init__() self.path = path self.registration_path = registration_path + self.base_path = base_path self._registration = None def load(self): with open(self.path, 'r') as stream: self._data = yaml.load(stream) + def load_base(self): + try: + with open(self.base_path, 'r') as stream: + return DictWithRecursion(yaml.load(stream)) + except OSError: + pass + return None + def save(self): with open(self.path, 'w') as stream: yaml.dump(self._data, stream) @@ -136,126 +123,85 @@ class Config(DictWithRecursion): def _new_token(): return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(64)) - def update_0_1(self): - permissions = self["bridge.permissions"] or CommentedMap() - for entry in self["bridge.whitelist"] or []: - permissions[entry] = "full" - for entry in self["bridge.admins"] or []: - permissions[entry] = "admin" + def update(self): + base = self.load_base() + if not base: + return - self["bridge.permissions"] = permissions - del self["bridge.whitelist"] - del self["bridge.admins"] + def copy(from_path, to_path=None): + if from_path in self: + base[to_path or from_path] = self[from_path] - if "bridge.authless_relaybot_portals" not in self: - self["bridge.authless_relaybot_portals"] = True - self.comment("bridge.authless_relaybot_portals", - "Whether or not to allow creating portals from Telegram.") - if "bridge.max_telegram_delete" not in self: - self["bridge.max_telegram_delete"] = 10 - self.comment("bridge.max_telegram_delete", - "The maximum number of simultaneous Telegram deletions to handle.\n" - "A large number of simultaneous redactions could put strain on your " - "homeserver.") + def copy_dict(from_path, to_path=None): + if from_path in self: + to_path = to_path or from_path + base[to_path] = CommentedMap() + for key, value in self[from_path].items(): + base[to_path][key] = value - self.comment("bridge.permissions", "\n".join(( - "", - "Permissions for using the bridge.", - "Permitted values:", - " relaybot - Only use the bridge via the relaybot, no access to commands.", - " full - Full access to use the bridge via relaybot or logging in with Telegram account.", - " admin - Full access to use the bridge and some extra administration commands.", - "Permitted keys:", - " * - All Matrix users", - " domain - All users on that homeserver", - " mxid - Specific user"))) - # The telegram section comment disappears for some reason 3: - self.comment("telegram", "\nTelegram config") + copy("homeserver.address") + copy("homeserver.verify_ssl") + copy("homeserver.domain") - self["version"] = 1 - # Add newline before version - self.comment("version", - "\nThe version of the config. The bridge will read this and automatically " - "update the config if\nthe schema has changed. For the latest version, " - "check the example config.") - return self["version"] + copy("appservice.protocol") + copy("appservice.hostname") + copy("appservice.port") - def update_1_2(self): - del self["bridge.link_in_reply"] - del self["bridge.native_replies"] - if "bridge.bridge_notices" not in self: - self["bridge.bridge_notices"] = False - self.comment("bridge.bridge_notices", - "Whether or not Matrix bot messages (type m.notice) should be bridged.") - if "bridge.allow_matrix_login" not in self: - self["bridge.allow_matrix_login"] = True - self.comment("bridge.allow_matrix_login", - "Allow logging in within Matrix. If false, the only way to log in is " - "using the out-of-Matrix login website (see appservice.public config " - "section)") - if "bridge.inline_images" not in self: - self["bridge.inline_images"] = False - self.comment("bridge.inline_images", - "Use inline images instead of m.image to make rich captions possible.\n" - "N.B. Inline images are not supported on all clients (e.g. Riot iOS).") - if "appservice.public" not in self: - self["appservice.public.enabled"] = False - self["appservice.public.prefix"] = "/public" - self["appservice.public.external"] = "https://example.com/public" - self.comment("appservice.public", - "Public part of web server for out-of-Matrix interaction with the " - "bridge.\nUsed for things like login if the user wants to make sure the " - "2FA password isn't stored in the HS database.") - self.comment("appservice.public.enabled", - "Whether or not the public-facing endpoints should be enabled.") - self.comment("appservice.public.prefix", - "The prefix to use in the public-facing endpoints.") - self.comment("appservice.public.external", - "The base URL where the public-facing endpoints are available. The " - "prefix is not added\nimplicitly.") - if "homeserver.verify_ssl" not in self: - self["homeserver.verify_ssl"] = True - self["version"] = 2 - return self["version"] + copy("appservice.public.enabled") + copy("appservice.public.prefix") + copy("appservice.public.external") + + copy("appservice.debug") + + copy("appservice.id") + copy("appservice.bot_username") + copy("appservice.bot_displayname") + + copy("appservice.as_token") + copy("appservice.hs_token") + + copy("bridge.username_template") + copy("bridge.alias_template") + copy("bridge.displayname_template") + + copy("bridge.displayname_preference") + + copy("bridge.edits_as_replies") + copy("bridge.highlight_edits") + copy("bridge.bridge_notices") + copy("bridge.max_telegram_delete") + copy("bridge.allow_matrix_login") + copy("bridge.inline_images") + copy("bridge.plaintext_highlights") + + copy("bridge.command_prefix") + + migrate_permissions = ("bridge.permissions" not in self + or "bridge.whitelist" in self + or "bridge.admins" in self) + if migrate_permissions: + permissions = self["bridge.permissions"] or CommentedMap() + for entry in self["bridge.whitelist"] or []: + permissions[entry] = "full" + for entry in self["bridge.admins"] or []: + permissions[entry] = "admin" + base["bridge.permissions"] = permissions + else: + copy_dict("bridge.permissions") - def update_2_3(self): - if "bridge.plaintext_highlights" not in self: - self["bridge.plaintext_highlights"] = False - self.comment("bridge.plaintext_highlights", - "Whether or not to bridge plaintext highlights.\n" - "Only enable this if your displayname_template has some static part that " - "the bridge can use to\nreliably identify what is a plaintext highlight.") - if "bridge.highlight_edits" not in self: - self["bridge.highlight_edits"] = False - self.comment("bridge.highlight_edits", - "Highlight changed/added parts in edits. Requires lxml.") if "bridge.relaybot" not in self: - self["bridge.relaybot.authless_portals"] = bool( - self["bridge.authless_relaybot_portals"]) or True - del self["bridge.authless_relaybot_portals"] - self["bridge.relaybot.whitelist_group_admins"] = True - self["bridge.relaybot.whitelist"] = [] - self.comment("bridge.relaybot", "Options related to the message relay Telegram bot.") - self.comment("bridge.relaybot.authless_portals", - "Whether or not to allow creating portals from Telegram.") - self.comment("bridge.relaybot.whitelist_group_admins", - "Whether or not to allow Telegram group admins to use the bot commands.") - self.comment("bridge.relaybot.whitelist", - "List of usernames/user IDs who are also allowed to use the bot commands.") - self["version"] = 3 - return self["version"] + copy("bridge.authless_relaybot_portals", "bridge.relaybot.authless_portals") + else: + copy("bridge.relaybot.authless_portals") + copy("bridge.relaybot.whitelist_group_admins") + copy("bridge.relaybot.whitelist") - def check_updates(self): - version = self.get("version", 0) - new_version = version - if version < 1: - new_version = self.update_0_1() - if version < 2: - new_version = self.update_1_2() - if version < 3: - new_version = self.update_2_3() - if new_version != version: - self.save() + copy("telegram.api_id") + copy("telegram.api_hash") + copy("telegram.bot_token") + + self._data = base._data + self.save() def _get_permissions(self, key): level = self["bridge.permissions"].get(key, "")