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()