diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index 3b42822d..d28003f0 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -179,6 +179,21 @@ class MatrixClient(nio.AsyncClient): if host in ("127.0.0.1", "localhost", "::1"): proxy = None + # self is passed in explicitly, so that it will also be passed to + # handlers in the user config + self.cmd_handler_map = { + "me ": MatrixClient.handle_cmd_emote, + } + + try: + self.cmd_handler_map = { + **self.cmd_handler_map, + **backend.settings.Commands.get_cmd_handler_map(), + } + except (AttributeError, KeyError): + # make sure we don't break older configs + pass + super().__init__( homeserver = homeserver, user = user, @@ -618,22 +633,40 @@ class MatrixClient(nio.AsyncClient): display_name_mentions: Optional[Dict[str, str]] = None, # {id: name} reply_to_event_id: Optional[str] = None, ) -> None: - """Send a markdown `m.text` or `m.notice` (with `/me`) message .""" + if text.startswith("//") or text.startswith(r"\/"): + await self._send_text(room_id, text[1:], display_name_mentions, reply_to_event_id) + elif text.startswith("/"): + for k,v in self.cmd_handler_map.items(): + if text.startswith("/"+k): + await v(self, room_id, text[len("/"+k):], display_name_mentions, reply_to_event_id) + break + else: + await self.send_fake_notice( + room_id, + r"That command was not recognised. To send a message starting with /, use //", + ) + else: + await self._send_text(room_id, text, display_name_mentions, reply_to_event_id) + + + async def _send_text( + self, + room_id: str, + text: str, + display_name_mentions: Optional[Dict[str, str]] = None, # {id: name} + reply_to_event_id: Optional[str] = None, + emote: bool = False, + ) -> None: + """Send a markdown `m.text` or `m.emote` message .""" from_md = partial( HTML.from_markdown, display_name_mentions=display_name_mentions, ) - escape = False - if text.startswith("//") or text.startswith(r"\/"): - escape = True - text = text[1:] - content: Dict[str, Any] - if text.startswith("/me ") and not escape: + if emote: event_type = nio.RoomMessageEmote - text = text[len("/me "):] content = {"body": text, "msgtype": "m.emote"} to_html = from_md(text, inline=True, outgoing=True) echo_body = from_md(text, inline=True) @@ -699,6 +732,22 @@ class MatrixClient(nio.AsyncClient): await self._send_message(room_id, content, tx_id) + async def handle_cmd_emote( + self, + room_id: str, + text: str, + display_name_mentions: Optional[Dict[str, str]] = None, # {id: name} + reply_to_event_id: Optional[str] = None, + ): + await self._send_text( + room_id, + text, + display_name_mentions, + reply_to_event_id, + emote=True + ) + + async def toggle_pause_transfer( self, room_id: str, uuid: Union[str, UUID], ) -> None: @@ -1023,6 +1072,7 @@ class MatrixClient(nio.AsyncClient): room_id: str, transaction_id: UUID, event_type: Type[nio.Event], + fake_event: bool = False, **event_fields, ) -> None: """Register a local model `Event` while waiting for the server. @@ -1056,7 +1106,7 @@ class MatrixClient(nio.AsyncClient): sender_id = self.user_id, sender_name = our_info.display_name, sender_avatar = our_info.avatar_url, - is_local_echo = True, + is_local_echo = not fake_event, links = Event.parse_links(content), **event_fields, ) @@ -1069,6 +1119,16 @@ class MatrixClient(nio.AsyncClient): await self.set_room_last_event(room_id, event) + async def send_fake_notice(self, room_id, msg): + await self._local_echo( + room_id, + uuid4(), + nio.RoomMessageNotice, + content = msg, + fake_event = True, + ) + + async def _send_message( self, room_id: str, content: dict, transaction_id: UUID, ) -> None: diff --git a/src/config/settings.py b/src/config/settings.py index 23515279..a1b800c3 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -541,3 +541,36 @@ class Keys: # Sign out checked sessions if any, else sign out all sessions. sign_out = ["Alt+S", "Delete"] + +class Commands: + # If you are an advanced user, here you can define new /commands + # + # get_cmd_handler_map should return a dictionary of the form + # { + # "command": command_handling_function, + # "another_command": ... + # } + # + # each command_handling_function should have the signature: + # + # async def command_handling_function( + # matrix_client: moment.backend.MatrixClient, + # room_id: str, + # text: str, # text after the end of /command + # display_name_mentions: Optional[Dict[str, str]] = None, # {id: name} + # reply_to_event_id: Optional[str] = None, + # ) -> None: + + # Example: + + def get_cmd_handler_map(self): + return { + # "rot13 ": self.handle_cmd_rot13, + } + + # @staticmethod + # def handle_cmd_rot13(matrix_client, room_id, text, display_name_mentions, reply_to_event_id): + # import codecs + # matrix_client._send_text( + # room_id, codecs.encode(text, 'rot_13'), display_name_mentions, reply_to_event_id + # )