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