Ctrl K
ComunidadGuías…

group-translate/README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Group Auto-Translation

> Auto-translates group messages between participants' languages via a LibreTranslate backend, controlled
> in-chat with `/tr` commands — so a mixed-language group reads in everyone's own language.

![type: extension](https://img.shields.io/badge/type-extension-blue.svg)
![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)
![built for OpenWA](https://img.shields.io/badge/OpenWA-%E2%89%A5%200.8.0-25D366.svg)
[![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Frmyndharis%2FOpenWA-plugins%2Fbadges%2Fdownloads%2Fgroup-translate.json)](https://github.com/rmyndharis/OpenWA-plugins/releases?q=group-translate)

## Details

<!-- BEGIN DETAILS (generated by scripts/catalog.mjs — do not edit by hand) -->
| Field | Value |
| ----- | ----- |
| **Identifier** | `group-translate` |
| **Version** | 1.3.17 |
| **Released** | 2026-11-10 |
| **Status** | stable |
| **Author** | Yudhi Armyndharis |
| **License** | MIT |
| **Type** | `extension` |
| **Requires OpenWA** | ≥ 0.8.0 (tested 0.30.0) |
| **Keywords** | translation, libretranslate, i18n, groups, whatsapp, openwa |
| **Repository** | [OpenWA-plugins/group-translate](https://github.com/rmyndharis/OpenWA-plugins/tree/main/group-translate) |
<!-- END DETAILS -->

## Features

- **Per-participant languages** — learns each member's language from what they type (or pin it with
  `/tr setlang`), then posts a combined reply translating a message into the other languages in the group.
- **In-chat control** — manage everything with `/tr` commands; no dashboard round-trip for day-to-day use.
- **Admin-gated** — state-changing commands require a group admin (resolved via `ctx.engine.getGroupInfo`).
  Authors that arrive under WhatsApp's `@lid` privacy id are resolved to their phone identity, so an
  admin is recognized whichever dialect the host delivers. Disabled until an admin runs `/tr on`.
- **Resilient backend** — a per-call timeout plus a circuit breaker back off a slow or flaky LibreTranslate
  instance instead of stalling the chat.
- **Guarded outbound HTTP** — all calls go through the host's SSRF-guarded `ctx.net.fetch`; the backend host
  must be allow-listed (see **Security**). The API key is stored as a redacted secret.

## Commands

Default prefix `/tr` (configurable). `/tr help` is open to anyone; every other command is admin-only.

| Command | Who | Effect |
| ------- | --- | ------ |
| `/tr help` | anyone | Show the command list (answered at most once a minute per group) |
| `/tr status` | admin | Show whether translation is on + per-participant languages |
| `/tr on` · `/tr off` | admin | Enable / disable translation in this group |
| `/tr setlang <code> [@user]` | admin | Pin a language (e.g. `/tr setlang id @member`) |
| `/tr auto [@user]` | admin | Go back to auto-learning a participant's language |
| `/tr ignore` · `/tr unignore [@user]` | admin | Skip / resume translating a participant |
| `/tr grant` · `/tr revoke [@user]` | admin | Delegate / remove control to a non-admin participant |

## Setup

1. **Run a LibreTranslate instance.** The plugin calls its `/translate` endpoint over `ctx.net.fetch`.
   Set `libretranslateUrl` to it — no repackaging needed for either supported shape:
   - **loopback, any port** (`http://localhost:7001`, `http://127.0.0.1:5000`) — allowed as shipped
   - **any other host, over https** (`https://translate.example.com`) — allowed via the manifest's
     `allowConfigHosts`, which admits an operator-configured host only over https

   A plain-http backend on a non-loopback host (e.g. `http://libretranslate:5000` inside a Docker
   network) is the one case still outside that: add its `host:port` to the manifest `net.allow` and
   repackage, or put it behind https.
2. **Allow the address on the OpenWA host if it is loopback or private.** The gateway's SSRF guard
   blocks those by default, so set the environment variable **`SSRF_ALLOWED_HOSTS`** on the OpenWA host
   itself — not in this plugin's config — e.g. `SSRF_ALLOWED_HOSTS=localhost,127.0.0.1`. The default
   `libretranslateUrl` *is* a loopback address, so out of the box the plugin cannot reach its backend
   until this is done (see [Security](#security)).
3. Have OpenWA **≥ 0.8.0** running with a logged-in WhatsApp session for the group(s) you want to translate.
4. Install and enable the plugin (see [Install](#install)), then have a group admin run `/tr on`.
   Enabling the plugin is silent: it says nothing in any group until someone addresses it with a
   `/tr` command. It never translates until an admin has run `/tr on` in that specific group, and it
   never introduces itself unless you turn on `announceInGroups` — which posts into *every* group the
   account is in, so leave it off unless the account is a dedicated bot number.

## Install

```bash
# Build from source (released .zip files are on the Releases page):
node package.mjs group-translate    # produces group-translate.zip at the repo root

curl -X POST "https://your-openwa-host/api/plugins/install" \
  -H "X-API-Key: <ADMIN_API_KEY>" -F "file=@group-translate.zip"

curl -X PUT "https://your-openwa-host/api/plugins/group-translate/config" \
  -H "X-API-Key: <ADMIN_API_KEY>" -H "Content-Type: application/json" \
  -d '{ "config": { "libretranslateUrl": "https://translate.example.com", "timeoutMs": 4000 } }'

curl -X POST "https://your-openwa-host/api/plugins/group-translate/enable" \
  -H "X-API-Key: <ADMIN_API_KEY>"
```

Then, in the group, an admin runs `/tr on`. Or install the packaged `.zip` from
[Releases](https://github.com/rmyndharis/OpenWA-plugins/releases) via the dashboard **Plugins → Install**.

## Configuration

| Key | Required | Default | Description |
| --- | -------- | ------- | ----------- |
| `libretranslateUrl` | yes | `http://localhost:7001` | Base URL of your LibreTranslate instance. Loopback on any port works as shipped; any other host is admitted over **https** only. A loopback/private address **also** requires `SSRF_ALLOWED_HOSTS` on the gateway (see [Security](#security)) |
| `libretranslateApiKey` | no | — | Secret API key, if your instance requires one (redacted on read) |
| `timeoutMs` | no | `4000` | Per-call timeout; keep ≤ the host hook budget (5000 ms) |
| `commandPrefix` | no | `/tr` | The in-chat command prefix |
| `minLength` | no | `2` | Minimum message length to translate |
| `maxLength` | no | `2000` | Maximum message length to translate |
| `denyReply` | no | `false` | Reply "admins only" when a non-admin runs a restricted command |
| `announceInGroups` | no | `false` | Post an introduction into a group the first time the plugin sees a message there, once per group. **Off by default** — it fires per group, so turning it on announces the bot into every group the WhatsApp account belongs to. `/tr help` gives anyone the same text on request |

## Compatibility

Checked against the OpenWA v0.30.0 worker runtime with isolated services. This covers plugin lifecycle, hooks or ingress dispatch, capability calls and per-session configuration. Live WhatsApp sessions, provider authentication and dashboard rendering require separate deployment checks.

Targets OpenWA **≥ 0.8.0**, the release that introduced `net.allowConfigHosts`. Outbound HTTP itself
uses the v0.7 `ctx.net.fetch` capability, but on a 0.7.x host only the shipped loopback entries are
reachable, so any other `libretranslateUrl` is refused and nothing is translated. Declares
`messages:send`, `engine:read` (for admin checks), `net:fetch`, and `storage:use` (each group's
settings and learned participant languages).

Shared contact cards are never translated: from OpenWA 0.23.2 a card carries its vCard in the message
body, which would otherwise send a third party's name and number to your translation backend, post a
machine-translated card into the group, and let language detection pin the sender's language from
vCard field names. Poll questions are translated normally.

Catalog orders and shared product cards are never translated either. From OpenWA 0.23.5 they arrive
typed `order` and `product` on both engines, and on Baileys they now carry the order note or the
product title as the body. Earlier hosts deliver both as `unknown`, which the plugin cannot tell apart
from other messages. On Baileys a share of a whole catalog still arrives as `unknown` with the catalog
title as its body, and is translated like any other text.

Messages delivered late are not acted on. From OpenWA 0.23.6 a Baileys session delivers, after it
reconnects, the messages WhatsApp queued while it was disconnected. A message that reaches the plugin
more than 5 minutes after it was sent is not translated, and a `/tr` command that late is not run,
though it is still kept from other bots: send it again once the session is back. The check compares
the message's WhatsApp send time with the gateway's clock, so keep that clock in sync.

Forwarded messages are translated into the group's languages, including the forwarder's own, without
teaching or switching the forwarder's learned language or running a `/tr` command. From OpenWA 0.29.1
both engines flag a forwarded message with `isForwarded`.

### Per-session config

**Supported, with a caveat.** Every config field may be overridden per WhatsApp session via the
dashboard; an override that changes a coordinator-affecting field (e.g. `libretranslateUrl`,
`libretranslateApiKey`, `timeoutMs`, `commandPrefix`, `minLength`, `maxLength`, `denyReply`,
`announceInGroups`) takes
effect on the next inbound message. The plugin uses config-signature caching: the coordinator (and the
LibreTranslate client's circuit breaker) is reused across messages with the same resolved config, and
rebuilt only when the signature changes.

Commands for one group remain serialized across coordinator rebuilds, so an older command cannot
overwrite a later command after a configuration update. Different groups can still run concurrently.

**Caveat for multi-backend deployments:** when two sessions point at *different* LibreTranslate
instances, they share one coordinator slot and clobber each other's circuit-breaker state on every
message alternation. OpenWA installs a plugin once per gateway (a second install of the same id is
refused with `409`) and gives separate instances only to plugins with ingress routes, so this plugin has
no per-session instance: keep one LibreTranslate for all sessions, or run one OpenWA gateway per backend.

**Host overrides and the API key.** From OpenWA 0.26.0 a per-session override that points
`libretranslateUrl` at another origin than the base config does not inherit the stored
`libretranslateApiKey`. A LibreTranslate that needs a key must get it in that override, otherwise
requests go out without one.

## Security

Outbound translate calls go **exclusively** through the host's SSRF-guarded `ctx.net.fetch`; there is no
raw socket in the plugin. Two independent controls apply and **both** must pass:

1. **Manifest allowlist (deny by default).** The plugin ships allowing `localhost` and `127.0.0.1` on
   any port, plus — via `net.allowConfigHosts` — the host of whatever `libretranslateUrl` you configure,
   **provided that URL is https**. The host deliberately ignores a non-https config value here, so a
   plain-http backend on a non-loopback host is the one shape that still needs `net.allow` edited and the
   plugin repackaged — e.g. `"net": { "allow": ["libretranslate:7001"] }`.
2. **Host SSRF guard.** The host additionally blocks private / loopback addresses (`127.0.0.0/8`, `::1`,
   RFC-1918, link-local) at connect time, **regardless of `net.allow`**, with no plugin opt-out.

Because of (2), a **self-hosted LibreTranslate on `localhost` / `127.0.0.1` / a private LAN or Docker
host** — including this plugin's default `http://localhost:7001` — is blocked unless the operator adds
that hostname to the gateway environment variable **`SSRF_ALLOWED_HOSTS`** (e.g.
`SSRF_ALLOWED_HOSTS=localhost,127.0.0.1`, or the Docker service name). Without it, translate calls fail at
connect: the plugin fails open (messages pass through **untranslated**) and the circuit opens after a few
failures. A **public** LibreTranslate host passes (2) unconditionally and needs no `SSRF_ALLOWED_HOSTS`.

The API key travels only in the request body to the allow-listed host and is stored as a redacted secret.
Commands that change group state are admin-gated via `ctx.engine.getGroupInfo`. When WhatsApp delivers the
author under its `@lid` privacy id, the plugin resolves that to the author's phone identity through
`ctx.engine.getContactById` before comparing — resolution widens *recognition*, never permission: an
author who resolves to someone outside the admin and delegated-controller lists is still refused. If
WhatsApp does not return the group's admin list, nobody counts as an admin for that command (delegated
controllers still do), and `denyReply` decides whether the sender is told. A `/tr` command is claimed
as soon as it is recognized, so no other plugin answers it even when the lookup is slow. The
per-call timeout (≤ the host hook budget) and circuit breaker keep a slow backend from stalling the host.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md).

## License

[MIT](../LICENSE) © Yudhi Armyndharis & OpenWA Contributors.