mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
feat(telegram): unique chat binding and numeric measurement capture
Two related changes to the Telegram bot's data path. One chat ⇄ one account. `User.telegramChatId` is now UNIQUE (migration 0189, defensive pre-clean of any pre-existing duplicate binding). The inbound webhook resolves the user from the chat id, so a chat shared across two accounts would route a reply or button tap ambiguously. The index is over a nullable column, so an unlinked account (null chat id) never collides — only two accounts binding the same non-null chat id are rejected. Numeric measurement capture. A numeric reply to a measurement reminder is now captured as a Measurement (`source = TELEGRAM`, added to the enum in 0189). On "Erledigt" for a reminder that names a single-value metric, the bot offers an optional value prompt via force_reply; a numeric reply is written through the shared `logTelegramMeasurement` helper — same range guard, canonical unit, cache + rollup refresh as the manual route. The reply binds to the reminder and user through a TelegramPromptContext keyed on the prompt message id (the same strict chat/message binding the mood-note path uses); the userId is never read from the payload. Comma decimals are tolerated; non-numeric and out-of-range replies get a calm hint and write nothing; the reply self-cleans on the ~30-min sweep. Blood pressure (two values) is out of scope for the single-numeric path and keeps its satisfy-only flow.
This commit is contained in:
1 parent
5c0583133d
commit
9c8fb8c7c8
13 files changed
+562
-11
No files matched your search
@@ -0,0 +1,55 @@
|
||||
-- v1.19.2 — Telegram: one chat ⇄ one account, plus a TELEGRAM measurement
|
||||
-- source for numeric-reply capture.
|
||||
--
|
||||
-- 1. `measurement_source` += `TELEGRAM`. A numeric reply to a measurement
|
||||
-- reminder is captured as a Measurement that enters through the
|
||||
-- chat-bound webhook (not the cookie/Bearer client write path), so it
|
||||
-- carries its own source label. Purely-additive enum extension; no row
|
||||
-- touched. `ADD VALUE IF NOT EXISTS` makes the rerun safe. The new value
|
||||
-- is NOT used elsewhere in this migration, so it is safe to extend the
|
||||
-- enum in the same step (Postgres only forbids USING a freshly-added
|
||||
-- value in the same transaction). Mirrors the 0160 source-extension
|
||||
-- precedent.
|
||||
--
|
||||
-- 2. `users.telegram_chat_id` UNIQUE. The inbound webhook resolves the user
|
||||
-- from `telegram_chat_id` (`findTelegramUser`); a chat id shared across
|
||||
-- two accounts would route a reply / button tap / numeric capture
|
||||
-- ambiguously. The index is over a nullable column, so Postgres' default
|
||||
-- NULL-distinct semantics keep an unlinked account (NULL chat id) from
|
||||
-- ever colliding — only two accounts both binding the SAME non-NULL chat
|
||||
-- id are rejected, which is the bug we are closing.
|
||||
--
|
||||
-- Defensive pre-clean: should any duplicate non-NULL chat ids already
|
||||
-- exist (a chat re-pointed between accounts before this constraint
|
||||
-- landed), keep the most recently updated row's binding and NULL out the
|
||||
-- stale ones so the unique index can be built without error. The cleared
|
||||
-- accounts simply fall back to "Telegram not linked" and re-link from
|
||||
-- Settings; no data beyond the binding is touched.
|
||||
--
|
||||
-- Idempotent guards (`IF NOT EXISTS`) make a rerun safe. Forward-only.
|
||||
--
|
||||
-- Reversibility: Postgres cannot remove an enum value, so `TELEGRAM` stays
|
||||
-- (inert with no rows). The unique index drops with
|
||||
-- `DROP INDEX IF EXISTS "users_telegram_chat_id_key"`.
|
||||
|
||||
-- ── 1. measurement_source — append the user-driven webhook source ──────
|
||||
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'TELEGRAM';
|
||||
|
||||
-- ── 2. users.telegram_chat_id — one chat binds one account ─────────────
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY telegram_chat_id
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM "users"
|
||||
WHERE telegram_chat_id IS NOT NULL
|
||||
)
|
||||
UPDATE "users" u
|
||||
SET telegram_chat_id = NULL
|
||||
FROM ranked
|
||||
WHERE u.id = ranked.id AND ranked.rn > 1;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "users_telegram_chat_id_key"
|
||||
ON "users" ("telegram_chat_id");
|
||||
+14
-1
@@ -94,7 +94,12 @@ model User {
|
||||
|
||||
// Telegram notifications (per-user)
|
||||
telegramBotToken String? @map("telegram_bot_token") // AES-256-GCM encrypted
|
||||
telegramChatId String? @map("telegram_chat_id")
|
||||
// v1.19.2 — one Telegram chat binds to exactly one HealthLog account.
|
||||
// The inbound webhook resolves the user from this chat id; a chat shared
|
||||
// across two accounts would route a reply / button tap ambiguously. The
|
||||
// unique is partial by Postgres NULL semantics (multiple NULLs allowed),
|
||||
// so an unlinked account (NULL chat id) never collides. Migration 0189.
|
||||
telegramChatId String? @unique @map("telegram_chat_id")
|
||||
telegramEnabled Boolean @default(false) @map("telegram_enabled")
|
||||
|
||||
// Withings OAuth credentials (per-user, encrypted)
|
||||
@@ -1002,6 +1007,14 @@ enum MeasurementSource {
|
||||
// path); rejected on batch + single POST like WITHINGS/WHOOP/FITBIT. iOS
|
||||
// DTO mirrors this exact spelling so /api/sync/changes decodes the rows.
|
||||
OURA
|
||||
// v1.19.2 — measurements captured from a numeric reply to a Telegram
|
||||
// measurement reminder. Unlike the server-owned integrations above, this
|
||||
// is a user-driven write, but it enters through the webhook (chat-bound)
|
||||
// rather than the cookie/Bearer client write path, so it carries its own
|
||||
// source label. The client batch + single-POST surfaces still reject it
|
||||
// (server-resolved only); iOS DTO mirrors this spelling so the
|
||||
// /api/sync/changes delta feed decodes the rows.
|
||||
TELEGRAM
|
||||
|
||||
@@map("measurement_source")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user