whatsapp-adb-container/README.md
2026-07-22 21:40:40 +02:00

531 lines
16 KiB
Markdown

# WhatsApp ADB Container
Appium 3 + UiAutomator2-driver in Docker, voor geautomatiseerde aansturing van een fysieke Android-telefoon
via ADB. Ontworpen voor de **Nexus 6P** (niet geroot) met WhatsApp.
Twee containers:
| Container | Functie | Poort |
|-----------|---------|-------|
| `appium-adb` | ADB-server + Appium | `:4723` |
| `wa-appium-api` | Express REST API (fallback voor wwebjs-api) | `:3010` |
---
## Architectuur
```
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ appium-adb │ │ wa-appium-api │
│ │ │ │
│ entrypoint.sh: ────────────┤ │ server.js: │
│ adb start-server │ │ Express REST API │
│ adb connect/usb │ │ WebDriverIO → Appium │
│ wifi_sleep_policy=2 │ │ /client/getChats/:ses │
│ svc power stayon usb │ │ /chat/fetchMessages/:ses │
│ dumpsys deviceidle off │ │ /client/sendMessage/:ses │
│ reconnect loop (30s) │ │ │
│ appium server :4723 ─────┼─────┤ localhost:4723 │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
│ USB │ REST (localhost:3010)
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Nexus 6P │ │ Clients │
│ (ENU7N15B…) │ │ (scripts, │
│ WhatsApp │ │ curl, etc) │
└──────────────┘ └──────────────┘
```
**Fallback-strategie:** De primaire WhatsApp-laag is [[Whatsapp rest-api|wwebjs-api]] (`:3001`)
via WhatsApp Web. Als die sessie wegvalt (geblokkeerd nummer, browser-crash), springt
wa-appium-api (`:3010`) in — die de telefoon direct via USB aanstuurt.
---
## Snelstart
### USB-modus (telefoon aan de kabel)
Zet USB debugging aan op de telefoon (Settings > Developer options > USB debugging).
```bash
# check of de telefoon zichtbaar is
adb devices
# → ENU7N15B14000616 device
# start beide containers
docker compose up -d
# smoke test
node smoke-test.js
# health check
curl http://127.0.0.1:3010/health
```
### TCP/IP-modus (telefoon remote)
```bash
# eenmalig na elke reboot (kabel nodig)
adb usb && adb tcpip 5555
# kabel los, start met PHONE_IP
PHONE_IP=192.168.5.100 docker compose up -d
```
---
## Environment variabelen
### appium-adb
| Variabele | Default | Beschrijving |
|------------|---------|-------------|
| `PHONE_IP` | *(leeg)* | IP voor TCP/IP-modus. Leeg = USB-modus |
| `ADB_PORT` | `5555` | ADB-poort (na `adb tcpip <poort>`) |
### wa-appium-api
| Variabele | Default | Beschrijving |
|---------------|-------------|-------------|
| `APPIUM_HOST` | `127.0.0.1` | Appium server host |
| `APPIUM_PORT` | `4723` | Appium server poort |
| `PORT` | `3010` | REST API luisterpoort |
Kopieer `.env.example` naar `.env` om defaults aan te passen.
---
## USB-passthrough
De compose mount `/dev/bus/usb` in de `appium-adb` container. De container draait als root,
dus USB-devices zijn direct toegankelijk. Het ADB-serverproces detecteert USB-hotplug
automatisch.
**Eerste keer op een nieuwe host:**
1. udev-regel aanmaken: `SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0660", GROUP="plugdev"`
2. Gebruiker toevoegen aan `plugdev`-groep
3. Kabel los/koppelen of `chown root:plugdev /dev/bus/usb/XXX/YYY`
4. RSA-vingerafdruk accepteren op de telefoon ("Altijd toestaan")
---
## wa-appium-api (fallback)
REST API die wwebjs-api-endpoints spiegelt, maar via Appium de Android-telefoon aanstuurt
in plaats van WhatsApp Web. Trager en layout-gevoelig, maar werkt ook als de Web-sessie
geblokkeerd is.
### Endpoints
#### `GET /health`
```bash
curl http://127.0.0.1:3010/health
{"status":"ok","sessionId":"..."}
```
#### `GET /client/getChats/:session`
Haalt alle chats op van het WhatsApp-hoofdscherm.
```bash
curl http://127.0.0.1:3010/client/getChats/EdGpt
{
"chats": [
{
"name": "+31 6 12345678",
"lastMessage": "Foto",
"timestamp": "17:28",
"unread": 0
},
...
]
}
```
| Veld | Type | Beschrijving |
|------|------|-------------|
| `name` | string | Contactnaam of telefoonnummer |
| `lastMessage` | string | Laatste bericht preview (leeg bij media) |
| `timestamp` | string | Tijdstip laatste bericht |
| `unread` | int | Aantal ongelezen berichten |
**Limitaties:** alleen zichtbare chats op het scherm. Scrollen wordt nog niet ondersteund.
Media-berichten tonen lege preview of `NaN`.
#### `POST /chat/fetchMessages/:session`
Haalt berichten op uit een specifieke chat.
```bash
curl -X POST http://127.0.0.1:3010/chat/fetchMessages/EdGpt \
-H 'Content-Type: application/json' \
-d '{"chatId": "31612345678"}'
{
"messages": [
{
"text": "Hallo!",
"timestamp": "10:00"
},
...
]
}
```
| Parameter | Type | Verplicht | Beschrijving |
|-----------|------|-----------|-------------|
| `chatId` | string | ja | Telefoonnummer (met of zonder `@c.us`) of contactnaam |
**Limitaties:** alleen tekstberichten worden opgehaald (geen media, voice, of system
messages). Alleen zichtbare berichten in de weergave (scrollt niet).
#### `POST /client/sendMessage/:session`
Verstuurt een tekstbericht.
```bash
curl -X POST http://127.0.0.1:3010/client/sendMessage/EdGpt \
-H 'Content-Type: application/json' \
-d '{
"chatId": "31612345678",
"content": "Hallo vanuit de API!"
}'
{"status":"sent","content":"Hallo vanuit de API!"}
```
| Parameter | Type | Verplicht | Beschrijving |
|-----------|------|-----------|-------------|
| `chatId` | string | ja | Telefoonnummer (zonder `@c.us`) of contactnaam |
| `content` | string | ja | Berichttekst |
**Werkt met:** individuele chats. Groepsberichten nog niet getest.
### Hoe het werkt (intern)
De API gebruikt WebDriverIO om via Appium de WhatsApp Android-app aan te sturen:
```
getChats: startActivity → lees contact_row_container elementen → parse naam/bericht/tijd/ongelezen
openChat: startActivity → klik search_bar_inner_layout → type in search_input → klik contact_row_container
sendMessage: openChat → setValue in entry → klik send → goHome
fetchMessages: openChat → lees main_layout → message_text + date
```
**Gebruikte resource-IDs** (WhatsApp v2.25+, kunnen wijzigen per versie):
| Resource-ID | Functie |
|-------------|---------|
| `com.whatsapp:id/search_bar_inner_layout` | Zoekbalk op home |
| `com.whatsapp:id/search_input` | Zoekveld |
| `com.whatsapp:id/contact_row_container` | Chatrij in lijst |
| `com.whatsapp:id/conversations_row_contact_name` | Contactnaam |
| `com.whatsapp:id/single_msg_tv` | Bericht preview (individueel) |
| `com.whatsapp:id/msg_from_tv` | Bericht preview (community/groep) |
| `com.whatsapp:id/conversations_row_date` | Tijdstip |
| `com.whatsapp:id/conversations_row_unread_indicator` | Ongelezen indicator |
| `com.whatsapp:id/entry` | Tekstinvoerveld |
| `com.whatsapp:id/send` | Verzendknop |
| `com.whatsapp:id/main_layout` | Berichtcontainer in chat |
| `com.whatsapp:id/message_text` | Berichttekst |
| `com.whatsapp:id/date` | Datum/tijd van bericht |
---
## Containers in detail
### appium-adb
**Image build:**
- Base: `node:lts-bookworm-slim`
- OpenJDK 17 + Android platform-tools (Google) + Appium 3.x + uiautomator2-driver
- Zie `Dockerfile`
**Entrypoint (`entrypoint.sh`):**
1. `adb start-server`
2. Detecteer USB (`adb -d`) of TCP (`adb connect`)
3. Pas instellingen toe (non-root): `wifi_sleep_policy=2`, `svc power stayon usb`, `deviceidle disable`
4. Start reconnect loop op achtergrond (elke 30s check of device nog zichtbaar is)
5. `exec appium --address 0.0.0.0 --port 4723 --allow-cors --relaxed-security`
### wa-appium-api
**Image build:**
- Base: `node:lts-bookworm-slim`
- Express + WebDriverIO
- Zie `wa-appium-api/Dockerfile`
**Belangrijke code in `wa-appium-api/server.js`:**
- `getDriver()` — maakt Appium-sessie, herconnect bij drop
- `ensureWhatsApp(d)` — start WhatsApp als het niet op de voorgrond staat
- `goHome(d)` — navigeer naar WhatsApp home
- `openChat(d, query)` — zoek chat via search, open 'm
- Finders gebruiken UiAutomator2 `new UiSelector()` via WebDriverIO
---
## Troubleshooting
### "Could not find a connected Android device"
```
docker logs appium-adb — check of adb connect/usb lukt
```
- Zit de USB-kabel er goed in? Check met `adb devices` op de **host**.
- USB debugging aan op de telefoon? (Developer options)
- Nieuwe RSA-key? Kabel los/koppelen en "Altijd toestaan" op telefoon.
- Voor TCP/IP: `adb connect <IP>:5555` testen vanaf host.
### ADB-serverversie-mismatch
ADB in de container (Google platform-tools) en op de host kunnen verschillen.
Meestal geen probleem. Bij vreemd gedrag:
```bash
# host ADB updaten
sudo -A dnf install android-tools # Fedora
# of gebruik ADB uit container
docker exec appium-adb adb devices
```
### Appium vindt uiautomator2-driver niet
```
driver uiautomator2 is not installed
```
```bash
docker exec appium-adb appium driver install uiautomator2
# of rebuild
docker compose build --no-cache appium
```
### Telefoon niet meer bereikbaar na reboot
Na een reboot moet USB debugging opnieuw bevestigd worden (RSA-vingerafdruk):
```bash
adb devices
# zo niet: kabel los/koppelen, bevestig op telefoon
# voor TCP/IP: adb usb && adb tcpip 5555
```
### Verbinding valt weg na verloop van tijd
De entrypoint past toe:
```
settings put global wifi_sleep_policy 2
svc power stayon usb
dumpsys deviceidle disable
```
Bij USB-verbinding zou dit zelden moeten gebeuren. Check:
1. `docker logs appium-adb` — zie je reconnect-pogingen?
2. **Kabel:** probeer een andere. Goedkope kabels geven drops.
3. **USB-poort:** direct op moederbord, niet via hub.
4. **Stay awake:** zet aan in Developer options op de telefoon.
5. **Settings opnieuw:** `docker restart appium-adb`.
6. **Laatste redmiddel:** rooten + `iw wlan0 set power_save off` (alleen TCP/IP).
### wa-appium-api fault
```bash
docker logs wa-appium-api
# als server.js is aangepast, rebuild nodig:
docker compose build wa-appium-api
docker compose up -d wa-appium-api
```
### wa-appium-api: session lost
De `getDriver()` functie herconnect automatisch bij een verbroken sessie.
Als dat niet werkt:
```bash
docker restart wa-appium-api
```
---
## Android Notification Bridge (event-driven)
Dit systeem maakt **real-time** notificatie van binnenkomende WhatsApp-berichten
mogelijk zonder te pollen via Appium. Het bestaat uit twee componenten:
```
WhatsApp → Android Notification → listener-app (APK) → HTTP POST → wa-appium-api → subscribers
```
### Architectuur
1. **Android app** (`android-notification-listener/`) luistert naar WhatsApp-notificaties
2. Bij elk bericht stuurt de app een HTTP POST naar de API-container
3. De API valideert het gedeelde secret en stuurt het bericht door naar subscribers
4. Subscribers registreren zich via een apart endpoint
### Android-app: bouwen en installeren
**Builden** (vereist Android SDK lokaal of op een build-server):
```bash
cd android-notification-listener
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk
```
Als `gradlew` niet bestaat, genereer het eerst:
```bash
gradle wrapper --gradle-version 8.5
```
**Notificatietoegang verlenen** (eenmalig via ADB):
```bash
adb shell settings put secure enabled_notification_listeners \
com.eddydevink.whatsapplistener/com.eddydevink.whatsapplistener.WhatsAppNotificationListener
```
Verifiëren:
```bash
adb shell settings get secure enabled_notification_listeners
# moet de package/service naam bevatten
```
**Configureren:** open de app op de telefoon ("WA Notification Bridge"),
vul het webhook-URL en shared secret in.
### Webhook configuratie (API-zijde)
Zet in `.env` of als environment variable:
```bash
WEBHOOK_SHARED_SECRET=verander-mij # moet gelijk zijn aan de Android-app
```
### Webhook endpoints
#### `POST /webhooks/whatsapp/incoming`
Ontvangt berichten van de Android notification-listener.
```bash
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": "Hallo!",
"chatId": "+31 6 12345678",
"timestamp": 1700000000000
}'
```
| Header | Waarde |
|--------|--------|
| `X-Webhook-Secret` | Gedeeld geheim (moet matchen met `WEBHOOK_SHARED_SECRET`) |
| Veld | Type | Verplicht | Beschrijving |
|------|------|-----------|-------------|
| `sender` | string | ja | Naam of nummer van afzender |
| `text` | string | ja | Berichttekst |
| `chatId` | string | ja | Chat-naam zoals getoond in notificatie |
| `timestamp` | long | nee | Unix timestamp (ms) |
#### `POST /webhooks/subscribe`
Registreer een subscriber (interne service die notificaties wil ontvangen).
```bash
curl -X POST http://127.0.0.1:3010/webhooks/subscribe \
-H 'Content-Type: application/json' \
-d '{"url": "http://192.168.5.252:3003/webhook"}'
```
#### `GET /webhooks/subscribers`
Lijst van actieve subscribers.
```bash
curl http://127.0.0.1:3010/webhooks/subscribers
```
### Beperkingen
- **Samengevoegde notificaties:** als meerdere berichten in dezelfde chat
binnenkomen voordat de eerste is verwerkt, kan Android één samengevoegde
notificatie sturen. De listener verwerkt dan alle `messages` uit de
`MessagingStyle`, dus individuele berichten gaan niet verloren.
- **Alleen push-notificaties:** berichten die binnenkomen terwijl de app
open is (dus geen notificatie genereren) worden niet gevangen. Gebruik
dan Appium-polling als aanvulling.
- **Eén telefoon, één listener:** de app luistert naar alle chats.
---
## Onderhoud
### Rebuild en restart
```bash
# alles
docker compose build --no-cache
docker compose up -d
# alleen Appium (bv. na driver update)
docker compose build --no-cache appium
docker compose up -d appium
# alleen API (bv. na code wijziging)
docker compose build wa-appium-api
docker compose up -d wa-appium-api
```
### Logs
```bash
docker logs appium-adb -f
docker logs wa-appium-api -f
```
### Phone settings resetten
```bash
docker exec appium-adb adb shell settings put global wifi_sleep_policy 2
docker exec appium-adb adb shell svc power stayon usb
docker exec appium-adb adb shell dumpsys deviceidle disable
```
---
## Bestanden
```
├── Dockerfile # Image: Node LTS + Java 17 + adb + Appium
├── docker-compose.yml # Service-definities (appium + wa-appium-api)
├── entrypoint.sh # USB/TCP-detectie + settings + reconnect-loop
├── wa-appium-api/
│ ├── Dockerfile # Image: Node LTS + Express + WebDriverIO
│ ├── package.json # Dependencies
│ └── server.js # Express REST API
├── smoke-test.js # Node.js smoke-test (geen dependencies)
├── .env.example # Voorbeeld env-variabelen
├── android-notification-listener/ # Android-app: notificaties → webhook
│ ├── app/src/main/java/.../ # Kotlin broncode
│ │ ├── SettingsActivity.kt
│ │ ├── WhatsAppNotificationListener.kt
│ │ └── WebhookClient.kt
│ ├── build.gradle.kts
│ └── settings.gradle.kts
└── README.md # Dit bestand
```
## Zie ook
- [[Whatsapp rest-api]] — primaire WhatsApp API (wwebjs-api, `:3001`)
- [[Android Telefoon]] — Nexus 6P met Termux
- [[Whatsapp Scripts Migratie]] — overzicht WhatsApp automatisering