Continue AI features and improve protocol support

This commit is contained in:
2026-02-15 16:57:32 +00:00
parent 2d3b8fdac6
commit 85e97e895d
62 changed files with 5472 additions and 441 deletions

View File

@@ -10,12 +10,13 @@ from slixmpp.stanza import Message
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.stanzabase import ET
from core.clients import ClientBase, signalapi
from core.clients import ClientBase
from core.messaging import ai, history, replies, utils
from core.models import (
ChatSession,
Manipulation,
PatternMitigationAutoSettings,
PatternMitigationCorrection,
PatternMitigationGame,
PatternMitigationPlan,
PatternMitigationRule,
@@ -91,21 +92,13 @@ class XMPPComponent(ComponentXMPP):
sender_bare_jid = sender_parts[0] # Always present: user@domain
sender_username, sender_domain = sender_bare_jid.split("@", 1)
sender_resource = (
sender_parts[1] if len(sender_parts) > 1 else None
) # Extract resource if present
# Extract recipient JID (should match component JID format)
recipient_jid = str(msg["to"])
if "@" in recipient_jid:
recipient_username, recipient_domain = recipient_jid.split("@", 1)
recipient_username = recipient_jid.split("@", 1)[0]
else:
recipient_username = recipient_jid
recipient_domain = recipient_jid
# Extract message body
body = msg["body"] if msg["body"] else "[No Body]"
# Parse recipient_name and recipient_service (e.g., "mark|signal")
if "|" in recipient_username:
person_name, service = recipient_username.split("|")
@@ -134,9 +127,15 @@ class XMPPComponent(ComponentXMPP):
return None
def _get_workspace_conversation(self, user, person):
primary_identifier = (
PersonIdentifier.objects.filter(user=user, person=person)
.order_by("service")
.first()
)
platform_type = primary_identifier.service if primary_identifier else "signal"
conversation, _ = WorkspaceConversation.objects.get_or_create(
user=user,
platform_type="signal",
platform_type=platform_type,
title=f"{person.name} Workspace",
defaults={"platform_thread_id": str(person.id)},
)
@@ -186,6 +185,10 @@ class XMPPComponent(ComponentXMPP):
".mitigation rule-del <person>|<title> | "
".mitigation game-add <person>|<title>|<instructions> | "
".mitigation game-del <person>|<title> | "
".mitigation correction-add <person>|<title>|<clarification> | "
".mitigation correction-del <person>|<title> | "
".mitigation fundamentals-set <person>|<item1;item2;...> | "
".mitigation plan-set <person>|<draft|active|archived>|<auto|guided> | "
".mitigation auto <person>|on|off | "
".mitigation auto-status <person>"
)
@@ -214,7 +217,9 @@ class XMPPComponent(ComponentXMPP):
if command.startswith(".mitigation show "):
person_name = command.replace(".mitigation show ", "", 1).strip().title()
person = await sync_to_async(
lambda: Person.objects.filter(user=sender_user, name__iexact=person_name).first()
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
@@ -231,9 +236,15 @@ class XMPPComponent(ComponentXMPP):
if len(parts) < 3:
sym("Usage: .mitigation rule-add <person>|<title>|<content>")
return True
person_name, title, content = parts[0].title(), parts[1], "|".join(parts[2:])
person_name, title, content = (
parts[0].title(),
parts[1],
"|".join(parts[2:]),
)
person = await sync_to_async(
lambda: Person.objects.filter(user=sender_user, name__iexact=person_name).first()
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
@@ -257,7 +268,9 @@ class XMPPComponent(ComponentXMPP):
return True
person_name, title = parts[0].title(), "|".join(parts[1:])
person = await sync_to_async(
lambda: Person.objects.filter(user=sender_user, name__iexact=person_name).first()
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
@@ -279,9 +292,15 @@ class XMPPComponent(ComponentXMPP):
if len(parts) < 3:
sym("Usage: .mitigation game-add <person>|<title>|<instructions>")
return True
person_name, title, content = parts[0].title(), parts[1], "|".join(parts[2:])
person_name, title, content = (
parts[0].title(),
parts[1],
"|".join(parts[2:]),
)
person = await sync_to_async(
lambda: Person.objects.filter(user=sender_user, name__iexact=person_name).first()
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
@@ -305,7 +324,9 @@ class XMPPComponent(ComponentXMPP):
return True
person_name, title = parts[0].title(), "|".join(parts[1:])
person = await sync_to_async(
lambda: Person.objects.filter(user=sender_user, name__iexact=person_name).first()
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
@@ -321,6 +342,128 @@ class XMPPComponent(ComponentXMPP):
sym("Game deleted." if deleted else "Game not found.")
return True
if command.startswith(".mitigation correction-add "):
payload = command.replace(".mitigation correction-add ", "", 1)
parts = parse_parts(payload)
if len(parts) < 3:
sym(
"Usage: .mitigation correction-add <person>|<title>|<clarification>"
)
return True
person_name, title, clarification = (
parts[0].title(),
parts[1],
"|".join(parts[2:]),
)
person = await sync_to_async(
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
return True
plan = await sync_to_async(self._get_or_create_plan)(sender_user, person)
await sync_to_async(PatternMitigationCorrection.objects.create)(
user=sender_user,
plan=plan,
title=title[:255],
clarification=clarification,
source_phrase="",
perspective="second_person",
share_target="both",
language_style="adapted",
enabled=True,
)
sym("Correction added.")
return True
if command.startswith(".mitigation correction-del "):
payload = command.replace(".mitigation correction-del ", "", 1)
parts = parse_parts(payload)
if len(parts) < 2:
sym("Usage: .mitigation correction-del <person>|<title>")
return True
person_name, title = parts[0].title(), "|".join(parts[1:])
person = await sync_to_async(
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
return True
plan = await sync_to_async(self._get_or_create_plan)(sender_user, person)
deleted, _ = await sync_to_async(
lambda: PatternMitigationCorrection.objects.filter(
user=sender_user,
plan=plan,
title__iexact=title,
).delete()
)()
sym("Correction deleted." if deleted else "Correction not found.")
return True
if command.startswith(".mitigation fundamentals-set "):
payload = command.replace(".mitigation fundamentals-set ", "", 1)
parts = parse_parts(payload)
if len(parts) < 2:
sym("Usage: .mitigation fundamentals-set <person>|<item1;item2;...>")
return True
person_name, values = parts[0].title(), "|".join(parts[1:])
person = await sync_to_async(
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
return True
plan = await sync_to_async(self._get_or_create_plan)(sender_user, person)
items = [item.strip() for item in values.split(";") if item.strip()]
plan.fundamental_items = items
await sync_to_async(plan.save)(
update_fields=["fundamental_items", "updated_at"]
)
sym(f"Fundamentals updated ({len(items)}).")
return True
if command.startswith(".mitigation plan-set "):
payload = command.replace(".mitigation plan-set ", "", 1)
parts = parse_parts(payload)
if len(parts) < 3:
sym(
"Usage: .mitigation plan-set <person>|<draft|active|archived>|<auto|guided>"
)
return True
person_name, status_value, mode_value = (
parts[0].title(),
parts[1].lower(),
parts[2].lower(),
)
person = await sync_to_async(
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
return True
plan = await sync_to_async(self._get_or_create_plan)(sender_user, person)
valid_status = {key for key, _ in PatternMitigationPlan.STATUS_CHOICES}
valid_modes = {
key for key, _ in PatternMitigationPlan.CREATION_MODE_CHOICES
}
if status_value in valid_status:
plan.status = status_value
if mode_value in valid_modes:
plan.creation_mode = mode_value
await sync_to_async(plan.save)(
update_fields=["status", "creation_mode", "updated_at"]
)
sym(f"Plan updated: status={plan.status}, mode={plan.creation_mode}")
return True
if command.startswith(".mitigation auto "):
payload = command.replace(".mitigation auto ", "", 1)
parts = parse_parts(payload)
@@ -329,31 +472,47 @@ class XMPPComponent(ComponentXMPP):
return True
person_name, state = parts[0].title(), parts[1].lower()
person = await sync_to_async(
lambda: Person.objects.filter(user=sender_user, name__iexact=person_name).first()
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
return True
conversation = await sync_to_async(self._get_workspace_conversation)(sender_user, person)
auto_obj, _ = await sync_to_async(PatternMitigationAutoSettings.objects.get_or_create)(
conversation = await sync_to_async(self._get_workspace_conversation)(
sender_user, person
)
auto_obj, _ = await sync_to_async(
PatternMitigationAutoSettings.objects.get_or_create
)(
user=sender_user,
conversation=conversation,
)
auto_obj.enabled = state in {"on", "true", "1", "yes"}
await sync_to_async(auto_obj.save)(update_fields=["enabled", "updated_at"])
sym(f"Automation {'enabled' if auto_obj.enabled else 'disabled'} for {person.name}.")
sym(
f"Automation {'enabled' if auto_obj.enabled else 'disabled'} for {person.name}."
)
return True
if command.startswith(".mitigation auto-status "):
person_name = command.replace(".mitigation auto-status ", "", 1).strip().title()
person_name = (
command.replace(".mitigation auto-status ", "", 1).strip().title()
)
person = await sync_to_async(
lambda: Person.objects.filter(user=sender_user, name__iexact=person_name).first()
lambda: Person.objects.filter(
user=sender_user, name__iexact=person_name
).first()
)()
if not person:
sym("Unknown person.")
return True
conversation = await sync_to_async(self._get_workspace_conversation)(sender_user, person)
auto_obj, _ = await sync_to_async(PatternMitigationAutoSettings.objects.get_or_create)(
conversation = await sync_to_async(self._get_workspace_conversation)(
sender_user, person
)
auto_obj, _ = await sync_to_async(
PatternMitigationAutoSettings.objects.get_or_create
)(
user=sender_user,
conversation=conversation,
)
@@ -383,7 +542,7 @@ class XMPPComponent(ComponentXMPP):
"""
self.log.info(f"Chat state: Active from {msg['from']}.")
identifier = self.get_identifier(msg)
self.get_identifier(msg)
def on_chatstate_composing(self, msg):
"""
@@ -392,6 +551,13 @@ class XMPPComponent(ComponentXMPP):
self.log.info(f"Chat state: Composing from {msg['from']}.")
identifier = self.get_identifier(msg)
if identifier:
asyncio.create_task(
self.ur.started_typing(
"xmpp",
identifier=identifier,
)
)
def on_chatstate_paused(self, msg):
"""
@@ -400,6 +566,13 @@ class XMPPComponent(ComponentXMPP):
self.log.info(f"Chat state: Paused from {msg['from']}.")
identifier = self.get_identifier(msg)
if identifier:
asyncio.create_task(
self.ur.stopped_typing(
"xmpp",
identifier=identifier,
)
)
def on_chatstate_inactive(self, msg):
"""
@@ -407,7 +580,7 @@ class XMPPComponent(ComponentXMPP):
"""
self.log.info(f"Chat state: Inactive from {msg['from']}.")
identifier = self.get_identifier(msg)
self.get_identifier(msg)
def on_chatstate_gone(self, msg):
"""
@@ -415,7 +588,7 @@ class XMPPComponent(ComponentXMPP):
"""
self.log.info(f"Chat state: Gone from {msg['from']}.")
identifier = self.get_identifier(msg)
self.get_identifier(msg)
def on_presence_available(self, pres):
"""
@@ -621,7 +794,9 @@ class XMPPComponent(ComponentXMPP):
Process incoming XMPP messages.
"""
sym = lambda x: msg.reply(f"[>] {x}").send()
def sym(value):
msg.reply(f"[>] {value}").send()
# self.log.info(f"Received message: {msg}")
# Extract sender JID (full format: user@domain/resource)
@@ -710,7 +885,7 @@ class XMPPComponent(ComponentXMPP):
# Construct contact list response
contact_names = [person.name for person in persons]
response_text = f"Contacts: " + ", ".join(contact_names)
response_text = "Contacts: " + ", ".join(contact_names)
sym(response_text)
elif body == ".help":
sym("Commands: .contacts, .whoami, .mitigation help")
@@ -785,12 +960,11 @@ class XMPPComponent(ComponentXMPP):
)
self.log.info(f"MANIP11 {manipulations}")
if not manipulations:
tss = await signalapi.send_message_raw(
identifier.identifier,
await identifier.send(
body,
attachments,
)
self.log.info(f"Message sent unaltered")
self.log.info("Message sent unaltered")
return
manip = manipulations.first()
@@ -810,12 +984,11 @@ class XMPPComponent(ComponentXMPP):
text=result,
ts=int(now().timestamp() * 1000),
)
tss = await signalapi.send_message_raw(
identifier.identifier,
await identifier.send(
result,
attachments,
)
self.log.info(f"Message sent with modifications")
self.log.info("Message sent with modifications")
async def request_upload_slots(self, recipient_jid, attachments):
"""Requests upload slots for multiple attachments concurrently."""
@@ -898,7 +1071,7 @@ class XMPPComponent(ComponentXMPP):
# Step 2: Request upload slots concurrently
valid_uploads = await self.request_upload_slots(recipient_jid, attachments)
self.log.info(f"Got upload slots")
self.log.info("Got upload slots")
if not valid_uploads:
self.log.warning("No valid upload slots obtained.")
# return