mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(telegram): answer someone who asks for help in their own language
The bot's real commands are slash commands, and `telegram.helpBody` documents exactly those in all six bundles, so nothing a user is TOLD to type depended on their language. The undocumented courtesies did: the bare word "help" and "hilfe" opened the help text and five greetings were answered, everything else fell through to the intake keyword, failed it, and returned without sending anything. Someone typing "aide" or "pomoc" got silence, which is the worst possible answer to a request for help. Both lists now cover the six shipped languages and are compared against the folded message, so "cześć" and "czesc" are one entry and a capital cannot miss. The match stays anchored to the whole message: "help me log my weight" is prose the later paths should still see. These are ordinary words rather than app labels, so there is nothing in the bundles to derive them from.
This commit is contained in:
1 parent
4d77fe2a43
commit
2d26a6fdfe
2 files changed
+123
-7
No files matched your search
@@ -546,6 +546,67 @@ describe("Telegram webhook — text-message dispatch", () => {
|
||||
expect(prisma.medicationIntakeEvent.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/**
|
||||
* The bare word is an alias for `/help`. It knew "help" and "hilfe", so the
|
||||
* four other shipped languages asking for help fell through to the intake
|
||||
* keyword, failed it, and the handler returned without sending anything —
|
||||
* silence, not an error. These pin one spelling per shipped language.
|
||||
*/
|
||||
it.each([
|
||||
["help", 210],
|
||||
["Hilfe", 211],
|
||||
["aide", 212],
|
||||
["ayuda", 213],
|
||||
["aiuto", 214],
|
||||
["Pomoc", 215],
|
||||
])("answers the bare help word %s", async (word, updateId) => {
|
||||
const res = await POST(
|
||||
tgRequest({
|
||||
update_id: updateId,
|
||||
message: { message_id: 20, text: word, chat: { id: 7777 } },
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(sendTelegramMessage).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(sendTelegramMessage).mock.calls[0][2]).toMatch(
|
||||
/available commands/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not swallow a sentence that merely starts with a help word", async () => {
|
||||
// The alias matches the WHOLE message; "help me log my weight" is prose
|
||||
// the later paths should still see, not a command.
|
||||
const res = await POST(
|
||||
tgRequest({
|
||||
update_id: 216,
|
||||
message: {
|
||||
message_id: 21,
|
||||
text: "help me log my weight",
|
||||
chat: { id: 7777 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(sendTelegramMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["bonjour", 220],
|
||||
["Hola", 221],
|
||||
["ciao", 222],
|
||||
["cześć", 223],
|
||||
])("answers the greeting %s", async (word, updateId) => {
|
||||
const res = await POST(
|
||||
tgRequest({
|
||||
update_id: updateId,
|
||||
message: { message_id: 22, text: word, chat: { id: 7777 } },
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(sendTelegramMessage).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(sendTelegramMessage).mock.calls[0][2]).toMatch(/👋/);
|
||||
});
|
||||
|
||||
it("greeting like 'hi' echoes a friendly reply", async () => {
|
||||
const res = await POST(
|
||||
tgRequest({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/lib/telegram";
|
||||
import { getServerTranslator } from "@/lib/i18n/server-translator";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
import { foldForMatch } from "@/lib/i18n/fold-for-match";
|
||||
import { recomputeMedicationComplianceForEvent } from "@/lib/rollups/medication-compliance-rollups";
|
||||
import {
|
||||
applyCanonicalSlotWrite,
|
||||
@@ -43,6 +44,60 @@ import {
|
||||
* from the linked chat via `findTelegramUser` — never from the payload.
|
||||
*/
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────────
|
||||
* Bare-word aliases.
|
||||
*
|
||||
* The bot's real commands are slash commands (`/help`, `/start`, `/add`) plus
|
||||
* the `taken <name>` text command, and `telegram.helpBody` documents exactly
|
||||
* those in all six bundles — none of that depends on the reader's language.
|
||||
* What did depend on it were the two undocumented courtesies below, which knew
|
||||
* English and German: "aide", "ayuda", "aiuto" and "pomoc" reached the intake
|
||||
* keyword instead, failed it, and the handler returned without replying at all.
|
||||
* Silence is the worst answer to someone asking for help.
|
||||
*
|
||||
* These are ordinary words, not app labels, so there is nothing in the bundles
|
||||
* to derive them from. They are matched on the FOLDED text so "cześć" and
|
||||
* "czesc" are one entry and a stray capital or accent cannot miss.
|
||||
* ────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** "Help", in every language the app ships. */
|
||||
const HELP_WORDS: readonly string[] = [
|
||||
"help",
|
||||
"hilfe",
|
||||
"aide",
|
||||
"ayuda",
|
||||
"aiuto",
|
||||
"pomoc",
|
||||
];
|
||||
|
||||
/** Greetings the bot answers with a wave, in every language the app ships. */
|
||||
const GREETING_WORDS: readonly string[] = [
|
||||
"hi",
|
||||
"hello",
|
||||
"hey",
|
||||
"hallo",
|
||||
"moin",
|
||||
"bonjour",
|
||||
"salut",
|
||||
"hola",
|
||||
"buenas",
|
||||
"ciao",
|
||||
"salve",
|
||||
"czesc",
|
||||
"witaj",
|
||||
"dzien dobry",
|
||||
];
|
||||
|
||||
/**
|
||||
* True when the whole message is one of `words`. Folded on both sides, so the
|
||||
* comparison is case-, accent- and punctuation-insensitive; anchored to the
|
||||
* whole message, so "help me log my weight" is not swallowed as a command.
|
||||
*/
|
||||
function isBareCommandWord(text: string, words: readonly string[]): boolean {
|
||||
const folded = foldForMatch(text);
|
||||
return folded !== "" && words.includes(folded);
|
||||
}
|
||||
|
||||
export interface TelegramUpdate {
|
||||
update_id: number;
|
||||
message?: {
|
||||
@@ -1106,12 +1161,15 @@ export async function handleTextMessage(update: TelegramUpdate) {
|
||||
}
|
||||
}
|
||||
|
||||
// "Help" / start: accept English + German keyword aliases independent of locale
|
||||
// Help / start. The slash commands are locale-independent by construction and
|
||||
// are what `telegram.helpBody` documents in every bundle. The BARE word is an
|
||||
// undocumented convenience that used to know English and German only, so a
|
||||
// French user typing "aide" got silence — the handler falls through to the
|
||||
// intake keyword and returns without a reply.
|
||||
if (
|
||||
/^\/help\b/i.test(text) ||
|
||||
/^\/start\b/i.test(text) ||
|
||||
/^hilfe$/i.test(text) ||
|
||||
/^help$/i.test(text)
|
||||
isBareCommandWord(text, HELP_WORDS)
|
||||
) {
|
||||
const userMsgId = message?.message_id;
|
||||
const resp = await sendTelegramMessage(
|
||||
@@ -1212,10 +1270,7 @@ export async function handleTextMessage(update: TelegramUpdate) {
|
||||
}
|
||||
|
||||
// Greeting responses (kept locale-independent)
|
||||
const greetings = ["hi", "hallo", "hey", "moin", "hello"];
|
||||
const lowerText = text.toLowerCase();
|
||||
const matchedGreeting = greetings.find((g) => lowerText === g);
|
||||
if (matchedGreeting) {
|
||||
if (isBareCommandWord(text, GREETING_WORDS)) {
|
||||
const userMsgId = message?.message_id;
|
||||
const reply = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
const resp = await sendTelegramMessage(botToken, chatId, `${reply}! 👋`);
|
||||
|
||||
Reference in new issue
Block a user