From f775e40b1668f95fdc0618b6098b33dae23b85af Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 12 Feb 2019 15:05:36 +0200 Subject: [PATCH] Move db to own package --- mautrix_telegram/__main__.py | 3 +- mautrix_telegram/db.py | 445 --------------------------- mautrix_telegram/db/__init__.py | 34 ++ mautrix_telegram/{ => db}/base.py | 0 mautrix_telegram/{ => db}/base.pyi | 0 mautrix_telegram/db/bot_chat.py | 29 ++ mautrix_telegram/db/message.py | 87 ++++++ mautrix_telegram/db/portal.py | 80 +++++ mautrix_telegram/db/puppet.py | 86 ++++++ mautrix_telegram/db/room_state.py | 58 ++++ mautrix_telegram/db/telegram_file.py | 55 ++++ mautrix_telegram/db/user.py | 121 ++++++++ mautrix_telegram/db/user_profile.py | 68 ++++ 13 files changed, 619 insertions(+), 447 deletions(-) delete mode 100644 mautrix_telegram/db.py create mode 100644 mautrix_telegram/db/__init__.py rename mautrix_telegram/{ => db}/base.py (100%) rename mautrix_telegram/{ => db}/base.pyi (100%) create mode 100644 mautrix_telegram/db/bot_chat.py create mode 100644 mautrix_telegram/db/message.py create mode 100644 mautrix_telegram/db/portal.py create mode 100644 mautrix_telegram/db/puppet.py create mode 100644 mautrix_telegram/db/room_state.py create mode 100644 mautrix_telegram/db/telegram_file.py create mode 100644 mautrix_telegram/db/user.py create mode 100644 mautrix_telegram/db/user_profile.py 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)) +