1414from app .notifier import NotificationPayload
1515from app .notifier import feishu_sender as feishu_mod
1616from app .notifier import telegram_sender as telegram_mod
17+ from app .notifier .base import NotifierActionError , NotifierActionResult
1718from app .storage .db import get_user_settings , list_audit_logs , upsert_user_settings
1819
1920router = APIRouter (prefix = "/api/settings" , tags = ["settings" ])
@@ -34,17 +35,17 @@ def _test_message_error(
3435 http_status : int | None = None ,
3536 error_code : Any | None = None ,
3637 description : str | None = None ,
37- ) -> dict [ str , Any ] :
38+ ) -> NotifierActionError :
3839 # Keep a stable, cross-channel error shape for test_message endpoints.
3940 msg = str (message or "" ).strip ()
4041 desc = str (description or msg ).strip ()
41- return {
42- " category" : str (category or "" ).strip () or "provider_error" ,
43- " message" : msg or desc or "unknown" ,
44- " http_status" : int (http_status ) if http_status is not None else None ,
45- " error_code" : error_code ,
46- " description" : desc or None ,
47- }
42+ return NotifierActionError (
43+ category = str (category or "" ).strip () or "provider_error" ,
44+ message = msg or desc or "unknown" ,
45+ http_status = int (http_status ) if http_status is not None else None ,
46+ error_code = error_code ,
47+ description = desc or None ,
48+ )
4849
4950
5051def _now_iso_seconds () -> str :
@@ -405,7 +406,7 @@ async def put_telegram_credential(request: Request, payload: TelegramCredentialI
405406
406407
407408@router .post ("/notifications/telegram/test_message" )
408- async def post_telegram_test_message (request : Request ) -> dict :
409+ async def post_telegram_test_message (request : Request ) -> NotifierActionResult :
409410 user_id = get_holdings_user_id (request )
410411
411412 # Cooldown check
@@ -459,7 +460,7 @@ async def post_telegram_test_message(request: Request) -> dict:
459460 req_payload ["parse_mode" ] = parse_mode
460461
461462 max_attempts = retry_times + 1
462- last_error : dict [ str , Any ] | None = None
463+ last_error : NotifierActionError | None = None
463464
464465 for attempt in range (1 , max_attempts + 1 ):
465466 try :
@@ -486,32 +487,34 @@ async def post_telegram_test_message(request: Request) -> dict:
486487 )
487488 # Record cooldown
488489 _test_message_limiter .record_success (cooldown_key )
489- return {
490- "ok" : True ,
491- " sent" : True ,
492- " trace_id" : trace_id ,
493- " attempts" : attempt ,
494- " max_attempts" : max_attempts ,
495- " error" : None ,
496- }
490+ return NotifierActionResult (
491+ ok = True ,
492+ sent = True ,
493+ trace_id = trace_id ,
494+ attempts = attempt ,
495+ max_attempts = max_attempts ,
496+ error = None ,
497+ )
497498
498499 error_code = resp_json .get ("error_code" ) if isinstance (resp_json , dict ) else None
499500 description = (
500501 str (resp_json .get ("description" ) or resp_json .get ("message" ) or "" ).strip ()
501502 if isinstance (resp_json , dict )
502503 else ""
503504 )
504- category = "provider_error "
505+ category = "unknown "
505506 try :
506507 code_int = int (error_code )
507508 except Exception :
508509 code_int = - 1
509510 if code_int == 401 :
510- category = "unauthorized "
511+ category = "auth_failed "
511512 elif code_int == 403 :
512513 category = "forbidden"
513- elif code_int == 400 :
514- category = "bad_request"
514+ elif code_int == 400 and "chat not found" in description .lower ():
515+ category = "chat_not_found"
516+ elif code_int == 429 :
517+ category = "rate_limited"
515518 last_error = _test_message_error (
516519 category ,
517520 description or category ,
@@ -531,28 +534,28 @@ async def post_telegram_test_message(request: Request) -> dict:
531534 trace_id = trace_id ,
532535 ok = False ,
533536 sent = False ,
534- error_category = str (( last_error or {}). get ( " category" ) or "unknown" ),
537+ error_category = str (last_error . category if last_error else "unknown" ),
535538 )
536539 _persist_last_test_history (
537540 user_id = user_id ,
538541 channel = "telegram" ,
539542 trace_id = trace_id ,
540543 ok = False ,
541544 sent = False ,
542- error_category = str ((last_error or {}).get ("category" ) or "unknown" ),
545+ error_category = str (last_error .category if last_error else "unknown" ),
546+ )
547+ return NotifierActionResult (
548+ ok = False ,
549+ sent = False ,
550+ trace_id = trace_id ,
551+ attempts = max_attempts ,
552+ max_attempts = max_attempts ,
553+ error = last_error or _test_message_error ("provider_error" , "unknown" ),
543554 )
544- return {
545- "ok" : False ,
546- "sent" : False ,
547- "trace_id" : trace_id ,
548- "attempts" : max_attempts ,
549- "max_attempts" : max_attempts ,
550- "error" : last_error or _test_message_error ("provider_error" , "unknown" ),
551- }
552555
553556
554557@router .post ("/notifications/feishu/test_message" )
555- async def post_feishu_test_message (request : Request ) -> dict :
558+ async def post_feishu_test_message (request : Request ) -> NotifierActionResult :
556559 user_id = get_holdings_user_id (request )
557560
558561 # Cooldown check
@@ -592,7 +595,7 @@ async def post_feishu_test_message(request: Request) -> dict:
592595 retry_times = feishu_mod .FeishuSender ._coerce_retry_times (section .get ("retry_times" , feishu_mod .DEFAULT_RETRY_TIMES ))
593596 timeout_seconds = feishu_mod .FeishuSender ._coerce_timeout (section .get ("timeout_seconds" , feishu_mod .DEFAULT_TIMEOUT_SECONDS ))
594597 max_attempts = retry_times + 1
595- last_error : dict [ str , Any ] | None = None
598+ last_error : NotifierActionError | None = None
596599
597600 for attempt in range (1 , max_attempts + 1 ):
598601 try :
@@ -622,14 +625,14 @@ async def post_feishu_test_message(request: Request) -> dict:
622625 )
623626 # Record cooldown on success
624627 _test_message_limiter .record_success (cooldown_key )
625- return {
626- "ok" : True ,
627- " sent" : True ,
628- " trace_id" : trace_id ,
629- " attempts" : attempt ,
630- " max_attempts" : max_attempts ,
631- " error" : None ,
632- }
628+ return NotifierActionResult (
629+ ok = True ,
630+ sent = True ,
631+ trace_id = trace_id ,
632+ attempts = attempt ,
633+ max_attempts = max_attempts ,
634+ error = None ,
635+ )
633636
634637 provider_message = str (resp_json .get ("StatusMessage" ) or resp_json .get ("msg" ) or "" ).strip () if isinstance (resp_json , dict ) else ""
635638 category = "provider_error"
@@ -659,24 +662,24 @@ async def post_feishu_test_message(request: Request) -> dict:
659662 trace_id = trace_id ,
660663 ok = False ,
661664 sent = False ,
662- error_category = str (( last_error or {}). get ( " category" ) or "unknown" ),
665+ error_category = str (last_error . category if last_error else "unknown" ),
663666 )
664667 _persist_last_test_history (
665668 user_id = user_id ,
666669 channel = "feishu" ,
667670 trace_id = trace_id ,
668671 ok = False ,
669672 sent = False ,
670- error_category = str ((last_error or {}).get ("category" ) or "unknown" ),
673+ error_category = str (last_error .category if last_error else "unknown" ),
674+ )
675+ return NotifierActionResult (
676+ ok = False ,
677+ sent = False ,
678+ trace_id = trace_id ,
679+ attempts = max_attempts ,
680+ max_attempts = max_attempts ,
681+ error = last_error or _test_message_error ("provider_error" , "unknown" ),
671682 )
672- return {
673- "ok" : False ,
674- "sent" : False ,
675- "trace_id" : trace_id ,
676- "attempts" : max_attempts ,
677- "max_attempts" : max_attempts ,
678- "error" : last_error or _test_message_error ("provider_error" , "unknown" ),
679- }
680683
681684
682685@router .get ("/notifications/status" )
0 commit comments