diff --git a/app/local_settings.py b/app/local_settings.py index d5437db..ef0962e 100644 --- a/app/local_settings.py +++ b/app/local_settings.py @@ -48,13 +48,7 @@ if DEBUG: SETTINGS_EXPORT = ["BILLING_ENABLED"] SIGNAL_NUMBER = getenv("SIGNAL_NUMBER") -_container_runtime = getenv("container", "").strip().lower() -_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) +SIGNAL_HTTP_URL = getenv("SIGNAL_HTTP_URL", "http://signal:8080") WHATSAPP_ENABLED = getenv("WHATSAPP_ENABLED", "false").lower() in trues WHATSAPP_HTTP_URL = getenv("WHATSAPP_HTTP_URL", "http://whatsapp:8080") diff --git a/app/urls.py b/app/urls.py index 8121d89..d4abeaf 100644 --- a/app/urls.py +++ b/app/urls.py @@ -36,6 +36,7 @@ from core.views import ( queues, sessions, signal, + system, whatsapp, workspace, ) @@ -54,6 +55,11 @@ urlpatterns = [ notifications.NotificationsUpdate.as_view(), name="notifications_update", ), + path( + "settings/system/", + system.SystemSettings.as_view(), + name="system_settings", + ), path( "services/signal/", signal.Signal.as_view(), diff --git a/core/clients/signal.py b/core/clients/signal.py index 7b6a2d6..ec8dea4 100644 --- a/core/clients/signal.py +++ b/core/clients/signal.py @@ -1,6 +1,5 @@ import asyncio import json -import os from urllib.parse import quote_plus, urlparse import aiohttp @@ -25,10 +24,7 @@ if _signal_http_url: SIGNAL_HOST = parsed.hostname or "signal" SIGNAL_PORT = parsed.port or 8080 else: - if settings.DEBUG: - SIGNAL_HOST = "127.0.0.1" - else: - SIGNAL_HOST = "signal" + SIGNAL_HOST = "signal" SIGNAL_PORT = 8080 SIGNAL_URL = f"{SIGNAL_HOST}:{SIGNAL_PORT}" @@ -241,12 +237,10 @@ class HandleMessage(Command): "raw_message": c.message.raw_message, } raw = json.loads(c.message.raw_message) - dest = ( - raw.get("envelope", {}) - .get("syncMessage", {}) - .get("sentMessage", {}) - .get("destinationUuid") + sent_message = ( + raw.get("envelope", {}).get("syncMessage", {}).get("sentMessage", {}) or {} ) + dest = sent_message.get("destinationUuid") account = raw.get("account", "") source_name = raw.get("envelope", {}).get("sourceName", "") @@ -271,25 +265,22 @@ class HandleMessage(Command): envelope_source_uuid = envelope.get("sourceUuid") envelope_source_number = envelope.get("sourceNumber") envelope_source = envelope.get("source") - destination_number = ( - raw.get("envelope", {}) - .get("syncMessage", {}) - .get("sentMessage", {}) - .get("destination") - ) + destination_number = sent_message.get("destination") primary_identifier = dest if is_from_bot else source_uuid - if is_from_bot: - # Outbound events must route only by destination identity. - # Including the bot's own UUID/number leaks messages across people - # if "self" identifiers are linked anywhere. - identifier_candidates = _identifier_candidates( - dest, - destination_number, - primary_identifier, - ) + if dest or destination_number: + # Sync "sentMessage" events are outbound; route by destination only. + # This prevents copying one outbound message into multiple people + # when source fields include the bot's own identifier. + identifier_candidates = _identifier_candidates(dest, destination_number) + elif is_from_bot: + identifier_candidates = _identifier_candidates(primary_identifier) 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, source_uuid, source_number, @@ -297,8 +288,12 @@ class HandleMessage(Command): envelope_source_uuid, envelope_source_number, envelope_source, - dest, ) + identifier_candidates = [ + value + for value in incoming_candidates + if value and value not in bot_identifiers + ] if not identifier_candidates: log.warning("No Signal identifier available for message routing.") return @@ -598,12 +593,13 @@ class HandleMessage(Command): class SignalClient(ClientBase): def __init__(self, ur, *args, **kwargs): super().__init__(ur, *args, **kwargs) + signal_number = str(getattr(settings, "SIGNAL_NUMBER", "")).strip() self.client = NewSignalBot( ur, self.service, { "signal_service": SIGNAL_URL, - "phone_number": "+447490296227", + "phone_number": signal_number, }, ) diff --git a/core/clients/whatsapp.py b/core/clients/whatsapp.py index 34e7f2f..de27748 100644 --- a/core/clients/whatsapp.py +++ b/core/clients/whatsapp.py @@ -2211,16 +2211,18 @@ class WhatsAppClient(ClientBase): raw = str(recipient or "").strip() if not raw: 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: try: return self._build_jid(raw) except Exception: pass - if "@" in raw: - return raw - digits = re.sub(r"[^0-9]", "", raw) - if digits: - return f"{digits}@s.whatsapp.net" return raw def _blob_key_to_compose_url(self, blob_key): @@ -2275,6 +2277,23 @@ class WhatsAppClient(ClientBase): jid = self._to_jid(recipient) if not jid: 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_ts = 0 @@ -2318,16 +2337,50 @@ class WhatsAppClient(ClientBase): self.log.warning("whatsapp attachment send failed: %s", exc) if text: - try: - response = await self._maybe_await(self._client.send_message(jid, text)) - sent_any = True - except TypeError: - response = await self._maybe_await( - self._client.send_message(jid, message=text) - ) - sent_any = True - except Exception as exc: - self.log.warning("whatsapp text send failed: %s", exc) + response = None + last_error = None + for attempt in range(3): + try: + response = await self._maybe_await(self._client.send_message(jid, text)) + sent_any = True + last_error = None + break + except TypeError: + 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 sent_ts = max( sent_ts, diff --git a/core/templates/base.html b/core/templates/base.html index 0e4327e..cf91aa8 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -416,6 +416,11 @@ AI + {% if user.is_superuser %} + + System + + {% endif %} {% endif %} diff --git a/core/templates/pages/system-settings.html b/core/templates/pages/system-settings.html new file mode 100644 index 0000000..067f206 --- /dev/null +++ b/core/templates/pages/system-settings.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

System Maintenance

+

Superuser tools for data cleanup (current user scope).

+ + {% if notice_message %} +
+ {{ notice_message }} +
+ {% endif %} + +
+
+
+

Purge Non-OSINT Data

+

+ Removes message/workspace/AI/mitigation runtime rows but keeps OSINT setup objects. +

+
+ Chat Sessions: {{ counts.chat_sessions }} + Messages: {{ counts.messages }} + Queued: {{ counts.queued_messages }} + Events: {{ counts.message_events }} + Workspace: {{ counts.workspace_conversations }} + Snapshots: {{ counts.workspace_snapshots }} + AI Requests: {{ counts.ai_requests }} + AI Results: {{ counts.ai_results }} + Memory: {{ counts.memory_items }} + Mitigation Plans: {{ counts.mitigation_plans }} +
+
+ {% csrf_token %} + + +
+
+
+ +
+
+

Purge OSINT Setup Categories

+

+ Category-specific cleanup controls. +

+
+
+ {% csrf_token %} + + +
+
+ {% csrf_token %} + + +
+
+ {% csrf_token %} + + +
+
+ {% csrf_token %} + + +
+
+
+
+
+
+
+{% endblock %} + diff --git a/core/templates/partials/compose-panel.html b/core/templates/partials/compose-panel.html index 19ac44a..575e794 100644 --- a/core/templates/partials/compose-panel.html +++ b/core/templates/partials/compose-panel.html @@ -12,6 +12,18 @@ data-service="{{ option.service }}" data-identifier="{{ option.identifier }}" 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-widget-url="{{ option.compose_widget_url }}" {% if option.is_active %}selected{% endif %}> @@ -2024,6 +2036,10 @@ if (!service || !identifier) { return; } + if (renderMode === "page" && pageUrl) { + window.location.assign(String(pageUrl)); + return; + } if ( String(thread.dataset.service || "").toLowerCase() === service && String(thread.dataset.identifier || "") === identifier @@ -2065,13 +2081,6 @@ thread.dataset.lastTs = "0"; glanceState = { gap: null, metrics: [] }; renderGlanceItems([]); - if (renderMode === "page" && pageUrl) { - try { - window.history.replaceState({}, "", String(pageUrl)); - } catch (err) { - // Ignore history API failures. - } - } poll(true); }; @@ -2685,14 +2694,33 @@ if (!selected) { return; } - const selectedService = selected.dataset.service || ""; - const selectedIdentifier = selected.dataset.identifier || ""; + const currentService = String(thread.dataset.service || "").toLowerCase(); + 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 selectedPageUrl = ( + let selectedPageUrl = ( renderMode === "page" - ? selected.dataset.pageUrl - : selected.dataset.widgetUrl + ? selected.dataset[servicePageUrlKey] + : 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( selectedService, selectedIdentifier, diff --git a/core/views/compose.py b/core/views/compose.py index 86b8c17..08566ee 100644 --- a/core/views/compose.py +++ b/core/views/compose.py @@ -1308,7 +1308,7 @@ def _context_base(user, service, identifier, 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( user=user, service=service, @@ -1553,10 +1553,34 @@ def _recent_manual_contacts( if not all_rows: 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 = { (str(row.get("service") or "").strip().lower(), str(row.get("identifier") or "").strip()): row 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 = [] seen_keys = set() recent_values = ( @@ -1582,15 +1606,15 @@ def _recent_manual_contacts( if len(ordered_keys) >= limit: 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 in ordered_keys: ordered_keys.remove(current_key) ordered_keys.insert(0, current_key) - if len(ordered_keys) > limit: - ordered_keys = ordered_keys[:limit] rows = [] + seen_people = set() + seen_unknown = set() for service_key, identifier_value in ordered_keys: row = dict(row_by_key.get((service_key, identifier_value)) or {}) if not row: @@ -1611,13 +1635,86 @@ def _recent_manual_contacts( "linked_person": False, "source": "recent", } - row["service_label"] = _service_label(service_key) 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"] = ( - service_key == _default_service(current_service) - and identifier_value == str(current_identifier or "").strip() + row.get("service") == current_service_key + and str(row.get("identifier") or "").strip() == current_identifier_value ) rows.append(row) + if len(rows) >= limit: + break return rows @@ -2127,7 +2224,7 @@ class ComposeThread(LoginRequiredMixin, View): session_ids = ComposeHistorySync._session_ids_for_scope( user=request.user, person=base["person"], - service=service, + service=base["service"], person_identifier=base["person_identifier"], explicit_identifier=base["identifier"], ) @@ -2241,15 +2338,14 @@ class ComposeHistorySync(LoginRequiredMixin, View): ) variants = cls._identifier_variants(service, explicit_identifier) if variants: - identifiers.extend( - list( - PersonIdentifier.objects.filter( - user=user, - service=service, - identifier__in=variants, - ) - ) + variant_qs = PersonIdentifier.objects.filter( + user=user, + service=service, + identifier__in=variants, ) + if person is not None: + variant_qs = variant_qs.filter(person=person) + identifiers.extend(list(variant_qs)) unique_ids = [] seen = set() for row in identifiers: diff --git a/core/views/system.py b/core/views/system.py new file mode 100644 index 0000000..c7d11b4 --- /dev/null +++ b/core/views/system.py @@ -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, + }, + ) diff --git a/docker-compose.yml b/docker-compose.yml index bb70ada..beaa77f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,7 @@ services: REGISTRATION_OPEN: "${REGISTRATION_OPEN}" OPERATION: "${OPERATION}" SIGNAL_NUMBER: "${SIGNAL_NUMBER}" + SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}" WHATSAPP_DB_DIR: "${WHATSAPP_DB_DIR}" WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}" INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}" @@ -108,6 +109,7 @@ services: REGISTRATION_OPEN: "${REGISTRATION_OPEN}" OPERATION: "${OPERATION}" SIGNAL_NUMBER: "${SIGNAL_NUMBER}" + SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}" WHATSAPP_DB_DIR: "${WHATSAPP_DB_DIR}" WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}" INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}" @@ -160,6 +162,7 @@ services: REGISTRATION_OPEN: "${REGISTRATION_OPEN}" OPERATION: "${OPERATION}" SIGNAL_NUMBER: "${SIGNAL_NUMBER}" + SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}" WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}" INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}" XMPP_ADDRESS: "${XMPP_ADDRESS}" @@ -210,6 +213,7 @@ services: REGISTRATION_OPEN: "${REGISTRATION_OPEN}" OPERATION: "${OPERATION}" SIGNAL_NUMBER: "${SIGNAL_NUMBER}" + SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}" WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}" INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}" XMPP_ADDRESS: "${XMPP_ADDRESS}" @@ -253,6 +257,7 @@ services: REGISTRATION_OPEN: "${REGISTRATION_OPEN}" OPERATION: "${OPERATION}" SIGNAL_NUMBER: "${SIGNAL_NUMBER}" + SIGNAL_HTTP_URL: "${SIGNAL_HTTP_URL:-http://signal:8080}" WHATSAPP_ENABLED: "${WHATSAPP_ENABLED}" INSTAGRAM_ENABLED: "${INSTAGRAM_ENABLED}" XMPP_ADDRESS: "${XMPP_ADDRESS}"