mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-07-28 22:39:26 +00:00
feat(webhook_service): start webhook service early and handle readiness
- Modified the core service to start the webhook service before establishing a radio connection, reducing the window for connection refusals. - Implemented a readiness check in the webhook service to return a 503 status when the bot is not connected, ensuring clear communication to external callers. - Added unit tests to verify the webhook service's behavior when the bot's connection status changes, including rate limiting and readiness responses.
This commit is contained in:
+17
-1
@@ -1990,6 +1990,20 @@ long_jokes = false
|
||||
self.web_viewer_integration.start_viewer()
|
||||
self.logger.info("Web viewer started (early, before radio connect)")
|
||||
|
||||
# Start the inbound webhook service early too (before radio connect). Unlike
|
||||
# the other service plugins, this one accepts inbound connections — starting
|
||||
# it late leaves a window (proportional to how long radio connect() takes)
|
||||
# where external callers get connection-refused instead of a clear response.
|
||||
# The handler itself gates on self.connected and returns 503 until the mesh
|
||||
# link is actually ready, so it's safe to bind before we're connected.
|
||||
webhook_service = self.services.get('webhook')
|
||||
if webhook_service is not None and getattr(webhook_service, 'enabled', False):
|
||||
try:
|
||||
await webhook_service.start()
|
||||
self.logger.info("Service 'webhook' started (early, before radio connect)")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to start service 'webhook' early: {e}")
|
||||
|
||||
# Connect to MeshCore node
|
||||
if not await self.connect():
|
||||
self.logger.error("Failed to connect to MeshCore node")
|
||||
@@ -2032,8 +2046,10 @@ long_jokes = false
|
||||
# Send startup advert if enabled
|
||||
await self.send_startup_advert()
|
||||
|
||||
# Start all loaded services
|
||||
# Start all loaded services (webhook already started early, before radio connect)
|
||||
for service_name, service_instance in self.services.items():
|
||||
if service_name == 'webhook':
|
||||
continue
|
||||
try:
|
||||
await service_instance.start()
|
||||
self.logger.info(f"Service '{service_name}' started")
|
||||
|
||||
@@ -37,6 +37,11 @@ Response codes:
|
||||
400 {"error": "..."} bad/missing fields
|
||||
401 {"error": "Unauthorized"} wrong / missing token
|
||||
405 method not allowed
|
||||
429 {"error": "Rate limit exceeded"}
|
||||
503 {"error": "Bot not yet connected to mesh, try again shortly"}
|
||||
The listener starts before the radio connection is established, so
|
||||
callers may see this briefly after service start; retry after a
|
||||
few seconds.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
@@ -177,6 +182,18 @@ class WebhookService(BaseServicePlugin):
|
||||
text='{"error": "Rate limit exceeded"}',
|
||||
)
|
||||
|
||||
# --- Readiness ---
|
||||
# The listener starts before the mesh connection is established (see
|
||||
# core.py), so reject requests with a clear, retryable error until the
|
||||
# bot is actually connected, rather than accepting them only to fail
|
||||
# message dispatch.
|
||||
if not getattr(self.bot, "connected", False):
|
||||
return aio_web.Response(
|
||||
status=503,
|
||||
content_type="application/json",
|
||||
text='{"error": "Bot not yet connected to mesh, try again shortly"}',
|
||||
)
|
||||
|
||||
# --- Auth ---
|
||||
if self.secret_token and not self._verify_token(request):
|
||||
self.logger.warning(
|
||||
|
||||
@@ -30,6 +30,7 @@ def _make_bot(mock_logger, extra_cfg=None):
|
||||
bot.command_manager = Mock()
|
||||
bot.command_manager.send_channel_message = AsyncMock(return_value=True)
|
||||
bot.command_manager.send_dm = AsyncMock(return_value=True)
|
||||
bot.connected = True
|
||||
return bot
|
||||
|
||||
|
||||
@@ -115,6 +116,57 @@ class TestVerifyToken:
|
||||
assert svc._verify_token(req) is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestHandleWebhook — readiness
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHandleWebhookReadiness:
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_connected_returns_503(self, mock_logger):
|
||||
svc, bot = _make_service(mock_logger)
|
||||
bot.connected = False
|
||||
req = _make_request(body={"channel": "general", "message": "hi"})
|
||||
resp = await svc._handle_webhook(req)
|
||||
assert resp.status == 503
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_connected_does_not_dispatch(self, mock_logger):
|
||||
svc, bot = _make_service(mock_logger)
|
||||
bot.connected = False
|
||||
req = _make_request(body={"channel": "general", "message": "hi"})
|
||||
await svc._handle_webhook(req)
|
||||
bot.command_manager.send_channel_message.assert_not_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connected_true_proceeds_normally(self, mock_logger):
|
||||
svc, bot = _make_service(mock_logger)
|
||||
bot.connected = True
|
||||
req = _make_request(body={"channel": "general", "message": "hi"})
|
||||
resp = await svc._handle_webhook(req)
|
||||
assert resp.status == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_connected_attr_defaults_to_not_ready(self, mock_logger):
|
||||
"""If `bot.connected` isn't present at all, fail safe (503) rather than assume readiness."""
|
||||
svc, bot = _make_service(mock_logger)
|
||||
del bot.connected
|
||||
# Mock() raises AttributeError for deleted attrs, so getattr(..., False) applies.
|
||||
req = _make_request(body={"channel": "general", "message": "hi"})
|
||||
resp = await svc._handle_webhook(req)
|
||||
assert resp.status == 503
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_checked_before_readiness(self, mock_logger):
|
||||
"""Rate limiting should still apply even while the bot isn't connected yet."""
|
||||
svc, bot = _make_service(mock_logger, {"rate_limit_per_minute": "1"})
|
||||
bot.connected = False
|
||||
req = _make_request(body={"channel": "general", "message": "hi"})
|
||||
await svc._handle_webhook(req)
|
||||
resp = await svc._handle_webhook(req)
|
||||
assert resp.status == 429
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestHandleWebhook — auth
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user