Merge branch 'master' into rlottie
This commit is contained in:
+12
-1
@@ -170,6 +170,9 @@ bridge:
|
|||||||
# gifc - uses same utility as png, faster but without transparency at all
|
# 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)
|
# mp4 - video in mp4 container (ffmpeg binary and takes a lot of time, but less than gif)
|
||||||
animated_sticker_target_type: 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.
|
# Whether to bridge Telegram bot messages as m.notices or m.texts.
|
||||||
bot_messages_as_notices: true
|
bot_messages_as_notices: true
|
||||||
@@ -192,7 +195,6 @@ bridge:
|
|||||||
# You might need to increase this on high-traffic bridge instances.
|
# You might need to increase this on high-traffic bridge instances.
|
||||||
cache_queue_length: 20
|
cache_queue_length: 20
|
||||||
|
|
||||||
|
|
||||||
# The formats to use when sending messages to Telegram via the relay bot.
|
# The formats to use when sending messages to Telegram via the relay bot.
|
||||||
# Text msgtypes (m.text, m.notice and m.emote) support HTML, media msgtypes don't.
|
# 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.
|
# Options related to the message relay Telegram bot.
|
||||||
relaybot:
|
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.
|
# Whether or not to allow creating portals from Telegram.
|
||||||
authless_portals: true
|
authless_portals: true
|
||||||
# Whether or not to allow Telegram group admins to use the bot commands.
|
# Whether or not to allow Telegram group admins to use the bot commands.
|
||||||
|
|||||||
@@ -405,10 +405,15 @@ class AbstractUser(ABC):
|
|||||||
async def update_message(self, original_update: UpdateMessage) -> None:
|
async def update_message(self, original_update: UpdateMessage) -> None:
|
||||||
update, sender, portal = self.get_message_details(original_update)
|
update, sender, portal = self.get_message_details(original_update)
|
||||||
|
|
||||||
if self.is_bot and not portal.mxid:
|
if self.is_bot:
|
||||||
self.log.debug(f"Ignoring message received by bot in unbridged chat %s",
|
if update.is_private:
|
||||||
portal.tgid_log)
|
if not config["bridge.relaybot.private_chat.invite"]:
|
||||||
return
|
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:
|
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)
|
self.log.debug(f"Ignoring relaybot-sent message %s to %s", update, portal.tgid_log)
|
||||||
|
|||||||
+16
-4
@@ -19,7 +19,7 @@ import logging
|
|||||||
from telethon.tl.patched import Message, MessageService
|
from telethon.tl.patched import Message, MessageService
|
||||||
from telethon.tl.types import (
|
from telethon.tl.types import (
|
||||||
ChannelParticipantAdmin, ChannelParticipantCreator, ChatForbidden, ChatParticipantAdmin,
|
ChannelParticipantAdmin, ChannelParticipantCreator, ChatForbidden, ChatParticipantAdmin,
|
||||||
ChatParticipantCreator, InputChannel, InputUser, MessageActionChatAddUser,
|
ChatParticipantCreator, InputChannel, InputUser, MessageActionChatAddUser, PeerUser,
|
||||||
MessageActionChatDeleteUser, MessageEntityBotCommand, PeerChannel, PeerChat, TypePeer,
|
MessageActionChatDeleteUser, MessageEntityBotCommand, PeerChannel, PeerChat, TypePeer,
|
||||||
UpdateNewChannelMessage, UpdateNewMessage, MessageActionChatMigrateTo, User)
|
UpdateNewChannelMessage, UpdateNewMessage, MessageActionChatMigrateTo, User)
|
||||||
from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest
|
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.
|
# chat is a normal group or a supergroup/channel when using the ID.
|
||||||
if isinstance(message.to_id, PeerChannel):
|
if isinstance(message.to_id, PeerChannel):
|
||||||
return reply(f"-100{message.to_id.channel_id}")
|
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:
|
def match_command(self, text: str, command: str) -> bool:
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
@@ -223,13 +228,20 @@ class Bot(AbstractUser):
|
|||||||
|
|
||||||
async def handle_command(self, message: Message) -> None:
|
async def handle_command(self, message: Message) -> None:
|
||||||
def reply(reply_text: str) -> Awaitable[Message]:
|
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
|
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)
|
await self.handle_command_id(message, reply)
|
||||||
return
|
return
|
||||||
|
elif message.is_private:
|
||||||
|
return
|
||||||
|
|
||||||
portal = po.Portal.get_by_entity(message.to_id)
|
portal = po.Portal.get_by_entity(message.to_id)
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.image_as_file_size")
|
copy("bridge.image_as_file_size")
|
||||||
copy("bridge.max_document_size")
|
copy("bridge.max_document_size")
|
||||||
copy("bridge.animated_sticker_target_type")
|
copy("bridge.animated_sticker_target_type")
|
||||||
|
copy("bridge.federate_rooms")
|
||||||
|
|
||||||
copy("bridge.bot_messages_as_notices")
|
copy("bridge.bot_messages_as_notices")
|
||||||
if isinstance(self["bridge.bridge_notices"], bool):
|
if isinstance(self["bridge.bridge_notices"], bool):
|
||||||
@@ -145,6 +146,9 @@ class Config(BaseBridgeConfig):
|
|||||||
if "bridge.relaybot" not in self:
|
if "bridge.relaybot" not in self:
|
||||||
copy("bridge.authless_relaybot_portals", "bridge.relaybot.authless_portals")
|
copy("bridge.authless_relaybot_portals", "bridge.relaybot.authless_portals")
|
||||||
else:
|
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.authless_portals")
|
||||||
copy("bridge.relaybot.whitelist_group_admins")
|
copy("bridge.relaybot.whitelist_group_admins")
|
||||||
copy("bridge.relaybot.whitelist")
|
copy("bridge.relaybot.whitelist")
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
portal = po.Portal.get_by_mxid(room_id)
|
portal = po.Portal.get_by_mxid(room_id)
|
||||||
if user and await user.has_full_access(allow_bot=True) and portal:
|
if user and await user.has_full_access(allow_bot=True) and portal:
|
||||||
await portal.invite_telegram(inviter, user)
|
await portal.invite_telegram(inviter, user)
|
||||||
return
|
|
||||||
|
|
||||||
async def handle_join(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None:
|
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()
|
user = await u.User.get_by_mxid(user_id).ensure_started()
|
||||||
@@ -188,6 +187,30 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
else:
|
else:
|
||||||
await portal.leave_matrix(user, event_id)
|
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
|
@staticmethod
|
||||||
async def allow_message(user: 'u.User') -> bool:
|
async def allow_message(user: 'u.User') -> bool:
|
||||||
return user.relaybot_whitelisted
|
return user.relaybot_whitelisted
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ class BasePortal(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def has_bot(self) -> bool:
|
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
|
@property
|
||||||
def main_intent(self) -> IntentAPI:
|
def main_intent(self) -> IntentAPI:
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
|
|||||||
**kwargs: Any) -> None:
|
**kwargs: Any) -> None:
|
||||||
if not self.has_bot:
|
if not self.has_bot:
|
||||||
return
|
return
|
||||||
|
elif self.peer_type == "user" and not config["bridge.relaybot.private_chat.state_changes"]:
|
||||||
|
return
|
||||||
async with self.send_lock(self.bot.tgid):
|
async with self.send_lock(self.bot.tgid):
|
||||||
message = await self._get_state_change_message(event, user, **kwargs)
|
message = await self._get_state_change_message(event, user, **kwargs)
|
||||||
if not message:
|
if not message:
|
||||||
@@ -124,27 +126,33 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
|
|||||||
await user.client.send_read_acknowledge(self.peer, max_id=message.tgid,
|
await user.client.send_read_acknowledge(self.peer, max_id=message.tgid,
|
||||||
clear_mentions=True)
|
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:
|
if user.tgid == source.tgid:
|
||||||
return
|
return None
|
||||||
if self.peer_type == "user" and user.tgid == self.tgid:
|
if self.peer_type == "user" and user.tgid == self.tgid:
|
||||||
self.delete()
|
self.delete()
|
||||||
try:
|
return None
|
||||||
del self.by_tgid[self.tgid_full]
|
|
||||||
del self.by_mxid[self.mxid]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
if isinstance(user, u.User) and await user.needs_relaybot(self):
|
if isinstance(user, u.User) and await user.needs_relaybot(self):
|
||||||
if not self.bot:
|
if not self.bot:
|
||||||
return
|
return None
|
||||||
# TODO kick and ban message
|
# TODO kick message
|
||||||
return
|
return None
|
||||||
if await source.needs_relaybot(self):
|
if await source.needs_relaybot(self):
|
||||||
if not self.has_bot:
|
if not self.has_bot:
|
||||||
return
|
return None
|
||||||
source = self.bot
|
return self.bot
|
||||||
await source.client.kick_participant(self.peer, user.peer)
|
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:
|
async def leave_matrix(self, user: 'u.User', event_id: EventID) -> None:
|
||||||
if await user.needs_relaybot(self):
|
if await user.needs_relaybot(self):
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ class PortalMetadata(BasePortal, ABC):
|
|||||||
AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0))
|
AddChatUserRequest(chat_id=self.tgid, user_id=puppet.tgid, fwd_limit=0))
|
||||||
elif self.peer_type == "channel":
|
elif self.peer_type == "channel":
|
||||||
await source.client(InviteToChannelRequest(channel=self.peer, users=[puppet.tgid]))
|
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")
|
raise ValueError("Invalid peer type for Telegram user invite")
|
||||||
|
|
||||||
async def sync_matrix_members(self) -> None:
|
async def sync_matrix_members(self) -> None:
|
||||||
@@ -293,6 +294,11 @@ class PortalMetadata(BasePortal, ABC):
|
|||||||
if not direct:
|
if not direct:
|
||||||
users, participants = await self._get_users(user, entity)
|
users, participants = await self._get_users(user, entity)
|
||||||
self._participants_to_power_levels(participants, power_levels)
|
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 = [{
|
initial_state = [{
|
||||||
"type": EventType.ROOM_POWER_LEVELS.serialize(),
|
"type": EventType.ROOM_POWER_LEVELS.serialize(),
|
||||||
"content": power_levels.serialize(),
|
"content": power_levels.serialize(),
|
||||||
@@ -302,11 +308,15 @@ class PortalMetadata(BasePortal, ABC):
|
|||||||
"type": "m.room.related_groups",
|
"type": "m.room.related_groups",
|
||||||
"content": {"groups": [config["appservice.community_id"]]},
|
"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,
|
room_id = await self.main_intent.create_room(alias_localpart=alias, preset=preset,
|
||||||
is_direct=direct, invitees=invites or [],
|
is_direct=direct, invitees=invites or [],
|
||||||
name=self.title, topic=self.about,
|
name=self.title, topic=self.about,
|
||||||
initial_state=initial_state)
|
initial_state=initial_state,
|
||||||
|
creation_content=creation_content)
|
||||||
if not room_id:
|
if not room_id:
|
||||||
raise Exception(f"Failed to create room")
|
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}")
|
self.log.debug(f"default_banned_rights is None in {entity}")
|
||||||
dbr = ChatBannedRights(invite_users=True, change_info=True, pin_messages=True,
|
dbr = ChatBannedRights(invite_users=True, change_info=True, pin_messages=True,
|
||||||
send_stickers=False, send_messages=False, until_date=None)
|
send_stickers=False, send_messages=False, until_date=None)
|
||||||
levels.ban = 99
|
levels.ban = 50
|
||||||
levels.kick = 50
|
levels.kick = 50
|
||||||
levels.redact = 50
|
levels.redact = 50
|
||||||
levels.invite = 50 if dbr.invite_users else 0
|
levels.invite = 50 if dbr.invite_users else 0
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ setuptools.setup(
|
|||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"aiohttp>=3.0.1,<4",
|
"aiohttp>=3.0.1,<4",
|
||||||
"mautrix>=0.4.0.dev70,<0.5",
|
"mautrix>=0.4.0.dev71,<0.5",
|
||||||
"SQLAlchemy>=1.2.3,<2",
|
"SQLAlchemy>=1.2.3,<2",
|
||||||
"alembic>=1.0.0,<2",
|
"alembic>=1.0.0,<2",
|
||||||
"commonmark>=0.8.1,<0.10",
|
"commonmark>=0.8.1,<0.10",
|
||||||
|
|||||||
Reference in New Issue
Block a user