004_telegram.up.sql 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. -- 004_telegram.up.sql
  2. -- M3 Telegram delivery + bot commands. See SPEC §8.
  3. --
  4. -- What lands in M3:
  5. -- telegram_bots — one row per (company, bot). For M3 we
  6. -- support one bot per company; the table
  7. -- keys on (company_id, bot_id) so adding
  8. -- more later is a one-line change.
  9. -- individuals: — adds telegram_chat_id, telegram_user_id,
  10. -- telegram_invite_code (set on individual
  11. -- creation by the admin; admin hands the
  12. -- code to the user out-of-band), and
  13. -- mute_until (per-individual global mute
  14. -- for the /mute command).
  15. --
  16. -- What stays out of M3:
  17. -- Bot token encryption at rest. M3 stores the token in
  18. -- plaintext in telegram_bots.bot_token with a dev-only
  19. -- annotation. AES-256-GCM is a security milestone (§11).
  20. -- Real Telegram API. M3 ships faketgmd and a long-poll
  21. -- bot loop. Webhook mode is M5/M9.
  22. -- Retry + DLQ for failed sends. The single-attempt pattern
  23. -- from M1's fcm path is repeated. M9 adds the retry chain.
  24. --
  25. -- Naming: snake_case to match the rest of the schema.
  26. -- ── telegram_bots ────────────────────────────────────────────────
  27. CREATE TABLE IF NOT EXISTS telegram_bots (
  28. bot_id TEXT NOT NULL, -- short id; e.g. "primary"
  29. company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
  30. name TEXT NOT NULL, -- human label; e.g. "Acme Ops"
  31. bot_token TEXT NOT NULL, -- DEV ONLY; encrypt in M11 (security milestone)
  32. status TEXT NOT NULL DEFAULT 'active', -- active | paused
  33. last_seen_at TIMESTAMPTZ, -- last getUpdates / webhook hit
  34. created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  35. PRIMARY KEY (company_id, bot_id)
  36. );
  37. CREATE INDEX IF NOT EXISTS idx_telegram_bots_company ON telegram_bots(company_id) WHERE status = 'active';
  38. -- ── extend individuals ───────────────────────────────────────────
  39. -- All four columns are nullable: an individual that has not yet
  40. -- linked their Telegram account has no chat_id / user_id. The
  41. -- invite_code is set when the admin creates the individual.
  42. ALTER TABLE individuals
  43. ADD COLUMN IF NOT EXISTS telegram_chat_id TEXT,
  44. ADD COLUMN IF NOT EXISTS telegram_user_id BIGINT,
  45. ADD COLUMN IF NOT EXISTS telegram_invite_code TEXT,
  46. ADD COLUMN IF NOT EXISTS mute_until TIMESTAMPTZ;
  47. -- Unique index for fast "/start <code>" lookup. Partial index
  48. -- so historical / legacy individuals without a code don't bloat
  49. -- the index.
  50. CREATE UNIQUE INDEX IF NOT EXISTS idx_individuals_invite_code
  51. ON individuals(telegram_invite_code)
  52. WHERE telegram_invite_code IS NOT NULL;
  53. -- Unique index for fast "is this telegram_user_id already linked?"
  54. -- check. Partial: most individuals won't be linked.
  55. CREATE UNIQUE INDEX IF NOT EXISTS idx_individuals_telegram_user
  56. ON individuals(company_id, telegram_user_id)
  57. WHERE telegram_user_id IS NOT NULL;