From 5b7bc236483c538f3d7a459a9b5b8b9ab352ea67 Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Wed, 22 Jul 2026 20:57:57 +0200 Subject: [PATCH] feat: notification bridge + webhook system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New: android-notification-listener/ — Kotlin app vangt WhatsApp-notificaties - New: POST /webhooks/whatsapp/incoming — ontvangt berichten van de Android-app - New: POST /webhooks/subscribe + GET /webhooks/subscribers — fan-out - Updated: docker-compose.yml met WEBHOOK_SHARED_SECRET - Updated: README met volledige documentatie --- .env.example | 4 + README.md | 122 ++++++++++++++++++ android-notification-listener/.gitignore | 11 ++ .../app/build.gradle.kts | 39 ++++++ .../app/src/main/AndroidManifest.xml | 33 +++++ .../whatsapplistener/SettingsActivity.kt | 62 +++++++++ .../whatsapplistener/WebhookClient.kt | 65 ++++++++++ .../WhatsAppNotificationListener.kt | 54 ++++++++ .../src/main/res/layout/activity_settings.xml | 76 +++++++++++ .../app/src/main/res/values/strings.xml | 4 + .../build.gradle.kts | 4 + .../gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../settings.gradle.kts | 18 +++ docker-compose.yml | 1 + wa-appium-api/server.js | 54 ++++++++ 16 files changed, 556 insertions(+) create mode 100644 android-notification-listener/.gitignore create mode 100644 android-notification-listener/app/build.gradle.kts create mode 100644 android-notification-listener/app/src/main/AndroidManifest.xml create mode 100644 android-notification-listener/app/src/main/java/com/eddydevink/whatsapplistener/SettingsActivity.kt create mode 100644 android-notification-listener/app/src/main/java/com/eddydevink/whatsapplistener/WebhookClient.kt create mode 100644 android-notification-listener/app/src/main/java/com/eddydevink/whatsapplistener/WhatsAppNotificationListener.kt create mode 100644 android-notification-listener/app/src/main/res/layout/activity_settings.xml create mode 100644 android-notification-listener/app/src/main/res/values/strings.xml create mode 100644 android-notification-listener/build.gradle.kts create mode 100644 android-notification-listener/gradle.properties create mode 100644 android-notification-listener/gradle/wrapper/gradle-wrapper.properties create mode 100644 android-notification-listener/settings.gradle.kts diff --git a/.env.example b/.env.example index d8e6b81..5a96266 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,7 @@ # TCP/IP-modus: zet PHONE_IP op het IP-adres van de telefoon # PHONE_IP=192.168.5.100 # ADB_PORT=5555 + +# Webhook shared secret (Android notification bridge → API) +# Moet overeenkomen met de instelling in de Android-app +# WEBHOOK_SHARED_SECRET=verander-mij diff --git a/README.md b/README.md index 61e58e0..b07bd73 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,121 @@ 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": "Eddy de Vink", + "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 @@ -399,6 +514,13 @@ docker exec appium-adb adb shell dumpsys deviceidle disable │ └── 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 ``` diff --git a/android-notification-listener/.gitignore b/android-notification-listener/.gitignore new file mode 100644 index 0000000..e3bdbed --- /dev/null +++ b/android-notification-listener/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/app/build diff --git a/android-notification-listener/app/build.gradle.kts b/android-notification-listener/app/build.gradle.kts new file mode 100644 index 0000000..db5c124 --- /dev/null +++ b/android-notification-listener/app/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.eddydevink.whatsapplistener" + compileSdk = 34 + + defaultConfig { + applicationId = "com.eddydevink.whatsapplistener" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("com.squareup.okhttp3:okhttp:4.12.0") +} diff --git a/android-notification-listener/app/src/main/AndroidManifest.xml b/android-notification-listener/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6b12b71 --- /dev/null +++ b/android-notification-listener/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-notification-listener/app/src/main/java/com/eddydevink/whatsapplistener/SettingsActivity.kt b/android-notification-listener/app/src/main/java/com/eddydevink/whatsapplistener/SettingsActivity.kt new file mode 100644 index 0000000..878dc7f --- /dev/null +++ b/android-notification-listener/app/src/main/java/com/eddydevink/whatsapplistener/SettingsActivity.kt @@ -0,0 +1,62 @@ +package com.eddydevink.whatsapplistener + +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import android.service.notification.NotificationListenerService +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager + +class SettingsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + + val webhookUrlInput = findViewById(R.id.webhookUrl) + val secretInput = findViewById(R.id.sharedSecret) + val saveButton = findViewById