Fix Signal receiving

This commit is contained in:
2026-02-22 18:05:26 +00:00
parent 0f36b2dde7
commit 4521755344
7 changed files with 227 additions and 12 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.git
.podman
artifacts
.container-home
db.sqlite3
docker/data
signal-cli-config
__pycache__
*.pyc

1
.gitignore vendored
View File

@@ -166,3 +166,4 @@ oom
node_modules/ node_modules/
.podman/ .podman/
.beads/ .beads/
.sisyphus/

View File

@@ -4,7 +4,7 @@ run:
bash $(QUADLET_MGR) up bash $(QUADLET_MGR) up
build: build:
docker-compose --env-file=stack.env build OPERATION=uwsgi podman build --build-arg OPERATION=uwsgi -t localhost/xf/gia:prod -f Dockerfile .
stop: stop:
bash $(QUADLET_MGR) down bash $(QUADLET_MGR) down

View File

@@ -220,6 +220,16 @@ urlpatterns = [
compose.ComposeContactMatch.as_view(), compose.ComposeContactMatch.as_view(),
name="compose_contact_match", name="compose_contact_match",
), ),
path(
"compose/contacts/create/",
compose.ComposeContactCreate.as_view(),
name="compose_contact_create",
),
path(
"compose/contacts/create-all/",
compose.ComposeContactCreateAll.as_view(),
name="compose_contact_create_all",
),
# AIs # AIs
path( path(
"ai/workspace/", "ai/workspace/",

View File

@@ -13,7 +13,18 @@
</div> </div>
</div> </div>
<div class="level-right"> <div class="level-right">
<a class="button is-light" href="{% url 'compose_workspace' %}"> <form id="create-all-form" method="post" action="{% url 'compose_contact_create_all' %}">
{% csrf_token %}
<button
type="submit"
class="button is-info is-light js-create-all"
id="create-all-btn"
title="Create persons for all unlinked contacts with detected names">
<span class="icon is-small"><i class="fa-solid fa-user-plus"></i></span>
<span>Create All</span>
</button>
</form>
<a class="button is-light ml-2" href="{% url 'compose_workspace' %}">
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span> <span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
<span>Manual Workspace</span> <span>Manual Workspace</span>
</a> </a>
@@ -187,8 +198,27 @@
</td> </td>
<td data-discovered-col="3" class="discovered-col-3"><code>{{ row.identifier }}</code></td> <td data-discovered-col="3" class="discovered-col-3"><code>{{ row.identifier }}</code></td>
<td data-discovered-col="4" class="discovered-col-4"> <td data-discovered-col="4" class="discovered-col-4">
{% if not row.linked_person and row.suggestions %} {% if not row.linked_person %}
<div class="buttons are-small"> <div class="buttons are-small">
{% if row.detected_name %}
<form method="post" action="{% url 'compose_contact_create' %}" style="display: inline-flex;">
{% csrf_token %}
<input type="hidden" name="service" value="{{ row.service }}">
<input type="hidden" name="identifier" value="{{ row.identifier }}">
<input type="hidden" name="person_name" value="{{ row.detected_name }}">
<button
type="submit"
class="button is-small is-info is-light js-create-contact"
data-service="{{ row.service }}"
data-identifier="{{ row.identifier }}"
data-name="{{ row.detected_name }}"
title="Create new person from detected name">
<span class="icon is-small"><i class="fa-solid fa-plus"></i></span>
<span>Create</span>
</button>
</form>
{% endif %}
{% if row.suggestions %}
{% for suggestion in row.suggestions %} {% for suggestion in row.suggestions %}
<form method="post" style="display: inline-flex;"> <form method="post" style="display: inline-flex;">
{% csrf_token %} {% csrf_token %}
@@ -204,6 +234,7 @@
</button> </button>
</form> </form>
{% endfor %} {% endfor %}
{% endif %}
</div> </div>
{% else %} {% else %}
<span class="has-text-grey">-</span> <span class="has-text-grey">-</span>
@@ -436,6 +467,41 @@
if (ev.key === "Escape") hidePopover(); if (ev.key === "Escape") hidePopover();
}); });
document.querySelectorAll(".js-create-contact").forEach((btn) => {
btn.addEventListener("click", function (ev) {
const name = this.dataset.name || "this contact";
const identifier = this.dataset.identifier || "";
const service = this.dataset.service || "";
const confirmMsg = "Create new person '" + name + "' and link to " + identifier + " (" + service + ")?";
if (!confirm(confirmMsg)) {
ev.preventDefault();
return false;
}
});
});
const createAllBtn = document.getElementById("create-all-btn");
if (createAllBtn) {
createAllBtn.addEventListener("click", function (ev) {
const table = document.getElementById("discovered-contacts-table");
if (!table) return;
const unlinkedWithName = table.querySelectorAll("tbody tr").filter((row) => {
const statusCell = row.querySelector('[data-discovered-col="5"]');
return statusCell && statusCell.textContent.includes("unlinked");
}).length;
if (unlinkedWithName === 0) {
ev.preventDefault();
alert("No unlinked contacts with detected names to create.");
return false;
}
const confirmMsg = "Create " + unlinkedWithName + " new contact" + (unlinkedWithName > 1 ? "s" : "") + " from detected names?";
if (!confirm(confirmMsg)) {
ev.preventDefault();
return false;
}
});
}
applyFilters(); applyFilters();
applyColumns(); applyColumns();
})(); })();

View File

@@ -2361,6 +2361,131 @@ class ComposeContactMatch(LoginRequiredMixin, View):
) )
class ComposeContactCreate(LoginRequiredMixin, View):
template_name = "pages/compose-contact-match.html"
def post(self, request):
service = _default_service(request.POST.get("service"))
identifier = str(request.POST.get("identifier") or "").strip()
person_name = str(request.POST.get("person_name") or "").strip()
if not identifier:
return render(
request,
self.template_name,
self._context(request, "Identifier is required.", "warning"),
)
if not person_name:
return render(
request,
self.template_name,
self._context(request, "Person name is required.", "warning"),
)
existing = PersonIdentifier.objects.filter(
user=request.user,
service=service,
identifier=identifier,
).first()
if existing and existing.person:
return render(
request,
self.template_name,
self._context(
request,
f"{identifier} ({service}) is already linked to {existing.person.name}.",
"warning",
),
)
person = Person.objects.create(user=request.user, name=person_name)
PersonIdentifier.objects.create(
user=request.user,
person=person,
service=service,
identifier=identifier,
)
message = f"Created person '{person_name}' and linked {identifier} ({service})."
return render(
request,
self.template_name,
self._context(request, message, "success"),
)
class ComposeContactCreateAll(LoginRequiredMixin, View):
template_name = "pages/compose-contact-match.html"
def post(self, request):
candidates = _manual_contact_rows(request.user)
created_count = 0
skipped_count = 0
errors = []
for candidate in candidates:
if candidate.get("linked_person"):
skipped_count += 1
continue
detected_name = candidate.get("detected_name", "")
if not detected_name:
skipped_count += 1
continue
service = candidate.get("service", "")
identifier = candidate.get("identifier", "")
if not service or not identifier:
skipped_count += 1
continue
existing = PersonIdentifier.objects.filter(
user=request.user,
service=service,
identifier=identifier,
).first()
if existing and existing.person:
skipped_count += 1
continue
try:
person = Person.objects.create(user=request.user, name=detected_name)
PersonIdentifier.objects.create(
user=request.user,
person=person,
service=service,
identifier=identifier,
)
created_count += 1
except Exception as e:
errors.append(f"{identifier} ({service}): {str(e)}")
skipped_count += 1
if errors:
message = f"Created {created_count} contacts. Errors: {'; '.join(errors[:5])}"
level = "warning"
elif created_count > 0:
message = f"Created {created_count} new contact{'s' if created_count != 1 else ''}. Skipped {skipped_count}."
level = "success"
else:
message = f"No new contacts to create. {skipped_count} already linked or without names."
level = "info"
return render(
request,
self.template_name,
self._context(request, message, level),
)
class ComposePage(LoginRequiredMixin, View): class ComposePage(LoginRequiredMixin, View):
template_name = "pages/compose.html" template_name = "pages/compose.html"

View File

@@ -35,7 +35,11 @@ ensure_dirs() {
mkdir -p "$REDIS_DATA_DIR" "$WHATSAPP_DATA_DIR" "$VRUN_DIR" "$ROOT_DIR/signal-cli-config" mkdir -p "$REDIS_DATA_DIR" "$WHATSAPP_DATA_DIR" "$VRUN_DIR" "$ROOT_DIR/signal-cli-config"
# Container runs as uid 1000 (xf); rootless Podman remaps uids so plain # Container runs as uid 1000 (xf); rootless Podman remaps uids so plain
# chown won't work — podman unshare translates to the correct host uid. # chown won't work — podman unshare translates to the correct host uid.
if [[ -n "${QUADLET_SKIP_UNSHARE:-}" ]] || [[ -n "${CONTAINER_HOST:-}" ]] || [[ -n "${PODMAN_HOST:-}" ]] || [[ -n "${DOCKER_HOST:-}" ]]; then
echo "Skipping podman unshare chown (QUADLET_SKIP_UNSHARE or remote host detected)"
else
podman unshare chown 1000:1000 "$WHATSAPP_DATA_DIR" 2>/dev/null || true podman unshare chown 1000:1000 "$WHATSAPP_DATA_DIR" 2>/dev/null || true
fi
} }
rm_if_exists() { rm_if_exists() {