From 261f99ac82cef196dd9910c11ad518e42a8ec31b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 6 Apr 2022 12:40:55 +0300 Subject: [PATCH] Add provisioning API for listing contacts and starting DMs --- CHANGELOG.md | 10 ++- mautrix_telegram/puppet.py | 8 +++ mautrix_telegram/user.py | 16 +++-- mautrix_telegram/web/provisioning/__init__.py | 62 ++++++++++++++++++- 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f85293f..d28122a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added * Added `list-invite-links` command to list invite links in a chat. +* Added option to use [MSC2246] async media uploads. +* Provisioning API for listing contacts and starting private chats. ### Improved * Dropped Python 3.7 support. @@ -30,6 +32,8 @@ to user mentions otherwise. * Fixed newlines being lost in unformatted forwarded messages. +[MSC2246]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246 + # v0.11.2 (2022-02-14) **N.B.** This will be the last release to support Python 3.7. Future versions @@ -261,8 +265,8 @@ path. * Bridging events of a user whose power level is malformed (i.e. a string instead of an integer) now works. -[MSC2409]: https://github.com/matrix-org/matrix-doc/pull/2409 -[MSC2778]: https://github.com/matrix-org/matrix-doc/pull/2778 +[MSC2409]: https://github.com/matrix-org/matrix-spec-proposals/pull/2409 +[MSC2778]: https://github.com/matrix-org/matrix-spec-proposals/pull/2778 # v0.8.2 (2020-07-27) @@ -310,7 +314,7 @@ update (v0.5.8) and a fix to the Docker image. * Fixed `sync_direct_chats` option creating non-working portals. * Fixed video thumbnailing sometimes leaving behind downloaded videos in `/tmp`. -[MSC2346]: https://github.com/matrix-org/matrix-doc/pull/2346 +[MSC2346]: https://github.com/matrix-org/matrix-spec-proposals/pull/2346 ## rc1 (2020-04-25) diff --git a/mautrix_telegram/puppet.py b/mautrix_telegram/puppet.py index a787f9df..da338884 100644 --- a/mautrix_telegram/puppet.py +++ b/mautrix_telegram/puppet.py @@ -129,6 +129,14 @@ class Puppet(DBPuppet, BasePuppet): PeerChannel(channel_id=self.tgid) if self.is_channel else PeerUser(user_id=self.tgid) ) + @property + def contact_info(self) -> dict: + return { + "name": self.displayname, + "username": self.username, + "is_bot": self.is_bot, + } + @property def plain_displayname(self) -> str: return self.displayname_template.parse(self.displayname) or self.displayname diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index 0e0bb454..f98e3fe3 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -659,20 +659,28 @@ class User(DBUser, AbstractUser, BaseUser): acc = (acc * 20261 + contact) & 0xFFFFFFFF return acc & 0x7FFFFFFF - async def sync_contacts(self) -> None: + async def sync_contacts(self, get_info: bool = False) -> dict[TelegramID, dict]: existing_contacts = await self.get_contacts() contact_hash = self._hash_contacts(self.saved_contacts, existing_contacts) response = await self.client(GetContactsRequest(hash=contact_hash)) if isinstance(response, ContactsNotModified): - return + if get_info: + return { + tgid: (await pu.Puppet.get_by_tgid(tgid)).contact_info + for tgid in existing_contacts + } + return {} self.log.debug(f"Updating contacts of {self.name}...") if self.saved_contacts != response.saved_count: self.saved_contacts = response.saved_count await self.save() + contacts = {} for user in response.users: - puppet = await pu.Puppet.get_by_tgid(user.id) + puppet: pu.Puppet = await pu.Puppet.get_by_tgid(user.id) await puppet.update_info(self, user) - await self.set_contacts(user.id for user in response.users) + contacts[user.id] = puppet.contact_info + await self.set_contacts(contacts.keys()) + return contacts # endregion # region Class instance lookup diff --git a/mautrix_telegram/web/provisioning/__init__.py b/mautrix_telegram/web/provisioning/__init__.py index 9e7ab778..2ca4f911 100644 --- a/mautrix_telegram/web/provisioning/__init__.py +++ b/mautrix_telegram/web/provisioning/__init__.py @@ -21,7 +21,7 @@ import json import logging from aiohttp import web -from telethon.tl.types import ChannelForbidden, ChatForbidden, TypeChat +from telethon.tl.types import ChannelForbidden, ChatForbidden, TypeChat, User as TLUser from telethon.utils import get_peer_id, resolve_id from mautrix.appservice import AppService @@ -65,6 +65,8 @@ class ProvisioningAPI(AuthAPI): user_prefix = "/user/{mxid:@[^:]*:[^/]+}" self.app.router.add_route("GET", f"{user_prefix}", self.get_user_info) self.app.router.add_route("GET", f"{user_prefix}/chats", self.get_chats) + self.app.router.add_route("GET", f"{user_prefix}/contacts", self.get_contacts) + self.app.router.add_route("POST", f"{user_prefix}/pm/{{identifier}}", self.start_dm) self.app.router.add_route("POST", f"{user_prefix}/logout", self.logout) self.app.router.add_route("POST", f"{user_prefix}/login/bot_token", self.send_bot_token) @@ -393,6 +395,62 @@ class ProvisioningAPI(AuthAPI): ] ) + async def get_contacts(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request, expect_logged_in=True) + if err is not None: + return err + return web.json_response(data=await user.sync_contacts()) + + async def start_dm(self, request: web.Request) -> web.Response: + data, user, err = await self.get_user_request_info(request, expect_logged_in=True) + if err is not None: + return err + try: + identifier: str | int = request.match_info["identifier"] + if isinstance(identifier, str) and identifier.isdecimal(): + identifier = int(identifier) + target = await user.client.get_entity(identifier) + except ValueError: + return web.json_response( + { + "error": "Invalid user identifier or user not found.", + "errcode": "M_NOT_FOUND", + }, + status=404, + ) + + if not target: + return web.json_response( + { + "error": "User not found.", + "errcode": "M_NOT_FOUND", + }, + status=404, + ) + elif not isinstance(target, TLUser): + return web.json_response( + { + "error": "Identifier is not a user.", + }, + status=400, + ) + portal = await Portal.get_by_entity(target, tg_receiver=user.tgid) + puppet = await portal.get_dm_puppet() + if portal.mxid: + just_created = False + else: + await portal.create_matrix_room(user, target, [user.mxid]) + just_created = True + return web.json_response( + { + "room_id": portal.mxid, + "just_created": just_created, + "id": portal.tgid, + "contact_info": puppet.contact_info, + }, + status=201 if just_created else 200, + ) + async def send_bot_token(self, request: web.Request) -> web.Response: data, user, err = await self.get_user_request_info(request) if err is not None: @@ -574,7 +632,7 @@ class ProvisioningAPI(AuthAPI): data = None if want_data and (request.method == "POST" or request.method == "PUT"): data = await self.get_data(request) - if not data: + if data is None: return ( None, None,