diff --git a/README.md b/README.md
index 7b7e43f4..0a54ff4f 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py
index d1e322a2..5a922bc9 100644
--- a/mautrix_appservice/intent_api.py
+++ b/mautrix_appservice/intent_api.py
@@ -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, {
diff --git a/mautrix_telegram/commands.py b/mautrix_telegram/commands.py
index d97ac954..9562d00a 100644
--- a/mautrix_telegram/commands.py
+++ b/mautrix_telegram/commands.py
@@ -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.")
diff --git a/mautrix_telegram/db.py b/mautrix_telegram/db.py
index a9353daf..d7cf1a22 100644
--- a/mautrix_telegram/db.py
+++ b/mautrix_telegram/db.py
@@ -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):
diff --git a/mautrix_telegram/formatter.py b/mautrix_telegram/formatter.py
new file mode 100644
index 00000000..c17f61fc
--- /dev/null
+++ b/mautrix_telegram/formatter.py
@@ -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 {entity_text}")
+ elif entity_type == MessageEntityPre:
+ if entity.language:
+ html.append("
"
+ f"{entity_text}"
+ "")
+ else:
+ html.append(f"{entity_text}")
+ 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"{entity_text}")
+ 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"{entity_text}")
+ else:
+ skip_entity = True
+ elif entity_type == MessageEntityEmail:
+ html.append(f"{entity_text}")
+ elif entity_type == MessageEntityUrl:
+ html.append(f"{entity_text}")
+ elif entity_type == MessageEntityTextUrl:
+ html.append(f"{entity_text}")
+ elif entity_type == MessageEntityBotCommand:
+ html.append(f"!{entity_text[1:]}")
+ elif entity_type == MessageEntityHashtag:
+ html.append(f"{entity_text}")
+ else:
+ skip_entity = True
+ last_offset = entity.offset + (0 if skip_entity else entity.length)
+ html.append(text[last_offset:])
+ return "".join(html)
diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py
index 6f6ee043..0b3d129b 100644
--- a/mautrix_telegram/portal.py
+++ b/mautrix_telegram/portal.py
@@ -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):
diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py
index 14734956..ce7e4c77 100644
--- a/mautrix_telegram/puppet.py
+++ b/mautrix_telegram/puppet.py
@@ -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
diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py
index 0b7ebd31..dde58eaf 100644
--- a/mautrix_telegram/user.py
+++ b/mautrix_telegram/user.py
@@ -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