146 lines
5.0 KiB
Python
146 lines
5.0 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 _channel_variants(service: str, channel_identifier: str) -> list[str]:
|
|
value = str(channel_identifier or "").strip()
|
|
if not value:
|
|
return []
|
|
variants = [value]
|
|
svc = str(service or "").strip().lower()
|
|
if svc == "whatsapp":
|
|
bare = value.split("@", 1)[0].strip()
|
|
if bare and bare not in variants:
|
|
variants.append(bare)
|
|
group = f"{bare}@g.us" if bare else ""
|
|
if group and group not in variants:
|
|
variants.append(group)
|
|
return variants
|
|
|
|
|
|
def ensure_handlers_registered():
|
|
global _REGISTERED
|
|
if _REGISTERED:
|
|
return
|
|
register(BPCommandHandler())
|
|
_REGISTERED = True
|
|
|
|
|
|
async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]:
|
|
def _load():
|
|
direct_variants = _channel_variants(ctx.service, ctx.channel_identifier)
|
|
if not direct_variants:
|
|
return []
|
|
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__in=direct_variants,
|
|
).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()
|
|
fallback_variants = _channel_variants(fallback_service, fallback_identifier)
|
|
if not fallback_service or not fallback_variants:
|
|
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__in=fallback_variants,
|
|
).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
|