diff --git a/src/dpy_webhooks/dpy_webhooks.py b/src/dpy_webhooks/dpy_webhooks.py index 27799b2..ae04d81 100644 --- a/src/dpy_webhooks/dpy_webhooks.py +++ b/src/dpy_webhooks/dpy_webhooks.py @@ -60,6 +60,7 @@ through :meth:`clear` as usual. """ from __future__ import annotations +import asyncio import io import logging import random @@ -71,7 +72,6 @@ log = logging.getLogger(__name__) MAX_WEBHOOKS_PER_CHANNEL = 10 -_DEAD_WEBHOOK_STATUSES = (401, 404) _UNKNOWN_WEBHOOK_CODE = 10015 _INVALID_TOKEN_CODE = 50027 @@ -132,11 +132,8 @@ class InMemoryWebhookStore: def _is_dead_webhook(exc: discord.HTTPException) -> bool: """true when an HTTPException signals a webhook that no longer exists or has an invalid - token (the recover-and-recreate trigger)""" - if isinstance(exc, discord.NotFound): - return True - if exc.status in _DEAD_WEBHOOK_STATUSES: - return True + token (the recover-and-recreate trigger); keyed on discord's error code only - a bare + 404/401 can be another resource (e.g. 10003 Unknown Channel for a deleted thread=)""" return exc.code in (_UNKNOWN_WEBHOOK_CODE, _INVALID_TOKEN_CODE) @@ -203,6 +200,7 @@ class DPYWebhooks: self.default_name = default_name self._cache: dict[int, discord.Webhook] = {} self._rr: dict[int, dict] = {} + self._create_lock = asyncio.Lock() async def get_or_create( self, @@ -212,16 +210,19 @@ class DPYWebhooks: ) -> discord.Webhook: """return the channel's managed webhook, restoring from store or creating if absent; raises WebhookCapacityError at the cap unless evict_oldest, and propagates Forbidden - when Manage Webhooks is missing""" + when Manage Webhooks is missing; concurrent cold calls share one create""" cached = self._cache.get(channel.id) if cached is not None: return cached - restored = await self._restore_from_store(channel) - if restored is not None: - return restored - - return await self._create(channel, evict_oldest=evict_oldest) + async with self._create_lock: + cached = self._cache.get(channel.id) + if cached is not None: + return cached + restored = await self._restore_from_store(channel) + if restored is not None: + return restored + return await self._create(channel, evict_oldest=evict_oldest) async def get(self, channel: discord.TextChannel) -> "Optional[discord.Webhook]": """return the managed webhook if one exists (in-process cache then store), else None;