Add command to update room-specific config

This commit is contained in:
Tulir Asokan
2018-09-24 17:44:00 +03:00
parent fc23461445
commit 9d2d34a25c
6 changed files with 158 additions and 17 deletions
+2 -2
View File
@@ -114,6 +114,8 @@ bridge:
# Only enable this if your displayname_template has some static part that the bridge can use to # Only enable this if your displayname_template has some static part that the bridge can use to
# reliably identify what is a plaintext highlight. # reliably identify what is a plaintext highlight.
plaintext_highlights: false plaintext_highlights: false
# Highlight changed/added parts in edits. Requires lxml.
highlight_edits: false
# Whether or not to make portals of publicly joinable channels/supergroups publicly joinable on Matrix. # Whether or not to make portals of publicly joinable channels/supergroups publicly joinable on Matrix.
public_portals: true public_portals: true
# Whether or not to fetch and handle Telegram updates at startup from the time the bridge was down. # Whether or not to fetch and handle Telegram updates at startup from the time the bridge was down.
@@ -136,8 +138,6 @@ bridge:
# Show message editing as a reply to the original message. # Show message editing as a reply to the original message.
# If this is false, message edits are not shown at all, as Matrix does not support editing yet. # If this is false, message edits are not shown at all, as Matrix does not support editing yet.
edits_as_replies: false edits_as_replies: false
# Highlight changed/added parts in edits. Requires lxml.
highlight_edits: false
bridge_notices: bridge_notices:
# Whether or not Matrix bot messages (type m.notice) should be bridged. # Whether or not Matrix bot messages (type m.notice) should be bridged.
default: false default: false
+78 -1
View File
@@ -15,6 +15,7 @@
# 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 Dict, Callable, Optional, Tuple, Coroutine from typing import Dict, Callable, Optional, Tuple, Coroutine
from io import StringIO
import asyncio import asyncio
from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError, from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
@@ -23,7 +24,8 @@ from telethon.tl.types import ChatForbidden, ChannelForbidden
from mautrix_appservice import MatrixRequestError, IntentAPI from mautrix_appservice import MatrixRequestError, IntentAPI
from ..types import MatrixRoomID, TelegramID from ..types import MatrixRoomID, TelegramID
from .. import portal as po, user as u from ..config import yaml
from .. import portal as po, user as u, util
from . import (command_handler, CommandEvent, from . import (command_handler, CommandEvent,
SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT) SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT)
@@ -391,6 +393,81 @@ async def upgrade(evt: CommandEvent) -> Dict:
return await evt.reply(e.args[0]) return await evt.reply(e.args[0])
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
help_text="View or change per-portal settings.",
help_args="<`help`|_subcommand_> [...]")
async def config(evt: CommandEvent) -> Dict:
cmd = evt.args[0].lower() if len(evt.args) > 0 else "help"
portal = po.Portal.get_by_mxid(evt.room_id)
if not portal and cmd != "help":
return await evt.reply("This is not a portal room.")
if cmd == "help":
return await evt.reply("""`$cmdprefix config`:
* `help` - View this help text.
* `view` - View the current config data.
* `defaults` - View the default config values.
* `set` <_key_> <_value_> - Set a config value.
* `unset` <_key_> - Remove a config value.
* `add` <_key_> <_value_> - Add a value to an array.
* `del` <_key_> <_value_> - Remove a value from an array.
""")
elif cmd == "view":
stream = StringIO()
yaml.dump(portal.local_config, stream)
return await evt.reply(f"Room-specific config:\n```yaml\n{stream.getvalue()}\n```")
elif cmd == "defaults":
stream = StringIO()
yaml.dump({
"edits_as_replies": evt.config["bridge.edits_as_replies"],
"bridge_notices": evt.config["bridge.bridge_notices"],
"bot_messages_as_notices": evt.config["bridge.bot_messages_as_notices"],
"inline_images": evt.config["bridge.inline_images"],
"native_stickers": evt.config["native_stickers"],
"message_formats": evt.config["message_formats"],
"state_event_formats": evt.config["state_event_formats"],
}, stream)
return await evt.reply(f"Bridge instance wide config:\n```yaml\n{stream.getvalue()}\n```")
key = evt.args[1] if len(evt.args) > 1 else None
value = yaml.load(evt.args[2:]) if len(evt.args) > 2 else None
if cmd == "set":
if not key or not value:
return await evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} <key> <value>`")
elif util.recursive_set(portal.local_config, key, value):
return await evt.reply(f"Successfully set the value of `{key}` to `{value}`.")
else:
return await evt.reply(f"Failed to set value of `{key}`. "
"Does the path contain non-map types?")
elif cmd == "unset":
if not key:
return await evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} <key>`")
elif util.recursive_del(portal.local_config, key):
return await evt.reply(f"Successfully deleted `{key}` from config.")
else:
return await evt.reply(f"`{key}` not found in config.")
elif cmd == "add" or cmd == "del":
if not key or not value:
return await evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} <key> <value>`")
arr = util.recursive_get(portal.local_config, key)
if not arr:
return await evt.reply(f"`{key}` not found in config. "
f"Maybe do `$cmdprefix+sp config set {key} []` first?")
elif not isinstance(arr, list):
return await evt.reply("`{key}` does not seem to be an array.")
elif cmd == "add":
if value in arr:
return await evt.reply(f"The array at `{key}` already contains `{value}`.")
arr.append(value)
return await evt.reply(f"Successfully added `{value}` to the array at `{key}`")
else:
if value not in arr:
return await evt.reply(f"The array at `{key}` does not contain `{value}`.")
arr.remove(value)
return await evt.reply(f"Successfully removed `{value}` from the array at `{key}`")
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT, @command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
help_args="<_name_|`-`>", help_args="<_name_|`-`>",
help_text="Change the username of a supergroup/channel. " help_text="Change the username of a supergroup/channel. "
+20 -11
View File
@@ -20,7 +20,7 @@ from ruamel.yaml.comments import CommentedMap
import random import random
import string import string
yaml = YAML() yaml = YAML() # type: YAML
yaml.indent(4) yaml.indent(4)
@@ -28,9 +28,20 @@ class DictWithRecursion:
def __init__(self, data: Optional[CommentedMap] = None) -> None: def __init__(self, data: Optional[CommentedMap] = None) -> None:
self._data = data or CommentedMap() # type: CommentedMap self._data = data or CommentedMap() # type: CommentedMap
@staticmethod
def _parse_key(key: str) -> Tuple[str, Optional[str]]:
if '.' not in key:
return key, None
key, next_key = key.split('.', 1)
if len(key) > 0 and key[0] == "[":
end_index = next_key.index("]")
key = key[1:] + "." + next_key[:end_index]
next_key = next_key[end_index + 2:] if len(next_key) > end_index + 1 else None
return key, next_key
def _recursive_get(self, data: CommentedMap, key: str, default_value: Any) -> Any: def _recursive_get(self, data: CommentedMap, key: str, default_value: Any) -> Any:
if '.' in key: key, next_key = self._parse_key(key)
key, next_key = key.split('.', 1) if next_key is not None:
next_data = data.get(key, CommentedMap()) next_data = data.get(key, CommentedMap())
return self._recursive_get(next_data, next_key, default_value) return self._recursive_get(next_data, next_key, default_value)
return data.get(key, default_value) return data.get(key, default_value)
@@ -47,13 +58,12 @@ class DictWithRecursion:
return self[key] is not None return self[key] is not None
def _recursive_set(self, data: CommentedMap, key: str, value: Any) -> None: def _recursive_set(self, data: CommentedMap, key: str, value: Any) -> None:
if '.' in key: key, next_key = self._parse_key(key)
key, next_key = key.split('.', 1) if next_key is not None:
if key not in data: if key not in data:
data[key] = CommentedMap() data[key] = CommentedMap()
next_data = data.get(key, CommentedMap()) next_data = data.get(key, CommentedMap())
self._recursive_set(next_data, next_key, value) return self._recursive_set(next_data, next_key, value)
return
data[key] = value data[key] = value
def set(self, key: str, value: Any, allow_recursion: bool = True) -> None: def set(self, key: str, value: Any, allow_recursion: bool = True) -> None:
@@ -66,13 +76,12 @@ class DictWithRecursion:
self.set(key, value) self.set(key, value)
def _recursive_del(self, data: CommentedMap, key: str) -> None: def _recursive_del(self, data: CommentedMap, key: str) -> None:
if '.' in key: key, next_key = self._parse_key(key)
key, next_key = key.split('.', 1) if next_key is not None:
if key not in data: if key not in data:
return return
next_data = data[key] next_data = data[key]
self._recursive_del(next_data, next_key) return self._recursive_del(next_data, next_key)
return
try: try:
del data[key] del data[key]
del data.ca.items[key] del data.ca.items[key]
+4 -3
View File
@@ -719,14 +719,14 @@ class Portal:
return "" return ""
def get_config(self, key: str) -> Any: def get_config(self, key: str) -> Any:
local = self.local_config.get("state_event_formats", None) local = util.recursive_get(self.local_config, key)
if local is not None: if local is not None:
return local return local
return config[f"bridge.{key}"] return config[f"bridge.{key}"]
async def _get_state_change_message(self, event: str, user: 'u.User', async def _get_state_change_message(self, event: str, user: 'u.User',
arguments: Optional[Dict] = None) -> Optional[Dict]: arguments: Optional[Dict] = None) -> Optional[Dict]:
tpl = self.get_config("state_event_formats").get(event, "") tpl = self.get_config(f"state_event_formats.{event}")
if len(tpl) == 0: if len(tpl) == 0:
# Empty format means they don't want the message # Empty format means they don't want the message
return None return None
@@ -843,7 +843,8 @@ class Portal:
message["formatted_body"] = escape_html(message.get("body", "")) message["formatted_body"] = escape_html(message.get("body", ""))
body = message["formatted_body"] body = message["formatted_body"]
tpl = config.get("message_formats", {}).get(msgtype, "<b>$sender_displayname</b>: $message") tpl = (self.get_config(f"message_formats.[{msgtype}]")
or "<b>$sender_displayname</b>: $message")
displayname = await self.get_displayname(sender) displayname = await self.get_displayname(sender)
tpl_args = dict(sender_mxid=sender.mxid, tpl_args = dict(sender_mxid=sender.mxid,
sender_username=sender.mxid_localpart, sender_username=sender.mxid_localpart,
+1
View File
@@ -1,3 +1,4 @@
from .file_transfer import transfer_file_to_matrix, convert_image from .file_transfer import transfer_file_to_matrix, convert_image
from .format_duration import format_duration from .format_duration import format_duration
from .signed_token import sign_token, verify_token from .signed_token import sign_token, verify_token
from .recursive_dict import recursive_del, recursive_set, recursive_get
+53
View File
@@ -0,0 +1,53 @@
# -*- coding: future_fstrings -*-
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2018 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from typing import Dict, Any
from ..config import DictWithRecursion
def recursive_set(data: Dict[str, Any], key: str, value: Any) -> bool:
key, next_key = DictWithRecursion._parse_key(key)
if next_key is not None:
if key not in data:
data[key] = {}
next_data = data.get(key, {})
if not isinstance(next_data, dict):
return False
return recursive_set(next_data, next_key, value)
data[key] = value
return True
def recursive_get(data: Dict[str, Any], key: str) -> Any:
key, next_key = DictWithRecursion._parse_key(key)
if next_key is not None:
next_data = data.get(key, None)
if not next_data:
return None
return recursive_get(next_data, next_key)
return data.get(key, None)
def recursive_del(data: Dict[str, any], key: str) -> bool:
key, next_key = DictWithRecursion._parse_key(key)
if next_key is not None:
if key not in data:
return False
next_data = data.get(key, {})
recursive_del(next_data, next_key)
if key in data:
del data[key]
return True
return False