Files
GIA/core/commands/engine.py

126 lines
4.3 KiB
Python

from __future__ import annotations
from asgiref.sync import sync_to_async
from core.commands.base import CommandContext, CommandResult
from core.commands.handlers.bp import BPCommandHandler
from core.commands.registry import get as get_handler
from core.commands.registry import register
from core.messaging.reply_sync import is_mirrored_origin
from core.models import CommandChannelBinding, CommandProfile, Message
from core.util import logs
log = logs.get_logger("command_engine")
_REGISTERED = False
def ensure_handlers_registered():
global _REGISTERED
if _REGISTERED:
return
register(BPCommandHandler())
_REGISTERED = True
async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]:
def _load():
direct = list(
CommandProfile.objects.filter(
user_id=ctx.user_id,
enabled=True,
channel_bindings__enabled=True,
channel_bindings__direction="ingress",
channel_bindings__service=ctx.service,
channel_bindings__channel_identifier=ctx.channel_identifier,
).distinct()
)
if direct:
return direct
# Compose-originated messages use `web` service even when the
# underlying conversation is mapped to a platform identifier.
if str(ctx.service or "").strip().lower() != "web":
return []
trigger = (
Message.objects.select_related("session", "session__identifier")
.filter(id=ctx.message_id, user_id=ctx.user_id)
.first()
)
identifier = getattr(getattr(trigger, "session", None), "identifier", None)
fallback_service = str(getattr(identifier, "service", "") or "").strip().lower()
fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip()
if not fallback_service or not fallback_identifier:
return []
return list(
CommandProfile.objects.filter(
user_id=ctx.user_id,
enabled=True,
channel_bindings__enabled=True,
channel_bindings__direction="ingress",
channel_bindings__service=fallback_service,
channel_bindings__channel_identifier=fallback_identifier,
).distinct()
)
return await sync_to_async(_load)()
def _matches_trigger(profile: CommandProfile, text: str) -> bool:
body = str(text or "").strip()
trigger = str(profile.trigger_token or "").strip()
if not trigger:
return False
if profile.exact_match_only:
return body == trigger
return trigger in body
async def process_inbound_message(ctx: CommandContext) -> list[CommandResult]:
ensure_handlers_registered()
trigger_message = await sync_to_async(
lambda: Message.objects.filter(id=ctx.message_id).first()
)()
if trigger_message is None:
return []
if is_mirrored_origin(trigger_message.message_meta):
return []
profiles = await _eligible_profiles(ctx)
results: list[CommandResult] = []
for profile in profiles:
if not _matches_trigger(profile, ctx.message_text):
continue
if profile.reply_required and trigger_message.reply_to_id is None:
results.append(
CommandResult(
ok=False,
status="skipped",
error="reply_required",
payload={"profile": profile.slug},
)
)
continue
handler = get_handler(profile.slug)
if handler is None:
results.append(
CommandResult(
ok=False,
status="failed",
error=f"missing_handler:{profile.slug}",
)
)
continue
try:
result = await handler.execute(ctx)
results.append(result)
except Exception as exc:
log.exception("command execution failed for profile=%s: %s", profile.slug, exc)
results.append(
CommandResult(
ok=False,
status="failed",
error=f"handler_exception:{exc}",
)
)
return results