From 06cc5246abb84a14262b19eee168da9f15fd9e1c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 27 Jan 2018 21:01:54 +0200 Subject: [PATCH] Add initial power level bridging --- README.md | 4 +- mautrix_appservice/appservice.py | 23 +++++++-- mautrix_appservice/intent_api.py | 20 +++++++- mautrix_telegram/matrix.py | 9 ++++ mautrix_telegram/portal.py | 81 ++++++++++++++++++++++++++++---- mautrix_telegram/user.py | 38 ++++++++++----- 6 files changed, 148 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 88fe10c9..8472f518 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ does not do this automatically.~~ * [ ] Presence (currently always shown as online on Telegram) * [ ] Typing notifications (may not be possible) * [ ] Pinning messages - * [ ] Power level + * [x] Power level * [ ] Membership actions * [ ] Inviting * [ ] Kicking @@ -96,7 +96,7 @@ does not do this automatically.~~ * [x] Presence * [x] Typing notifications * [ ] Pinning messages - * [ ] Admin status + * [x] Admin/chat creator status * [x] Membership actions * [x] Inviting * [x] Kicking diff --git a/mautrix_appservice/appservice.py b/mautrix_appservice/appservice.py index 342357e5..12d144a7 100644 --- a/mautrix_appservice/appservice.py +++ b/mautrix_appservice/appservice.py @@ -28,7 +28,6 @@ class StateStore: def __init__(self): self.memberships = {} self.power_levels = {} - self.power_level_requirements = {} def _get_membership(self, room, user): return self.memberships.get(room, {}).get(user, "left") @@ -50,13 +49,29 @@ class StateStore: def left(self, room, user): return self._set_membership(room, user, "left") + def has_power_level_data(self, room): + return room in self.power_levels + def has_power_level(self, room, user, event): - return True + room_levels = self.power_levels.get(room, {}) + required = room_levels["events"].get(event, 95) + has = room_levels["users"].get(user, 0) + return has >= required def set_power_level(self, room, user, level): if not room in self.power_levels: - self.power_levels[room] = {} - self.power_levels[room][user] = level + self.power_levels[room] = { + "users": {}, + "events": {}, + } + self.power_levels[room]["users"][user] = level + + def set_power_levels(self, room, content): + if "events" not in content: + content["events"] = {} + if "users" not in content: + content["users"] = {} + self.power_levels[room] = content class AppService: diff --git a/mautrix_appservice/intent_api.py b/mautrix_appservice/intent_api.py index f6ba846d..81f449b7 100644 --- a/mautrix_appservice/intent_api.py +++ b/mautrix_appservice/intent_api.py @@ -59,7 +59,7 @@ class HTTPAPI(MatrixHttpApi): 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=()): + invitees=(), initial_state=[]): """Perform /createRoom. Args: alias (str): Optional. The room alias name to set for this room. @@ -79,6 +79,8 @@ class HTTPAPI(MatrixHttpApi): content["name"] = name if topic: content["topic"] = topic + if initial_state: + content["initial_state"] = initial_state content["is_direct"] = is_direct return self._send("POST", "/createRoom", content) @@ -212,6 +214,17 @@ class IntentAPI: self._ensure_has_power_level_for(room_id, "m.room.name") return self.client.set_room_name(room_id, name) + def get_power_levels(self, room_id): + self._ensure_joined(room_id) + levels = self.client.get_power_levels(room_id) + self.state_store.set_power_levels(room_id, levels) + return levels + + def set_power_levels(self, room_id, content): + response = self.send_state_event(room_id, "m.room.power_levels", content) + self.state_store.set_power_levels(room_id, content) + return response + 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) @@ -311,8 +324,13 @@ class IntentAPI: self.registered = True def _ensure_has_power_level_for(self, room_id, event_type): + if not self.state_store.has_power_level_data(room_id): + self.get_power_levels(room_id) if self.state_store.has_power_level(room_id, self.mxid, event_type): return + elif not self.bot: + pass + # raise IntentError(f"Power level of {self.mxid} is not enough for {event_type} in {room_id}") # TODO implement # endregion diff --git a/mautrix_telegram/matrix.py b/mautrix_telegram/matrix.py index 430273ad..1a5303f0 100644 --- a/mautrix_telegram/matrix.py +++ b/mautrix_telegram/matrix.py @@ -93,6 +93,12 @@ class MatrixHandler: if portal: portal.handle_matrix_deletion(sender, event_id) + def handle_power_levels(self, room, sender, new, old): + portal = Portal.get_by_mxid(room) + if portal: + sender = User.get_by_mxid(sender) + portal.handle_matrix_power_levels(sender, new["users"], old["users"]) + def filter_matrix_event(self, event): return event["sender"] == self.az.bot_mxid or self.is_puppet(event["sender"]) @@ -114,3 +120,6 @@ class MatrixHandler: self.handle_message(evt["room_id"], evt["sender"], content, evt["event_id"]) elif type == "m.room.redaction": self.handle_redaction(evt["room_id"], evt["sender"], evt["redacts"]) + elif type == "m.room.power_levels": + self.handle_power_levels(evt["room_id"], evt["sender"], evt["content"], + evt["prev_content"]) diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py index 7ca102a0..4f1c8c8e 100644 --- a/mautrix_telegram/portal.py +++ b/mautrix_telegram/portal.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from telethon.tl.functions.messages import GetFullChatRequest +from telethon.tl.functions.messages import GetFullChatRequest, EditChatAdminRequest from telethon.tl.functions.channels import GetParticipantsRequest from telethon.errors.rpc_error_list import ChatAdminRequiredError from telethon.tl.types import * @@ -79,8 +79,9 @@ class Portal: if self.mxid: if update_if_exists: self.update_info(user, entity) - users = self.get_users(user, entity) + users, participants = self.get_users(user, entity) self.sync_telegram_users(user, users) + self.update_telegram_participants(participants) self.invite_matrix(invites) return self.mxid @@ -94,9 +95,23 @@ 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 + + power_level_requirement = 0 if self.peer_type == "chat" else 50 + initial_power_levels = { + "ban": 100, + "events": { + "m.room.name": power_level_requirement, + "m.room.avatar": power_level_requirement, + "m.room.topic": 50, + "m.room.power_levels": 50, + "invite": power_level_requirement, + }, + "users_default": 0, + } + # TODO set room alias if public channel. - room = intent.create_room(invitees=invites, name=title, - is_direct=direct) + room = intent.create_room(invitees=invites, name=title, is_direct=direct, + initial_state=[initial_power_levels]) if not room: raise Exception(f"Failed to create room for {self.tgid}") @@ -105,8 +120,9 @@ class Portal: self.save() if not direct: self.update_info(user, entity) - users = self.get_users(user, entity) + users, participants = self.get_users(user, entity) self.sync_telegram_users(user, users) + self.update_telegram_participants(participants) else: puppet.update_info(user, entity) puppet.intent.join_room(self.mxid) @@ -185,17 +201,18 @@ class Portal: def get_users(self, user, entity): if self.peer_type == "chat": - return user.client(GetFullChatRequest(chat_id=self.tgid)).users + chat = user.client(GetFullChatRequest(chat_id=self.tgid)) + return chat.users, chat.full_chat.participants.participants elif self.peer_type == "channel": try: participants = user.client(GetParticipantsRequest( entity, ChannelParticipantsRecent(), offset=0, limit=100, hash=0 )) - return participants.users + return participants.users, participants.participants except ChatAdminRequiredError: return [] elif self.peer_type == "user": - return [entity] + return [entity], [] # endregion # region Matrix event handling @@ -246,6 +263,20 @@ class Portal: return deleter.client.delete_messages(self.peer, [message.tgid]) + def handle_matrix_power_levels(self, sender, new_users, old_users): + for user, level in new_users.items(): + puppet_match = p.Puppet.mxid_regex.search(user) + if puppet_match: + user_id = int(puppet_match.group(1)) + else: + mx_user = u.User.get_by_mxid(user, create=False) + if not mx_user or not mx_user.tgid: + continue + user_id = mx_user.tgid + if user not in old_users or level != old_users[user]: + sender.client( + EditChatAdminRequest(chat_id=self.tgid, user_id=user_id, is_admin=level >= 50)) + # endregion # region Telegram event handling @@ -374,6 +405,40 @@ class Portal: else: self.log.debug("Unhandled Telegram action in %s: %s", self.title, action) + def set_telegram_admin(self, puppet, user): + levels = self.main_intent.get_power_levels(self.mxid) + if user: + levels["users"][user.mxid] = 50 + if puppet: + levels["users"][puppet.mxid] = 50 + self.main_intent.set_power_levels(self.mxid, levels) + + def update_telegram_participants(self, participants): + levels = self.main_intent.get_power_levels(self.mxid) + levels["events"]["m.room.power_levels"] = 50 + for participant in participants: + puppet = p.Puppet.get(participant.user_id) + user = u.User.get_by_tgid(participant.user_id) + new_level = 0 + if isinstance(participant, ChatParticipantAdmin): + new_level = 50 + elif isinstance(participant, ChatParticipantCreator): + new_level = 95 + if user: + levels["users"][user.mxid] = new_level + if puppet: + levels["users"][puppet.mxid] = new_level + self.main_intent.set_power_levels(self.mxid, levels) + + def set_telegram_admins_enabled(self, enabled): + level = 50 if enabled else 10 + levels = self.main_intent.get_power_levels(self.mxid) + print(levels) + levels["invite"] = level + levels["events"]["m.room.name"] = level + levels["events"]["m.room.avatar"] = level + self.main_intent.set_power_levels(self.mxid, levels) + # endregion # region Database conversion diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index c4075171..a367c2bb 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -190,22 +190,23 @@ class User: return self.update_typing(update) elif isinstance(update, UpdateUserStatus): return self.update_status(update) + elif isinstance(update, (UpdateChatAdmins, UpdateChatParticipantAdmin)): + return self.update_admin(update) + elif isinstance(update, UpdateChatParticipants): + portal = po.Portal.get_by_tgid(update.participants.chat_id, "chat") + portal.update_telegram_participants(update.participants.participants) else: self.log.debug("Unhandled update: %s", update) return - def get_message_details(self, update): - if isinstance(update, UpdateShortChatMessage): - portal = po.Portal.get_by_tgid(update.chat_id, "chat") - sender = pu.Puppet.get(update.from_id) - elif isinstance(update, UpdateShortMessage): - portal = po.Portal.get_by_tgid(update.user_id, "user") - sender = pu.Puppet.get(self.tgid if update.out else update.user_id) - elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)): - update = update.message - sender = pu.Puppet.get(update.from_id) - portal = po.Portal.get_by_entity(update.to_id) - return update, sender, portal + def update_admin(self, update): + portal = po.Portal.get_by_tgid(update.chat_id, "chat") + if isinstance(update, UpdateChatAdmins): + portal.set_telegram_admins_enabled(update.enabled) + elif isinstance(update, UpdateChatParticipantAdmin): + puppet = pu.Puppet.get(update.user_id) + user = User.get_by_tgid(update.user_id) + portal.set_telegram_admin(puppet, user) def update_typing(self, update): if isinstance(update, UpdateUserTyping): @@ -223,6 +224,19 @@ class User: puppet.intent.set_presence("offline") return + def get_message_details(self, update): + if isinstance(update, UpdateShortChatMessage): + portal = po.Portal.get_by_tgid(update.chat_id, "chat") + sender = pu.Puppet.get(update.from_id) + elif isinstance(update, UpdateShortMessage): + portal = po.Portal.get_by_tgid(update.user_id, "user") + sender = pu.Puppet.get(self.tgid if update.out else update.user_id) + elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)): + update = update.message + sender = pu.Puppet.get(update.from_id) + portal = po.Portal.get_by_entity(update.to_id) + return update, sender, portal + def update_message(self, update): update, sender, portal = self.get_message_details(update)