chore: clarify panel webhook subscription logs
This commit is contained in:
@@ -46,6 +46,13 @@ EVENT_MAP = {
|
|||||||
days_left=1,
|
days_left=1,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
ACTIONABLE_EVENTS = frozenset(
|
||||||
|
{
|
||||||
|
*EVENT_MAP.keys(),
|
||||||
|
"user.expired",
|
||||||
|
"user.expired_24_hours_ago",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PanelWebhookService:
|
class PanelWebhookService:
|
||||||
@@ -128,6 +135,15 @@ class PanelWebhookService:
|
|||||||
if not self.settings.SUBSCRIPTION_NOTIFICATIONS_ENABLED:
|
if not self.settings.SUBSCRIPTION_NOTIFICATIONS_ENABLED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if event_name not in ACTIONABLE_EVENTS:
|
||||||
|
logging.info(
|
||||||
|
"Panel webhook event %s ignored: event is not used for subscription "
|
||||||
|
"notifications; %s",
|
||||||
|
event_name,
|
||||||
|
self._payload_log_context(user_payload),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
async with self.async_session_factory() as session:
|
async with self.async_session_factory() as session:
|
||||||
db_user = await self._user_for_payload(session, user_payload)
|
db_user = await self._user_for_payload(session, user_payload)
|
||||||
sub = await self._subscription_for_payload(session, user_payload, db_user)
|
sub = await self._subscription_for_payload(session, user_payload, db_user)
|
||||||
@@ -144,7 +160,17 @@ class PanelWebhookService:
|
|||||||
)
|
)
|
||||||
if not sub:
|
if not sub:
|
||||||
if not telegram_id:
|
if not telegram_id:
|
||||||
logging.warning("Panel webhook event %s has no local subscription", event_name)
|
local_user_id = getattr(db_user, "user_id", None) if db_user else None
|
||||||
|
logging.warning(
|
||||||
|
"Panel webhook event %s cannot be matched to a local subscription; "
|
||||||
|
"notification skipped. %s local_user_id=%s. Possible causes: "
|
||||||
|
"panel user was created outside the bot, subscription was deleted "
|
||||||
|
"or not synced, panel identifiers changed, or skip_notifications "
|
||||||
|
"is enabled for the local subscription.",
|
||||||
|
event_name,
|
||||||
|
self._payload_log_context(user_payload),
|
||||||
|
local_user_id or "N/A",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
await self._send_legacy_without_dedupe(
|
await self._send_legacy_without_dedupe(
|
||||||
event_name,
|
event_name,
|
||||||
@@ -403,6 +429,31 @@ class PanelWebhookService:
|
|||||||
def _payload_expire_date(user_payload: dict) -> str:
|
def _payload_expire_date(user_payload: dict) -> str:
|
||||||
return str(user_payload.get("expireAt") or "")[:10]
|
return str(user_payload.get("expireAt") or "")[:10]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _payload_log_context(user_payload: dict) -> str:
|
||||||
|
telegram_id = PanelWebhookService._payload_telegram_id(user_payload)
|
||||||
|
panel_uuid = PanelWebhookService._payload_panel_uuid(user_payload)
|
||||||
|
email = PanelWebhookService._mask_email(str(user_payload.get("email") or "").strip())
|
||||||
|
expire_at = str(user_payload.get("expireAt") or "").strip()
|
||||||
|
payload_keys = ",".join(sorted(str(key) for key in user_payload.keys())) or "none"
|
||||||
|
return (
|
||||||
|
f"telegramId={telegram_id or 'N/A'} "
|
||||||
|
f"panel_uuid={panel_uuid or 'N/A'} "
|
||||||
|
f"email={email or 'N/A'} "
|
||||||
|
f"expireAt={expire_at or 'N/A'} "
|
||||||
|
f"payload_keys={payload_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _mask_email(email: str) -> str:
|
||||||
|
if not email:
|
||||||
|
return ""
|
||||||
|
local_part, separator, domain = email.partition("@")
|
||||||
|
if not separator or not domain:
|
||||||
|
return "present"
|
||||||
|
visible = local_part[:2] if len(local_part) > 2 else local_part[:1]
|
||||||
|
return f"{visible}***@{domain}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _payload_expire_datetime(user_payload: dict) -> Optional[datetime]:
|
def _payload_expire_datetime(user_payload: dict) -> Optional[datetime]:
|
||||||
raw = str(user_payload.get("expireAt") or "").strip()
|
raw = str(user_payload.get("expireAt") or "").strip()
|
||||||
|
|||||||
@@ -133,5 +133,69 @@ class HandleWebhookQueueingTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(background_seen, [("user.expired", {"telegramId": 7})])
|
self.assertEqual(background_seen, [("user.expired", {"telegramId": 7})])
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeSessionContext:
|
||||||
|
async def __aenter__(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeSessionFactory:
|
||||||
|
def __call__(self):
|
||||||
|
return _FakeSessionContext()
|
||||||
|
|
||||||
|
|
||||||
|
class HandleEventLoggingTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_unsupported_event_is_logged_as_ignored(self):
|
||||||
|
service = _make_service()
|
||||||
|
|
||||||
|
with self.assertLogs(level="INFO") as logs:
|
||||||
|
await service.handle_event(
|
||||||
|
"user.modified",
|
||||||
|
{"uuid": "panel-user-1", "email": "client@example.com"},
|
||||||
|
)
|
||||||
|
|
||||||
|
message = "\n".join(logs.output)
|
||||||
|
self.assertIn("Panel webhook event user.modified ignored", message)
|
||||||
|
self.assertIn("event is not used for subscription notifications", message)
|
||||||
|
self.assertIn("panel_uuid=panel-user-1", message)
|
||||||
|
self.assertIn("email=cl***@example.com", message)
|
||||||
|
|
||||||
|
async def test_missing_subscription_warning_includes_payload_context(self):
|
||||||
|
service = _make_service()
|
||||||
|
service.async_session_factory = _FakeSessionFactory()
|
||||||
|
|
||||||
|
async def fake_user_for_payload(session, user_payload):
|
||||||
|
return SimpleNamespace(user_id=123, language_code=None)
|
||||||
|
|
||||||
|
async def fake_subscription_for_payload(session, user_payload, db_user):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(service, "_user_for_payload", fake_user_for_payload),
|
||||||
|
patch.object(service, "_subscription_for_payload", fake_subscription_for_payload),
|
||||||
|
self.assertLogs(level="WARNING") as logs,
|
||||||
|
):
|
||||||
|
await service.handle_event(
|
||||||
|
"user.expired",
|
||||||
|
{
|
||||||
|
"uuid": "panel-user-2",
|
||||||
|
"email": "person@example.com",
|
||||||
|
"expireAt": "2026-05-30T07:18:32Z",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
message = "\n".join(logs.output)
|
||||||
|
self.assertIn("cannot be matched to a local subscription", message)
|
||||||
|
self.assertIn("notification skipped", message)
|
||||||
|
self.assertIn("telegramId=N/A", message)
|
||||||
|
self.assertIn("panel_uuid=panel-user-2", message)
|
||||||
|
self.assertIn("email=pe***@example.com", message)
|
||||||
|
self.assertIn("expireAt=2026-05-30T07:18:32Z", message)
|
||||||
|
self.assertIn("local_user_id=123", message)
|
||||||
|
self.assertIn("subscription was deleted or not synced", message)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user