Initial option to replace Matrix puppet of own Telegram account

This commit is contained in:
Tulir Asokan
2018-07-20 12:35:22 -04:00
parent ad7b7f5c06
commit 2b92483c50
7 changed files with 108 additions and 33 deletions
@@ -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")
+2 -2
View File
@@ -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
+20
View File
@@ -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_>",
+2
View File
@@ -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)
+16 -14
View File
@@ -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"<a href='https://matrix.to/#/{self.az.bot_mxid}'>the bridge bot</a> "
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"<a href='https://matrix.to/#/{portal.mxid}'>"
"Link to room"
"</a>"))
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:
+40 -15
View File
@@ -15,10 +15,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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
+2 -2
View File
@@ -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()