diff --git a/bot/chorus_bot/cogs/manual_share.py b/bot/chorus_bot/cogs/manual_share.py index f5e9c90..d047e19 100644 --- a/bot/chorus_bot/cogs/manual_share.py +++ b/bot/chorus_bot/cogs/manual_share.py @@ -22,7 +22,13 @@ class ManualShareCog(commands.Cog): if modality is None: return + attachment_urls = [attachment.url for attachment in message.attachments] + raw_content = message.content + if attachment_urls: + attachments_block = "Attachments:\n" + "\n".join(attachment_urls) + raw_content = f"{raw_content}\n\n{attachments_block}" if raw_content else attachments_block + display_content = message.content or "(attachment only - see original message)" snippet = display_content if len(display_content) <= 120 else display_content[:117] + "..." @@ -33,6 +39,7 @@ class ManualShareCog(commands.Cog): raw_content=raw_content, captured_at=message.created_at, direction=direction_for_modality(modality), + image_url=attachment_urls[0] if attachment_urls else None, ) inbox = self.bot.get_channel(self.config.inbox_channel_id) diff --git a/bot/chorus_bot/ping.py b/bot/chorus_bot/ping.py index 8a1dd4f..a938af7 100644 --- a/bot/chorus_bot/ping.py +++ b/bot/chorus_bot/ping.py @@ -36,7 +36,7 @@ def _safe_field_value(value: str) -> str: def build_ping_embed( *, modality: str, sender_id: str | None, snippet: str, raw_content: str, - captured_at: datetime, direction: str = "inbound", + captured_at: datetime, direction: str = "inbound", image_url: str | None = None, ) -> discord.Embed: label = MODALITY_LABELS.get(modality, modality) embed = discord.Embed( @@ -52,6 +52,11 @@ def build_ping_embed( embed.add_field( name="raw_content", value=_safe_field_value(raw_content), inline=False, ) + # Discord CDN attachment URLs carry a signed expiry (ex/is/hm query params), + # typically valid ~24h. Fine for same-day triage; a stale link just stops + # rendering the preview without breaking anything else in the embed. + if image_url: + embed.set_image(url=image_url) return embed diff --git a/bot/tests/test_manual_share.py b/bot/tests/test_manual_share.py index 3ca7747..da1a435 100644 --- a/bot/tests/test_manual_share.py +++ b/bot/tests/test_manual_share.py @@ -10,12 +10,20 @@ class _FakeConfig: triage_role_id = 222 -def _make_message(*, channel_name, content="hello", author_bot=False, channel_id=999): +def _make_attachment(url): + attachment = MagicMock(spec=discord.Attachment) + attachment.url = url + return attachment + + +def _make_message(*, channel_name, content="hello", author_bot=False, channel_id=999, + attachments=None): message = MagicMock(spec=discord.Message) message.author.bot = author_bot message.channel.id = channel_id message.channel.name = channel_name message.content = content + message.attachments = attachments or [] message.created_at = __import__("datetime").datetime(2026, 7, 13, 7, 0, 0) return message @@ -49,3 +57,42 @@ async def test_existing_modality_channel_still_forwards_with_inbound_direction() sent_embed = inbox.send.call_args.kwargs["embed"] parsed = parse_ping_embed(sent_embed) assert parsed["direction"] == "inbound" + + +@pytest.mark.asyncio +async def test_message_with_attachment_includes_image_and_url_in_raw_content(): + bot = MagicMock() + inbox = AsyncMock() + bot.get_channel.return_value = inbox + cog = ManualShareCog(bot, _FakeConfig()) + + attachment = _make_attachment("https://cdn.discordapp.com/attachments/1/2/screenshot.png") + message = _make_message( + channel_name="leads-found", content="Found on Nextdoor", attachments=[attachment], + ) + await cog.on_message(message) + + sent_embed = inbox.send.call_args.kwargs["embed"] + assert sent_embed.image.url == "https://cdn.discordapp.com/attachments/1/2/screenshot.png" + + parsed = parse_ping_embed(sent_embed) + assert "https://cdn.discordapp.com/attachments/1/2/screenshot.png" in parsed["raw_content"] + assert "Found on Nextdoor" in parsed["raw_content"] + + +@pytest.mark.asyncio +async def test_attachment_only_message_still_includes_image(): + bot = MagicMock() + inbox = AsyncMock() + bot.get_channel.return_value = inbox + cog = ManualShareCog(bot, _FakeConfig()) + + attachment = _make_attachment("https://cdn.discordapp.com/attachments/1/2/screenshot.png") + message = _make_message(channel_name="leads-found", content="", attachments=[attachment]) + await cog.on_message(message) + + sent_embed = inbox.send.call_args.kwargs["embed"] + assert sent_embed.image.url == "https://cdn.discordapp.com/attachments/1/2/screenshot.png" + + parsed = parse_ping_embed(sent_embed) + assert "https://cdn.discordapp.com/attachments/1/2/screenshot.png" in parsed["raw_content"] diff --git a/bot/tests/test_ping.py b/bot/tests/test_ping.py index 20c0289..ca34072 100644 --- a/bot/tests/test_ping.py +++ b/bot/tests/test_ping.py @@ -124,3 +124,21 @@ def test_parse_ping_embed_defaults_direction_when_field_missing(): parsed = ping.parse_ping_embed(embed) assert parsed["direction"] == "inbound" + + +def test_build_ping_embed_sets_image_when_url_given(): + embed = ping.build_ping_embed( + modality="lead_found", sender_id=None, snippet="found post", + raw_content="Found on Nextdoor", + captured_at=datetime(2026, 7, 13, 9, 0, 0, tzinfo=timezone.utc), + image_url="https://cdn.discordapp.com/attachments/1/2/screenshot.png", + ) + assert embed.image.url == "https://cdn.discordapp.com/attachments/1/2/screenshot.png" + + +def test_build_ping_embed_no_image_by_default(): + embed = ping.build_ping_embed( + modality="bh_email", sender_id="jane@example.com", snippet="hi", + raw_content="hi", captured_at=datetime(2026, 7, 13, 9, 0, 0, tzinfo=timezone.utc), + ) + assert embed.image.url is None