Implement Telegram -> Matrix formatted message bridging
This commit is contained in:
@@ -73,9 +73,9 @@ does not do this automatically.
|
||||
* [ ] Room invites
|
||||
* Telegram → Matrix
|
||||
* [x] Plaintext messages
|
||||
* [ ] Formatted messages
|
||||
* [ ] Bot commands (/command -> !command)
|
||||
* [ ] Mentions
|
||||
* [x] Formatted messages
|
||||
* [x] Bot commands (/command -> !command)
|
||||
* [x] Mentions
|
||||
* [ ] Images
|
||||
* [ ] Locations
|
||||
* [ ] Stickers
|
||||
|
||||
@@ -142,13 +142,13 @@ class IntentAPI:
|
||||
self._ensure_registered()
|
||||
return self.client.create_room(alias, is_public, name, topic, is_direct, invitees)
|
||||
|
||||
def send_text(self, room_id, text, html=False, formatted_text=None, notice=False):
|
||||
def send_text(self, room_id, text, html=None, notice=False):
|
||||
if html:
|
||||
return self.send_message(room_id, {
|
||||
"body": text,
|
||||
"msgtype": "m.notice" if notice else "m.text",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": formatted_text or text,
|
||||
"formatted_body": html or text,
|
||||
})
|
||||
else:
|
||||
return self.send_message(room_id, {
|
||||
|
||||
@@ -69,8 +69,7 @@ class CommandHandler:
|
||||
html = markdown.markdown(message, safe_mode="escape" if allow_html else False)
|
||||
elif allow_html:
|
||||
html = message
|
||||
self.az.intent.send_text(self._room_id, message, formatted_text=html,
|
||||
html=True if html else False, notice=True)
|
||||
self.az.intent.send_text(self._room_id, message, html=html, notice=True)
|
||||
|
||||
@command_handler
|
||||
def register(self, sender, args):
|
||||
@@ -103,6 +102,7 @@ class CommandHandler:
|
||||
|
||||
try:
|
||||
user = sender.client.sign_in(code=args[0])
|
||||
sender.update_info(user)
|
||||
sender.command_status = None
|
||||
return self.reply(f"Successfully logged in as @{user.username}")
|
||||
except PhoneNumberUnoccupiedError:
|
||||
@@ -143,6 +143,7 @@ class CommandHandler:
|
||||
|
||||
try:
|
||||
user = sender.client.sign_in(password=args[0])
|
||||
sender.update_info(user)
|
||||
sender.command_status = None
|
||||
return self.reply(f"Successfully logged in as @{user.username}")
|
||||
except PasswordHashInvalidError:
|
||||
@@ -156,7 +157,7 @@ class CommandHandler:
|
||||
def logout(self, sender, args):
|
||||
if not sender.logged_in:
|
||||
return self.reply("You're not logged in.")
|
||||
if sender.client.log_out():
|
||||
if sender.log_out():
|
||||
return self.reply("Logged out successfully.")
|
||||
return self.reply("Failed to log out.")
|
||||
|
||||
|
||||
@@ -33,10 +33,7 @@ class User(Base):
|
||||
|
||||
mxid = Column(String, primary_key=True)
|
||||
tgid = Column(Integer, nullable=True)
|
||||
|
||||
def __init__(self, mxid, tgid=None):
|
||||
self.mxid = mxid
|
||||
self.tgid = tgid
|
||||
tg_username = Column(String, nullable=True)
|
||||
|
||||
|
||||
class Puppet(Base):
|
||||
@@ -44,6 +41,7 @@ class Puppet(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
displayname = Column(String, nullable=True)
|
||||
username = Column(String, nullable=True)
|
||||
|
||||
|
||||
def init(db_factory):
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import re
|
||||
from html import escape, unescape
|
||||
from telethon.tl.types import *
|
||||
from . import user as u, puppet as p
|
||||
|
||||
|
||||
def telegram_to_matrix(text, entities):
|
||||
if not entities:
|
||||
return text
|
||||
html = []
|
||||
last_offset = 0
|
||||
for entity in entities:
|
||||
if entity.offset > last_offset:
|
||||
html.append(escape(text[last_offset:entity.offset]))
|
||||
elif entity.offset < last_offset:
|
||||
continue
|
||||
|
||||
skip_entity = False
|
||||
entity_text = escape(text[entity.offset:entity.offset + entity.length])
|
||||
entity_type = type(entity)
|
||||
|
||||
if entity_type == MessageEntityBold:
|
||||
html.append(f"<strong>{entity_text}</strong>")
|
||||
elif entity_type == MessageEntityItalic:
|
||||
html.append(f"<em>{entity_text}</em>")
|
||||
elif entity_type == MessageEntityCode:
|
||||
html.append(f"<code>{entity_text}</code>")
|
||||
elif entity_type == MessageEntityPre:
|
||||
if entity.language:
|
||||
html.append("<pre>"
|
||||
f"<code class='language-{entity.language}'>{entity_text}</code>"
|
||||
"</pre>")
|
||||
else:
|
||||
html.append(f"<pre><code>{entity_text}</code></pre>")
|
||||
elif entity_type == MessageEntityMention:
|
||||
username = entity_text[1:]
|
||||
|
||||
user = u.User.find_by_username(username)
|
||||
if user:
|
||||
mxid = user.mxid
|
||||
else:
|
||||
puppet = p.Puppet.find_by_username(username)
|
||||
mxid = puppet.mxid if puppet else None
|
||||
if mxid:
|
||||
html.append(f"<a href='https://matrix.to/#/{mxid}'>{entity_text}</a>")
|
||||
else:
|
||||
skip_entity = True
|
||||
elif entity_type == MessageEntityMentionName:
|
||||
user = u.User.get_by_tgid(entity.user_id)
|
||||
if user:
|
||||
mxid = user.mxid
|
||||
else:
|
||||
puppet = p.Puppet.get(entity.user_id, create=False)
|
||||
mxid = puppet.mxid if puppet else None
|
||||
if mxid:
|
||||
html.append(f"<a href='https://matrix.to/#/{mxid}'>{entity_text}</a>")
|
||||
else:
|
||||
skip_entity = True
|
||||
elif entity_type == MessageEntityEmail:
|
||||
html.append(f"<a href='mailto:{entity_text}'>{entity_text}</a>")
|
||||
elif entity_type == MessageEntityUrl:
|
||||
html.append(f"<a href='{entity_text}'>{entity_text}</a>")
|
||||
elif entity_type == MessageEntityTextUrl:
|
||||
html.append(f"<a href='{escape(entity.url)}'>{entity_text}</a>")
|
||||
elif entity_type == MessageEntityBotCommand:
|
||||
html.append(f"<font color='blue'>!{entity_text[1:]}")
|
||||
elif entity_type == MessageEntityHashtag:
|
||||
html.append(f"<font color='blue'>{entity_text}</font>")
|
||||
else:
|
||||
skip_entity = True
|
||||
last_offset = entity.offset + (0 if skip_entity else entity.length)
|
||||
html.append(text[last_offset:])
|
||||
return "".join(html)
|
||||
@@ -17,7 +17,7 @@ from telethon.tl.functions.messages import GetFullChatRequest
|
||||
from telethon.tl.functions.channels import GetParticipantsRequest
|
||||
from telethon.tl.types import ChannelParticipantsRecent, PeerChat, PeerChannel, PeerUser
|
||||
from .db import Portal as DBPortal
|
||||
from . import puppet as p
|
||||
from . import puppet as p, formatter
|
||||
|
||||
config = None
|
||||
|
||||
@@ -70,7 +70,6 @@ class Portal:
|
||||
puppet.update_info(entity)
|
||||
puppet.intent.join_room(self.mxid)
|
||||
|
||||
|
||||
def sync_telegram_users(self, users=[]):
|
||||
for entity in users:
|
||||
user = p.Puppet.get(entity.id)
|
||||
@@ -82,9 +81,14 @@ class Portal:
|
||||
if type == "m.text":
|
||||
sender.client.send_message(self.peer, message["body"])
|
||||
|
||||
def handle_telegram_message(self, sender, message):
|
||||
self.log.debug("Sending %s to %s by %d", message.message, self.mxid, sender.id)
|
||||
sender.intent.send_text(self.mxid, message.message)
|
||||
def handle_telegram_message(self, sender, evt):
|
||||
self.log.debug("Sending %s to %s by %d", evt.message, self.mxid, sender.id)
|
||||
if evt.message:
|
||||
if evt.entities:
|
||||
html = formatter.telegram_to_matrix(evt.message, evt.entities)
|
||||
sender.intent.send_text(self.mxid, evt.message, html=html)
|
||||
else:
|
||||
sender.intent.send_text(self.mxid, evt.message)
|
||||
|
||||
@property
|
||||
def peer(self):
|
||||
|
||||
@@ -24,23 +24,25 @@ config = None
|
||||
class Puppet:
|
||||
cache = {}
|
||||
|
||||
def __init__(self, id=None, displayname=None):
|
||||
def __init__(self, id=None, username=None, displayname=None):
|
||||
self.id = id
|
||||
|
||||
self.localpart = config.get("bridge.alias_template", "telegram_{}").format(self.id)
|
||||
hs = config["homeserver"]["domain"]
|
||||
self.mxid = f"@{self.localpart}:{hs}"
|
||||
self.username = username
|
||||
self.displayname = displayname
|
||||
self.intent = self.az.intent.user(self.mxid)
|
||||
|
||||
self.cache[id] = self
|
||||
|
||||
def to_db(self):
|
||||
return self.db.merge(DBPuppet(id=self.id, displayname=self.displayname))
|
||||
return self.db.merge(
|
||||
DBPuppet(id=self.id, username=self.username, displayname=self.displayname))
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, db_puppet):
|
||||
return Puppet(db_puppet.id, db_puppet.displayname)
|
||||
return Puppet(db_puppet.id, db_puppet.username, db_puppet.displayname)
|
||||
|
||||
def save(self):
|
||||
self.to_db()
|
||||
@@ -59,6 +61,9 @@ class Puppet:
|
||||
|
||||
def update_info(self, info):
|
||||
changed = False
|
||||
if self.username != info.username:
|
||||
self.username = info.username
|
||||
changed = True
|
||||
displayname = self.get_displayname(info)
|
||||
if displayname != self.displayname:
|
||||
self.intent.set_display_name(displayname)
|
||||
@@ -87,6 +92,18 @@ class Puppet:
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def find_by_username(cls, username):
|
||||
for _, puppet in cls.cache.items():
|
||||
if puppet.username == username:
|
||||
return puppet
|
||||
|
||||
puppet = DBPuppet.query.filter(DBPuppet.username == username).one_or_none()
|
||||
if puppet:
|
||||
return cls.from_db(puppet)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def init(context):
|
||||
global config
|
||||
|
||||
@@ -27,9 +27,10 @@ class User:
|
||||
by_mxid = {}
|
||||
by_tgid = {}
|
||||
|
||||
def __init__(self, mxid, tgid=None):
|
||||
def __init__(self, mxid, tgid=None, username=None):
|
||||
self.mxid = mxid
|
||||
self.tgid = tgid
|
||||
self.username = username
|
||||
|
||||
self.command_status = None
|
||||
self.connected = False
|
||||
@@ -44,7 +45,7 @@ class User:
|
||||
return self.client.is_user_authorized()
|
||||
|
||||
def to_db(self):
|
||||
return self.db.merge(DBUser(self.mxid, self.tgid))
|
||||
return self.db.merge(DBUser(mxid=self.mxid, tgid=self.tgid, tg_username=self.username))
|
||||
|
||||
def save(self):
|
||||
self.to_db()
|
||||
@@ -52,7 +53,7 @@ class User:
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, db_user):
|
||||
return User(db_user.mxid, db_user.tgid)
|
||||
return User(db_user.mxid, db_user.tgid, db_user.tg_username)
|
||||
|
||||
def start(self):
|
||||
self.client = TelegramClient(self.mxid,
|
||||
@@ -62,9 +63,27 @@ class User:
|
||||
self.connected = self.client.connect()
|
||||
if self.logged_in:
|
||||
self.sync_dialogs()
|
||||
self.update_info()
|
||||
self.client.add_update_handler(self.update_catch)
|
||||
return self
|
||||
|
||||
def update_info(self, info=None):
|
||||
info = info or self.client.get_me()
|
||||
self.username = info.username
|
||||
if self.tgid != info.id:
|
||||
self.tgid = info.id
|
||||
self.by_tgid[self.tgid] = self
|
||||
self.save()
|
||||
|
||||
def log_out(self):
|
||||
self.connected = False
|
||||
if self.tgid:
|
||||
try:
|
||||
del self.tgid[self.tgid]
|
||||
except KeyError:
|
||||
pass
|
||||
return self.client.log_out()
|
||||
|
||||
def stop(self):
|
||||
self.client.disconnect()
|
||||
self.client = None
|
||||
@@ -137,6 +156,17 @@ class User:
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def find_by_username(cls, username):
|
||||
for _, user in cls.by_tgid.items():
|
||||
if user.username == username:
|
||||
return user
|
||||
|
||||
puppet = DBUser.query.filter(DBUser.tg_username == username).one_or_none()
|
||||
if puppet:
|
||||
return cls.from_db(puppet)
|
||||
|
||||
return None
|
||||
|
||||
def init(context):
|
||||
global config
|
||||
|
||||
Reference in New Issue
Block a user