# CHANNEL_SETUP.md — connecting shared channels

Update uses **one shared line per channel type** (one WhatsApp number, one Telegram bot, one SMS
shortcode, one inbound email address) for the whole platform. Organizations never configure their
own — the **routing engine** decides which event each message belongs to from the `#EVENTCODE`
token on the first line.

This guide covers what *you* (the platform operator) configure with each provider to point it at
Update's webhooks. Store secrets either in `.env` or, preferably, per-channel via the portal's
**Platform → Channels** screen (they land in `channels.config_json` and take precedence over
`.env`).

Your webhook base URL is:

```
https://YOUR-DOMAIN/update/api/webhook/{whatsapp|telegram|sms|email}
```

Providers require **public HTTPS**. For local XAMPP testing, expose it with a tunnel
(e.g. `ngrok http 80`) and use the tunnel URL.

Create each channel row first (portal → Platform → Channels → **+ Add channel**), then configure
the provider below.

---

## 1. WhatsApp Business — Meta Cloud API

**You'll need:** a Meta app with the WhatsApp product, a phone number id, a permanent access
token, an **App Secret**, and a **Verify Token** (any string you choose).

**Channel config keys** (`config_json`): `app_secret`, `verify_token`, `phone_number_id`,
`access_token`. (Env fallbacks: `WHATSAPP_APP_SECRET`, `WHATSAPP_VERIFY_TOKEN`,
`WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_ACCESS_TOKEN`.)

**Steps**
1. In the Meta App dashboard → WhatsApp → **Configuration**, set the **Callback URL** to
   `https://YOUR-DOMAIN/update/api/webhook/whatsapp` and the **Verify Token** to the same value
   you saved in the channel config.
2. Meta sends a `GET` with `hub.mode/hub.verify_token/hub.challenge`; Update's
   `GET /webhook/whatsapp` echoes the challenge when the token matches.
3. **Subscribe** to the `messages` field.
4. Inbound `POST`s are signed with `X-Hub-Signature-256: sha256=<HMAC_SHA256(app_secret, body)>`.
   Update verifies this — if `app_secret` is unset, verification is skipped (test only; set it in
   production).
5. **Auto-replies** (e.g. the "use #EVENTCODE" help text for unmatched messages) are sent back via
   the Cloud API using `access_token` + `phone_number_id`. If those are unset, replies are skipped
   and logged — ingestion still works.

**Test:** from a WhatsApp client, message the number:
```
#NGR2027
Doors are open, crowd is building.
```

---

## 2. Telegram Bot API

**You'll need:** a bot token from **@BotFather**, and a **webhook secret** (any string).

**Channel config keys:** `bot_token`, `webhook_secret`. (Env fallbacks: `TELEGRAM_BOT_TOKEN`,
`TELEGRAM_WEBHOOK_SECRET`.)

**Steps**
1. Create the bot with @BotFather, note the token.
2. Register the webhook (Telegram echoes your secret back on every call in a header):
   ```bash
   curl "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
     -d "url=https://YOUR-DOMAIN/update/api/webhook/telegram" \
     -d "secret_token=<YOUR_WEBHOOK_SECRET>"
   ```
3. Update verifies `X-Telegram-Bot-Api-Secret-Token` on every inbound update. If `webhook_secret`
   is unset, verification is skipped (test only).
4. Auto-replies use `sendMessage` with `bot_token`.

**Test:** message the bot:
```
#NGR2027 When does registration open?
```
(That single-line form is supported as well as the multiline form.)

---

## 3. SMS — placeholder (Termii / Africa's Talking)

SMS providers differ; this is the integration point to adapt.

**Channel config keys:** `webhook_secret` (optional shared secret). (Env fallbacks: `SMS_PROVIDER`,
`SMS_API_KEY`, `SMS_WEBHOOK_SECRET`.)

**Steps**
1. Provision an inbound shortcode/keyword with your provider (e.g. Termii or Africa's Talking).
2. Set the provider's **inbound/delivery webhook** to
   `https://YOUR-DOMAIN/update/api/webhook/sms`.
3. **Field mapping** — edit [`src/Webhooks/SmsWebhook.php`](src/Webhooks/SmsWebhook.php) to match
   your provider's payload. It already tries common names:
   - sender: `from` | `msisdn` | `sender` | `source`
   - text: `text` | `message` | `body` | `content`
   - id: `id` | `messageId` | `message_id`
4. Most SMS providers don't HMAC-sign inbound hooks. If `webhook_secret` is set, Update expects it
   as `?secret=` or the `X-SMS-Secret` header; otherwise use the provider's IP allowlist.
5. **Outbound auto-replies** are left as an integration point (outbound SMS is billable and
   provider-specific) — wire your provider's send API where noted in the handler.

**Test:** send an inbound SMS `#NGR2027 crowd building at gate 2`, or simulate:
```bash
curl -X POST "https://YOUR-DOMAIN/update/api/webhook/sms" \
  -d "from=2348010000000" -d "text=#NGR2027 test from sms" -d "id=abc123"
```

---

## 4. Inbound email — placeholder (Mailgun / SendGrid inbound parse)

**Channel config keys:** `signing_key` (Mailgun HTTP webhook signing key). (Env fallbacks:
`EMAIL_PROVIDER`, `EMAIL_WEBHOOK_SIGNING_KEY`.)

**Steps**
1. Point an inbound address (e.g. `updates@inbound.YOUR-DOMAIN`) at your provider's inbound parse.
2. **Mailgun:** Receiving → Routes → *forward* to
   `https://YOUR-DOMAIN/update/api/webhook/email`. **SendGrid:** Inbound Parse → set the POST URL.
3. **Field mapping** — [`src/Webhooks/EmailWebhook.php`](src/Webhooks/EmailWebhook.php) reads
   common fields (`sender`/`from`, `subject`, `stripped-text`/`body-plain`/`text`, `Message-Id`).
   Adjust for your provider if needed.
4. **Routing for email:** the `#EVENTCODE` token is looked for in the **body's first line**, then
   the **subject** as a fallback — so senders can put the code in either place.
5. Mailgun signs webhooks (`timestamp` + `token` + `signature`); Update verifies against
   `signing_key` when present.

**Test:** email the inbox with subject `#NGR2027` and body `Registration is now open.`

---

## Inbound media & location

WhatsApp and Telegram messages carrying **photos, video, audio, documents, or a shared location**
are ingested automatically:

- **Media** is re-hosted into the platform's own storage so the public feed serves stable URLs.
  This requires credentials so we can download the provider's temporary media: WhatsApp needs
  `access_token`; Telegram needs `bot_token` (the same tokens used for replies). Without them, the
  update is still recorded with its `media_type`, just without a hosted URL.
- **Location** messages are stored as a geotag (`latitude`/`longitude`) and surface as a map link
  in the feed/widget. No credentials needed.
- The caption of a media message is where the `#EVENTCODE` token goes (a media message with no
  caption has no routing token and will be treated as unmatched unless a fallback event is set).

## Contributor channel login (email + OTP)

If `REQUIRE_CHANNEL_LOGIN=true` in `.env`, contributors must log in before they can post. The whole
flow happens over the channel; the one-time code is delivered to their **email** (out-of-band):

1. First message from a new sender → the platform replies asking for their **email**.
2. They reply an email. Known account → a 6-digit code is emailed. Unknown → they're asked for a
   name (registration), then the code is emailed.
3. They reply the **code** → verified → a **30-day session** starts on that channel.
4. `#EVENTCODE …` messages then post as normal. `LOGOUT` ends the session; `RESEND` re-sends a code.

Because the account is keyed by email, the **same person is recognized across channels** — logging
in with the same email on WhatsApp and later on Telegram links both to one account.

**You must configure two things for this to work:**

- **Outbound email** for the OTP — set `MAIL_PROVIDER` to `mailgun` or `sendgrid` and provide
  `MAIL_API_KEY` (+ `MAIL_DOMAIN` for Mailgun) and `MAIL_FROM`. For local testing use
  `MAIL_PROVIDER=log`, which writes the code to the `webhook_log` table instead of emailing it.
- **Channel replies** so the platform can prompt the user through the flow — WhatsApp/Telegram use
  their send credentials (same as auto-replies); SMS/email replies are an integration point.

Signed-in staff posting through the web/app (`POST /api/updates`) are never gated. Set
`REQUIRE_CHANNEL_LOGIN=false` to keep posting fully open (the event code alone is the gate).
Moderators can also grant/end a session for a contributor from the portal (Contributors page).

## Fallback event (catch-all for no/unknown hashtag)

To mimic the "WhatsApp Live" behavior — messages with no or an unknown `#code` still get posted —
set a fallback event on the channel. In **Platform → Channels → Edit**, add to the JSON config:

```json
{ "fallback_event_id": 12 }
```

The referenced event must be **live** and must accept that channel (`accepted_channels`). Unmatched
inbound on that channel then routes to the fallback event instead of the triage queue.

## Verifying end-to-end

1. Create a **live** event and note its code (portal → Events).
2. Send a message on any connected channel with `#CODE` on the first line.
3. It appears in the org's **Moderation** queue as `pending`.
4. Messages with no/invalid code land in **Platform → Unmatched** for triage.
5. Watch `webhook_log` (DB table) for the routing decision of every inbound message.

> **Idempotency:** providers retry webhooks. Update dedupes on the provider message id, so retries
> don't create duplicate updates — no action needed on your side.
