Fix issues with multiplatform conversation displays
This commit is contained in:
@@ -48,13 +48,7 @@ if DEBUG:
|
|||||||
SETTINGS_EXPORT = ["BILLING_ENABLED"]
|
SETTINGS_EXPORT = ["BILLING_ENABLED"]
|
||||||
|
|
||||||
SIGNAL_NUMBER = getenv("SIGNAL_NUMBER")
|
SIGNAL_NUMBER = getenv("SIGNAL_NUMBER")
|
||||||
_container_runtime = getenv("container", "").strip().lower()
|
SIGNAL_HTTP_URL = getenv("SIGNAL_HTTP_URL", "http://signal:8080")
|
||||||
_signal_default_url = (
|
|
||||||
"http://127.0.0.1:8080"
|
|
||||||
if _container_runtime == "podman"
|
|
||||||
else "http://signal:8080"
|
|
||||||
)
|
|
||||||
SIGNAL_HTTP_URL = getenv("SIGNAL_HTTP_URL", _signal_default_url)
|
|
||||||
|
|
||||||
WHATSAPP_ENABLED = getenv("WHATSAPP_ENABLED", "false").lower() in trues
|
WHATSAPP_ENABLED = getenv("WHATSAPP_ENABLED", "false").lower() in trues
|
||||||
WHATSAPP_HTTP_URL = getenv("WHATSAPP_HTTP_URL", "http://whatsapp:8080")
|
WHATSAPP_HTTP_URL = getenv("WHATSAPP_HTTP_URL", "http://whatsapp:8080")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from core.views import (
|
|||||||
queues,
|
queues,
|
||||||
sessions,
|
sessions,
|
||||||
signal,
|
signal,
|
||||||
|
system,
|
||||||
whatsapp,
|
whatsapp,
|
||||||
workspace,
|
workspace,
|
||||||
)
|
)
|
||||||
@@ -54,6 +55,11 @@ urlpatterns = [
|
|||||||
notifications.NotificationsUpdate.as_view(),
|
notifications.NotificationsUpdate.as_view(),
|
||||||
name="notifications_update",
|
name="notifications_update",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"settings/system/",
|
||||||
|
system.SystemSettings.as_view(),
|
||||||
|
name="system_settings",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"services/signal/",
|
"services/signal/",
|
||||||
signal.Signal.as_view(),
|
signal.Signal.as_view(),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
from urllib.parse import quote_plus, urlparse
|
from urllib.parse import quote_plus, urlparse
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -25,10 +24,7 @@ if _signal_http_url:
|
|||||||
SIGNAL_HOST = parsed.hostname or "signal"
|
SIGNAL_HOST = parsed.hostname or "signal"
|
||||||
SIGNAL_PORT = parsed.port or 8080
|
SIGNAL_PORT = parsed.port or 8080
|
||||||
else:
|
else:
|
||||||
if settings.DEBUG:
|
SIGNAL_HOST = "signal"
|
||||||
SIGNAL_HOST = "127.0.0.1"
|
|
||||||
else:
|
|
||||||
SIGNAL_HOST = "signal"
|
|
||||||
SIGNAL_PORT = 8080
|
SIGNAL_PORT = 8080
|
||||||
|
|
||||||
SIGNAL_URL = f"{SIGNAL_HOST}:{SIGNAL_PORT}"
|
SIGNAL_URL = f"{SIGNAL_HOST}:{SIGNAL_PORT}"
|
||||||
@@ -241,12 +237,10 @@ class HandleMessage(Command):
|
|||||||
"raw_message": c.message.raw_message,
|
"raw_message": c.message.raw_message,
|
||||||
}
|
}
|
||||||
raw = json.loads(c.message.raw_message)
|
raw = json.loads(c.message.raw_message)
|
||||||
dest = (
|
sent_message = (
|
||||||
raw.get("envelope", {})
|
raw.get("envelope", {}).get("syncMessage", {}).get("sentMessage", {}) or {}
|
||||||
.get("syncMessage", {})
|
|
||||||
.get("sentMessage", {})
|
|
||||||
.get("destinationUuid")
|
|
||||||
)
|
)
|
||||||
|
dest = sent_message.get("destinationUuid")
|
||||||
|
|
||||||
account = raw.get("account", "")
|
account = raw.get("account", "")
|
||||||
source_name = raw.get("envelope", {}).get("sourceName", "")
|
source_name = raw.get("envelope", {}).get("sourceName", "")
|
||||||
@@ -271,25 +265,22 @@ class HandleMessage(Command):
|
|||||||
envelope_source_uuid = envelope.get("sourceUuid")
|
envelope_source_uuid = envelope.get("sourceUuid")
|
||||||
envelope_source_number = envelope.get("sourceNumber")
|
envelope_source_number = envelope.get("sourceNumber")
|
||||||
envelope_source = envelope.get("source")
|
envelope_source = envelope.get("source")
|
||||||
destination_number = (
|
destination_number = sent_message.get("destination")
|
||||||
raw.get("envelope", {})
|
|
||||||
.get("syncMessage", {})
|
|
||||||
.get("sentMessage", {})
|
|
||||||
.get("destination")
|
|
||||||
)
|
|
||||||
|
|
||||||
primary_identifier = dest if is_from_bot else source_uuid
|
primary_identifier = dest if is_from_bot else source_uuid
|
||||||
if is_from_bot:
|
if dest or destination_number:
|
||||||
# Outbound events must route only by destination identity.
|
# Sync "sentMessage" events are outbound; route by destination only.
|
||||||
# Including the bot's own UUID/number leaks messages across people
|
# This prevents copying one outbound message into multiple people
|
||||||
# if "self" identifiers are linked anywhere.
|
# when source fields include the bot's own identifier.
|
||||||
identifier_candidates = _identifier_candidates(
|
identifier_candidates = _identifier_candidates(dest, destination_number)
|
||||||
dest,
|
elif is_from_bot:
|
||||||
destination_number,
|
identifier_candidates = _identifier_candidates(primary_identifier)
|
||||||
primary_identifier,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
identifier_candidates = _identifier_candidates(
|
bot_identifiers = {
|
||||||
|
str(c.bot.bot_uuid or "").strip(),
|
||||||
|
str(getattr(c.bot, "phone_number", "") or "").strip(),
|
||||||
|
}
|
||||||
|
incoming_candidates = _identifier_candidates(
|
||||||
primary_identifier,
|
primary_identifier,
|
||||||
source_uuid,
|
source_uuid,
|
||||||
source_number,
|
source_number,
|
||||||
@@ -297,8 +288,12 @@ class HandleMessage(Command):
|
|||||||
envelope_source_uuid,
|
envelope_source_uuid,
|
||||||
envelope_source_number,
|
envelope_source_number,
|
||||||
envelope_source,
|
envelope_source,
|
||||||
dest,
|
|
||||||
)
|
)
|
||||||
|
identifier_candidates = [
|
||||||
|
value
|
||||||
|
for value in incoming_candidates
|
||||||
|
if value and value not in bot_identifiers
|
||||||
|
]
|
||||||
if not identifier_candidates:
|
if not identifier_candidates:
|
||||||
log.warning("No Signal identifier available for message routing.")
|
log.warning("No Signal identifier available for message routing.")
|
||||||
return
|
return
|
||||||
@@ -598,12 +593,13 @@ class HandleMessage(Command):
|
|||||||
class SignalClient(ClientBase):
|
class SignalClient(ClientBase):
|
||||||
def __init__(self, ur, *args, **kwargs):
|
def __init__(self, ur, *args, **kwargs):
|
||||||
super().__init__(ur, *args, **kwargs)
|
super().__init__(ur, *args, **kwargs)
|
||||||
|
signal_number = str(getattr(settings, "SIGNAL_NUMBER", "")).strip()
|
||||||
self.client = NewSignalBot(
|
self.client = NewSignalBot(
|
||||||
ur,
|
ur,
|
||||||
self.service,
|
self.service,
|
||||||
{
|
{
|
||||||
"signal_service": SIGNAL_URL,
|
"signal_service": SIGNAL_URL,
|
||||||
"phone_number": "+447490296227",
|
"phone_number": signal_number,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2211,16 +2211,18 @@ class WhatsAppClient(ClientBase):
|
|||||||
raw = str(recipient or "").strip()
|
raw = str(recipient or "").strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
return ""
|
return ""
|
||||||
|
if "@" in raw:
|
||||||
|
return raw
|
||||||
|
digits = re.sub(r"[^0-9]", "", raw)
|
||||||
|
if digits:
|
||||||
|
# Prefer direct JID formatting for phone numbers; Neonize build_jid
|
||||||
|
# can trigger a usync lookup path that intermittently times out.
|
||||||
|
return f"{digits}@s.whatsapp.net"
|
||||||
if self._build_jid is not None:
|
if self._build_jid is not None:
|
||||||
try:
|
try:
|
||||||
return self._build_jid(raw)
|
return self._build_jid(raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if "@" in raw:
|
|
||||||
return raw
|
|
||||||
digits = re.sub(r"[^0-9]", "", raw)
|
|
||||||
if digits:
|
|
||||||
return f"{digits}@s.whatsapp.net"
|
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
def _blob_key_to_compose_url(self, blob_key):
|
def _blob_key_to_compose_url(self, blob_key):
|
||||||
@@ -2275,6 +2277,23 @@ class WhatsAppClient(ClientBase):
|
|||||||
jid = self._to_jid(recipient)
|
jid = self._to_jid(recipient)
|
||||||
if not jid:
|
if not jid:
|
||||||
return False
|
return False
|
||||||
|
if not self._connected and hasattr(self._client, "connect"):
|
||||||
|
try:
|
||||||
|
await self._maybe_await(self._client.connect())
|
||||||
|
self._connected = True
|
||||||
|
self._publish_state(
|
||||||
|
connected=True,
|
||||||
|
last_event="send_reconnect_ok",
|
||||||
|
warning="",
|
||||||
|
last_error="",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._publish_state(
|
||||||
|
connected=False,
|
||||||
|
last_event="send_reconnect_failed",
|
||||||
|
last_error=str(exc),
|
||||||
|
warning=f"WhatsApp reconnect before send failed: {exc}",
|
||||||
|
)
|
||||||
|
|
||||||
sent_any = False
|
sent_any = False
|
||||||
sent_ts = 0
|
sent_ts = 0
|
||||||
@@ -2318,16 +2337,50 @@ class WhatsAppClient(ClientBase):
|
|||||||
self.log.warning("whatsapp attachment send failed: %s", exc)
|
self.log.warning("whatsapp attachment send failed: %s", exc)
|
||||||
|
|
||||||
if text:
|
if text:
|
||||||
try:
|
response = None
|
||||||
response = await self._maybe_await(self._client.send_message(jid, text))
|
last_error = None
|
||||||
sent_any = True
|
for attempt in range(3):
|
||||||
except TypeError:
|
try:
|
||||||
response = await self._maybe_await(
|
response = await self._maybe_await(self._client.send_message(jid, text))
|
||||||
self._client.send_message(jid, message=text)
|
sent_any = True
|
||||||
)
|
last_error = None
|
||||||
sent_any = True
|
break
|
||||||
except Exception as exc:
|
except TypeError:
|
||||||
self.log.warning("whatsapp text send failed: %s", exc)
|
try:
|
||||||
|
response = await self._maybe_await(
|
||||||
|
self._client.send_message(jid, message=text)
|
||||||
|
)
|
||||||
|
sent_any = True
|
||||||
|
last_error = None
|
||||||
|
break
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
|
||||||
|
error_text = str(last_error or "").lower()
|
||||||
|
is_transient = "usync query" in error_text or "timed out" in error_text
|
||||||
|
if is_transient and attempt < 2:
|
||||||
|
if hasattr(self._client, "connect"):
|
||||||
|
try:
|
||||||
|
await self._maybe_await(self._client.connect())
|
||||||
|
self._connected = True
|
||||||
|
self._publish_state(
|
||||||
|
connected=True,
|
||||||
|
last_event="send_retry_reconnect_ok",
|
||||||
|
warning="",
|
||||||
|
)
|
||||||
|
except Exception as reconnect_exc:
|
||||||
|
self._publish_state(
|
||||||
|
connected=False,
|
||||||
|
last_event="send_retry_reconnect_failed",
|
||||||
|
last_error=str(reconnect_exc),
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0.8 * (attempt + 1))
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if last_error is not None and not sent_any:
|
||||||
|
self.log.warning("whatsapp text send failed: %s", last_error)
|
||||||
return False
|
return False
|
||||||
sent_ts = max(
|
sent_ts = max(
|
||||||
sent_ts,
|
sent_ts,
|
||||||
|
|||||||
@@ -416,6 +416,11 @@
|
|||||||
<a class="navbar-item" href="{% url 'ais' type='page' %}">
|
<a class="navbar-item" href="{% url 'ais' type='page' %}">
|
||||||
AI
|
AI
|
||||||
</a>
|
</a>
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<a class="navbar-item" href="{% url 'system_settings' %}">
|
||||||
|
System
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
76
core/templates/pages/system-settings.html
Normal file
76
core/templates/pages/system-settings.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title is-4">System Maintenance</h1>
|
||||||
|
<p class="subtitle is-6">Superuser tools for data cleanup (current user scope).</p>
|
||||||
|
|
||||||
|
{% if notice_message %}
|
||||||
|
<article class="notification is-{{ notice_level|default:'info' }} is-light">
|
||||||
|
{{ notice_message }}
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-12">
|
||||||
|
<article class="box">
|
||||||
|
<h2 class="title is-6">Purge Non-OSINT Data</h2>
|
||||||
|
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.65rem;">
|
||||||
|
Removes message/workspace/AI/mitigation runtime rows but keeps OSINT setup objects.
|
||||||
|
</p>
|
||||||
|
<div class="tags" style="margin-bottom: 0.75rem;">
|
||||||
|
<span class="tag is-light">Chat Sessions: {{ counts.chat_sessions }}</span>
|
||||||
|
<span class="tag is-light">Messages: {{ counts.messages }}</span>
|
||||||
|
<span class="tag is-light">Queued: {{ counts.queued_messages }}</span>
|
||||||
|
<span class="tag is-light">Events: {{ counts.message_events }}</span>
|
||||||
|
<span class="tag is-light">Workspace: {{ counts.workspace_conversations }}</span>
|
||||||
|
<span class="tag is-light">Snapshots: {{ counts.workspace_snapshots }}</span>
|
||||||
|
<span class="tag is-light">AI Requests: {{ counts.ai_requests }}</span>
|
||||||
|
<span class="tag is-light">AI Results: {{ counts.ai_results }}</span>
|
||||||
|
<span class="tag is-light">Memory: {{ counts.memory_items }}</span>
|
||||||
|
<span class="tag is-light">Mitigation Plans: {{ counts.mitigation_plans }}</span>
|
||||||
|
</div>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="purge_non_osint">
|
||||||
|
<button type="submit" class="button is-danger is-light">Purge Non-OSINT</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12">
|
||||||
|
<article class="box">
|
||||||
|
<h2 class="title is-6">Purge OSINT Setup Categories</h2>
|
||||||
|
<p class="is-size-7 has-text-grey" style="margin-bottom: 0.65rem;">
|
||||||
|
Category-specific cleanup controls.
|
||||||
|
</p>
|
||||||
|
<div class="buttons are-small" style="margin-bottom: 0.75rem;">
|
||||||
|
<form method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="purge_osint_people">
|
||||||
|
<button type="submit" class="button is-warning is-light">Purge People ({{ counts.osint_people }})</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="purge_osint_identifiers">
|
||||||
|
<button type="submit" class="button is-warning is-light">Purge Identifiers ({{ counts.osint_identifiers }})</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="purge_osint_groups">
|
||||||
|
<button type="submit" class="button is-warning is-light">Purge Groups ({{ counts.osint_groups }})</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="margin: 0;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="purge_osint_personas">
|
||||||
|
<button type="submit" class="button is-warning is-light">Purge Personas ({{ counts.osint_personas }})</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
@@ -12,6 +12,18 @@
|
|||||||
data-service="{{ option.service }}"
|
data-service="{{ option.service }}"
|
||||||
data-identifier="{{ option.identifier }}"
|
data-identifier="{{ option.identifier }}"
|
||||||
data-person="{{ option.person_id }}"
|
data-person="{{ option.person_id }}"
|
||||||
|
data-signal-identifier="{{ option.signal_identifier|default:'' }}"
|
||||||
|
data-whatsapp-identifier="{{ option.whatsapp_identifier|default:'' }}"
|
||||||
|
data-instagram-identifier="{{ option.instagram_identifier|default:'' }}"
|
||||||
|
data-xmpp-identifier="{{ option.xmpp_identifier|default:'' }}"
|
||||||
|
data-signal-page-url="{{ option.signal_compose_url|default:'' }}"
|
||||||
|
data-whatsapp-page-url="{{ option.whatsapp_compose_url|default:'' }}"
|
||||||
|
data-instagram-page-url="{{ option.instagram_compose_url|default:'' }}"
|
||||||
|
data-xmpp-page-url="{{ option.xmpp_compose_url|default:'' }}"
|
||||||
|
data-signal-widget-url="{{ option.signal_compose_widget_url|default:'' }}"
|
||||||
|
data-whatsapp-widget-url="{{ option.whatsapp_compose_widget_url|default:'' }}"
|
||||||
|
data-instagram-widget-url="{{ option.instagram_compose_widget_url|default:'' }}"
|
||||||
|
data-xmpp-widget-url="{{ option.xmpp_compose_widget_url|default:'' }}"
|
||||||
data-page-url="{{ option.compose_url }}"
|
data-page-url="{{ option.compose_url }}"
|
||||||
data-widget-url="{{ option.compose_widget_url }}"
|
data-widget-url="{{ option.compose_widget_url }}"
|
||||||
{% if option.is_active %}selected{% endif %}>
|
{% if option.is_active %}selected{% endif %}>
|
||||||
@@ -2024,6 +2036,10 @@
|
|||||||
if (!service || !identifier) {
|
if (!service || !identifier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (renderMode === "page" && pageUrl) {
|
||||||
|
window.location.assign(String(pageUrl));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
String(thread.dataset.service || "").toLowerCase() === service
|
String(thread.dataset.service || "").toLowerCase() === service
|
||||||
&& String(thread.dataset.identifier || "") === identifier
|
&& String(thread.dataset.identifier || "") === identifier
|
||||||
@@ -2065,13 +2081,6 @@
|
|||||||
thread.dataset.lastTs = "0";
|
thread.dataset.lastTs = "0";
|
||||||
glanceState = { gap: null, metrics: [] };
|
glanceState = { gap: null, metrics: [] };
|
||||||
renderGlanceItems([]);
|
renderGlanceItems([]);
|
||||||
if (renderMode === "page" && pageUrl) {
|
|
||||||
try {
|
|
||||||
window.history.replaceState({}, "", String(pageUrl));
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore history API failures.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
poll(true);
|
poll(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2685,14 +2694,33 @@
|
|||||||
if (!selected) {
|
if (!selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedService = selected.dataset.service || "";
|
const currentService = String(thread.dataset.service || "").toLowerCase();
|
||||||
const selectedIdentifier = selected.dataset.identifier || "";
|
const serviceIdentifierKey = currentService + "Identifier";
|
||||||
|
const servicePageUrlKey = currentService + "PageUrl";
|
||||||
|
const serviceWidgetUrlKey = currentService + "WidgetUrl";
|
||||||
|
let selectedService = currentService || (selected.dataset.service || "");
|
||||||
|
let selectedIdentifier = String(
|
||||||
|
selected.dataset[serviceIdentifierKey]
|
||||||
|
|| selected.dataset.identifier
|
||||||
|
|| ""
|
||||||
|
).trim();
|
||||||
const selectedPerson = selected.dataset.person || "";
|
const selectedPerson = selected.dataset.person || "";
|
||||||
const selectedPageUrl = (
|
let selectedPageUrl = (
|
||||||
renderMode === "page"
|
renderMode === "page"
|
||||||
? selected.dataset.pageUrl
|
? selected.dataset[servicePageUrlKey]
|
||||||
: selected.dataset.widgetUrl
|
: selected.dataset[serviceWidgetUrlKey]
|
||||||
) || "";
|
) || "";
|
||||||
|
if (!selectedIdentifier) {
|
||||||
|
selectedService = selected.dataset.service || selectedService;
|
||||||
|
selectedIdentifier = selected.dataset.identifier || "";
|
||||||
|
}
|
||||||
|
if (!selectedPageUrl) {
|
||||||
|
selectedPageUrl = (
|
||||||
|
renderMode === "page"
|
||||||
|
? selected.dataset.pageUrl
|
||||||
|
: selected.dataset.widgetUrl
|
||||||
|
) || "";
|
||||||
|
}
|
||||||
switchThreadContext(
|
switchThreadContext(
|
||||||
selectedService,
|
selectedService,
|
||||||
selectedIdentifier,
|
selectedIdentifier,
|
||||||
|
|||||||
@@ -1308,7 +1308,7 @@ def _context_base(user, service, identifier, person):
|
|||||||
).first()
|
).first()
|
||||||
or PersonIdentifier.objects.filter(user=user, person=person).first()
|
or PersonIdentifier.objects.filter(user=user, person=person).first()
|
||||||
)
|
)
|
||||||
if person_identifier is None and identifier:
|
if person_identifier is None and identifier and person is None:
|
||||||
person_identifier = PersonIdentifier.objects.filter(
|
person_identifier = PersonIdentifier.objects.filter(
|
||||||
user=user,
|
user=user,
|
||||||
service=service,
|
service=service,
|
||||||
@@ -1553,10 +1553,34 @@ def _recent_manual_contacts(
|
|||||||
if not all_rows:
|
if not all_rows:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
current_service_key = _default_service(current_service)
|
||||||
|
current_identifier_value = str(current_identifier or "").strip()
|
||||||
|
current_person_id = str(current_person.id) if current_person else ""
|
||||||
|
|
||||||
row_by_key = {
|
row_by_key = {
|
||||||
(str(row.get("service") or "").strip().lower(), str(row.get("identifier") or "").strip()): row
|
(str(row.get("service") or "").strip().lower(), str(row.get("identifier") or "").strip()): row
|
||||||
for row in all_rows
|
for row in all_rows
|
||||||
}
|
}
|
||||||
|
by_person_service = {}
|
||||||
|
person_links = (
|
||||||
|
PersonIdentifier.objects.filter(user=user)
|
||||||
|
.select_related("person")
|
||||||
|
.order_by("person__name", "service", "identifier")
|
||||||
|
)
|
||||||
|
for link in person_links:
|
||||||
|
person_id = str(link.person_id or "")
|
||||||
|
if not person_id:
|
||||||
|
continue
|
||||||
|
service_key = _default_service(link.service)
|
||||||
|
identifier_value = str(link.identifier or "").strip()
|
||||||
|
if not identifier_value:
|
||||||
|
continue
|
||||||
|
by_person_service.setdefault(person_id, {})
|
||||||
|
if service_key not in by_person_service[person_id]:
|
||||||
|
by_person_service[person_id][service_key] = {
|
||||||
|
"identifier": identifier_value,
|
||||||
|
"person_name": str(link.person.name or "").strip() or identifier_value,
|
||||||
|
}
|
||||||
ordered_keys = []
|
ordered_keys = []
|
||||||
seen_keys = set()
|
seen_keys = set()
|
||||||
recent_values = (
|
recent_values = (
|
||||||
@@ -1582,15 +1606,15 @@ def _recent_manual_contacts(
|
|||||||
if len(ordered_keys) >= limit:
|
if len(ordered_keys) >= limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
current_key = (_default_service(current_service), str(current_identifier or "").strip())
|
current_key = (current_service_key, current_identifier_value)
|
||||||
if current_key[1]:
|
if current_key[1]:
|
||||||
if current_key in ordered_keys:
|
if current_key in ordered_keys:
|
||||||
ordered_keys.remove(current_key)
|
ordered_keys.remove(current_key)
|
||||||
ordered_keys.insert(0, current_key)
|
ordered_keys.insert(0, current_key)
|
||||||
if len(ordered_keys) > limit:
|
|
||||||
ordered_keys = ordered_keys[:limit]
|
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
|
seen_people = set()
|
||||||
|
seen_unknown = set()
|
||||||
for service_key, identifier_value in ordered_keys:
|
for service_key, identifier_value in ordered_keys:
|
||||||
row = dict(row_by_key.get((service_key, identifier_value)) or {})
|
row = dict(row_by_key.get((service_key, identifier_value)) or {})
|
||||||
if not row:
|
if not row:
|
||||||
@@ -1611,13 +1635,86 @@ def _recent_manual_contacts(
|
|||||||
"linked_person": False,
|
"linked_person": False,
|
||||||
"source": "recent",
|
"source": "recent",
|
||||||
}
|
}
|
||||||
row["service_label"] = _service_label(service_key)
|
|
||||||
row["person_id"] = str(row.get("person_id") or "")
|
row["person_id"] = str(row.get("person_id") or "")
|
||||||
|
person_id = row["person_id"]
|
||||||
|
if person_id:
|
||||||
|
if person_id in seen_people:
|
||||||
|
continue
|
||||||
|
seen_people.add(person_id)
|
||||||
|
service_map = dict(by_person_service.get(person_id) or {})
|
||||||
|
if service_key not in service_map and identifier_value:
|
||||||
|
service_map[service_key] = {
|
||||||
|
"identifier": identifier_value,
|
||||||
|
"person_name": str(row.get("person_name") or "").strip()
|
||||||
|
or identifier_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_service = service_key
|
||||||
|
selected_identifier = identifier_value
|
||||||
|
if person_id == current_person_id and current_service_key in service_map:
|
||||||
|
selected_service = current_service_key
|
||||||
|
selected_identifier = str(
|
||||||
|
(service_map.get(current_service_key) or {}).get("identifier") or ""
|
||||||
|
).strip()
|
||||||
|
elif selected_service not in service_map:
|
||||||
|
for fallback_service in ("whatsapp", "signal", "instagram", "xmpp"):
|
||||||
|
if fallback_service in service_map:
|
||||||
|
selected_service = fallback_service
|
||||||
|
selected_identifier = str(
|
||||||
|
(service_map.get(fallback_service) or {}).get("identifier")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
break
|
||||||
|
selected_identifier = selected_identifier or identifier_value
|
||||||
|
selected_urls = _compose_urls(
|
||||||
|
selected_service,
|
||||||
|
selected_identifier,
|
||||||
|
person_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
row["service"] = selected_service
|
||||||
|
row["service_label"] = _service_label(selected_service)
|
||||||
|
row["identifier"] = selected_identifier
|
||||||
|
row["compose_url"] = selected_urls["page_url"]
|
||||||
|
row["compose_widget_url"] = selected_urls["widget_url"]
|
||||||
|
row["person_name"] = (
|
||||||
|
str(row.get("linked_person_name") or "").strip()
|
||||||
|
or str(row.get("person_name") or "").strip()
|
||||||
|
or selected_identifier
|
||||||
|
)
|
||||||
|
|
||||||
|
for svc in ("signal", "whatsapp", "instagram", "xmpp"):
|
||||||
|
svc_identifier = str(
|
||||||
|
(service_map.get(svc) or {}).get("identifier") or ""
|
||||||
|
).strip()
|
||||||
|
row[f"{svc}_identifier"] = svc_identifier
|
||||||
|
if svc_identifier:
|
||||||
|
svc_urls = _compose_urls(svc, svc_identifier, person_id)
|
||||||
|
row[f"{svc}_compose_url"] = svc_urls["page_url"]
|
||||||
|
row[f"{svc}_compose_widget_url"] = svc_urls["widget_url"]
|
||||||
|
else:
|
||||||
|
row[f"{svc}_compose_url"] = ""
|
||||||
|
row[f"{svc}_compose_widget_url"] = ""
|
||||||
|
else:
|
||||||
|
unknown_key = (service_key, identifier_value)
|
||||||
|
if unknown_key in seen_unknown:
|
||||||
|
continue
|
||||||
|
seen_unknown.add(unknown_key)
|
||||||
|
row["service_label"] = _service_label(service_key)
|
||||||
|
for svc in ("signal", "whatsapp", "instagram", "xmpp"):
|
||||||
|
row[f"{svc}_identifier"] = identifier_value if svc == service_key else ""
|
||||||
|
row[f"{svc}_compose_url"] = row.get("compose_url") if svc == service_key else ""
|
||||||
|
row[f"{svc}_compose_widget_url"] = (
|
||||||
|
row.get("compose_widget_url") if svc == service_key else ""
|
||||||
|
)
|
||||||
|
|
||||||
row["is_active"] = (
|
row["is_active"] = (
|
||||||
service_key == _default_service(current_service)
|
row.get("service") == current_service_key
|
||||||
and identifier_value == str(current_identifier or "").strip()
|
and str(row.get("identifier") or "").strip() == current_identifier_value
|
||||||
)
|
)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
if len(rows) >= limit:
|
||||||
|
break
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@@ -2127,7 +2224,7 @@ class ComposeThread(LoginRequiredMixin, View):
|
|||||||
session_ids = ComposeHistorySync._session_ids_for_scope(
|
session_ids = ComposeHistorySync._session_ids_for_scope(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
person=base["person"],
|
person=base["person"],
|
||||||
service=service,
|
service=base["service"],
|
||||||
person_identifier=base["person_identifier"],
|
person_identifier=base["person_identifier"],
|
||||||
explicit_identifier=base["identifier"],
|
explicit_identifier=base["identifier"],
|
||||||
)
|
)
|
||||||
@@ -2241,15 +2338,14 @@ class ComposeHistorySync(LoginRequiredMixin, View):
|
|||||||
)
|
)
|
||||||
variants = cls._identifier_variants(service, explicit_identifier)
|
variants = cls._identifier_variants(service, explicit_identifier)
|
||||||
if variants:
|
if variants:
|
||||||
identifiers.extend(
|
variant_qs = PersonIdentifier.objects.filter(
|
||||||
list(
|
user=user,
|
||||||
PersonIdentifier.objects.filter(
|
service=service,
|
||||||
user=user,
|
identifier__in=variants,
|
||||||
service=service,
|
|
||||||
identifier__in=variants,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
if person is not None:
|
||||||
|
variant_qs = variant_qs.filter(person=person)
|
||||||
|
identifiers.extend(list(variant_qs))
|
||||||
unique_ids = []
|
unique_ids = []
|
||||||
seen = set()
|
seen = set()
|
||||||
for row in identifiers:
|
for row in identifiers:
|
||||||
|
|||||||
152
core/views/system.py
Normal file
152
core/views/system.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from core.models import (
|
||||||
|
AIRequest,
|
||||||
|
AIResult,
|
||||||
|
AIResultSignal,
|
||||||
|
Chat,
|
||||||
|
ChatSession,
|
||||||
|
Group,
|
||||||
|
MemoryItem,
|
||||||
|
Message,
|
||||||
|
MessageEvent,
|
||||||
|
PatternArtifactExport,
|
||||||
|
PatternMitigationAutoSettings,
|
||||||
|
PatternMitigationCorrection,
|
||||||
|
PatternMitigationGame,
|
||||||
|
PatternMitigationMessage,
|
||||||
|
PatternMitigationPlan,
|
||||||
|
PatternMitigationRule,
|
||||||
|
Person,
|
||||||
|
PersonIdentifier,
|
||||||
|
Persona,
|
||||||
|
QueuedMessage,
|
||||||
|
WorkspaceConversation,
|
||||||
|
WorkspaceMetricSnapshot,
|
||||||
|
)
|
||||||
|
from core.views.manage.permissions import SuperUserRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SystemSettings(SuperUserRequiredMixin, View):
|
||||||
|
template_name = "pages/system-settings.html"
|
||||||
|
|
||||||
|
def _counts(self, user):
|
||||||
|
return {
|
||||||
|
"chat_sessions": ChatSession.objects.filter(user=user).count(),
|
||||||
|
"messages": Message.objects.filter(user=user).count(),
|
||||||
|
"queued_messages": QueuedMessage.objects.filter(user=user).count(),
|
||||||
|
"message_events": MessageEvent.objects.filter(user=user).count(),
|
||||||
|
"workspace_conversations": WorkspaceConversation.objects.filter(user=user).count(),
|
||||||
|
"workspace_snapshots": WorkspaceMetricSnapshot.objects.filter(
|
||||||
|
conversation__user=user
|
||||||
|
).count(),
|
||||||
|
"ai_requests": AIRequest.objects.filter(user=user).count(),
|
||||||
|
"ai_results": AIResult.objects.filter(user=user).count(),
|
||||||
|
"ai_result_signals": AIResultSignal.objects.filter(user=user).count(),
|
||||||
|
"memory_items": MemoryItem.objects.filter(user=user).count(),
|
||||||
|
"mitigation_plans": PatternMitigationPlan.objects.filter(user=user).count(),
|
||||||
|
"mitigation_rules": PatternMitigationRule.objects.filter(user=user).count(),
|
||||||
|
"mitigation_games": PatternMitigationGame.objects.filter(user=user).count(),
|
||||||
|
"mitigation_corrections": PatternMitigationCorrection.objects.filter(
|
||||||
|
user=user
|
||||||
|
).count(),
|
||||||
|
"mitigation_messages": PatternMitigationMessage.objects.filter(
|
||||||
|
user=user
|
||||||
|
).count(),
|
||||||
|
"mitigation_auto_settings": PatternMitigationAutoSettings.objects.filter(
|
||||||
|
user=user
|
||||||
|
).count(),
|
||||||
|
"mitigation_exports": PatternArtifactExport.objects.filter(user=user).count(),
|
||||||
|
"osint_people": Person.objects.filter(user=user).count(),
|
||||||
|
"osint_identifiers": PersonIdentifier.objects.filter(user=user).count(),
|
||||||
|
"osint_groups": Group.objects.filter(user=user).count(),
|
||||||
|
"osint_personas": Persona.objects.filter(user=user).count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _purge_non_osint(self, user):
|
||||||
|
deleted = 0
|
||||||
|
deleted += PatternArtifactExport.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += PatternMitigationMessage.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += PatternMitigationCorrection.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += PatternMitigationGame.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += PatternMitigationRule.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += PatternMitigationAutoSettings.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += PatternMitigationPlan.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += AIResultSignal.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += AIResult.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += AIRequest.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += MemoryItem.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += WorkspaceMetricSnapshot.objects.filter(conversation__user=user).delete()[0]
|
||||||
|
deleted += MessageEvent.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += Message.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += QueuedMessage.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += WorkspaceConversation.objects.filter(user=user).delete()[0]
|
||||||
|
deleted += ChatSession.objects.filter(user=user).delete()[0]
|
||||||
|
# Chat rows are legacy Signal cache rows and are not user-scoped.
|
||||||
|
deleted += Chat.objects.all().delete()[0]
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
def _purge_osint_people(self, user):
|
||||||
|
return Person.objects.filter(user=user).delete()[0]
|
||||||
|
|
||||||
|
def _purge_osint_identifiers(self, user):
|
||||||
|
return PersonIdentifier.objects.filter(user=user).delete()[0]
|
||||||
|
|
||||||
|
def _purge_osint_groups(self, user):
|
||||||
|
return Group.objects.filter(user=user).delete()[0]
|
||||||
|
|
||||||
|
def _purge_osint_personas(self, user):
|
||||||
|
return Persona.objects.filter(user=user).delete()[0]
|
||||||
|
|
||||||
|
def _handle_action(self, request):
|
||||||
|
action = str(request.POST.get("action") or "").strip().lower()
|
||||||
|
if action == "purge_non_osint":
|
||||||
|
return (
|
||||||
|
"success",
|
||||||
|
f"Purged {self._purge_non_osint(request.user)} non-OSINT row(s).",
|
||||||
|
)
|
||||||
|
if action == "purge_osint_people":
|
||||||
|
return (
|
||||||
|
"warning",
|
||||||
|
f"Purged {self._purge_osint_people(request.user)} OSINT people row(s).",
|
||||||
|
)
|
||||||
|
if action == "purge_osint_identifiers":
|
||||||
|
return (
|
||||||
|
"warning",
|
||||||
|
f"Purged {self._purge_osint_identifiers(request.user)} OSINT identifier row(s).",
|
||||||
|
)
|
||||||
|
if action == "purge_osint_groups":
|
||||||
|
return (
|
||||||
|
"warning",
|
||||||
|
f"Purged {self._purge_osint_groups(request.user)} OSINT group row(s).",
|
||||||
|
)
|
||||||
|
if action == "purge_osint_personas":
|
||||||
|
return (
|
||||||
|
"warning",
|
||||||
|
f"Purged {self._purge_osint_personas(request.user)} OSINT persona row(s).",
|
||||||
|
)
|
||||||
|
return ("danger", "Unknown action.")
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
self.template_name,
|
||||||
|
{
|
||||||
|
"counts": self._counts(request.user),
|
||||||
|
"notice_level": "",
|
||||||
|
"notice_message": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
notice_level, notice_message = self._handle_action(request)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
self.template_name,
|
||||||
|
{
|
||||||
|
"counts": self._counts(request.user),
|
||||||
|
"notice_level": notice_level,
|
||||||
|
"notice_message": notice_message,
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -32,6 +32,7 @@ services:
|
|||||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||||
OPERATION: "${OPERATION}"
|
OPERATION: "${OPERATION}"
|
||||||
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
||||||
|
SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}"
|
||||||
WHATSAPP_DB_DIR: "${WHATSAPP_DB_DIR}"
|
WHATSAPP_DB_DIR: "${WHATSAPP_DB_DIR}"
|
||||||
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
||||||
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
||||||
@@ -108,6 +109,7 @@ services:
|
|||||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||||
OPERATION: "${OPERATION}"
|
OPERATION: "${OPERATION}"
|
||||||
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
||||||
|
SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}"
|
||||||
WHATSAPP_DB_DIR: "${WHATSAPP_DB_DIR}"
|
WHATSAPP_DB_DIR: "${WHATSAPP_DB_DIR}"
|
||||||
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
||||||
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
||||||
@@ -160,6 +162,7 @@ services:
|
|||||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||||
OPERATION: "${OPERATION}"
|
OPERATION: "${OPERATION}"
|
||||||
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
||||||
|
SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}"
|
||||||
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
||||||
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
||||||
XMPP_ADDRESS: "${XMPP_ADDRESS}"
|
XMPP_ADDRESS: "${XMPP_ADDRESS}"
|
||||||
@@ -210,6 +213,7 @@ services:
|
|||||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||||
OPERATION: "${OPERATION}"
|
OPERATION: "${OPERATION}"
|
||||||
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
||||||
|
SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}"
|
||||||
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
||||||
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
||||||
XMPP_ADDRESS: "${XMPP_ADDRESS}"
|
XMPP_ADDRESS: "${XMPP_ADDRESS}"
|
||||||
@@ -253,6 +257,7 @@ services:
|
|||||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||||
OPERATION: "${OPERATION}"
|
OPERATION: "${OPERATION}"
|
||||||
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
SIGNAL_NUMBER: "${SIGNAL_NUMBER}"
|
||||||
|
SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}"
|
||||||
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}"
|
||||||
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}"
|
||||||
XMPP_ADDRESS: "${XMPP_ADDRESS}"
|
XMPP_ADDRESS: "${XMPP_ADDRESS}"
|
||||||
|
|||||||
Reference in New Issue
Block a user