From 7515b3116463fa3ae9e7f8af514d666796b16eaf Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 28 Jun 2018 00:20:01 +0300
Subject: [PATCH 01/65] Move Matrix state cache to main database. Fixes #159
---
...d51e4_move_state_store_to_main_database.py | 126 ++++++++++++++++++
mautrix_telegram/__main__.py | 4 +-
mautrix_telegram/db.py | 49 ++++++-
mautrix_telegram/puppet.py | 8 +-
mautrix_telegram/sqlstatestore.py | 99 ++++++++++++++
5 files changed, 281 insertions(+), 5 deletions(-)
create mode 100644 alembic/versions/6ca3d74d51e4_move_state_store_to_main_database.py
create mode 100644 mautrix_telegram/sqlstatestore.py
diff --git a/alembic/versions/6ca3d74d51e4_move_state_store_to_main_database.py b/alembic/versions/6ca3d74d51e4_move_state_store_to_main_database.py
new file mode 100644
index 00000000..7e06de1f
--- /dev/null
+++ b/alembic/versions/6ca3d74d51e4_move_state_store_to_main_database.py
@@ -0,0 +1,126 @@
+"""Move state store to main database
+
+Revision ID: 6ca3d74d51e4
+Revises: 2228d49c383f
+Create Date: 2018-06-26 21:31:26.911307
+
+"""
+from alembic import context, op
+import sqlalchemy.orm as orm
+import sqlalchemy as sa
+import json
+import re
+
+from mautrix_telegram.config import Config
+from mautrix_telegram.base import Base
+
+# revision identifiers, used by Alembic.
+revision = "6ca3d74d51e4"
+down_revision = "2228d49c383f"
+branch_labels = None
+depends_on = None
+
+
+class RoomState(Base):
+ query = None
+ __tablename__ = "mx_room_state"
+ __table_args__ = {"extend_existing": True}
+
+ room_id = sa.Column(sa.String, primary_key=True)
+ power_levels = sa.Column("power_levels", sa.Text, nullable=True)
+
+
+class UserProfile(Base):
+ query = None
+ __tablename__ = "mx_user_profile"
+ __table_args__ = {"extend_existing": True}
+
+ room_id = sa.Column(sa.String, primary_key=True)
+ user_id = sa.Column(sa.String, primary_key=True)
+ membership = sa.Column(sa.String, nullable=False, default="leave")
+ displayname = sa.Column(sa.String, nullable=True)
+ avatar_url = sa.Column(sa.String, nullable=True)
+
+
+class Puppet(Base):
+ query = None
+ __tablename__ = "puppet"
+ __table_args__ = {"extend_existing": True}
+
+ id = sa.Column(sa.Integer, primary_key=True)
+ displayname = sa.Column(sa.String, nullable=True)
+ displayname_source = sa.Column(sa.Integer, nullable=True)
+ username = sa.Column(sa.String, nullable=True)
+ photo_id = sa.Column(sa.String, nullable=True)
+ is_bot = sa.Column(sa.Boolean, nullable=True)
+ matrix_registered = sa.Column(sa.Boolean, nullable=False, default=False)
+
+
+def upgrade():
+ op.add_column("puppet", sa.Column("matrix_registered", sa.Boolean(), nullable=False,
+ server_default=sa.sql.expression.false()))
+ op.create_table("mx_room_state",
+ sa.Column("room_id", sa.String(), nullable=False),
+ sa.Column("power_levels", sa.Text(), nullable=True),
+ sa.PrimaryKeyConstraint("room_id"))
+ op.create_table("mx_user_profile",
+ sa.Column("room_id", sa.String(), nullable=False),
+ sa.Column("user_id", sa.String(), nullable=False),
+ sa.Column("membership", sa.String(), nullable=False,
+ default="leave"),
+ sa.Column("displayname", sa.String(), nullable=True),
+ sa.Column("avatar_url", sa.String(), nullable=True),
+ sa.PrimaryKeyConstraint("room_id", "user_id"))
+
+ conn = op.get_bind()
+ session = orm.sessionmaker(bind=conn)
+ session = orm.scoping.scoped_session(session)
+ Puppet.query = session.query_property()
+
+ with open("mx-state.json") as file:
+ data = json.load(file)
+ if not data:
+ return
+ registrations = data.get("registrations", [])
+
+ mxtg_config_path = context.get_x_argument(as_dictionary=True).get("config", "config.yaml")
+ mxtg_config = Config(mxtg_config_path, None, None)
+ mxtg_config.load()
+
+ username_template = mxtg_config.get("bridge.username_template", "telegram_{userid}")
+ hs_domain = mxtg_config["homeserver.domain"]
+ localpart = username_template.format(userid="(.+)")
+ mxid_regex = re.compile(f"@{localpart}:{hs_domain}")
+ for user in registrations:
+ match = mxid_regex.match(user)
+ if not match:
+ continue
+
+ puppet = Puppet.query.get(match.group(1))
+ if not puppet:
+ continue
+
+ puppet.matrix_registered = True
+ session.merge(puppet)
+ session.commit()
+
+ user_profiles = [UserProfile(room_id=room, user_id=user,
+ membership=member.get("membership", "leave"),
+ displayname=member.get("displayname", None),
+ avatar_url=member.get("avatar_url", None))
+ for room, members in data.get("members", {}).items()
+ for user, member in members.items()]
+ session.add_all(user_profiles)
+ session.commit()
+
+ room_state = [RoomState(room_id=room, power_levels=json.dumps(levels))
+ for room, levels in data.get("power_levels", {}).items()]
+ session.add_all(room_state)
+ session.commit()
+
+
+def downgrade():
+ op.drop_table("mx_user_profile")
+ op.drop_table("mx_room_state")
+ with op.batch_alter_table("puppet") as batch_op:
+ batch_op.drop_column("matrix_registered")
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index b8f29d58..f50bfc92 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -38,6 +38,7 @@ from .puppet import init as init_puppet
from .formatter import init as init_formatter
from .public import PublicBridgeWebsite
from .context import Context
+from .sqlstatestore import SQLStateStore
log = logging.getLogger("mau")
time_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s@%(name)s] %(message)s")
@@ -87,10 +88,11 @@ telethon_session_container = AlchemySessionContainer(engine=db_engine, session=d
loop = asyncio.get_event_loop()
+state_store = SQLStateStore(db_session)
appserv = AppService(config["homeserver.address"], config["homeserver.domain"],
config["appservice.as_token"], config["appservice.hs_token"],
config["appservice.bot_username"], log="mau.as", loop=loop,
- verify_ssl=config["homeserver.verify_ssl"])
+ verify_ssl=config["homeserver.verify_ssl"], state_store=state_store)
context = Context(appserv, db_session, config, loop, None, None, telethon_session_container)
diff --git a/mautrix_telegram/db.py b/mautrix_telegram/db.py
index 2ef9525e..4709fbff 100644
--- a/mautrix_telegram/db.py
+++ b/mautrix_telegram/db.py
@@ -15,8 +15,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from sqlalchemy import (Column, UniqueConstraint, ForeignKey, ForeignKeyConstraint, Integer,
- BigInteger, String, Boolean)
+ BigInteger, String, Boolean, Text)
+from sqlalchemy.sql import expression
from sqlalchemy.orm import relationship
+import json
from .base import Base
@@ -80,6 +82,48 @@ class User(Base):
portals = relationship("Portal", secondary="user_portal")
+class RoomState(Base):
+ query = None
+ __tablename__ = "mx_room_state"
+
+ room_id = Column(String, primary_key=True)
+ _power_levels_text = Column("power_levels", Text, nullable=True)
+ _power_levels_json = None
+
+# def __init__(self, *args, **kwargs):
+# super().__init__(*args, **kwargs)
+# self._power_levels_json = None
+
+ @property
+ def power_levels(self):
+ if not self._power_levels_json and self._power_levels_text:
+ self._power_levels_json = json.loads(self._power_levels_text)
+ return self._power_levels_json or {}
+
+ @power_levels.setter
+ def power_levels(self, val):
+ self._power_levels_json = val
+ self._power_levels_text = json.dumps(val)
+
+
+class UserProfile(Base):
+ query = None
+ __tablename__ = "mx_user_profile"
+
+ room_id = Column(String, primary_key=True)
+ user_id = Column(String, primary_key=True)
+ membership = Column(String, nullable=False, default="leave")
+ displayname = Column(String, nullable=True)
+ avatar_url = Column(String, nullable=True)
+
+ def dict(self):
+ return {
+ "membership": self.membership,
+ "displayname": self.displayname,
+ "avatar_url": self.avatar_url,
+ }
+
+
class Contact(Base):
query = None
__tablename__ = "contact"
@@ -98,6 +142,7 @@ class Puppet(Base):
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())
# Fucking Telegram not telling bots what chats they are in 3:<
@@ -132,3 +177,5 @@ def init(db_session):
Puppet.query = db_session.query_property()
BotChat.query = db_session.query_property()
TelegramFile.query = db_session.query_property()
+ UserProfile.query = db_session.query_property()
+ RoomState.query = db_session.query_property()
diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py
index ad0648bc..b0977036 100644
--- a/mautrix_telegram/puppet.py
+++ b/mautrix_telegram/puppet.py
@@ -36,7 +36,7 @@ class Puppet:
cache = {}
def __init__(self, id=None, username=None, displayname=None, displayname_source=None,
- photo_id=None, is_bot=None, db_instance=None):
+ photo_id=None, is_bot=None, is_registered=False, db_instance=None):
self.id = id
self.mxid = self.get_mxid_from_id(self.id)
@@ -45,6 +45,7 @@ class Puppet:
self.displayname_source = displayname_source
self.photo_id = photo_id
self.is_bot = is_bot
+ self.is_registered = is_registered
self._db_instance = db_instance
self.intent = self.az.intent.user(self.mxid)
@@ -67,13 +68,13 @@ class Puppet:
def new_db_instance(self):
return DBPuppet(id=self.id, username=self.username, displayname=self.displayname,
displayname_source=self.displayname_source, photo_id=self.photo_id,
- is_bot=self.is_bot)
+ is_bot=self.is_bot, matrix_registered=self.is_registered)
@classmethod
def from_db(cls, db_puppet):
return Puppet(db_puppet.id, db_puppet.username, db_puppet.displayname,
db_puppet.displayname_source, db_puppet.photo_id, db_puppet.is_bot,
- db_instance=db_puppet)
+ db_puppet.matrix_registered, db_instance=db_puppet)
def save(self):
self.db_instance.username = self.username
@@ -81,6 +82,7 @@ class Puppet:
self.db_instance.displayname_source = self.displayname_source
self.db_instance.photo_id = self.photo_id
self.db_instance.is_bot = self.is_bot
+ self.db_instance.matrix_registered = self.is_registered
self.db.commit()
def similarity(self, query):
diff --git a/mautrix_telegram/sqlstatestore.py b/mautrix_telegram/sqlstatestore.py
new file mode 100644
index 00000000..1d9442e2
--- /dev/null
+++ b/mautrix_telegram/sqlstatestore.py
@@ -0,0 +1,99 @@
+# -*- coding: future_fstrings -*-
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2018 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+import json
+
+from mautrix_appservice import StateStore
+
+from . import puppet as pu
+from .db import RoomState, UserProfile
+
+
+class SQLStateStore(StateStore):
+ def __init__(self, db):
+ super().__init__()
+ self.db = db
+
+ def is_registered(self, user: str) -> bool:
+ puppet = pu.Puppet.get_by_mxid(user)
+ return puppet.is_registered if puppet else False
+
+ def registered(self, user: str):
+ puppet = pu.Puppet.get_by_mxid(user)
+ if puppet:
+ puppet.is_registered = True
+ puppet.save()
+
+ def update_state(self, event: dict):
+ event_type = event["type"]
+ if event_type == "m.room.power_levels":
+ self.set_power_levels(event["room_id"], event["content"])
+ elif event_type == "m.room.member":
+ self.set_member(event["room_id"], event["state_key"], event["content"])
+
+ def get_member(self, room: str, user: str) -> dict:
+ profile = UserProfile.query.get((room, user))
+ if profile:
+ return profile.dict()
+ return {}
+
+ def set_member(self, room: str, user: str, member: dict):
+ profile = UserProfile(room_id=room, user_id=user,
+ membership=member.get("membership", "leave"),
+ displayname=member.get("displayname", None),
+ avatar_url=member.get("avatar_url", None))
+ self.db.merge(profile)
+ self.db.commit()
+
+ def set_membership(self, room: str, user: str, membership: str):
+ profile = UserProfile.query.get((room, user))
+ if not profile:
+ profile = UserProfile(room_id=room, user_id=user, membership=membership)
+ self.db.add(profile)
+ else:
+ profile.membership = membership
+ self.db.commit()
+
+ def has_power_levels(self, room: str) -> bool:
+ room = RoomState.query.get(room)
+ return room and room._power_levels_text
+
+ def get_power_levels(self, room: str) -> dict:
+ return RoomState.query.get(room).power_levels
+
+ def set_power_level(self, room: str, user: str, level: int):
+ room_state = RoomState.query.get(room)
+ if not room_state:
+ room_state = RoomState(room)
+ self.db.add(room_state)
+
+ power_levels = room_state.power_levels
+ if not power_levels:
+ power_levels = {
+ "users": {},
+ "events": {},
+ }
+ power_levels[room]["users"][user] = level
+ room_state.power_levels = power_levels
+ self.db.commit()
+
+ def set_power_levels(self, room: str, content: dict):
+ state = RoomState.query.get(room)
+ if not state:
+ state = RoomState(room_id=room)
+ self.db.add(state)
+ state.power_levels = content
+ self.db.commit()
From 042d89cf65578a93c71730347ff716935864cf7f Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 12 Jul 2018 22:47:40 +0300
Subject: [PATCH 02/65] Add full log config. Fixes #166
---
.gitignore | 1 +
example-config.yaml | 31 ++++++++++++++++++++++++++++---
mautrix_telegram/__main__.py | 27 ++++++++++++---------------
mautrix_telegram/abstract_user.py | 4 ++--
mautrix_telegram/config.py | 10 ++++++++--
mautrix_telegram/user.py | 2 +-
6 files changed, 52 insertions(+), 23 deletions(-)
diff --git a/.gitignore b/.gitignore
index b7e3188b..7d5457da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ __pycache__
config.yaml
registration.yaml
+logs/
*.db
*.session
*.json
diff --git a/example-config.yaml b/example-config.yaml
index ea03b25f..0c88b987 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -34,9 +34,6 @@ appservice:
# implicitly.
external: https://example.com/public
- # Whether or not to enable debug messages in the console.
- debug: true
-
# The unique ID of this appservice.
id: telegram
# Username of the appservice bot.
@@ -194,3 +191,31 @@ telegram:
api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz
# (Optional) Create your own bot at https://t.me/BotFather
bot_token: disabled
+
+# Python logging configuration.
+#
+# See section 16.7.2 of the Python documentation for more info:
+# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema
+logging:
+ version: 1
+ formatters:
+ precise:
+ format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s"
+ handlers:
+ file:
+ class: logging.handlers.RotatingFileHandler
+ formatter: precise
+ filename: ./mautrix-telegram.log
+ maxBytes: 10485760
+ backupCount: 10
+ console:
+ class: logging.StreamHandler
+ formatter: precise
+ loggers:
+ mau:
+ level: DEBUG
+ telethon:
+ level: DEBUG
+ root:
+ level: DEBUG
+ handlers: [file, console]
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index b8f29d58..8fce6699 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -17,6 +17,7 @@
import argparse
import sys
import logging
+import logging.config
import asyncio
import sqlalchemy as sql
@@ -29,6 +30,7 @@ from .base import Base
from .config import Config
from .matrix import MatrixHandler
+from . import __version__
from .db import init as init_db
from .abstract_user import init as init_abstract_user
from .user import init as init_user, User
@@ -39,12 +41,6 @@ from .formatter import init as init_formatter
from .public import PublicBridgeWebsite
from .context import Context
-log = logging.getLogger("mau")
-time_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s@%(name)s] %(message)s")
-handler = logging.StreamHandler()
-handler.setFormatter(time_formatter)
-log.addHandler(handler)
-
parser = argparse.ArgumentParser(
description="A Matrix-Telegram puppeting bridge.",
prog="python -m mautrix-telegram")
@@ -69,14 +65,11 @@ if args.generate_registration:
print(f"Registration generated and saved to {config.registration_path}")
sys.exit(0)
-if config["appservice.debug"]:
- telethon_log = logging.getLogger("telethon")
- telethon_log.addHandler(handler)
- telethon_log.setLevel(logging.DEBUG)
- log.setLevel(logging.DEBUG)
- log.debug("Debug messages enabled.")
+logging.config.dictConfig(config["logging"])
+log = logging.getLogger("mau.init")
+log.debug(f"Initializing mautrix-telegram {__version__}")
-db_engine = sql.create_engine(config.get("appservice.database", "sqlite:///mautrix-telegram.db"))
+db_engine = sql.create_engine(config["appservice.database"] or "sqlite:///mautrix-telegram.db")
db_factory = orm.sessionmaker(bind=db_engine)
db_session = orm.scoping.scoped_session(db_factory)
Base.metadata.bind = db_engine
@@ -112,9 +105,13 @@ with appserv.run(config["appservice.hostname"], config["appservice.port"]) as st
startup_actions.append(context.bot.start())
try:
+ log.debug("Initialization complete, running startup actions")
loop.run_until_complete(asyncio.gather(*startup_actions, loop=loop))
+ log.debug("Startup actions complete, now running forever")
loop.run_forever()
except KeyboardInterrupt:
- for user in User.by_tgid.values():
- user.stop()
+ log.debug("Keyboard interrupt received, stopping clients")
+ loop.run_until_complete(
+ asyncio.gather(*[user.stop() for user in User.by_tgid.values()], loop=loop))
+ log.debug("Clients stopped, shutting down")
sys.exit(0)
diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py
index 0658f904..722b4655 100644
--- a/mautrix_telegram/abstract_user.py
+++ b/mautrix_telegram/abstract_user.py
@@ -118,8 +118,8 @@ class AbstractUser:
await self.start(delete_unless_authenticated=not even_if_no_session)
return self
- def stop(self):
- self.client.disconnect()
+ async def stop(self):
+ await self.client.disconnect()
self.client = None
# region Telegram update handling
diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py
index 09476659..8c9f9410 100644
--- a/mautrix_telegram/config.py
+++ b/mautrix_telegram/config.py
@@ -154,8 +154,6 @@ class Config(DictWithRecursion):
copy("appservice.public.prefix")
copy("appservice.public.external")
- copy("appservice.debug")
-
copy("appservice.id")
copy("appservice.bot_username")
copy("appservice.bot_displayname")
@@ -218,6 +216,14 @@ class Config(DictWithRecursion):
copy("telegram.api_hash")
copy("telegram.bot_token")
+ if "appservice.debug" in self and "logging" not in self:
+ level = "DEBUG" if self["appservice.debug"] else "INFO"
+ base["logging.root.level"] = level
+ base["logging.loggers.mau.level"] = level
+ base["logging.loggers.telethon.level"] = level
+ else:
+ copy("logging")
+
self._data = base._data
self.save()
diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py
index 1a72fd0a..da62fad8 100644
--- a/mautrix_telegram/user.py
+++ b/mautrix_telegram/user.py
@@ -145,7 +145,7 @@ class User(AbstractUser):
asyncio.ensure_future(self.post_login(), loop=self.loop)
elif delete_unless_authenticated:
self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...")
- self.client.disconnect()
+ await self.client.disconnect()
self.client.session.delete()
return self
From 1d9455f6398527bdee1375c63a8570dfa74104f6 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 12 Jul 2018 22:59:17 +0300
Subject: [PATCH 03/65] Allow specifying address and listen host/port
separately. Fixes #160
---
example-config.yaml | 9 ++++-----
mautrix_telegram/config.py | 13 ++++++++-----
2 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/example-config.yaml b/example-config.yaml
index 0c88b987..82042e2d 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -11,12 +11,11 @@ homeserver:
# Application service host/registration related details
# Changing these values requires regeneration of the registration.
appservice:
- # The protocol the homeserver should use when connecting to this appservice.
- # Usually "http" or "https".
- protocol: http
+ # The address that the homeserver can use to connect to this appservice.
+ address: http://localhost:8080
- # The hostname and port where the homeserver can find this appservice.
- hostname: localhost
+ # The hostname and port where this appservice should listen.
+ hostname: 0.0.0.0
port: 8080
# The full URI to the database.
diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py
index 8c9f9410..8b90eabd 100644
--- a/mautrix_telegram/config.py
+++ b/mautrix_telegram/config.py
@@ -144,7 +144,12 @@ class Config(DictWithRecursion):
copy("homeserver.verify_ssl")
copy("homeserver.domain")
- copy("appservice.protocol")
+ if "appservice.protocol" in self and "appservice.address" not in self:
+ protocol, hostname, port = (self["appservice.protocol"], self["appservice.hostname"],
+ self["appservice.port"])
+ base["appservice.address"] = f"{protocol}://{hostname}:{port}"
+ else:
+ copy("appservice.address")
copy("appservice.hostname")
copy("appservice.port")
@@ -257,10 +262,8 @@ class Config(DictWithRecursion):
self.set("appservice.as_token", self._new_token())
self.set("appservice.hs_token", self._new_token())
- url = (f"{self['appservice.protocol']}://"
- f"{self['appservice.hostname']}:{self['appservice.port']}")
self._registration = {
- "id": self.get("appservice.id", "telegram"),
+ "id": self["appservice.id"] or "telegram",
"as_token": self["appservice.as_token"],
"hs_token": self["appservice.hs_token"],
"namespaces": {
@@ -273,7 +276,7 @@ class Config(DictWithRecursion):
"regex": f"#{alias_format}:{homeserver}"
}]
},
- "url": url,
+ "url": self["appservice.address"],
"sender_localpart": self["appservice.bot_username"],
"rate_limited": False
}
From 15fd394d5433c0df4570174ada2c3324b88b46f4 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 12 Jul 2018 23:08:08 +0300
Subject: [PATCH 04/65] Add proxy config. Fixes #153
---
example-config.yaml | 13 +++++++++++++
mautrix_telegram/abstract_user.py | 24 ++++++++++++++++++++++--
mautrix_telegram/config.py | 6 ++++++
3 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/example-config.yaml b/example-config.yaml
index 82042e2d..27c64991 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -190,6 +190,19 @@ telegram:
api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz
# (Optional) Create your own bot at https://t.me/BotFather
bot_token: disabled
+ # Telethon proxy configuration.
+ # You must install PySocks from pip for proxies to work.
+ proxy:
+ # Allowed types: disabled, socks4, socks5, http
+ type: disabled
+ # Proxy IP address and port.
+ address: 127.0.0.1
+ port: 1080
+ # Whether or not to perform DNS resolving remotely.
+ rdns: true
+ # Proxy authentication (optional).
+ username: ""
+ password: ""
# Python logging configuration.
#
diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py
index 722b4655..dd1fe03b 100644
--- a/mautrix_telegram/abstract_user.py
+++ b/mautrix_telegram/abstract_user.py
@@ -50,6 +50,23 @@ class AbstractUser:
def connected(self):
return self.client and self.client.is_connected()
+ @property
+ def _proxy_settings(self):
+ type = config["telegram.proxy.type"].lower()
+ if type == "disabled":
+ return None
+ elif type == "socks4":
+ type = 1
+ elif type == "socks5":
+ type = 2
+ elif type == "http":
+ type = 3
+
+ return (type,
+ config["telegram.proxy.address"], config["telegram.proxy.port"],
+ config["telegram.proxy.rdns"],
+ config["telegram.proxy.username"], config["telegram.proxy.password"])
+
def _init_client(self):
self.log.debug(f"Initializing client for {self.name}")
device = f"{platform.system()} {platform.release()}"
@@ -62,7 +79,8 @@ class AbstractUser:
app_version=__version__,
system_version=sysversion,
device_model=device,
- timeout=120)
+ timeout=120,
+ proxy=self._proxy_settings)
self.client.add_event_handler(self._update_catch)
async def update(self, update):
@@ -95,7 +113,9 @@ class AbstractUser:
return self.client and await self.client.is_user_authorized()
async def has_full_access(self, allow_bot=False):
- return self.puppet_whitelisted and (not self.is_bot or allow_bot) and await self.is_logged_in()
+ return (self.puppet_whitelisted
+ and (not self.is_bot or allow_bot)
+ and await self.is_logged_in())
async def start(self, delete_unless_authenticated=False):
if not self.client:
diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py
index 8b90eabd..fb2e14a2 100644
--- a/mautrix_telegram/config.py
+++ b/mautrix_telegram/config.py
@@ -220,6 +220,12 @@ class Config(DictWithRecursion):
copy("telegram.api_id")
copy("telegram.api_hash")
copy("telegram.bot_token")
+ copy("telegram.proxy.type")
+ copy("telegram.proxy.address")
+ copy("telegram.proxy.port")
+ copy("telegram.proxy.rdns")
+ copy("telegram.proxy.username")
+ copy("telegram.proxy.password")
if "appservice.debug" in self and "logging" not in self:
level = "DEBUG" if self["appservice.debug"] else "INFO"
From f6923a5e1b518ac5754ef825cec65dede9cb4dbf Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sun, 24 Jun 2018 21:22:12 +0300
Subject: [PATCH 05/65] Add provisioning API config (ref #154)
---
example-config.yaml | 11 +++++++++++
mautrix_telegram/__main__.py | 16 +++++++++++-----
mautrix_telegram/abstract_user.py | 2 +-
mautrix_telegram/config.py | 4 ++++
mautrix_telegram/context.py | 4 ++--
mautrix_telegram/provisioning_api.py | 27 +++++++++++++++++++++++++++
6 files changed, 56 insertions(+), 8 deletions(-)
create mode 100644 mautrix_telegram/provisioning_api.py
diff --git a/example-config.yaml b/example-config.yaml
index 27c64991..6f9e8df5 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -33,6 +33,17 @@ appservice:
# implicitly.
external: https://example.com/public
+ # Provisioning API part of the web server for automated portal creation and fetching information.
+ # Used by things like Dimension (https://dimension.t2bot.io/).
+ provisioning:
+ # Whether or not the provisioning API should be enabled.
+ enabled: true
+ # The prefix to use in the provisioning API endpoints.
+ prefix: /_matrix/provision
+ # The shared secret to authorize users of the API.
+ # You can generate a decent secret with `pwgen -snc 32 1`
+ shared_secret: "Very secret shared secret"
+
# The unique ID of this appservice.
id: telegram
# Username of the appservice bot.
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index 8fce6699..84d01698 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -39,6 +39,7 @@ from .portal import init as init_portal
from .puppet import init as init_puppet
from .formatter import init as init_formatter
from .public import PublicBridgeWebsite
+from .provisioning_api import ProvisioningAPI
from .context import Context
parser = argparse.ArgumentParser(
@@ -74,9 +75,9 @@ db_factory = orm.sessionmaker(bind=db_engine)
db_session = orm.scoping.scoped_session(db_factory)
Base.metadata.bind = db_engine
-telethon_session_container = AlchemySessionContainer(engine=db_engine, session=db_session,
- table_base=Base, table_prefix="telethon_",
- manage_tables=False)
+session_container = AlchemySessionContainer(engine=db_engine, session=db_session,
+ table_base=Base, table_prefix="telethon_",
+ manage_tables=False)
loop = asyncio.get_event_loop()
@@ -85,11 +86,16 @@ appserv = AppService(config["homeserver.address"], config["homeserver.domain"],
config["appservice.bot_username"], log="mau.as", loop=loop,
verify_ssl=config["homeserver.verify_ssl"])
-context = Context(appserv, db_session, config, loop, None, None, telethon_session_container)
+context = Context(appserv, db_session, config, loop, None, None, session_container)
if config["appservice.public.enabled"]:
public = PublicBridgeWebsite(loop)
- appserv.app.add_subapp(config.get("appservice.public.prefix", "/public"), public.app)
+ appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public.app)
+
+if config["appservice.provisioning.enabled"]:
+ provisioning_api = ProvisioningAPI(loop)
+ appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning",
+ provisioning_api.app)
with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start:
init_db(db_session)
diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py
index dd1fe03b..d15e9e66 100644
--- a/mautrix_telegram/abstract_user.py
+++ b/mautrix_telegram/abstract_user.py
@@ -328,5 +328,5 @@ class AbstractUser:
def init(context):
global config, MAX_DELETIONS
AbstractUser.az, AbstractUser.db, config, AbstractUser.loop, _ = context
- AbstractUser.session_container = context.telethon_session_container
+ AbstractUser.session_container = context.session_container
MAX_DELETIONS = config.get("bridge.max_telegram_delete", 10)
diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py
index fb2e14a2..07944b3c 100644
--- a/mautrix_telegram/config.py
+++ b/mautrix_telegram/config.py
@@ -159,6 +159,10 @@ class Config(DictWithRecursion):
copy("appservice.public.prefix")
copy("appservice.public.external")
+ copy("appservice.provisioning.enabled")
+ copy("appservice.provisioning.prefix")
+ copy("appservice.provisioning.shared_secret")
+
copy("appservice.id")
copy("appservice.bot_username")
copy("appservice.bot_displayname")
diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py
index 530f1eed..dfb32b59 100644
--- a/mautrix_telegram/context.py
+++ b/mautrix_telegram/context.py
@@ -17,14 +17,14 @@
class Context:
- def __init__(self, az, db, config, loop, bot, mx, telethon_session_container):
+ def __init__(self, az, db, config, loop, bot, mx, session_container):
self.az = az
self.db = db
self.config = config
self.loop = loop
self.bot = bot
self.mx = mx
- self.telethon_session_container = telethon_session_container
+ self.session_container = session_container
def __iter__(self):
yield self.az
diff --git a/mautrix_telegram/provisioning_api.py b/mautrix_telegram/provisioning_api.py
new file mode 100644
index 00000000..db89c506
--- /dev/null
+++ b/mautrix_telegram/provisioning_api.py
@@ -0,0 +1,27 @@
+# -*- coding: future_fstrings -*-
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2018 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+from aiohttp import web
+import logging
+
+
+class ProvisioningAPI:
+ log = logging.getLogger("mau.provisioning")
+
+ def __init__(self, loop):
+ self.loop = loop
+
+ self.app = web.Application(loop=loop)
From 5d48040eb8bda89b41c7f304a1903df1541f9eba Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sun, 24 Jun 2018 21:35:07 +0300
Subject: [PATCH 06/65] Separate auth methods from public API
---
mautrix_telegram/public/__init__.py | 125 ++++++----------------------
mautrix_telegram/public/auth_api.py | 115 +++++++++++++++++++++++++
2 files changed, 141 insertions(+), 99 deletions(-)
create mode 100644 mautrix_telegram/public/auth_api.py
diff --git a/mautrix_telegram/public/__init__.py b/mautrix_telegram/public/__init__.py
index 6a463f2e..60e22fd6 100644
--- a/mautrix_telegram/public/__init__.py
+++ b/mautrix_telegram/public/__init__.py
@@ -26,12 +26,14 @@ from ..user import User
from ..commands.auth import enter_password
from ..util import format_duration
+from .auth_api import AuthAPI
-class PublicBridgeWebsite:
+
+class PublicBridgeWebsite(AuthAPI):
log = logging.getLogger("mau.public")
def __init__(self, loop):
- self.loop = loop
+ super(AuthAPI, self).__init__(loop)
self.login = Template(
pkg_resources.resource_string("mautrix_telegram", "public/login.html.mako"))
@@ -43,22 +45,24 @@ class PublicBridgeWebsite:
pkg_resources.resource_filename("mautrix_telegram", "public/"))
async def get_login(self, request):
- user = (User.get_by_mxid(request.rel_url.query["mxid"], create=False)
- if "mxid" in request.rel_url.query else None)
state = "token" if request.rel_url.query.get("mode", "") == "bot" else "request"
+
+ mxid = request.rel_url.query.get("mxid", None)
+ user = User.get_by_mxid(mxid, create=False) if mxid else None
+
if not user:
- return self.render_login(
- mxid=request.rel_url.query["mxid"] if "mxid" in request.rel_url.query else None,
- state=state)
+ return self.get_login_response(mxid=mxid, state=state)
elif not user.puppet_whitelisted:
- return self.render_login(mxid=user.mxid, error="You are not whitelisted.", status=403)
+ return self.get_login_response(mxid=user.mxid, error="You are not whitelisted.",
+ status=403)
await user.ensure_started()
if not await user.is_logged_in():
- return self.render_login(mxid=user.mxid, state=state)
+ return self.get_login_response(mxid=user.mxid, state=state)
- return self.render_login(mxid=user.mxid, username=user.username)
+ return self.get_login_response(mxid=user.mxid, username=user.username)
- def render_login(self, status=200, username="", state="", error="", message="", mxid=""):
+ def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
+ errcode=""):
return web.Response(status=status, content_type="text/html",
text=self.login.render(username=username, state=state, error=error,
message=message, mxid=mxid))
@@ -69,101 +73,24 @@ class PublicBridgeWebsite:
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
- return self.render_login(mxid=user.mxid, state="logged-in", status=200,
- username=user_info.username)
+ return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
+ username=user_info.username)
except Exception:
self.log.exception("Error sending bot token")
- return self.render_login(mxid=user.mxid, state="token", status=500,
- error="Internal server error while sending token.")
-
- async def post_login_phone(self, user, phone):
- try:
- await user.client.sign_in(phone or "+123")
- return self.render_login(mxid=user.mxid, state="code", status=200,
- message="Code requested successfully.")
- except PhoneNumberInvalidError:
- return self.render_login(mxid=user.mxid, state="request", status=400,
- error="Invalid phone number.")
- except PhoneNumberUnoccupiedError:
- return self.render_login(mxid=user.mxid, state="request", status=404,
- error="That phone number has not been registered.")
- except PhoneNumberFloodError:
- return self.render_login(
- mxid=user.mxid, state="request", status=429,
- error="Your phone number has been temporarily blocked for flooding. "
- "The ban is usually applied for around a day.")
- except FloodWaitError as e:
- return self.render_login(
- mxid=user.mxid, state="request", status=429,
- error="Your phone number has been temporarily blocked for flooding. "
- f"Please wait for {format_duration(e.seconds)} before trying again.")
- except PhoneNumberBannedError:
- return self.render_login(mxid=user.mxid, state="request", status=401,
- error="Your phone number is banned from Telegram.")
- except PhoneNumberAppSignupForbiddenError:
- return self.render_login(mxid=user.mxid, state="request", status=401,
- error="You have disabled 3rd party apps on your account.")
- except Exception:
- self.log.exception("Error requesting phone code")
- return self.render_login(mxid=user.mxid, state="request", status=500,
- error="Internal server error while requesting code.")
-
- async def post_login_code(self, user, code, password_in_data):
- try:
- user_info = await user.client.sign_in(code=code)
- 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
- return self.render_login(mxid=user.mxid, state="logged-in", status=200,
- username=user_info.username)
- except PhoneCodeInvalidError:
- return self.render_login(mxid=user.mxid, state="code", status=403,
- error="Incorrect phone code.")
- except PhoneCodeExpiredError:
- return self.render_login(mxid=user.mxid, state="code", status=403,
- error="Phone code expired.")
- except SessionPasswordNeededError:
- if not password_in_data:
- if user.command_status and user.command_status["action"] == "Login":
- user.command_status = {
- "next": enter_password,
- "action": "Login (password entry)",
- }
- return self.render_login(
- mxid=user.mxid, state="password", status=200,
- message="Code accepted, but you have 2-factor authentication is enabled.")
- return None
- except Exception:
- self.log.exception("Error sending phone code")
- return self.render_login(mxid=user.mxid, state="code", status=500,
- error="Internal server error while sending code.")
-
- async def post_login_password(self, user, password):
- try:
- user_info = await user.client.sign_in(password=password)
- asyncio.ensure_future(user.post_login(user_info), loop=self.loop)
- if user.command_status and user.command_status["action"] == "Login (password entry)":
- user.command_status = None
- return self.render_login(mxid=user.mxid, state="logged-in", status=200,
- username=user_info.username)
- except (PasswordHashInvalidError, PasswordEmptyError):
- return self.render_login(mxid=user.mxid, state="password", status=400,
- error="Incorrect password.")
- except Exception:
- self.log.exception("Error sending password")
- return self.render_login(mxid=user.mxid, state="password", status=500,
- error="Internal server error while sending password.")
+ return self.get_login_response(mxid=user.mxid, state="token", status=500,
+ error="Internal server error while sending token.")
async def post_login(self, request):
data = await request.post()
if "mxid" not in data:
- return self.render_login(error="Please enter your Matrix ID.", status=400)
+ return self.get_login_response(error="Please enter your Matrix ID.", status=400)
- user = await User.get_by_mxid(data["mxid"]).ensure_started()
+ user = await User.get_by_mxid(data["mxid"]).ensure_started(even_if_no_session=True)
if not user.puppet_whitelisted:
- return self.render_login(mxid=user.mxid, error="You are not whitelisted.", status=403)
+ return self.get_login_response(mxid=user.mxid, error="You are not whitelisted.",
+ status=403)
elif await user.is_logged_in():
- return self.render_login(mxid=user.mxid, username=user.username)
+ return self.get_login_response(mxid=user.mxid, username=user.username)
await user.ensure_started(even_if_no_session=True)
@@ -177,8 +104,8 @@ class PublicBridgeWebsite:
if resp or "password" not in data:
return resp
elif "password" not in data:
- return self.render_login(error="No data given.", status=400)
+ return self.get_login_response(error="No data given.", status=400)
if "password" in data:
return await self.post_login_password(user, data["password"])
- return self.render_login(error="This should never happen.", status=500)
+ return self.get_login_response(error="This should never happen.", status=500)
diff --git a/mautrix_telegram/public/auth_api.py b/mautrix_telegram/public/auth_api.py
new file mode 100644
index 00000000..ec763452
--- /dev/null
+++ b/mautrix_telegram/public/auth_api.py
@@ -0,0 +1,115 @@
+# -*- coding: future_fstrings -*-
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2018 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+from abc import abstractmethod
+import abc
+import asyncio
+import logging
+
+from telethon.errors import *
+
+from ..commands.auth import enter_password
+from ..util import format_duration
+
+
+class AuthAPI(abc.ABC):
+ log = logging.getLogger("mau.public.auth")
+
+ def __init__(self, loop):
+ self.loop = loop
+
+ @abstractmethod
+ def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
+ errcode=""):
+ raise NotImplementedError()
+
+ async def post_login_phone(self, user, phone):
+ try:
+ await user.client.sign_in(phone or "+123")
+ return self.get_login_response(mxid=user.mxid, state="code", status=200,
+ message="Code requested successfully.")
+ except PhoneNumberInvalidError:
+ return self.get_login_response(mxid=user.mxid, state="request", status=400,
+ error="Invalid phone number.")
+ except PhoneNumberUnoccupiedError:
+ return self.get_login_response(mxid=user.mxid, state="request", status=404,
+ error="That phone number has not been registered.")
+ except PhoneNumberFloodError:
+ return self.get_login_response(
+ mxid=user.mxid, state="request", status=429,
+ error="Your phone number has been temporarily blocked for flooding. "
+ "The ban is usually applied for around a day.")
+ except FloodWaitError as e:
+ return self.get_login_response(
+ mxid=user.mxid, state="request", status=429,
+ error="Your phone number has been temporarily blocked for flooding. "
+ f"Please wait for {format_duration(e.seconds)} before trying again.")
+ except PhoneNumberBannedError:
+ return self.get_login_response(mxid=user.mxid, state="request", status=401,
+ error="Your phone number is banned from Telegram.")
+ except PhoneNumberAppSignupForbiddenError:
+ return self.get_login_response(mxid=user.mxid, state="request", status=401,
+ error="You have disabled 3rd party apps on your account.")
+ except Exception:
+ self.log.exception("Error requesting phone code")
+ return self.get_login_response(mxid=user.mxid, state="request", status=500,
+ error="Internal server error while requesting code.")
+
+ async def post_login_code(self, user, code, password_in_data):
+ try:
+ user_info = await user.client.sign_in(code=code)
+ 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
+ return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
+ username=user_info.username)
+ except PhoneCodeInvalidError:
+ return self.get_login_response(mxid=user.mxid, state="code", status=403,
+ error="Incorrect phone code.")
+ except PhoneCodeExpiredError:
+ return self.get_login_response(mxid=user.mxid, state="code", status=403,
+ error="Phone code expired.")
+ except SessionPasswordNeededError:
+ if not password_in_data:
+ if user.command_status and user.command_status["action"] == "Login":
+ user.command_status = {
+ "next": enter_password,
+ "action": "Login (password entry)",
+ }
+ return self.get_login_response(
+ mxid=user.mxid, state="password", status=200,
+ message="Code accepted, but you have 2-factor authentication is enabled.")
+ return None
+ except Exception:
+ self.log.exception("Error sending phone code")
+ return self.get_login_response(mxid=user.mxid, state="code", status=500,
+ error="Internal server error while sending code.")
+
+ async def post_login_password(self, user, password):
+ try:
+ user_info = await user.client.sign_in(password=password)
+ asyncio.ensure_future(user.post_login(user_info), loop=self.loop)
+ if user.command_status and user.command_status["action"] == "Login (password entry)":
+ user.command_status = None
+ return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
+ username=user_info.username)
+ except (PasswordHashInvalidError, PasswordEmptyError):
+ return self.get_login_response(mxid=user.mxid, state="password", status=400,
+ error="Incorrect password.")
+ except Exception:
+ self.log.exception("Error sending password")
+ return self.get_login_response(mxid=user.mxid, state="password", status=500,
+ error="Internal server error while sending password.")
From fa30cb5c1f6cdff7329be426701ef5ed3966b25b Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sun, 24 Jun 2018 21:39:01 +0300
Subject: [PATCH 07/65] Move web stuff to web package
---
mautrix_telegram/__main__.py | 4 +--
mautrix_telegram/web/__init__.py | 2 ++
mautrix_telegram/web/common/__init__.py | 1 +
.../{public => web/common}/auth_api.py | 32 +++++++++---------
.../provisioning/__init__.py} | 0
mautrix_telegram/web/provisioning/spec.yaml | 0
mautrix_telegram/{ => web}/public/__init__.py | 9 ++---
mautrix_telegram/{ => web}/public/favicon.png | Bin
mautrix_telegram/{ => web}/public/login.css | 0
.../{ => web}/public/login.html.mako | 0
10 files changed, 23 insertions(+), 25 deletions(-)
create mode 100644 mautrix_telegram/web/__init__.py
create mode 100644 mautrix_telegram/web/common/__init__.py
rename mautrix_telegram/{public => web/common}/auth_api.py (79%)
rename mautrix_telegram/{provisioning_api.py => web/provisioning/__init__.py} (100%)
create mode 100644 mautrix_telegram/web/provisioning/spec.yaml
rename mautrix_telegram/{ => web}/public/__init__.py (96%)
rename mautrix_telegram/{ => web}/public/favicon.png (100%)
rename mautrix_telegram/{ => web}/public/login.css (100%)
rename mautrix_telegram/{ => web}/public/login.html.mako (100%)
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index 84d01698..9702468f 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -38,8 +38,8 @@ from .bot import init as init_bot
from .portal import init as init_portal
from .puppet import init as init_puppet
from .formatter import init as init_formatter
-from .public import PublicBridgeWebsite
-from .provisioning_api import ProvisioningAPI
+from .web.public import PublicBridgeWebsite
+from .web.provisioning import ProvisioningAPI
from .context import Context
parser = argparse.ArgumentParser(
diff --git a/mautrix_telegram/web/__init__.py b/mautrix_telegram/web/__init__.py
new file mode 100644
index 00000000..002510e8
--- /dev/null
+++ b/mautrix_telegram/web/__init__.py
@@ -0,0 +1,2 @@
+from .provisioning import ProvisioningAPI
+from .public import PublicBridgeWebsite
diff --git a/mautrix_telegram/web/common/__init__.py b/mautrix_telegram/web/common/__init__.py
new file mode 100644
index 00000000..ccb0d922
--- /dev/null
+++ b/mautrix_telegram/web/common/__init__.py
@@ -0,0 +1 @@
+from .auth_api import AuthAPI
diff --git a/mautrix_telegram/public/auth_api.py b/mautrix_telegram/web/common/auth_api.py
similarity index 79%
rename from mautrix_telegram/public/auth_api.py
rename to mautrix_telegram/web/common/auth_api.py
index ec763452..9502db53 100644
--- a/mautrix_telegram/public/auth_api.py
+++ b/mautrix_telegram/web/common/auth_api.py
@@ -21,8 +21,8 @@ import logging
from telethon.errors import *
-from ..commands.auth import enter_password
-from ..util import format_duration
+from mautrix_telegram.commands.auth import enter_password
+from mautrix_telegram.util import format_duration
class AuthAPI(abc.ABC):
@@ -33,20 +33,20 @@ class AuthAPI(abc.ABC):
@abstractmethod
def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
- errcode=""):
+ errcode=""):
raise NotImplementedError()
async def post_login_phone(self, user, phone):
try:
await user.client.sign_in(phone or "+123")
return self.get_login_response(mxid=user.mxid, state="code", status=200,
- message="Code requested successfully.")
+ message="Code requested successfully.")
except PhoneNumberInvalidError:
return self.get_login_response(mxid=user.mxid, state="request", status=400,
- error="Invalid phone number.")
+ error="Invalid phone number.")
except PhoneNumberUnoccupiedError:
return self.get_login_response(mxid=user.mxid, state="request", status=404,
- error="That phone number has not been registered.")
+ error="That phone number has not been registered.")
except PhoneNumberFloodError:
return self.get_login_response(
mxid=user.mxid, state="request", status=429,
@@ -59,14 +59,14 @@ class AuthAPI(abc.ABC):
f"Please wait for {format_duration(e.seconds)} before trying again.")
except PhoneNumberBannedError:
return self.get_login_response(mxid=user.mxid, state="request", status=401,
- error="Your phone number is banned from Telegram.")
+ error="Your phone number is banned from Telegram.")
except PhoneNumberAppSignupForbiddenError:
return self.get_login_response(mxid=user.mxid, state="request", status=401,
- error="You have disabled 3rd party apps on your account.")
+ error="You have disabled 3rd party apps on your account.")
except Exception:
self.log.exception("Error requesting phone code")
return self.get_login_response(mxid=user.mxid, state="request", status=500,
- error="Internal server error while requesting code.")
+ error="Internal server error while requesting code.")
async def post_login_code(self, user, code, password_in_data):
try:
@@ -75,13 +75,13 @@ class AuthAPI(abc.ABC):
if user.command_status and user.command_status["action"] == "Login":
user.command_status = None
return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
- username=user_info.username)
+ username=user_info.username)
except PhoneCodeInvalidError:
return self.get_login_response(mxid=user.mxid, state="code", status=403,
- error="Incorrect phone code.")
+ error="Incorrect phone code.")
except PhoneCodeExpiredError:
return self.get_login_response(mxid=user.mxid, state="code", status=403,
- error="Phone code expired.")
+ error="Phone code expired.")
except SessionPasswordNeededError:
if not password_in_data:
if user.command_status and user.command_status["action"] == "Login":
@@ -96,7 +96,7 @@ class AuthAPI(abc.ABC):
except Exception:
self.log.exception("Error sending phone code")
return self.get_login_response(mxid=user.mxid, state="code", status=500,
- error="Internal server error while sending code.")
+ error="Internal server error while sending code.")
async def post_login_password(self, user, password):
try:
@@ -105,11 +105,11 @@ class AuthAPI(abc.ABC):
if user.command_status and user.command_status["action"] == "Login (password entry)":
user.command_status = None
return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
- username=user_info.username)
+ username=user_info.username)
except (PasswordHashInvalidError, PasswordEmptyError):
return self.get_login_response(mxid=user.mxid, state="password", status=400,
- error="Incorrect password.")
+ error="Incorrect password.")
except Exception:
self.log.exception("Error sending password")
return self.get_login_response(mxid=user.mxid, state="password", status=500,
- error="Internal server error while sending password.")
+ error="Internal server error while sending password.")
diff --git a/mautrix_telegram/provisioning_api.py b/mautrix_telegram/web/provisioning/__init__.py
similarity index 100%
rename from mautrix_telegram/provisioning_api.py
rename to mautrix_telegram/web/provisioning/__init__.py
diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml
new file mode 100644
index 00000000..e69de29b
diff --git a/mautrix_telegram/public/__init__.py b/mautrix_telegram/web/public/__init__.py
similarity index 96%
rename from mautrix_telegram/public/__init__.py
rename to mautrix_telegram/web/public/__init__.py
index 60e22fd6..c08b1bc6 100644
--- a/mautrix_telegram/public/__init__.py
+++ b/mautrix_telegram/web/public/__init__.py
@@ -20,13 +20,8 @@ import asyncio
import pkg_resources
import logging
-from telethon.errors import *
-
-from ..user import User
-from ..commands.auth import enter_password
-from ..util import format_duration
-
-from .auth_api import AuthAPI
+from ...user import User
+from ..common import AuthAPI
class PublicBridgeWebsite(AuthAPI):
diff --git a/mautrix_telegram/public/favicon.png b/mautrix_telegram/web/public/favicon.png
similarity index 100%
rename from mautrix_telegram/public/favicon.png
rename to mautrix_telegram/web/public/favicon.png
diff --git a/mautrix_telegram/public/login.css b/mautrix_telegram/web/public/login.css
similarity index 100%
rename from mautrix_telegram/public/login.css
rename to mautrix_telegram/web/public/login.css
diff --git a/mautrix_telegram/public/login.html.mako b/mautrix_telegram/web/public/login.html.mako
similarity index 100%
rename from mautrix_telegram/public/login.html.mako
rename to mautrix_telegram/web/public/login.html.mako
From f07009d0d2d509b22398d77a5f13a9633658faaa Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sun, 24 Jun 2018 23:19:29 +0300
Subject: [PATCH 08/65] Add initial parts of provisioning API spec
---
mautrix_telegram/web/provisioning/spec.yaml | 70 +++++++++++++++++++++
1 file changed, 70 insertions(+)
diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml
index e69de29b..7b0dc01f 100644
--- a/mautrix_telegram/web/provisioning/spec.yaml
+++ b/mautrix_telegram/web/provisioning/spec.yaml
@@ -0,0 +1,70 @@
+tags:
+ -
+ name: login
+ description: 'Authentication endpoints.'
+definitions:
+ Error:
+ x-oad-type: object
+ type: object
+ title: Error
+ properties:
+ errcode:
+ x-oad-type: string
+ type: string
+ title: 'Error code'
+ description: 'A machine-readable error code'
+ error:
+ x-oad-type: string
+ type: string
+ title: Error
+ description: 'A human-readable description of the error'
+ status:
+ x-oad-type: integer
+ type: integer
+ title: Status
+ description: 'The HTTP status code'
+ format: int32
+ AuthSuccess:
+ x-oad-type: object
+ type: object
+ properties:
+ state:
+ x-oad-type: string
+ type: string
+ enum:
+ - code
+ - request
+ - password
+ - token
+ - logged-in
+security:
+ -
+ Bearer: []
+securityDefinitions:
+ Bearer:
+ description: 'Required authentication for all endpoints'
+ name: Authorization
+ in: header
+ type: apiKey
+info:
+ title: 'mautrix-telegram provisioning'
+ version: 0.3.0
+ description: 'The provisioning API for mautrix-telegram.'
+ contact:
+ name: 'Tulir Asokan'
+ email: tulir@maunium.net
+ url: 'https://maunium.net'
+ license:
+ name: AGPLv3
+ url: 'https://github.com/tulir/mautrix-telegram/blob/master/LICENSE'
+externalDocs:
+ description: 'Provisioning API wiki page on GitHub.'
+ url: 'https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API'
+basePath: /_matrix/provisioning
+schemes:
+ - https
+consumes:
+ - application/json
+produces:
+ - application/json
+swagger: '2.0'
From c0ceb1b2b007ad0f878a4c11ae993e7eb7f59b24 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Thu, 12 Jul 2018 23:45:15 +0300
Subject: [PATCH 09/65] Move post_login_token to common/auth_api
---
mautrix_telegram/web/common/auth_api.py | 13 +++++++++++++
mautrix_telegram/web/public/__init__.py | 13 -------------
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py
index 9502db53..3ce557ba 100644
--- a/mautrix_telegram/web/common/auth_api.py
+++ b/mautrix_telegram/web/common/auth_api.py
@@ -68,6 +68,19 @@ class AuthAPI(abc.ABC):
return self.get_login_response(mxid=user.mxid, state="request", status=500,
error="Internal server error while requesting code.")
+ async def post_login_token(self, user, token):
+ try:
+ user_info = await user.client.sign_in(bot_token=token)
+ 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
+ return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
+ username=user_info.username)
+ except Exception:
+ self.log.exception("Error sending bot token")
+ return self.get_login_response(mxid=user.mxid, state="token", status=500,
+ error="Internal server error while sending token.")
+
async def post_login_code(self, user, code, password_in_data):
try:
user_info = await user.client.sign_in(code=code)
diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py
index c08b1bc6..89a76fd7 100644
--- a/mautrix_telegram/web/public/__init__.py
+++ b/mautrix_telegram/web/public/__init__.py
@@ -62,19 +62,6 @@ class PublicBridgeWebsite(AuthAPI):
text=self.login.render(username=username, state=state, error=error,
message=message, mxid=mxid))
- async def post_login_token(self, user, token):
- try:
- user_info = await user.client.sign_in(bot_token=token)
- 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
- return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
- username=user_info.username)
- except Exception:
- self.log.exception("Error sending bot token")
- return self.get_login_response(mxid=user.mxid, state="token", status=500,
- error="Internal server error while sending token.")
-
async def post_login(self, request):
data = await request.post()
if "mxid" not in data:
From 1fd920255fc15856d82b96c7322febfd6848e81b Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 21:25:51 +0300
Subject: [PATCH 10/65] Finish initial provisioning API spec and impl
---
mautrix_telegram/web/common/auth_api.py | 44 +-
mautrix_telegram/web/provisioning/__init__.py | 68 ++-
mautrix_telegram/web/provisioning/spec.yaml | 437 +++++++++++++++---
mautrix_telegram/web/public/__init__.py | 1 -
4 files changed, 480 insertions(+), 70 deletions(-)
diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py
index 3ce557ba..6980f1af 100644
--- a/mautrix_telegram/web/common/auth_api.py
+++ b/mautrix_telegram/web/common/auth_api.py
@@ -43,29 +43,34 @@ class AuthAPI(abc.ABC):
message="Code requested successfully.")
except PhoneNumberInvalidError:
return self.get_login_response(mxid=user.mxid, state="request", status=400,
+ errcode="phone_number_invalid",
error="Invalid phone number.")
+ except PhoneNumberBannedError:
+ return self.get_login_response(mxid=user.mxid, state="request", status=403,
+ errcode="phone_number_banned",
+ error="Your phone number is banned from Telegram.")
+ except PhoneNumberAppSignupForbiddenError:
+ return self.get_login_response(mxid=user.mxid, state="request", status=403,
+ errcode="phone_number_app_signup_forbidden",
+ error="You have disabled 3rd party apps on your account.")
except PhoneNumberUnoccupiedError:
return self.get_login_response(mxid=user.mxid, state="request", status=404,
+ errcode="phone_number_unoccupied",
error="That phone number has not been registered.")
except PhoneNumberFloodError:
return self.get_login_response(
- mxid=user.mxid, state="request", status=429,
+ mxid=user.mxid, state="request", status=429, errcode="phone_number_flood",
error="Your phone number has been temporarily blocked for flooding. "
"The ban is usually applied for around a day.")
except FloodWaitError as e:
return self.get_login_response(
- mxid=user.mxid, state="request", status=429,
+ mxid=user.mxid, state="request", status=429, errcode="flood_wait",
error="Your phone number has been temporarily blocked for flooding. "
f"Please wait for {format_duration(e.seconds)} before trying again.")
- except PhoneNumberBannedError:
- return self.get_login_response(mxid=user.mxid, state="request", status=401,
- error="Your phone number is banned from Telegram.")
- except PhoneNumberAppSignupForbiddenError:
- return self.get_login_response(mxid=user.mxid, state="request", status=401,
- error="You have disabled 3rd party apps on your account.")
except Exception:
self.log.exception("Error requesting phone code")
return self.get_login_response(mxid=user.mxid, state="request", status=500,
+ errcode="exception",
error="Internal server error while requesting code.")
async def post_login_token(self, user, token):
@@ -76,6 +81,14 @@ class AuthAPI(abc.ABC):
user.command_status = None
return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
username=user_info.username)
+ except AccessTokenInvalidError:
+ return self.get_login_response(mxid=user.mxid, state="token", status=401,
+ errcode="bot_token_invalid",
+ error="Bot token invalid.")
+ except AccessTokenExpiredError:
+ return self.get_login_response(mxid=user.mxid, state="token", status=403,
+ errcode="bot_token_expired",
+ error="Bot token expired.")
except Exception:
self.log.exception("Error sending bot token")
return self.get_login_response(mxid=user.mxid, state="token", status=500,
@@ -90,10 +103,12 @@ class AuthAPI(abc.ABC):
return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
username=user_info.username)
except PhoneCodeInvalidError:
- return self.get_login_response(mxid=user.mxid, state="code", status=403,
+ return self.get_login_response(mxid=user.mxid, state="code", status=401,
+ errcode="phone_code_invalid",
error="Incorrect phone code.")
except PhoneCodeExpiredError:
return self.get_login_response(mxid=user.mxid, state="code", status=403,
+ errcode="phone_code_expired",
error="Phone code expired.")
except SessionPasswordNeededError:
if not password_in_data:
@@ -103,12 +118,13 @@ class AuthAPI(abc.ABC):
"action": "Login (password entry)",
}
return self.get_login_response(
- mxid=user.mxid, state="password", status=200,
+ mxid=user.mxid, state="password", status=202,
message="Code accepted, but you have 2-factor authentication is enabled.")
return None
except Exception:
self.log.exception("Error sending phone code")
return self.get_login_response(mxid=user.mxid, state="code", status=500,
+ errcode="exception",
error="Internal server error while sending code.")
async def post_login_password(self, user, password):
@@ -119,10 +135,16 @@ class AuthAPI(abc.ABC):
user.command_status = None
return self.get_login_response(mxid=user.mxid, state="logged-in", status=200,
username=user_info.username)
- except (PasswordHashInvalidError, PasswordEmptyError):
+ except PasswordEmptyError:
return self.get_login_response(mxid=user.mxid, state="password", status=400,
+ errcode="password_empty",
+ error="Empty password.")
+ except PasswordHashInvalidError:
+ return self.get_login_response(mxid=user.mxid, state="password", status=401,
+ errcode="password_invalid",
error="Incorrect password.")
except Exception:
self.log.exception("Error sending password")
return self.get_login_response(mxid=user.mxid, state="password", status=500,
+ errcode="exception",
error="Internal server error while sending password.")
diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py
index db89c506..05c73816 100644
--- a/mautrix_telegram/web/provisioning/__init__.py
+++ b/mautrix_telegram/web/provisioning/__init__.py
@@ -17,11 +17,75 @@
from aiohttp import web
import logging
+from ..common import AuthAPI
-class ProvisioningAPI:
+
+class ProvisioningAPI(AuthAPI):
log = logging.getLogger("mau.provisioning")
def __init__(self, loop):
- self.loop = loop
+ super(AuthAPI, self).__init__(loop)
self.app = web.Application(loop=loop)
+
+ login_prefix = "/login/{mxid:@[^:]*:.+}"
+ self.app.router.add_route("POST", f"{login_prefix}/bot_token", self.send_bot_token)
+ self.app.router.add_route("POST", f"{login_prefix}/request_code", self.request_code)
+ self.app.router.add_route("POST", f"{login_prefix}/send_code", self.send_code)
+ self.app.router.add_route("POST", f"{login_prefix}/send_password", self.send_password)
+
+ def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
+ errcode=""):
+ if username:
+ resp = {
+ "state": "logged-in",
+ "username": username,
+ }
+ elif message:
+ resp = {
+ "message": message
+ }
+ else:
+ resp = {
+ "error": error,
+ "errcode": errcode,
+ }
+ return web.json_response(resp, status=status)
+
+ async def get_user(self, request: web.Request):
+ mxid = request.match_info["mxid"]
+ user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True)
+ if not user.puppet_whitelisted:
+ return user, self.get_login_response(mxid=user.mxid, error="You are not whitelisted.",
+ errcode="mxid_not_whitelisted", status=403)
+ elif await user.is_logged_in():
+ return user, self.get_login_response(mxid=user.mxid, username=user.username, status=409)
+ return user, None
+
+ async def send_bot_token(self, request: web.Request):
+ user, err = await self.get_user(request)
+ if err:
+ return err
+ data = await request.json()
+ return await self.post_login_token(user, data.get("token", ""))
+
+ async def request_code(self, request: web.Request):
+ user, err = await self.get_user(request)
+ if err:
+ return err
+ data = await request.json()
+ return await self.post_login_phone(user, data.get("phone", ""))
+
+ async def send_code(self, request: web.Request):
+ user, err = await self.get_user(request)
+ if err:
+ return err
+ data = await request.json()
+ return await self.post_login_code(user, data.get("code", 0), password_in_data=False)
+
+ async def send_password(self, request: web.Request):
+ user, err = await self.get_user(request)
+ if err:
+ return err
+ data = await request.json()
+ return await self.post_login_password(user, data.get("password", ""))
diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml
index 7b0dc01f..e8fe08f3 100644
--- a/mautrix_telegram/web/provisioning/spec.yaml
+++ b/mautrix_telegram/web/provisioning/spec.yaml
@@ -1,70 +1,395 @@
+swagger: "2.0"
+
+info:
+ title: mautrix-telegram provisioning
+ version: 0.3.0
+ description: The provisioning API for mautrix-telegram.
+ contact:
+ name: Tulir Asokan
+ email: tulir@maunium.net
+ url: https://maunium.net
+ license:
+ name: AGPLv3
+ url: https://github.com/tulir/mautrix-telegram/blob/master/LICENSE
+
+externalDocs:
+ description: Provisioning API wiki page on GitHub.
+ url: https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API
+
+basePath: /_matrix/provision
+
+schemes: [https]
+consumes: [application/json]
+produces: [application/json]
+
tags:
- -
- name: login
- description: 'Authentication endpoints.'
+- name: Authentication
+
+paths:
+ /login/{mxid}/bot_token:
+ post:
+ operationId: post_bot_token
+ summary: Log in with a bot token
+ tags: [Authentication]
+ responses:
+ 200:
+ description: Login successful
+ schema:
+ $ref: "#/definitions/AuthSuccess"
+ 400:
+ $ref: "#/responses/MissingMXIDError"
+ 401:
+ description: Invalid or expired bot token
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ example: bot_token_
+ enum:
+ - bot_token_invalid
+ - bot_token_expired
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 403:
+ $ref: "#/responses/NotWhitelistedError"
+ 409:
+ $ref: "#/responses/AlreadyLoggedInError"
+ 500:
+ $ref: "#/responses/UnknownError"
+ parameters:
+ - name: mxid
+ in: path
+ description: The Matrix ID of the user who to log in as
+ required: true
+ type: string
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ token:
+ type: string
+ description: The access token of the bot to log in as
+ example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv"
+ /login/{mxid}/request_code:
+ post:
+ operationId: post_login_phone
+ summary: Request a phone code from Telegram
+ tags: [Authentication]
+ responses:
+ 200:
+ description: Code requested successfully
+ schema:
+ $ref: "#/definitions/AuthSuccess"
+ 400:
+ description: Invalid phone number or missing Matrix ID
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ example: machine_readable_error
+ enum:
+ - phone_number_invalid
+ - mxid_empty
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 403:
+ description: Matrix ID is not whitelisted or phone number is banned or has forbidden 3rd party apps
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ example: machine_readable_error
+ enum:
+ - mxid_not_whitelisted
+ - phone_number_banned
+ - phone_number_app_signup_forbidden
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 404:
+ description: Unregistered phone number
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - phone_number_unoccupied
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 409:
+ $ref: "#/responses/AlreadyLoggedInError"
+ 429:
+ description: Phone number has been temporarily blocked for flooding
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - flood_wait
+ - phone_number_flood
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 500:
+ $ref: "#/responses/UnknownError"
+ parameters:
+ - name: mxid
+ in: path
+ description: The Matrix ID of the user who to log in as
+ required: true
+ type: string
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ phone:
+ type: string
+ description: The phone number to log in as.
+ example: "+123456789"
+ /login/{mxid}/send_code:
+ post:
+ operationId: post_login_code
+ summary: Send the login code
+ tags: [Authentication]
+ responses:
+ 200:
+ description: Login successful
+ schema:
+ $ref: "#/definitions/AuthSuccess"
+ 202:
+ description: Correct code, but two-factor authentication is enabled
+ schema:
+ $ref: "#/definitions/AuthSuccess"
+ 400:
+ $ref: "#/responses/MissingMXIDError"
+ 401:
+ description: Invalid phone code
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - phone_code_invalid
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 403:
+ description: Matrix ID not whitelisted or phone code expired
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ example: machine_readable_error
+ enum:
+ - mxid_not_whitelisted
+ - phone_code_expired
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 409:
+ $ref: "#/responses/AlreadyLoggedInError"
+ 500:
+ $ref: "#/responses/UnknownError"
+ parameters:
+ - name: mxid
+ in: path
+ description: The Matrix ID of the user who to log in as
+ required: true
+ type: string
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ code:
+ type: integer
+ description: The phone code from Telegram.
+ format: int32
+ example: 123456
+ /login/{mxid}/send_password:
+ post:
+ operationId: post_login_password
+ summary: Send the two-factor auth password
+ tags: [Authentication]
+ responses:
+ 200:
+ description: Login successful
+ schema:
+ $ref: "#/definitions/AuthSuccess"
+ 400:
+ description: Missing password or Matrix ID
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ example: _empty
+ enum:
+ - password_empty
+ - mxid_empty
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 401:
+ description: Incorrect password
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - password_invalid
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 403:
+ $ref: "#/responses/NotWhitelistedError"
+ 409:
+ $ref: "#/responses/AlreadyLoggedInError"
+ 500:
+ $ref: "#/responses/UnknownError"
+ parameters:
+ - name: mxid
+ in: path
+ description: The Matrix ID of the user who to log in as
+ required: true
+ type: string
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ password:
+ type: string
+ description: The two-factor auth password
+ format: password
+ example: hunter2
+
+responses:
+ NotWhitelistedError:
+ description: Matrix ID not whitelisted for puppeting
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - mxid_not_whitelisted
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ AlreadyLoggedInError:
+ description: The Matrix user is already logged in
+ schema:
+ type: object
+ properties:
+ state:
+ type: string
+ enum:
+ - logged-in
+ username:
+ type: string
+ description: The Telegram username the user is logged in as.
+ MissingMXIDError:
+ description: Missing Matrix ID
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - mxid_empty
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ UnknownError:
+ description: Unknown error
+ schema:
+ type: object
+ title: UnknownError
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - exception
+ error:
+ type: string
+ title: Error
+ description: A human-readable description of the error
+ example: Internal server error while .
+ enum:
+ - Internal server error while requesting code.
+ - Internal server error while sending code.
+ - Internal server error while sending password.
+ - Internal server error while sending token.
+
definitions:
- Error:
- x-oad-type: object
- type: object
- title: Error
- properties:
- errcode:
- x-oad-type: string
- type: string
- title: 'Error code'
- description: 'A machine-readable error code'
- error:
- x-oad-type: string
- type: string
- title: Error
- description: 'A human-readable description of the error'
- status:
- x-oad-type: integer
- type: integer
- title: Status
- description: 'The HTTP status code'
- format: int32
+ HumanReadableError:
+ type: string
+ description: A human-readable description of the error
+ example: A human-readable description of the error
AuthSuccess:
- x-oad-type: object
type: object
properties:
state:
- x-oad-type: string
type: string
+ description: The state/next step after the successful operation.
enum:
- - code
- - request
- - password
- - token
- - logged-in
+ - code
+ - request
+ - password
+ - token
+ - logged-in
+ username:
+ type: string
+ description: The Telegram username the user is logged in as. Only applicable if state=logged-in
+
+
security:
- -
- Bearer: []
+ - Bearer: []
securityDefinitions:
Bearer:
- description: 'Required authentication for all endpoints'
+ description: Required authentication for all endpoints
name: Authorization
in: header
type: apiKey
-info:
- title: 'mautrix-telegram provisioning'
- version: 0.3.0
- description: 'The provisioning API for mautrix-telegram.'
- contact:
- name: 'Tulir Asokan'
- email: tulir@maunium.net
- url: 'https://maunium.net'
- license:
- name: AGPLv3
- url: 'https://github.com/tulir/mautrix-telegram/blob/master/LICENSE'
-externalDocs:
- description: 'Provisioning API wiki page on GitHub.'
- url: 'https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API'
-basePath: /_matrix/provisioning
-schemes:
- - https
-consumes:
- - application/json
-produces:
- - application/json
-swagger: '2.0'
diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py
index 89a76fd7..7a83d731 100644
--- a/mautrix_telegram/web/public/__init__.py
+++ b/mautrix_telegram/web/public/__init__.py
@@ -16,7 +16,6 @@
# along with this program. If not, see .
from aiohttp import web
from mako.template import Template
-import asyncio
import pkg_resources
import logging
From bc160e0593a3a2d26c6429b84a24631d47d07013 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 22:11:05 +0300
Subject: [PATCH 11/65] Update logger names
---
mautrix_telegram/web/common/auth_api.py | 2 +-
mautrix_telegram/web/provisioning/__init__.py | 2 +-
mautrix_telegram/web/public/__init__.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/mautrix_telegram/web/common/auth_api.py b/mautrix_telegram/web/common/auth_api.py
index 6980f1af..2ea50bca 100644
--- a/mautrix_telegram/web/common/auth_api.py
+++ b/mautrix_telegram/web/common/auth_api.py
@@ -26,7 +26,7 @@ from mautrix_telegram.util import format_duration
class AuthAPI(abc.ABC):
- log = logging.getLogger("mau.public.auth")
+ log = logging.getLogger("mau.web.auth")
def __init__(self, loop):
self.loop = loop
diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py
index 05c73816..c2f087a9 100644
--- a/mautrix_telegram/web/provisioning/__init__.py
+++ b/mautrix_telegram/web/provisioning/__init__.py
@@ -21,7 +21,7 @@ from ..common import AuthAPI
class ProvisioningAPI(AuthAPI):
- log = logging.getLogger("mau.provisioning")
+ log = logging.getLogger("mau.web.provisioning")
def __init__(self, loop):
super(AuthAPI, self).__init__(loop)
diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py
index 7a83d731..43738d8e 100644
--- a/mautrix_telegram/web/public/__init__.py
+++ b/mautrix_telegram/web/public/__init__.py
@@ -24,7 +24,7 @@ from ..common import AuthAPI
class PublicBridgeWebsite(AuthAPI):
- log = logging.getLogger("mau.public")
+ log = logging.getLogger("mau.web.public")
def __init__(self, loop):
super(AuthAPI, self).__init__(loop)
From 48665acf1d16cc1e7abbfd17aa8b12790398508a Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 22:14:04 +0300
Subject: [PATCH 12/65] Fix imports and other mistakes
---
mautrix_telegram/web/provisioning/__init__.py | 3 ++-
mautrix_telegram/web/public/__init__.py | 8 ++++----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py
index c2f087a9..f5d56c5e 100644
--- a/mautrix_telegram/web/provisioning/__init__.py
+++ b/mautrix_telegram/web/provisioning/__init__.py
@@ -17,6 +17,7 @@
from aiohttp import web
import logging
+from ...user import User
from ..common import AuthAPI
@@ -24,7 +25,7 @@ class ProvisioningAPI(AuthAPI):
log = logging.getLogger("mau.web.provisioning")
def __init__(self, loop):
- super(AuthAPI, self).__init__(loop)
+ super().__init__(loop)
self.app = web.Application(loop=loop)
diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py
index 43738d8e..eab1f5fe 100644
--- a/mautrix_telegram/web/public/__init__.py
+++ b/mautrix_telegram/web/public/__init__.py
@@ -27,16 +27,16 @@ class PublicBridgeWebsite(AuthAPI):
log = logging.getLogger("mau.web.public")
def __init__(self, loop):
- super(AuthAPI, self).__init__(loop)
+ super().__init__(loop)
self.login = Template(
- pkg_resources.resource_string("mautrix_telegram", "public/login.html.mako"))
+ pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako"))
self.app = web.Application(loop=loop)
self.app.router.add_route("GET", "/login", self.get_login)
self.app.router.add_route("POST", "/login", self.post_login)
- self.app.router.add_static("/",
- pkg_resources.resource_filename("mautrix_telegram", "public/"))
+ self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_telegram",
+ "web/public/"))
async def get_login(self, request):
state = "token" if request.rel_url.query.get("mode", "") == "bot" else "request"
From 5082cd1c94308be66f7b4fa07389a13828de9509 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 22:28:43 +0300
Subject: [PATCH 13/65] Fix bad JSON handling and include state in all
responses
---
mautrix_telegram/web/provisioning/__init__.py | 34 +++++++++++--------
1 file changed, 20 insertions(+), 14 deletions(-)
diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py
index f5d56c5e..909725c6 100644
--- a/mautrix_telegram/web/provisioning/__init__.py
+++ b/mautrix_telegram/web/provisioning/__init__.py
@@ -44,49 +44,55 @@ class ProvisioningAPI(AuthAPI):
}
elif message:
resp = {
- "message": message
+ "state": state,
+ "message": message,
}
else:
resp = {
+ "state": state,
"error": error,
"errcode": errcode,
}
return web.json_response(resp, status=status)
- async def get_user(self, request: web.Request):
+ async def get_request_info(self, request: web.Request):
mxid = request.match_info["mxid"]
user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True)
if not user.puppet_whitelisted:
- return user, self.get_login_response(mxid=user.mxid, error="You are not whitelisted.",
- errcode="mxid_not_whitelisted", status=403)
+ return None, user, self.get_login_response(mxid=user.mxid,
+ error="You are not whitelisted.",
+ errcode="mxid_not_whitelisted", status=403)
elif await user.is_logged_in():
- return user, self.get_login_response(mxid=user.mxid, username=user.username, status=409)
- return user, None
+ return None, user, self.get_login_response(mxid=user.mxid, username=user.username,
+ status=409)
+
+ try:
+ data = await request.json()
+ except Exception:
+ return None, user, self.get_login_response(mxid=user.mxid, error="Invalid JSON.",
+ errcode="invalid_json", status=400)
+ return data, user, None
async def send_bot_token(self, request: web.Request):
- user, err = await self.get_user(request)
+ data, user, err = await self.get_request_info(request)
if err:
return err
- data = await request.json()
return await self.post_login_token(user, data.get("token", ""))
async def request_code(self, request: web.Request):
- user, err = await self.get_user(request)
+ data, user, err = await self.get_request_info(request)
if err:
return err
- data = await request.json()
return await self.post_login_phone(user, data.get("phone", ""))
async def send_code(self, request: web.Request):
- user, err = await self.get_user(request)
+ data, user, err = await self.get_request_info(request)
if err:
return err
- data = await request.json()
return await self.post_login_code(user, data.get("code", 0), password_in_data=False)
async def send_password(self, request: web.Request):
- user, err = await self.get_user(request)
+ data, user, err = await self.get_request_info(request)
if err:
return err
- data = await request.json()
return await self.post_login_password(user, data.get("password", ""))
From 998e2fa19cb76ac34dc8cad83df94cf35838979b Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 22:46:38 +0300
Subject: [PATCH 14/65] Enable aiohttp logging by default
---
example-config.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/example-config.yaml b/example-config.yaml
index 6f9e8df5..1d5cfdb7 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -239,6 +239,8 @@ logging:
level: DEBUG
telethon:
level: DEBUG
+ aiohttp:
+ level: INFO
root:
level: DEBUG
handlers: [file, console]
From 94a2344f3bc8a1eadde4d04cd00e5923421afe40 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 22:47:09 +0300
Subject: [PATCH 15/65] Enable and spec authorization and json validation
---
mautrix_telegram/__main__.py | 2 +-
mautrix_telegram/web/provisioning/__init__.py | 39 ++++++++++++-------
mautrix_telegram/web/provisioning/spec.yaml | 32 ++++++++++++---
3 files changed, 51 insertions(+), 22 deletions(-)
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index 9702468f..c8865686 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -93,7 +93,7 @@ if config["appservice.public.enabled"]:
appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public.app)
if config["appservice.provisioning.enabled"]:
- provisioning_api = ProvisioningAPI(loop)
+ provisioning_api = ProvisioningAPI(config, loop)
appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning",
provisioning_api.app)
diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py
index 909725c6..1c609aaf 100644
--- a/mautrix_telegram/web/provisioning/__init__.py
+++ b/mautrix_telegram/web/provisioning/__init__.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
from aiohttp import web
import logging
+import json
from ...user import User
from ..common import AuthAPI
@@ -24,8 +25,9 @@ from ..common import AuthAPI
class ProvisioningAPI(AuthAPI):
log = logging.getLogger("mau.web.provisioning")
- def __init__(self, loop):
+ def __init__(self, config, loop):
super().__init__(loop)
+ self.secret = config["appservice.provisioning.shared_secret"]
self.app = web.Application(loop=loop)
@@ -56,43 +58,50 @@ class ProvisioningAPI(AuthAPI):
return web.json_response(resp, status=status)
async def get_request_info(self, request: web.Request):
+ auth = request.headers.get("Authorization", "")
+ if auth != f"Bearer {self.secret}":
+ return None, None, self.get_login_response(error="Shared secret is not valid.",
+ errcode="shared_secret_invalid",
+ status=401)
+
+ data = None
+ try:
+ data = await request.json()
+ except json.JSONDecodeError:
+ pass
+ if not data:
+ return None, None, self.get_login_response(error="Invalid JSON.",
+ errcode="json_invalid", status=400)
+
mxid = request.match_info["mxid"]
user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True)
if not user.puppet_whitelisted:
- return None, user, self.get_login_response(mxid=user.mxid,
- error="You are not whitelisted.",
+ return None, user, self.get_login_response(error="You are not whitelisted.",
errcode="mxid_not_whitelisted", status=403)
elif await user.is_logged_in():
- return None, user, self.get_login_response(mxid=user.mxid, username=user.username,
- status=409)
-
- try:
- data = await request.json()
- except Exception:
- return None, user, self.get_login_response(mxid=user.mxid, error="Invalid JSON.",
- errcode="invalid_json", status=400)
+ return None, user, self.get_login_response(username=user.username, status=409)
return data, user, None
async def send_bot_token(self, request: web.Request):
data, user, err = await self.get_request_info(request)
- if err:
+ if err is not None:
return err
return await self.post_login_token(user, data.get("token", ""))
async def request_code(self, request: web.Request):
data, user, err = await self.get_request_info(request)
- if err:
+ if err is not None:
return err
return await self.post_login_phone(user, data.get("phone", ""))
async def send_code(self, request: web.Request):
data, user, err = await self.get_request_info(request)
- if err:
+ if err is not None:
return err
return await self.post_login_code(user, data.get("code", 0), password_in_data=False)
async def send_password(self, request: web.Request):
data, user, err = await self.get_request_info(request)
- if err:
+ if err is not None:
return err
return await self.post_login_password(user, data.get("password", ""))
diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml
index e8fe08f3..aa911d46 100644
--- a/mautrix_telegram/web/provisioning/spec.yaml
+++ b/mautrix_telegram/web/provisioning/spec.yaml
@@ -39,7 +39,7 @@ paths:
400:
$ref: "#/responses/MissingMXIDError"
401:
- description: Invalid or expired bot token
+ description: Invalid or expired bot token or invalid shared secret
schema:
type: object
title: Error
@@ -52,6 +52,7 @@ paths:
enum:
- bot_token_invalid
- bot_token_expired
+ - shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
@@ -87,7 +88,7 @@ paths:
schema:
$ref: "#/definitions/AuthSuccess"
400:
- description: Invalid phone number or missing Matrix ID
+ description: Invalid phone number or JSON or missing Matrix ID
schema:
type: object
title: Error
@@ -100,6 +101,21 @@ paths:
enum:
- phone_number_invalid
- mxid_empty
+ - json_invalid
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 401:
+ description: Invalid shared secret
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
@@ -185,7 +201,7 @@ paths:
400:
$ref: "#/responses/MissingMXIDError"
401:
- description: Invalid phone code
+ description: Invalid phone code or shared secret
schema:
type: object
title: Error
@@ -196,6 +212,7 @@ paths:
description: A machine-readable error code
enum:
- phone_code_invalid
+ - shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
@@ -246,7 +263,7 @@ paths:
schema:
$ref: "#/definitions/AuthSuccess"
400:
- description: Missing password or Matrix ID
+ description: Missing password or Matrix ID or invalid JSON
schema:
type: object
title: Error
@@ -259,10 +276,11 @@ paths:
enum:
- password_empty
- mxid_empty
+ - json_invalid
error:
$ref: "#/definitions/HumanReadableError"
401:
- description: Incorrect password
+ description: Incorrect password or invalid shared secret
schema:
type: object
title: Error
@@ -273,6 +291,7 @@ paths:
description: A machine-readable error code
enum:
- password_invalid
+ - shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
@@ -327,7 +346,7 @@ responses:
type: string
description: The Telegram username the user is logged in as.
MissingMXIDError:
- description: Missing Matrix ID
+ description: Missing Matrix ID or invalid JSON.
schema:
type: object
title: Error
@@ -338,6 +357,7 @@ responses:
description: A machine-readable error code
enum:
- mxid_empty
+ - json_invalid
error:
$ref: "#/definitions/HumanReadableError"
UnknownError:
From ac4d7cc412440daa0a3d64287e63c8af7aad2814 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 22:58:07 +0300
Subject: [PATCH 16/65] Add /get_me endpoint
---
mautrix_telegram/web/provisioning/__init__.py | 37 ++++++++---
mautrix_telegram/web/provisioning/spec.yaml | 64 +++++++++++++++++++
2 files changed, 92 insertions(+), 9 deletions(-)
diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py
index 1c609aaf..95919c58 100644
--- a/mautrix_telegram/web/provisioning/__init__.py
+++ b/mautrix_telegram/web/provisioning/__init__.py
@@ -31,6 +31,7 @@ class ProvisioningAPI(AuthAPI):
self.app = web.Application(loop=loop)
+ self.app.router.add_route("GET", "/{mxid:@[^:]*:.+}/get_me", self.get_me)
login_prefix = "/login/{mxid:@[^:]*:.+}"
self.app.router.add_route("POST", f"{login_prefix}/bot_token", self.send_bot_token)
self.app.router.add_route("POST", f"{login_prefix}/request_code", self.request_code)
@@ -57,7 +58,7 @@ class ProvisioningAPI(AuthAPI):
}
return web.json_response(resp, status=status)
- async def get_request_info(self, request: web.Request):
+ async def get_request_info(self, request: web.Request, get_data=True, fail_on_logged_in=True):
auth = request.headers.get("Authorization", "")
if auth != f"Bearer {self.secret}":
return None, None, self.get_login_response(error="Shared secret is not valid.",
@@ -65,23 +66,41 @@ class ProvisioningAPI(AuthAPI):
status=401)
data = None
- try:
- data = await request.json()
- except json.JSONDecodeError:
- pass
- if not data:
- return None, None, self.get_login_response(error="Invalid JSON.",
- errcode="json_invalid", status=400)
+ if get_data:
+ try:
+ data = await request.json()
+ except json.JSONDecodeError:
+ pass
+ if not data:
+ return None, None, self.get_login_response(error="Invalid JSON.",
+ errcode="json_invalid", status=400)
mxid = request.match_info["mxid"]
user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True)
if not user.puppet_whitelisted:
return None, user, self.get_login_response(error="You are not whitelisted.",
errcode="mxid_not_whitelisted", status=403)
- elif await user.is_logged_in():
+ elif fail_on_logged_in and await user.is_logged_in():
return None, user, self.get_login_response(username=user.username, status=409)
return data, user, None
+ async def get_me(self, request: web.Request):
+ data, user, err = await self.get_request_info(request, get_data=False,
+ fail_on_logged_in=False)
+ if err is not None:
+ return err
+ if not await user.is_logged_in():
+ return self.get_login_response(status=403, error="You are not logged in.",
+ errcode="not_logged_in")
+ me = await user.client.get_me()
+ return web.json_response({
+ "username": me.username,
+ "first_name": me.first_name,
+ "last_name": me.last_name,
+ "phone": me.phone,
+ "is_bot": me.bot,
+ })
+
async def send_bot_token(self, request: web.Request):
data, user, err = await self.get_request_info(request)
if err is not None:
diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml
index aa911d46..5766af2e 100644
--- a/mautrix_telegram/web/provisioning/spec.yaml
+++ b/mautrix_telegram/web/provisioning/spec.yaml
@@ -26,6 +26,51 @@ tags:
- name: Authentication
paths:
+ /{mxid}/get_me:
+ get:
+ operationId: get_me
+ summary: Get the info of the Telegram user the given Matrix user is logged in as
+ tags: [Authentication]
+ responses:
+ 200:
+ description: User is logged in
+ schema:
+ $ref: "#/definitions/AuthInfo"
+ 400:
+ $ref: "#/responses/MissingMXIDError"
+ 403:
+ description: User is not logged in or not whitelisted
+ schema:
+ type: object
+ title: Error
+ properties:
+ errcode:
+ type: string
+ title: Error code
+ description: A machine-readable error code
+ enum:
+ - not_logged_in
+ - mxid_not_whitelisted
+ error:
+ $ref: "#/definitions/HumanReadableError"
+ 500:
+ $ref: "#/responses/UnknownError"
+ parameters:
+ - name: mxid
+ in: path
+ description: The Matrix ID of the user who to log in as
+ required: true
+ type: string
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ token:
+ type: string
+ description: The access token of the bot to log in as
+ example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv"
/login/{mxid}/bot_token:
post:
operationId: post_bot_token
@@ -388,6 +433,25 @@ definitions:
type: string
description: A human-readable description of the error
example: A human-readable description of the error
+ AuthInfo:
+ type: object
+ properties:
+ username:
+ type: string
+ example: username
+ first_name:
+ type: string
+ example: Usern
+ last_name:
+ type: string
+ example: A.
+ phone:
+ type: string
+ example: +123456789
+ is_bot:
+ type: boolean
+ example: false
+
AuthSuccess:
type: object
properties:
From f6fb37f5daa927a5b1465f34295462cd25d579b3 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 22:59:26 +0300
Subject: [PATCH 17/65] Update endpoint paths
---
mautrix_telegram/web/provisioning/__init__.py | 12 ++++++------
mautrix_telegram/web/provisioning/spec.yaml | 10 +++++-----
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py
index 95919c58..a792720b 100644
--- a/mautrix_telegram/web/provisioning/__init__.py
+++ b/mautrix_telegram/web/provisioning/__init__.py
@@ -31,12 +31,12 @@ class ProvisioningAPI(AuthAPI):
self.app = web.Application(loop=loop)
- self.app.router.add_route("GET", "/{mxid:@[^:]*:.+}/get_me", self.get_me)
- login_prefix = "/login/{mxid:@[^:]*:.+}"
- self.app.router.add_route("POST", f"{login_prefix}/bot_token", self.send_bot_token)
- self.app.router.add_route("POST", f"{login_prefix}/request_code", self.request_code)
- self.app.router.add_route("POST", f"{login_prefix}/send_code", self.send_code)
- self.app.router.add_route("POST", f"{login_prefix}/send_password", self.send_password)
+ auth_prefix = "/auth/{mxid:@[^:]*:.+}"
+ self.app.router.add_route("GET", f"{auth_prefix}/get_me", self.get_me)
+ self.app.router.add_route("POST", f"{auth_prefix}/send_bot_token", self.send_bot_token)
+ self.app.router.add_route("POST", f"{auth_prefix}/request_code", self.request_code)
+ self.app.router.add_route("POST", f"{auth_prefix}/send_code", self.send_code)
+ self.app.router.add_route("POST", f"{auth_prefix}/send_password", self.send_password)
def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
errcode=""):
diff --git a/mautrix_telegram/web/provisioning/spec.yaml b/mautrix_telegram/web/provisioning/spec.yaml
index 5766af2e..c0b5e3a6 100644
--- a/mautrix_telegram/web/provisioning/spec.yaml
+++ b/mautrix_telegram/web/provisioning/spec.yaml
@@ -26,7 +26,7 @@ tags:
- name: Authentication
paths:
- /{mxid}/get_me:
+ /auth/{mxid}/get_me:
get:
operationId: get_me
summary: Get the info of the Telegram user the given Matrix user is logged in as
@@ -71,7 +71,7 @@ paths:
type: string
description: The access token of the bot to log in as
example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv"
- /login/{mxid}/bot_token:
+ /auth/{mxid}/send_bot_token:
post:
operationId: post_bot_token
summary: Log in with a bot token
@@ -122,7 +122,7 @@ paths:
type: string
description: The access token of the bot to log in as
example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv"
- /login/{mxid}/request_code:
+ /auth/{mxid}/request_code:
post:
operationId: post_login_phone
summary: Request a phone code from Telegram
@@ -229,7 +229,7 @@ paths:
type: string
description: The phone number to log in as.
example: "+123456789"
- /login/{mxid}/send_code:
+ /auth/{mxid}/send_code:
post:
operationId: post_login_code
summary: Send the login code
@@ -297,7 +297,7 @@ paths:
description: The phone code from Telegram.
format: int32
example: 123456
- /login/{mxid}/send_password:
+ /auth/{mxid}/send_password:
post:
operationId: post_login_password
summary: Send the two-factor auth password
From 90e7a09b7e83a78359559da0b31625e1efc71b9d Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Fri, 13 Jul 2018 23:03:34 +0300
Subject: [PATCH 18/65] Automatically generate provisioning shared secret if it
has the default value
---
example-config.yaml | 2 +-
mautrix_telegram/config.py | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/example-config.yaml b/example-config.yaml
index 1d5cfdb7..eb6c57d8 100644
--- a/example-config.yaml
+++ b/example-config.yaml
@@ -41,7 +41,7 @@ appservice:
# The prefix to use in the provisioning API endpoints.
prefix: /_matrix/provision
# The shared secret to authorize users of the API.
- # You can generate a decent secret with `pwgen -snc 32 1`
+ # If you leave the default token, a random token will be generated and saved at startup.
shared_secret: "Very secret shared secret"
# The unique ID of this appservice.
diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py
index 07944b3c..facb4806 100644
--- a/mautrix_telegram/config.py
+++ b/mautrix_telegram/config.py
@@ -162,6 +162,8 @@ class Config(DictWithRecursion):
copy("appservice.provisioning.enabled")
copy("appservice.provisioning.prefix")
copy("appservice.provisioning.shared_secret")
+ if base["appservice.provisioning.shared_secret"] == "Very secret shared secret":
+ base["appservice.provisioning.shared_secret"] = self._new_token()
copy("appservice.id")
copy("appservice.bot_username")
From 298e326de7c71488104cfd4ca6b29cf5c7896973 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sat, 14 Jul 2018 14:39:49 +0300
Subject: [PATCH 19/65] Fix login command and add token login error handlers
---
mautrix_telegram/commands/auth.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py
index 8caf21a5..4c6b135c 100644
--- a/mautrix_telegram/commands/auth.py
+++ b/mautrix_telegram/commands/auth.py
@@ -174,7 +174,7 @@ async def enter_phone_or_token(evt: CommandEvent):
# phone numbers don't contain colons but telegram bot auth tokens do
if evt.args[0].find(":") > 0:
try:
- await sign_in(bot_token=evt.args[0])
+ await sign_in(evt, bot_token=evt.args[0])
except Exception:
evt.log.exception("Error sending auth token")
return await evt.reply("Unhandled exception while sending auth token. "
@@ -194,7 +194,7 @@ async def enter_code(evt: CommandEvent):
return await evt.reply("This bridge instance does not allow in-Matrix login. "
"Please use `$cmdprefix+sp login` to get login instructions")
try:
- await sign_in(code=evt.args[0])
+ await sign_in(evt, code=evt.args[0])
except Exception:
evt.log.exception("Error sending phone code")
return await evt.reply("Unhandled exception while sending code. "
@@ -209,12 +209,17 @@ async def enter_password(evt: CommandEvent):
return await evt.reply("This bridge instance does not allow in-Matrix login. "
"Please use `$cmdprefix+sp login` to get login instructions")
try:
- await sign_in(password=" ".join(evt.args))
+ await sign_in(evt, password=" ".join(evt.args))
+ except AccessTokenInvalidError:
+ return await evt.reply("That bot token is not valid.")
+ except AccessTokenExpiredError:
+ return await evt.reply("That bot token has expired.")
except Exception:
evt.log.exception("Error sending password")
return await evt.reply("Unhandled exception while sending password. "
"Check console for more details.")
+
async def sign_in(evt: CommandEvent, **sign_in_info):
try:
await evt.sender.ensure_started(even_if_no_session=True)
@@ -236,6 +241,7 @@ async def sign_in(evt: CommandEvent, **sign_in_info):
return await evt.reply("Your account has two-factor authentication. "
"Please send your password here.")
+
@command_handler(needs_auth=True,
help_section=SECTION_AUTH,
help_text="Log out from Telegram.")
From d97281bcdc20ef9df26952430a495d7fed4a2117 Mon Sep 17 00:00:00 2001
From: Tulir Asokan
Date: Sat, 14 Jul 2018 16:00:20 +0300
Subject: [PATCH 20/65] Require authentication for web login. Fixes #163
---
mautrix_telegram/__main__.py | 11 +++--
mautrix_telegram/commands/auth.py | 2 +-
mautrix_telegram/commands/handler.py | 7 +--
mautrix_telegram/context.py | 29 +++++++----
mautrix_telegram/formatter/__init__.py | 4 +-
mautrix_telegram/formatter/from_matrix.py | 5 +-
mautrix_telegram/formatter/from_telegram.py | 5 +-
mautrix_telegram/util/__init__.py | 1 +
mautrix_telegram/util/signed_token.py | 53 +++++++++++++++++++++
mautrix_telegram/web/public/__init__.py | 40 ++++++++++++----
mautrix_telegram/web/public/login.html.mako | 10 ++--
11 files changed, 130 insertions(+), 37 deletions(-)
create mode 100644 mautrix_telegram/util/signed_token.py
diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index c8865686..1969437b 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -85,18 +85,21 @@ appserv = AppService(config["homeserver.address"], config["homeserver.domain"],
config["appservice.as_token"], config["appservice.hs_token"],
config["appservice.bot_username"], log="mau.as", loop=loop,
verify_ssl=config["homeserver.verify_ssl"])
-
-context = Context(appserv, db_session, config, loop, None, None, session_container)
+public_website = None
+provisioning_api = None
if config["appservice.public.enabled"]:
- public = PublicBridgeWebsite(loop)
- appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public.app)
+ public_website = PublicBridgeWebsite(loop)
+ appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public_website.app)
if config["appservice.provisioning.enabled"]:
provisioning_api = ProvisioningAPI(config, loop)
appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning",
provisioning_api.app)
+context = Context(appserv, db_session, config, loop, None, None, session_container, public_website,
+ provisioning_api)
+
with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start:
init_db(db_session)
init_abstract_user(context)
diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py
index 8caf21a5..b5eaf1d9 100644
--- a/mautrix_telegram/commands/auth.py
+++ b/mautrix_telegram/commands/auth.py
@@ -114,7 +114,7 @@ async def login(evt: CommandEvent):
if evt.config["appservice.public.enabled"]:
prefix = evt.config["appservice.public.external"]
- url = f"{prefix}/login?mxid={evt.sender.mxid}"
+ url = f"{prefix}/login?token={evt.public_website.make_token(evt.sender.mxid)}"
if evt.config.get("bridge.allow_matrix_login", True):
return await evt.reply(
"This bridge instance allows you to log in inside or outside Matrix.\n\n"
diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py
index 7bf4323d..53a71a4b 100644
--- a/mautrix_telegram/commands/handler.py
+++ b/mautrix_telegram/commands/handler.py
@@ -22,8 +22,7 @@ import logging
from telethon.errors import FloodWaitError
from ..util import format_duration
-from ..context import Context
-from .. import user as u
+from .. import user as u, context as c
command_handlers = {} # type: Dict[str, CommandHandler]
@@ -45,6 +44,7 @@ class CommandEvent:
self.loop = processor.loop
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.sender = sender
@@ -131,8 +131,9 @@ def command_handler(_func: Optional[Callable[[CommandEvent], None]] = None, *, n
class CommandProcessor:
log = logging.getLogger("mau.commands")
- def __init__(self, context: Context):
+ def __init__(self, context: c.Context):
self.az, self.db, self.config, self.loop, self.tgbot = context
+ self.public_website = context.public_website
self.command_prefix = self.config["bridge.command_prefix"]
async def handle(self, room: str, sender: u.User, command: str, args: List[str],
diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py
index dfb32b59..ad48d7e4 100644
--- a/mautrix_telegram/context.py
+++ b/mautrix_telegram/context.py
@@ -14,17 +14,30 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import asyncio
+
+from sqlalchemy.orm import scoped_session
+from alchemysession import AlchemySessionContainer
+from mautrix_appservice import AppService
class Context:
- def __init__(self, az, db, config, loop, bot, mx, session_container):
- self.az = az
- self.db = db
- self.config = config
- self.loop = loop
- self.bot = bot
- self.mx = mx
- self.session_container = session_container
+ def __init__(self, az, db, config, loop, bot, mx, session_container, public_website,
+ provisioning_api):
+ from .web import PublicBridgeWebsite, ProvisioningAPI
+ from .config import Config
+ from .bot import Bot
+ from .matrix import MatrixHandler
+
+ self.az = az # type: AppService
+ self.db = db # type: scoped_session
+ self.config = config # type: Config
+ self.loop = loop # type: asyncio.AbstractEventLoop
+ self.bot = bot # type: Bot
+ self.mx = mx # type: MatrixHandler
+ self.session_container = session_container # type: AlchemySessionContainer
+ self.public_website = public_website # type: PublicBridgeWebsite
+ self.provisioning_api = provisioning_api # type: ProvisioningAPI
def __iter__(self):
yield self.az
diff --git a/mautrix_telegram/formatter/__init__.py b/mautrix_telegram/formatter/__init__.py
index 7cb102f7..51802ebb 100644
--- a/mautrix_telegram/formatter/__init__.py
+++ b/mautrix_telegram/formatter/__init__.py
@@ -1,9 +1,9 @@
from .from_matrix import (matrix_reply_to_telegram, matrix_to_telegram, matrix_text_to_telegram,
init_mx)
from .from_telegram import (telegram_reply_to_matrix, telegram_to_matrix, init_tg)
-from ..context import Context
+from .. import context as c
-def init(context: Context):
+def init(context: c.Context):
init_mx(context)
init_tg(context)
diff --git a/mautrix_telegram/formatter/from_matrix.py b/mautrix_telegram/formatter/from_matrix.py
index 145177fc..f98d3ad5 100644
--- a/mautrix_telegram/formatter/from_matrix.py
+++ b/mautrix_telegram/formatter/from_matrix.py
@@ -27,8 +27,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, M
MessageEntityItalic, MessageEntityCode, MessageEntityPre,
MessageEntityBotCommand, TypeMessageEntity)
-from ..context import Context
-from .. import user as u, puppet as pu, portal as po
+from .. import user as u, puppet as pu, portal as po, context as c
from ..db import Message as DBMessage
from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html,
trim_reply_fallback_text, html_to_unicode)
@@ -352,7 +351,7 @@ def plain_mention_to_text() -> Tuple[List[TypeMessageEntity], Callable[[str], st
return entities, replacer
-def init_mx(context: Context):
+def init_mx(context: c.Context):
global plain_mention_regex, should_bridge_plaintext_highlights
config = context.config
dn_template = config.get("bridge.displayname_template", "{displayname} (Telegram)")
diff --git a/mautrix_telegram/formatter/from_telegram.py b/mautrix_telegram/formatter/from_telegram.py
index 3e9992e4..70a13a55 100644
--- a/mautrix_telegram/formatter/from_telegram.py
+++ b/mautrix_telegram/formatter/from_telegram.py
@@ -33,8 +33,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName,
from mautrix_appservice import MatrixRequestError
from mautrix_appservice.intent_api import IntentAPI
-from .. import user as u, puppet as pu, portal as po
-from ..context import Context
+from .. import user as u, puppet as pu, portal as po, context as c
from ..db import Message as DBMessage
from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html,
trim_reply_fallback_text, unicode_to_html)
@@ -321,6 +320,6 @@ def _parse_url(html: List[str], entity_text: str, url: str) -> bool:
return False
-def init_tg(context: Context):
+def init_tg(context: c.Context):
global should_highlight_edits
should_highlight_edits = htmldiff and context.config["bridge.highlight_edits"]
diff --git a/mautrix_telegram/util/__init__.py b/mautrix_telegram/util/__init__.py
index 7d431396..99cdee2a 100644
--- a/mautrix_telegram/util/__init__.py
+++ b/mautrix_telegram/util/__init__.py
@@ -1,2 +1,3 @@
from .file_transfer import transfer_file_to_matrix, convert_image
from .format_duration import format_duration
+from .signed_token import sign_token, verify_token
diff --git a/mautrix_telegram/util/signed_token.py b/mautrix_telegram/util/signed_token.py
new file mode 100644
index 00000000..13281012
--- /dev/null
+++ b/mautrix_telegram/util/signed_token.py
@@ -0,0 +1,53 @@
+# -*- coding: future_fstrings -*-
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2018 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+from typing import Optional
+import json
+import base64
+import hashlib
+
+
+def _get_checksum(key: str, payload: bytes) -> str:
+ hasher = hashlib.sha256()
+ hasher.update(payload)
+ hasher.update(key.encode("utf-8"))
+ checksum = hasher.hexdigest()
+ return checksum
+
+
+def sign_token(key: str, payload: dict) -> str:
+ payload = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8"))
+ checksum = _get_checksum(key, payload)
+ return f"{checksum}:{payload.decode('utf-8')}"
+
+
+def verify_token(key: str, data: str) -> Optional[dict]:
+ if not data:
+ return None
+
+ try:
+ checksum, payload = data.split(":", 1)
+ except ValueError:
+ return None
+
+ if checksum != _get_checksum(key, payload.encode("utf-8")):
+ return None
+
+ payload = base64.urlsafe_b64decode(payload).decode("utf-8")
+ try:
+ return json.loads(payload)
+ except json.JSONDecodeError:
+ return None
diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py
index eab1f5fe..fb5f6de7 100644
--- a/mautrix_telegram/web/public/__init__.py
+++ b/mautrix_telegram/web/public/__init__.py
@@ -18,7 +18,11 @@ from aiohttp import web
from mako.template import Template
import pkg_resources
import logging
+import random
+import string
+import time
+from ...util import sign_token, verify_token
from ...user import User
from ..common import AuthAPI
@@ -28,6 +32,8 @@ class PublicBridgeWebsite(AuthAPI):
def __init__(self, loop):
super().__init__(loop)
+ self.secret_key = "".join(
+ random.choice(string.ascii_lowercase + string.digits) for _ in range(64))
self.login = Template(
pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako"))
@@ -38,10 +44,24 @@ class PublicBridgeWebsite(AuthAPI):
self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_telegram",
"web/public/"))
- async def get_login(self, request):
- state = "token" if request.rel_url.query.get("mode", "") == "bot" else "request"
+ def make_token(self, mxid, expires_in=900):
+ return sign_token(self.secret_key, {
+ "mxid": mxid,
+ "expiry": int(time.time()) + expires_in,
+ })
- mxid = request.rel_url.query.get("mxid", None)
+ def verify_token(self, token):
+ token = verify_token(self.secret_key, token)
+ if token and token.get("expiry", 0) > int(time.time()):
+ return token.get("mxid", None)
+ return None
+
+ async def get_login(self, request):
+ state = "bot_token" if request.rel_url.query.get("mode", "") == "bot" else "request"
+
+ mxid = self.verify_token(request.rel_url.query.get("token", None))
+ if not mxid:
+ return self.get_login_response(status=401, state="invalid-token")
user = User.get_by_mxid(mxid, create=False) if mxid else None
if not user:
@@ -62,11 +82,13 @@ class PublicBridgeWebsite(AuthAPI):
message=message, mxid=mxid))
async def post_login(self, request):
- data = await request.post()
- if "mxid" not in data:
- return self.get_login_response(error="Please enter your Matrix ID.", status=400)
+ mxid = self.verify_token(request.rel_url.query.get("token", None))
+ if not mxid:
+ return self.get_login_response(status=401, state="invalid-token")
- user = await User.get_by_mxid(data["mxid"]).ensure_started(even_if_no_session=True)
+ data = await request.post()
+
+ user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True)
if not user.puppet_whitelisted:
return self.get_login_response(mxid=user.mxid, error="You are not whitelisted.",
status=403)
@@ -77,8 +99,8 @@ class PublicBridgeWebsite(AuthAPI):
if "phone" in data:
return await self.post_login_phone(user, data["phone"])
- elif "token" in data:
- return await self.post_login_token(user, data["token"])
+ elif "bot_token" in data:
+ return await self.post_login_token(user, data["bot_token"])
elif "code" in data:
resp = await self.post_login_code(user, data["code"],
password_in_data="password" in data)
diff --git a/mautrix_telegram/web/public/login.html.mako b/mautrix_telegram/web/public/login.html.mako
index 8c03cbdc..f00b6a69 100644
--- a/mautrix_telegram/web/public/login.html.mako
+++ b/mautrix_telegram/web/public/login.html.mako
@@ -76,6 +76,9 @@ along with this program. If not, see .
management command first.
% endif
+ % elif state == "invalid-token":
+ Invalid or expired token
+ Please ask the bridge bot for a new login link.
% else:
Log in to Telegram
% if error:
@@ -87,8 +90,7 @@ along with this program. If not, see .