Merge pull request #231 from tulir/room-specific-settings
Add room specific config
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
"""Add portal-specific config
|
||||||
|
|
||||||
|
Revision ID: b54929c22c86
|
||||||
|
Revises: d5f7b8b4b456
|
||||||
|
Create Date: 2018-09-24 23:40:33.528710
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "b54929c22c86"
|
||||||
|
down_revision = "d5f7b8b4b456"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column("portal", sa.Column("config", sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table("portal") as batch_op:
|
||||||
|
batch_op.drop_column("config")
|
||||||
+23
-16
@@ -95,15 +95,6 @@ bridge:
|
|||||||
- username
|
- username
|
||||||
- phone number
|
- phone number
|
||||||
|
|
||||||
# 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.
|
|
||||||
edits_as_replies: false
|
|
||||||
# Highlight changed/added parts in edits. Requires lxml.
|
|
||||||
highlight_edits: false
|
|
||||||
# Whether or not Matrix bot messages (type m.notice) should be bridged.
|
|
||||||
bridge_notices: true
|
|
||||||
# Whether to bridge Telegram bot messages as m.notices or m.texts.
|
|
||||||
bot_messages_as_notices: true
|
|
||||||
# Maximum number of members to sync per portal when starting up. Other members will be
|
# Maximum number of members to sync per portal when starting up. Other members will be
|
||||||
# synced when they send messages. The maximum is 10000, after which the Telegram server
|
# synced when they send messages. The maximum is 10000, after which the Telegram server
|
||||||
# will not send any more members.
|
# will not send any more members.
|
||||||
@@ -119,19 +110,14 @@ bridge:
|
|||||||
# Allow logging in within Matrix. If false, the only way to log in is using the out-of-Matrix
|
# 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)
|
# login website (see appservice.public config section)
|
||||||
allow_matrix_login: true
|
allow_matrix_login: true
|
||||||
# Use inline images instead of m.image to make rich captions possible.
|
|
||||||
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
|
|
||||||
inline_images: false
|
|
||||||
# Whether or not to bridge plaintext highlights.
|
# Whether or not to bridge plaintext highlights.
|
||||||
# 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 to send stickers as the new native m.sticker type or normal m.images.
|
|
||||||
# Old versions of Riot don't support the new type at all.
|
|
||||||
# Remember that proper sticker support always requires Pillow to convert webp into png.
|
|
||||||
native_stickers: 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.
|
||||||
# Currently only works for private chats and normal groups.
|
# Currently only works for private chats and normal groups.
|
||||||
catch_up: false
|
catch_up: false
|
||||||
@@ -149,6 +135,27 @@ bridge:
|
|||||||
# You might need to increase this on high-traffic bridge instances.
|
# You might need to increase this on high-traffic bridge instances.
|
||||||
cache_queue_length: 20
|
cache_queue_length: 20
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
edits_as_replies: false
|
||||||
|
bridge_notices:
|
||||||
|
# Whether or not Matrix bot messages (type m.notice) should be bridged.
|
||||||
|
default: false
|
||||||
|
# List of user IDs for whom the previous flag is flipped.
|
||||||
|
# e.g. if bridge_notices.default is false, notices from other users will not be bridged, but
|
||||||
|
# notices from users listed here will be bridged.
|
||||||
|
exceptions:
|
||||||
|
- "@importantbot:example.com"
|
||||||
|
# Whether to bridge Telegram bot messages as m.notices or m.texts.
|
||||||
|
bot_messages_as_notices: true
|
||||||
|
# Use inline images instead of m.image to make rich captions possible.
|
||||||
|
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
|
||||||
|
inline_images: false
|
||||||
|
# Whether to send stickers as the new native m.sticker type or normal m.images.
|
||||||
|
# Old versions of Riot don't support the new type at all.
|
||||||
|
# Remember that proper sticker support always requires Pillow to convert webp into png.
|
||||||
|
native_stickers: true
|
||||||
|
|
||||||
# The formats to use when sending messages to Telegram via the relay bot.
|
# The formats to use when sending messages to Telegram via the relay bot.
|
||||||
#
|
#
|
||||||
# Telegram doesn't have built-in emotes, so the m.emote format is also used for non-relaybot users.
|
# Telegram doesn't have built-in emotes, so the m.emote format is also used for non-relaybot users.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ 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
|
from .types import TelegramID, MatrixUserID
|
||||||
from .tgclient import MautrixTelegramClient
|
from .tgclient import MautrixTelegramClient
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -69,7 +69,7 @@ class AbstractUser(ABC):
|
|||||||
self.relaybot_whitelisted = False # type: bool
|
self.relaybot_whitelisted = False # type: bool
|
||||||
self.client = None # type: MautrixTelegramClient
|
self.client = None # type: MautrixTelegramClient
|
||||||
self.tgid = None # type: TelegramID
|
self.tgid = None # type: TelegramID
|
||||||
self.mxid = None # type: str
|
self.mxid = None # type: MatrixUserID
|
||||||
self.is_relaybot = False # type: bool
|
self.is_relaybot = False # type: bool
|
||||||
self.is_bot = False # type: bool
|
self.is_bot = False # type: bool
|
||||||
self.relaybot = None # type: Optional[Bot]
|
self.relaybot = None # type: Optional[Bot]
|
||||||
|
|||||||
@@ -15,7 +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 Awaitable, Callable, Dict, List, NamedTuple, Optional
|
from typing import Awaitable, Callable, Dict, List, NamedTuple, Optional
|
||||||
import markdown
|
import commonmark
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from telethon.errors import FloodWaitError
|
from telethon.errors import FloodWaitError
|
||||||
@@ -60,7 +60,8 @@ class CommandEvent:
|
|||||||
message = message.replace("$cmdprefix", self.command_prefix)
|
message = message.replace("$cmdprefix", self.command_prefix)
|
||||||
html = None
|
html = None
|
||||||
if render_markdown:
|
if render_markdown:
|
||||||
html = markdown.markdown(message, safe_mode="escape" if allow_html else False)
|
html = commonmark.commonmark(message if allow_html else
|
||||||
|
message.replace("<", "<").replace(">", ">"))
|
||||||
elif allow_html:
|
elif allow_html:
|
||||||
html = message
|
html = message
|
||||||
return self.az.intent.send_notice(self.room_id, message, html=html)
|
return self.az.intent.send_notice(self.room_id, message, html=html)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
#
|
#
|
||||||
# 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, Awaitable
|
||||||
|
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,119 @@ 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) -> None:
|
||||||
|
cmd = evt.args[0].lower() if len(evt.args) > 0 else "help"
|
||||||
|
if cmd not in ("view", "defaults", "set", "unset", "add", "del"):
|
||||||
|
await config_help(evt)
|
||||||
|
return
|
||||||
|
elif cmd == "defaults":
|
||||||
|
await config_defaults(evt)
|
||||||
|
return
|
||||||
|
|
||||||
|
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||||
|
if not portal:
|
||||||
|
await evt.reply("This is not a portal room.")
|
||||||
|
return
|
||||||
|
elif cmd == "view":
|
||||||
|
await config_view(evt, portal)
|
||||||
|
return
|
||||||
|
|
||||||
|
key = evt.args[1] if len(evt.args) > 1 else None
|
||||||
|
value = yaml.load(" ".join(evt.args[2:])) if len(evt.args) > 2 else None
|
||||||
|
if cmd == "set":
|
||||||
|
await config_set(evt, portal, key, value)
|
||||||
|
elif cmd == "unset":
|
||||||
|
await config_unset(evt, portal, key)
|
||||||
|
elif cmd == "add" or cmd == "del":
|
||||||
|
await config_add_del(evt, portal, key, value, cmd)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
portal.save()
|
||||||
|
|
||||||
|
|
||||||
|
def config_help(evt: CommandEvent) -> Awaitable[Dict]:
|
||||||
|
return evt.reply("""**Usage:** `$cmdprefix config <subcommand> [...]`. Subcommands:
|
||||||
|
|
||||||
|
* **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.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[Dict]:
|
||||||
|
stream = StringIO()
|
||||||
|
yaml.dump(portal.local_config, stream)
|
||||||
|
return evt.reply(f"Room-specific config:\n\n```yaml\n{stream.getvalue()}```",
|
||||||
|
allow_html=True)
|
||||||
|
|
||||||
|
|
||||||
|
def config_defaults(evt: CommandEvent) -> Awaitable[Dict]:
|
||||||
|
stream = StringIO()
|
||||||
|
yaml.dump({
|
||||||
|
"edits_as_replies": evt.config["bridge.edits_as_replies"],
|
||||||
|
"bridge_notices": {
|
||||||
|
"default": evt.config["bridge.bridge_notices.default"],
|
||||||
|
"exceptions": evt.config["bridge.bridge_notices.exceptions"],
|
||||||
|
},
|
||||||
|
"bot_messages_as_notices": evt.config["bridge.bot_messages_as_notices"],
|
||||||
|
"inline_images": evt.config["bridge.inline_images"],
|
||||||
|
"native_stickers": evt.config["bridge.native_stickers"],
|
||||||
|
"message_formats": evt.config["bridge.message_formats"],
|
||||||
|
"state_event_formats": evt.config["bridge.state_event_formats"],
|
||||||
|
}, stream)
|
||||||
|
return evt.reply(f"Bridge instance wide config:\n\n```yaml\n{stream.getvalue()}```",
|
||||||
|
allow_html=True)
|
||||||
|
|
||||||
|
|
||||||
|
def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Awaitable[Dict]:
|
||||||
|
if not key or value is None:
|
||||||
|
return evt.reply(f"**Usage:** `$cmdprefix+sp config set <key> <value>`")
|
||||||
|
elif util.recursive_set(portal.local_config, key, value):
|
||||||
|
return evt.reply(f"Successfully set the value of `{key}` to `{value}`.")
|
||||||
|
else:
|
||||||
|
return evt.reply(f"Failed to set value of `{key}`. "
|
||||||
|
"Does the path contain non-map types?")
|
||||||
|
|
||||||
|
|
||||||
|
def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[Dict]:
|
||||||
|
if not key:
|
||||||
|
return evt.reply(f"**Usage:** `$cmdprefix+sp config unset <key>`")
|
||||||
|
elif util.recursive_del(portal.local_config, key):
|
||||||
|
return evt.reply(f"Successfully deleted `{key}` from config.")
|
||||||
|
else:
|
||||||
|
return evt.reply(f"`{key}` not found in config.")
|
||||||
|
|
||||||
|
|
||||||
|
def config_add_del(evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str
|
||||||
|
) -> Awaitable[Dict]:
|
||||||
|
if not key or value is None:
|
||||||
|
return evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} <key> <value>`")
|
||||||
|
|
||||||
|
arr = util.recursive_get(portal.local_config, key)
|
||||||
|
if not arr:
|
||||||
|
return evt.reply(f"`{key}` not found in config. "
|
||||||
|
f"Maybe do `$cmdprefix+sp config set {key} []` first?")
|
||||||
|
elif not isinstance(arr, list):
|
||||||
|
return evt.reply("`{key}` does not seem to be an array.")
|
||||||
|
elif cmd == "add":
|
||||||
|
if value in arr:
|
||||||
|
return evt.reply(f"The array at `{key}` already contains `{value}`.")
|
||||||
|
arr.append(value)
|
||||||
|
return evt.reply(f"Successfully added `{value}` to the array at `{key}`")
|
||||||
|
else:
|
||||||
|
if value not in arr:
|
||||||
|
return evt.reply(f"The array at `{key}` does not contain `{value}`.")
|
||||||
|
arr.remove(value)
|
||||||
|
return 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. "
|
||||||
|
|||||||
+27
-12
@@ -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]
|
||||||
@@ -183,7 +192,13 @@ class Config(DictWithRecursion):
|
|||||||
|
|
||||||
copy("bridge.edits_as_replies")
|
copy("bridge.edits_as_replies")
|
||||||
copy("bridge.highlight_edits")
|
copy("bridge.highlight_edits")
|
||||||
copy("bridge.bridge_notices")
|
if isinstance(self["bridge.bridge_notices"], bool):
|
||||||
|
base["bridge.bridge_notices"] = {
|
||||||
|
"default": self["bridge.bridge_notices"],
|
||||||
|
"exceptions": ["@importantbot:example.com"],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
copy("bridge.bridge_notices")
|
||||||
copy("bridge.bot_messages_as_notices")
|
copy("bridge.bot_messages_as_notices")
|
||||||
copy("bridge.max_initial_member_sync")
|
copy("bridge.max_initial_member_sync")
|
||||||
copy("bridge.sync_channel_members")
|
copy("bridge.sync_channel_members")
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class Portal(Base):
|
|||||||
# Matrix portal information
|
# Matrix portal information
|
||||||
mxid = Column(String, unique=True, nullable=True) # type: Optional[MatrixRoomID]
|
mxid = Column(String, unique=True, nullable=True) # type: Optional[MatrixRoomID]
|
||||||
|
|
||||||
|
config = Column(Text, nullable=True)
|
||||||
|
|
||||||
# Telegram chat metadata
|
# Telegram chat metadata
|
||||||
username = Column(String, nullable=True)
|
username = Column(String, nullable=True)
|
||||||
title = Column(String, nullable=True)
|
title = Column(String, nullable=True)
|
||||||
|
|||||||
@@ -14,7 +14,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 Optional, List, Tuple, Callable, Pattern, Match, TYPE_CHECKING
|
from typing import Optional, List, Tuple, Callable, Pattern, Match, TYPE_CHECKING, Dict, Any
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, M
|
|||||||
TypeMessageEntity)
|
TypeMessageEntity)
|
||||||
|
|
||||||
from ... import puppet as pu
|
from ... import puppet as pu
|
||||||
|
from ...types import TelegramID, MatrixRoomID
|
||||||
from ...db import Message as DBMessage
|
from ...db import Message as DBMessage
|
||||||
from ..util import (add_surrogates, remove_surrogates, trim_reply_fallback_html,
|
from ..util import (add_surrogates, remove_surrogates, trim_reply_fallback_html,
|
||||||
trim_reply_fallback_text)
|
trim_reply_fallback_text)
|
||||||
@@ -90,8 +91,8 @@ def matrix_to_telegram(html: str) -> ParsedMessage:
|
|||||||
raise FormatError(f"Failed to convert Matrix format: {html}") from e
|
raise FormatError(f"Failed to convert Matrix format: {html}") from e
|
||||||
|
|
||||||
|
|
||||||
def matrix_reply_to_telegram(content: dict, tg_space: int, room_id: Optional[str] = None
|
def matrix_reply_to_telegram(content: Dict[str, Any], tg_space: TelegramID,
|
||||||
) -> Optional[int]:
|
room_id: Optional[MatrixRoomID] = None) -> Optional[TelegramID]:
|
||||||
try:
|
try:
|
||||||
reply = content["m.relates_to"]["m.in_reply_to"]
|
reply = content["m.relates_to"]["m.in_reply_to"]
|
||||||
room_id = room_id or reply["room_id"]
|
room_id = room_id or reply["room_id"]
|
||||||
|
|||||||
+74
-44
@@ -14,7 +14,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 Awaitable, Dict, List, Optional, Pattern, Tuple, Union, cast, TYPE_CHECKING
|
from typing import Awaitable, Dict, List, Optional, Pattern, Tuple, Union, cast, TYPE_CHECKING, Any
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from string import Template
|
from string import Template
|
||||||
@@ -25,6 +25,7 @@ import mimetypes
|
|||||||
import unicodedata
|
import unicodedata
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
@@ -79,8 +80,8 @@ config = None # type: Config
|
|||||||
|
|
||||||
TypeMessage = Union[Message, MessageService]
|
TypeMessage = Union[Message, MessageService]
|
||||||
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
|
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
|
||||||
DedupMXID = Tuple[str, int]
|
DedupMXID = Tuple[MatrixEventID, TelegramID]
|
||||||
InviteList = Union[str, List[str]]
|
InviteList = Union[MatrixUserID, List[MatrixUserID]]
|
||||||
|
|
||||||
|
|
||||||
class Portal:
|
class Portal:
|
||||||
@@ -90,10 +91,13 @@ class Portal:
|
|||||||
bot = None # type: Bot
|
bot = None # type: Bot
|
||||||
loop = None # type: asyncio.AbstractEventLoop
|
loop = None # type: asyncio.AbstractEventLoop
|
||||||
|
|
||||||
|
# Config cache
|
||||||
filter_mode = None # type: str
|
filter_mode = None # type: str
|
||||||
filter_list = None # type: List[str]
|
filter_list = None # type: List[str]
|
||||||
|
|
||||||
bridge_notices = False # type: bool
|
public_portals = False # type: bool
|
||||||
|
max_initial_member_sync = -1 # type: int
|
||||||
|
sync_channel_members = True # type: bool
|
||||||
|
|
||||||
dedup_pre_db_check = False # type: bool
|
dedup_pre_db_check = False # type: bool
|
||||||
dedup_cache_queue_length = 20 # type: int
|
dedup_cache_queue_length = 20 # type: int
|
||||||
@@ -102,6 +106,7 @@ class Portal:
|
|||||||
mx_alias_regex = None # type: Pattern
|
mx_alias_regex = None # type: Pattern
|
||||||
hs_domain = None # type: str
|
hs_domain = None # type: str
|
||||||
|
|
||||||
|
# Instance cache
|
||||||
by_mxid = {} # type: Dict[MatrixRoomID, Portal]
|
by_mxid = {} # type: Dict[MatrixRoomID, Portal]
|
||||||
by_tgid = {} # type: Dict[Tuple[TelegramID, TelegramID], Portal]
|
by_tgid = {} # type: Dict[Tuple[TelegramID, TelegramID], Portal]
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ class Portal:
|
|||||||
mxid: Optional[MatrixRoomID] = None, username: Optional[str] = None,
|
mxid: Optional[MatrixRoomID] = 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,
|
||||||
db_instance: DBPortal = None) -> None:
|
config: Optional[str] = None, db_instance: DBPortal = None) -> None:
|
||||||
self.mxid = mxid # type: Optional[MatrixRoomID]
|
self.mxid = mxid # type: Optional[MatrixRoomID]
|
||||||
self.tgid = tgid # type: TelegramID
|
self.tgid = tgid # type: TelegramID
|
||||||
self.tg_receiver = tg_receiver or tgid # type: TelegramID
|
self.tg_receiver = tg_receiver or tgid # type: TelegramID
|
||||||
@@ -119,6 +124,7 @@ class Portal:
|
|||||||
self.title = title # type: Optional[str]
|
self.title = title # type: Optional[str]
|
||||||
self.about = about # type: str
|
self.about = about # type: str
|
||||||
self.photo_id = photo_id # type: str
|
self.photo_id = photo_id # type: str
|
||||||
|
self.local_config = json.loads(config or "{}") # type: Dict[str, Any]
|
||||||
self._db_instance = db_instance # type: DBPortal
|
self._db_instance = db_instance # type: DBPortal
|
||||||
self.deleted = False # type: bool
|
self.deleted = False # type: bool
|
||||||
|
|
||||||
@@ -248,7 +254,7 @@ class Portal:
|
|||||||
try:
|
try:
|
||||||
found_mxid = self._dedup_mxid[evt_hash]
|
found_mxid = self._dedup_mxid[evt_hash]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "None", 0
|
return MatrixEventID("None"), TelegramID(0)
|
||||||
|
|
||||||
if found_mxid != expected_mxid:
|
if found_mxid != expected_mxid:
|
||||||
return found_mxid
|
return found_mxid
|
||||||
@@ -346,7 +352,7 @@ 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 = config["bridge.public_portals"]
|
public = Portal.public_portals
|
||||||
alias = self._get_alias_localpart(entity.username)
|
alias = self._get_alias_localpart(entity.username)
|
||||||
self.username = entity.username
|
self.username = entity.username
|
||||||
else:
|
else:
|
||||||
@@ -451,7 +457,7 @@ class Portal:
|
|||||||
# * The member sync count is limited, because then we might ignore some members.
|
# * The member sync count is limited, because then we might ignore some members.
|
||||||
# * It's a channel, because non-admins don't have access to the member list.
|
# * It's a channel, because non-admins don't have access to the member list.
|
||||||
trust_member_list = (len(allowed_tgids) < 9900
|
trust_member_list = (len(allowed_tgids) < 9900
|
||||||
and config["bridge.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[MatrixUserID],
|
||||||
@@ -533,7 +539,7 @@ class Portal:
|
|||||||
self.username = username or None
|
self.username = username or None
|
||||||
if self.username:
|
if self.username:
|
||||||
await self.main_intent.add_room_alias(self.mxid, self._get_alias_localpart())
|
await self.main_intent.add_room_alias(self.mxid, self._get_alias_localpart())
|
||||||
if config["bridge.public_portals"]:
|
if Portal.public_portals:
|
||||||
await self.main_intent.set_join_rule(self.mxid, "public")
|
await self.main_intent.set_join_rule(self.mxid, "public")
|
||||||
else:
|
else:
|
||||||
await self.main_intent.set_join_rule(self.mxid, "invite")
|
await self.main_intent.set_join_rule(self.mxid, "invite")
|
||||||
@@ -593,10 +599,10 @@ class Portal:
|
|||||||
chat = await user.client(GetFullChatRequest(chat_id=self.tgid))
|
chat = await user.client(GetFullChatRequest(chat_id=self.tgid))
|
||||||
return chat.users, chat.full_chat.participants.participants
|
return chat.users, chat.full_chat.participants.participants
|
||||||
elif self.peer_type == "channel":
|
elif self.peer_type == "channel":
|
||||||
if not self.megagroup and not config["bridge.sync_channel_members"]:
|
if not self.megagroup and not Portal.sync_channel_members:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
limit = config["bridge.max_initial_member_sync"]
|
limit = Portal.max_initial_member_sync
|
||||||
if limit == 0:
|
if limit == 0:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
@@ -712,9 +718,15 @@ class Portal:
|
|||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def get_config(self, key: str) -> Any:
|
||||||
|
local = util.recursive_get(self.local_config, key)
|
||||||
|
if local is not None:
|
||||||
|
return local
|
||||||
|
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 = config[f"bridge.state_event_formats.{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
|
||||||
@@ -731,7 +743,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: str) -> None:
|
event_id: MatrixEventID) -> 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,
|
||||||
@@ -805,7 +817,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: str) -> None:
|
async def join_matrix(self, user: 'u.User', event_id: MatrixEventID) -> 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)
|
||||||
@@ -824,14 +836,15 @@ class Portal:
|
|||||||
# We'll just assume the user is already in the chat.
|
# We'll just assume the user is already in the chat.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _apply_msg_format(self, sender: 'u.User', msgtype: str, message: Dict) -> None:
|
async def _apply_msg_format(self, sender: 'u.User', msgtype: str, message: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
if "formatted_body" not in message:
|
if "formatted_body" not in message:
|
||||||
message["format"] = "org.matrix.custom.html"
|
message["format"] = "org.matrix.custom.html"
|
||||||
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["bridge.message_formats"].get(msgtype,
|
tpl = (self.get_config(f"message_formats.[{msgtype}]")
|
||||||
"<b>$sender_displayname</b>: $message")
|
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,
|
||||||
@@ -840,7 +853,7 @@ class Portal:
|
|||||||
message["formatted_body"] = Template(tpl).safe_substitute(tpl_args)
|
message["formatted_body"] = Template(tpl).safe_substitute(tpl_args)
|
||||||
|
|
||||||
async def _pre_process_matrix_message(self, sender: 'u.User', use_relaybot: bool,
|
async def _pre_process_matrix_message(self, sender: 'u.User', use_relaybot: bool,
|
||||||
message: dict) -> None:
|
message: Dict[str, Any]) -> None:
|
||||||
msgtype = message.get("msgtype", "m.text")
|
msgtype = message.get("msgtype", "m.text")
|
||||||
if msgtype == "m.emote":
|
if msgtype == "m.emote":
|
||||||
await self._apply_msg_format(sender, msgtype, message)
|
await self._apply_msg_format(sender, msgtype, message)
|
||||||
@@ -849,7 +862,8 @@ class Portal:
|
|||||||
await self._apply_msg_format(sender, msgtype, message)
|
await self._apply_msg_format(sender, msgtype, message)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _matrix_event_to_entities(event: Dict) -> Tuple[str, Optional[List[TypeMessageEntity]]]:
|
def _matrix_event_to_entities(event: Dict[str, Any]) -> Tuple[
|
||||||
|
str, Optional[List[TypeMessageEntity]]]:
|
||||||
try:
|
try:
|
||||||
if event.get("format", None) == "org.matrix.custom.html":
|
if event.get("format", None) == "org.matrix.custom.html":
|
||||||
message, entities = formatter.matrix_to_telegram(event.get("formatted_body", ""))
|
message, entities = formatter.matrix_to_telegram(event.get("formatted_body", ""))
|
||||||
@@ -859,7 +873,7 @@ class Portal:
|
|||||||
message, entities = None, None
|
message, entities = None, None
|
||||||
return message, entities
|
return message, entities
|
||||||
|
|
||||||
def require_send_lock(self, user_id: int) -> asyncio.Lock:
|
def require_send_lock(self, user_id: TelegramID) -> asyncio.Lock:
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
raise ValueError("Required send lock for none id")
|
raise ValueError("Required send lock for none id")
|
||||||
try:
|
try:
|
||||||
@@ -868,7 +882,7 @@ class Portal:
|
|||||||
self._send_locks[user_id] = asyncio.Lock()
|
self._send_locks[user_id] = asyncio.Lock()
|
||||||
return self._send_locks[user_id]
|
return self._send_locks[user_id]
|
||||||
|
|
||||||
def optional_send_lock(self, user_id: int) -> Optional[asyncio.Lock]:
|
def optional_send_lock(self, user_id: TelegramID) -> Optional[asyncio.Lock]:
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
@@ -876,18 +890,19 @@ class Portal:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _handle_matrix_text(self, sender_id: int, event_id: str, space: int,
|
async def _handle_matrix_text(self, sender_id: TelegramID, event_id: MatrixEventID,
|
||||||
client: 'MautrixTelegramClient', message: Dict, reply_to: int
|
space: TelegramID, client: 'MautrixTelegramClient', message: Dict,
|
||||||
) -> None:
|
reply_to: TelegramID) -> None:
|
||||||
lock = self.require_send_lock(sender_id)
|
lock = self.require_send_lock(sender_id)
|
||||||
async with lock:
|
async with lock:
|
||||||
response = await client.send_message(self.peer, message, reply_to=reply_to,
|
response = await client.send_message(self.peer, message, reply_to=reply_to,
|
||||||
parse_mode=self._matrix_event_to_entities)
|
parse_mode=self._matrix_event_to_entities)
|
||||||
self._add_telegram_message_to_db(event_id, space, response)
|
self._add_telegram_message_to_db(event_id, space, response)
|
||||||
|
|
||||||
async def _handle_matrix_file(self, msgtype: str, sender_id: int, event_id: str, space: int,
|
async def _handle_matrix_file(self, msgtype: str, sender_id: TelegramID,
|
||||||
client: 'MautrixTelegramClient', message: dict, reply_to: int
|
event_id: MatrixEventID, space: TelegramID,
|
||||||
) -> None:
|
client: 'MautrixTelegramClient', message: dict,
|
||||||
|
reply_to: TelegramID) -> None:
|
||||||
file = await self.main_intent.download_file(message["url"])
|
file = await self.main_intent.download_file(message["url"])
|
||||||
|
|
||||||
info = message.get("info", {})
|
info = message.get("info", {})
|
||||||
@@ -919,9 +934,9 @@ class Portal:
|
|||||||
caption=caption)
|
caption=caption)
|
||||||
self._add_telegram_message_to_db(event_id, space, response)
|
self._add_telegram_message_to_db(event_id, space, response)
|
||||||
|
|
||||||
async def _handle_matrix_location(self, sender_id: int, event_id: str, space: int,
|
async def _handle_matrix_location(self, sender_id: TelegramID, event_id: MatrixEventID,
|
||||||
client: 'MautrixTelegramClient', message: Dict,
|
space: TelegramID, client: 'MautrixTelegramClient',
|
||||||
reply_to: int) -> None:
|
message: Dict[str, Any], reply_to: TelegramID) -> None:
|
||||||
try:
|
try:
|
||||||
lat, long = message["geo_uri"][len("geo:"):].split(",")
|
lat, long = message["geo_uri"][len("geo:"):].split(",")
|
||||||
lat, long = float(lat), float(long)
|
lat, long = float(lat), float(long)
|
||||||
@@ -937,7 +952,7 @@ class Portal:
|
|||||||
caption=caption, entities=entities)
|
caption=caption, entities=entities)
|
||||||
self._add_telegram_message_to_db(event_id, space, response)
|
self._add_telegram_message_to_db(event_id, space, response)
|
||||||
|
|
||||||
def _add_telegram_message_to_db(self, event_id: str, space: int,
|
def _add_telegram_message_to_db(self, event_id: MatrixEventID, space: TelegramID,
|
||||||
response: TypeMessage) -> None:
|
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))
|
self.is_duplicate(response, (event_id, space))
|
||||||
@@ -948,7 +963,8 @@ class Portal:
|
|||||||
mxid=event_id))
|
mxid=event_id))
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
async def handle_matrix_message(self, sender: 'u.User', message: dict, event_id: str) -> None:
|
async def handle_matrix_message(self, sender: 'u.User', message: Dict[str, Any],
|
||||||
|
event_id: MatrixEventID) -> None:
|
||||||
puppet = p.Puppet.get_by_custom_mxid(sender.mxid)
|
puppet = p.Puppet.get_by_custom_mxid(sender.mxid)
|
||||||
if puppet and message.get("net.maunium.telegram.puppet", False):
|
if puppet and message.get("net.maunium.telegram.puppet", False):
|
||||||
self.log.debug("Ignoring puppet-sent message by confirmed puppet user %s", sender.mxid)
|
self.log.debug("Ignoring puppet-sent message by confirmed puppet user %s", sender.mxid)
|
||||||
@@ -965,7 +981,13 @@ class Portal:
|
|||||||
await self._pre_process_matrix_message(sender, not logged_in, message)
|
await self._pre_process_matrix_message(sender, not logged_in, message)
|
||||||
msgtype = message["msgtype"]
|
msgtype = message["msgtype"]
|
||||||
|
|
||||||
if msgtype == "m.text" or (self.bridge_notices and msgtype == "m.notice"):
|
if msgtype == "m.notice":
|
||||||
|
bridge_notices = self.get_config("bridge_notices")
|
||||||
|
if not bridge_notices.get("default", False) and sender_id not in bridge_notices.get(
|
||||||
|
"exceptions"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if msgtype == "m.text":
|
||||||
await self._handle_matrix_text(sender_id, event_id, space, client, message, reply_to)
|
await self._handle_matrix_text(sender_id, event_id, space, client, message, reply_to)
|
||||||
elif msgtype == "m.location":
|
elif msgtype == "m.location":
|
||||||
await self._handle_matrix_location(sender_id, event_id, space, client, message,
|
await self._handle_matrix_location(sender_id, event_id, space, client, message,
|
||||||
@@ -976,7 +998,8 @@ class Portal:
|
|||||||
else:
|
else:
|
||||||
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', pinned_message: Optional[str]) -> None:
|
async def handle_matrix_pin(self, sender: 'u.User',
|
||||||
|
pinned_message: Optional[MatrixEventID]) -> None:
|
||||||
if self.peer_type != "channel":
|
if self.peer_type != "channel":
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -1093,7 +1116,7 @@ class Portal:
|
|||||||
# endregion
|
# endregion
|
||||||
# region Telegram chat info updating
|
# region Telegram chat info updating
|
||||||
|
|
||||||
async def _get_telegram_users_in_matrix_room(self) -> List[int]:
|
async def _get_telegram_users_in_matrix_room(self) -> List[TelegramID]:
|
||||||
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:
|
||||||
@@ -1202,7 +1225,8 @@ class Portal:
|
|||||||
largest_size.location)
|
largest_size.location)
|
||||||
if not file:
|
if not file:
|
||||||
return None
|
return None
|
||||||
if config["bridge.inline_images"] and (evt.message or evt.fwd_from or evt.reply_to_msg_id):
|
if self.get_config("inline_images") and (evt.message
|
||||||
|
or evt.fwd_from or evt.reply_to_msg_id):
|
||||||
text, html, relates_to = await formatter.telegram_to_matrix(
|
text, html, relates_to = await formatter.telegram_to_matrix(
|
||||||
evt, source, self.main_intent,
|
evt, source, self.main_intent,
|
||||||
prefix_html=f"<img src='{file.mxc}' alt='Inline Telegram photo'/><br/>",
|
prefix_html=f"<img src='{file.mxc}' alt='Inline Telegram photo'/><br/>",
|
||||||
@@ -1305,7 +1329,7 @@ class Portal:
|
|||||||
"external_url": self.get_external_url(evt)
|
"external_url": self.get_external_url(evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs["is_sticker"] and config["bridge.native_stickers"]:
|
if attrs["is_sticker"] and self.get_config("native_stickers"):
|
||||||
return await intent.send_sticker(**kwargs)
|
return await intent.send_sticker(**kwargs)
|
||||||
|
|
||||||
mime_type = info["mimetype"]
|
mime_type = info["mimetype"]
|
||||||
@@ -1352,7 +1376,7 @@ class Portal:
|
|||||||
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)
|
||||||
msgtype = "m.notice" if is_bot and config["bridge.bot_messages_as_notices"] else "m.text"
|
msgtype = "m.notice" if is_bot and self.get_config("bot_messages_as_notices") else "m.text"
|
||||||
return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to,
|
return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to,
|
||||||
msgtype=msgtype, timestamp=evt.date,
|
msgtype=msgtype, timestamp=evt.date,
|
||||||
external_url=self.get_external_url(evt))
|
external_url=self.get_external_url(evt))
|
||||||
@@ -1361,7 +1385,7 @@ class Portal:
|
|||||||
evt: Message) -> None:
|
evt: Message) -> None:
|
||||||
if not self.mxid:
|
if not self.mxid:
|
||||||
return
|
return
|
||||||
elif not config["bridge.edits_as_replies"]:
|
elif not self.get_config("edits_as_replies"):
|
||||||
self.log.debug("Edits as replies disabled, ignoring edit event...")
|
self.log.debug("Edits as replies disabled, ignoring edit event...")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1372,7 +1396,8 @@ 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 = f"${random.randint(1000000000000,9999999999999)}TGBRIDGEDITEMP"
|
temporary_identifier = MatrixEventID(
|
||||||
|
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:
|
||||||
mxid, other_tg_space = duplicate_found
|
mxid, other_tg_space = duplicate_found
|
||||||
@@ -1419,7 +1444,8 @@ 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 = f"${random.randint(1000000000000,9999999999999)}TGBRIDGETEMP"
|
temporary_identifier = MatrixEventID(
|
||||||
|
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:
|
||||||
mxid, other_tg_space = duplicate_found
|
mxid, other_tg_space = duplicate_found
|
||||||
@@ -1681,7 +1707,8 @@ class Portal:
|
|||||||
def new_db_instance(self) -> DBPortal:
|
def new_db_instance(self) -> DBPortal:
|
||||||
return DBPortal(tgid=self.tgid, tg_receiver=self.tg_receiver, peer_type=self.peer_type,
|
return DBPortal(tgid=self.tgid, tg_receiver=self.tg_receiver, peer_type=self.peer_type,
|
||||||
mxid=self.mxid, username=self.username, megagroup=self.megagroup,
|
mxid=self.mxid, username=self.username, megagroup=self.megagroup,
|
||||||
title=self.title, about=self.about, photo_id=self.photo_id)
|
title=self.title, about=self.about, photo_id=self.photo_id,
|
||||||
|
config=json.dumps(self.local_config))
|
||||||
|
|
||||||
def migrate_and_save(self, new_id: TelegramID) -> None:
|
def migrate_and_save(self, new_id: TelegramID) -> None:
|
||||||
existing = DBPortal.query.get(self.tgid_full)
|
existing = DBPortal.query.get(self.tgid_full)
|
||||||
@@ -1702,6 +1729,7 @@ class Portal:
|
|||||||
self.db_instance.title = self.title
|
self.db_instance.title = self.title
|
||||||
self.db_instance.about = self.about
|
self.db_instance.about = self.about
|
||||||
self.db_instance.photo_id = self.photo_id
|
self.db_instance.photo_id = self.photo_id
|
||||||
|
self.db_instance.config = json.dumps(self.local_config)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
@@ -1724,7 +1752,7 @@ class Portal:
|
|||||||
peer_type=db_portal.peer_type, mxid=db_portal.mxid,
|
peer_type=db_portal.peer_type, mxid=db_portal.mxid,
|
||||||
username=db_portal.username, megagroup=db_portal.megagroup,
|
username=db_portal.username, megagroup=db_portal.megagroup,
|
||||||
title=db_portal.title, about=db_portal.about, photo_id=db_portal.photo_id,
|
title=db_portal.title, about=db_portal.about, photo_id=db_portal.photo_id,
|
||||||
db_instance=db_portal)
|
config=db_portal.config, db_instance=db_portal)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region Class instance lookup
|
# region Class instance lookup
|
||||||
@@ -1822,7 +1850,9 @@ class Portal:
|
|||||||
def init(context: Context) -> None:
|
def init(context: Context) -> None:
|
||||||
global config
|
global config
|
||||||
Portal.az, Portal.db, config, Portal.loop, Portal.bot = context.core
|
Portal.az, Portal.db, config, Portal.loop, Portal.bot = context.core
|
||||||
Portal.bridge_notices = config["bridge.bridge_notices"]
|
Portal.max_initial_member_sync = config["bridge.max_initial_member_sync"]
|
||||||
|
Portal.sync_channel_members = config["bridge.sync_channel_members"]
|
||||||
|
Portal.public_portals = config["bridge.public_portals"]
|
||||||
Portal.filter_mode = config["bridge.filter.mode"]
|
Portal.filter_mode = config["bridge.filter.mode"]
|
||||||
Portal.filter_list = config["bridge.filter.list"]
|
Portal.filter_list = config["bridge.filter.list"]
|
||||||
Portal.dedup_pre_db_check = config["bridge.deduplication.pre_db_check"]
|
Portal.dedup_pre_db_check = config["bridge.deduplication.pre_db_check"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- 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, {})
|
||||||
|
return recursive_del(next_data, next_key)
|
||||||
|
if key in data:
|
||||||
|
del data[key]
|
||||||
|
return True
|
||||||
|
return False
|
||||||
+1
-1
@@ -4,7 +4,7 @@ ruamel.yaml
|
|||||||
python-magic
|
python-magic
|
||||||
SQLAlchemy
|
SQLAlchemy
|
||||||
alembic
|
alembic
|
||||||
Markdown
|
commonmark
|
||||||
future-fstrings
|
future-fstrings
|
||||||
telethon
|
telethon
|
||||||
telethon-session-sqlalchemy
|
telethon-session-sqlalchemy
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ setuptools.setup(
|
|||||||
"mautrix-appservice>=0.3.6,<0.4.0",
|
"mautrix-appservice>=0.3.6,<0.4.0",
|
||||||
"SQLAlchemy>=1.2.3,<2",
|
"SQLAlchemy>=1.2.3,<2",
|
||||||
"alembic>=1.0.0,<2",
|
"alembic>=1.0.0,<2",
|
||||||
"Markdown>=2.6.11,<3",
|
"commonmark>=0.8.1,<1",
|
||||||
"ruamel.yaml>=0.15.35,<0.16",
|
"ruamel.yaml>=0.15.35,<0.16",
|
||||||
"future-fstrings>=0.4.2",
|
"future-fstrings>=0.4.2",
|
||||||
"python-magic>=0.4.15,<0.5",
|
"python-magic>=0.4.15,<0.5",
|
||||||
|
|||||||
Reference in New Issue
Block a user