+42
-92
backend/src/backend/_internal/notifications.py
+42
-92
backend/src/backend/_internal/notifications.py
···
273
273
severity: str,
274
274
categories: list[str],
275
275
context: str,
276
-
):
276
+
) -> NotificationResult | None:
277
277
"""send notification about a flagged image.
278
278
279
279
args:
···
282
282
categories: list of violated policy categories
283
283
context: where the image was uploaded (e.g., "track cover", "album cover")
284
284
"""
285
-
if not self.dm_client or not self.recipient_did:
286
-
logger.warning(
287
-
"dm client not authenticated or recipient not set, skipping notification"
288
-
)
289
-
return
290
-
291
-
try:
292
-
dm = self.dm_client.chat.bsky.convo
293
-
294
-
convo_response = await dm.get_convo_for_members(
295
-
models.ChatBskyConvoGetConvoForMembers.Params(
296
-
members=[self.recipient_did]
297
-
)
298
-
)
299
-
300
-
if not convo_response.convo or not convo_response.convo.id:
301
-
raise ValueError("failed to get conversation ID")
285
+
if not self.recipient_did:
286
+
logger.warning("recipient not set, skipping notification")
287
+
return None
302
288
303
-
convo_id = convo_response.convo.id
289
+
categories_str = ", ".join(categories) if categories else "unspecified"
290
+
message_text = (
291
+
f"🚨 image flagged on {settings.app.name}\n\n"
292
+
f"context: {context}\n"
293
+
f"image_id: {image_id}\n"
294
+
f"severity: {severity}\n"
295
+
f"categories: {categories_str}"
296
+
)
304
297
305
-
categories_str = ", ".join(categories) if categories else "unspecified"
306
-
message_text = (
307
-
f"🚨 image flagged on {settings.app.name}\n\n"
308
-
f"context: {context}\n"
309
-
f"image_id: {image_id}\n"
310
-
f"severity: {severity}\n"
311
-
f"categories: {categories_str}"
312
-
)
313
-
314
-
await dm.send_message(
315
-
models.ChatBskyConvoSendMessage.Data(
316
-
convo_id=convo_id,
317
-
message=models.ChatBskyConvoDefs.MessageInput(text=message_text),
318
-
)
319
-
)
320
-
298
+
result = await self._send_dm_to_did(self.recipient_did, message_text)
299
+
if result.success:
321
300
logger.info(f"sent image flag notification for {image_id}")
301
+
return result
322
302
323
-
except Exception:
324
-
logger.exception(f"error sending image flag notification for {image_id}")
325
-
326
-
async def send_track_notification(self, track: Track):
303
+
async def send_track_notification(self, track: Track) -> NotificationResult | None:
327
304
"""send notification about a new track."""
328
-
if not self.dm_client or not self.recipient_did:
329
-
logger.warning(
330
-
"dm client not authenticated or recipient not set, skipping notification"
331
-
)
332
-
return
333
-
334
-
try:
335
-
# create shortcut to convo methods
336
-
dm = self.dm_client.chat.bsky.convo
337
-
338
-
# get or create conversation with the target user
339
-
convo_response = await dm.get_convo_for_members(
340
-
models.ChatBskyConvoGetConvoForMembers.Params(
341
-
members=[self.recipient_did]
342
-
)
343
-
)
305
+
if not self.recipient_did:
306
+
logger.warning("recipient not set, skipping notification")
307
+
return None
344
308
345
-
if not convo_response.convo or not convo_response.convo.id:
346
-
raise ValueError("failed to get conversation ID")
309
+
artist_handle = track.artist.handle
347
310
348
-
convo_id = convo_response.convo.id
349
-
350
-
# format the message with rich information
351
-
artist_handle = track.artist.handle
311
+
# only include link if we have a non-localhost frontend URL
312
+
track_url = None
313
+
frontend_url = settings.frontend.url
314
+
if frontend_url and "localhost" not in frontend_url:
315
+
track_url = f"{frontend_url}/track/{track.id}"
352
316
353
-
# only include link if we have a non-localhost frontend URL
354
-
track_url = None
355
-
frontend_url = settings.frontend.url
356
-
if frontend_url and "localhost" not in frontend_url:
357
-
track_url = f"{frontend_url}/track/{track.id}"
358
-
359
-
if track_url:
360
-
message_text: str = (
361
-
f"🎵 new track on {settings.app.name}!\n\n"
362
-
f"'{track.title}' by @{artist_handle}\n\n"
363
-
f"listen: {track_url}\n"
364
-
f"uploaded: {track.created_at.strftime('%b %d at %H:%M UTC')}"
365
-
)
366
-
else:
367
-
# dev environment - no link
368
-
message_text: str = (
369
-
f"🎵 new track on {settings.app.name}!\n\n"
370
-
f"'{track.title}' by @{artist_handle}\n"
371
-
f"uploaded: {track.created_at.strftime('%b %d at %H:%M UTC')}"
372
-
)
373
-
374
-
# send the DM
375
-
await dm.send_message(
376
-
models.ChatBskyConvoSendMessage.Data(
377
-
convo_id=convo_id,
378
-
message=models.ChatBskyConvoDefs.MessageInput(text=message_text),
379
-
)
317
+
if track_url:
318
+
message_text = (
319
+
f"🎵 new track on {settings.app.name}!\n\n"
320
+
f"'{track.title}' by @{artist_handle}\n\n"
321
+
f"listen: {track_url}\n"
322
+
f"uploaded: {track.created_at.strftime('%b %d at %H:%M UTC')}"
323
+
)
324
+
else:
325
+
# dev environment - no link
326
+
message_text = (
327
+
f"🎵 new track on {settings.app.name}!\n\n"
328
+
f"'{track.title}' by @{artist_handle}\n"
329
+
f"uploaded: {track.created_at.strftime('%b %d at %H:%M UTC')}"
380
330
)
381
331
382
-
logger.info(f"sent notification for track {track.id} to {convo_id}")
383
-
384
-
except Exception:
385
-
logger.exception(f"error sending notification for track {track.id}")
332
+
result = await self._send_dm_to_did(self.recipient_did, message_text)
333
+
if result.success:
334
+
logger.info(f"sent notification for track {track.id}")
335
+
return result
386
336
387
337
async def shutdown(self):
388
338
"""cleanup resources."""