Kaynağa Gözat

Improve and/or break name syncing

Tulir Asokan 4 yıl önce
ebeveyn
işleme
64bc5c36a5

+ 21 - 1
mausignald/errors.py

@@ -3,7 +3,7 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from typing import Any, Dict
+from typing import Any, Dict, Optional
 
 
 class RPCError(Exception):
@@ -46,3 +46,23 @@ def make_linking_error(data: Dict[str, Any]) -> LinkingError:
         1: LinkingTimeout,
         3: LinkingConflict,
     }.get(msg_number, LinkingError)(message, msg_number)
+
+
+class ResponseError(RPCError):
+    def __init__(self, data: Dict[str, Any], message_override: Optional[str] = None) -> None:
+        self.data = data
+        super().__init__(message_override or data["message"])
+
+
+class InvalidRequest(ResponseError):
+    def __init__(self, data: Dict[str, Any]) -> None:
+        super().__init__(data, ", ".join(data.get("validationResults", "")))
+
+
+response_error_types = {
+    "invalid_request": InvalidRequest,
+}
+
+
+def make_response_error(data: Dict[str, Any]) -> ResponseError:
+    return response_error_types.get(data["type"], ResponseError)(data)

+ 3 - 1
mausignald/rpc.py

@@ -11,7 +11,7 @@ import json
 
 from mautrix.util.logging import TraceLogger
 
-from .errors import NotConnected, UnexpectedError, UnexpectedResponse
+from .errors import NotConnected, UnexpectedError, UnexpectedResponse, make_response_error
 
 EventHandler = Callable[[Dict[str, Any]], Awaitable[None]]
 
@@ -112,6 +112,8 @@ class SignaldRPCClient:
                 waiter.set_exception(UnexpectedError(data["message"]))
             except KeyError:
                 waiter.set_exception(UnexpectedError("Unexpected error with no message"))
+        elif data and "error" in data:
+            waiter.set_exception(make_response_error(data["error"]))
         else:
             waiter.set_result((command, data))
 

+ 2 - 2
mausignald/signald.py

@@ -180,8 +180,8 @@ class SignaldClient(SignaldRPCClient):
 
     async def get_profile(self, username: str, address: Address) -> Optional[Profile]:
         try:
-            resp = await self.request("get_profile", "profile", username=username,
-                                      recipientAddress=address.serialize())
+            resp = await self.request("get_profile", "get_profile", account=username,
+                                      address=address.serialize(), version="v1")
         except UnexpectedResponse as e:
             if e.resp_type == "profile_not_available":
                 return None

+ 10 - 0
mausignald/types.py

@@ -87,14 +87,24 @@ class Contact(SerializableAttrs['Contact']):
     message_expiration_time: int = attr.ib(default=0, metadata={"json": "messageExpirationTime"})
 
 
+@dataclass
+class Capabilities(SerializableAttrs['Capabilities']):
+    gv2: bool = False
+    storage: bool = False
+    gv1_migration: bool = attr.ib(default=False, metadata={"json": "gv1-migration"})
+
+
 @dataclass
 class Profile(SerializableAttrs['Profile']):
     name: str = ""
+    profile_name: str = ""
     avatar: str = ""
     identity_key: str = ""
     unidentified_access: str = ""
     unrestricted_unidentified_access: bool = False
     address: Optional[Address] = None
+    expiration_time: int = 0
+    capabilities: Optional[Capabilities] = None
 
 
 @dataclass

+ 2 - 11
mautrix_signal/commands/signal.py

@@ -116,18 +116,9 @@ async def safety_number(evt: CommandEvent) -> None:
 
 
 @command_handler(needs_admin=False, needs_auth=True, help_section=SECTION_SIGNAL,
-                 help_text="Sync data from Signal", help_args="[--profiles]")
+                 help_text="Sync data from Signal")
 async def sync(evt: CommandEvent) -> None:
-    fetch_profiles = False
-    for arg in evt.args:
-        arg = arg.lower()
-        if arg == "--profiles":
-            fetch_profiles = True
-        elif arg == "--help":
-            await evt.reply("**Usage:** `$cmdprefix+sp sync [--profiles]`\n\n"
-                            "* `--profiles`: Fetch Signal profile names for users")
-            return
-    await evt.sender.sync(fetch_profiles=fetch_profiles)
+    await evt.sender.sync()
     await evt.reply("Sync complete")
 
 

+ 4 - 1
mautrix_signal/config.py

@@ -58,7 +58,10 @@ class Config(BaseBridgeConfig):
 
         copy("bridge.username_template")
         copy("bridge.displayname_template")
-        copy("bridge.allow_contact_list_name_updates")
+        if self["bridge.allow_contact_list_name_updates"]:
+            base["bridge.contact_list_names"] = "allow"
+        else:
+            copy("bridge.contact_list_names")
         copy("bridge.displayname_preference")
 
         copy("bridge.autocreate_group_portal")

+ 5 - 2
mautrix_signal/example-config.yaml

@@ -82,8 +82,11 @@ bridge:
     # can also be used here directly.
     displayname_template: "{displayname} (Signal)"
     # Whether or not contact list displaynames should be used.
-    # Using this isn't recommended on multi-user instances.
-    allow_contact_list_name_updates: false
+    # Possible values: disallow, allow, prefer
+    #
+    # Multi-user instances are recommended to disallow contact list names, as otherwise there can
+    # be conflicts between names from different users' contact lists.
+    contact_list_names: disallow
     # Available variables: full_name, first_name, last_name, phone, uuid
     displayname_preference:
     - full_name

+ 7 - 7
mautrix_signal/portal.py

@@ -455,7 +455,7 @@ class Portal(DBPortal, BasePortal):
     # endregion
     # region Updating portal info
 
-    async def update_info(self, info: ChatInfo) -> None:
+    async def update_info(self, source: 'u.User', info: ChatInfo) -> None:
         if self.is_direct:
             if not isinstance(info, (Contact, Profile, Address)):
                 raise ValueError(f"Unexpected type for direct chat update_info: {type(info)}")
@@ -475,7 +475,7 @@ class Portal(DBPortal, BasePortal):
         else:
             raise ValueError(f"Unexpected type for group update_info: {type(info)}")
         changed = await self._update_avatar()
-        await self._update_participants(info.members)
+        await self._update_participants(source, info.members)
         if changed:
             await self.update_bridge_info()
             await self.update()
@@ -517,7 +517,7 @@ class Portal(DBPortal, BasePortal):
         self.avatar_hash = new_hash
         return True
 
-    async def _update_participants(self, participants: List[Address]) -> None:
+    async def _update_participants(self, source: 'u.User', participants: List[Address]) -> None:
         # TODO add support for pending_members and maybe requesting_members?
         if not self.mxid or not participants:
             return
@@ -525,7 +525,7 @@ class Portal(DBPortal, BasePortal):
         for address in participants:
             puppet = await p.Puppet.get_by_address(address)
             if not puppet.name:
-                await puppet._update_name(None)
+                await source.sync_contact(address)
             await puppet.intent_for(self).ensure_joined(self.mxid)
 
     # endregion
@@ -608,7 +608,7 @@ class Portal(DBPortal, BasePortal):
             if did_join and self.is_direct:
                 await source.update_direct_chats({self.main_intent.mxid: [self.mxid]})
 
-        await self.update_info(info)
+        await self.update_info(source, info)
 
         # TODO
         # up = DBUserPortal.get(source.fbid, self.fbid, self.fb_receiver)
@@ -624,7 +624,7 @@ class Portal(DBPortal, BasePortal):
         if self.mxid:
             await self._update_matrix_room(source, info)
             return self.mxid
-        await self.update_info(info)
+        await self.update_info(source, info)
         self.log.debug("Creating Matrix room")
         name: Optional[str] = None
         initial_state = [{
@@ -684,7 +684,7 @@ class Portal(DBPortal, BasePortal):
         self.log.debug(f"Matrix room created: {self.mxid}")
         self.by_mxid[self.mxid] = self
         if not self.is_direct:
-            await self._update_participants(info.members)
+            await self._update_participants(source, info.members)
         else:
             puppet = await p.Puppet.get_by_custom_mxid(source.mxid)
             if puppet:

+ 7 - 3
mautrix_signal/puppet.py

@@ -154,10 +154,14 @@ class Puppet(DBPuppet, BasePuppet):
             address = info.address if isinstance(info, Contact) else info
             if address.uuid and not self.uuid:
                 await self.handle_uuid_receive(address.uuid)
-            if not self.config["bridge.allow_contact_list_name_updates"] and self.name is not None:
-                return
 
-        name = info.name if isinstance(info, (Contact, Profile)) else None
+        contact_names = self.config["bridge.contact_list_names"]
+        if isinstance(info, Profile) and contact_names != "prefer" and info.profile_name:
+            name = info.profile_name
+        elif isinstance(info, (Contact, Profile)) and contact_names != "disallow":
+            name = info.name
+        else:
+            name = None
 
         async with self._update_info_lock:
             update = False

+ 1 - 1
mautrix_signal/signal.py

@@ -98,7 +98,7 @@ class SignalHandler(SignaldClient):
         if msg.body or msg.attachments or msg.sticker:
             await portal.handle_signal_message(user, sender, msg)
         if msg.group and msg.group.type == "UPDATE":
-            await portal.update_info(msg.group)
+            await portal.update_info(user, msg.group)
         if msg.remote_delete:
             await portal.handle_signal_delete(sender, msg.remote_delete.target_sent_timestamp)
 

+ 12 - 17
mautrix_signal/user.py

@@ -13,7 +13,7 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from typing import Dict, Optional, AsyncGenerator, TYPE_CHECKING, cast
+from typing import Union, Dict, Optional, AsyncGenerator, TYPE_CHECKING, cast
 from collections import defaultdict
 from uuid import UUID
 import asyncio
@@ -122,13 +122,13 @@ class User(DBUser, BaseUser):
             self.log.info(f"Automatically enabling custom puppet")
             await puppet.switch_mxid(access_token="auto", mxid=self.mxid)
 
-    async def sync(self, fetch_profiles: bool = False) -> None:
+    async def sync(self) -> None:
         try:
             await self._sync_puppet()
         except Exception:
             self.log.exception("Error while syncing own puppet")
         try:
-            await self._sync_contacts(fetch_profiles=fetch_profiles)
+            await self._sync_contacts()
         except Exception:
             self.log.exception("Error while syncing contacts")
         try:
@@ -136,19 +136,15 @@ class User(DBUser, BaseUser):
         except Exception:
             self.log.exception("Error while syncing groups")
 
-    async def _sync_contact(self, contact: Contact, create_portals: bool,
-                            fetch_profile: bool = False) -> None:
+    async def sync_contact(self, contact: Union[Contact, Address], create_portals: bool = False
+                           ) -> None:
         self.log.trace("Syncing contact %s", contact)
-        puppet = await pu.Puppet.get_by_address(contact.address)
-        if not puppet.name or (fetch_profile and contact.profile_key):
-            profile = await self.bridge.signal.get_profile(self.username, contact.address)
-            if profile and profile.name:
-                self.log.trace("Got profile for %s: %s", contact.address, profile)
-            else:
-                profile = None
+        address = contact.address if isinstance(contact, Contact) else contact
+        puppet = await pu.Puppet.get_by_address(address)
+        profile = await self.bridge.signal.get_profile(self.username, address)
+        if profile and profile.name:
+            self.log.trace("Got profile for %s: %s", address, profile)
         else:
-            # get_profile probably does a request to the servers, so let's not do that unless
-            # necessary, but maybe we could listen for updates?
             profile = None
         await puppet.update_info(profile or contact)
         if create_portals:
@@ -171,12 +167,11 @@ class User(DBUser, BaseUser):
         elif portal.mxid:
             await portal.update_matrix_room(self, group)
 
-    async def _sync_contacts(self, fetch_profiles: bool = False) -> None:
+    async def _sync_contacts(self) -> None:
         create_contact_portal = self.config["bridge.autocreate_contact_portal"]
         for contact in await self.bridge.signal.list_contacts(self.username):
             try:
-                await self._sync_contact(contact, create_contact_portal,
-                                         fetch_profile=fetch_profiles)
+                await self.sync_contact(contact, create_contact_portal)
             except Exception:
                 self.log.exception(f"Failed to sync contact {contact.address}")