Continue AI features and improve protocol support
This commit is contained in:
@@ -2,10 +2,8 @@ import logging
|
||||
|
||||
# import stripe
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import View
|
||||
from django.views.generic.edit import CreateView
|
||||
|
||||
|
||||
369
core/views/compose.py
Normal file
369
core/views/compose.py
Normal file
@@ -0,0 +1,369 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import time
|
||||
from datetime import datetime, timezone as dt_timezone
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone as dj_timezone
|
||||
from django.views import View
|
||||
|
||||
from core.clients import transport
|
||||
from core.models import ChatSession, Message, Person, PersonIdentifier
|
||||
|
||||
|
||||
def _default_service(service: str | None) -> str:
|
||||
value = str(service or "").strip().lower()
|
||||
if value in {"signal", "whatsapp", "instagram", "xmpp"}:
|
||||
return value
|
||||
return "signal"
|
||||
|
||||
|
||||
def _safe_limit(raw) -> int:
|
||||
try:
|
||||
value = int(raw or 40)
|
||||
except (TypeError, ValueError):
|
||||
value = 40
|
||||
return max(10, min(value, 200))
|
||||
|
||||
|
||||
def _safe_after_ts(raw) -> int:
|
||||
try:
|
||||
value = int(raw or 0)
|
||||
except (TypeError, ValueError):
|
||||
value = 0
|
||||
return max(0, value)
|
||||
|
||||
|
||||
def _format_ts_label(ts_value: int) -> str:
|
||||
try:
|
||||
as_dt = datetime.fromtimestamp(int(ts_value) / 1000, tz=dt_timezone.utc)
|
||||
return dj_timezone.localtime(as_dt).strftime("%H:%M")
|
||||
except Exception:
|
||||
return str(ts_value or "")
|
||||
|
||||
|
||||
def _is_outgoing(msg: Message) -> bool:
|
||||
return str(msg.custom_author or "").upper() in {"USER", "BOT"}
|
||||
|
||||
|
||||
def _serialize_message(msg: Message) -> dict:
|
||||
author = str(msg.custom_author or "").strip()
|
||||
return {
|
||||
"id": str(msg.id),
|
||||
"ts": int(msg.ts or 0),
|
||||
"display_ts": _format_ts_label(int(msg.ts or 0)),
|
||||
"text": str(msg.text or ""),
|
||||
"author": author,
|
||||
"outgoing": _is_outgoing(msg),
|
||||
}
|
||||
|
||||
|
||||
def _context_base(user, service, identifier, person):
|
||||
person_identifier = None
|
||||
if person is not None:
|
||||
person_identifier = (
|
||||
PersonIdentifier.objects.filter(
|
||||
user=user,
|
||||
person=person,
|
||||
service=service,
|
||||
).first()
|
||||
or PersonIdentifier.objects.filter(user=user, person=person).first()
|
||||
)
|
||||
if person_identifier is None and identifier:
|
||||
person_identifier = PersonIdentifier.objects.filter(
|
||||
user=user,
|
||||
service=service,
|
||||
identifier=identifier,
|
||||
).first()
|
||||
|
||||
if person_identifier:
|
||||
service = person_identifier.service
|
||||
identifier = person_identifier.identifier
|
||||
person = person_identifier.person
|
||||
|
||||
return {
|
||||
"person_identifier": person_identifier,
|
||||
"service": service,
|
||||
"identifier": identifier,
|
||||
"person": person,
|
||||
}
|
||||
|
||||
|
||||
def _compose_urls(service, identifier, person_id):
|
||||
query = {"service": service, "identifier": identifier}
|
||||
if person_id:
|
||||
query["person"] = str(person_id)
|
||||
payload = urlencode(query)
|
||||
return {
|
||||
"page_url": f"{reverse('compose_page')}?{payload}",
|
||||
"widget_url": f"{reverse('compose_widget')}?{payload}",
|
||||
}
|
||||
|
||||
|
||||
def _load_messages(user, person_identifier, limit):
|
||||
if person_identifier is None:
|
||||
return {"session": None, "messages": []}
|
||||
|
||||
session, _ = ChatSession.objects.get_or_create(
|
||||
user=user,
|
||||
identifier=person_identifier,
|
||||
)
|
||||
messages = list(
|
||||
Message.objects.filter(user=user, session=session)
|
||||
.select_related("session", "session__identifier", "session__identifier__person")
|
||||
.order_by("-ts")[:limit]
|
||||
)
|
||||
messages.reverse()
|
||||
return {"session": session, "messages": messages}
|
||||
|
||||
|
||||
def _panel_context(
|
||||
request,
|
||||
service: str,
|
||||
identifier: str,
|
||||
person: Person | None,
|
||||
render_mode: str,
|
||||
notice: str = "",
|
||||
level: str = "success",
|
||||
):
|
||||
base = _context_base(request.user, service, identifier, person)
|
||||
limit = _safe_limit(request.GET.get("limit") or request.POST.get("limit"))
|
||||
session_bundle = _load_messages(request.user, base["person_identifier"], limit)
|
||||
last_ts = 0
|
||||
if session_bundle["messages"]:
|
||||
last_ts = int(session_bundle["messages"][-1].ts or 0)
|
||||
urls = _compose_urls(
|
||||
base["service"],
|
||||
base["identifier"],
|
||||
base["person"].id if base["person"] else None,
|
||||
)
|
||||
|
||||
unique_raw = f"{base['service']}|{base['identifier']}|{request.user.id}"
|
||||
unique = hashlib.sha1(unique_raw.encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
return {
|
||||
"service": base["service"],
|
||||
"identifier": base["identifier"],
|
||||
"person": base["person"],
|
||||
"person_identifier": base["person_identifier"],
|
||||
"session": session_bundle["session"],
|
||||
"messages": session_bundle["messages"],
|
||||
"serialized_messages": [
|
||||
_serialize_message(msg) for msg in session_bundle["messages"]
|
||||
],
|
||||
"last_ts": last_ts,
|
||||
"limit": limit,
|
||||
"notice_message": notice,
|
||||
"notice_level": level,
|
||||
"render_mode": render_mode,
|
||||
"compose_page_url": urls["page_url"],
|
||||
"compose_widget_url": urls["widget_url"],
|
||||
"ai_workspace_url": (
|
||||
f"{reverse('ai_workspace')}?person={base['person'].id}"
|
||||
if base["person"]
|
||||
else reverse("ai_workspace")
|
||||
),
|
||||
"manual_icon_class": "fa-solid fa-paper-plane",
|
||||
"panel_id": f"compose-panel-{unique}",
|
||||
}
|
||||
|
||||
|
||||
class ComposeContactsDropdown(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
rows = list(
|
||||
PersonIdentifier.objects.filter(user=request.user)
|
||||
.select_related("person")
|
||||
.order_by("person__name", "service", "identifier")
|
||||
)
|
||||
items = []
|
||||
for row in rows:
|
||||
urls = _compose_urls(row.service, row.identifier, row.person_id)
|
||||
items.append(
|
||||
{
|
||||
"person_name": row.person.name,
|
||||
"service": row.service,
|
||||
"identifier": row.identifier,
|
||||
"compose_url": urls["page_url"],
|
||||
}
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"partials/nav-contacts-dropdown.html",
|
||||
{
|
||||
"items": items,
|
||||
"manual_icon_class": "fa-solid fa-paper-plane",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ComposePage(LoginRequiredMixin, View):
|
||||
template_name = "pages/compose.html"
|
||||
|
||||
def get(self, request):
|
||||
service = _default_service(request.GET.get("service"))
|
||||
identifier = str(request.GET.get("identifier") or "").strip()
|
||||
person = None
|
||||
person_id = request.GET.get("person")
|
||||
if person_id:
|
||||
person = get_object_or_404(Person, id=person_id, user=request.user)
|
||||
if not identifier and person is None:
|
||||
return HttpResponseBadRequest("Missing contact identifier.")
|
||||
|
||||
context = _panel_context(
|
||||
request=request,
|
||||
service=service,
|
||||
identifier=identifier,
|
||||
person=person,
|
||||
render_mode="page",
|
||||
)
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class ComposeWidget(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
service = _default_service(request.GET.get("service"))
|
||||
identifier = str(request.GET.get("identifier") or "").strip()
|
||||
person = None
|
||||
person_id = request.GET.get("person")
|
||||
if person_id:
|
||||
person = get_object_or_404(Person, id=person_id, user=request.user)
|
||||
if not identifier and person is None:
|
||||
return HttpResponseBadRequest("Missing contact identifier.")
|
||||
|
||||
panel_context = _panel_context(
|
||||
request=request,
|
||||
service=service,
|
||||
identifier=identifier,
|
||||
person=person,
|
||||
render_mode="widget",
|
||||
)
|
||||
title_name = (
|
||||
panel_context["person"].name
|
||||
if panel_context["person"] is not None
|
||||
else panel_context["identifier"]
|
||||
)
|
||||
context = {
|
||||
"title": f"Manual Chat: {title_name}",
|
||||
"unique": f"compose-{panel_context['panel_id']}",
|
||||
"window_content": "partials/compose-panel.html",
|
||||
"widget_options": 'gs-w="6" gs-h="12" gs-x="0" gs-y="0" gs-min-w="4"',
|
||||
**panel_context,
|
||||
}
|
||||
return render(request, "mixins/wm/widget.html", context)
|
||||
|
||||
|
||||
class ComposeThread(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
service = _default_service(request.GET.get("service"))
|
||||
identifier = str(request.GET.get("identifier") or "").strip()
|
||||
person = None
|
||||
person_id = request.GET.get("person")
|
||||
if person_id:
|
||||
person = get_object_or_404(Person, id=person_id, user=request.user)
|
||||
if not identifier and person is None:
|
||||
return HttpResponseBadRequest("Missing contact identifier.")
|
||||
|
||||
limit = _safe_limit(request.GET.get("limit") or 60)
|
||||
after_ts = _safe_after_ts(request.GET.get("after_ts"))
|
||||
base = _context_base(request.user, service, identifier, person)
|
||||
latest_ts = after_ts
|
||||
messages = []
|
||||
if base["person_identifier"] is not None:
|
||||
session, _ = ChatSession.objects.get_or_create(
|
||||
user=request.user,
|
||||
identifier=base["person_identifier"],
|
||||
)
|
||||
queryset = Message.objects.filter(user=request.user, session=session)
|
||||
if after_ts > 0:
|
||||
queryset = queryset.filter(ts__gt=after_ts)
|
||||
messages = list(
|
||||
queryset.select_related(
|
||||
"session",
|
||||
"session__identifier",
|
||||
"session__identifier__person",
|
||||
)
|
||||
.order_by("ts")[:limit]
|
||||
)
|
||||
newest = (
|
||||
Message.objects.filter(user=request.user, session=session)
|
||||
.order_by("-ts")
|
||||
.values_list("ts", flat=True)
|
||||
.first()
|
||||
)
|
||||
if newest:
|
||||
latest_ts = max(latest_ts, int(newest))
|
||||
payload = {
|
||||
"messages": [_serialize_message(msg) for msg in messages],
|
||||
"last_ts": latest_ts,
|
||||
}
|
||||
return JsonResponse(payload)
|
||||
|
||||
|
||||
class ComposeSend(LoginRequiredMixin, View):
|
||||
def post(self, request):
|
||||
service = _default_service(request.POST.get("service"))
|
||||
identifier = str(request.POST.get("identifier") or "").strip()
|
||||
person = None
|
||||
person_id = request.POST.get("person")
|
||||
if person_id:
|
||||
person = get_object_or_404(Person, id=person_id, user=request.user)
|
||||
render_mode = str(request.POST.get("render_mode") or "page").strip().lower()
|
||||
if render_mode not in {"page", "widget"}:
|
||||
render_mode = "page"
|
||||
|
||||
if not identifier and person is None:
|
||||
return HttpResponseBadRequest("Missing contact identifier.")
|
||||
|
||||
text = str(request.POST.get("text") or "").strip()
|
||||
if not text:
|
||||
return render(
|
||||
request,
|
||||
"partials/compose-send-status.html",
|
||||
{"notice_message": "Message is empty.", "notice_level": "danger"},
|
||||
)
|
||||
|
||||
base = _context_base(request.user, service, identifier, person)
|
||||
ts = async_to_sync(transport.send_message_raw)(
|
||||
base["service"],
|
||||
base["identifier"],
|
||||
text=text,
|
||||
attachments=[],
|
||||
)
|
||||
if not ts:
|
||||
return render(
|
||||
request,
|
||||
"partials/compose-send-status.html",
|
||||
{
|
||||
"notice_message": "Send failed. Check service account state.",
|
||||
"notice_level": "danger",
|
||||
},
|
||||
)
|
||||
|
||||
if base["person_identifier"] is not None:
|
||||
session, _ = ChatSession.objects.get_or_create(
|
||||
user=request.user,
|
||||
identifier=base["person_identifier"],
|
||||
)
|
||||
Message.objects.create(
|
||||
user=request.user,
|
||||
session=session,
|
||||
sender_uuid="",
|
||||
text=text,
|
||||
ts=int(ts) if str(ts).isdigit() else int(time.time() * 1000),
|
||||
delivered_ts=int(ts) if str(ts).isdigit() else None,
|
||||
custom_author="USER",
|
||||
)
|
||||
|
||||
response = render(
|
||||
request,
|
||||
"partials/compose-send-status.html",
|
||||
{"notice_message": "Sent.", "notice_level": "success"},
|
||||
)
|
||||
response["HX-Trigger"] = "composeMessageSent"
|
||||
return response
|
||||
29
core/views/instagram.py
Normal file
29
core/views/instagram.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from core.clients import transport
|
||||
from core.views.signal import Signal, SignalAccountAdd, SignalAccounts
|
||||
|
||||
|
||||
class Instagram(Signal):
|
||||
service = "instagram"
|
||||
page_title = "Instagram"
|
||||
accounts_url_name = "instagram_accounts"
|
||||
|
||||
|
||||
class InstagramAccounts(SignalAccounts):
|
||||
service = "instagram"
|
||||
context_object_name_singular = "Instagram Account"
|
||||
context_object_name = "Instagram Accounts"
|
||||
list_url_name = "instagram_accounts"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
self.extra_context = self._service_context(
|
||||
service="instagram",
|
||||
label="Instagram",
|
||||
add_url_name="instagram_account_add",
|
||||
show_contact_actions=False,
|
||||
)
|
||||
return self._normalize_accounts(transport.list_accounts("instagram"))
|
||||
|
||||
|
||||
class InstagramAccountAdd(SignalAccountAdd):
|
||||
service = "instagram"
|
||||
detail_url_name = "instagram_account_add"
|
||||
@@ -2,11 +2,11 @@ from asgiref.sync import async_to_sync
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone as dj_timezone
|
||||
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from core.clients import signalapi
|
||||
from core.forms import QueueForm
|
||||
from core.models import Message, QueuedMessage
|
||||
from core.util import logs
|
||||
@@ -28,22 +28,18 @@ class AcceptMessageAPI(LoginRequiredMixin, APIView):
|
||||
except QueuedMessage.DoesNotExist:
|
||||
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if queued.session.identifier.service != "signal":
|
||||
log.warning(
|
||||
"Queue accept failed: unsupported service '%s' for queued message %s",
|
||||
queued.session.identifier.service,
|
||||
queued.id,
|
||||
)
|
||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ts = async_to_sync(signalapi.send_message_raw)(
|
||||
queued.session.identifier.identifier,
|
||||
ts = async_to_sync(queued.session.identifier.send)(
|
||||
queued.text or "",
|
||||
[],
|
||||
)
|
||||
if not ts:
|
||||
log.error("Queue accept send failed for queued message %s", queued.id)
|
||||
return HttpResponse(status=status.HTTP_502_BAD_GATEWAY)
|
||||
sent_ts = (
|
||||
int(ts)
|
||||
if (ts is not None and not isinstance(ts, bool))
|
||||
else int(dj_timezone.now().timestamp() * 1000)
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
Message.objects.create(
|
||||
@@ -51,7 +47,9 @@ class AcceptMessageAPI(LoginRequiredMixin, APIView):
|
||||
session=queued.session,
|
||||
custom_author=queued.custom_author or "BOT",
|
||||
text=queued.text,
|
||||
ts=ts,
|
||||
ts=sent_ts,
|
||||
delivered_ts=sent_ts,
|
||||
read_source_service=queued.session.identifier.service,
|
||||
)
|
||||
queued.delete()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import base64
|
||||
|
||||
import orjson
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
from mixins.views import ObjectList, ObjectRead
|
||||
|
||||
from core.models import Chat
|
||||
from core.clients import transport
|
||||
from core.models import Chat, PersonIdentifier
|
||||
from core.views.manage.permissions import SuperUserRequiredMixin
|
||||
|
||||
|
||||
@@ -18,13 +19,25 @@ class CustomObjectRead(ObjectRead):
|
||||
|
||||
class Signal(SuperUserRequiredMixin, View):
|
||||
template_name = "pages/signal.html"
|
||||
service = "signal"
|
||||
page_title = "Signal"
|
||||
accounts_url_name = "signal_accounts"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name)
|
||||
return render(
|
||||
request,
|
||||
self.template_name,
|
||||
{
|
||||
"service": self.service,
|
||||
"service_label": self.page_title,
|
||||
"accounts_url_name": self.accounts_url_name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SignalAccounts(SuperUserRequiredMixin, ObjectList):
|
||||
list_template = "partials/signal-accounts.html"
|
||||
service = "signal"
|
||||
|
||||
context_object_name_singular = "Signal Account"
|
||||
context_object_name = "Signal Accounts"
|
||||
@@ -32,13 +45,44 @@ class SignalAccounts(SuperUserRequiredMixin, ObjectList):
|
||||
list_url_name = "signal_accounts"
|
||||
list_url_args = ["type"]
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
# url = signal:8080/v1/accounts
|
||||
url = f"http://signal:8080/v1/accounts"
|
||||
response = requests.get(url)
|
||||
accounts = orjson.loads(response.text)
|
||||
def _normalize_accounts(self, rows):
|
||||
out = []
|
||||
for item in rows or []:
|
||||
if isinstance(item, dict):
|
||||
value = (
|
||||
item.get("number")
|
||||
or item.get("id")
|
||||
or item.get("jid")
|
||||
or item.get("account")
|
||||
)
|
||||
if value:
|
||||
out.append(str(value))
|
||||
elif item:
|
||||
out.append(str(item))
|
||||
return out
|
||||
|
||||
return accounts
|
||||
def _service_context(self, service, label, add_url_name, show_contact_actions):
|
||||
return {
|
||||
"service": service,
|
||||
"service_label": label,
|
||||
"account_add_url_name": add_url_name,
|
||||
"show_contact_actions": show_contact_actions,
|
||||
"endpoint_base": str(
|
||||
getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080")
|
||||
).rstrip("/")
|
||||
if service == "signal"
|
||||
else "",
|
||||
"service_warning": transport.get_service_warning(service),
|
||||
}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
self.extra_context = self._service_context(
|
||||
service="signal",
|
||||
label="Signal",
|
||||
add_url_name="signal_account_add",
|
||||
show_contact_actions=True,
|
||||
)
|
||||
return self._normalize_accounts(transport.list_accounts("signal"))
|
||||
|
||||
|
||||
class SignalContactsList(SuperUserRequiredMixin, ObjectList):
|
||||
@@ -55,13 +99,16 @@ class SignalContactsList(SuperUserRequiredMixin, ObjectList):
|
||||
# /v1/configuration/{number}/settings
|
||||
# /v1/identities/{number}
|
||||
# /v1/contacts/{number}
|
||||
# response = requests.get(f"http://signal:8080/v1/configuration/{self.kwargs['pk']}/settings")
|
||||
# response = requests.get(
|
||||
# f"http://signal:8080/v1/configuration/{self.kwargs['pk']}/settings"
|
||||
# )
|
||||
# config = orjson.loads(response.text)
|
||||
|
||||
response = requests.get(f"http://signal:8080/v1/identities/{self.kwargs['pk']}")
|
||||
base = getattr(settings, "SIGNAL_HTTP_URL", "http://signal:8080").rstrip("/")
|
||||
response = requests.get(f"{base}/v1/identities/{self.kwargs['pk']}")
|
||||
identities = orjson.loads(response.text)
|
||||
|
||||
response = requests.get(f"http://signal:8080/v1/contacts/{self.kwargs['pk']}")
|
||||
response = requests.get(f"{base}/v1/contacts/{self.kwargs['pk']}")
|
||||
contacts = orjson.loads(response.text)
|
||||
|
||||
# add identities to contacts
|
||||
@@ -90,8 +137,59 @@ class SignalChatsList(SuperUserRequiredMixin, ObjectList):
|
||||
|
||||
def get_queryset(self, *args, **kwargs):
|
||||
pk = self.kwargs.get("pk", "")
|
||||
object_list = Chat.objects.filter(account=pk)
|
||||
return object_list
|
||||
chats = list(Chat.objects.filter(account=pk))
|
||||
rows = []
|
||||
for chat in chats:
|
||||
identifier_candidates = [
|
||||
str(chat.source_uuid or "").strip(),
|
||||
str(chat.source_number or "").strip(),
|
||||
]
|
||||
identifier_candidates = [value for value in identifier_candidates if value]
|
||||
person_identifier = None
|
||||
if identifier_candidates:
|
||||
person_identifier = (
|
||||
PersonIdentifier.objects.filter(
|
||||
user=self.request.user,
|
||||
service="signal",
|
||||
identifier__in=identifier_candidates,
|
||||
)
|
||||
.select_related("person")
|
||||
.first()
|
||||
)
|
||||
|
||||
identifier_value = (
|
||||
person_identifier.identifier if person_identifier else ""
|
||||
) or (chat.source_uuid or chat.source_number or "")
|
||||
service = "signal"
|
||||
compose_page_url = ""
|
||||
compose_widget_url = ""
|
||||
if identifier_value:
|
||||
query = f"service={service}&identifier={identifier_value}"
|
||||
if person_identifier:
|
||||
query += f"&person={person_identifier.person_id}"
|
||||
compose_page_url = f"{reverse('compose_page')}?{query}"
|
||||
compose_widget_url = f"{reverse('compose_widget')}?{query}"
|
||||
if person_identifier:
|
||||
ai_url = (
|
||||
f"{reverse('ai_workspace')}?person={person_identifier.person_id}"
|
||||
)
|
||||
else:
|
||||
ai_url = reverse("ai_workspace")
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"chat": chat,
|
||||
"compose_page_url": compose_page_url,
|
||||
"compose_widget_url": compose_widget_url,
|
||||
"ai_url": ai_url,
|
||||
"person_name": (
|
||||
person_identifier.person.name if person_identifier else ""
|
||||
),
|
||||
"manual_icon_class": "fa-solid fa-paper-plane",
|
||||
"can_compose": bool(compose_page_url),
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
class SignalMessagesList(SuperUserRequiredMixin, ObjectList):
|
||||
@@ -100,6 +198,7 @@ class SignalMessagesList(SuperUserRequiredMixin, ObjectList):
|
||||
|
||||
class SignalAccountAdd(SuperUserRequiredMixin, CustomObjectRead):
|
||||
detail_template = "partials/signal-account-add.html"
|
||||
service = "signal"
|
||||
|
||||
context_object_name_singular = "Add Account"
|
||||
context_object_name = "Add Account"
|
||||
@@ -112,9 +211,7 @@ class SignalAccountAdd(SuperUserRequiredMixin, CustomObjectRead):
|
||||
def get_object(self, **kwargs):
|
||||
form_args = self.request.POST.dict()
|
||||
device_name = form_args["device"]
|
||||
url = f"http://signal:8080/v1/qrcodelink?device_name={device_name}"
|
||||
response = requests.get(url)
|
||||
image_bytes = response.content
|
||||
base64_image = base64.b64encode(image_bytes).decode("utf-8")
|
||||
image_bytes = transport.get_link_qr(self.service, device_name)
|
||||
base64_image = transport.image_bytes_to_base64(image_bytes)
|
||||
|
||||
return base64_image
|
||||
|
||||
29
core/views/whatsapp.py
Normal file
29
core/views/whatsapp.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from core.clients import transport
|
||||
from core.views.signal import Signal, SignalAccountAdd, SignalAccounts
|
||||
|
||||
|
||||
class WhatsApp(Signal):
|
||||
service = "whatsapp"
|
||||
page_title = "WhatsApp"
|
||||
accounts_url_name = "whatsapp_accounts"
|
||||
|
||||
|
||||
class WhatsAppAccounts(SignalAccounts):
|
||||
service = "whatsapp"
|
||||
context_object_name_singular = "WhatsApp Account"
|
||||
context_object_name = "WhatsApp Accounts"
|
||||
list_url_name = "whatsapp_accounts"
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
self.extra_context = self._service_context(
|
||||
service="whatsapp",
|
||||
label="WhatsApp",
|
||||
add_url_name="whatsapp_account_add",
|
||||
show_contact_actions=False,
|
||||
)
|
||||
return self._normalize_accounts(transport.list_accounts("whatsapp"))
|
||||
|
||||
|
||||
class WhatsAppAccountAdd(SignalAccountAdd):
|
||||
service = "whatsapp"
|
||||
detail_url_name = "whatsapp_account_add"
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user