diff --git a/alembic/versions/d5f7b8b4b456_add_access_token_and_custom_mxid_fields_.py b/alembic/versions/d5f7b8b4b456_add_access_token_and_custom_mxid_fields_.py new file mode 100644 index 00000000..5c5a940a --- /dev/null +++ b/alembic/versions/d5f7b8b4b456_add_access_token_and_custom_mxid_fields_.py @@ -0,0 +1,26 @@ +"""Add access_token and custom_mxid fields for puppets + +Revision ID: d5f7b8b4b456 +Revises: 6ca3d74d51e4 +Create Date: 2018-07-20 12:09:30.277960 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "d5f7b8b4b456" +down_revision = "6ca3d74d51e4" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("puppet", sa.Column("access_token", sa.String(), nullable=True)) + op.add_column("puppet", sa.Column("custom_mxid", sa.String(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table("puppet") as batch_op: + batch_op.drop_column("custom_mxid") + batch_op.drop_column("access_token") diff --git a/mautrix_telegram/abstract_user.py b/mautrix_telegram/abstract_user.py index 1095e993..a4ae028c 100644 --- a/mautrix_telegram/abstract_user.py +++ b/mautrix_telegram/abstract_user.py @@ -229,9 +229,9 @@ class AbstractUser: async def update_status(self, update): puppet = pu.Puppet.get(update.user_id) if isinstance(update.status, UserStatusOnline): - await puppet.intent.set_presence("online") + await puppet.default_mxid_intent.set_presence("online") elif isinstance(update.status, UserStatusOffline): - await puppet.intent.set_presence("offline") + await puppet.default_mxid_intent.set_presence("offline") else: self.log.warning("Unexpected user status update: %s", update) return diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py index 67c7ab64..38bcdf52 100644 --- a/mautrix_telegram/commands/auth.py +++ b/mautrix_telegram/commands/auth.py @@ -49,6 +49,26 @@ async def ping_bot(evt: CommandEvent): "To use the bot, simply invite it to a portal room.") +@command_handler(needs_auth=True, management_only=True, + help_section=SECTION_AUTH, + help_args="<_token_>", + help_text="Replace your Telegram account's Matrix puppet with your own Matrix " + "account") +async def login_matrix(evt: CommandEvent): + puppet = pu.Puppet.get(evt.sender.tgid) + prev_info = puppet.custom_mxid, puppet.access_token + puppet.custom_mxid = evt.sender.mxid + puppet.access_token = " ".join(evt.args) + puppet.refresh_intents() + if not await puppet.get_profile(): + puppet.custom_mxid, puppet.access_token = prev_info + puppet.refresh_intents() + return await evt.reply("Failed to verify access token.") + puppet.save() + return await evt.reply( + f"Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}.") + + @command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH, help_args="<_phone_> <_full name_>", diff --git a/mautrix_telegram/db.py b/mautrix_telegram/db.py index 5393acad..32099908 100644 --- a/mautrix_telegram/db.py +++ b/mautrix_telegram/db.py @@ -137,6 +137,8 @@ class Puppet(Base): __tablename__ = "puppet" id = Column(Integer, primary_key=True) + custom_mxid = Column(String, nullable=True) + access_token = Column(String, nullable=True) displayname = Column(String, nullable=True) displayname_source = Column(Integer, nullable=True) username = Column(String, nullable=True) diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 30604296..6da5a9f5 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -39,7 +39,8 @@ class MatrixHandler: displayname = self.config["appservice.bot_displayname"] if displayname: try: - await self.az.intent.set_display_name(displayname if displayname != "remove" else "") + await self.az.intent.set_display_name( + displayname if displayname != "remove" else "") except asyncio.TimeoutError: self.log.exception("TimeoutError when trying to set displayname") @@ -51,19 +52,20 @@ class MatrixHandler: self.log.exception("TimeoutError when trying to set avatar") async def handle_puppet_invite(self, room, puppet, inviter): + intent = puppet.default_mxid_intent self.log.debug(f"{inviter} invited puppet for {puppet.tgid} to {room}") if not await inviter.is_logged_in(): - await puppet.intent.error_and_leave( + await intent.error_and_leave( room, text="Please log in before inviting Telegram puppets.") return portal = Portal.get_by_mxid(room) if portal: if portal.peer_type == "user": - await puppet.intent.error_and_leave( + await intent.error_and_leave( room, text="You can not invite additional users to private chats.") return await portal.invite_telegram(inviter, puppet) - await puppet.intent.join_room(room) + await intent.join_room(room) return try: members = await self.az.intent.get_room_members(room) @@ -71,34 +73,34 @@ class MatrixHandler: members = [] if self.az.bot_mxid not in members: if len(members) > 1: - await puppet.intent.error_and_leave(room, text=None, html=( + await intent.error_and_leave(room, text=None, html=( f"Please invite " f"the bridge bot " f"first if you want to create a Telegram chat.")) return - await puppet.intent.join_room(room) + await intent.join_room(room) portal = Portal.get_by_tgid(puppet.tgid, inviter.tgid, "user") if portal.mxid: try: - await puppet.intent.invite(portal.mxid, inviter.mxid) - await puppet.intent.send_notice(room, text=None, html=( + await intent.invite(portal.mxid, inviter.mxid) + await intent.send_notice(room, text=None, html=( "You already have a private chat with me: " f"" "Link to room" "")) - await puppet.intent.leave_room(room) + await intent.leave_room(room) return except MatrixRequestError: pass portal.mxid = room portal.save() inviter.register_portal(portal) - await puppet.intent.send_notice(room, "Portal to private chat created.") + await intent.send_notice(room, "Portal to private chat created.") else: - await puppet.intent.join_room(room) - await puppet.intent.send_notice(room, "This puppet will remain inactive until a " - "Telegram chat is created for this room.") + await intent.join_room(room) + await intent.send_notice(room, "This puppet will remain inactive until a " + "Telegram chat is created for this room.") async def accept_bot_invite(self, room, inviter): tries = 0 @@ -215,7 +217,7 @@ class MatrixHandler: await portal.handle_matrix_message(sender, message, event_id) return - if not sender.whitelisted or message["msgtype"] != "m.text": + if not sender.whitelisted or message.get("msgtype", "m.unknown") != "m.text": return try: diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py index b0977036..1403e64d 100644 --- a/mautrix_telegram/puppet.py +++ b/mautrix_telegram/puppet.py @@ -15,10 +15,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from difflib import SequenceMatcher +from typing import Optional import re import logging from telethon.tl.types import UserProfilePhoto +from mautrix_appservice import MatrixError, IntentAPI from .db import Puppet as DBPuppet from . import util @@ -35,10 +37,15 @@ class Puppet: hs_domain = None cache = {} - def __init__(self, id=None, username=None, displayname=None, displayname_source=None, - photo_id=None, is_bot=None, is_registered=False, db_instance=None): + def __init__(self, id=None, access_token=None, custom_mxid=None, username=None, + displayname=None, displayname_source=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) + self.access_token = access_token + self.custom_mxid = custom_mxid + self.is_real_user = self.custom_mxid and self.access_token + self.default_mxid = self.get_mxid_from_id(self.id) + self.mxid = self.custom_mxid or self.default_mxid self.username = username self.displayname = displayname @@ -48,10 +55,23 @@ class Puppet: self.is_registered = is_registered self._db_instance = db_instance - self.intent = self.az.intent.user(self.mxid) + self.default_mxid_intent = self.az.intent.user(self.default_mxid) + self.intent = None # type: IntentAPI + self.refresh_intents() self.cache[id] = self + def refresh_intents(self): + self.is_real_user = self.custom_mxid and self.access_token + self.intent = (self.az.intent.user(self.custom_mxid, self.access_token) + if self.is_real_user else self.default_mxid_intent) + + async def get_profile(self): + try: + return await self.intent.get_profile(self.custom_mxid) + except MatrixError: + return None + @property def tgid(self): return self.id @@ -66,17 +86,21 @@ class Puppet: return self._db_instance def new_db_instance(self): - return DBPuppet(id=self.id, username=self.username, displayname=self.displayname, + return DBPuppet(id=self.id, access_token=self.access_token, custom_mxid=self.custom_mxid, + username=self.username, displayname=self.displayname, displayname_source=self.displayname_source, photo_id=self.photo_id, 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_puppet.matrix_registered, db_instance=db_puppet) + return Puppet(db_puppet.id, db_puppet.access_token, db_puppet.custom_mxid, + db_puppet.username, db_puppet.displayname, db_puppet.displayname_source, + db_puppet.photo_id, db_puppet.is_bot, db_puppet.matrix_registered, + db_instance=db_puppet) def save(self): + self.db_instance.access_token = self.access_token + self.db_instance.custom_mxid = self.custom_mxid self.db_instance.username = self.username self.db_instance.displayname = self.displayname self.db_instance.displayname_source = self.displayname_source @@ -145,7 +169,7 @@ class Puppet: displayname = self.get_displayname(info) if displayname != self.displayname: - await self.intent.set_display_name(displayname) + await self.default_mxid_intent.set_display_name(displayname) self.displayname = displayname self.displayname_source = source.tgid return True @@ -156,15 +180,16 @@ class Puppet: async def update_avatar(self, source, photo): photo_id = f"{photo.volume_id}-{photo.local_id}" if self.photo_id != photo_id: - file = await util.transfer_file_to_matrix(self.db, source.client, self.intent, photo) + file = await util.transfer_file_to_matrix(self.db, source.client, + self.default_mxid_intent, photo) if file: - await self.intent.set_avatar(file.mxc) + await self.default_mxid_intent.set_avatar(file.mxc) self.photo_id = photo_id return True return False @classmethod - def get(cls, id, create=True): + def get(cls, id, create=True) -> "Optional[Puppet]": try: return cls.cache[id] except KeyError: @@ -183,7 +208,7 @@ class Puppet: return None @classmethod - def get_by_mxid(cls, mxid, create=True): + def get_by_mxid(cls, mxid, create=True) -> "Optional[Puppet]": tgid = cls.get_id_from_mxid(mxid) return cls.get(tgid, create) if tgid else None @@ -199,7 +224,7 @@ class Puppet: return f"@{cls.username_template.format(userid=id)}:{cls.hs_domain}" @classmethod - def find_by_username(cls, username): + def find_by_username(cls, username) -> "Optional[Puppet]": if not username: return None @@ -214,7 +239,7 @@ class Puppet: return None @classmethod - def find_by_displayname(cls, displayname): + def find_by_displayname(cls, displayname) -> "Optional[Puppet]": if not displayname: return None diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index ea0b92e3..43ad5dec 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -111,14 +111,14 @@ class User(AbstractUser): def new_db_instance(self): return DBUser(mxid=self.mxid, tgid=self.tgid, tg_username=self.username, - contacts=self.db_contacts, saved_contacts=self.saved_contacts, + contacts=self.db_contacts, saved_contacts=self.saved_contacts or 0, portals=self.db_portals) def save(self): self.db_instance.tgid = self.tgid self.db_instance.username = self.username self.db_instance.contacts = self.db_contacts - self.db_instance.saved_contacts = self.saved_contacts + self.db_instance.saved_contacts = self.saved_contacts or 0 self.db_instance.portals = self.db_portals self.db.commit()