feat: media send/receive (intent-based + notification bridge)

- POST /messages/send-media: multipart upload + adb push + intent + Appium
- Webhook mediaType: image detection + adb pull + fanout met download-url
- Download endpoint /download/:token (10 min TTL, cleanup every 5 min)
- Android NotificationListener: detecteert media-notificaties (lege/emoji text)
- Dockerfile: adb toegevoegd voor push/pull
- MEDIA_WAIT_MS env var (default 8000ms) voor pull-timing
This commit is contained in:
Eddy de Vink 2026-07-22 21:47:59 +02:00
commit dc121f71de

View file

@ -235,6 +235,88 @@ fetchMessages: openChat → lees main_layout → message_text + date
---
## Media-ondersteuning (foto's)
### Verzenden: `POST /messages/send-media`
Verstuurt een foto via WhatsApp. Gebruikt een ADB-intent om het deelscherm
te openen, daarna Appium voor de contactkeuze en versturen.
```bash
curl -X POST http://127.0.0.1:3010/messages/send-media \
-F 'chatName=+31 6 12345678' \
-F 'caption=Optioneel bijschrift' \
-F 'image=@/pad/naar/foto.jpg'
```
| Veld | Type | Verplicht | Beschrijving |
|------|------|-----------|-------------|
| `image` | file | ja | JPEG/PNG/WEBP, max 20MB |
| `chatName` | string | ja | Contactnaam of telefoonnummer |
| `caption` | string | nee | Optioneel bijschrift bij de foto |
**Flow:**
1. Bestand wordt geüpload naar de API-container
2. `adb push` naar `/sdcard/Pictures/` op de telefoon
3. `adb shell am start` met `ACTION.SEND` intent naar WhatsApp
4. Appium: contact zoeken en selecteren in het deelscherm
5. Caption typen (indien opgegeven)
6. Send-knop klikken
7. Tijdelijke bestanden opruimen (telefoon + container)
**Let op:** de contact picker in het deelscherm gebruikt mogelijk andere
resource-IDs dan de normale WhatsApp-zoekbalk. Bij wijzigingen in
WhatsApp kan dit endpoint breken.
### Ontvangen (notification bridge → API)
De Android notification-listener detecteert media-notificaties door te
checken of de berichttekst leeg is of alleen emoji bevat. Bij een media-
notificatie stuurt de app `mediaType: "image"` mee in de webhook-payload.
De API verwerkt dit als volgt:
1. Webhook binnen met `mediaType: "image"`
2. API wacht `MEDIA_WAIT_MS` ms (default 8s) zodat WhatsApp de download
kan voltooien
3. `adb shell ls -t` in `WhatsApp Images/` — zoekt het nieuwste bestand
4. `adb pull` van dat bestand naar de container
5. Opslaan met een uniek token, beschikbaar via `/download/:token`
6. Fanout naar subscribers met `mediaUrl` + `mediaFilename` in de payload
```bash
# test: verstuur een kunstmatig media-event
curl -X POST http://127.0.0.1:3010/webhooks/whatsapp/incoming \
-H 'Content-Type: application/json' \
-H 'X-Webhook-Secret: verander-mij' \
-d '{
"sender":"+31 6 12345678",
"text":"",
"chatId":"Test Chat",
"mediaType":"image",
"timestamp": 1700000000000
}'
```
### Bekende beperkingen (media)
- **Race condition:** als kort na elkaar meerdere foto's in verschillende
chats binnenkomen, kan de "nieuwste bestand"-detectie de verkeerde
foto pakken. Mitigatie: verleng `MEDIA_WAIT_MS` of implementeer
bestandsgrootte/hash-vergelijking.
- **Alleen WhatsApp Images:** de huidige implementatie kijkt alleen in
`/sdcard/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/`.
Video's, spraakberichten en andere media worden niet ondersteund.
- **Android-versie-specifiek:** de paden naar de WhatsApp-media-map
kunnen wijzigen per Android-versie. Geverifieerd op Nexus 6P met
Android 8.
- **Alleen push-notificaties:** media die binnenkomt terwijl de app open
is (geen notificatie) wordt niet opgevangen.
- **Verouderde bestanden:** bij een trage WhatsApp-download kan het
gebeuren dat het bestand nog niet op de telefoon staat wanneer de API
het zoekt. Verleng `MEDIA_WAIT_MS` in dat geval.
---
## Containers in detail
### appium-adb