531 lines
16 KiB
Markdown
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
|