Even even more migrations to mautrix-python
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.7.0+dev"
|
||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
||||
|
||||
@@ -15,16 +15,18 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from itertools import chain
|
||||
|
||||
from mautrix.bridge import Bridge
|
||||
from alchemysession import AlchemySessionContainer
|
||||
|
||||
from mautrix.bridge import Bridge
|
||||
from mautrix.bridge.db import Base
|
||||
|
||||
from .web.provisioning import ProvisioningAPI
|
||||
from .web.public import PublicBridgeWebsite
|
||||
from .abstract_user import init as init_abstract_user
|
||||
from .bot import Bot, init as init_bot
|
||||
from .config import Config
|
||||
from .context import Context
|
||||
from .db import Base, init as init_db
|
||||
from .db import init as init_db
|
||||
from .formatter import init as init_formatter
|
||||
from .matrix import MatrixHandler
|
||||
from .portal import init as init_portal
|
||||
|
||||
@@ -381,8 +381,7 @@ class AbstractUser(ABC):
|
||||
return
|
||||
|
||||
for message_id in update.messages:
|
||||
messages = DBMessage.get_all_by_tgid(TelegramID(message_id), self.tgid)
|
||||
for message in messages:
|
||||
for message in DBMessage.get_all_by_tgid(TelegramID(message_id), self.tgid):
|
||||
message.delete()
|
||||
number_left = DBMessage.count_spaces_by_mxid(message.mxid, message.mx_room)
|
||||
if number_left == 0:
|
||||
@@ -395,8 +394,7 @@ class AbstractUser(ABC):
|
||||
channel_id = TelegramID(update.channel_id)
|
||||
|
||||
for message_id in update.messages:
|
||||
messages = DBMessage.get_all_by_tgid(TelegramID(message_id), channel_id)
|
||||
for message in messages:
|
||||
for message in DBMessage.get_all_by_tgid(TelegramID(message_id), channel_id):
|
||||
message.delete()
|
||||
await self._try_redact(message)
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ class Bot(AbstractUser):
|
||||
del self.chats[chat_id]
|
||||
except KeyError:
|
||||
pass
|
||||
BotChat.delete(chat_id)
|
||||
BotChat.delete_by_id(chat_id)
|
||||
|
||||
async def _can_use_commands(self, chat: TypePeer, tgid: TelegramID) -> bool:
|
||||
if tgid in self.tg_whitelist:
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from .handler import (command_handler, command_handlers as _command_handlers,
|
||||
CommandHandler, CommandProcessor, CommandEvent,
|
||||
SECTION_GENERAL, SECTION_AUTH, SECTION_CREATING_PORTALS,
|
||||
SECTION_PORTAL_MANAGEMENT, SECTION_MISC, SECTION_ADMIN)
|
||||
from . import portal, telegram, clean_rooms, matrix_auth, meta
|
||||
from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent,
|
||||
SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT,
|
||||
SECTION_MISC, SECTION_ADMIN)
|
||||
from . import portal, telegram, clean_rooms, matrix_auth
|
||||
|
||||
__all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent",
|
||||
"SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS",
|
||||
"SECTION_PORTAL_MANAGEMENT"]
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
#
|
||||
# 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, List, NamedTuple, Optional, Tuple, Union
|
||||
from typing import List, NamedTuple, Tuple, Union
|
||||
|
||||
from mautrix.appservice import IntentAPI
|
||||
from mautrix.errors import MatrixRequestError
|
||||
from mautrix.types import RoomID, UserID
|
||||
from mautrix.types import RoomID, UserID, EventID
|
||||
|
||||
from . import command_handler, CommandEvent, SECTION_ADMIN
|
||||
from .. import puppet as pu, portal as po
|
||||
@@ -61,7 +61,7 @@ async def _find_rooms(intent: IntentAPI) -> Tuple[List[ManagementRoom], List[Roo
|
||||
@command_handler(needs_admin=True, needs_auth=False, management_only=True, name="clean-rooms",
|
||||
help_section=SECTION_ADMIN,
|
||||
help_text="Clean up unused portal/management rooms.")
|
||||
async def clean_rooms(evt: CommandEvent) -> Optional[Dict]:
|
||||
async def clean_rooms(evt: CommandEvent) -> EventID:
|
||||
management_rooms, unidentified_rooms, portals, empty_portals = await _find_rooms(evt.az.intent)
|
||||
|
||||
reply = ["#### Management rooms (M)"]
|
||||
@@ -107,10 +107,10 @@ async def clean_rooms(evt: CommandEvent) -> Optional[Dict]:
|
||||
|
||||
|
||||
async def set_rooms_to_clean(evt, management_rooms: List[ManagementRoom],
|
||||
unidentified_rooms: List[MatrixRoomID], portals: List["po.Portal"],
|
||||
unidentified_rooms: List[RoomID], portals: List["po.Portal"],
|
||||
empty_portals: List["po.Portal"]) -> None:
|
||||
command = evt.args[0]
|
||||
rooms_to_clean = [] # type: List[Union[po.Portal, MatrixRoomID]]
|
||||
rooms_to_clean: List[Union[po.Portal, RoomID]] = []
|
||||
if command == "clean-recommended":
|
||||
rooms_to_clean += empty_portals
|
||||
rooms_to_clean += unidentified_rooms
|
||||
@@ -159,7 +159,7 @@ async def set_rooms_to_clean(evt, management_rooms: List[ManagementRoom],
|
||||
"`$cmdprefix+sp confirm-clean`.")
|
||||
|
||||
|
||||
async def execute_room_cleanup(evt, rooms_to_clean: List[Union[po.Portal, MatrixRoomID]]) -> None:
|
||||
async def execute_room_cleanup(evt, rooms_to_clean: List[Union[po.Portal, RoomID]]) -> None:
|
||||
if len(evt.args) > 0 and evt.args[0] == "confirm-clean":
|
||||
await evt.reply(f"Cleaning {len(rooms_to_clean)} rooms. "
|
||||
"This might take a while.")
|
||||
@@ -168,7 +168,7 @@ async def execute_room_cleanup(evt, rooms_to_clean: List[Union[po.Portal, Matrix
|
||||
if isinstance(room, po.Portal):
|
||||
await room.cleanup_and_delete()
|
||||
cleaned += 1
|
||||
elif isinstance(room, str): # str is aliased by MatrixRoomID
|
||||
else:
|
||||
await po.Portal.cleanup_room(evt.az.intent, room, message="Room deleted")
|
||||
cleaned += 1
|
||||
evt.sender.command_status = None
|
||||
|
||||
@@ -14,24 +14,23 @@
|
||||
# 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/>.
|
||||
"""This module contains classes handling commands issued by Matrix users."""
|
||||
from typing import Awaitable, Callable, Dict, List, NamedTuple, Optional
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import commonmark
|
||||
from typing import Awaitable, Callable, List, Optional, NamedTuple, Any
|
||||
|
||||
from telethon.errors import FloodWaitError
|
||||
|
||||
from mautrix.types import RoomID, EventID
|
||||
from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent,
|
||||
CommandHandler as BaseCommandHandler,
|
||||
CommandProcessor as BaseCommandProcessor,
|
||||
CommandHandlerFunc, command_handler as base_command_handler)
|
||||
|
||||
from ..util import format_duration
|
||||
from .. import user as u, context as c
|
||||
|
||||
command_handlers: Dict[str, 'CommandHandler'] = {}
|
||||
HelpCacheKey = NamedTuple('HelpCacheKey',
|
||||
is_management=bool, is_portal=bool, puppet_whitelisted=bool,
|
||||
matrix_puppet_whitelisted=bool, is_admin=bool, is_logged_in=bool)
|
||||
|
||||
HelpSection = NamedTuple('HelpSection', [('name', str), ('order', int), ('description', str)])
|
||||
|
||||
SECTION_GENERAL = HelpSection("General", 0, "")
|
||||
SECTION_AUTH = HelpSection("Authentication", 10, "")
|
||||
SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "")
|
||||
SECTION_PORTAL_MANAGEMENT = HelpSection("Portal management", 30, "")
|
||||
@@ -39,186 +38,42 @@ SECTION_MISC = HelpSection("Miscellaneous", 40, "")
|
||||
SECTION_ADMIN = HelpSection("Administration", 50, "")
|
||||
|
||||
|
||||
class HtmlEscapingRenderer(commonmark.HtmlRenderer):
|
||||
def __init__(self, allow_html: bool = False):
|
||||
super().__init__()
|
||||
self.allow_html = allow_html
|
||||
class CommandEvent(BaseCommandEvent):
|
||||
sender: u.User
|
||||
|
||||
def lit(self, s):
|
||||
if self.allow_html:
|
||||
return super().lit(s)
|
||||
return super().lit(s.replace("<", "<").replace(">", ">"))
|
||||
|
||||
def image(self, node, entering):
|
||||
prev = self.allow_html
|
||||
self.allow_html = True
|
||||
super().image(node, entering)
|
||||
self.allow_html = prev
|
||||
|
||||
|
||||
md_parser = commonmark.Parser()
|
||||
md_renderer = HtmlEscapingRenderer()
|
||||
|
||||
|
||||
def ensure_trailing_newline(s: str) -> str:
|
||||
"""Returns the passed string, but with a guaranteed trailing newline."""
|
||||
return s + ("" if s[-1] == "\n" else "\n")
|
||||
|
||||
|
||||
class CommandEvent:
|
||||
"""Holds information about a command issued in a Matrix room.
|
||||
|
||||
When a Matrix command was issued to the bot, CommandEvent will hold
|
||||
information regarding the event.
|
||||
|
||||
Attributes:
|
||||
room_id: The id of the Matrix room in which the command was issued.
|
||||
event_id: The id of the matrix event which contained the command.
|
||||
sender: The user who issued the command.
|
||||
command: The issued command.
|
||||
args: Arguments given with the issued command.
|
||||
is_management: Determines whether the room in which the command wa
|
||||
issued is a management room.
|
||||
is_portal: Determines whether the room in which the command was issued
|
||||
is a portal.
|
||||
"""
|
||||
|
||||
def __init__(self, processor: 'CommandProcessor', room: RoomID, event: EventID,
|
||||
def __init__(self, processor: 'CommandProcessor', room_id: RoomID, event_id: EventID,
|
||||
sender: u.User, command: str, args: List[str], is_management: bool,
|
||||
is_portal: bool) -> None:
|
||||
self.az = processor.az
|
||||
self.log = processor.log
|
||||
self.loop = processor.loop
|
||||
super().__init__(processor, room_id, event_id, sender, command, args, is_management,
|
||||
is_portal)
|
||||
self.tgbot = processor.tgbot
|
||||
self.config = processor.config
|
||||
self.public_website = processor.public_website
|
||||
self.command_prefix = processor.command_prefix
|
||||
self.room_id = room
|
||||
self.event_id = event
|
||||
self.sender = sender
|
||||
self.command = command
|
||||
self.args = args
|
||||
self.is_management = is_management
|
||||
self.is_portal = is_portal
|
||||
|
||||
def reply(self, message: str, allow_html: bool = False, render_markdown: bool = True
|
||||
) -> Awaitable[EventID]:
|
||||
"""Write a reply to the room in which the command was issued.
|
||||
|
||||
Replaces occurences of "$cmdprefix" in the message with the command
|
||||
prefix and replaces occurences of "$cmdprefix+sp " with the command
|
||||
prefix if the command was not issued in a management room.
|
||||
If allow_html and render_markdown are both False, the message will not
|
||||
be rendered to html and sending of html is disabled.
|
||||
|
||||
Args:
|
||||
message: The message to post in the room.
|
||||
allow_html: Escape html in the message or don't render html at all
|
||||
if markdown is disabled.
|
||||
render_markdown: Use markdown formatting to render the passed
|
||||
message to html.
|
||||
|
||||
Returns:
|
||||
Handler for the message sending function.
|
||||
"""
|
||||
message_cmd = self._replace_command_prefix(message)
|
||||
html = self._render_message(message_cmd, allow_html=allow_html,
|
||||
render_markdown=render_markdown)
|
||||
|
||||
return self.az.intent.send_notice(self.room_id, message_cmd, html=html)
|
||||
|
||||
def mark_read(self) -> Awaitable[Dict]:
|
||||
"""Marks the command as read by the bot."""
|
||||
return self.az.intent.mark_read(self.room_id, self.event_id)
|
||||
|
||||
def _replace_command_prefix(self, message: str) -> str:
|
||||
"""Returns the string with the proper command prefix entered."""
|
||||
message = message.replace(
|
||||
"$cmdprefix+sp ", "" if self.is_management else f"{self.command_prefix} "
|
||||
)
|
||||
return message.replace("$cmdprefix", self.command_prefix)
|
||||
|
||||
@staticmethod
|
||||
def _render_message(message: str, allow_html: bool, render_markdown: bool) -> Optional[str]:
|
||||
"""Renders the message as HTML.
|
||||
|
||||
Args:
|
||||
allow_html: Flag to allow custom HTML in the message.
|
||||
render_markdown: If true, markdown styling is applied to the message.
|
||||
|
||||
Returns:
|
||||
The message rendered as HTML.
|
||||
None is returned if no styled output is required.
|
||||
"""
|
||||
html = ""
|
||||
if render_markdown:
|
||||
md_renderer.allow_html = allow_html
|
||||
html = md_renderer.render(md_parser.parse(message))
|
||||
elif allow_html:
|
||||
html = message
|
||||
return ensure_trailing_newline(html) if html else None
|
||||
async def get_help_key(self) -> HelpCacheKey:
|
||||
return HelpCacheKey(self.is_management, self.is_portal, self.sender.puppet_whitelisted,
|
||||
self.sender.matrix_puppet_whitelisted, self.sender.is_admin,
|
||||
await self.sender.is_logged_in())
|
||||
|
||||
|
||||
class CommandHandler:
|
||||
"""A command which can be executed from a Matrix room.
|
||||
class CommandHandler(BaseCommandHandler):
|
||||
name: str
|
||||
|
||||
The command manages its permission and help texts.
|
||||
When called, it will check the permission of the command event and execute
|
||||
the command or, in case of error, report back to the user.
|
||||
|
||||
Attributes:
|
||||
needs_auth: Flag indicating if the sender is required to be logged in.
|
||||
needs_puppeting: Flag indicating if the sender is required to use
|
||||
Telegram puppeteering for this command.
|
||||
needs_matrix_puppeting: Flag indicating if the sender is required to use
|
||||
Matrix pupeteering.
|
||||
needs_admin: Flag for whether only admin users can issue this command.
|
||||
management_only: Whether the command can exclusively be issued in a
|
||||
management room.
|
||||
name: The name of this command.
|
||||
help_section: Section of the help in which this command will appear.
|
||||
"""
|
||||
management_only: bool
|
||||
needs_auth: bool
|
||||
needs_puppeting: bool
|
||||
needs_matrix_puppeting: bool
|
||||
needs_admin: bool
|
||||
|
||||
def __init__(self, handler: Callable[[CommandEvent], Awaitable[EventID]], needs_auth: bool,
|
||||
needs_puppeting: bool, needs_matrix_puppeting: bool, needs_admin: bool,
|
||||
management_only: bool, name: str, help_text: str, help_args: str,
|
||||
help_section: HelpSection) -> None:
|
||||
"""
|
||||
Args:
|
||||
handler: The function handling the execution of this command.
|
||||
needs_auth: Flag indicating if the sender is required to be logged in.
|
||||
needs_puppeting: Flag indicating if the sender is required to use
|
||||
Telegram puppeteering for this command.
|
||||
needs_matrix_puppeting: Flag indicating if the sender is required to
|
||||
use Matrix pupeteering.
|
||||
needs_admin: Flag for whether only admin users can issue this command.
|
||||
management_only: Whether the command can exclusively be issued
|
||||
in a management room.
|
||||
name: The name of this command.
|
||||
help_text: The text displayed in the help for this command.
|
||||
help_args: Help text for the arguments of this command.
|
||||
help_section: Section of the help in which this command will appear.
|
||||
"""
|
||||
self._handler = handler
|
||||
self.needs_auth = needs_auth
|
||||
self.needs_puppeting = needs_puppeting
|
||||
self.needs_matrix_puppeting = needs_matrix_puppeting
|
||||
self.needs_admin = needs_admin
|
||||
self.management_only = management_only
|
||||
self.name = name
|
||||
self._help_text = help_text
|
||||
self._help_args = help_args
|
||||
self.help_section = help_section
|
||||
super().__init__(handler, management_only, name, help_text, help_args, help_section,
|
||||
needs_auth=needs_auth, needs_puppeting=needs_puppeting,
|
||||
needs_matrix_puppeting=needs_matrix_puppeting, needs_admin=needs_admin)
|
||||
|
||||
async def get_permission_error(self, evt: CommandEvent) -> Optional[str]:
|
||||
"""Returns the reason why the command could not be issued.
|
||||
|
||||
Args:
|
||||
evt: The event for which to get the error information.
|
||||
|
||||
Returns:
|
||||
A string describing the error or None if there was no error.
|
||||
"""
|
||||
if self.management_only and not evt.is_management:
|
||||
return (f"`{evt.command}` is a restricted command: "
|
||||
"you may only run it in management rooms.")
|
||||
@@ -232,134 +87,40 @@ class CommandHandler:
|
||||
return "This command requires you to be logged in."
|
||||
return None
|
||||
|
||||
def has_permission(self, is_management: bool, puppet_whitelisted: bool,
|
||||
matrix_puppet_whitelisted: bool, is_admin: bool, is_logged_in: bool) -> bool:
|
||||
"""Checks the permission for this command with the given status.
|
||||
|
||||
Args:
|
||||
is_management: If the room in which the command will be issued is a
|
||||
management room.
|
||||
puppet_whitelisted: If the connected Telegram account puppet is
|
||||
allowed to issue the command.
|
||||
matrix_puppet_whitelisted: If the connected Matrix account puppet is
|
||||
allowed to issue the command.
|
||||
is_admin: If the issuing user is an admin.
|
||||
is_logged_in: If the issuing user is logged in.
|
||||
|
||||
Returns:
|
||||
True if a user with the given state is allowed to issue the
|
||||
command.
|
||||
"""
|
||||
return ((not self.management_only or is_management) and
|
||||
(not self.needs_puppeting or puppet_whitelisted) and
|
||||
(not self.needs_matrix_puppeting or matrix_puppet_whitelisted) and
|
||||
(not self.needs_admin or is_admin) and
|
||||
(not self.needs_auth or is_logged_in))
|
||||
|
||||
async def __call__(self, evt: CommandEvent) -> EventID:
|
||||
"""Executes the command if evt was issued with proper rights.
|
||||
|
||||
Args:
|
||||
evt: The CommandEvent for which to check permissions.
|
||||
|
||||
Returns:
|
||||
The result of the command or the error message function.
|
||||
|
||||
Raises:
|
||||
FloodWaitError
|
||||
"""
|
||||
error = await self.get_permission_error(evt)
|
||||
if error is not None:
|
||||
return await evt.reply(error)
|
||||
return await self._handler(evt)
|
||||
|
||||
@property
|
||||
def has_help(self) -> bool:
|
||||
"""Returns true if this command has a help text."""
|
||||
return bool(self.help_section) and bool(self._help_text)
|
||||
|
||||
@property
|
||||
def help(self) -> str:
|
||||
"""Returns the help text to this command."""
|
||||
return f"**{self.name}** {self._help_args} - {self._help_text}"
|
||||
def has_permission(self, key: HelpCacheKey) -> bool:
|
||||
return ((not self.management_only or key.is_management) and
|
||||
(not self.needs_puppeting or key.puppet_whitelisted) and
|
||||
(not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted) and
|
||||
(not self.needs_admin or key.is_admin) and
|
||||
(not self.needs_auth or key.is_logged_in))
|
||||
|
||||
|
||||
def command_handler(_func: Optional[Callable[[CommandEvent], Awaitable[EventID]]] = None, *,
|
||||
needs_auth: bool = True, needs_puppeting: bool = True,
|
||||
needs_matrix_puppeting: bool = False, needs_admin: bool = False,
|
||||
management_only: bool = False, name: Optional[str] = None,
|
||||
help_text: str = "", help_args: str = "", help_section: HelpSection = None
|
||||
) -> Callable[[Callable[[CommandEvent], Awaitable[Optional[EventID]]]],
|
||||
CommandHandler]:
|
||||
def decorator(func: Callable[[CommandEvent], Awaitable[Optional[EventID]]]) -> CommandHandler:
|
||||
actual_name = name or func.__name__.replace("_", "-")
|
||||
handler = CommandHandler(func, needs_auth, needs_puppeting, needs_matrix_puppeting,
|
||||
needs_admin, management_only, actual_name, help_text, help_args,
|
||||
help_section)
|
||||
command_handlers[handler.name] = handler
|
||||
return handler
|
||||
|
||||
return decorator if _func is None else decorator(_func)
|
||||
def command_handler(_func: Optional[CommandHandlerFunc] = None, *, needs_auth: bool = True,
|
||||
needs_puppeting: bool = True, needs_matrix_puppeting: bool = False,
|
||||
needs_admin: bool = False, management_only: bool = False,
|
||||
name: Optional[str] = None, help_text: str = "", help_args: str = "",
|
||||
help_section: HelpSection = None) -> Callable[[CommandHandlerFunc],
|
||||
CommandHandler]:
|
||||
return base_command_handler(
|
||||
_func, _handler_class=CommandHandler, name=name, help_text=help_text, help_args=help_args,
|
||||
help_section=help_section, management_only=management_only, needs_auth=needs_auth,
|
||||
needs_admin=needs_admin, needs_puppeting=needs_puppeting,
|
||||
needs_matrix_puppeting=needs_matrix_puppeting)
|
||||
|
||||
|
||||
class CommandProcessor:
|
||||
"""Handles the raw commands issued by a user to the Matrix bot."""
|
||||
log = logging.getLogger("mau.commands")
|
||||
|
||||
class CommandProcessor(BaseCommandProcessor):
|
||||
def __init__(self, context: c.Context) -> None:
|
||||
super().__init__(az=context.az, config=context.config, event_class=CommandEvent,
|
||||
loop=context.loop)
|
||||
self.tgbot = context.bot
|
||||
self.az, self.config, self.loop, self.tgbot = context.core
|
||||
self.public_website = context.public_website
|
||||
self.command_prefix = self.config["bridge.command_prefix"]
|
||||
|
||||
async def handle(self, room: RoomID, event_id: EventID, sender: u.User,
|
||||
command: str, args: List[str], is_management: bool, is_portal: bool
|
||||
) -> Optional[EventID]:
|
||||
"""Handles the raw commands issued by a user to the Matrix bot.
|
||||
|
||||
If the command is not known, it might be a followup command and is
|
||||
delegated to a command handler registered for that purpose in the
|
||||
senders command_status as "next".
|
||||
|
||||
Args:
|
||||
room: ID of the Matrix room in which the command was issued.
|
||||
event_id: ID of the event by which the command was issued.
|
||||
sender: The sender who issued the command.
|
||||
command: The issued command, case insensitive.
|
||||
args: Arguments given with the command.
|
||||
is_management: Whether the room is a management room.
|
||||
is_portal: Whether the room is a portal.
|
||||
|
||||
Returns:
|
||||
The result of the error message function or None if no error
|
||||
occured. Unknown and delegated commands do not count as errors.
|
||||
"""
|
||||
if not command_handlers or "unknown-command" not in command_handlers:
|
||||
raise ValueError("command_handlers are not properly initialized.")
|
||||
|
||||
evt = CommandEvent(self, room, event_id, sender, command, args, is_management, is_portal)
|
||||
orig_command = command
|
||||
command = command.lower()
|
||||
@staticmethod
|
||||
async def _run_handler(handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent
|
||||
) -> Any:
|
||||
try:
|
||||
handler = command_handlers[command]
|
||||
except KeyError:
|
||||
if sender.command_status and "next" in sender.command_status:
|
||||
args.insert(0, orig_command)
|
||||
evt.command = ""
|
||||
handler = sender.command_status["next"]
|
||||
else:
|
||||
handler = command_handlers["unknown-command"]
|
||||
try:
|
||||
await handler(evt)
|
||||
return await handler(evt)
|
||||
except FloodWaitError as e:
|
||||
return await evt.reply(f"Flood error: Please wait {format_duration(e.seconds)}")
|
||||
except Exception:
|
||||
self.log.exception("Unhandled error while handling command "
|
||||
f"{evt.command} {' '.join(args)} from {sender.mxid}")
|
||||
if evt.sender.is_admin and evt.is_management:
|
||||
return await evt.reply("Unhandled error while handling command:\n\n"
|
||||
"```traceback\n"
|
||||
f"{traceback.format_exc()}"
|
||||
"```")
|
||||
return await evt.reply("Unhandled error while handling command. "
|
||||
"Check logs for more details.")
|
||||
return None
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
#
|
||||
# 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, Optional
|
||||
from mautrix.types import EventID
|
||||
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
|
||||
|
||||
from . import command_handler, CommandEvent, SECTION_AUTH
|
||||
from .. import puppet as pu
|
||||
|
||||
|
||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Revert your Telegram account's Matrix puppet to use the default Matrix "
|
||||
"account.")
|
||||
async def logout_matrix(evt: CommandEvent) -> Optional[Dict]:
|
||||
help_section=SECTION_AUTH, help_text="Revert your Telegram account's Matrix "
|
||||
"puppet to use the default Matrix account.")
|
||||
async def logout_matrix(evt: CommandEvent) -> EventID:
|
||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
||||
if not puppet.is_real_user:
|
||||
return await evt.reply("You are not logged in with your Matrix account.")
|
||||
@@ -35,7 +35,7 @@ async def logout_matrix(evt: CommandEvent) -> Optional[Dict]:
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Replace your Telegram account's Matrix puppet with your own Matrix "
|
||||
"account.")
|
||||
async def login_matrix(evt: CommandEvent) -> Optional[Dict]:
|
||||
async def login_matrix(evt: CommandEvent) -> EventID:
|
||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
||||
if puppet.is_real_user:
|
||||
return await evt.reply("You have already logged in with your Matrix account. "
|
||||
@@ -70,31 +70,29 @@ async def login_matrix(evt: CommandEvent) -> Optional[Dict]:
|
||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True,
|
||||
help_section=SECTION_AUTH,
|
||||
help_text="Pings the server with the stored matrix authentication.")
|
||||
async def ping_matrix(evt: CommandEvent) -> Optional[Dict]:
|
||||
async def ping_matrix(evt: CommandEvent) -> EventID:
|
||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
||||
if not puppet.is_real_user:
|
||||
return await evt.reply("You are not logged in with your Matrix account.")
|
||||
resp = await puppet.init_custom_mxid()
|
||||
if resp == pu.PuppetError.InvalidAccessToken:
|
||||
try:
|
||||
await puppet.init_custom_mxid()
|
||||
except InvalidAccessToken:
|
||||
return await evt.reply("Your access token is invalid.")
|
||||
elif resp == pu.PuppetError.Success:
|
||||
return await evt.reply("Your Matrix login is working.")
|
||||
return await evt.reply(f"Unknown response while checking your Matrix login: {resp}.")
|
||||
return await evt.reply("Your Matrix login is working.")
|
||||
|
||||
|
||||
async def enter_matrix_token(evt: CommandEvent) -> Dict:
|
||||
async def enter_matrix_token(evt: CommandEvent) -> EventID:
|
||||
evt.sender.command_status = None
|
||||
|
||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
||||
if puppet.is_real_user:
|
||||
return await evt.reply("You have already logged in with your Matrix account. "
|
||||
"Log out with `$cmdprefix+sp logout-matrix` first.")
|
||||
|
||||
resp = await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
|
||||
if resp == pu.PuppetError.OnlyLoginSelf:
|
||||
try:
|
||||
await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
|
||||
except OnlyLoginSelf:
|
||||
return await evt.reply("You can only log in as your own Matrix user.")
|
||||
elif resp == pu.PuppetError.InvalidAccessToken:
|
||||
except InvalidAccessToken:
|
||||
return await evt.reply("Failed to verify access token.")
|
||||
assert resp == pu.PuppetError.Success, "Encountered an unhandled PuppetError."
|
||||
return await evt.reply(
|
||||
f"Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}.")
|
||||
return await evt.reply("Replaced your Telegram account's Matrix puppet "
|
||||
f"with {puppet.custom_mxid}.")
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 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, List, Optional, Tuple
|
||||
|
||||
from . import command_handler, CommandEvent, _command_handlers, SECTION_GENERAL
|
||||
from .handler import HelpSection
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, needs_puppeting=False,
|
||||
help_section=SECTION_GENERAL,
|
||||
help_text="Cancel an ongoing action (such as login)")
|
||||
async def cancel(evt: CommandEvent) -> Optional[Dict]:
|
||||
if evt.sender.command_status:
|
||||
action = evt.sender.command_status["action"]
|
||||
evt.sender.command_status = None
|
||||
return await evt.reply(f"{action} cancelled.")
|
||||
else:
|
||||
return await evt.reply("No ongoing command.")
|
||||
|
||||
|
||||
@command_handler(needs_auth=False, needs_puppeting=False)
|
||||
async def unknown_command(evt: CommandEvent) -> Optional[Dict]:
|
||||
return await evt.reply("Unknown command. Try `$cmdprefix+sp help` for help.")
|
||||
|
||||
|
||||
help_cache = {} # type: Dict[Tuple[bool, bool, bool, bool, bool], str]
|
||||
|
||||
|
||||
async def _get_help_text(evt: CommandEvent) -> str:
|
||||
cache_key = (evt.is_management, evt.sender.puppet_whitelisted,
|
||||
evt.sender.matrix_puppet_whitelisted, evt.sender.is_admin,
|
||||
await evt.sender.is_logged_in())
|
||||
if cache_key not in help_cache:
|
||||
help_sections = {} # type: Dict[HelpSection, List[str]]
|
||||
for handler in _command_handlers.values():
|
||||
if handler.has_help and handler.has_permission(*cache_key):
|
||||
help_sections.setdefault(handler.help_section, [])
|
||||
help_sections[handler.help_section].append(handler.help + " ")
|
||||
help_sorted = sorted(help_sections.items(), key=lambda item: item[0].order)
|
||||
helps = ["#### {}\n{}\n".format(key.name, "\n".join(value)) for key, value in help_sorted]
|
||||
help_cache[cache_key] = "\n".join(helps)
|
||||
return help_cache[cache_key]
|
||||
|
||||
|
||||
def _get_management_status(evt: CommandEvent) -> str:
|
||||
if evt.is_management:
|
||||
return "This is a management room: prefixing commands with `$cmdprefix` is not required."
|
||||
elif evt.is_portal:
|
||||
return ("**This is a portal room**: you must always prefix commands with `$cmdprefix`.\n"
|
||||
"Management commands will not be sent to Telegram.")
|
||||
return "**This is not a management room**: you must prefix commands with `$cmdprefix`."
|
||||
|
||||
|
||||
@command_handler(name="help", needs_auth=False, needs_puppeting=False,
|
||||
help_section=SECTION_GENERAL,
|
||||
help_text="Show this help message.")
|
||||
async def help_cmd(evt: CommandEvent) -> Optional[Dict]:
|
||||
return await evt.reply(_get_management_status(evt) + "\n" + await _get_help_text(evt))
|
||||
@@ -13,10 +13,10 @@
|
||||
#
|
||||
# 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
|
||||
import asyncio
|
||||
|
||||
from mautrix_appservice import MatrixRequestError
|
||||
from mautrix.errors import MatrixRequestError
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po, puppet as pu, user as u
|
||||
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||
@@ -26,7 +26,7 @@ from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="<_level_> [_mxid_]",
|
||||
help_text="Set a temporary power level without affecting Telegram.")
|
||||
async def set_power_level(evt: CommandEvent) -> Dict:
|
||||
async def set_power_level(evt: CommandEvent) -> EventID:
|
||||
try:
|
||||
level = int(evt.args[0])
|
||||
except KeyError:
|
||||
@@ -35,20 +35,19 @@ async def set_power_level(evt: CommandEvent) -> Dict:
|
||||
return await evt.reply("The level must be an integer.")
|
||||
levels = await evt.az.intent.get_power_levels(evt.room_id)
|
||||
mxid = evt.args[1] if len(evt.args) > 1 else evt.sender.mxid
|
||||
levels["users"][mxid] = level
|
||||
levels.users[mxid] = level
|
||||
try:
|
||||
await evt.az.intent.set_power_levels(evt.room_id, levels)
|
||||
return await evt.az.intent.set_power_levels(evt.room_id, levels)
|
||||
except MatrixRequestError:
|
||||
evt.log.exception("Failed to set power level.")
|
||||
return await evt.reply("Failed to set power level.")
|
||||
return {}
|
||||
|
||||
|
||||
@command_handler(needs_admin=True, needs_auth=False,
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="<`portal`|`puppet`|`user`>",
|
||||
help_text="Clear internal bridge caches")
|
||||
async def clear_db_cache(evt: CommandEvent) -> Dict:
|
||||
async def clear_db_cache(evt: CommandEvent) -> EventID:
|
||||
try:
|
||||
section = evt.args[0].lower()
|
||||
except IndexError:
|
||||
@@ -62,9 +61,8 @@ async def clear_db_cache(evt: CommandEvent) -> Dict:
|
||||
for puppet in pu.Puppet.by_custom_mxid.values():
|
||||
puppet.sync_task.cancel()
|
||||
pu.Puppet.by_custom_mxid = {}
|
||||
await asyncio.gather(
|
||||
*[puppet.init_custom_mxid() for puppet in pu.Puppet.all_with_custom_mxid()],
|
||||
loop=evt.loop)
|
||||
await asyncio.gather(*[puppet.start() for puppet in pu.Puppet.all_with_custom_mxid()],
|
||||
loop=evt.loop)
|
||||
await evt.reply("Cleared puppet cache and restarted custom puppet syncers")
|
||||
elif section == "user":
|
||||
u.User.by_mxid = {
|
||||
@@ -80,7 +78,7 @@ async def clear_db_cache(evt: CommandEvent) -> Dict:
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="[_mxid_]",
|
||||
help_text="Reload and reconnect a user")
|
||||
async def reload_user(evt: CommandEvent) -> Dict:
|
||||
async def reload_user(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) > 0:
|
||||
mxid = evt.args[0]
|
||||
else:
|
||||
@@ -96,5 +94,5 @@ async def reload_user(evt: CommandEvent) -> Dict:
|
||||
user = u.User.get_by_mxid(mxid)
|
||||
await user.ensure_started()
|
||||
if puppet:
|
||||
await puppet.init_custom_mxid()
|
||||
await evt.reply(f"Reloaded and reconnected {user.mxid} (telegram: {user.human_tg_id})")
|
||||
await puppet.start()
|
||||
return await evt.reply(f"Reloaded and reconnected {user.mxid} (telegram: {user.human_tg_id})")
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
#
|
||||
# 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, Optional, Tuple, Coroutine
|
||||
from typing import Optional, Tuple, Coroutine
|
||||
import asyncio
|
||||
|
||||
from telethon.tl.types import ChatForbidden, ChannelForbidden
|
||||
|
||||
from ...types import MatrixRoomID, TelegramID
|
||||
from ...util import ignore_coro
|
||||
from mautrix.types import EventID, RoomID
|
||||
|
||||
from ...types import TelegramID
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
|
||||
from .util import user_has_power_level, get_initial_state
|
||||
@@ -31,7 +32,7 @@ from .util import user_has_power_level, get_initial_state
|
||||
help_text="Bridge the current Matrix room to the Telegram chat with the given "
|
||||
"ID. The ID must be the prefixed version that you get with the `/id` "
|
||||
"command of the Telegram-side bot.")
|
||||
async def bridge(evt: CommandEvent) -> Dict:
|
||||
async def bridge(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** "
|
||||
"`$cmdprefix+sp bridge <Telegram chat ID> [Matrix room ID]`")
|
||||
@@ -39,7 +40,7 @@ async def bridge(evt: CommandEvent) -> Dict:
|
||||
if evt.args[0] == "--usebot" and evt.sender.is_admin:
|
||||
force_use_bot = True
|
||||
evt.args = evt.args[1:]
|
||||
room_id = MatrixRoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id
|
||||
room_id = RoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id
|
||||
that_this = "This" if room_id == evt.room_id else "That"
|
||||
|
||||
portal = po.Portal.get_by_mxid(room_id)
|
||||
@@ -104,7 +105,8 @@ async def bridge(evt: CommandEvent) -> Dict:
|
||||
|
||||
|
||||
async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Portal"
|
||||
) -> Tuple[bool, Optional[Coroutine[None, None, None]]]:
|
||||
) -> Tuple[
|
||||
bool, Optional[Coroutine[None, None, None]]]:
|
||||
if not portal.mxid:
|
||||
await evt.reply("The portal seems to have lost its Matrix room between you"
|
||||
"calling `$cmdprefix+sp bridge` and this command.\n\n"
|
||||
@@ -127,7 +129,7 @@ async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Porta
|
||||
return False, None
|
||||
|
||||
|
||||
async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]:
|
||||
async def confirm_bridge(evt: CommandEvent) -> Optional[EventID]:
|
||||
status = evt.sender.command_status
|
||||
try:
|
||||
portal = po.Portal.get_by_tgid(status["tgid"], peer_type=status["peer_type"])
|
||||
@@ -142,7 +144,7 @@ async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]:
|
||||
if not ok:
|
||||
return None
|
||||
elif coro:
|
||||
ignore_coro(asyncio.ensure_future(coro, loop=evt.loop))
|
||||
asyncio.ensure_future(coro, loop=evt.loop)
|
||||
await evt.reply("Cleaning up previous portal room...")
|
||||
elif portal.mxid:
|
||||
evt.sender.command_status = None
|
||||
@@ -179,8 +181,7 @@ async def confirm_bridge(evt: CommandEvent) -> Optional[Dict]:
|
||||
portal.photo_id = ""
|
||||
portal.save()
|
||||
|
||||
ignore_coro(asyncio.ensure_future(portal.update_matrix_room(user, entity, direct,
|
||||
levels=levels),
|
||||
loop=evt.loop))
|
||||
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels),
|
||||
loop=evt.loop)
|
||||
|
||||
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
#
|
||||
# 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, Awaitable
|
||||
from typing import Awaitable
|
||||
from io import StringIO
|
||||
|
||||
from ...config import yaml
|
||||
from mautrix.util.config import yaml
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po, util
|
||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||
|
||||
@@ -54,7 +56,7 @@ async def config(evt: CommandEvent) -> None:
|
||||
portal.save()
|
||||
|
||||
|
||||
def config_help(evt: CommandEvent) -> Awaitable[Dict]:
|
||||
def config_help(evt: CommandEvent) -> Awaitable[EventID]:
|
||||
return evt.reply("""**Usage:** `$cmdprefix config <subcommand> [...]`. Subcommands:
|
||||
|
||||
* **help** - View this help text.
|
||||
@@ -67,13 +69,13 @@ def config_help(evt: CommandEvent) -> Awaitable[Dict]:
|
||||
""")
|
||||
|
||||
|
||||
def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[Dict]:
|
||||
def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[EventID]:
|
||||
stream = StringIO()
|
||||
yaml.dump(portal.local_config, stream)
|
||||
return evt.reply(f"Room-specific config:\n\n```yaml\n{stream.getvalue()}```")
|
||||
|
||||
|
||||
def config_defaults(evt: CommandEvent) -> Awaitable[Dict]:
|
||||
def config_defaults(evt: CommandEvent) -> Awaitable[EventID]:
|
||||
stream = StringIO()
|
||||
yaml.dump({
|
||||
"bridge_notices": {
|
||||
@@ -89,7 +91,7 @@ def config_defaults(evt: CommandEvent) -> Awaitable[Dict]:
|
||||
return evt.reply(f"Bridge instance wide config:\n\n```yaml\n{stream.getvalue()}```")
|
||||
|
||||
|
||||
def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Awaitable[Dict]:
|
||||
def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Awaitable[EventID]:
|
||||
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):
|
||||
@@ -99,7 +101,7 @@ def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: str) -> Aw
|
||||
"Does the path contain non-map types?")
|
||||
|
||||
|
||||
def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[Dict]:
|
||||
def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[EventID]:
|
||||
if not key:
|
||||
return evt.reply(f"**Usage:** `$cmdprefix+sp config unset <key>`")
|
||||
elif util.recursive_del(portal.local_config, key):
|
||||
@@ -109,7 +111,7 @@ def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[Di
|
||||
|
||||
|
||||
def config_add_del(evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str
|
||||
) -> Awaitable[Dict]:
|
||||
) -> Awaitable[EventID]:
|
||||
if not key or value is None:
|
||||
return evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} <key> <value>`")
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#
|
||||
# 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
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po
|
||||
from ...types import TelegramID
|
||||
@@ -26,7 +26,7 @@ from .util import user_has_power_level, get_initial_state
|
||||
help_text="Create a Telegram chat of the given type for the current Matrix room. "
|
||||
"The type is either `group`, `supergroup` or `channel` (defaults to "
|
||||
"`group`).")
|
||||
async def create(evt: CommandEvent) -> Dict:
|
||||
async def create(evt: CommandEvent) -> EventID:
|
||||
type = evt.args[0] if len(evt.args) > 0 else "group"
|
||||
if type not in {"chat", "group", "supergroup", "channel"}:
|
||||
return await evt.reply(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#
|
||||
# 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, Optional
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||
@@ -24,7 +24,7 @@ from .. import command_handler, CommandEvent, SECTION_ADMIN
|
||||
help_args="<`whitelist`|`blacklist`>",
|
||||
help_text="Change whether the bridge will allow or disallow bridging rooms by "
|
||||
"default.")
|
||||
async def filter_mode(evt: CommandEvent) -> Dict:
|
||||
async def filter_mode(evt: CommandEvent) -> EventID:
|
||||
try:
|
||||
mode = evt.args[0]
|
||||
if mode not in ("whitelist", "blacklist"):
|
||||
@@ -49,7 +49,7 @@ async def filter_mode(evt: CommandEvent) -> Dict:
|
||||
help_section=SECTION_ADMIN,
|
||||
help_args="<`whitelist`|`blacklist`> <_chat ID_>",
|
||||
help_text="Allow or disallow bridging a specific chat.")
|
||||
async def edit_filter(evt: CommandEvent) -> Optional[Dict]:
|
||||
async def edit_filter(evt: CommandEvent) -> EventID:
|
||||
try:
|
||||
action = evt.args[0]
|
||||
if action not in ("whitelist", "blacklist", "add", "remove"):
|
||||
@@ -91,4 +91,5 @@ async def edit_filter(evt: CommandEvent) -> Optional[Dict]:
|
||||
filter_id_list.remove(filter_id)
|
||||
save()
|
||||
return await evt.reply(f"Chat ID removed from {mode}.")
|
||||
return None
|
||||
else:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp filter <whitelist/blacklist> <chat ID>`")
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
|
||||
UsernameNotModifiedError, UsernameOccupiedError)
|
||||
|
||||
from mautrix.types import EventID
|
||||
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT, SECTION_MISC
|
||||
from .util import user_has_power_level
|
||||
@@ -26,7 +26,7 @@ from .util import user_has_power_level
|
||||
@command_handler(needs_admin=False, needs_puppeting=False, needs_auth=False,
|
||||
help_section=SECTION_MISC,
|
||||
help_text="Fetch Matrix room state to ensure the bridge has up-to-date info.")
|
||||
async def sync_state(evt: CommandEvent) -> Dict:
|
||||
async def sync_state(evt: CommandEvent) -> EventID:
|
||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||
if not portal:
|
||||
return await evt.reply("This is not a portal room.")
|
||||
@@ -40,7 +40,7 @@ async def sync_state(evt: CommandEvent) -> Dict:
|
||||
@command_handler(name="id", needs_admin=False, needs_puppeting=False, needs_auth=False,
|
||||
help_section=SECTION_MISC,
|
||||
help_text="Get the ID of the Telegram chat where this room is bridged.")
|
||||
async def get_id(evt: CommandEvent) -> Dict:
|
||||
async def get_id(evt: CommandEvent) -> EventID:
|
||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||
if not portal:
|
||||
return await evt.reply("This is not a portal room.")
|
||||
@@ -54,7 +54,7 @@ async def get_id(evt: CommandEvent) -> Dict:
|
||||
|
||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Get a Telegram invite link to the current chat.")
|
||||
async def invite_link(evt: CommandEvent) -> Dict:
|
||||
async def invite_link(evt: CommandEvent) -> EventID:
|
||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||
if not portal:
|
||||
return await evt.reply("This is not a portal room.")
|
||||
@@ -73,7 +73,7 @@ async def invite_link(evt: CommandEvent) -> Dict:
|
||||
|
||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Upgrade a normal Telegram group to a supergroup.")
|
||||
async def upgrade(evt: CommandEvent) -> Dict:
|
||||
async def upgrade(evt: CommandEvent) -> EventID:
|
||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||
if not portal:
|
||||
return await evt.reply("This is not a portal room.")
|
||||
@@ -95,7 +95,7 @@ async def upgrade(evt: CommandEvent) -> Dict:
|
||||
help_args="<_name_|`-`>",
|
||||
help_text="Change the username of a supergroup/channel. "
|
||||
"To disable, use a dash (`-`) as the name.")
|
||||
async def group_name(evt: CommandEvent) -> Dict:
|
||||
async def group_name(evt: CommandEvent) -> EventID:
|
||||
if len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp group-name <name/->`")
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from typing import Dict, Callable, Optional
|
||||
|
||||
from ...types import MatrixRoomID
|
||||
from mautrix.types import RoomID, EventID
|
||||
|
||||
from ... import portal as po
|
||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||
from .util import user_has_power_level
|
||||
@@ -24,7 +25,7 @@ from .util import user_has_power_level
|
||||
async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
|
||||
action: Optional[str] = None
|
||||
) -> Optional[po.Portal]:
|
||||
room_id = MatrixRoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id
|
||||
room_id = RoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id
|
||||
|
||||
portal = po.Portal.get_by_mxid(room_id)
|
||||
if not portal:
|
||||
@@ -41,7 +42,7 @@ async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
|
||||
|
||||
def _get_portal_murder_function(action: str, room_id: str, function: Callable, command: str,
|
||||
completed_message: str) -> Dict:
|
||||
async def post_confirm(confirm) -> Optional[Dict]:
|
||||
async def post_confirm(confirm) -> Optional[EventID]:
|
||||
confirm.sender.command_status = None
|
||||
if len(confirm.args) > 0 and confirm.args[0] == f"confirm-{command}":
|
||||
await function()
|
||||
@@ -62,7 +63,7 @@ def _get_portal_murder_function(action: str, room_id: str, function: Callable, c
|
||||
help_text="Remove all users from the current portal room and forget the portal. "
|
||||
"Only works for group chats; to delete a private chat portal, simply "
|
||||
"leave the room.")
|
||||
async def delete_portal(evt: CommandEvent) -> Optional[Dict]:
|
||||
async def delete_portal(evt: CommandEvent) -> Optional[EventID]:
|
||||
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
||||
if not portal:
|
||||
return None
|
||||
@@ -83,7 +84,7 @@ async def delete_portal(evt: CommandEvent) -> Optional[Dict]:
|
||||
@command_handler(needs_auth=False, needs_puppeting=False,
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Remove puppets from the current portal room and forget the portal.")
|
||||
async def unbridge(evt: CommandEvent) -> Optional[Dict]:
|
||||
async def unbridge(evt: CommandEvent) -> Optional[EventID]:
|
||||
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
||||
if not portal:
|
||||
return None
|
||||
|
||||
@@ -13,43 +13,48 @@
|
||||
#
|
||||
# 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, Tuple
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from mautrix_appservice import MatrixRequestError, IntentAPI
|
||||
from mautrix.errors import MatrixRequestError
|
||||
from mautrix.appservice import IntentAPI
|
||||
from mautrix.types import RoomID, EventType, PowerLevelStateEventContent
|
||||
|
||||
from ... import user as u
|
||||
|
||||
OptStr = Optional[str]
|
||||
|
||||
async def get_initial_state(intent: IntentAPI, room_id: str) -> Tuple[str, str, Dict]:
|
||||
state = await intent.get_room_state(room_id)
|
||||
title = None
|
||||
about = None
|
||||
levels = None
|
||||
|
||||
async def get_initial_state(intent: IntentAPI, room_id: RoomID
|
||||
) -> Tuple[OptStr, OptStr, Optional[PowerLevelStateEventContent]]:
|
||||
state = await intent.get_state(room_id)
|
||||
title: OptStr = None
|
||||
about: OptStr = None
|
||||
levels: Optional[PowerLevelStateEventContent] = None
|
||||
for event in state:
|
||||
try:
|
||||
if event["type"] == "m.room.name":
|
||||
title = event["content"]["name"]
|
||||
elif event["type"] == "m.room.topic":
|
||||
about = event["content"]["topic"]
|
||||
elif event["type"] == "m.room.power_levels":
|
||||
levels = event["content"]
|
||||
elif event["type"] == "m.room.canonical_alias":
|
||||
title = title or event["content"]["alias"]
|
||||
if event.type == EventType.ROOM_NAME:
|
||||
title = event.content.name
|
||||
elif event.type == EventType.ROOM_TOPIC:
|
||||
about = event.content.topic
|
||||
elif event.type == EventType.ROOM_POWER_LEVELS:
|
||||
levels = event.content
|
||||
elif event.type == EventType.ROOM_CANONICAL_ALIAS:
|
||||
title = title or event.content.canonical_alias
|
||||
except KeyError:
|
||||
# Some state event probably has empty content
|
||||
pass
|
||||
return title, about, levels
|
||||
|
||||
|
||||
async def user_has_power_level(room: str, intent, sender: u.User, event: str, default: int = 50
|
||||
) -> bool:
|
||||
async def user_has_power_level(room_id: RoomID, intent: IntentAPI, sender: u.User,
|
||||
event: str) -> bool:
|
||||
if sender.is_admin:
|
||||
return True
|
||||
# Make sure the state store contains the power levels.
|
||||
try:
|
||||
await intent.get_power_levels(room)
|
||||
await intent.get_power_levels(room_id)
|
||||
except MatrixRequestError:
|
||||
return False
|
||||
return intent.state_store.has_power_level(room, sender.mxid,
|
||||
event=f"net.maunium.telegram.{event}",
|
||||
default=default)
|
||||
event_type = EventType.find(f"net.maunium.telegram.{event}")
|
||||
event_type.t_class = EventType.Class.STATE
|
||||
return intent.state_store.has_power_level(room_id, sender.mxid, event_type)
|
||||
|
||||
@@ -21,6 +21,8 @@ from telethon.tl.types import Authorization
|
||||
from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest,
|
||||
ResetAuthorizationRequest, UpdateProfileRequest)
|
||||
|
||||
from mautrix.types import EventID
|
||||
|
||||
from .. import command_handler, CommandEvent, SECTION_AUTH
|
||||
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from typing import Any, Dict, List, NamedTuple
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
|
||||
from mautrix.types import UserID
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
#
|
||||
# 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 .base import Base
|
||||
from mautrix.bridge.db import UserProfile, RoomState
|
||||
|
||||
from .bot_chat import BotChat
|
||||
from .message import Message
|
||||
from .portal import Portal
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||
# Copyright (C) 2019 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 abc import abstractmethod
|
||||
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.engine.base import Engine
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from sqlalchemy.sql.base import ImmutableColumnCollection
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
|
||||
class BaseBase:
|
||||
db: Engine = None
|
||||
t: Table = None
|
||||
__table__: Table = None
|
||||
c: ImmutableColumnCollection = None
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def _one_or_none(cls, rows: RowProxy):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _select_one_or_none(cls, *args):
|
||||
return cls._one_or_none(cls.db.execute(cls.t.select().where(*args)))
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _edit_identity(self):
|
||||
pass
|
||||
|
||||
def update(self, **values) -> None:
|
||||
with self.db.begin() as conn:
|
||||
conn.execute(self.t.update()
|
||||
.where(self._edit_identity)
|
||||
.values(**values))
|
||||
for key, value in values.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def delete(self) -> None:
|
||||
with self.db.begin() as conn:
|
||||
conn.execute(self.t.delete().where(self._edit_identity))
|
||||
|
||||
|
||||
Base = declarative_base(cls=BaseBase)
|
||||
@@ -1,26 +0,0 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.engine.base import Engine
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from sqlalchemy.sql.base import ImmutableColumnCollection
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
class Base(declarative_base):
|
||||
db: Engine
|
||||
t: Table
|
||||
__table__: Table
|
||||
c: ImmutableColumnCollection
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def _one_or_none(cls, rows: RowProxy): ...
|
||||
|
||||
@classmethod
|
||||
def _select_one_or_none(cls, *args): ...
|
||||
|
||||
def _edit_identity(self): ...
|
||||
|
||||
def update(self, **values) -> None: ...
|
||||
|
||||
def delete(self) -> None: ...
|
||||
@@ -16,28 +16,31 @@
|
||||
from typing import Iterable
|
||||
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
|
||||
from mautrix.bridge.db import Base
|
||||
|
||||
from ..types import TelegramID
|
||||
from .base import Base
|
||||
|
||||
|
||||
# Fucking Telegram not telling bots what chats they are in 3:<
|
||||
class BotChat(Base):
|
||||
__tablename__ = "bot_chat"
|
||||
id = Column(Integer, primary_key=True) # type: TelegramID
|
||||
type = Column(String, nullable=False)
|
||||
id: TelegramID = Column(Integer, primary_key=True)
|
||||
type: str = Column(String, nullable=False)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, chat_id: TelegramID) -> None:
|
||||
def delete_by_id(cls, chat_id: TelegramID) -> None:
|
||||
with cls.db.begin() as conn:
|
||||
conn.execute(cls.t.delete().where(cls.c.id == chat_id))
|
||||
|
||||
@classmethod
|
||||
def scan(cls, row: RowProxy) -> 'BotChat':
|
||||
return cls(id=row[0], type=row[1])
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> Iterable['BotChat']:
|
||||
rows = cls.db.execute(cls.t.select())
|
||||
for row in rows:
|
||||
chat_id, chat_type = row
|
||||
yield cls(id=chat_id, type=chat_type)
|
||||
return cls._select_all()
|
||||
|
||||
def insert(self) -> None:
|
||||
with self.db.begin() as conn:
|
||||
|
||||
@@ -13,42 +13,35 @@
|
||||
#
|
||||
# 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 Optional, Iterator
|
||||
|
||||
from sqlalchemy import Column, UniqueConstraint, Integer, String, and_, func, desc, select
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.sql.expression import ClauseElement
|
||||
|
||||
from ..types import MatrixRoomID, MatrixEventID, TelegramID
|
||||
from .base import Base
|
||||
from mautrix.types import RoomID, EventID
|
||||
from mautrix.bridge.db import Base
|
||||
|
||||
from ..types import TelegramID
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "message"
|
||||
|
||||
mxid = Column(String) # type: MatrixEventID
|
||||
mx_room = Column(String) # type: MatrixRoomID
|
||||
tgid = Column(Integer, primary_key=True) # type: TelegramID
|
||||
tg_space = Column(Integer, primary_key=True) # type: TelegramID
|
||||
edit_index = Column(Integer, primary_key=True) # type: int
|
||||
mxid: EventID = Column(String)
|
||||
mx_room: RoomID = Column(String)
|
||||
tgid: TelegramID = Column(Integer, primary_key=True)
|
||||
tg_space: TelegramID = Column(Integer, primary_key=True)
|
||||
edit_index: int = Column(Integer, primary_key=True)
|
||||
|
||||
__table_args__ = (UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room"),)
|
||||
|
||||
@classmethod
|
||||
def _one_or_none(cls, rows: RowProxy) -> Optional['Message']:
|
||||
try:
|
||||
mxid, mx_room, tgid, tg_space, edit_index = next(rows)
|
||||
return cls(mxid=mxid, mx_room=mx_room, tgid=tgid, tg_space=tg_space,
|
||||
edit_index=edit_index)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _all(rows: RowProxy) -> List['Message']:
|
||||
return [Message(mxid=row[0], mx_room=row[1], tgid=row[2], tg_space=row[3],
|
||||
edit_index=row[4])
|
||||
for row in rows]
|
||||
def scan(cls, row: RowProxy) -> 'Message':
|
||||
return cls(mxid=row[0], mx_room=row[1], tgid=row[2], tg_space=row[3], edit_index=row[4])
|
||||
|
||||
@classmethod
|
||||
def get_all_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> List['Message']:
|
||||
def get_all_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> Iterator['Message']:
|
||||
return cls._all(cls.db.execute(cls.t.select().where(and_(cls.c.tgid == tgid,
|
||||
cls.c.tg_space == tg_space))))
|
||||
|
||||
@@ -68,7 +61,7 @@ class Message(Base):
|
||||
return cls._one_or_none(cls.db.execute(query))
|
||||
|
||||
@classmethod
|
||||
def count_spaces_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID) -> int:
|
||||
def count_spaces_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> int:
|
||||
rows = cls.db.execute(select([func.count(cls.c.tg_space)])
|
||||
.where(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room)))
|
||||
try:
|
||||
@@ -78,7 +71,7 @@ class Message(Base):
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def get_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID, tg_space: TelegramID
|
||||
def get_by_mxid(cls, mxid: EventID, mx_room: RoomID, tg_space: TelegramID
|
||||
) -> Optional['Message']:
|
||||
return cls._select_one_or_none(and_(cls.c.mxid == mxid,
|
||||
cls.c.mx_room == mx_room,
|
||||
@@ -94,14 +87,14 @@ class Message(Base):
|
||||
.values(**values))
|
||||
|
||||
@classmethod
|
||||
def update_by_mxid(cls, s_mxid: MatrixEventID, s_mx_room: MatrixRoomID, **values) -> None:
|
||||
def update_by_mxid(cls, s_mxid: EventID, s_mx_room: RoomID, **values) -> None:
|
||||
with cls.db.begin() as conn:
|
||||
conn.execute(cls.t.update()
|
||||
.where(and_(cls.c.mxid == s_mxid, cls.c.mx_room == s_mx_room))
|
||||
.values(**values))
|
||||
|
||||
@property
|
||||
def _edit_identity(self):
|
||||
def _edit_identity(self) -> ClauseElement:
|
||||
return and_(self.c.tgid == self.tgid, self.c.tg_space == self.tg_space,
|
||||
self.c.edit_index == self.edit_index)
|
||||
|
||||
|
||||
@@ -13,55 +13,52 @@
|
||||
#
|
||||
# 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 sqlalchemy import Column, Integer, String, Boolean, Text, and_
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from typing import Optional
|
||||
|
||||
from ..types import MatrixRoomID, TelegramID
|
||||
from .base import Base
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Text, and_
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from sqlalchemy.sql.expression import ClauseElement
|
||||
|
||||
from mautrix.types import RoomID
|
||||
from mautrix.bridge.db import Base
|
||||
|
||||
from ..types import TelegramID
|
||||
|
||||
|
||||
class Portal(Base):
|
||||
__tablename__ = "portal"
|
||||
|
||||
# Telegram chat information
|
||||
tgid = Column(Integer, primary_key=True) # type: TelegramID
|
||||
tg_receiver = Column(Integer, primary_key=True) # type: TelegramID
|
||||
peer_type = Column(String, nullable=False)
|
||||
megagroup = Column(Boolean)
|
||||
tgid: TelegramID = Column(Integer, primary_key=True)
|
||||
tg_receiver: TelegramID = Column(Integer, primary_key=True)
|
||||
peer_type: str = Column(String, nullable=False)
|
||||
megagroup: bool = Column(Boolean)
|
||||
|
||||
# Matrix portal information
|
||||
mxid = Column(String, unique=True, nullable=True) # type: Optional[MatrixRoomID]
|
||||
mxid: RoomID = Column(String, unique=True, nullable=True)
|
||||
|
||||
config = Column(Text, nullable=True)
|
||||
config: str = Column(Text, nullable=True)
|
||||
|
||||
# Telegram chat metadata
|
||||
username = Column(String, nullable=True)
|
||||
title = Column(String, nullable=True)
|
||||
about = Column(String, nullable=True)
|
||||
photo_id = Column(String, nullable=True)
|
||||
username: str = Column(String, nullable=True)
|
||||
title: str = Column(String, nullable=True)
|
||||
about: str = Column(String, nullable=True)
|
||||
photo_id: str = Column(String, nullable=True)
|
||||
|
||||
@classmethod
|
||||
def scan(cls, row) -> Optional['Portal']:
|
||||
def scan(cls, row: RowProxy) -> Optional['Portal']:
|
||||
(tgid, tg_receiver, peer_type, megagroup, mxid, config, username, title, about,
|
||||
photo_id) = row
|
||||
return cls(tgid=tgid, tg_receiver=tg_receiver, peer_type=peer_type, megagroup=megagroup,
|
||||
mxid=mxid, config=config, username=username, title=title, about=about,
|
||||
photo_id=photo_id)
|
||||
|
||||
@classmethod
|
||||
def _one_or_none(cls, rows: RowProxy) -> Optional['Portal']:
|
||||
try:
|
||||
return cls.scan(next(rows))
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_by_tgid(cls, tgid: TelegramID, tg_receiver: TelegramID) -> Optional['Portal']:
|
||||
return cls._select_one_or_none(and_(cls.c.tgid == tgid, cls.c.tg_receiver == tg_receiver))
|
||||
|
||||
@classmethod
|
||||
def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['Portal']:
|
||||
def get_by_mxid(cls, mxid: RoomID) -> Optional['Portal']:
|
||||
return cls._select_one_or_none(cls.c.mxid == mxid)
|
||||
|
||||
@classmethod
|
||||
@@ -69,7 +66,7 @@ class Portal(Base):
|
||||
return cls._select_one_or_none(cls.c.username == username)
|
||||
|
||||
@property
|
||||
def _edit_identity(self):
|
||||
def _edit_identity(self) -> ClauseElement:
|
||||
return and_(self.c.tgid == self.tgid, self.c.tg_receiver == self.tg_receiver)
|
||||
|
||||
def insert(self) -> None:
|
||||
|
||||
@@ -13,31 +13,35 @@
|
||||
#
|
||||
# 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 sqlalchemy import Column, Integer, String, Boolean
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from sqlalchemy.sql import expression
|
||||
from typing import Optional, Iterable
|
||||
|
||||
from ..types import MatrixUserID, MatrixRoomID, TelegramID
|
||||
from .base import Base
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from sqlalchemy.sql.expression import ClauseElement
|
||||
|
||||
from mautrix.types import UserID
|
||||
from mautrix.bridge.db import Base
|
||||
|
||||
from ..types import TelegramID
|
||||
|
||||
|
||||
class Puppet(Base):
|
||||
__tablename__ = "puppet"
|
||||
|
||||
id = Column(Integer, primary_key=True) # type: TelegramID
|
||||
custom_mxid = Column(String, nullable=True) # type: Optional[MatrixUserID]
|
||||
access_token = Column(String, nullable=True)
|
||||
displayname = Column(String, nullable=True)
|
||||
displayname_source = Column(Integer, nullable=True) # type: Optional[TelegramID]
|
||||
username = Column(String, nullable=True)
|
||||
photo_id = Column(String, nullable=True)
|
||||
is_bot = Column(Boolean, nullable=True)
|
||||
matrix_registered = Column(Boolean, nullable=False, server_default=expression.false())
|
||||
disable_updates = Column(Boolean, nullable=False, server_default=expression.false())
|
||||
id: TelegramID = Column(Integer, primary_key=True)
|
||||
custom_mxid: UserID = Column(String, nullable=True)
|
||||
access_token: str = Column(String, nullable=True)
|
||||
displayname: str = Column(String, nullable=True)
|
||||
displayname_source: TelegramID = Column(Integer, nullable=True)
|
||||
username: str = Column(String, nullable=True)
|
||||
photo_id: str = Column(String, nullable=True)
|
||||
is_bot: bool = Column(Boolean, nullable=True)
|
||||
matrix_registered: bool = Column(Boolean, nullable=False, server_default=expression.false())
|
||||
disable_updates: bool = Column(Boolean, nullable=False, server_default=expression.false())
|
||||
|
||||
@classmethod
|
||||
def scan(cls, row) -> Optional['Puppet']:
|
||||
def scan(cls, row: RowProxy) -> Optional['Puppet']:
|
||||
(id, custom_mxid, access_token, displayname, displayname_source, username, photo_id,
|
||||
is_bot, matrix_registered, disable_updates) = row
|
||||
return cls(id=id, custom_mxid=custom_mxid, access_token=access_token,
|
||||
@@ -45,13 +49,6 @@ class Puppet(Base):
|
||||
username=username, photo_id=photo_id, is_bot=is_bot,
|
||||
matrix_registered=matrix_registered, disable_updates=disable_updates)
|
||||
|
||||
@classmethod
|
||||
def _one_or_none(cls, rows: RowProxy) -> Optional['Puppet']:
|
||||
try:
|
||||
return cls.scan(next(rows))
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def all_with_custom_mxid(cls) -> Iterable['Puppet']:
|
||||
rows = cls.db.execute(cls.t.select().where(cls.c.custom_mxid != None))
|
||||
@@ -63,7 +60,7 @@ class Puppet(Base):
|
||||
return cls._select_one_or_none(cls.c.id == tgid)
|
||||
|
||||
@classmethod
|
||||
def get_by_custom_mxid(cls, mxid: MatrixUserID) -> Optional['Puppet']:
|
||||
def get_by_custom_mxid(cls, mxid: UserID) -> Optional['Puppet']:
|
||||
return cls._select_one_or_none(cls.c.custom_mxid == mxid)
|
||||
|
||||
@classmethod
|
||||
@@ -75,7 +72,7 @@ class Puppet(Base):
|
||||
return cls._select_one_or_none(cls.c.displayname == displayname)
|
||||
|
||||
@property
|
||||
def _edit_identity(self):
|
||||
def _edit_identity(self) -> ClauseElement:
|
||||
return self.c.id == self.id
|
||||
|
||||
def insert(self) -> None:
|
||||
|
||||
@@ -13,40 +13,40 @@
|
||||
#
|
||||
# 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 sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean
|
||||
from typing import Optional
|
||||
|
||||
from mautrix.types import ContentURI
|
||||
from sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
|
||||
from .base import Base
|
||||
from mautrix.types import ContentURI
|
||||
from mautrix.bridge.db import Base
|
||||
|
||||
|
||||
class TelegramFile(Base):
|
||||
__tablename__ = "telegram_file"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
id: str = Column(String, primary_key=True)
|
||||
mxc: ContentURI = Column(String)
|
||||
mime_type = Column(String)
|
||||
was_converted = Column(Boolean)
|
||||
timestamp = Column(BigInteger)
|
||||
size = Column(Integer, nullable=True)
|
||||
width = Column(Integer, nullable=True)
|
||||
height = Column(Integer, nullable=True)
|
||||
thumbnail_id = Column("thumbnail", String, ForeignKey("telegram_file.id"), nullable=True)
|
||||
thumbnail = None # type: Optional[TelegramFile]
|
||||
mime_type: str = Column(String)
|
||||
was_converted: bool = Column(Boolean)
|
||||
timestamp: int = Column(BigInteger)
|
||||
size: int = Column(Integer, nullable=True)
|
||||
width: int = Column(Integer, nullable=True)
|
||||
height: int = Column(Integer, nullable=True)
|
||||
thumbnail_id: str = Column("thumbnail", String, ForeignKey("telegram_file.id"), nullable=True)
|
||||
thumbnail: Optional['TelegramFile'] = None
|
||||
|
||||
def scan(cls, row: RowProxy) -> 'TelegramFile':
|
||||
loc_id, mxc, mime, conv, ts, s, w, h, thumb_id = row
|
||||
thumb = None
|
||||
if thumb_id:
|
||||
thumb = cls.get(thumb_id)
|
||||
return cls(id=loc_id, mxc=mxc, mime_type=mime, was_converted=conv, timestamp=ts,
|
||||
size=s, width=w, height=h, thumbnail_id=thumb_id, thumbnail=thumb)
|
||||
|
||||
@classmethod
|
||||
def get(cls, loc_id: str) -> Optional['TelegramFile']:
|
||||
rows = cls.db.execute(cls.t.select().where(cls.c.id == loc_id))
|
||||
try:
|
||||
loc_id, mxc, mime, conv, ts, s, w, h, thumb_id = next(rows)
|
||||
thumb = None
|
||||
if thumb_id:
|
||||
thumb = cls.get(thumb_id)
|
||||
return cls(id=loc_id, mxc=mxc, mime_type=mime, was_converted=conv, timestamp=ts,
|
||||
size=s, width=w, height=h, thumbnail_id=thumb_id, thumbnail=thumb)
|
||||
except StopIteration:
|
||||
return None
|
||||
return cls._select_one_or_none(cls.c.id == loc_id)
|
||||
|
||||
def insert(self) -> None:
|
||||
with self.db.begin() as conn:
|
||||
|
||||
+26
-29
@@ -13,46 +13,43 @@
|
||||
#
|
||||
# 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 sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from typing import Optional, Iterable, Tuple
|
||||
|
||||
from ..types import MatrixUserID, TelegramID
|
||||
from .base import Base
|
||||
from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String
|
||||
from sqlalchemy.engine.result import RowProxy
|
||||
from sqlalchemy.sql.expression import ClauseElement
|
||||
|
||||
from mautrix.types import UserID
|
||||
from mautrix.bridge.db import Base
|
||||
|
||||
from ..types import TelegramID
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "user"
|
||||
|
||||
mxid = Column(String, primary_key=True) # type: MatrixUserID
|
||||
tgid = Column(Integer, nullable=True, unique=True) # type: Optional[TelegramID]
|
||||
tg_username = Column(String, nullable=True)
|
||||
tg_phone = Column(String, nullable=True)
|
||||
saved_contacts = Column(Integer, default=0, nullable=False)
|
||||
mxid: UserID = Column(String, primary_key=True)
|
||||
tgid: Optional[TelegramID] = Column(Integer, nullable=True, unique=True)
|
||||
tg_username: str = Column(String, nullable=True)
|
||||
tg_phone: str = Column(String, nullable=True)
|
||||
saved_contacts: int = Column(Integer, default=0, nullable=False)
|
||||
|
||||
@classmethod
|
||||
def _one_or_none(cls, rows: RowProxy) -> Optional['User']:
|
||||
try:
|
||||
mxid, tgid, tg_username, tg_phone, saved_contacts = next(rows)
|
||||
return cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone,
|
||||
saved_contacts=saved_contacts)
|
||||
except StopIteration:
|
||||
return None
|
||||
def scan(cls, row: RowProxy) -> 'User':
|
||||
mxid, tgid, tg_username, tg_phone, saved_contacts = row
|
||||
return cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone,
|
||||
saved_contacts=saved_contacts)
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> Iterable['User']:
|
||||
rows = cls.db.execute(cls.t.select())
|
||||
for row in rows:
|
||||
mxid, tgid, tg_username, tg_phone, saved_contacts = row
|
||||
yield cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone,
|
||||
saved_contacts=saved_contacts)
|
||||
return cls._select_all()
|
||||
|
||||
@classmethod
|
||||
def get_by_tgid(cls, tgid: TelegramID) -> Optional['User']:
|
||||
return cls._select_one_or_none(cls.c.tgid == tgid)
|
||||
|
||||
@classmethod
|
||||
def get_by_mxid(cls, mxid: MatrixUserID) -> Optional['User']:
|
||||
def get_by_mxid(cls, mxid: UserID) -> Optional['User']:
|
||||
return cls._select_one_or_none(cls.c.mxid == mxid)
|
||||
|
||||
@classmethod
|
||||
@@ -60,7 +57,7 @@ class User(Base):
|
||||
return cls._select_one_or_none(cls.c.tg_username == username)
|
||||
|
||||
@property
|
||||
def _edit_identity(self):
|
||||
def _edit_identity(self) -> ClauseElement:
|
||||
return self.c.mxid == self.mxid
|
||||
|
||||
def insert(self) -> None:
|
||||
@@ -112,10 +109,10 @@ class User(Base):
|
||||
class UserPortal(Base):
|
||||
__tablename__ = "user_portal"
|
||||
|
||||
user = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE", ondelete="CASCADE"),
|
||||
primary_key=True) # type: TelegramID
|
||||
portal = Column(Integer, primary_key=True) # type: TelegramID
|
||||
portal_receiver = Column(Integer, primary_key=True) # type: TelegramID
|
||||
user: TelegramID = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE",
|
||||
ondelete="CASCADE"), primary_key=True)
|
||||
portal: TelegramID = Column(Integer, primary_key=True)
|
||||
portal_receiver: TelegramID = Column(Integer, primary_key=True)
|
||||
|
||||
__table_args__ = (ForeignKeyConstraint(("portal", "portal_receiver"),
|
||||
("portal.tgid", "portal.tg_receiver"),
|
||||
@@ -125,5 +122,5 @@ class UserPortal(Base):
|
||||
class Contact(Base):
|
||||
__tablename__ = "contact"
|
||||
|
||||
user = Column(Integer, ForeignKey("user.tgid"), primary_key=True) # type: TelegramID
|
||||
contact = Column(Integer, ForeignKey("puppet.id"), primary_key=True) # type: TelegramID
|
||||
user: TelegramID = Column(Integer, ForeignKey("user.tgid"), primary_key=True)
|
||||
contact: TelegramID = Column(Integer, ForeignKey("puppet.id"), primary_key=True)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#
|
||||
# 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, List, Optional, Tuple, TYPE_CHECKING
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from html import escape
|
||||
import logging
|
||||
import re
|
||||
@@ -26,14 +26,15 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, M
|
||||
MessageEntityBlockquote, MessageEntityStrike, MessageFwdHeader,
|
||||
MessageEntityUnderline, PeerUser)
|
||||
|
||||
from mautrix_appservice import MatrixRequestError
|
||||
from mautrix_appservice.intent_api import IntentAPI
|
||||
from mautrix.errors import MatrixRequestError
|
||||
from mautrix.appservice import IntentAPI
|
||||
from mautrix.types import (TextMessageEventContent, RelatesTo, RelationType, Format, MessageType,
|
||||
MessageEvent)
|
||||
|
||||
from .. import user as u, puppet as pu, portal as po
|
||||
from ..types import TelegramID
|
||||
from ..db import Message as DBMessage
|
||||
from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html,
|
||||
trim_reply_fallback_text)
|
||||
from .util import (add_surrogates, remove_surrogates)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..abstract_user import AbstractUser
|
||||
@@ -41,29 +42,22 @@ if TYPE_CHECKING:
|
||||
log: logging.Logger = logging.getLogger("mau.fmt.tg")
|
||||
|
||||
|
||||
def telegram_reply_to_matrix(evt: Message, source: 'AbstractUser') -> Dict:
|
||||
def telegram_reply_to_matrix(evt: Message, source: 'AbstractUser') -> Optional[RelatesTo]:
|
||||
if evt.reply_to_msg_id:
|
||||
space = (evt.to_id.channel_id
|
||||
if isinstance(evt, Message) and isinstance(evt.to_id, PeerChannel)
|
||||
else source.tgid)
|
||||
msg = DBMessage.get_one_by_tgid(evt.reply_to_msg_id, space)
|
||||
if msg:
|
||||
return {
|
||||
"m.in_reply_to": {
|
||||
"event_id": msg.mxid,
|
||||
"room_id": msg.mx_room,
|
||||
},
|
||||
"rel_type": "m.reference",
|
||||
"event_id": msg.mxid,
|
||||
"room_id": msg.mx_room,
|
||||
}
|
||||
return {}
|
||||
return RelatesTo(rel_type=RelationType.REFERENCE, event_id=msg.mxid)
|
||||
return None
|
||||
|
||||
|
||||
async def _add_forward_header(source, text: str, html: Optional[str],
|
||||
fwd_from: MessageFwdHeader) -> Tuple[str, str]:
|
||||
if not html:
|
||||
html = escape(text)
|
||||
async def _add_forward_header(source: 'AbstractUser', content: TextMessageEventContent,
|
||||
fwd_from: MessageFwdHeader) -> None:
|
||||
if not content.formatted_body or content.format != Format.HTML:
|
||||
content.format = Format.HTML
|
||||
content.formatted_body = escape(content.body)
|
||||
fwd_from_html, fwd_from_text = None, None
|
||||
if fwd_from.from_id:
|
||||
user = u.User.get_by_tgid(TelegramID(fwd_from.from_id))
|
||||
@@ -106,64 +100,32 @@ async def _add_forward_header(source, text: str, html: Optional[str],
|
||||
fwd_from_text = "Unknown source"
|
||||
fwd_from_html = f"<b>{fwd_from_text}</b>"
|
||||
|
||||
text = "\n".join([f"> {line}" for line in text.split("\n")])
|
||||
text = f"Forwarded from {fwd_from_text}:\n{text}"
|
||||
html = (f"Forwarded message from {fwd_from_html}<br/>"
|
||||
f"<tg-forward><blockquote>{html}</blockquote></tg-forward>")
|
||||
return text, html
|
||||
content.body = "\n".join([f"> {line}" for line in content.body.split("\n")])
|
||||
content.body = f"Forwarded from {fwd_from_text}:\n{content.body}"
|
||||
content.formatted_body = (
|
||||
f"Forwarded message from {fwd_from_html}<br/>"
|
||||
f"<tg-forward><blockquote>{content.formatted_body}</blockquote></tg-forward>")
|
||||
|
||||
|
||||
async def _add_reply_header(source: "AbstractUser", text: str, html: str, evt: Message,
|
||||
relates_to: Dict, main_intent: IntentAPI) -> Tuple[str, str]:
|
||||
async def _add_reply_header(source: 'AbstractUser', content: TextMessageEventContent, evt: Message,
|
||||
main_intent: IntentAPI):
|
||||
space = (evt.to_id.channel_id
|
||||
if isinstance(evt, Message) and isinstance(evt.to_id, PeerChannel)
|
||||
else source.tgid)
|
||||
|
||||
msg = DBMessage.get_one_by_tgid(evt.reply_to_msg_id, space)
|
||||
if not msg:
|
||||
return text, html
|
||||
return
|
||||
|
||||
relates_to["rel_type"] = "m.reference"
|
||||
relates_to["event_id"] = msg.mxid
|
||||
relates_to["room_id"] = msg.mx_room
|
||||
relates_to["m.in_reply_to"] = {
|
||||
"event_id": msg.mxid,
|
||||
"room_id": msg.mx_room,
|
||||
}
|
||||
content.relates_to = RelatesTo(rel_type=RelationType.REFERENCE, event_id=msg.mxid)
|
||||
|
||||
try:
|
||||
event = await main_intent.get_event(msg.mx_room, msg.mxid)
|
||||
|
||||
content = event["content"]
|
||||
r_sender = event["sender"]
|
||||
|
||||
r_text_body = trim_reply_fallback_text(content["body"])
|
||||
r_html_body = trim_reply_fallback_html(content["formatted_body"]
|
||||
if "formatted_body" in content
|
||||
else escape(content["body"]))
|
||||
|
||||
puppet = pu.Puppet.get_by_mxid(r_sender, create=False)
|
||||
r_displayname = puppet.displayname if puppet else r_sender
|
||||
r_sender_link = f"<a href='https://matrix.to/#/{r_sender}'>{escape(r_displayname)}</a>"
|
||||
except (ValueError, KeyError, MatrixRequestError):
|
||||
r_sender_link = "unknown user"
|
||||
r_displayname = "unknown user"
|
||||
r_text_body = "Failed to fetch message"
|
||||
r_html_body = "<em>Failed to fetch message</em>"
|
||||
|
||||
r_msg_link = f"<a href='https://matrix.to/#/{msg.mx_room}/{msg.mxid}'>In reply to</a>"
|
||||
html = (
|
||||
f"<mx-reply><blockquote>{r_msg_link} {r_sender_link}\n{r_html_body}</blockquote></mx-reply>"
|
||||
+ (html or escape(text)))
|
||||
|
||||
lines = r_text_body.strip().split("\n")
|
||||
text_with_quote = f"> <{r_displayname}> {lines.pop(0)}"
|
||||
for line in lines:
|
||||
if line:
|
||||
text_with_quote += f"\n> {line}"
|
||||
text_with_quote += "\n\n"
|
||||
text_with_quote += text
|
||||
return text_with_quote, html
|
||||
event: MessageEvent = await main_intent.get_event(msg.mx_room, msg.mxid)
|
||||
if isinstance(event.content, TextMessageEventContent):
|
||||
event.content.trim_reply_fallback()
|
||||
content.set_reply(event)
|
||||
except MatrixRequestError:
|
||||
pass
|
||||
|
||||
|
||||
async def telegram_to_matrix(evt: Message, source: "AbstractUser",
|
||||
@@ -171,33 +133,43 @@ async def telegram_to_matrix(evt: Message, source: "AbstractUser",
|
||||
prefix_text: Optional[str] = None, prefix_html: Optional[str] = None,
|
||||
override_text: str = None,
|
||||
override_entities: List[TypeMessageEntity] = None,
|
||||
no_reply_fallback: bool = False) -> Tuple[str, str, Dict]:
|
||||
text = add_surrogates(override_text or evt.message)
|
||||
no_reply_fallback: bool = False) -> TextMessageEventContent:
|
||||
content = TextMessageEventContent(
|
||||
msgtype=MessageType.TEXT,
|
||||
body=add_surrogates(override_text or evt.message),
|
||||
)
|
||||
entities = override_entities or evt.entities
|
||||
html = _telegram_entities_to_matrix_catch(text, entities) if entities else None
|
||||
relates_to = {} # type: Dict
|
||||
if entities:
|
||||
content.format = Format.HTML
|
||||
content.formatted_body = _telegram_entities_to_matrix_catch(content.body, entities)
|
||||
|
||||
if prefix_html:
|
||||
html = prefix_html + (html or escape(text))
|
||||
if not content.formatted_body:
|
||||
content.format = Format.HTML
|
||||
content.formatted_body = escape(content.body)
|
||||
content.formatted_body = prefix_html + content.formatted_body
|
||||
if prefix_text:
|
||||
text = prefix_text + text
|
||||
content.body = prefix_text + content.body
|
||||
|
||||
if evt.fwd_from:
|
||||
text, html = await _add_forward_header(source, text, html, evt.fwd_from)
|
||||
await _add_forward_header(source, content, evt.fwd_from)
|
||||
|
||||
if evt.reply_to_msg_id and not no_reply_fallback:
|
||||
text, html = await _add_reply_header(source, text, html, evt, relates_to, main_intent)
|
||||
await _add_reply_header(source, content, evt, main_intent)
|
||||
|
||||
if isinstance(evt, Message) and evt.post and evt.post_author:
|
||||
if not html:
|
||||
html = escape(text)
|
||||
text += f"\n- {evt.post_author}"
|
||||
html += f"<br/><i>- <u>{evt.post_author}</u></i>"
|
||||
if not content.formatted_body:
|
||||
content.formatted_body = escape(content.body)
|
||||
content.body += f"\n- {evt.post_author}"
|
||||
content.formatted_body += f"<br/><i>- <u>{evt.post_author}</u></i>"
|
||||
|
||||
if html:
|
||||
html = html.replace("\n", "<br/>")
|
||||
if content.formatted_body:
|
||||
content.formatted_body = content.formatted_body.replace("\n", "<br/>")
|
||||
|
||||
return remove_surrogates(text), remove_surrogates(html), relates_to
|
||||
content.body = remove_surrogates(content.body)
|
||||
content.formatted_body = remove_surrogates(content.formatted_body)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def _telegram_entities_to_matrix_catch(text: str, entities: List[TypeMessageEntity]) -> str:
|
||||
|
||||
@@ -32,24 +32,3 @@ def remove_surrogates(text: Optional[str]) -> Optional[str]:
|
||||
if text is None:
|
||||
return None
|
||||
return text.encode("utf-16", "surrogatepass").decode("utf-16")
|
||||
|
||||
|
||||
# trim_reply_fallback_text, html_reply_fallback_regex and trim_reply_fallback_html are Matrix
|
||||
# reply fallback utility functions.
|
||||
# You may copy and use them under any OSI-approved license.
|
||||
def trim_reply_fallback_text(text: str) -> str:
|
||||
if not text.startswith("> ") or "\n" not in text:
|
||||
return text
|
||||
lines = text.split("\n")
|
||||
while len(lines) > 0 and lines[0].startswith("> "):
|
||||
lines.pop(0)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
html_reply_fallback_regex: Pattern = re.compile("^<mx-reply>"
|
||||
r"[\s\S]+?"
|
||||
"</mx-reply>")
|
||||
|
||||
|
||||
def trim_reply_fallback_html(html: str) -> str:
|
||||
return html_reply_fallback_regex.sub("", html)
|
||||
|
||||
+117
-135
@@ -13,7 +13,7 @@
|
||||
#
|
||||
# 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 (Awaitable, Dict, List, Optional, Pattern, Tuple, Union, Any, Deque, cast,
|
||||
from typing import (Awaitable, Dict, List, Optional, Pattern, Tuple, Union, Any, Deque,
|
||||
TYPE_CHECKING)
|
||||
from html import escape as escape_html
|
||||
from collections import deque
|
||||
@@ -64,14 +64,17 @@ from telethon.tl.types import (
|
||||
UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping, User, UserFull, MessageEntityPre,
|
||||
InputMediaUploadedDocument, InputPeerPhotoFileLocation)
|
||||
|
||||
from mautrix.errors import MatrixRequestError, IntentError
|
||||
from mautrix.errors import MatrixRequestError, IntentError, MForbidden
|
||||
from mautrix.appservice import AppService, IntentAPI
|
||||
from mautrix.types import EventID, RoomID, UserID, RoomCreatePreset, ContentURI, MessageType
|
||||
from mautrix.bridge import BasePortal
|
||||
from mautrix.types import (EventID, RoomID, UserID, RoomCreatePreset, ContentURI, MessageType,
|
||||
ImageInfo, ThumbnailInfo, EventType, PowerLevelStateEventContent,
|
||||
RoomAlias, TextMessageEventContent, Format)
|
||||
|
||||
from .types import TelegramID
|
||||
from .context import Context
|
||||
from .db import Portal as DBPortal, Message as DBMessage, TelegramFile as DBTelegramFile
|
||||
from .util import ignore_coro, sane_mimetypes
|
||||
from .util import sane_mimetypes
|
||||
from . import puppet as p, user as u, formatter, util
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -88,7 +91,7 @@ DedupMXID = Tuple[EventID, TelegramID]
|
||||
InviteList = Union[UserID, List[UserID]]
|
||||
|
||||
|
||||
class Portal:
|
||||
class Portal(BasePortal):
|
||||
base_log: logging.Logger = logging.getLogger("mau.portal")
|
||||
az: AppService = None
|
||||
bot: 'Bot' = None
|
||||
@@ -225,7 +228,7 @@ class Portal:
|
||||
# endregion
|
||||
# region Permission checks
|
||||
|
||||
async def can_user_perform(self, user: 'u.User', event: str, default: int = 50) -> bool:
|
||||
async def can_user_perform(self, user: 'u.User', event: str) -> bool:
|
||||
if user.is_admin:
|
||||
return True
|
||||
if not self.mxid:
|
||||
@@ -235,10 +238,9 @@ class Portal:
|
||||
await self.main_intent.get_power_levels(self.mxid)
|
||||
except MatrixRequestError:
|
||||
return False
|
||||
return self.main_intent.state_store.has_power_level(
|
||||
self.mxid, user.mxid,
|
||||
event=f"net.maunium.telegram.{event}",
|
||||
default=default)
|
||||
evt_type = EventType.find(f"net.maunium.telegram.{event}")
|
||||
evt_type.t_class = EventType.Class.STATE
|
||||
return self.main_intent.state_store.has_power_level(self.mxid, user.mxid, event=evt_type)
|
||||
|
||||
# endregion
|
||||
# region Deduplication
|
||||
@@ -340,7 +342,8 @@ class Portal:
|
||||
await self.main_intent.invite_user(self.mxid, users, check_cache=True)
|
||||
|
||||
async def update_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User],
|
||||
direct: bool, puppet: p.Puppet = None, levels: Dict = None,
|
||||
direct: bool, puppet: p.Puppet = None,
|
||||
levels: PowerLevelStateEventContent = None,
|
||||
users: List[User] = None,
|
||||
participants: List[TypeParticipant] = None) -> None:
|
||||
if not direct:
|
||||
@@ -368,7 +371,7 @@ class Portal:
|
||||
if synchronous:
|
||||
await update
|
||||
else:
|
||||
ignore_coro(asyncio.ensure_future(update, loop=self.loop))
|
||||
asyncio.ensure_future(update, loop=self.loop)
|
||||
await self.invite_to_matrix(invites or [])
|
||||
return self.mxid
|
||||
async with self._room_create_lock:
|
||||
@@ -388,7 +391,7 @@ class Portal:
|
||||
entity = await self.get_entity(user)
|
||||
self.log.debug("Fetched data: %s", entity)
|
||||
|
||||
self.log.debug(f"Creating room")
|
||||
self.log.debug("Creating room")
|
||||
|
||||
try:
|
||||
self.title = entity.title
|
||||
@@ -414,14 +417,14 @@ class Portal:
|
||||
# TODO? properly handle existing room aliases
|
||||
await self.main_intent.remove_room_alias(alias)
|
||||
|
||||
power_levels = self._get_base_power_levels({}, entity)
|
||||
power_levels = self._get_base_power_levels(entity=entity)
|
||||
users = participants = None
|
||||
if not direct:
|
||||
users, participants = await self._get_users(user, entity)
|
||||
self._participants_to_power_levels(participants, power_levels)
|
||||
initial_state = [{
|
||||
"type": "m.room.power_levels",
|
||||
"content": power_levels,
|
||||
"type": EventType.ROOM_POWER_LEVELS.serialize(),
|
||||
"content": power_levels.serialize(),
|
||||
}]
|
||||
if config["appservice.community_id"]:
|
||||
initial_state.append({
|
||||
@@ -440,62 +443,56 @@ class Portal:
|
||||
self.save()
|
||||
self.az.state_store.set_power_levels(self.mxid, power_levels)
|
||||
user.register_portal(self)
|
||||
ignore_coro(asyncio.ensure_future(self.update_matrix_room(user, entity, direct, puppet,
|
||||
levels=power_levels, users=users,
|
||||
participants=participants),
|
||||
loop=self.loop))
|
||||
asyncio.ensure_future(self.update_matrix_room(user, entity, direct, puppet,
|
||||
levels=power_levels, users=users,
|
||||
participants=participants), loop=self.loop)
|
||||
|
||||
return self.mxid
|
||||
|
||||
def _get_base_power_levels(self, levels: dict = None, entity: TypeChat = None) -> dict:
|
||||
levels = levels or {}
|
||||
def _get_base_power_levels(self, levels: PowerLevelStateEventContent = None,
|
||||
entity: TypeChat = None) -> PowerLevelStateEventContent:
|
||||
levels = levels or PowerLevelStateEventContent()
|
||||
if self.peer_type == "user":
|
||||
levels["ban"] = 100
|
||||
levels["kick"] = 100
|
||||
levels["invite"] = 100
|
||||
levels.setdefault("events", {})
|
||||
levels["events"]["m.room.name"] = 0
|
||||
levels["events"]["m.room.avatar"] = 0
|
||||
levels["events"]["m.room.topic"] = 0
|
||||
levels["state_default"] = 0
|
||||
levels["users_default"] = 0
|
||||
levels["events_default"] = 0
|
||||
levels.ban = 100
|
||||
levels.kick = 100
|
||||
levels.invite = 100
|
||||
levels.events[EventType.ROOM_NAME] = 0
|
||||
levels.events[EventType.ROOM_AVATAR] = 0
|
||||
levels.events[EventType.ROOM_TOPIC] = 0
|
||||
levels.state_default = 0
|
||||
levels.users_default = 0
|
||||
levels.events_default = 0
|
||||
else:
|
||||
dbr = entity.default_banned_rights
|
||||
if not dbr:
|
||||
self.log.debug(f"default_banned_rights is None in {entity}")
|
||||
dbr = ChatBannedRights(invite_users=True, change_info=True, pin_messages=True,
|
||||
send_stickers=False, send_messages=False, until_date=0)
|
||||
levels["ban"] = 99
|
||||
levels["kick"] = 50
|
||||
levels["invite"] = 50 if dbr.invite_users else 0
|
||||
levels.setdefault("events", {})
|
||||
levels["events"]["m.room.name"] = 50 if dbr.change_info else 0
|
||||
levels["events"]["m.room.avatar"] = 50 if dbr.change_info else 0
|
||||
levels["events"]["m.room.topic"] = 50 if dbr.change_info else 0
|
||||
levels["events"][
|
||||
"m.room.pinned_events"] = 50 if dbr.pin_messages else 0
|
||||
levels["events"]["m.room.power_levels"] = 75
|
||||
levels["events"]["m.room.history_visibility"] = 75
|
||||
levels["state_default"] = 50
|
||||
levels["users_default"] = 0
|
||||
levels["events_default"] = (50 if (self.peer_type == "channel" and not entity.megagroup
|
||||
or entity.default_banned_rights.send_messages)
|
||||
else 0)
|
||||
levels["events"]["m.sticker"] = 50 if dbr.send_stickers else levels["events_default"]
|
||||
if "users" not in levels:
|
||||
levels["users"] = {
|
||||
self.main_intent.mxid: 100
|
||||
}
|
||||
else:
|
||||
levels["users"][self.main_intent.mxid] = 100
|
||||
levels.ban = 99
|
||||
levels.kick = 50
|
||||
levels.invite = 50 if dbr.invite_users else 0
|
||||
levels.events[EventType.ROOM_ENCRYPTED] = 99
|
||||
levels.events[EventType.ROOM_TOMBSTONE] = 99
|
||||
levels.events[EventType.ROOM_NAME] = 50 if dbr.change_info else 0
|
||||
levels.events[EventType.ROOM_AVATAR] = 50 if dbr.change_info else 0
|
||||
levels.events[EventType.ROOM_TOPIC] = 50 if dbr.change_info else 0
|
||||
levels.events[EventType.ROOM_PINNED_EVENTS] = 50 if dbr.pin_messages else 0
|
||||
levels.events[EventType.ROOM_POWER_LEVELS] = 75
|
||||
levels.events[EventType.ROOM_HISTORY_VISIBILITY] = 75
|
||||
levels.state_default = 50
|
||||
levels.users_default = 0
|
||||
levels.events_default = (50 if (self.peer_type == "channel" and not entity.megagroup
|
||||
or entity.default_banned_rights.send_messages)
|
||||
else 0)
|
||||
levels.events[EventType.STICKER] = 50 if dbr.send_stickers else levels.events_default
|
||||
levels.users[self.main_intent.mxid] = 100
|
||||
return levels
|
||||
|
||||
@property
|
||||
def alias(self) -> Optional[str]:
|
||||
def alias(self) -> Optional[RoomAlias]:
|
||||
if not self.username:
|
||||
return None
|
||||
return f"#{self._get_alias_localpart()}:{self.hs_domain}"
|
||||
return RoomAlias(f"#{self._get_alias_localpart()}:{self.hs_domain}")
|
||||
|
||||
def _get_alias_localpart(self, username: Optional[str] = None) -> Optional[str]:
|
||||
username = username or self.username
|
||||
@@ -537,8 +534,7 @@ class Portal:
|
||||
and Portal.max_initial_member_sync == -1
|
||||
and (self.megagroup or self.peer_type != "channel"))
|
||||
if trust_member_list:
|
||||
joined_mxids = cast(List[UserID],
|
||||
await self.main_intent.get_room_members(self.mxid))
|
||||
joined_mxids = await self.main_intent.get_room_members(self.mxid)
|
||||
for user_mxid in joined_mxids:
|
||||
if user_mxid == self.az.bot_mxid:
|
||||
continue
|
||||
@@ -547,7 +543,7 @@ class Portal:
|
||||
if self.bot and puppet_id == self.bot.tgid:
|
||||
self.bot.remove_chat(self.tgid)
|
||||
await self.main_intent.kick_user(self.mxid, user_mxid,
|
||||
"User had left this Telegram chat.")
|
||||
"User had left this Telegram chat.")
|
||||
continue
|
||||
mx_user = u.User.get_by_mxid(user_mxid, create=False)
|
||||
if mx_user and mx_user.is_bot and mx_user.tgid not in allowed_tgids:
|
||||
@@ -555,14 +551,14 @@ class Portal:
|
||||
|
||||
if mx_user and not self.has_bot and mx_user.tgid not in allowed_tgids:
|
||||
await self.main_intent.kick_user(self.mxid, mx_user.mxid,
|
||||
"You had left this Telegram chat.")
|
||||
"You had left this Telegram chat.")
|
||||
continue
|
||||
|
||||
async def add_telegram_user(self, user_id: TelegramID, source: Optional['AbstractUser'] = None
|
||||
) -> None:
|
||||
puppet = p.Puppet.get(user_id)
|
||||
if source:
|
||||
entity = await source.client.get_entity(PeerUser(user_id)) # type: User
|
||||
entity: User = await source.client.get_entity(PeerUser(user_id))
|
||||
await puppet.update_info(source, entity)
|
||||
await puppet.intent.join_room(self.mxid)
|
||||
|
||||
@@ -577,12 +573,21 @@ class Portal:
|
||||
kick_message = (f"Kicked by {sender.displayname}"
|
||||
if sender and sender.tgid != puppet.tgid
|
||||
else "Left Telegram chat")
|
||||
if sender and sender.tgid != puppet.tgid:
|
||||
await self.main_intent.kick_user(self.mxid, puppet.mxid, kick_message)
|
||||
if sender.tgid != puppet.tgid:
|
||||
try:
|
||||
await sender.intent.kick_user(self.mxid, puppet.mxid)
|
||||
except MForbidden:
|
||||
await self.main_intent.kick_user(self.mxid, puppet.mxid, kick_message)
|
||||
else:
|
||||
await puppet.intent.leave_room(self.mxid)
|
||||
if user:
|
||||
user.unregister_portal(self)
|
||||
if sender.tgid != puppet.tgid:
|
||||
try:
|
||||
await sender.intent.kick_user(self.mxid, puppet.mxid)
|
||||
return
|
||||
except MForbidden:
|
||||
pass
|
||||
await self.main_intent.kick_user(self.mxid, user.mxid, kick_message)
|
||||
|
||||
async def update_info(self, user: 'AbstractUser', entity: TypeChat = None) -> None:
|
||||
@@ -706,7 +711,7 @@ class Portal:
|
||||
return False
|
||||
|
||||
async def _get_users(self, user: 'AbstractUser',
|
||||
entity: Union[TypeInputPeer, InputUser, TypeChat, TypeUser]
|
||||
entity: Union[TypeInputPeer, InputUser, TypeChat, TypeUser, InputChannel]
|
||||
) -> Tuple[List[TypeUser], List[TypeParticipant]]:
|
||||
if self.peer_type == "chat":
|
||||
chat = await user.client(GetFullChatRequest(chat_id=self.tgid))
|
||||
@@ -764,13 +769,13 @@ class Portal:
|
||||
members = await self.main_intent.get_room_members(self.mxid)
|
||||
except MatrixRequestError:
|
||||
return []
|
||||
authenticated = [] # type: List[u.User]
|
||||
authenticated: List[u.User] = []
|
||||
has_bot = self.has_bot
|
||||
for member_str in members:
|
||||
member = UserID(member_str)
|
||||
if p.Puppet.get_id_from_mxid(member) or member == self.main_intent.mxid:
|
||||
continue
|
||||
user = await u.User.get_by_mxid(member).ensure_started() # type: u.User
|
||||
user = await u.User.get_by_mxid(member).ensure_started()
|
||||
authenticated_through_bot = has_bot and user.relaybot_whitelisted
|
||||
if authenticated_through_bot or await user.has_full_access(allow_bot=True):
|
||||
authenticated.append(user)
|
||||
@@ -825,30 +830,28 @@ class Portal:
|
||||
return local
|
||||
return config[f"bridge.{key}"]
|
||||
|
||||
async def _get_state_change_message(self, event: str, user: 'u.User',
|
||||
arguments: Optional[Dict] = None) -> Optional[Dict]:
|
||||
async def _get_state_change_message(self, event: str, user: 'u.User', **kwargs: Any
|
||||
) -> Optional[str]:
|
||||
tpl = self.get_config(f"state_event_formats.{event}")
|
||||
if len(tpl) == 0:
|
||||
# Empty format means they don't want the message
|
||||
return None
|
||||
displayname = await self.get_displayname(user)
|
||||
|
||||
tpl_args = dict(mxid=user.mxid,
|
||||
username=user.mxid_localpart,
|
||||
displayname=escape_html(displayname))
|
||||
tpl_args = {**tpl_args, **(arguments or {})}
|
||||
message = Template(tpl).safe_substitute(tpl_args)
|
||||
return {
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": message,
|
||||
tpl_args = {
|
||||
"mxid": user.mxid,
|
||||
"username": user.mxid_localpart,
|
||||
"displayname": escape_html(displayname),
|
||||
**kwargs,
|
||||
}
|
||||
return Template(tpl).safe_substitute(tpl_args)
|
||||
|
||||
async def name_change_matrix(self, user: 'u.User', displayname: str, prev_displayname: str,
|
||||
event_id: EventID) -> None:
|
||||
async with self.require_send_lock(self.bot.tgid):
|
||||
message = await self._get_state_change_message(
|
||||
"name_change", user,
|
||||
dict(displayname=displayname, prev_displayname=prev_displayname))
|
||||
displayname=displayname, prev_displayname=prev_displayname)
|
||||
if not message:
|
||||
return
|
||||
response = await self.bot.client.send_message(
|
||||
@@ -858,7 +861,7 @@ class Portal:
|
||||
self.is_duplicate(response, (event_id, space))
|
||||
|
||||
async def get_displayname(self, user: 'u.User') -> str:
|
||||
# FIXME mautrix4
|
||||
# FIXME this doesn't seem to use cache in mautrix 0.4
|
||||
return (await self.main_intent.get_displayname(self.mxid, user.mxid)
|
||||
or user.mxid)
|
||||
|
||||
@@ -994,13 +997,15 @@ class Portal:
|
||||
await self._apply_msg_format(sender, msgtype, message["m.new_content"])
|
||||
|
||||
@staticmethod
|
||||
def _matrix_event_to_entities(event: Dict[str, Any]
|
||||
def _matrix_event_to_entities(event: Union[str, TextMessageEventContent]
|
||||
) -> Tuple[str, Optional[List[TypeMessageEntity]]]:
|
||||
try:
|
||||
if event.get("format", None) == "org.matrix.custom.html":
|
||||
message, entities = formatter.matrix_to_telegram(event.get("formatted_body", ""))
|
||||
if isinstance(event, str):
|
||||
message, entities = formatter.matrix_to_telegram(event)
|
||||
elif isinstance(event, TextMessageEventContent) and event.format == Format.HTML:
|
||||
message, entities = formatter.matrix_to_telegram(event.formatted_body)
|
||||
else:
|
||||
message, entities = formatter.matrix_text_to_telegram(event.get("body", ""))
|
||||
message, entities = formatter.matrix_text_to_telegram(event.body)
|
||||
except KeyError:
|
||||
message, entities = None, None
|
||||
return message, entities
|
||||
@@ -1403,8 +1408,7 @@ class Portal:
|
||||
self.bot.add_chat(self.tgid, self.peer_type)
|
||||
|
||||
levels = await self.main_intent.get_power_levels(self.mxid)
|
||||
bot_level = self._get_bot_level(levels)
|
||||
if bot_level == 100:
|
||||
if levels.get_user_level(self.main_intent.mxid) == 100:
|
||||
levels = self._get_base_power_levels(levels, entity)
|
||||
await self.main_intent.set_power_levels(self.mxid, levels)
|
||||
await self.handle_matrix_power_levels(source, levels["users"], {})
|
||||
@@ -1441,22 +1445,17 @@ class Portal:
|
||||
return None
|
||||
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(
|
||||
content = await formatter.telegram_to_matrix(
|
||||
evt, source, self.main_intent,
|
||||
prefix_html=f"<img src='{file.mxc}' alt='Inline Telegram photo'/><br/>",
|
||||
prefix_text="Inline image: ")
|
||||
content.external_url = self.get_external_url(evt)
|
||||
await intent.set_typing(self.mxid, is_typing=False)
|
||||
return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to,
|
||||
timestamp=evt.date,
|
||||
external_url=self.get_external_url(evt))
|
||||
info = {
|
||||
"h": largest_size.h,
|
||||
"w": largest_size.w,
|
||||
"size": len(largest_size.bytes) if (
|
||||
isinstance(largest_size, PhotoCachedSize)) else largest_size.size,
|
||||
"orientation": 0,
|
||||
"mimetype": file.mime_type,
|
||||
}
|
||||
return await intent.send_message(self.mxid, content, timestamp=evt.date)
|
||||
info = ImageInfo(
|
||||
height=largest_size.h, width=largest_size.w, orientation=0, mimetype=file.mime_type,
|
||||
size=(len(largest_size.bytes) if (isinstance(largest_size, PhotoCachedSize))
|
||||
else largest_size.size))
|
||||
name = f"image{sane_mimetypes.guess_extension(file.mime_type)}"
|
||||
await intent.set_typing(self.mxid, is_typing=False)
|
||||
result = await intent.send_image(self.mxid, file.mxc, info=info, text=name,
|
||||
@@ -1492,7 +1491,7 @@ class Portal:
|
||||
|
||||
@staticmethod
|
||||
def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: Dict,
|
||||
thumb_size: TypePhotoSize) -> Tuple[Dict, str]:
|
||||
thumb_size: TypePhotoSize) -> Tuple[ImageInfo, str]:
|
||||
document = evt.media.document
|
||||
name = evt.message or attrs["name"]
|
||||
if attrs["is_sticker"]:
|
||||
@@ -1508,26 +1507,21 @@ class Portal:
|
||||
mime_type = document.mime_type or file.mime_type
|
||||
else:
|
||||
mime_type = file.mime_type or document.mime_type
|
||||
info = {
|
||||
"size": file.size,
|
||||
"mimetype": mime_type,
|
||||
}
|
||||
info = ImageInfo(size=file.size, mimetype=mime_type)
|
||||
|
||||
if attrs["mime_type"] and not file.was_converted:
|
||||
file.mime_type = attrs["mime_type"] or file.mime_type
|
||||
if file.width and file.height:
|
||||
info["w"], info["h"] = file.width, file.height
|
||||
info.width, info.height = file.width, file.height
|
||||
elif attrs["width"] and attrs["height"]:
|
||||
info["w"], info["h"] = attrs["width"], attrs["height"]
|
||||
info.width, info.height = attrs["width"], attrs["height"]
|
||||
|
||||
if file.thumbnail:
|
||||
info["thumbnail_url"] = file.thumbnail.mxc
|
||||
info["thumbnail_info"] = {
|
||||
"mimetype": file.thumbnail.mime_type,
|
||||
"h": file.thumbnail.height or thumb_size.h,
|
||||
"w": file.thumbnail.width or thumb_size.w,
|
||||
"size": file.thumbnail.size,
|
||||
}
|
||||
info.thumbnail_url = file.thumbnail.mxc
|
||||
info.thumbnail_info = ThumbnailInfo(mimetype=file.thumbnail.mime_type,
|
||||
height=file.thumbnail.height or thumb_size.h,
|
||||
width=file.thumbnail.width or thumb_size.w,
|
||||
size=file.thumbnail.size)
|
||||
|
||||
return info, name
|
||||
|
||||
@@ -1948,7 +1942,7 @@ class Portal:
|
||||
await self.update_telegram_pin()
|
||||
|
||||
@staticmethod
|
||||
def _get_level_from_participant(participant: TypeParticipant, _: Dict) -> int:
|
||||
def _get_level_from_participant(participant: TypeParticipant) -> int:
|
||||
# TODO use the power level requirements to get better precision in channels
|
||||
if isinstance(participant, (ChatParticipantAdmin, ChannelParticipantAdmin)):
|
||||
return 50
|
||||
@@ -1957,28 +1951,16 @@ class Portal:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _participant_to_power_levels(levels: dict, user: Union['u.User', p.Puppet], new_level: int,
|
||||
def _participant_to_power_levels(levels: PowerLevelStateEventContent,
|
||||
user: Union['u.User', p.Puppet], new_level: int,
|
||||
bot_level: int) -> bool:
|
||||
new_level = min(new_level, bot_level)
|
||||
default_level = levels["users_default"] if "users_default" in levels else 0
|
||||
try:
|
||||
user_level = int(levels["users"][user.mxid])
|
||||
except (ValueError, KeyError):
|
||||
user_level = default_level
|
||||
user_level = levels.get_user_level(user.mxid)
|
||||
if user_level != new_level and user_level < bot_level:
|
||||
levels["users"][user.mxid] = new_level
|
||||
levels.users[user.mxid] = new_level
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_bot_level(self, levels: dict) -> int:
|
||||
try:
|
||||
return levels["users"][self.main_intent.mxid]
|
||||
except KeyError:
|
||||
try:
|
||||
return levels["users_default"]
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _get_powerlevel_level(levels: dict) -> int:
|
||||
try:
|
||||
@@ -1989,21 +1971,21 @@ class Portal:
|
||||
except KeyError:
|
||||
return 50
|
||||
|
||||
def _participants_to_power_levels(self, participants: List[TypeParticipant], levels: Dict
|
||||
) -> bool:
|
||||
bot_level = self._get_bot_level(levels)
|
||||
if bot_level < self._get_powerlevel_level(levels):
|
||||
def _participants_to_power_levels(self, participants: List[TypeParticipant],
|
||||
levels: PowerLevelStateEventContent) -> bool:
|
||||
bot_level = levels.get_user_level(self.main_intent.mxid)
|
||||
if bot_level < levels.get_event_level(EventType.ROOM_POWER_LEVELS):
|
||||
return False
|
||||
changed = False
|
||||
admin_power_level = min(75 if self.peer_type == "channel" else 50, bot_level)
|
||||
if levels["events"]["m.room.power_levels"] != admin_power_level:
|
||||
if levels.events[EventType.ROOM_POWER_LEVELS] != admin_power_level:
|
||||
changed = True
|
||||
levels["events"]["m.room.power_levels"] = admin_power_level
|
||||
levels.events[EventType.ROOM_POWER_LEVELS] = admin_power_level
|
||||
|
||||
for participant in participants:
|
||||
puppet = p.Puppet.get(TelegramID(participant.user_id))
|
||||
user = u.User.get_by_tgid(TelegramID(participant.user_id))
|
||||
new_level = self._get_level_from_participant(participant, levels)
|
||||
new_level = self._get_level_from_participant(participant)
|
||||
|
||||
if user:
|
||||
user.register_portal(self)
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
from asyncio import Future
|
||||
|
||||
from .file_transfer import transfer_file_to_matrix, convert_image
|
||||
from .format_duration import format_duration
|
||||
from .signed_token import sign_token, verify_token
|
||||
from .recursive_dict import recursive_del, recursive_set, recursive_get
|
||||
|
||||
|
||||
def ignore_coro(_: Future) -> None:
|
||||
pass
|
||||
|
||||
@@ -26,7 +26,7 @@ from telethon.errors import *
|
||||
from mautrix.bridge import OnlyLoginSelf, InvalidAccessToken
|
||||
|
||||
from ...commands.telegram.auth import enter_password
|
||||
from ...util import format_duration, ignore_coro
|
||||
from ...util import format_duration
|
||||
from ...puppet import Puppet
|
||||
from ...user import User
|
||||
|
||||
@@ -119,7 +119,7 @@ class AuthAPI(abc.ABC):
|
||||
existing_user = User.get_by_tgid(user_info.id)
|
||||
if existing_user and existing_user != user:
|
||||
await existing_user.log_out()
|
||||
ignore_coro(asyncio.ensure_future(user.post_login(user_info), loop=self.loop))
|
||||
asyncio.ensure_future(user.post_login(user_info), loop=self.loop)
|
||||
if user.command_status and user.command_status["action"] == "Login":
|
||||
user.command_status = None
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ from mautrix.types import UserID
|
||||
from ...types import TelegramID
|
||||
from ...user import User
|
||||
from ...portal import Portal
|
||||
from ...util import ignore_coro
|
||||
from ...commands.portal.util import user_has_power_level, get_initial_state
|
||||
from ..common import AuthAPI
|
||||
|
||||
@@ -188,9 +187,8 @@ class ProvisioningAPI(AuthAPI):
|
||||
portal.photo_id = ""
|
||||
portal.save()
|
||||
|
||||
ignore_coro(asyncio.ensure_future(portal.update_matrix_room(user, entity, direct,
|
||||
levels=levels),
|
||||
loop=self.loop))
|
||||
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels),
|
||||
loop=self.loop)
|
||||
|
||||
return web.Response(status=202, body="{}")
|
||||
|
||||
@@ -269,7 +267,8 @@ class ProvisioningAPI(AuthAPI):
|
||||
require_puppeting=False, require_user=False)
|
||||
if err is not None:
|
||||
return err
|
||||
elif user and not await user_has_power_level(portal.mxid, self.az.intent, user, "unbridge"):
|
||||
elif user and not await user_has_power_level(portal.mxid, self.az.intent, user,
|
||||
"unbridge"):
|
||||
return self.get_error_response(403, "not_enough_permissions",
|
||||
"You do not have the permissions to unbridge that room.")
|
||||
|
||||
@@ -284,7 +283,7 @@ class ProvisioningAPI(AuthAPI):
|
||||
self.log.exception("Failed to disconnect chat")
|
||||
return self.get_error_response(500, "exception", "Failed to disconnect chat")
|
||||
else:
|
||||
ignore_coro(asyncio.ensure_future(coro, loop=self.loop))
|
||||
asyncio.ensure_future(coro, loop=self.loop)
|
||||
return web.json_response({}, status=200 if sync else 202)
|
||||
|
||||
async def get_user_info(self, request: web.Request) -> web.Response:
|
||||
|
||||
Reference in New Issue
Block a user