Implement room name and avatar change handling
This commit is contained in:
@@ -57,7 +57,8 @@ does not do this automatically.
|
||||
* [x] Formatted messages
|
||||
* [ ] Bot commands (!command -> /command)
|
||||
* [x] Mentions
|
||||
* [ ] Locations
|
||||
* [ ] Rich quotes
|
||||
* [ ] Locations (not implemented in Riot)
|
||||
* [ ] Images
|
||||
* [ ] Files
|
||||
* [ ] Message redactions
|
||||
@@ -76,6 +77,8 @@ does not do this automatically.
|
||||
* [x] Formatted messages
|
||||
* [x] Bot commands (/command -> !command)
|
||||
* [x] Mentions
|
||||
* [ ] Replies
|
||||
* [ ] Forwards
|
||||
* [ ] Images
|
||||
* [ ] Locations
|
||||
* [ ] Stickers
|
||||
@@ -91,8 +94,10 @@ does not do this automatically.
|
||||
* [ ] Inviting
|
||||
* [ ] Kicking
|
||||
* [ ] Joining/leaving
|
||||
* [ ] Chat metadata changes
|
||||
* [ ] Initial chat metadata
|
||||
* [x] Chat metadata changes
|
||||
* [ ] Public channel username changes
|
||||
* [x] Initial chat metadata
|
||||
* [ ] Supergroup upgrade
|
||||
* [ ] Message edits
|
||||
* Initiating chats
|
||||
* [x] Automatic portal creation for groups/channels at startup
|
||||
@@ -104,6 +109,4 @@ does not do this automatically.
|
||||
* [ ] Creating Telegram chats for existing Matrix rooms
|
||||
* Misc
|
||||
* [ ] Use optional bot to relay messages for unauthenticated Matrix users
|
||||
* [ ] Properly handle upgrading groups to supergroups
|
||||
* [ ] Allow upgrading group to supergroup from Matrix
|
||||
* [ ] Handle public channel username changes
|
||||
* [ ] Command to upgrade chat to supergroup from Matrix
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import re
|
||||
import json
|
||||
import magic
|
||||
from matrix_client.api import MatrixHttpApi
|
||||
from matrix_client.errors import MatrixRequestError
|
||||
|
||||
@@ -44,12 +45,14 @@ class HTTPAPI(MatrixHttpApi):
|
||||
def intent(self, user):
|
||||
return IntentAPI(user, self.user(user), self, log=self.log)
|
||||
|
||||
def _send(self, method, path, content=None, query_params={}, headers={}):
|
||||
def _send(self, method, path, content=None, query_params={}, headers={},
|
||||
api_path="/_matrix/client/r0"):
|
||||
if not query_params:
|
||||
query_params = {}
|
||||
query_params["user_id"] = self.identity
|
||||
self.log.debug("%s %s %s", method, path, content)
|
||||
return super()._send(method, path, content, query_params, headers)
|
||||
log_content = content if not isinstance(content, bytes) else f"<{len(content)} bytes>"
|
||||
self.log.debug("%s %s %s", method, path, log_content)
|
||||
return super()._send(method, path, content, query_params, headers, api_path=api_path)
|
||||
|
||||
def create_room(self, alias=None, is_public=False, name=None, topic=None, is_direct=False,
|
||||
invitees=()):
|
||||
@@ -157,6 +160,11 @@ class IntentAPI:
|
||||
self._ensure_registered()
|
||||
return self.client.set_presence(status)
|
||||
|
||||
def media_upload(self, photo_data, mime_type=None):
|
||||
self._ensure_registered()
|
||||
mime_type = mime_type or magic.from_buffer(photo_data, mime=True)
|
||||
return self.client.media_upload(photo_data, mime_type)
|
||||
|
||||
def set_typing(self, room_id, is_typing=True, timeout=5000):
|
||||
self._ensure_joined(room_id)
|
||||
return self.client.set_typing(room_id, is_typing, timeout)
|
||||
@@ -166,6 +174,21 @@ class IntentAPI:
|
||||
self._ensure_registered()
|
||||
return self.client.create_room(alias, is_public, name, topic, is_direct, invitees)
|
||||
|
||||
def set_room_avatar(self, room_id, avatar_url, info=None):
|
||||
content = {
|
||||
"url": avatar_url,
|
||||
}
|
||||
if info:
|
||||
content["info"] = info
|
||||
self._ensure_joined(room_id)
|
||||
self._ensure_has_power_level_for(room_id, "m.room.avatar")
|
||||
return self.send_state_event(room_id, "m.room.avatar", content)
|
||||
|
||||
def set_room_name(self, room_id, name):
|
||||
self._ensure_joined(room_id)
|
||||
self._ensure_has_power_level_for(room_id, "m.room.name")
|
||||
return self.client.set_room_name(room_id, name)
|
||||
|
||||
def send_text(self, room_id, text, html=None, notice=False):
|
||||
if html:
|
||||
return self.send_message(room_id, {
|
||||
|
||||
@@ -23,10 +23,18 @@ from .base import Base
|
||||
class Portal(Base):
|
||||
__tablename__ = "portal"
|
||||
|
||||
# Telegram chat information
|
||||
tgid = Column(Integer, primary_key=True)
|
||||
peer_type = Column(String)
|
||||
|
||||
# Matrix portal information
|
||||
mxid = Column(String, unique=True, nullable=True)
|
||||
|
||||
# Telegram chat metadata
|
||||
username = Column(String, nullable=True)
|
||||
title = Column(String, nullable=True)
|
||||
photo_id = Column(String, nullable=True)
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "user"
|
||||
|
||||
+113
-9
@@ -13,9 +13,11 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
from io import BytesIO
|
||||
|
||||
from telethon.tl.functions.messages import GetFullChatRequest
|
||||
from telethon.tl.functions.channels import GetParticipantsRequest
|
||||
from telethon.tl.types import ChannelParticipantsRecent, PeerChat, PeerChannel, PeerUser
|
||||
from telethon.tl.types import *
|
||||
from .db import Portal as DBPortal
|
||||
from . import puppet as p, formatter
|
||||
|
||||
@@ -26,25 +28,35 @@ class Portal:
|
||||
by_mxid = {}
|
||||
by_tgid = {}
|
||||
|
||||
def __init__(self, tgid, peer_type, mxid=None):
|
||||
def __init__(self, tgid, peer_type, mxid=None, username=None, title=None, photo_id=None):
|
||||
self.mxid = mxid
|
||||
self.tgid = tgid
|
||||
self.peer_type = peer_type
|
||||
self.username = username
|
||||
self.title = title
|
||||
self.photo_id = photo_id
|
||||
|
||||
self.by_tgid[tgid] = self
|
||||
if mxid:
|
||||
self.by_mxid[mxid] = self
|
||||
|
||||
def create_room(self, user, entity=None, invites=[]):
|
||||
def get_main_intent(self):
|
||||
direct = self.peer_type == "user"
|
||||
puppet = p.Puppet.get(self.tgid) if direct else None
|
||||
return puppet.intent if direct else self.az.intent
|
||||
|
||||
def create_room(self, user, entity=None, invites=[], update_if_exists=True):
|
||||
self.log.debug("Creating room for %d", self.tgid)
|
||||
if not entity:
|
||||
entity = user.client.get_entity(self.peer)
|
||||
self.log.debug("Fetched data: %s", entity)
|
||||
|
||||
if self.mxid:
|
||||
if update_if_exists:
|
||||
self.update_info(user, entity)
|
||||
users = self.get_users(user, entity)
|
||||
self.sync_telegram_users(users)
|
||||
self.invite_matrix(invites)
|
||||
users = self.get_users(user, entity)
|
||||
self.sync_telegram_users(users)
|
||||
return self.mxid
|
||||
|
||||
try:
|
||||
@@ -55,8 +67,9 @@ class Portal:
|
||||
direct = self.peer_type == "user"
|
||||
puppet = p.Puppet.get(self.tgid) if direct else None
|
||||
intent = puppet.intent if direct else self.az.intent
|
||||
# TODO set room alias if public channel.
|
||||
room = intent.create_room(invitees=invites, name=title,
|
||||
is_direct=direct)
|
||||
is_direct=direct)
|
||||
if not room:
|
||||
raise Exception(f"Failed to create room for {self.tgid}")
|
||||
|
||||
@@ -64,6 +77,7 @@ class Portal:
|
||||
self.by_mxid[self.mxid] = self
|
||||
self.save()
|
||||
if not direct:
|
||||
self.update_info(user, entity)
|
||||
users = self.get_users(user, entity)
|
||||
self.sync_telegram_users(users)
|
||||
else:
|
||||
@@ -76,6 +90,33 @@ class Portal:
|
||||
user.update_info(entity)
|
||||
user.intent.join_room(self.mxid)
|
||||
|
||||
def update_info(self, user, entity=None):
|
||||
if self.peer_type == "user":
|
||||
self.log.warn("Called update_info() for direct chat portal %d", self.tgid)
|
||||
return
|
||||
|
||||
self.log.debug("Updating info of %d", self.tgid)
|
||||
if not entity:
|
||||
entity = user.client.get_entity(self.peer)
|
||||
self.log.debug("Fetched data: %s", entity)
|
||||
changed = False
|
||||
|
||||
intent = self.get_main_intent()
|
||||
|
||||
if self.peer_type == "channel":
|
||||
if self.username != entity.username:
|
||||
# TODO update room alias
|
||||
self.username = entity.username
|
||||
changed = True
|
||||
|
||||
changed = self.update_title(entity.title, intent) or changed
|
||||
|
||||
if isinstance(entity.photo, ChatPhoto):
|
||||
changed = self.update_avatar(user, entity.photo.photo_big, intent) or changed
|
||||
|
||||
if changed:
|
||||
self.save()
|
||||
|
||||
def handle_matrix_message(self, sender, message):
|
||||
type = message["msgtype"]
|
||||
if type == "m.text":
|
||||
@@ -97,6 +138,45 @@ class Portal:
|
||||
else:
|
||||
sender.intent.send_text(self.mxid, evt.message)
|
||||
|
||||
def update_title(self, title, intent=None):
|
||||
if self.title != title:
|
||||
self.title = title
|
||||
intent = intent or self.get_main_intent()
|
||||
intent.set_room_name(self.mxid, self.title)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def update_avatar(self, user, photo, intent=None):
|
||||
photo_id = f"{photo.volume_id}-{photo.local_id}"
|
||||
if self.photo_id != photo_id:
|
||||
intent = intent or self.get_main_intent()
|
||||
|
||||
file = BytesIO()
|
||||
|
||||
user.client.download_file(
|
||||
InputFileLocation(photo.volume_id, photo.local_id, photo.secret), file)
|
||||
|
||||
uploaded = intent.media_upload(file.getvalue())
|
||||
intent.set_room_avatar(self.mxid, uploaded["content_uri"])
|
||||
|
||||
file.close()
|
||||
|
||||
self.photo_id = photo_id
|
||||
return True
|
||||
return False
|
||||
|
||||
def handle_telegram_action(self, source, sender, action):
|
||||
intent = self.get_main_intent()
|
||||
action_type = type(action)
|
||||
if action_type == MessageActionChatEditTitle:
|
||||
if self.update_title(action.title, intent):
|
||||
self.save()
|
||||
elif action_type == MessageActionChatEditPhoto:
|
||||
largest_size = max(action.photo.sizes, key=lambda photo: photo.size)
|
||||
if self.update_avatar(source, largest_size.location, intent):
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def peer(self):
|
||||
if self.peer_type == "user":
|
||||
@@ -121,7 +201,9 @@ class Portal:
|
||||
pass
|
||||
|
||||
def to_db(self):
|
||||
return self.db.merge(DBPortal(tgid=self.tgid, peer_type=self.peer_type, mxid=self.mxid))
|
||||
return self.db.merge(DBPortal(tgid=self.tgid, peer_type=self.peer_type, mxid=self.mxid,
|
||||
username=self.username, title=self.title,
|
||||
photo_id=self.photo_id))
|
||||
|
||||
def save(self):
|
||||
self.to_db()
|
||||
@@ -129,7 +211,8 @@ class Portal:
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, db_portal):
|
||||
return Portal(db_portal.tgid, db_portal.peer_type, db_portal.mxid)
|
||||
return Portal(db_portal.tgid, db_portal.peer_type, db_portal.mxid, db_portal.username,
|
||||
db_portal.title, db_portal.photo_id)
|
||||
|
||||
@classmethod
|
||||
def get_by_mxid(cls, mxid):
|
||||
@@ -165,7 +248,28 @@ class Portal:
|
||||
|
||||
@classmethod
|
||||
def get_by_entity(cls, entity):
|
||||
return cls.get_by_tgid(entity.id, entity.__class__.__name__.lower())
|
||||
entity_type = type(entity)
|
||||
if entity_type in {Chat, ChatFull}:
|
||||
type_name = "chat"
|
||||
id = entity.id
|
||||
elif entity_type in {PeerChat, InputPeerChat}:
|
||||
type_name = "chat"
|
||||
id = entity.chat_id
|
||||
elif entity_type in {Channel, ChannelFull}:
|
||||
type_name = "channel"
|
||||
id = entity.id
|
||||
elif entity_type in {PeerChannel, InputPeerChannel, InputChannel}:
|
||||
type_name = "channel"
|
||||
id = entity.channel_id
|
||||
elif entity_type in {User, UserFull}:
|
||||
type_name = "user"
|
||||
id = entity.id
|
||||
elif entity_type in {PeerUser, InputPeerUser, InputUser}:
|
||||
type_name = "user"
|
||||
id = entity.user_id
|
||||
else:
|
||||
raise ValueError(f"Unknown entity type {entity_type.__name__}")
|
||||
return cls.get_by_tgid(id, type_name)
|
||||
|
||||
|
||||
def init(context):
|
||||
|
||||
@@ -50,7 +50,8 @@ class Puppet:
|
||||
self.to_db()
|
||||
self.db.commit()
|
||||
|
||||
def get_displayname(self, info):
|
||||
@staticmethod
|
||||
def get_displayname(info):
|
||||
if info.first_name or info.last_name:
|
||||
name = " ".join([info.first_name or "", info.last_name or ""]).strip()
|
||||
elif info.username:
|
||||
|
||||
@@ -123,7 +123,6 @@ class User:
|
||||
continue
|
||||
portal = po.Portal.get_by_entity(entity)
|
||||
portal.create_room(self, entity, invites=[self.mxid])
|
||||
# portal.update_info(self, entity)
|
||||
|
||||
def update_catch(self, update):
|
||||
try:
|
||||
@@ -153,15 +152,25 @@ class User:
|
||||
elif isinstance(update.status, UserStatusOffline):
|
||||
puppet.intent.set_presence("offline")
|
||||
return
|
||||
elif update_type == UpdateNewMessage or update_type == UpdateNewChannelMessage:
|
||||
update = update.message
|
||||
sender = pu.Puppet.get(update.from_id)
|
||||
portal = po.Portal.get_by_entity(update.to_id)
|
||||
else:
|
||||
self.log.debug("Unhandled update: %s", update)
|
||||
return
|
||||
|
||||
if not portal.mxid:
|
||||
portal.create_room(self, invites=[self.mxid])
|
||||
self.log.debug("Handling message portal=%s sender=%s update=%s", portal, sender,
|
||||
|
||||
if isinstance(update, MessageService):
|
||||
self.log.debug("Handling action portal=%s sender=%s action=%s", portal, sender,
|
||||
update.action)
|
||||
portal.handle_telegram_action(self, sender, update.action)
|
||||
else:
|
||||
self.log.debug("Handling message portal=%s sender=%s update=%s", portal, sender,
|
||||
update)
|
||||
portal.handle_telegram_message(sender, update)
|
||||
portal.handle_telegram_message(sender, update)
|
||||
|
||||
@classmethod
|
||||
def get_by_mxid(cls, mxid, create=True):
|
||||
|
||||
@@ -2,6 +2,7 @@ aiohttp
|
||||
-e git+git://github.com/Cadair/matrix-python-sdk#egg=matrix_client
|
||||
#matrix-client
|
||||
ruamel.yaml
|
||||
python-magic
|
||||
SQLAlchemy
|
||||
Telethon
|
||||
Markdown
|
||||
|
||||
Reference in New Issue
Block a user