+56
backend/src/backend/_internal/notifications.py
+56
backend/src/backend/_internal/notifications.py
···
68
68
self.dm_client = None
69
69
self.recipient_did = None
70
70
71
+
async def send_image_flag_notification(
72
+
self,
73
+
image_id: str,
74
+
severity: str,
75
+
categories: list[str],
76
+
context: str,
77
+
):
78
+
"""send notification about a flagged image.
79
+
80
+
args:
81
+
image_id: R2 storage ID of the flagged image
82
+
severity: severity level (low, medium, high)
83
+
categories: list of violated policy categories
84
+
context: where the image was uploaded (e.g., "track cover", "album cover")
85
+
"""
86
+
if not self.dm_client or not self.recipient_did:
87
+
logger.warning(
88
+
"dm client not authenticated or recipient not set, skipping notification"
89
+
)
90
+
return
91
+
92
+
try:
93
+
dm = self.dm_client.chat.bsky.convo
94
+
95
+
convo_response = await dm.get_convo_for_members(
96
+
models.ChatBskyConvoGetConvoForMembers.Params(
97
+
members=[self.recipient_did]
98
+
)
99
+
)
100
+
101
+
if not convo_response.convo or not convo_response.convo.id:
102
+
raise ValueError("failed to get conversation ID")
103
+
104
+
convo_id = convo_response.convo.id
105
+
106
+
categories_str = ", ".join(categories) if categories else "unspecified"
107
+
message_text = (
108
+
f"🚨 image flagged on {settings.app.name}\n\n"
109
+
f"context: {context}\n"
110
+
f"image_id: {image_id}\n"
111
+
f"severity: {severity}\n"
112
+
f"categories: {categories_str}"
113
+
)
114
+
115
+
await dm.send_message(
116
+
models.ChatBskyConvoSendMessage.Data(
117
+
convo_id=convo_id,
118
+
message=models.ChatBskyConvoDefs.MessageInput(text=message_text),
119
+
)
120
+
)
121
+
122
+
logger.info(f"sent image flag notification for {image_id}")
123
+
124
+
except Exception:
125
+
logger.exception(f"error sending image flag notification for {image_id}")
126
+
71
127
async def send_track_notification(self, track: Track):
72
128
"""send notification about a new track."""
73
129
if not self.dm_client or not self.recipient_did:
+11
-1
backend/src/backend/api/albums.py
+11
-1
backend/src/backend/api/albums.py
···
26
26
from backend._internal import require_artist_profile
27
27
from backend._internal.auth import get_session
28
28
from backend._internal.moderation_client import get_moderation_client
29
+
from backend._internal.notifications import notification_service
29
30
from backend.config import settings
30
31
from backend.models import Album, Artist, Track, TrackLike, get_db
31
32
from backend.schemas import TrackResponse
···
453
454
".png": "image/png",
454
455
".webp": "image/webp",
455
456
}.get(ext, "image/png")
456
-
await client.scan_image(bytes(image_data), image_id, content_type)
457
+
result = await client.scan_image(
458
+
bytes(image_data), image_id, content_type
459
+
)
457
460
# if image is flagged, it's automatically added to sensitive_images
458
461
# by the moderation service. the image is still saved and returned.
462
+
if not result.is_safe:
463
+
await notification_service.send_image_flag_notification(
464
+
image_id=image_id,
465
+
severity=result.severity,
466
+
categories=result.violated_categories,
467
+
context="album cover",
468
+
)
459
469
except Exception as e:
460
470
# log but don't block upload - moderation is best-effort
461
471
logger.warning("image moderation failed for %s: %s", image_id, e)
+9
-1
backend/src/backend/api/tracks/metadata_service.py
+9
-1
backend/src/backend/api/tracks/metadata_service.py
···
13
13
from backend._internal.atproto.handles import resolve_handle
14
14
from backend._internal.image import ImageFormat
15
15
from backend._internal.moderation_client import get_moderation_client
16
+
from backend._internal.notifications import notification_service
16
17
from backend.config import settings
17
18
from backend.models import Track
18
19
from backend.storage import storage
···
139
140
try:
140
141
client = get_moderation_client()
141
142
content_type = image_format.mime_type if image_format else "image/png"
142
-
await client.scan_image(image_data, image_id, content_type)
143
+
result = await client.scan_image(image_data, image_id, content_type)
143
144
# note: if image is flagged, it's automatically added to sensitive_images
144
145
# by the moderation service. the image is still saved and returned -
145
146
# sensitive images are just blurred in the UI, not rejected.
147
+
if not result.is_safe:
148
+
await notification_service.send_image_flag_notification(
149
+
image_id=image_id,
150
+
severity=result.severity,
151
+
categories=result.violated_categories,
152
+
context="track cover",
153
+
)
146
154
except Exception as e:
147
155
# log but don't block upload - moderation is best-effort
148
156
logger.warning("image moderation failed for %s: %s", image_id, e)