Skip to content

fix(group-translate): recognize admins across the lid/phone split, and let the backend be configured - #61

Merged
rmyndharis merged 1 commit into
mainfrom
fix/group-translate-lid-admin-and-configurable-backend
Aug 1, 2026
Merged

fix(group-translate): recognize admins across the lid/phone split, and let the backend be configured#61
rmyndharis merged 1 commit into
mainfrom
fix/group-translate-lid-admin-and-configurable-backend

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Two independent defects, both confirmed against a live OpenWA 0.12.1 host.

1. Group admins were refused their own commands

WhatsApp delivers the author of a group message under a privacy id (…@lid), while getGroupInfo returns the participant list under phone ids (…@c.us). Those two carry unrelated user numbers, so no normalization relates them — widEquals says as much in its own docblock. Every admin failed the comparison and /tr on, /tr setlang, /tr grant and the rest were silently denied.

Only the group's owner was ever recognized, because WhatsApp reports that one in the author's own dialect. And in a group created by the bot's own number, even that did not help: the owner's messages are fromMe and never reach the coordinator. The result was a group with no working administrator at all.

Observed live — a member the group API reported as 6281770008896@c.us admin=True was refused /tr on, while /tr setlang from the same person echoed their identity back as 148004841455867@lid.

Change. When the direct comparison finds nothing, the author is resolved to its canonical identity and compared again. getContactById is the bridge: handed a @lid, the engine looks the contact up in its own store and returns it keyed by its canonical <phone>@c.us id.

getContactById("148004841455867@lid")
  -> {"id":"6281770008896@c.us", "number":"148004841455867", ...}

(number is not the bridge — for a lid contact that is the lid's own user part, which is exactly the value that cannot be compared. id is.)

The same identity is what /tr grant stores for a controller named by phone number, so delegation starts working too — it was broken in precisely the same way.

Three properties this deliberately keeps:

  • Recognition widens, permission does not. An author who resolves to someone outside both the admin and delegated-controller lists is still refused.
  • Unresolvable denies. An unknown contact, a slow engine or a torn-down session all resolve to null and fall back to the direct comparison, never open.
  • It costs nothing on the happy path. The extra engine round-trip is spent only on a decision that is otherwise a denial, and memoized in a bounded table afterwards — a lid/phone binding does not change.

2. The LibreTranslate backend could not be configured without repackaging

net.allow was pinned to localhost:7001, so pointing the plugin at any other instance meant editing the manifest and rebuilding the .zip — while the free-form libretranslateUrl config field implied otherwise, and every other value failed silently at the plugin's own allowlist. For a plugin installed from the catalog that is not a workable setup path.

It now allows loopback on any port as shipped, and admits whatever host the operator configures via net.allowConfigHosts — which the gateway accepts over https only. That is the shape voice-transcription already uses for its backend, so the two now behave the same way.

The one case still outside it is a plain-http backend on a non-loopback host (a Docker service name). That still needs the manifest edited, because the gateway deliberately ignores a non-https config-supplied host; the README says so rather than leaving it to be discovered.

A loopback or private address continues to require SSRF_ALLOWED_HOSTS on the OpenWA host — a separate control the plugin cannot influence, documented in Setup and Security.

Tests

Six new cases: an admin recognized behind a @lid author; a delegated controller named by phone recognized the same way; a genuine non-admin still refused after resolution; no round-trip spent when the direct comparison already authorizes; the resolution memoized across commands; and an unresolvable author denying rather than failing open.

Removing the resolution step fails three of them. Full suite 535/535, typecheck and catalog check clean.

…d let the backend be configured

WhatsApp delivers a group message's author under a privacy id (`…@lid`) while
getGroupInfo lists participants under phone ids (`…@c.us`). The two carry
unrelated user numbers, so widEquals could never relate them: every admin
failed the check and only the group `owner` — which WhatsApp reports in the
author's own dialect — was recognized. In a group created by the bot's own
number even that failed, since the owner's messages are fromMe and never reach
the coordinator, leaving the group with no working administrator.

When the direct comparison finds nothing, the author is now resolved to its
canonical `@c.us` identity through getContactById and compared again. That is
the same identity `/tr grant` stores for a controller named by phone number, so
delegation starts working too. The call is spent only on what would otherwise
be a denial, and memoized afterwards — a lid/phone binding does not change.
Resolution widens recognition, never permission: an author who resolves to
someone outside both lists is still refused, and an unresolvable one denies.

Separately, `net.allow` pinned the backend to `localhost:7001`, so pointing the
plugin at any other LibreTranslate meant editing the manifest and rebuilding
the zip — while the free-form `libretranslateUrl` field implied otherwise and
silently failed. It now allows loopback on any port and, through
allowConfigHosts, whatever https host the operator configures, matching how
voice-transcription already handles its backend.
@rmyndharis
rmyndharis merged commit 678e9ab into main Aug 1, 2026
1 check passed
@rmyndharis
rmyndharis deleted the fix/group-translate-lid-admin-and-configurable-backend branch August 1, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant