# mars-bot

CLI for sending markdown text to Mars Telegram chats via Bot API. Used together with the `/send-summary` slash command in Claude to deliver meeting summaries to the right team chat.

Design: `docs/{vibe-coding} {plan} mars-bot mvp дизайн – 2026-06-05.md`
Implementation plan: `docs/{vibe-coding} {plan} mars-bot mvp имплементация – 2026-06-05.md`

## Architecture

- Local CLI on Mac, no long-running process
- Markdown → Telegram-compatible HTML conversion (custom, no external markdown lib)
- Recursive markdown-only chunking: 3500-char markdown target by default; if the resulting HTML still exceeds 4000 chars (e.g. dense `&`/links), recurse on the **source markdown** with halved target and re-convert. **HTML strings are never split** — invariant that prevents tearing `&amp;`, `<a href="...">`, `<b>...</b>` at chunk boundaries
- File-backed rate limit via `data/sent.log` — counts **Telegram messages, not CLI calls**: a long summary split into N chunks counts as N (limit 20/hour). One log line per successfully sent chunk; partial failures preserve what was sent
- Alias whitelist via `chats.json` — CLI refuses unknown aliases without calling the API

## Local setup

```bash
cd projects/vibe-coding/mars-bot
python3.11 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
```

## Configuration

Two files, both gitignored:

`.env`:
```
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
```

`chats.json`:
```json
{
  "team": -100123456789,
  "marketing": -100987654321,
  "ops": -100111222333,
  "test": 12345678
}
```

`test` — your personal DM with the bot. Use it for smoke tests.

## First-time setup

1. Create the bot via @BotFather: `/newbot` → name → username → copy the token.
2. `cp .env.example .env` and paste the token.
3. Add the bot as a member to each Mars chat (you need admin rights to add bots).
4. In each chat, after adding the bot, send at least one message (in groups: mention the bot or `/start@<botname>`) — otherwise the chat will not appear in `getUpdates`.
5. In your DM with the bot, send `/start` so the `test` alias becomes discoverable.
6. Run: `.venv/bin/python scripts/get_chat_ids.py`
7. Copy chat IDs from the output into `chats.json` under aliases of your choice (`cp chats.json.example chats.json` first).
8. Smoke-test: `.venv/bin/python scripts/mars-bot send --to test --text "hey"` — should arrive in your DM.

## Setup troubleshooting

- **`getUpdates` returns 409 Conflict or an empty list.** A webhook may be set. Clear it:
  ```
  curl https://api.telegram.org/bot<TOKEN>/deleteWebhook
  ```
  Then re-run `get_chat_ids.py`.
- **The `test` DM does not appear.** Send `/start` to the bot in your private chat, then re-run.
- **A group chat does not appear.** Two checks: (a) mention the bot or use `/start@<botname>` in the group; (b) if that doesn't help — @BotFather → `/mybots` → bot → `Bot Settings` → `Group Privacy` → `Turn off`. Then re-trigger a message in the group and re-run.
- **`send` returns 403 Forbidden at runtime.** The bot was removed from the chat or its right to write was revoked. Add the bot back.

## Usage

**Note:** Invoke via `.venv/bin/python scripts/mars-bot ...` rather than the console-script `.venv/bin/mars-bot`. The vault lives under iCloud-synced Desktop, which silently flags `.venv/**/*.pth` files as hidden, causing Python to skip the editable-install pointer and the console-script to fail with `ModuleNotFoundError`. The `scripts/mars-bot` wrapper injects `src/` into sys.path before importing, bypassing the broken `.pth` mechanism. Same CLI, same exit codes.

```bash
# Inline text
.venv/bin/python scripts/mars-bot send --to team --text "**hello** team"

# From file
.venv/bin/python scripts/mars-bot send --to team --text-file /tmp/summary.md

# From stdin (no --text or --text-file)
echo "**hello**" | .venv/bin/python scripts/mars-bot send --to team

# Preview without sending
.venv/bin/python scripts/mars-bot send --to team --text-file /tmp/summary.md --dry-run
```

Exit codes: 0 success, 2 config error, 3 unknown alias, 4 empty input, 5 rate limit, 6 Telegram error, 7 chunking error (markdown couldn't be split to fit TG limit even at the conservative fallback — should be unreachable in practice; if you ever see this, the input is pathological).

## Emergency stop

If the token leaks, the bot is misbehaving, or you need to immediately stop ALL sends:

1. Open @BotFather in Telegram
2. `/mybots` → choose `mars-bot` → `API Token` → `Revoke current token`
3. The old token is dead. No code (including this CLI and anyone else's) can send as the bot.
4. When resolved, generate a new token via @BotFather and update `.env`. The bot itself remains a member of all chats; only the token rotates.

## Tests

```bash
.venv/bin/pytest -v
```

All tests are offline — Telegram Bot API is mocked. Real send only happens via the smoke-test on step 8 of setup or by manually running `send` with a configured chat.

## Files

```
src/mars_bot/
  config.py          # .env + chats.json loader
  format.py          # md → HTML, two-pass chunking
  telegram.py        # Bot API sendMessage wrapper
  sent_log.py        # append + rate-limit counter
  cli.py             # argparse + cmd_send orchestration
scripts/
  get_chat_ids.py    # one-shot getUpdates discovery
data/
  sent.log           # append-only log + rate-limit source (gitignored)
```

## Mentions watcher

Periodically polls TGStat `channels/mentions` for mentions of Mars channels and sends an alert card to the configured chat alias.

### Configuration

Add to `.env`:
```
TGSTAT_TOKEN=<your token from api.tgstat.ru>
```

Add `_mentions` block to `chats.json`:
```json
{
  "test": 822794,
  "backoffice": -1001227818099,
  ...,
  "_mentions": {
    "alert_to": "test",
    "tracked": {
      "marsingru": "backoffice",
      "choooooooir": "backoffice",
      "tvorcheskiye_lyudi": "backoffice",
      "natashhhh": "test"
    }
  }
}
```

Each `tracked` key is a channel username (without `@`); each value is an alias from `chats.json` where alerts go.

`alert_to` (optional) is the alias for **failure alerts to the owner** (config errors, all TGStat calls failing). Use a private/test chat, never a work chat. If omitted, falls back to the `test` alias; if neither exists, failures are only logged.

### Local smoke

```bash
.venv/bin/python scripts/mars-bot-mentions check --dry-run
```

Prints rendered cards to stdout. No DB write, no TG send.

### Deploy on Pi

systemd unit templates: `docs/systemd/mars-bot-mentions.service`, `docs/systemd/mars-bot-mentions.timer`.

```bash
sudo cp docs/systemd/mars-bot-mentions.* /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mars-bot-mentions.timer
sudo systemctl status mars-bot-mentions.timer
journalctl -u mars-bot-mentions -f
```

Timer fires 5 min after boot, then every 4 hours.

Optional: enable `docs/systemd/mars-bot-mentions-fail.service` as an `OnFailure=` target for a fallback owner alert when the run fails so early the bot can't notify itself (e.g. broken `.env`/`chats.json`). See the comments in both unit files.

### Notes

- **Cold start**: first run with no `data/mentions.db` silently absorbs all current mentions without sending alerts. Completion is recorded via a `cold_start_done` marker in the `meta` table — if the cold run crashes before finishing, the next run stays cold (no history spam) instead of blasting old mentions as fresh alerts.
- **Multi-channel dedup**: dedup key is the composite `(post_id, mars_channel)`. A post mentioning several of our channels fires a separate alert per channel, each to that channel's destination.
- **Failure handling**: if TGStat fails for **all** tracked channels, the run exits non-zero (systemd sees it as failed) and sends an alert to `alert_to`/`test`. Partial failures (some channels OK) still succeed.
- **Retries**: failed sends keep `alerted=0` in DB; next run re-tries via pre-pass.
- **Schema migration**: on start, an old single-column-PK `mentions.db` is auto-migrated to the composite key, preserving `alerted` flags (already-sent mentions are not re-sent). Idempotent.
- **Token safety**: TGStat/Telegram tokens are masked in error messages before they reach logs/journald.
