From f5a1538609bdd504d2fe7ae335645cf6913e750e Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 9 Mar 2026 20:56:39 +0100 Subject: [PATCH 1/2] Add embed Discord link mentions --- .../src/five08/discord_bot/cogs/crm.py | 101 +++++++++++++----- tests/unit/test_crm.py | 43 ++++++++ 2 files changed, 119 insertions(+), 25 deletions(-) diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/crm.py b/apps/discord_bot/src/five08/discord_bot/cogs/crm.py index 40a8c73..49a0cf6 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/crm.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/crm.py @@ -1709,6 +1709,75 @@ def _format_applied_updates_value(cls, applied_lines: list[str]) -> str: joined = "\n".join(kept) return cls._truncate_embed_field(joined, cls._APPLIED_FIELD_TOTAL_LIMIT) + @staticmethod + def _format_discord_link_value( + *, + link_discord: dict[str, str] | None = None, + link_member: discord.Member | None = None, + ) -> str | None: + user_id = "" + username = "" + + if link_member is not None: + user_id = str(link_member.id) + username = str(link_member).strip() + elif link_discord: + user_id = str(link_discord.get("user_id") or "").strip() + username = str(link_discord.get("username") or "").strip() + + if not user_id.isdigit(): + return None + + mention = f"<@{user_id}>" + return f"{mention} ({username})" if username else mention + + def _build_apply_success_embed( + self, + *, + updated_fields: list[str], + updated_values: dict[str, Any], + link_discord_applied: bool | None, + ) -> discord.Embed: + embed = discord.Embed( + title="✅ CRM Updated", + description=f"Applied updates for **{self.contact_name}**.", + color=0x00FF00, + ) + display_fields = self._collapse_updated_fields(updated_fields) + labeled_fields = [self._field_label(field) for field in display_fields] + updated_fields_value = self._format_updated_fields_value(labeled_fields) + embed.add_field( + name="Updated Fields", + value=updated_fields_value, + inline=False, + ) + applied_lines = self._build_applied_updates_lines( + updated_fields=updated_fields, + updated_values=updated_values, + ) + if applied_lines: + applied_updates_value = self._format_applied_updates_value(applied_lines) + embed.add_field( + name="Applied Updates", + value=applied_updates_value, + inline=False, + ) + + if link_discord_applied: + discord_link_value = self._format_discord_link_value( + link_discord=self.link_discord + ) + if discord_link_value: + embed.add_field( + name="Discord Link", + value=f"Linked contact to {discord_link_value}", + inline=False, + ) + + profile_url = f"{self.crm_cog.base_url}/#Contact/view/{self.contact_id}" + embed.add_field(name="🔗 CRM Profile", value=f"[View in CRM]({profile_url})") + return embed + @classmethod def _collapse_updated_fields(cls, updated_fields: list[str]) -> list[str]: """Collapse skill fields into a single logical skills entry.""" @@ -2027,32 +2096,11 @@ def _audit_apply_event(result: str, metadata: dict[str, Any]) -> None: ) return - embed = discord.Embed( - title="✅ CRM Updated", - description=f"Applied updates for **{self.contact_name}**.", - color=0x00FF00, - ) - display_fields = self._collapse_updated_fields(updated_fields) - labeled_fields = [self._field_label(field) for field in display_fields] - updated_fields_value = self._format_updated_fields_value(labeled_fields) - embed.add_field( - name="Updated Fields", - value=updated_fields_value, - inline=False, - ) - applied_lines = self._build_applied_updates_lines( + embed = self._build_apply_success_embed( updated_fields=updated_fields, updated_values=updated_values, + link_discord_applied=link_discord_applied, ) - if applied_lines: - applied_updates_value = self._format_applied_updates_value(applied_lines) - embed.add_field( - name="Applied Updates", - value=applied_updates_value, - inline=False, - ) - profile_url = f"{self.crm_cog.base_url}/#Contact/view/{self.contact_id}" - embed.add_field(name="🔗 CRM Profile", value=f"[View in CRM]({profile_url})") _audit_apply_event( "success", { @@ -3194,11 +3242,14 @@ def format_skill_delta(current: Any, proposed: Any) -> str: inline=True, ) - if link_member: + discord_link_value = ResumeUpdateConfirmationView._format_discord_link_value( + link_member=link_member + ) + if discord_link_value: embed.add_field( name="Discord Link", value=truncate_field_value( - f"Will link contact to {link_member.mention}" + f"Will link contact to {discord_link_value}" ), inline=False, ) diff --git a/tests/unit/test_crm.py b/tests/unit/test_crm.py index d948f34..b00cb7a 100644 --- a/tests/unit/test_crm.py +++ b/tests/unit/test_crm.py @@ -251,6 +251,31 @@ async def test_resume_apply_confirmation_maps_skill_attrs_only_to_skills( assert collapsed == ["skills", "phoneNumber"] + @pytest.mark.asyncio + async def test_resume_apply_success_embed_includes_discord_link_mention( + self, crm_cog + ): + view = ResumeUpdateConfirmationView( + crm_cog=crm_cog, + requester_id=123, + contact_id="contact-1", + contact_name="Test User", + proposed_updates={}, + link_discord={"user_id": "123456789", "username": "test-user"}, + ) + + embed = view._build_apply_success_embed( + updated_fields=["skills"], + updated_values={"skills": ["python"]}, + link_discord_applied=True, + ) + + discord_link_field = next( + field for field in embed.fields if field.name == "Discord Link" + ) + assert "<@123456789>" in discord_link_field.value + assert "test-user" in discord_link_field.value + @pytest.mark.asyncio async def test_resume_apply_confirmation_groups_location_fields(self, crm_cog): """Applied updates should render location fields as one combined line.""" @@ -1049,6 +1074,24 @@ def test_resume_preview_embed_includes_debug_field(self, crm_cog): assert "current role" in evidence_field.value assert "developer profile" in evidence_field.value + def test_resume_preview_embed_includes_discord_link_mention(self, crm_cog): + link_member = Mock() + link_member.id = 123456789 + link_member.__str__ = Mock(return_value="resume-user") + + embed, _ = crm_cog._build_resume_preview_embed( + contact_id="contact-1", + contact_name="Test User", + result={"proposed_changes": []}, + link_member=link_member, + ) + + discord_link_field = next( + field for field in embed.fields if field.name == "Discord Link" + ) + assert "<@123456789>" in discord_link_field.value + assert "resume-user" in discord_link_field.value + def test_build_resume_extract_debug_file_serializes_raw_payload(self, crm_cog): """The debug attachment should include raw and normalized extraction payloads.""" debug_file = crm_cog._build_resume_extract_debug_file( From 6fe7db658c9ce24e476fb7c4571c2eb505c300d7 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Mon, 9 Mar 2026 22:11:49 +0100 Subject: [PATCH 2/2] Escape Discord link usernames --- apps/discord_bot/src/five08/discord_bot/cogs/crm.py | 5 ++++- tests/unit/test_crm.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/discord_bot/src/five08/discord_bot/cogs/crm.py b/apps/discord_bot/src/five08/discord_bot/cogs/crm.py index 49a0cf6..857c7a0 100644 --- a/apps/discord_bot/src/five08/discord_bot/cogs/crm.py +++ b/apps/discord_bot/src/five08/discord_bot/cogs/crm.py @@ -1729,7 +1729,10 @@ def _format_discord_link_value( return None mention = f"<@{user_id}>" - return f"{mention} ({username})" if username else mention + safe_username = discord.utils.escape_markdown( + discord.utils.escape_mentions(username) + ) + return f"{mention} ({safe_username})" if username else mention def _build_apply_success_embed( self, diff --git a/tests/unit/test_crm.py b/tests/unit/test_crm.py index b00cb7a..61edff7 100644 --- a/tests/unit/test_crm.py +++ b/tests/unit/test_crm.py @@ -276,6 +276,18 @@ async def test_resume_apply_success_embed_includes_discord_link_mention( assert "<@123456789>" in discord_link_field.value assert "test-user" in discord_link_field.value + def test_format_discord_link_value_escapes_username_mentions_and_markdown(self): + username = "@everyone **ops**" + expected_username = discord.utils.escape_markdown( + discord.utils.escape_mentions(username) + ) + + value = ResumeUpdateConfirmationView._format_discord_link_value( + link_discord={"user_id": "123456789", "username": username} + ) + + assert value == f"<@123456789> ({expected_username})" + @pytest.mark.asyncio async def test_resume_apply_confirmation_groups_location_fields(self, crm_cog): """Applied updates should render location fields as one combined line."""