diff --git a/example-config.yaml b/example-config.yaml index 115d2298..fbca058b 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -170,6 +170,9 @@ bridge: # gifc - uses same utility as png, faster but without transparency at all # mp4 - video in mp4 container (ffmpeg binary and takes a lot of time, but less than gif) animated_sticker_target_type: gif + # Whether or not created rooms should have federation enabled. + # If false, created portal rooms will never be federated. + federate_rooms: true # Whether to bridge Telegram bot messages as m.notices or m.texts. bot_messages_as_notices: true @@ -192,7 +195,6 @@ bridge: # You might need to increase this on high-traffic bridge instances. cache_queue_length: 20 - # The formats to use when sending messages to Telegram via the relay bot. # Text msgtypes (m.text, m.notice and m.emote) support HTML, media msgtypes don't. # @@ -265,6 +267,15 @@ bridge: # Options related to the message relay Telegram bot. relaybot: + private_chat: + # List of users to invite to the portal when someone starts a private chat with the bot. + # If empty, private chats with the bot won't create a portal. + invite: [] + # Whether or not to bridge state change messages in relaybot private chats. + state_changes: true + # When private_chat_invite is empty, this message is sent to users /starting the + # relaybot. Telegram's "markdown" is supported. + message: This is a Matrix bridge relaybot and does not support direct chats # Whether or not to allow creating portals from Telegram. authless_portals: true # Whether or not to allow Telegram group admins to use the bot commands. diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index ca4da7cd..a40badc9 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -405,10 +405,15 @@ class AbstractUser(ABC): async def update_message(self, original_update: UpdateMessage) -> None: update, sender, portal = self.get_message_details(original_update) - if self.is_bot and not portal.mxid: - self.log.debug(f"Ignoring message received by bot in unbridged chat %s", - portal.tgid_log) - return + if self.is_bot: + if update.is_private: + if not config["bridge.relaybot.private_chat.invite"]: + self.log.debug(f"Ignoring private message to bot from {sender.id}") + return + elif not portal.mxid: + self.log.debug( + f"Ignoring message received by bot in unbridged chat {portal.tgid_log}") + return if self.ignore_incoming_bot_events and self.relaybot and sender.id == self.relaybot.tgid: self.log.debug(f"Ignoring relaybot-sent message %s to %s", update, portal.tgid_log) diff --git a/mautrix_telegram/bot.py b/mautrix_telegram/bot.py index 9002a13d..ca0ac393 100644 --- a/mautrix_telegram/bot.py +++ b/mautrix_telegram/bot.py @@ -19,7 +19,7 @@ import logging from telethon.tl.patched import Message, MessageService from telethon.tl.types import ( ChannelParticipantAdmin, ChannelParticipantCreator, ChatForbidden, ChatParticipantAdmin, - ChatParticipantCreator, InputChannel, InputUser, MessageActionChatAddUser, + ChatParticipantCreator, InputChannel, InputUser, MessageActionChatAddUser, PeerUser, MessageActionChatDeleteUser, MessageEntityBotCommand, PeerChannel, PeerChat, TypePeer, UpdateNewChannelMessage, UpdateNewMessage, MessageActionChatMigrateTo, User) from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest @@ -204,7 +204,12 @@ class Bot(AbstractUser): # chat is a normal group or a supergroup/channel when using the ID. if isinstance(message.to_id, PeerChannel): return reply(f"-100{message.to_id.channel_id}") - return reply(str(-message.to_id.chat_id)) + elif isinstance(message.to_id, PeerChat): + return reply(str(-message.to_id.chat_id)) + elif isinstance(message.to_id, PeerUser): + return reply(f"Your user ID is {message.from_id}.") + else: + return reply("Failed to find chat ID.") def match_command(self, text: str, command: str) -> bool: text = text.lower() @@ -223,13 +228,20 @@ class Bot(AbstractUser): async def handle_command(self, message: Message) -> None: def reply(reply_text: str) -> Awaitable[Message]: - return self.client.send_message(message.to_id, reply_text, reply_to=message.id) + return self.client.send_message(message.chat_id, reply_text, reply_to=message.id) text = message.message - if self.match_command(text, "id"): + if self.match_command(text, "start"): + pcm = config["bridge.relaybot.private_chat.message"] + if pcm: + await reply(pcm) + return + elif self.match_command(text, "id"): await self.handle_command_id(message, reply) return + elif message.is_private: + return portal = po.Portal.get_by_entity(message.to_id) diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 3d4cf621..7361bfbf 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -102,6 +102,7 @@ class Config(BaseBridgeConfig): copy("bridge.image_as_file_size") copy("bridge.max_document_size") copy("bridge.animated_sticker_target_type") + copy("bridge.federate_rooms") copy("bridge.bot_messages_as_notices") if isinstance(self["bridge.bridge_notices"], bool): @@ -145,6 +146,9 @@ class Config(BaseBridgeConfig): if "bridge.relaybot" not in self: copy("bridge.authless_relaybot_portals", "bridge.relaybot.authless_portals") else: + copy("bridge.relaybot.private_chat.invite") + copy("bridge.relaybot.private_chat.state_changes") + copy("bridge.relaybot.private_chat.message") copy("bridge.relaybot.authless_portals") copy("bridge.relaybot.whitelist_group_admins") copy("bridge.relaybot.whitelist") diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 2a77c705..b7d658d2 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -138,7 +138,6 @@ class MatrixHandler(BaseMatrixHandler): portal = po.Portal.get_by_mxid(room_id) if user and await user.has_full_access(allow_bot=True) and portal: await portal.invite_telegram(inviter, user) - return async def handle_join(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None: user = await u.User.get_by_mxid(user_id).ensure_started() @@ -188,6 +187,30 @@ class MatrixHandler(BaseMatrixHandler): else: await portal.leave_matrix(user, event_id) + async def handle_ban(self, room_id: RoomID, user_id: UserID, banned_by: UserID, reason: str, + event_id: EventID) -> None: + self.log.debug(f"{user_id} was banned from {room_id} by {banned_by} for {reason}") + + sender = u.User.get_by_mxid(banned_by, create=False) + if not sender: + return + await sender.ensure_started() + + portal = po.Portal.get_by_mxid(room_id) + if not portal: + return + + puppet = pu.Puppet.get_by_mxid(user_id) + if puppet: + await portal.ban_matrix(puppet, sender) + return + + user = u.User.get_by_mxid(user_id, create=False) + if not user: + return + await user.ensure_started() + await portal.ban_matrix(user, sender) + @staticmethod async def allow_message(user: 'u.User') -> bool: return user.relaybot_whitelisted diff --git a/mautrix_telegram/portal/base.py b/mautrix_telegram/portal/base.py index fd4d3374..52a98edb 100644 --- a/mautrix_telegram/portal/base.py +++ b/mautrix_telegram/portal/base.py @@ -147,7 +147,8 @@ class BasePortal(ABC): @property def has_bot(self) -> bool: - return bool(self.bot and self.bot.is_in_chat(self.tgid)) + return ((bool(self.bot) and self.bot.is_in_chat(self.tgid)) + or (self.peer_type == "user" and self.tg_receiver == self.bot.tgid)) @property def main_intent(self) -> IntentAPI: diff --git a/mautrix_telegram/portal/matrix.py b/mautrix_telegram/portal/matrix.py index 5dfbe7a7..32730da3 100644 --- a/mautrix_telegram/portal/matrix.py +++ b/mautrix_telegram/portal/matrix.py @@ -90,6 +90,8 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): **kwargs: Any) -> None: if not self.has_bot: return + elif self.peer_type == "user" and not config["bridge.relaybot.private_chat.state_changes"]: + return async with self.send_lock(self.bot.tgid): message = await self._get_state_change_message(event, user, **kwargs) if not message: @@ -124,27 +126,33 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC): await user.client.send_read_acknowledge(self.peer, max_id=message.tgid, clear_mentions=True) - async def kick_matrix(self, user: Union['u.User', 'p.Puppet'], source: 'u.User') -> None: + async def _preproc_kick_ban(self, user: Union['u.User', 'p.Puppet'], source: 'u.User' + ) -> Optional['AbstractUser']: if user.tgid == source.tgid: - return + return None if self.peer_type == "user" and user.tgid == self.tgid: self.delete() - try: - del self.by_tgid[self.tgid_full] - del self.by_mxid[self.mxid] - except KeyError: - pass - return + return None if isinstance(user, u.User) and await user.needs_relaybot(self): if not self.bot: - return - # TODO kick and ban message - return + return None + # TODO kick message + return None if await source.needs_relaybot(self): if not self.has_bot: - return - source = self.bot - await source.client.kick_participant(self.peer, user.peer) + return None + return self.bot + return source + + async def kick_matrix(self, user: Union['u.User', 'p.Puppet'], source: 'u.User') -> None: + source = await self._preproc_kick_ban(user, source) + if source is not None: + await source.client.kick_participant(self.peer, user.peer) + + async def ban_matrix(self, user: Union['u.User', 'p.Puppet'], source: 'u.User'): + source = await self._preproc_kick_ban(user, source) + if source is not None: + await source.client.edit_permissions(self.peer, user.peer, view_messages=False) async def leave_matrix(self, user: 'u.User', event_id: EventID) -> None: if await user.needs_relaybot(self): diff --git a/mautrix_telegram/portal/metadata.py b/mautrix_telegram/portal/metadata.py index df1b8102..b9a833ca 100644 --- a/mautrix_telegram/portal/metadata.py +++ b/mautrix_telegram/portal/metadata.py @@ -164,7 +164,8 @@ class PortalMetadata(BasePortal, ABC): AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0)) elif self.peer_type == "channel": await source.client(InviteToChannelRequest(channel=self.peer, users=[puppet.tgid])) - else: + # We don't care if there are invites for private chat portals with the relaybot. + elif not self.bot or self.tg_receiver != self.bot.tgid: raise ValueError("Invalid peer type for Telegram user invite") async def sync_matrix_members(self) -> None: @@ -293,6 +294,11 @@ class PortalMetadata(BasePortal, ABC): if not direct: users, participants = await self._get_users(user, entity) self._participants_to_power_levels(participants, power_levels) + elif self.tg_receiver == self.bot.tgid: + invites = config["bridge.relaybot.private_chat.invite"] + for invite in invites: + power_levels.users[invite] = 100 + self.title = puppet.displayname initial_state = [{ "type": EventType.ROOM_POWER_LEVELS.serialize(), "content": power_levels.serialize(), @@ -302,11 +308,15 @@ class PortalMetadata(BasePortal, ABC): "type": "m.room.related_groups", "content": {"groups": [config["appservice.community_id"]]}, }) + creation_content = {} + if not config["bridge.federate_rooms"]: + creation_content["m.federate"] = False room_id = await self.main_intent.create_room(alias_localpart=alias, preset=preset, is_direct=direct, invitees=invites or [], name=self.title, topic=self.about, - initial_state=initial_state) + initial_state=initial_state, + creation_content=creation_content) if not room_id: raise Exception(f"Failed to create room") @@ -341,7 +351,7 @@ class PortalMetadata(BasePortal, ABC): 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=None) - levels.ban = 99 + levels.ban = 50 levels.kick = 50 levels.redact = 50 levels.invite = 50 if dbr.invite_users else 0 diff --git a/setup.py b/setup.py index 99574655..51ee2ae8 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setuptools.setup( install_requires=[ "aiohttp>=3.0.1,<4", - "mautrix>=0.4.0.dev70,<0.5", + "mautrix>=0.4.0.dev71,<0.5", "SQLAlchemy>=1.2.3,<2", "alembic>=1.0.0,<2", "commonmark>=0.8.1,<0.10",