Merge pull request #79 from tulir/authless-relaybot-portals
Allow creating relaybot portals without any authenticated users
This commit is contained in:
+20
-10
@@ -87,20 +87,26 @@ bridge:
|
||||
# Allow logging in within Matrix. If false, the only way to log in is using the out-of-Matrix
|
||||
# login website (see appservice.public config section)
|
||||
allow_matrix_login: true
|
||||
# Whether or not to allow creating portals from Telegram.
|
||||
authless_relaybot_portals: true
|
||||
|
||||
# The prefix for commands. Only required in non-management rooms.
|
||||
command_prefix: "!tg"
|
||||
|
||||
# Whitelist of user IDs that are allowed to use this bridge. Leave empty to disable.
|
||||
# You can enter a domain without the localpart to allow all users from that homeserver to use the bridge.
|
||||
whitelist:
|
||||
- "internal.example.com"
|
||||
- "@user:public.example.com"
|
||||
|
||||
# Admins can do things like delete portal rooms. Here you must specify the exact MXID, domains
|
||||
# are not accepted.
|
||||
admins:
|
||||
- "@admin:internal.example.com"
|
||||
# Permissions for using the bridge.
|
||||
# Permitted values:
|
||||
# relaybot - Only use the bridge via the relaybot, no access to commands.
|
||||
# full - Full access to use the bridge via relaybot or logging in with Telegram account.
|
||||
# admin - Full access to use the bridge and some extra administration commands.
|
||||
# Permitted keys:
|
||||
# * - All Matrix users
|
||||
# domain - All users on that homeserver
|
||||
# mxid - Specific user
|
||||
permissions:
|
||||
"*": "relaybot"
|
||||
"example.com": "full"
|
||||
"public.example.com": "full"
|
||||
"@admin:example.com": "admin"
|
||||
|
||||
# Telegram config
|
||||
telegram:
|
||||
@@ -109,3 +115,7 @@ telegram:
|
||||
api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz
|
||||
# (Optional) Create your own bot at https://t.me/BotFather
|
||||
#bot_token: 123456789:ABCD-QBPd3VrWRhg623xYh07WUWErYA9eMI
|
||||
|
||||
# The version of the config. The bridge will read this and automatically update the config if
|
||||
# the schema has changed. For the latest version, check the example config.
|
||||
version: 1
|
||||
|
||||
@@ -56,6 +56,7 @@ args = parser.parse_args()
|
||||
|
||||
config = Config(args.config, args.registration)
|
||||
config.load()
|
||||
config.check_updates()
|
||||
|
||||
if args.generate_registration:
|
||||
config.generate_registration()
|
||||
|
||||
@@ -88,6 +88,8 @@ class AbstractUser:
|
||||
return self.logged_in and self.whitelisted
|
||||
|
||||
async def start(self):
|
||||
if not self.client:
|
||||
self._init_client()
|
||||
self.connected = await self.client.connect()
|
||||
|
||||
async def ensure_started(self, even_if_no_session=False):
|
||||
|
||||
+56
-2
@@ -15,6 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import re
|
||||
|
||||
from telethon.tl.types import *
|
||||
from telethon.errors import ChannelInvalidError, ChannelPrivateError
|
||||
@@ -23,18 +24,20 @@ from telethon.tl.functions.channels import GetChannelsRequest
|
||||
|
||||
from .abstract_user import AbstractUser
|
||||
from .db import BotChat
|
||||
from . import puppet as pu
|
||||
from . import puppet as pu, portal as po, user as u
|
||||
|
||||
config = None
|
||||
|
||||
|
||||
class Bot(AbstractUser):
|
||||
log = logging.getLogger("mau.bot")
|
||||
mxid_regex = re.compile("@.+:.+")
|
||||
|
||||
def __init__(self, token):
|
||||
super().__init__()
|
||||
self.token = token
|
||||
self.whitelisted = True
|
||||
self.username = None
|
||||
self._init_client()
|
||||
self.chats = {chat.id: chat.type for chat in BotChat.query.all()}
|
||||
|
||||
@@ -48,6 +51,7 @@ class Bot(AbstractUser):
|
||||
async def post_login(self):
|
||||
info = await self.client.get_me()
|
||||
self.tgid = info.id
|
||||
self.username = info.username
|
||||
self.mxid = pu.Puppet.get_mxid_from_id(self.tgid)
|
||||
|
||||
chat_ids = [id for id, type in self.chats.items() if type == "chat"]
|
||||
@@ -85,10 +89,60 @@ class Bot(AbstractUser):
|
||||
self.db.delete(BotChat.query.get(id))
|
||||
self.db.commit()
|
||||
|
||||
async def handle_command_portal(self, portal, reply):
|
||||
if not config["bridge.authless_relaybot_portals"]:
|
||||
return await reply("This bridge doesn't allow portal creation/invites from Telegram.")
|
||||
|
||||
await portal.create_matrix_room(self)
|
||||
if portal.mxid:
|
||||
if portal.username:
|
||||
return await reply(
|
||||
f"Portal is public: [{portal.alias}](https://matrix.to/#/{portal.alias})")
|
||||
else:
|
||||
return await reply(
|
||||
"Portal is not public. Use `/invite <mxid>` to get an invite.")
|
||||
|
||||
async def handle_command_invite(self, portal, reply, mxid):
|
||||
if len(mxid) == 0:
|
||||
return await reply("Usage: `/invite <mxid>`")
|
||||
elif not portal.mxid:
|
||||
return await reply("Portal does not have Matrix room. "
|
||||
"Create one with /portal first.")
|
||||
if not self.mxid_regex.match(mxid):
|
||||
return await reply("That doesn't look like a Matrix ID.")
|
||||
user = await u.User.get_by_mxid(mxid).ensure_started()
|
||||
if not user.whitelisted:
|
||||
return await reply("That user is not whitelisted to use the bridge.")
|
||||
elif user.logged_in:
|
||||
displayname = f"@{user.username}" if user.username else user.displayname
|
||||
return await reply("That user seems to be logged in. "
|
||||
f"Just invite [{displayname}](tg://user?id={user.tgid})")
|
||||
else:
|
||||
await portal.main_intent.invite(portal.mxid, user.mxid)
|
||||
return await reply(f"Invited `{user.mxid}` to the portal.")
|
||||
|
||||
async def handle_command(self, message):
|
||||
def reply(reply_text):
|
||||
return self.client.send_message_super(message.to_id, reply_text)
|
||||
|
||||
text = message.message
|
||||
portal = po.Portal.get_by_entity(message.to_id)
|
||||
if text == "/portal":
|
||||
await self.handle_command_portal(portal, reply)
|
||||
elif text.startswith("/invite"):
|
||||
await self.handle_command_invite(portal, reply, mxid=text[len("/invite "):])
|
||||
|
||||
async def update(self, update):
|
||||
if not isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
|
||||
return
|
||||
elif not isinstance(update.message, MessageService):
|
||||
|
||||
is_command = (isinstance(update.message, Message)
|
||||
and update.message.entities and len(update.message.entities) > 0
|
||||
and isinstance(update.message.entities[0], MessageEntityBotCommand))
|
||||
if is_command:
|
||||
return await self.handle_command(update.message)
|
||||
|
||||
if not isinstance(update.message, MessageService):
|
||||
return
|
||||
|
||||
to_id = update.message.to_id
|
||||
|
||||
+104
-4
@@ -15,20 +15,22 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
import random
|
||||
import string
|
||||
|
||||
yaml = YAML()
|
||||
yaml.indent(4)
|
||||
|
||||
|
||||
class DictWithRecursion:
|
||||
def __init__(self, data=None):
|
||||
self._data = data or {}
|
||||
self._data = data or CommentedMap()
|
||||
|
||||
def _recursive_get(self, data, key, default_value):
|
||||
if '.' in key:
|
||||
key, next_key = key.split('.', 1)
|
||||
next_data = data.get(key, {})
|
||||
next_data = data.get(key, CommentedMap())
|
||||
return self._recursive_get(next_data, next_key, default_value)
|
||||
return data.get(key, default_value)
|
||||
|
||||
@@ -44,8 +46,8 @@ class DictWithRecursion:
|
||||
if '.' in key:
|
||||
key, next_key = key.split('.', 1)
|
||||
if key not in data:
|
||||
data[key] = {}
|
||||
next_data = data.get(key, {})
|
||||
data[key] = CommentedMap()
|
||||
next_data = data.get(key, CommentedMap())
|
||||
self._recursive_set(next_data, next_key, value)
|
||||
return
|
||||
data[key] = value
|
||||
@@ -59,6 +61,42 @@ class DictWithRecursion:
|
||||
def __setitem__(self, key, value):
|
||||
self.set(key, value)
|
||||
|
||||
def _recursive_del(self, data, key):
|
||||
if '.' in key:
|
||||
key, next_key = key.split('.', 1)
|
||||
if key not in data:
|
||||
return
|
||||
next_data = data[key]
|
||||
self._recursive_del(next_data, next_key)
|
||||
return
|
||||
try:
|
||||
del data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def delete(self, key, allow_recursion=True):
|
||||
if allow_recursion and '.' in key:
|
||||
self._recursive_del(self._data, key)
|
||||
return
|
||||
try:
|
||||
del self._data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.delete(key)
|
||||
|
||||
def comment(self, key, message):
|
||||
indent = key.count(".") * 4
|
||||
try:
|
||||
path, key = key.rsplit(".", 1)
|
||||
except ValueError:
|
||||
path = None
|
||||
entry = self[path] if path else self._data
|
||||
c = self._data.ca.items.setdefault(key, [None, [], None, None])
|
||||
c[1] = []
|
||||
entry.yaml_set_comment_before_after_key(key=key, before=message, indent=indent)
|
||||
|
||||
|
||||
class Config(DictWithRecursion):
|
||||
def __init__(self, path, registration_path):
|
||||
@@ -82,6 +120,68 @@ class Config(DictWithRecursion):
|
||||
def _new_token():
|
||||
return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(64))
|
||||
|
||||
def update_0_1(self):
|
||||
permissions = self["bridge.permissions"] or CommentedMap()
|
||||
for entry in self["bridge.whitelist"] or []:
|
||||
permissions[entry] = "full"
|
||||
for entry in self["bridge.admins"] or []:
|
||||
permissions[entry] = "admin"
|
||||
|
||||
self["bridge.permissions"] = permissions
|
||||
del self["bridge.whitelist"]
|
||||
del self["bridge.admins"]
|
||||
|
||||
self["bridge.authless_relaybot_portals"] = self.get("bridge.authless_relaybot_portals",
|
||||
True)
|
||||
self.comment("bridge.authless_relaybot_portals",
|
||||
"Whether or not to allow creating portals from Telegram.")
|
||||
|
||||
self.comment("bridge.permissions", "\n".join((
|
||||
"",
|
||||
"Permissions for using the bridge.",
|
||||
"Permitted values:",
|
||||
" relaybot - Only use the bridge via the relaybot, no access to commands.",
|
||||
" full - Full access to use the bridge via relaybot or logging in with Telegram account.",
|
||||
" admin - Full access to use the bridge and some extra administration commands.",
|
||||
"Permitted keys:",
|
||||
" * - All Matrix users",
|
||||
" domain - All users on that homeserver",
|
||||
" mxid - Specific user")))
|
||||
# The telegram section comment disappears for some reason 3:
|
||||
self.comment("telegram", "\nTelegram config")
|
||||
|
||||
self["version"] = 1
|
||||
# Add newline before version
|
||||
self.comment("version",
|
||||
"\nThe version of the config. The bridge will read this and automatically "
|
||||
"update the config if\nthe schema has changed. For the latest version, "
|
||||
"check the example config.")
|
||||
|
||||
def check_updates(self):
|
||||
if self.get("version", 0) == 0:
|
||||
self.update_0_1()
|
||||
else:
|
||||
return
|
||||
self.save()
|
||||
|
||||
def _get_permissions(self, key):
|
||||
level = self["bridge.permissions"].get(key, "")
|
||||
admin = level == "admin"
|
||||
whitelisted = level == "full" or admin
|
||||
relaybot = level == "relaybot" or whitelisted
|
||||
return relaybot, whitelisted, admin
|
||||
|
||||
def get_permissions(self, mxid):
|
||||
permissions = self["bridge.permissions"] or {}
|
||||
if mxid in permissions:
|
||||
return self._get_permissions(mxid)
|
||||
|
||||
homeserver = mxid[mxid.index(":") + 1:]
|
||||
if homeserver in permissions:
|
||||
return self._get_permissions(homeserver)
|
||||
|
||||
return self._get_permissions("*")
|
||||
|
||||
def generate_registration(self):
|
||||
homeserver = self["homeserver.domain"]
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ class MatrixHandler:
|
||||
if not portal:
|
||||
return
|
||||
|
||||
if not user.whitelisted:
|
||||
if not user.relaybot_whitelisted:
|
||||
await portal.main_intent.kick(room, user.mxid,
|
||||
"You are not whitelisted on this Telegram bridge.")
|
||||
return
|
||||
@@ -169,7 +169,7 @@ class MatrixHandler:
|
||||
|
||||
is_command, text = self.is_command(message)
|
||||
sender = await User.get_by_mxid(sender).ensure_started()
|
||||
if not sender.whitelisted:
|
||||
if not sender.relaybot_whitelisted:
|
||||
return
|
||||
|
||||
portal = Portal.get_by_mxid(room)
|
||||
@@ -177,7 +177,7 @@ class MatrixHandler:
|
||||
await portal.handle_matrix_message(sender, message, event_id)
|
||||
return
|
||||
|
||||
if message["msgtype"] != "m.text":
|
||||
if not sender.whitelisted or message["msgtype"] != "m.text":
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
@@ -45,9 +45,9 @@ class Portal:
|
||||
az = None
|
||||
bot = None
|
||||
bridge_notices = False
|
||||
alias_template = None
|
||||
mx_alias_regex = None
|
||||
hs_domain = None
|
||||
mxid_regex = None
|
||||
by_mxid = {}
|
||||
by_tgid = {}
|
||||
|
||||
@@ -223,7 +223,7 @@ class Portal:
|
||||
|
||||
if self.peer_type == "channel" and entity.username:
|
||||
public = True
|
||||
alias = self._get_room_alias(entity.username)
|
||||
alias = self._get_alias_localpart(entity.username)
|
||||
self.username = entity.username
|
||||
else:
|
||||
public = False
|
||||
@@ -281,8 +281,17 @@ class Portal:
|
||||
}
|
||||
return levels
|
||||
|
||||
def _get_room_alias(self, username=None):
|
||||
return self.alias_template.format(groupname=username or self.username)
|
||||
@property
|
||||
def alias(self):
|
||||
if not self.username:
|
||||
return None
|
||||
return f"#{self._get_alias_localpart()}:{self.hs_domain}"
|
||||
|
||||
def _get_alias_localpart(self, username=None):
|
||||
username = username or self.username
|
||||
if not username:
|
||||
return None
|
||||
return self.alias_template.format(groupname=username)
|
||||
|
||||
async def sync_telegram_users(self, source, users):
|
||||
allowed_tgids = set()
|
||||
@@ -361,10 +370,10 @@ class Portal:
|
||||
async def update_username(self, username):
|
||||
if self.username != username:
|
||||
if self.username:
|
||||
await self.main_intent.remove_room_alias(self._get_room_alias())
|
||||
await self.main_intent.remove_room_alias(self._get_alias_localpart())
|
||||
self.username = username or None
|
||||
if self.username:
|
||||
await self.main_intent.add_room_alias(self.mxid, self._get_room_alias())
|
||||
await self.main_intent.add_room_alias(self.mxid, self._get_alias_localpart())
|
||||
await self.main_intent.set_join_rule(self.mxid, "public")
|
||||
else:
|
||||
await self.main_intent.set_join_rule(self.mxid, "invite")
|
||||
@@ -453,7 +462,7 @@ class Portal:
|
||||
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()
|
||||
if (has_bot and user.whitelisted) or user.has_full_access:
|
||||
if (has_bot and user.relaybot_whitelisted) or user.has_full_access:
|
||||
authenticated.append(user)
|
||||
return authenticated
|
||||
|
||||
@@ -1156,7 +1165,7 @@ class Portal:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_by_entity(cls, entity, receiver_id=None):
|
||||
def get_by_entity(cls, entity, receiver_id=None, create=True):
|
||||
entity_type = type(entity)
|
||||
if entity_type in {Chat, ChatFull}:
|
||||
type_name = "chat"
|
||||
@@ -1178,7 +1187,9 @@ class Portal:
|
||||
id = entity.user_id
|
||||
else:
|
||||
raise ValueError(f"Unknown entity type {entity_type.__name__}")
|
||||
return cls.get_by_tgid(id, receiver_id if type_name == "user" else id, type_name)
|
||||
return cls.get_by_tgid(id,
|
||||
receiver_id if type_name == "user" else id,
|
||||
type_name if create else None)
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ from telethon.tl.types import *
|
||||
|
||||
|
||||
class MautrixTelegramClient(TelegramClient):
|
||||
def send_message_super(self, *args, **kwargs):
|
||||
return super().send_message(*args, **kwargs)
|
||||
|
||||
async def send_message(self, entity, message, reply_to=None, entities=None, link_preview=True):
|
||||
entity = await self.get_input_entity(entity)
|
||||
|
||||
|
||||
@@ -50,20 +50,14 @@ class User(AbstractUser):
|
||||
|
||||
self.command_status = None
|
||||
|
||||
self.is_admin = self.mxid in config.get("bridge.admins", [])
|
||||
|
||||
whitelist = config.get("bridge.whitelist", None) or [self.mxid]
|
||||
self.whitelisted = not whitelist or self.mxid in whitelist
|
||||
if not self.whitelisted:
|
||||
homeserver = self.mxid[self.mxid.index(":") + 1:]
|
||||
self.whitelisted = homeserver in whitelist
|
||||
(self.relaybot_whitelisted,
|
||||
self.whitelisted,
|
||||
self.is_admin) = config.get_permissions(self.mxid)
|
||||
|
||||
self.by_mxid[mxid] = self
|
||||
if tgid:
|
||||
self.by_tgid[tgid] = self
|
||||
|
||||
self._init_client()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.mxid
|
||||
|
||||
@@ -27,9 +27,9 @@ setuptools.setup(
|
||||
"python-magic>=0.4.15,<0.5",
|
||||
],
|
||||
dependency_links=[
|
||||
("https://github.com/LonamiWebs/Telethon/tarball/6e854325a8e0e800a4f337257293d09006946162#egg=Telethon"
|
||||
("https://github.com/LonamiWebs/Telethon/tarball/7998fd59f709ae1cd959c5cc4ab107982307f4a6#egg=Telethon"
|
||||
if sys.version_info >= (3, 6)
|
||||
else "https://github.com/tulir/Telethon/tarball/24dc21aea3305ef3bb8c7fcaef2025ae65d5c85e#egg=Telethon")
|
||||
else "https://github.com/tulir/Telethon/tarball/ca08fe28800d74fd6c19fd6f473e12fbf2c258de#egg=Telethon")
|
||||
],
|
||||
|
||||
classifiers=[
|
||||
|
||||
Reference in New Issue
Block a user