From fbff6b97384d3d3925ff9c283545f1cc4b54d39e Mon Sep 17 00:00:00 2001 From: litruv Date: Tue, 28 Jul 2026 00:18:33 +1000 Subject: [PATCH] Fix UnifiedPush distributor reset and native Matrix gateway discovery. --- .../java/com/paarrot/app/SyncServicePlugin.kt | 31 ++++++++++ .../com/paarrot/app/UnifiedPushManager.kt | 58 +++++++++++++++++++ cinny | 2 +- overlay/src/app/utils/backgroundSync.ts | 11 ++++ 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/paarrot/app/SyncServicePlugin.kt b/android/app/src/main/java/com/paarrot/app/SyncServicePlugin.kt index eb7730e..4037342 100644 --- a/android/app/src/main/java/com/paarrot/app/SyncServicePlugin.kt +++ b/android/app/src/main/java/com/paarrot/app/SyncServicePlugin.kt @@ -17,6 +17,8 @@ import com.getcapacitor.annotation.CapacitorPlugin * - `stop()` — clear persisted credentials and unregister UnifiedPush * - `setAppForeground({ foreground })` — tell the service whether the app UI is visible * - `getStatus()` — returns current fetch and UnifiedPush state + * - `requestDistributorSetup()` — clear saved distributor and re-open OS picker + * - `resolveUnifiedPushGateway({ endpoint })` — native Matrix gateway discovery * - `clearRoomNotifications({ roomId })` — dismiss native tray notifs for a room * - `setNotificationGroups({ rooms })` — persist space/DM grouping for tray nesting * - `showNotification({ title, body, roomId, groupId, groupName, kind, largeIconBase64 })` @@ -131,6 +133,35 @@ class SyncServicePlugin : Plugin() { call.resolve(result) } + /** + * Clears the saved UnifiedPush distributor and opens the OS default-distributor + * picker (`unifiedpush://link`), then re-registers on success. + */ + @PluginMethod + fun requestDistributorSetup(call: PluginCall) { + val currentActivity = activity + ?: return call.reject("Activity required to select a UnifiedPush distributor") + UnifiedPushManager.requestDistributorSetup(context, currentActivity) { success -> + call.resolve(JSObject().put("success", success)) + } + } + + /** + * Resolves the Matrix push gateway for a UnifiedPush endpoint using native HTTP + * so discovery is not blocked by missing CORS headers in the WebView. + */ + @PluginMethod + fun resolveUnifiedPushGateway(call: PluginCall) { + val endpoint = call.getString("endpoint") + ?: return call.reject("endpoint required") + Thread { + val gateway = UnifiedPushManager.resolveMatrixGateway(endpoint) + bridge?.executeOnMainThread { + call.resolve(JSObject().put("gatewayUrl", gateway)) + } + }.start() + } + /** * Cancels native tray notifications for a Matrix room after it has been marked as read. */ diff --git a/android/app/src/main/java/com/paarrot/app/UnifiedPushManager.kt b/android/app/src/main/java/com/paarrot/app/UnifiedPushManager.kt index 07ad961..40670da 100644 --- a/android/app/src/main/java/com/paarrot/app/UnifiedPushManager.kt +++ b/android/app/src/main/java/com/paarrot/app/UnifiedPushManager.kt @@ -4,9 +4,12 @@ import android.app.Activity import android.content.Context import android.util.Log import com.getcapacitor.JSObject +import org.json.JSONObject import org.unifiedpush.android.connector.FailedReason import org.unifiedpush.android.connector.INSTANCE_DEFAULT import org.unifiedpush.android.connector.UnifiedPush +import java.net.HttpURLConnection +import java.net.URL /** Coordinates UnifiedPush registration state and bridges events back to JS. */ object UnifiedPushManager { @@ -18,6 +21,7 @@ object UnifiedPushManager { private const val EVENT_UNREGISTERED = "unifiedPushUnregistered" private const val EVENT_REGISTRATION_FAILED = "unifiedPushRegistrationFailed" private const val DEFAULT_MESSAGE = "Paarrot notifications" + const val DEFAULT_MATRIX_GATEWAY = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" @Volatile private var plugin: SyncServicePlugin? = null @@ -53,6 +57,60 @@ object UnifiedPushManager { } } + /** + * Clears any saved distributor and re-opens the OS/default distributor picker + * (`unifiedpush://link`), then registers again on success. + */ + fun requestDistributorSetup(context: Context, activity: Activity, onDone: (Boolean) -> Unit) { + runCatching { UnifiedPush.removeDistributor(context) } + .onFailure { Log.w(TAG, "removeDistributor failed: ${it.message}") } + clearEndpoint(context) + + UnifiedPush.tryUseDefaultDistributor(activity) { success -> + if (success) { + requestRegistration(context) + } else { + dispatchRegistrationFailed(FailedReason.ACTION_REQUIRED.name, INSTANCE_DEFAULT) + } + onDone(success) + } + } + + /** + * Discovers the Matrix push gateway for a UnifiedPush endpoint via native HTTP + * (WebView `fetch` fails — ntfy responses omit CORS headers). + */ + fun resolveMatrixGateway(endpoint: String): String { + return try { + val endpointUrl = URL(endpoint) + val discoveryUrl = URL( + endpointUrl.protocol, + endpointUrl.host, + endpointUrl.port, + "/_matrix/push/v1/notify", + ) + val conn = discoveryUrl.openConnection() as HttpURLConnection + try { + conn.requestMethod = "GET" + conn.connectTimeout = 4_000 + conn.readTimeout = 6_000 + conn.setRequestProperty("Accept", "application/json") + if (conn.responseCode != 200) return DEFAULT_MATRIX_GATEWAY + + val body = JSONObject(conn.inputStream.bufferedReader().readText()) + val gateway = + body.optString("gateway").takeIf { it.isNotBlank() } + ?: body.optJSONObject("unifiedpush")?.optString("gateway") + if (gateway == "matrix") discoveryUrl.toString() else DEFAULT_MATRIX_GATEWAY + } finally { + conn.disconnect() + } + } catch (e: Exception) { + Log.w(TAG, "Matrix gateway discovery failed: ${e.message}") + DEFAULT_MATRIX_GATEWAY + } + } + fun unregister(context: Context) { runCatching { UnifiedPush.unregister(context, INSTANCE_DEFAULT) diff --git a/cinny b/cinny index 2603ca8..0de2fd9 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit 2603ca8e5c621daaa9e781ceba539e94a2f0e770 +Subproject commit 0de2fd942f62460e016d464529478645ab7766ff diff --git a/overlay/src/app/utils/backgroundSync.ts b/overlay/src/app/utils/backgroundSync.ts index fc50dc3..fb387ba 100644 --- a/overlay/src/app/utils/backgroundSync.ts +++ b/overlay/src/app/utils/backgroundSync.ts @@ -52,6 +52,8 @@ interface MatrixBackgroundSyncPlugin { setAppForeground(options: { foreground: boolean }): Promise; /** Returns fetch state and current UnifiedPush registration details. */ getStatus(): Promise; + /** Native Matrix gateway discovery for a UnifiedPush endpoint (avoids WebView CORS). */ + resolveUnifiedPushGateway(options: { endpoint: string }): Promise<{ gatewayUrl: string }>; /** Cancel native tray notifications posted for a Matrix room. */ clearRoomNotifications(options: { roomId: string }): Promise; /** @@ -170,6 +172,15 @@ const normalizeDistributors = (raw: UnifiedPushStatus['distributors']): string[] }; const resolveUnifiedPushGateway = async (endpoint: string): Promise => { + // Prefer native HTTP — ntfy/Matrix gateway responses omit CORS headers, so + // WebView fetch fails with TypeError: Failed to fetch and wrongly falls back. + try { + const result = await MatrixBackgroundSync.resolveUnifiedPushGateway({ endpoint }); + if (result?.gatewayUrl) return result.gatewayUrl; + } catch (err) { + console.warn('[BackgroundSync] Native UnifiedPush gateway discovery failed:', err); + } + try { const discoveryUrl = new URL(endpoint); discoveryUrl.pathname = '/_matrix/push/v1/notify';