diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py
index 5d57e338..10ad60ea 100644
--- a/mautrix_telegram/__main__.py
+++ b/mautrix_telegram/__main__.py
@@ -31,11 +31,10 @@ from alchemysession import AlchemySessionContainer
from .web.provisioning import ProvisioningAPI
from .web.public import PublicBridgeWebsite
from .abstract_user import init as init_abstract_user
-from .base import Base
from .bot import init as init_bot
from .config import Config
from .context import Context
-from .db import init as init_db
+from .db import Base, init as init_db
from .formatter import init as init_formatter
from .matrix import MatrixHandler
from .portal import init as init_portal
diff --git a/mautrix_telegram/db.py b/mautrix_telegram/db.py
deleted file mode 100644
index 463f2259..00000000
--- a/mautrix_telegram/db.py
+++ /dev/null
@@ -1,445 +0,0 @@
-# -*- 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 sqlalchemy import (Column, UniqueConstraint, ForeignKey, ForeignKeyConstraint, Integer,
- BigInteger, String, Boolean, Text,
- and_, func, select)
-from sqlalchemy.engine.result import RowProxy
-from sqlalchemy.sql import expression
-from sqlalchemy.orm import relationship, Query
-from typing import Dict, Optional, List, Iterable, Tuple
-import json
-
-from mautrix_telegram.types import MatrixUserID, MatrixRoomID, MatrixEventID
-from .types import TelegramID
-from .base import Base
-
-
-class Portal(Base):
- __tablename__ = "portal"
-
- # Telegram chat information
- tgid = Column(Integer, primary_key=True) # type: TelegramID
- tg_receiver = Column(Integer, primary_key=True) # type: TelegramID
- peer_type = Column(String, nullable=False)
- megagroup = Column(Boolean)
-
- # Matrix portal information
- mxid = Column(String, unique=True, nullable=True) # type: Optional[MatrixRoomID]
-
- config = Column(Text, nullable=True)
-
- # Telegram chat metadata
- username = Column(String, nullable=True)
- title = Column(String, nullable=True)
- about = Column(String, nullable=True)
- photo_id = Column(String, nullable=True)
-
- @classmethod
- def scan(cls, row) -> Optional['Portal']:
- (tgid, tg_receiver, peer_type, megagroup, mxid, config, username, title, about,
- photo_id) = row
- return cls(tgid=tgid, tg_receiver=tg_receiver, peer_type=peer_type, megagroup=megagroup,
- mxid=mxid, config=config, username=username, title=title, about=about,
- photo_id=photo_id)
-
- @classmethod
- def _one_or_none(cls, rows: RowProxy) -> Optional['Portal']:
- try:
- return cls.scan(next(rows))
- except StopIteration:
- return None
-
- @classmethod
- def get_by_tgid(cls, tgid: TelegramID, tg_receiver: TelegramID) -> Optional['Portal']:
- return cls._select_one_or_none(and_(cls.c.tgid == tgid, cls.c.tg_receiver == tg_receiver))
-
- @classmethod
- def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['Portal']:
- return cls._select_one_or_none(cls.c.mxid == mxid)
-
- @classmethod
- def get_by_username(cls, username: str) -> Optional['Portal']:
- return cls._select_one_or_none(cls.c.username == username)
-
- @property
- def _edit_identity(self):
- return and_(self.c.tgid == self.tgid, self.c.tg_receiver == self.tg_receiver)
-
- def insert(self) -> None:
- self.db.execute(self.t.insert().values(
- tgid=self.tgid, tg_receiver=self.tg_receiver, peer_type=self.peer_type,
- megagroup=self.megagroup, mxid=self.mxid, config=self.config, username=self.username,
- title=self.title, about=self.about, photo_id=self.photo_id))
-
-
-class Message(Base):
- __tablename__ = "message"
-
- mxid = Column(String) # type: MatrixEventID
- mx_room = Column(String) # type: MatrixRoomID
- tgid = Column(Integer, primary_key=True) # type: TelegramID
- tg_space = Column(Integer, primary_key=True) # type: TelegramID
-
- __table_args__ = (UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room"),)
-
- @classmethod
- def _one_or_none(cls, rows: RowProxy) -> Optional['Message']:
- try:
- mxid, mx_room, tgid, tg_space = next(rows)
- return cls(mxid=mxid, mx_room=mx_room, tgid=tgid, tg_space=tg_space)
- except StopIteration:
- return None
-
- @staticmethod
- def _all(rows: RowProxy) -> List['Message']:
- return [Message(mxid=row[0], mx_room=row[1], tgid=row[2], tg_space=row[3])
- for row in rows]
-
- @classmethod
- def get_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> Optional['Message']:
- return cls._select_one_or_none(and_(cls.c.tgid == tgid, cls.c.tg_space == tg_space))
-
- @classmethod
- def count_spaces_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID) -> int:
- rows = cls.db.execute(select([func.count(cls.c.tg_space)])
- .where(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room)))
- try:
- count, = next(rows)
- return count
- except StopIteration:
- return 0
-
- @classmethod
- def get_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID, tg_space: TelegramID
- ) -> Optional['Message']:
- return cls._select_one_or_none(and_(cls.c.mxid == mxid,
- cls.c.mx_room == mx_room,
- cls.c.tg_space == tg_space))
-
- @classmethod
- def update_by_tgid(cls, s_tgid: TelegramID, s_tg_space: TelegramID, **values) -> None:
- cls.db.execute(cls.t.update()
- .where(and_(cls.c.tgid == s_tgid, cls.c.tg_space == s_tg_space))
- .values(**values))
-
- @classmethod
- def update_by_mxid(cls, s_mxid: MatrixEventID, s_mx_room: MatrixRoomID, **values) -> None:
- cls.db.execute(cls.t.update()
- .where(and_(cls.c.mxid == s_mxid, cls.c.mx_room == s_mx_room))
- .values(**values))
-
- @property
- def _edit_identity(self):
- return and_(self.c.tgid == self.tgid, self.c.tg_space == self.tg_space)
-
- def insert(self) -> None:
- self.db.execute(self.t.insert().values(mxid=self.mxid, mx_room=self.mx_room, tgid=self.tgid,
- tg_space=self.tg_space))
-
-
-class User(Base):
- __tablename__ = "user"
-
- mxid = Column(String, primary_key=True) # type: MatrixUserID
- tgid = Column(Integer, nullable=True, unique=True) # type: Optional[TelegramID]
- tg_username = Column(String, nullable=True)
- tg_phone = Column(String, nullable=True)
- saved_contacts = Column(Integer, default=0, nullable=False)
-
- @classmethod
- def _one_or_none(cls, rows: RowProxy) -> Optional['User']:
- try:
- mxid, tgid, tg_username, tg_phone, saved_contacts = next(rows)
- return cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone,
- saved_contacts=saved_contacts)
- except StopIteration:
- return None
-
- @classmethod
- def all(cls) -> Iterable['User']:
- rows = cls.db.execute(cls.t.select())
- for row in rows:
- mxid, tgid, tg_username, tg_phone, saved_contacts = row
- yield cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone,
- saved_contacts=saved_contacts)
-
- @classmethod
- def get_by_tgid(cls, tgid: TelegramID) -> Optional['User']:
- return cls._select_one_or_none(cls.c.tgid == tgid)
-
- @classmethod
- def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['User']:
- return cls._select_one_or_none(cls.c.mxid == mxid)
-
- @classmethod
- def get_by_username(cls, username: str) -> Optional['User']:
- return cls._select_one_or_none(cls.c.username == username)
-
- @property
- def _edit_identity(self):
- return self.c.mxid == self.mxid
-
- def insert(self) -> None:
- self.db.execute(self.t.insert().values(
- mxid=self.mxid, tgid=self.tgid, tg_username=self.tg_username, tg_phone=self.tg_phone,
- saved_contacts=self.saved_contacts))
-
- @property
- def contacts(self) -> Iterable[TelegramID]:
- rows = self.db.execute(Contact.t.select().where(Contact.c.user == self.tgid))
- for row in rows:
- user, contact = row
- yield contact
-
- @contacts.setter
- def contacts(self, puppets: Iterable[TelegramID]) -> None:
- self.db.execute(Contact.t.delete().where(Contact.c.user == self.tgid))
- self.db.execute(Contact.t.insert(), [{"user": self.tgid, "contact": tgid}
- for tgid in puppets])
-
- @property
- def portals(self) -> Iterable[Tuple[TelegramID, TelegramID]]:
- rows = self.db.execute(UserPortal.t.select().where(UserPortal.c.user == self.tgid))
- for row in rows:
- user, portal, portal_receiver = row
- yield (portal, portal_receiver)
-
- @portals.setter
- def portals(self, portals: Iterable[Tuple[TelegramID, TelegramID]]) -> None:
- self.db.execute(UserPortal.t.delete().where(UserPortal.c.user == self.tgid))
- self.db.execute(UserPortal.t.insert(),
- [{
- "user": self.tgid,
- "portal": tgid,
- "portal_receiver": tg_receiver
- } for tgid, tg_receiver in portals])
-
-
-class UserPortal(Base):
- __tablename__ = "user_portal"
-
- user = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE", ondelete="CASCADE"),
- primary_key=True) # type: TelegramID
- portal = Column(Integer, primary_key=True) # type: TelegramID
- portal_receiver = Column(Integer, primary_key=True) # type: TelegramID
-
- __table_args__ = (ForeignKeyConstraint(("portal", "portal_receiver"),
- ("portal.tgid", "portal.tg_receiver"),
- onupdate="CASCADE", ondelete="CASCADE"),)
-
-
-class Contact(Base):
- __tablename__ = "contact"
-
- user = Column(Integer, ForeignKey("user.tgid"), primary_key=True) # type: TelegramID
- contact = Column(Integer, ForeignKey("puppet.id"), primary_key=True) # type: TelegramID
-
-
-class RoomState(Base):
- __tablename__ = "mx_room_state"
-
- room_id = Column(String, primary_key=True) # type: MatrixRoomID
- power_levels = Column("power_levels", Text, nullable=True) # type: Optional[Dict]
-
- @property
- def _power_levels_text(self) -> Optional[str]:
- return json.dumps(self.power_levels) if self.power_levels else None
-
- @property
- def has_power_levels(self) -> bool:
- return bool(self.power_levels)
-
- @classmethod
- def get(cls, room_id: MatrixRoomID) -> Optional['RoomState']:
- rows = cls.db.execute(cls.t.select().where(cls.c.room_id == room_id))
- try:
- room_id, power_levels_text = next(rows)
- return cls(room_id=room_id, power_levels=(json.loads(power_levels_text)
- if power_levels_text else None))
- except StopIteration:
- return None
-
- def update(self) -> None:
- return super().update(power_levels=self._power_levels_text)
-
- @property
- def _edit_identity(self):
- return self.c.room_id == self.room_id
-
- def insert(self) -> None:
- self.db.execute(self.t.insert().values(room_id=self.room_id,
- power_levels=self._power_levels_text))
-
-
-class UserProfile(Base):
- __tablename__ = "mx_user_profile"
-
- room_id = Column(String, primary_key=True) # type: MatrixRoomID
- user_id = Column(String, primary_key=True) # type: MatrixUserID
- membership = Column(String, nullable=False, default="leave")
- displayname = Column(String, nullable=True)
- avatar_url = Column(String, nullable=True)
-
- def dict(self) -> Dict[str, str]:
- return {
- "membership": self.membership,
- "displayname": self.displayname,
- "avatar_url": self.avatar_url,
- }
-
- @classmethod
- def get(cls, room_id: MatrixRoomID, user_id: MatrixUserID) -> Optional['UserProfile']:
- rows = cls.db.execute(
- cls.t.select().where(and_(cls.c.room_id == room_id, cls.c.user_id == user_id)))
- try:
- room_id, user_id, membership, displayname, avatar_url = next(rows)
- return cls(room_id=room_id, user_id=user_id, membership=membership,
- displayname=displayname, avatar_url=avatar_url)
- except StopIteration:
- return None
-
- @classmethod
- def delete_all(cls, room_id: MatrixRoomID) -> None:
- cls.db.execute(cls.t.delete().where(cls.c.room_id == room_id))
-
- def update(self) -> None:
- super().update(membership=self.membership, displayname=self.displayname,
- avatar_url=self.avatar_url)
-
- @property
- def _edit_identity(self):
- return and_(self.c.room_id == self.room_id, self.c.user_id == self.user_id)
-
- def insert(self) -> None:
- self.db.execute(self.t.insert().values(room_id=self.room_id, user_id=self.user_id,
- membership=self.membership,
- displayname=self.displayname,
- avatar_url=self.avatar_url))
-
-
-class Puppet(Base):
- __tablename__ = "puppet"
-
- id = Column(Integer, primary_key=True) # type: TelegramID
- custom_mxid = Column(String, nullable=True) # type: Optional[MatrixUserID]
- access_token = Column(String, nullable=True)
- displayname = Column(String, nullable=True)
- displayname_source = Column(Integer, nullable=True) # type: Optional[TelegramID]
- 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())
-
- @classmethod
- def scan(cls, row) -> Optional['Puppet']:
- (id, custom_mxid, access_token, displayname, displayname_source, username, photo_id,
- is_bot, matrix_registered) = row
- return cls(id=id, custom_mxid=custom_mxid, access_token=access_token,
- displayname=displayname, displayname_source=displayname_source,
- username=username, photo_id=photo_id, is_bot=is_bot,
- matrix_registered=matrix_registered)
-
- @classmethod
- def _one_or_none(cls, rows: RowProxy) -> Optional['Puppet']:
- try:
- return cls.scan(next(rows))
- except StopIteration:
- return None
-
- @classmethod
- def all_with_custom_mxid(cls) -> Iterable['Puppet']:
- rows = cls.db.execute(cls.t.select().where(cls.c.custom_mxid != None))
- for row in rows:
- yield cls.scan(row)
-
- @classmethod
- def get_by_tgid(cls, tgid: TelegramID) -> Optional['Puppet']:
- return cls._select_one_or_none(cls.c.id == tgid)
-
- @classmethod
- def get_by_custom_mxid(cls, mxid: MatrixRoomID) -> Optional['Puppet']:
- return cls._select_one_or_none(cls.c.custom_mxid == mxid)
-
- @classmethod
- def get_by_username(cls, username: str) -> Optional['Puppet']:
- return cls._select_one_or_none(cls.c.username == username)
-
- @classmethod
- def get_by_displayname(cls, displayname: str) -> Optional['Puppet']:
- return cls._select_one_or_none(cls.c.displayname == displayname)
-
- @property
- def _edit_identity(self):
- return self.c.id == self.id
-
- def insert(self) -> None:
- self.db.execute(self.t.insert().values(
- id=self.id, custom_mxid=self.custom_mxid, access_token=self.access_token,
- displayname=self.displayname, displayname_source=self.displayname_source,
- username=self.username, photo_id=self.photo_id, is_bot=self.is_bot,
- matrix_registered=self.matrix_registered))
-
-
-# Fucking Telegram not telling bots what chats they are in 3:<
-class BotChat(Base):
- query = None # type: Query
- __tablename__ = "bot_chat"
- id = Column(Integer, primary_key=True) # type: TelegramID
- type = Column(String, nullable=False)
-
-
-class TelegramFile(Base):
- __tablename__ = "telegram_file"
-
- id = Column(String, primary_key=True)
- mxc = Column(String)
- mime_type = Column(String)
- was_converted = Column(Boolean)
- timestamp = Column(BigInteger)
- size = Column(Integer, nullable=True)
- width = Column(Integer, nullable=True)
- height = Column(Integer, nullable=True)
- thumbnail_id = Column("thumbnail", String, ForeignKey("telegram_file.id"), nullable=True)
- thumbnail = relationship("TelegramFile", uselist=False)
-
- @classmethod
- def get(cls, id: str) -> Optional['TelegramFile']:
- rows = cls.db.execute(cls.t.select().where(cls.c.id == id))
- try:
- id, mxc, mime, conv, ts, s, w, h, thumb_id = next(rows)
- thumb = None
- if thumb_id:
- thumb = cls.get(thumb_id)
- return cls(id=id, mxc=mxc, mime_type=mime, was_converted=conv, timestamp=ts,
- size=s, width=w, height=h, thumbnail_id=thumb_id, thumbnail=thumb)
- except StopIteration:
- return None
-
- def insert(self) -> None:
- self.db.execute(self.t.insert().values(
- id=self.id, mxc=self.mxc, mime_type=self.mime_type, was_converted=self.was_converted,
- timestamp=self.timestamp, size=self.size, width=self.width, height=self.height,
- thumbnail=self.thumbnail.id if self.thumbnail else self.thumbnail_id))
-
-
-def init(db_session, db_engine) -> None:
- BotChat.query = db_session.query_property()
- for table in (Portal, Message, User, Contact, UserPortal, Puppet, TelegramFile, UserProfile,
- RoomState):
- table.db = db_engine
- table.t = table.__table__
- table.c = table.t.c
diff --git a/mautrix_telegram/db/__init__.py b/mautrix_telegram/db/__init__.py
new file mode 100644
index 00000000..af81d44c
--- /dev/null
+++ b/mautrix_telegram/db/__init__.py
@@ -0,0 +1,34 @@
+# -*- 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 .base import Base
+from .bot_chat import BotChat
+from .message import Message
+from .portal import Portal
+from .puppet import Puppet
+from .room_state import RoomState
+from .telegram_file import TelegramFile
+from .user import User, UserPortal, Contact
+from .user_profile import UserProfile
+
+
+def init(db_session, db_engine) -> None:
+ BotChat.query = db_session.query_property()
+ for table in (Portal, Message, User, Contact, UserPortal, Puppet, TelegramFile, UserProfile,
+ RoomState):
+ table.db = db_engine
+ table.t = table.__table__
+ table.c = table.t.c
diff --git a/mautrix_telegram/base.py b/mautrix_telegram/db/base.py
similarity index 100%
rename from mautrix_telegram/base.py
rename to mautrix_telegram/db/base.py
diff --git a/mautrix_telegram/base.pyi b/mautrix_telegram/db/base.pyi
similarity index 100%
rename from mautrix_telegram/base.pyi
rename to mautrix_telegram/db/base.pyi
diff --git a/mautrix_telegram/db/bot_chat.py b/mautrix_telegram/db/bot_chat.py
new file mode 100644
index 00000000..f675fed5
--- /dev/null
+++ b/mautrix_telegram/db/bot_chat.py
@@ -0,0 +1,29 @@
+# -*- 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 sqlalchemy import Column, Integer, String
+from sqlalchemy.orm import Query
+
+from ..types import TelegramID
+from .base import Base
+
+
+# Fucking Telegram not telling bots what chats they are in 3:<
+class BotChat(Base):
+ query = None # type: Query
+ __tablename__ = "bot_chat"
+ id = Column(Integer, primary_key=True) # type: TelegramID
+ type = Column(String, nullable=False)
diff --git a/mautrix_telegram/db/message.py b/mautrix_telegram/db/message.py
new file mode 100644
index 00000000..9a7499a9
--- /dev/null
+++ b/mautrix_telegram/db/message.py
@@ -0,0 +1,87 @@
+# -*- 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 sqlalchemy import Column, UniqueConstraint, Integer, String, and_, func, select
+from sqlalchemy.engine.result import RowProxy
+from typing import Optional, List
+
+from ..types import MatrixRoomID, MatrixEventID, TelegramID
+from .base import Base
+
+
+class Message(Base):
+ __tablename__ = "message"
+
+ mxid = Column(String) # type: MatrixEventID
+ mx_room = Column(String) # type: MatrixRoomID
+ tgid = Column(Integer, primary_key=True) # type: TelegramID
+ tg_space = Column(Integer, primary_key=True) # type: TelegramID
+
+ __table_args__ = (UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room"),)
+
+ @classmethod
+ def _one_or_none(cls, rows: RowProxy) -> Optional['Message']:
+ try:
+ mxid, mx_room, tgid, tg_space = next(rows)
+ return cls(mxid=mxid, mx_room=mx_room, tgid=tgid, tg_space=tg_space)
+ except StopIteration:
+ return None
+
+ @staticmethod
+ def _all(rows: RowProxy) -> List['Message']:
+ return [Message(mxid=row[0], mx_room=row[1], tgid=row[2], tg_space=row[3])
+ for row in rows]
+
+ @classmethod
+ def get_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> Optional['Message']:
+ return cls._select_one_or_none(and_(cls.c.tgid == tgid, cls.c.tg_space == tg_space))
+
+ @classmethod
+ def count_spaces_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID) -> int:
+ rows = cls.db.execute(select([func.count(cls.c.tg_space)])
+ .where(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room)))
+ try:
+ count, = next(rows)
+ return count
+ except StopIteration:
+ return 0
+
+ @classmethod
+ def get_by_mxid(cls, mxid: MatrixEventID, mx_room: MatrixRoomID, tg_space: TelegramID
+ ) -> Optional['Message']:
+ return cls._select_one_or_none(and_(cls.c.mxid == mxid,
+ cls.c.mx_room == mx_room,
+ cls.c.tg_space == tg_space))
+
+ @classmethod
+ def update_by_tgid(cls, s_tgid: TelegramID, s_tg_space: TelegramID, **values) -> None:
+ cls.db.execute(cls.t.update()
+ .where(and_(cls.c.tgid == s_tgid, cls.c.tg_space == s_tg_space))
+ .values(**values))
+
+ @classmethod
+ def update_by_mxid(cls, s_mxid: MatrixEventID, s_mx_room: MatrixRoomID, **values) -> None:
+ cls.db.execute(cls.t.update()
+ .where(and_(cls.c.mxid == s_mxid, cls.c.mx_room == s_mx_room))
+ .values(**values))
+
+ @property
+ def _edit_identity(self):
+ return and_(self.c.tgid == self.tgid, self.c.tg_space == self.tg_space)
+
+ def insert(self) -> None:
+ self.db.execute(self.t.insert().values(mxid=self.mxid, mx_room=self.mx_room, tgid=self.tgid,
+ tg_space=self.tg_space))
diff --git a/mautrix_telegram/db/portal.py b/mautrix_telegram/db/portal.py
new file mode 100644
index 00000000..c277dd63
--- /dev/null
+++ b/mautrix_telegram/db/portal.py
@@ -0,0 +1,80 @@
+# -*- 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 sqlalchemy import Column, Integer, String, Boolean, Text, and_
+from sqlalchemy.engine.result import RowProxy
+from typing import Optional
+
+from ..types import MatrixRoomID, TelegramID
+from .base import Base
+
+
+class Portal(Base):
+ __tablename__ = "portal"
+
+ # Telegram chat information
+ tgid = Column(Integer, primary_key=True) # type: TelegramID
+ tg_receiver = Column(Integer, primary_key=True) # type: TelegramID
+ peer_type = Column(String, nullable=False)
+ megagroup = Column(Boolean)
+
+ # Matrix portal information
+ mxid = Column(String, unique=True, nullable=True) # type: Optional[MatrixRoomID]
+
+ config = Column(Text, nullable=True)
+
+ # Telegram chat metadata
+ username = Column(String, nullable=True)
+ title = Column(String, nullable=True)
+ about = Column(String, nullable=True)
+ photo_id = Column(String, nullable=True)
+
+ @classmethod
+ def scan(cls, row) -> Optional['Portal']:
+ (tgid, tg_receiver, peer_type, megagroup, mxid, config, username, title, about,
+ photo_id) = row
+ return cls(tgid=tgid, tg_receiver=tg_receiver, peer_type=peer_type, megagroup=megagroup,
+ mxid=mxid, config=config, username=username, title=title, about=about,
+ photo_id=photo_id)
+
+ @classmethod
+ def _one_or_none(cls, rows: RowProxy) -> Optional['Portal']:
+ try:
+ return cls.scan(next(rows))
+ except StopIteration:
+ return None
+
+ @classmethod
+ def get_by_tgid(cls, tgid: TelegramID, tg_receiver: TelegramID) -> Optional['Portal']:
+ return cls._select_one_or_none(and_(cls.c.tgid == tgid, cls.c.tg_receiver == tg_receiver))
+
+ @classmethod
+ def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['Portal']:
+ return cls._select_one_or_none(cls.c.mxid == mxid)
+
+ @classmethod
+ def get_by_username(cls, username: str) -> Optional['Portal']:
+ return cls._select_one_or_none(cls.c.username == username)
+
+ @property
+ def _edit_identity(self):
+ return and_(self.c.tgid == self.tgid, self.c.tg_receiver == self.tg_receiver)
+
+ def insert(self) -> None:
+ self.db.execute(self.t.insert().values(
+ tgid=self.tgid, tg_receiver=self.tg_receiver, peer_type=self.peer_type,
+ megagroup=self.megagroup, mxid=self.mxid, config=self.config, username=self.username,
+ title=self.title, about=self.about, photo_id=self.photo_id))
diff --git a/mautrix_telegram/db/puppet.py b/mautrix_telegram/db/puppet.py
new file mode 100644
index 00000000..3fde9773
--- /dev/null
+++ b/mautrix_telegram/db/puppet.py
@@ -0,0 +1,86 @@
+# -*- 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 sqlalchemy import Column, Integer, String, Boolean
+from sqlalchemy.engine.result import RowProxy
+from sqlalchemy.sql import expression
+from typing import Optional, Iterable
+
+from ..types import MatrixUserID, MatrixRoomID, TelegramID
+from .base import Base
+
+
+class Puppet(Base):
+ __tablename__ = "puppet"
+
+ id = Column(Integer, primary_key=True) # type: TelegramID
+ custom_mxid = Column(String, nullable=True) # type: Optional[MatrixUserID]
+ access_token = Column(String, nullable=True)
+ displayname = Column(String, nullable=True)
+ displayname_source = Column(Integer, nullable=True) # type: Optional[TelegramID]
+ 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())
+
+ @classmethod
+ def scan(cls, row) -> Optional['Puppet']:
+ (id, custom_mxid, access_token, displayname, displayname_source, username, photo_id,
+ is_bot, matrix_registered) = row
+ return cls(id=id, custom_mxid=custom_mxid, access_token=access_token,
+ displayname=displayname, displayname_source=displayname_source,
+ username=username, photo_id=photo_id, is_bot=is_bot,
+ matrix_registered=matrix_registered)
+
+ @classmethod
+ def _one_or_none(cls, rows: RowProxy) -> Optional['Puppet']:
+ try:
+ return cls.scan(next(rows))
+ except StopIteration:
+ return None
+
+ @classmethod
+ def all_with_custom_mxid(cls) -> Iterable['Puppet']:
+ rows = cls.db.execute(cls.t.select().where(cls.c.custom_mxid != None))
+ for row in rows:
+ yield cls.scan(row)
+
+ @classmethod
+ def get_by_tgid(cls, tgid: TelegramID) -> Optional['Puppet']:
+ return cls._select_one_or_none(cls.c.id == tgid)
+
+ @classmethod
+ def get_by_custom_mxid(cls, mxid: MatrixRoomID) -> Optional['Puppet']:
+ return cls._select_one_or_none(cls.c.custom_mxid == mxid)
+
+ @classmethod
+ def get_by_username(cls, username: str) -> Optional['Puppet']:
+ return cls._select_one_or_none(cls.c.username == username)
+
+ @classmethod
+ def get_by_displayname(cls, displayname: str) -> Optional['Puppet']:
+ return cls._select_one_or_none(cls.c.displayname == displayname)
+
+ @property
+ def _edit_identity(self):
+ return self.c.id == self.id
+
+ def insert(self) -> None:
+ self.db.execute(self.t.insert().values(
+ id=self.id, custom_mxid=self.custom_mxid, access_token=self.access_token,
+ displayname=self.displayname, displayname_source=self.displayname_source,
+ username=self.username, photo_id=self.photo_id, is_bot=self.is_bot,
+ matrix_registered=self.matrix_registered))
diff --git a/mautrix_telegram/db/room_state.py b/mautrix_telegram/db/room_state.py
new file mode 100644
index 00000000..587663f2
--- /dev/null
+++ b/mautrix_telegram/db/room_state.py
@@ -0,0 +1,58 @@
+# -*- 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 sqlalchemy import Column, String, Text
+from typing import Dict, Optional
+import json
+
+from ..types import MatrixRoomID
+from .base import Base
+
+
+class RoomState(Base):
+ __tablename__ = "mx_room_state"
+
+ room_id = Column(String, primary_key=True) # type: MatrixRoomID
+ power_levels = Column("power_levels", Text, nullable=True) # type: Optional[Dict]
+
+ @property
+ def _power_levels_text(self) -> Optional[str]:
+ return json.dumps(self.power_levels) if self.power_levels else None
+
+ @property
+ def has_power_levels(self) -> bool:
+ return bool(self.power_levels)
+
+ @classmethod
+ def get(cls, room_id: MatrixRoomID) -> Optional['RoomState']:
+ rows = cls.db.execute(cls.t.select().where(cls.c.room_id == room_id))
+ try:
+ room_id, power_levels_text = next(rows)
+ return cls(room_id=room_id, power_levels=(json.loads(power_levels_text)
+ if power_levels_text else None))
+ except StopIteration:
+ return None
+
+ def update(self) -> None:
+ return super().update(power_levels=self._power_levels_text)
+
+ @property
+ def _edit_identity(self):
+ return self.c.room_id == self.room_id
+
+ def insert(self) -> None:
+ self.db.execute(self.t.insert().values(room_id=self.room_id,
+ power_levels=self._power_levels_text))
diff --git a/mautrix_telegram/db/telegram_file.py b/mautrix_telegram/db/telegram_file.py
new file mode 100644
index 00000000..f8917c2f
--- /dev/null
+++ b/mautrix_telegram/db/telegram_file.py
@@ -0,0 +1,55 @@
+# -*- 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 sqlalchemy import Column, ForeignKey, Integer, BigInteger, String, Boolean
+from sqlalchemy.orm import relationship
+from typing import Optional
+
+from .base import Base
+
+
+class TelegramFile(Base):
+ __tablename__ = "telegram_file"
+
+ id = Column(String, primary_key=True)
+ mxc = Column(String)
+ mime_type = Column(String)
+ was_converted = Column(Boolean)
+ timestamp = Column(BigInteger)
+ size = Column(Integer, nullable=True)
+ width = Column(Integer, nullable=True)
+ height = Column(Integer, nullable=True)
+ thumbnail_id = Column("thumbnail", String, ForeignKey("telegram_file.id"), nullable=True)
+ thumbnail = relationship("TelegramFile", uselist=False)
+
+ @classmethod
+ def get(cls, id: str) -> Optional['TelegramFile']:
+ rows = cls.db.execute(cls.t.select().where(cls.c.id == id))
+ try:
+ id, mxc, mime, conv, ts, s, w, h, thumb_id = next(rows)
+ thumb = None
+ if thumb_id:
+ thumb = cls.get(thumb_id)
+ return cls(id=id, mxc=mxc, mime_type=mime, was_converted=conv, timestamp=ts,
+ size=s, width=w, height=h, thumbnail_id=thumb_id, thumbnail=thumb)
+ except StopIteration:
+ return None
+
+ def insert(self) -> None:
+ self.db.execute(self.t.insert().values(
+ id=self.id, mxc=self.mxc, mime_type=self.mime_type, was_converted=self.was_converted,
+ timestamp=self.timestamp, size=self.size, width=self.width, height=self.height,
+ thumbnail=self.thumbnail.id if self.thumbnail else self.thumbnail_id))
diff --git a/mautrix_telegram/db/user.py b/mautrix_telegram/db/user.py
new file mode 100644
index 00000000..d53b38ac
--- /dev/null
+++ b/mautrix_telegram/db/user.py
@@ -0,0 +1,121 @@
+# -*- 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 sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String
+from sqlalchemy.engine.result import RowProxy
+from typing import Optional, Iterable, Tuple
+
+from ..types import MatrixUserID, MatrixRoomID, TelegramID
+from .base import Base
+
+
+class User(Base):
+ __tablename__ = "user"
+
+ mxid = Column(String, primary_key=True) # type: MatrixUserID
+ tgid = Column(Integer, nullable=True, unique=True) # type: Optional[TelegramID]
+ tg_username = Column(String, nullable=True)
+ tg_phone = Column(String, nullable=True)
+ saved_contacts = Column(Integer, default=0, nullable=False)
+
+ @classmethod
+ def _one_or_none(cls, rows: RowProxy) -> Optional['User']:
+ try:
+ mxid, tgid, tg_username, tg_phone, saved_contacts = next(rows)
+ return cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone,
+ saved_contacts=saved_contacts)
+ except StopIteration:
+ return None
+
+ @classmethod
+ def all(cls) -> Iterable['User']:
+ rows = cls.db.execute(cls.t.select())
+ for row in rows:
+ mxid, tgid, tg_username, tg_phone, saved_contacts = row
+ yield cls(mxid=mxid, tgid=tgid, tg_username=tg_username, tg_phone=tg_phone,
+ saved_contacts=saved_contacts)
+
+ @classmethod
+ def get_by_tgid(cls, tgid: TelegramID) -> Optional['User']:
+ return cls._select_one_or_none(cls.c.tgid == tgid)
+
+ @classmethod
+ def get_by_mxid(cls, mxid: MatrixRoomID) -> Optional['User']:
+ return cls._select_one_or_none(cls.c.mxid == mxid)
+
+ @classmethod
+ def get_by_username(cls, username: str) -> Optional['User']:
+ return cls._select_one_or_none(cls.c.username == username)
+
+ @property
+ def _edit_identity(self):
+ return self.c.mxid == self.mxid
+
+ def insert(self) -> None:
+ self.db.execute(self.t.insert().values(
+ mxid=self.mxid, tgid=self.tgid, tg_username=self.tg_username, tg_phone=self.tg_phone,
+ saved_contacts=self.saved_contacts))
+
+ @property
+ def contacts(self) -> Iterable[TelegramID]:
+ rows = self.db.execute(Contact.t.select().where(Contact.c.user == self.tgid))
+ for row in rows:
+ user, contact = row
+ yield contact
+
+ @contacts.setter
+ def contacts(self, puppets: Iterable[TelegramID]) -> None:
+ self.db.execute(Contact.t.delete().where(Contact.c.user == self.tgid))
+ self.db.execute(Contact.t.insert(), [{"user": self.tgid, "contact": tgid}
+ for tgid in puppets])
+
+ @property
+ def portals(self) -> Iterable[Tuple[TelegramID, TelegramID]]:
+ rows = self.db.execute(UserPortal.t.select().where(UserPortal.c.user == self.tgid))
+ for row in rows:
+ user, portal, portal_receiver = row
+ yield (portal, portal_receiver)
+
+ @portals.setter
+ def portals(self, portals: Iterable[Tuple[TelegramID, TelegramID]]) -> None:
+ self.db.execute(UserPortal.t.delete().where(UserPortal.c.user == self.tgid))
+ self.db.execute(UserPortal.t.insert(),
+ [{
+ "user": self.tgid,
+ "portal": tgid,
+ "portal_receiver": tg_receiver
+ } for tgid, tg_receiver in portals])
+
+
+class UserPortal(Base):
+ __tablename__ = "user_portal"
+
+ user = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE", ondelete="CASCADE"),
+ primary_key=True) # type: TelegramID
+ portal = Column(Integer, primary_key=True) # type: TelegramID
+ portal_receiver = Column(Integer, primary_key=True) # type: TelegramID
+
+ __table_args__ = (ForeignKeyConstraint(("portal", "portal_receiver"),
+ ("portal.tgid", "portal.tg_receiver"),
+ onupdate="CASCADE", ondelete="CASCADE"),)
+
+
+class Contact(Base):
+ __tablename__ = "contact"
+
+ user = Column(Integer, ForeignKey("user.tgid"), primary_key=True) # type: TelegramID
+ contact = Column(Integer, ForeignKey("puppet.id"), primary_key=True) # type: TelegramID
+
diff --git a/mautrix_telegram/db/user_profile.py b/mautrix_telegram/db/user_profile.py
new file mode 100644
index 00000000..e4db31dd
--- /dev/null
+++ b/mautrix_telegram/db/user_profile.py
@@ -0,0 +1,68 @@
+# -*- 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 sqlalchemy import Column, String, and_
+from typing import Dict, Optional
+
+from ..types import MatrixUserID, MatrixRoomID
+from .base import Base
+
+
+class UserProfile(Base):
+ __tablename__ = "mx_user_profile"
+
+ room_id = Column(String, primary_key=True) # type: MatrixRoomID
+ user_id = Column(String, primary_key=True) # type: MatrixUserID
+ membership = Column(String, nullable=False, default="leave")
+ displayname = Column(String, nullable=True)
+ avatar_url = Column(String, nullable=True)
+
+ def dict(self) -> Dict[str, str]:
+ return {
+ "membership": self.membership,
+ "displayname": self.displayname,
+ "avatar_url": self.avatar_url,
+ }
+
+ @classmethod
+ def get(cls, room_id: MatrixRoomID, user_id: MatrixUserID) -> Optional['UserProfile']:
+ rows = cls.db.execute(
+ cls.t.select().where(and_(cls.c.room_id == room_id, cls.c.user_id == user_id)))
+ try:
+ room_id, user_id, membership, displayname, avatar_url = next(rows)
+ return cls(room_id=room_id, user_id=user_id, membership=membership,
+ displayname=displayname, avatar_url=avatar_url)
+ except StopIteration:
+ return None
+
+ @classmethod
+ def delete_all(cls, room_id: MatrixRoomID) -> None:
+ cls.db.execute(cls.t.delete().where(cls.c.room_id == room_id))
+
+ def update(self) -> None:
+ super().update(membership=self.membership, displayname=self.displayname,
+ avatar_url=self.avatar_url)
+
+ @property
+ def _edit_identity(self):
+ return and_(self.c.room_id == self.room_id, self.c.user_id == self.user_id)
+
+ def insert(self) -> None:
+ self.db.execute(self.t.insert().values(room_id=self.room_id, user_id=self.user_id,
+ membership=self.membership,
+ displayname=self.displayname,
+ avatar_url=self.avatar_url))
+