Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ This repository provides:
| [`chat-flow`](./chat-flow) | Interactive, stateful auto-reply: a trigger word starts a greeting + numbered menu, replies traverse a configurable menu tree, and per-chat state expires after 15 minutes. | 1.1.1 | stable |
| [`chatwoot-adapter`](./chatwoot-adapter) | Two-way sync between a WhatsApp session and a Chatwoot inbox: relays WhatsApp messages (1:1 and groups, with media) into Chatwoot as an API-channel inbox, sends agent replies back to WhatsApp, and hands a chat over to a human agent — silencing other OpenWA bots — when an agent takes it in Chatwoot. First consumer of the OpenWA Integration SDK v1; runs sandboxed in the plugin worker. | 0.8.0 | stable |
| [`faq-bot`](./faq-bot) | Auto-replies to inbound WhatsApp messages from configurable FAQ keyword/regex rules. | 0.2.1 | stable |
| [`group-translate`](./group-translate) | Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled. | 1.1.1 | stable |
| [`group-translate`](./group-translate) | Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled. | 1.2.0 | stable |
| [`gsheets-logger`](./gsheets-logger) | Logs WhatsApp message events to a Google Sheet via a service account. | 0.3.2 | stable |
| [`http-action`](./http-action) | Triggers safe REST API requests from WhatsApp commands and renders JSON responses back to chat. | 0.2.1 | stable |
| [`supabase-otp-hook`](./supabase-otp-hook) | Deliver Supabase Auth phone OTPs over WhatsApp. | 0.3.0 | beta |
Expand Down
15 changes: 15 additions & 0 deletions group-translate/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ The version here always matches `manifest.json`'s `version`.

## [Unreleased]

## [1.2.0] — 2026-08-01

### Changed

- **This plugin no longer introduces itself in your groups unless you ask it to.** Until now, enabling
it made it post a "👋 Translation bot…" message into a group the first time it saw any message there.
That fired **per group**, so a single enable announced the bot into every group the WhatsApp account
belonged to — and the account running a plugin is usually somebody's own WhatsApp, not a dedicated
bot number. The introduction is now controlled by a new **Announce this bot in new groups** setting,
which is **off by default**, including for installs upgrading from an earlier version.

Nothing else about the plugin was reachable without addressing it first: translation still requires
an admin to run `/tr on`, and the same introduction text is still available to anyone on demand with
`/tr help`. If you want the old behaviour, turn the new setting on.

## [1.1.1] — 2026-08-01

No behaviour change. 1.1.0 has now been smoke-tested against a newer host, so the tested-version field
Expand Down
10 changes: 8 additions & 2 deletions group-translate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
| Field | Value |
| ----- | ----- |
| **Identifier** | `group-translate` |
| **Version** | 1.1.1 |
| **Version** | 1.2.0 |
| **Released** | 2026-08-01 |
| **Status** | stable |
| **Author** | Yudhi Armyndharis |
Expand Down Expand Up @@ -62,6 +62,10 @@ Default prefix `/tr` (configurable). Read-only commands are open; the rest are a
`SSRF_ALLOWED_HOSTS=localhost,127.0.0.1`.
2. Have OpenWA **≥ 0.7.0** running with a logged-in WhatsApp session for the group(s) you want to translate.
3. 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

Expand Down Expand Up @@ -94,6 +98,7 @@ Then, in the group, an admin runs `/tr on`. Or install the packaged `.zip` from
| `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

Expand All @@ -104,7 +109,8 @@ Targets OpenWA **≥ 0.7.0** — outbound HTTP uses the v0.7 `ctx.net.fetch` cap

**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`) takes
`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.
Expand Down
40 changes: 36 additions & 4 deletions group-translate/core/translation.coordinator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import assert from 'node:assert/strict';
import { TranslationCoordinator, CoordinatorOptions } from './translation.coordinator';
import { ChatGateway, ConfigStore, GroupState, InboundMessage, Translator, TranslationLogger } from './ports';

const OPTS: CoordinatorOptions = { prefix: '/tr', minLength: 2, maxLength: 2000, denyReply: false };
// The shipped defaults, so every test below runs against what an operator actually gets.
const OPTS: CoordinatorOptions = {
prefix: '/tr',
minLength: 2,
maxLength: 2000,
denyReply: false,
announceInGroups: false,
};

function freshState(over: Partial<GroupState> = {}): GroupState {
return {
Expand Down Expand Up @@ -150,12 +157,35 @@ describe('TranslationCoordinator', () => {
assert.equal(mocks.sendText.calls.length, 0);
});

test('announces once on first contact then stays dormant', async () => {
// Regression: enabling this plugin used to post an unsolicited introduction into a group the first
// time it saw any message there. Per group, so a single enable announced the bot into every group
// the account belonged to — on a personal WhatsApp that is every group the person is in. The
// introduction is now opt-in, and this asserts the DEFAULT, which is the value that shipped harm.
test('says nothing in a group it has never seen before (announcement off by default)', async () => {
const { store, gateway, translator, mocks } = makeDeps(freshState());
const c = new TranslationCoordinator(translator, store, gateway, OPTS);
const res = await c.handleMessage('s', msg());
assert.deepEqual(res, { swallow: false }, 'an ordinary group message is not claimed');
assert.equal(mocks.sendText.calls.length, 0, 'the plugin must not speak until it is addressed');
});

test('announces once per group when the operator opts in', async () => {
const state = freshState();
const { store, gateway, translator, saved, mocks } = makeDeps(state);
const c = new TranslationCoordinator(translator, store, gateway, { ...OPTS, announceInGroups: true });
await c.handleMessage('s', msg());
assert.equal(mocks.sendText.calls.length, 1);
assert.ok(mocks.save.calls.length > 0);
assert.equal(saved.at(-1)?.announced, true, 'the group is marked so the intro is not repeated');
});

// The intro is still reachable on demand — which is why suppressing it costs nothing.
test('replies with the help text when asked, whatever the announcement setting', async () => {
const { store, gateway, translator, mocks } = makeDeps(freshState());
const c = new TranslationCoordinator(translator, store, gateway, OPTS);
const res = await c.handleMessage('s', msg({ body: '/tr help' }));
assert.deepEqual(res, { swallow: true });
assert.equal(mocks.sendText.calls.length, 1);
assert.match(String(mocks.sendText.calls[0][2]), /Translation bot/);
});

test('activates only for an admin', async () => {
Expand Down Expand Up @@ -502,7 +532,9 @@ describe('TranslationCoordinator', () => {
detect: async () => ({ lang: 'en', confidence: 1 }), translate: async () => '',
languages: async () => ['en'], isHealthy: () => true,
};
const c = new TranslationCoordinator(translator, store, gateway, OPTS);
// Opted in deliberately: the announcement is this test's observable for the per-chat lock, and
// it is the one send that a load/save race could duplicate.
const c = new TranslationCoordinator(translator, store, gateway, { ...OPTS, announceInGroups: true });
await Promise.all([
c.handleMessage('s', msg({ id: 'm1', body: 'hello there' })),
c.handleMessage('s', msg({ id: 'm2', body: 'hello again' })),
Expand Down
9 changes: 8 additions & 1 deletion group-translate/core/translation.coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface CoordinatorOptions {
minLength: number;
maxLength: number;
denyReply: boolean;
/** Post the unsolicited introduction into a group the first time this plugin sees a message there.
* Off unless the operator turns it on: the account running this plugin is usually a person's own
* WhatsApp, and enabling the plugin would otherwise announce it into every group that account is in. */
announceInGroups: boolean;
}

const URL_OR_EMOJI_ONLY = /^(?:\s|\p{Emoji}|https?:\/\/\S+)+$/u;
Expand Down Expand Up @@ -71,7 +75,10 @@ export class TranslationCoordinator {
private async handleMessageLocked(sessionId: string, msg: InboundMessage): Promise<{ swallow: boolean }> {
const state = await this.store.load(sessionId, msg.chatId);

if (!state.announced) {
// Opt-in, and off by default. This is the only place the plugin speaks without being addressed,
// and it fires per GROUP, so simply enabling the plugin used to post an introduction into every
// group the account was in. `/tr help` gives anyone the same text on request.
if (this.opts.announceInGroups && !state.announced) {
await this.gateway.sendText(sessionId, msg.chatId, buildHelpText(this.opts.prefix));
state.announced = true;
await this.store.save(state);
Expand Down
4 changes: 4 additions & 0 deletions group-translate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class TranslationPlugin implements IPlugin {
readNumber(cfg, "minLength", 2),
readNumber(cfg, "maxLength", 2000),
readBool(cfg, "denyReply", false),
readBool(cfg, "announceInGroups", false),
]);
}

Expand All @@ -137,6 +138,9 @@ export class TranslationPlugin implements IPlugin {
minLength: readNumber(cfg, "minLength", 2),
maxLength: readNumber(cfg, "maxLength", 2000),
denyReply: readBool(cfg, "denyReply", false),
// Default false in the code as well as the manifest: an install that predates this field has no
// stored value, and the safe reading of a missing opt-in is "not opted in".
announceInGroups: readBool(cfg, "announceInGroups", false),
};
return new TranslationCoordinator(translator, store, gateway, opts, logger);
}
Expand Down
34 changes: 32 additions & 2 deletions group-translate/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "group-translate",
"name": "Group Auto-Translation",
"version": "1.1.1",
"version": "1.2.0",
"type": "extension",
"main": "dist/index.js",
"description": "Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled.",
Expand Down Expand Up @@ -83,6 +83,12 @@
"title": "Reply on denied commands",
"default": false,
"description": "If true, reply 'admins only' when a non-admin runs a restricted command."
},
"announceInGroups": {
"type": "boolean",
"title": "Announce this bot in new groups",
"default": false,
"description": "If true, the first time this plugin sees a message in a group it posts an introduction there, once per group. Off by default: the account running this plugin is normally a real person's WhatsApp, so turning this on announces the bot into every group that account belongs to. Anyone can get the same text on demand with the /tr help command."
}
}
},
Expand Down Expand Up @@ -111,6 +117,9 @@
},
"denyReply": {
"title": "Responder a comandos denegados"
},
"announceInGroups": {
"title": "Anunciar este bot en grupos nuevos"
}
}
},
Expand Down Expand Up @@ -138,6 +147,9 @@
},
"denyReply": {
"title": "Répondre aux commandes refusées"
},
"announceInGroups": {
"title": "Annoncer ce bot dans les nouveaux groupes"
}
}
},
Expand Down Expand Up @@ -165,6 +177,9 @@
},
"denyReply": {
"title": "Rispondi ai comandi negati"
},
"announceInGroups": {
"title": "Annunciare questo bot nei nuovi gruppi"
}
}
},
Expand Down Expand Up @@ -192,6 +207,9 @@
},
"denyReply": {
"title": "الرد على الأوامر المرفوضة"
},
"announceInGroups": {
"title": "الإعلان عن هذا البوت في المجموعات الجديدة"
}
}
},
Expand Down Expand Up @@ -219,6 +237,9 @@
},
"denyReply": {
"title": "השב על פקודות שנדחו"
},
"announceInGroups": {
"title": "להכריז על הבוט בקבוצות חדשות"
}
}
},
Expand Down Expand Up @@ -246,6 +267,9 @@
},
"denyReply": {
"title": "తిరస్కరించిన కమాండ్‌లకు జవాబివ్వు"
},
"announceInGroups": {
"title": "కొత్త గ్రూపుల్లో ఈ బాట్‌ను పరిచయం చేయి"
}
}
},
Expand Down Expand Up @@ -273,6 +297,9 @@
},
"denyReply": {
"title": "拒绝命令时回复"
},
"announceInGroups": {
"title": "在新群组中介绍此机器人"
}
}
},
Expand Down Expand Up @@ -300,8 +327,11 @@
},
"denyReply": {
"title": "拒絕指令時回覆"
},
"announceInGroups": {
"title": "喺新群組介紹呢個機械人"
}
}
}
}
}
}
28 changes: 26 additions & 2 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@
{
"id": "group-translate",
"name": "Group Auto-Translation",
"version": "1.1.1",
"version": "1.2.0",
"type": "extension",
"status": "stable",
"description": "Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled.",
Expand All @@ -853,7 +853,7 @@
"repoPath": "group-translate",
"repoUrl": "https://github.com/rmyndharis/OpenWA-plugins",
"homepage": "https://github.com/rmyndharis/OpenWA-plugins/tree/main/group-translate",
"download": "https://github.com/rmyndharis/OpenWA-plugins/releases/download/group-translate-v1.1.1/group-translate.zip",
"download": "https://github.com/rmyndharis/OpenWA-plugins/releases/download/group-translate-v1.2.0/group-translate.zip",
"i18n": {
"es": {
"name": "Traducción Automática de Grupos",
Expand All @@ -879,6 +879,9 @@
},
"denyReply": {
"title": "Responder a comandos denegados"
},
"announceInGroups": {
"title": "Anunciar este bot en grupos nuevos"
}
}
},
Expand Down Expand Up @@ -906,6 +909,9 @@
},
"denyReply": {
"title": "Répondre aux commandes refusées"
},
"announceInGroups": {
"title": "Annoncer ce bot dans les nouveaux groupes"
}
}
},
Expand Down Expand Up @@ -933,6 +939,9 @@
},
"denyReply": {
"title": "Rispondi ai comandi negati"
},
"announceInGroups": {
"title": "Annunciare questo bot nei nuovi gruppi"
}
}
},
Expand Down Expand Up @@ -960,6 +969,9 @@
},
"denyReply": {
"title": "الرد على الأوامر المرفوضة"
},
"announceInGroups": {
"title": "الإعلان عن هذا البوت في المجموعات الجديدة"
}
}
},
Expand Down Expand Up @@ -987,6 +999,9 @@
},
"denyReply": {
"title": "השב על פקודות שנדחו"
},
"announceInGroups": {
"title": "להכריז על הבוט בקבוצות חדשות"
}
}
},
Expand Down Expand Up @@ -1014,6 +1029,9 @@
},
"denyReply": {
"title": "తిరస్కరించిన కమాండ్‌లకు జవాబివ్వు"
},
"announceInGroups": {
"title": "కొత్త గ్రూపుల్లో ఈ బాట్‌ను పరిచయం చేయి"
}
}
},
Expand Down Expand Up @@ -1041,6 +1059,9 @@
},
"denyReply": {
"title": "拒绝命令时回复"
},
"announceInGroups": {
"title": "在新群组中介绍此机器人"
}
}
},
Expand Down Expand Up @@ -1068,6 +1089,9 @@
},
"denyReply": {
"title": "拒絕指令時回覆"
},
"announceInGroups": {
"title": "喺新群組介紹呢個機械人"
}
}
}
Expand Down
Loading